Compare commits
439 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ecb022abc | ||
|
|
12ab3972de | ||
|
|
a1db5f0f0a | ||
|
|
00d45c34ba | ||
|
|
44610621ab | ||
|
|
088aa94fa1 | ||
|
|
c34b814c16 | ||
|
|
881dc02349 | ||
|
|
b27ad76557 | ||
|
|
40d059c7c1 | ||
|
|
52b42dfc14 | ||
|
|
dcfc768d33 | ||
|
|
ccdbad9f90 | ||
|
|
f59e43320b | ||
|
|
ec337d3465 | ||
|
|
59950290a2 | ||
|
|
361d7f1ba5 | ||
|
|
80adafc207 | ||
|
|
641b2a4705 | ||
|
|
e287376cc8 | ||
|
|
21dfb68763 | ||
|
|
1c28ed4d5b | ||
|
|
10059309da | ||
|
|
0ee91a2e54 | ||
|
|
fea906b883 | ||
|
|
128391be0a | ||
|
|
2febb2965e | ||
|
|
e07557c9c5 | ||
|
|
7d12674430 | ||
|
|
4342b891eb | ||
|
|
2d94ea074c | ||
|
|
4dbeabc8ef | ||
|
|
7711b13766 | ||
|
|
3d6ba6f21f | ||
|
|
9abc71f9c8 | ||
|
|
52ee6dcaed | ||
|
|
5de9b6ac75 | ||
|
|
14a1a18243 | ||
|
|
3762259d3d | ||
|
|
0053d57ec4 | ||
|
|
c24d9675e8 | ||
|
|
e90b60b5cb | ||
|
|
5d2ac5b3e9 | ||
|
|
72fc0836e1 | ||
|
|
ea8c57263b | ||
|
|
83a5284ba9 | ||
|
|
af0679076b | ||
|
|
e9c6c5a8eb | ||
|
|
4e215c6b7b | ||
|
|
04ca25e199 | ||
|
|
205e7afb9d | ||
|
|
3b166ea249 | ||
|
|
694187b853 | ||
|
|
91fe25356e | ||
|
|
2e35065070 | ||
|
|
cca7f17b19 | ||
|
|
4591ba063c | ||
|
|
5ba9fbcbdb | ||
|
|
23d4b01137 | ||
|
|
a93a9122bb | ||
|
|
ac99d8543d | ||
|
|
5e9f31cf21 | ||
|
|
4e3c885808 | ||
|
|
711bc3ff1c | ||
|
|
c72f61ef2f | ||
|
|
174567350e | ||
|
|
dcb7e27a5e | ||
|
|
959d9c5fda | ||
|
|
955ffce775 | ||
|
|
6f3a70d35f | ||
|
|
0ec1f01b00 | ||
|
|
807def0f04 | ||
|
|
87e30a2981 | ||
|
|
0e1c7e8788 | ||
|
|
21454e4dfb | ||
|
|
18700c26f9 | ||
|
|
67bac41df4 | ||
|
|
9beaf65d19 | ||
|
|
f51481983d | ||
|
|
b10d3c4da2 | ||
|
|
3b179bad89 | ||
|
|
29e3a400f9 | ||
|
|
3141a1702c | ||
|
|
22689958da | ||
|
|
41d7c03cbb | ||
|
|
7828eee014 | ||
|
|
9b1e36ebf0 | ||
|
|
960ef14b7a | ||
|
|
b288c73437 | ||
|
|
9eeedd8515 | ||
|
|
0f58a3918a | ||
|
|
501c88ca3a | ||
|
|
b22784878f | ||
|
|
d8f627a9d7 | ||
|
|
711a6eaaaf | ||
|
|
9834e3ab7f | ||
|
|
a331eadf29 | ||
|
|
bf0cfae9c9 | ||
|
|
522cfd0c9a | ||
|
|
9bae5e37cd | ||
|
|
9fbb4f676b | ||
|
|
0f927f99a4 | ||
|
|
6bf997ee96 | ||
|
|
b8bcae9252 | ||
|
|
5d978c2729 | ||
|
|
9f4663b4d0 | ||
|
|
8cc5b99986 | ||
|
|
7a0e313895 | ||
|
|
722abf1b6b | ||
|
|
566d4fa76e | ||
|
|
4ef4466e67 | ||
|
|
6cefe45e5b | ||
|
|
d4a74923c1 | ||
|
|
e9b87fdd42 | ||
|
|
7f790ca908 | ||
|
|
2373dd9b95 | ||
|
|
67df5ae0ec | ||
|
|
7c0d46409f | ||
|
|
4ad423b1b9 | ||
|
|
3668de7cb7 | ||
|
|
d9826e3dfc | ||
|
|
f3eb559a71 | ||
|
|
4f13f4779a | ||
|
|
3566a8adc6 | ||
|
|
7fdc8f7c38 | ||
|
|
e71f33425c | ||
|
|
6ddd08d0b0 | ||
|
|
e41d23d26c | ||
|
|
f229282754 | ||
|
|
77fd0436a8 | ||
|
|
16a5475c0c | ||
|
|
8247fd1727 | ||
|
|
159d633a62 | ||
|
|
99a57e689d | ||
|
|
97bf4a829a | ||
|
|
350d53dbe1 | ||
|
|
1276855f40 | ||
|
|
edd143b084 | ||
|
|
80c2a33fd7 | ||
|
|
25e0a808cd | ||
|
|
0cd94b017f | ||
|
|
854d4a9c79 | ||
|
|
5bf7c799e2 | ||
|
|
4f48ede7a0 | ||
|
|
0532b1fd94 | ||
|
|
4b78eb7e76 | ||
|
|
5b62f2050b | ||
|
|
e6d6172b4c | ||
|
|
91f689575e | ||
|
|
dc0c85a227 | ||
|
|
5c5a0c965d | ||
|
|
162b6bf50b | ||
|
|
3e7abf5fe5 | ||
|
|
7131e7836d | ||
|
|
806a802daa | ||
|
|
9f12c964dc | ||
|
|
f78df8dccb | ||
|
|
955078abb0 | ||
|
|
405719cd3d | ||
|
|
52c4157723 | ||
|
|
5ea11af36b | ||
|
|
e8497b2d80 | ||
|
|
d3044e635e | ||
|
|
ca6caa86bc | ||
|
|
152921b82a | ||
|
|
ecc317d690 | ||
|
|
8016b0aee7 | ||
|
|
79043894b2 | ||
|
|
6607e6b2c3 | ||
|
|
87ce9d834f | ||
|
|
204aea856d | ||
|
|
2b1b51b5bb | ||
|
|
d8c5f15bc0 | ||
|
|
5117494a52 | ||
|
|
0cee954f84 | ||
|
|
3971ff5f72 | ||
|
|
f32de045e3 | ||
|
|
2185a29555 | ||
|
|
d12f89cdb2 | ||
|
|
0561d0a81c | ||
|
|
ed4c6ef1a7 | ||
|
|
ec91148672 | ||
|
|
458d82aff2 | ||
|
|
390b4986f9 | ||
|
|
6ecafe7606 | ||
|
|
04da610288 | ||
|
|
b397535392 | ||
|
|
d2a6509950 | ||
|
|
33b6244ae6 | ||
|
|
8ab26cd76d | ||
|
|
3a380245c6 | ||
|
|
8cde784383 | ||
|
|
8ef0a66d3e | ||
|
|
2dd697ee77 | ||
|
|
a8ac196cde | ||
|
|
bd7467a812 | ||
|
|
babcc420cc | ||
|
|
71704beb6f | ||
|
|
0b4b0874f4 | ||
|
|
6077cd8160 | ||
|
|
aa4998a3b5 | ||
|
|
2eac2ef69f | ||
|
|
e2196efe1a | ||
|
|
0eb908e950 | ||
|
|
8ca4f63572 | ||
|
|
3c3b08f517 | ||
|
|
0247d5c87f | ||
|
|
ba4d1bc1a4 | ||
|
|
22d352e875 | ||
|
|
63c3f51ffa | ||
|
|
efd40772ee | ||
|
|
fb67648c68 | ||
|
|
3d61086ac0 | ||
|
|
7a21997701 | ||
|
|
f2ecc1d2d5 | ||
|
|
f50a2586b6 | ||
|
|
565bfa16f2 | ||
|
|
fd33437d7a | ||
|
|
125722ae41 | ||
|
|
39571d828e | ||
|
|
1fe80c2d02 | ||
|
|
5fb642831c | ||
|
|
beb8807b2e | ||
|
|
b089eda8bf | ||
|
|
5f125c5e2f | ||
|
|
3761e74436 | ||
|
|
b7525263ff | ||
|
|
a3a6bf6ae1 | ||
|
|
ca3827efea | ||
|
|
d2584dd3e8 | ||
|
|
471f1c835c | ||
|
|
ac4b7f20ca | ||
|
|
41b3a59aae | ||
|
|
c1b9ad2f3a | ||
|
|
70ce49e32c | ||
|
|
d09821e1e1 | ||
|
|
b6ac3e7d08 | ||
|
|
afa930ff48 | ||
|
|
23ae82d282 | ||
|
|
ad8961030e | ||
|
|
68d57c3947 | ||
|
|
9b9892da03 | ||
|
|
79d047802a | ||
|
|
108e80419e | ||
|
|
008894d32b | ||
|
|
634054c07b | ||
|
|
17a087663b | ||
|
|
b04cef9f3f | ||
|
|
f08715972a | ||
|
|
22f3935024 | ||
|
|
0b0b3f0d9d | ||
|
|
0658285a3b | ||
|
|
1753a138e9 | ||
|
|
4e4e6c669d | ||
|
|
a638c9c8af | ||
|
|
a95295b9f9 | ||
|
|
e33c383345 | ||
|
|
996f7d30b9 | ||
|
|
9ec24bcc6c | ||
|
|
7a9291b3d1 | ||
|
|
bfa316ad09 | ||
|
|
d238cdbdd5 | ||
|
|
f25b5bdfab | ||
|
|
c39d02fa14 | ||
|
|
4a1c56113e | ||
|
|
1ed9133bb6 | ||
|
|
9f13b6ed2b | ||
|
|
4636ef0a29 | ||
|
|
fcbe2556d3 | ||
|
|
345d5a657a | ||
|
|
e7b3ef0c90 | ||
|
|
b371369699 | ||
|
|
067f101c99 | ||
|
|
0305aafff8 | ||
|
|
a59dc8b40c | ||
|
|
6b1b2e863a | ||
|
|
415be361b4 | ||
|
|
509d6e3ca9 | ||
|
|
6ff311c4e9 | ||
|
|
348359ad5a | ||
|
|
1a08482655 | ||
|
|
b686e1f972 | ||
|
|
5674ea450d | ||
|
|
f376659785 | ||
|
|
c678134de6 | ||
|
|
a25f6493de | ||
|
|
59ce9ccc8e | ||
|
|
a14eceb41a | ||
|
|
bfccfa4af1 | ||
|
|
0bce5d6057 | ||
|
|
fd566f134d | ||
|
|
40176a6b41 | ||
|
|
9f859c2bd3 | ||
|
|
62987372ba | ||
|
|
80b77ffc87 | ||
|
|
ed64d691fc | ||
|
|
1f235a7ba3 | ||
|
|
ce039f279b | ||
|
|
b6c2dbe581 | ||
|
|
884a18f8f1 | ||
|
|
0393855243 | ||
|
|
0e0b8e5ab4 | ||
|
|
6e6ca42261 | ||
|
|
7344296a39 | ||
|
|
f66cf7d838 | ||
|
|
2b80cb99be | ||
|
|
57e0b4ea5f | ||
|
|
0d95acefa2 | ||
|
|
5b155e37aa | ||
|
|
fd02060cd2 | ||
|
|
f5008a1467 | ||
|
|
c2242406f4 | ||
|
|
df6226f07f | ||
|
|
357a1c9752 | ||
|
|
9e5e244e7a | ||
|
|
3dcd7380ac | ||
|
|
b7dc28fff9 | ||
|
|
fa95bd331a | ||
|
|
4b973b2f86 | ||
|
|
5194b87dc4 | ||
|
|
85aeb1f3b1 | ||
|
|
03a40f491b | ||
|
|
36c6820096 | ||
|
|
9c204b7106 | ||
|
|
8e5d481cba | ||
|
|
37eae8eb4e | ||
|
|
4ede3a9ba5 | ||
|
|
e7d2451f60 | ||
|
|
5bbccf1fbd | ||
|
|
40574f0d72 | ||
|
|
1104deb58e | ||
|
|
29804f6a41 | ||
|
|
ae6260f717 | ||
|
|
aceee829d2 | ||
|
|
02c3303fee | ||
|
|
3f45f63ff8 | ||
|
|
3de53999ce | ||
|
|
b31e43e3db | ||
|
|
d4c2e6c77d | ||
|
|
049c54bb32 | ||
|
|
d35bef12b6 | ||
|
|
90ad3d3176 | ||
|
|
db9cc5350d | ||
|
|
f67c447402 | ||
|
|
cbaee05740 | ||
|
|
c827be4345 | ||
|
|
efebec3f95 | ||
|
|
c3dd68ecc3 | ||
|
|
2bb1bfc4a0 | ||
|
|
195bffdf44 | ||
|
|
17ed1bb12c | ||
|
|
f9321767a6 | ||
|
|
9c5108e492 | ||
|
|
529abd619a | ||
|
|
03fc3cec70 | ||
|
|
501b09a170 | ||
|
|
a95bb27c65 | ||
|
|
07bb8360d5 | ||
|
|
64672e655a | ||
|
|
cf49cec203 | ||
|
|
bc5654b900 | ||
|
|
d287ed5e47 | ||
|
|
f881d23c05 | ||
|
|
5310ec38ca | ||
|
|
1937059870 | ||
|
|
717c78dcda | ||
|
|
1c90797879 | ||
|
|
ba9f9bff19 | ||
|
|
1e16d2ca25 | ||
|
|
745e653339 | ||
|
|
f0e00daa1c | ||
|
|
0bdda1226f | ||
|
|
05436dfcac | ||
|
|
7f1f7809c8 | ||
|
|
44bbf4b983 | ||
|
|
0ddb310dd4 | ||
|
|
dacc69c56f | ||
|
|
7b4ede9c04 | ||
|
|
1a0a70b5cd | ||
|
|
093b95604f | ||
|
|
dd30a46f29 | ||
|
|
72c9cd5582 | ||
|
|
917a21b989 | ||
|
|
69f442dd79 | ||
|
|
a82df6ac7b | ||
|
|
bc78bc33f1 | ||
|
|
1a3ea0c108 | ||
|
|
1ac888715c | ||
|
|
6ecd545f17 | ||
|
|
be730fe66a | ||
|
|
09c1da9570 | ||
|
|
a0bf22c9a5 | ||
|
|
5729ee563c | ||
|
|
ebd967d7d3 | ||
|
|
313350534f | ||
|
|
e3336c603c | ||
|
|
2e38f4d599 | ||
|
|
2512d4c7f2 | ||
|
|
823e377ef1 | ||
|
|
db5cd4eace | ||
|
|
409a65ec64 | ||
|
|
28d3525b42 | ||
|
|
f7ef0d69b7 | ||
|
|
0ca5f98ba3 | ||
|
|
78eb6bd9ea | ||
|
|
74b30a6a1d | ||
|
|
ed4c8461d3 | ||
|
|
61d59b392a | ||
|
|
fba13c6dbc | ||
|
|
3397b80ddf | ||
|
|
d82b1fc600 | ||
|
|
7ea1edc4ff | ||
|
|
1762f9919a | ||
|
|
17fb287cd9 | ||
|
|
8242a9cd46 | ||
|
|
9cdde50f7c | ||
|
|
5957ef9b9d | ||
|
|
144aec8d6a | ||
|
|
3fe45404ea | ||
|
|
bfa78aaf74 | ||
|
|
54b5064f76 | ||
|
|
75b610d7fc | ||
|
|
ba6b8561c9 | ||
|
|
54a8473a91 | ||
|
|
df1351783e | ||
|
|
0bb4a7d13a | ||
|
|
ce2626e911 | ||
|
|
bc53b67065 | ||
|
|
2d7ff5d4cf | ||
|
|
70375dfbde | ||
|
|
1093e0d46f | ||
|
|
d87b98d945 | ||
|
|
752b01d83b | ||
|
|
a0f78584e9 | ||
|
|
4b44ae435a | ||
|
|
1c68946fb9 | ||
|
|
824d6b4102 | ||
|
|
b5f22c39ec | ||
|
|
f83098e493 |
@@ -28,4 +28,7 @@ b147b85e6ac19a9220cd1e2958a6ebd99373283a
|
||||
494bd9ef78313436f0424b918f200dab8fc7c20b
|
||||
|
||||
# bulk format python code with black
|
||||
baec607ff5905b1c67531096a9cf50ec7ff00a5d
|
||||
baec607ff5905b1c67531096a9cf50ec7ff00a5d
|
||||
|
||||
# ruff
|
||||
960ef14b7a68cfec9e309ec12845f521cb6a721c
|
||||
|
||||
@@ -55,28 +55,15 @@ repos:
|
||||
erpnext/templates/includes/.*
|
||||
)$
|
||||
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [
|
||||
'flake8-bugbear',
|
||||
'flake8-tuple',
|
||||
]
|
||||
args: ['--config', '.github/helper/.flake8_strict']
|
||||
exclude: ".*setup.py$"
|
||||
- id: ruff
|
||||
name: "Run ruff linter and apply fixes"
|
||||
args: ["--fix"]
|
||||
|
||||
- repo: https://github.com/adityahase/black
|
||||
rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
|
||||
hooks:
|
||||
- id: black
|
||||
additional_dependencies: ['click==8.0.4']
|
||||
|
||||
- repo: https://github.com/PyCQA/isort
|
||||
rev: 5.12.0
|
||||
hooks:
|
||||
- id: isort
|
||||
exclude: ".*setup.py$"
|
||||
- id: ruff-format
|
||||
name: "Format Python code"
|
||||
|
||||
|
||||
ci:
|
||||
|
||||
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "15.17.1"
|
||||
__version__ = "15.20.5"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
@@ -36,10 +36,8 @@ def get_default_cost_center(company):
|
||||
|
||||
if not frappe.flags.company_cost_center:
|
||||
frappe.flags.company_cost_center = {}
|
||||
if not company in frappe.flags.company_cost_center:
|
||||
frappe.flags.company_cost_center[company] = frappe.get_cached_value(
|
||||
"Company", company, "cost_center"
|
||||
)
|
||||
if company not in frappe.flags.company_cost_center:
|
||||
frappe.flags.company_cost_center[company] = frappe.get_cached_value("Company", company, "cost_center")
|
||||
return frappe.flags.company_cost_center[company]
|
||||
|
||||
|
||||
@@ -47,7 +45,7 @@ def get_company_currency(company):
|
||||
"""Returns the default company currency"""
|
||||
if not frappe.flags.company_currency:
|
||||
frappe.flags.company_currency = {}
|
||||
if not company in frappe.flags.company_currency:
|
||||
if company not in frappe.flags.company_currency:
|
||||
frappe.flags.company_currency[company] = frappe.db.get_value(
|
||||
"Company", company, "default_currency", cache=True
|
||||
)
|
||||
@@ -81,7 +79,7 @@ def is_perpetual_inventory_enabled(company):
|
||||
if not hasattr(frappe.local, "enable_perpetual_inventory"):
|
||||
frappe.local.enable_perpetual_inventory = {}
|
||||
|
||||
if not company in frappe.local.enable_perpetual_inventory:
|
||||
if company not in frappe.local.enable_perpetual_inventory:
|
||||
frappe.local.enable_perpetual_inventory[company] = (
|
||||
frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0
|
||||
)
|
||||
@@ -96,7 +94,7 @@ def get_default_finance_book(company=None):
|
||||
if not hasattr(frappe.local, "default_finance_book"):
|
||||
frappe.local.default_finance_book = {}
|
||||
|
||||
if not company in frappe.local.default_finance_book:
|
||||
if company not in frappe.local.default_finance_book:
|
||||
frappe.local.default_finance_book[company] = frappe.get_cached_value(
|
||||
"Company", company, "default_finance_book"
|
||||
)
|
||||
@@ -108,7 +106,7 @@ def get_party_account_type(party_type):
|
||||
if not hasattr(frappe.local, "party_account_types"):
|
||||
frappe.local.party_account_types = {}
|
||||
|
||||
if not party_type in frappe.local.party_account_types:
|
||||
if party_type not in frappe.local.party_account_types:
|
||||
frappe.local.party_account_types[party_type] = (
|
||||
frappe.db.get_value("Party Type", party_type, "account_type") or ""
|
||||
)
|
||||
|
||||
@@ -11,14 +11,14 @@ class ERPNextAddress(Address):
|
||||
def validate(self):
|
||||
self.validate_reference()
|
||||
self.update_compnay_address()
|
||||
super(ERPNextAddress, self).validate()
|
||||
super().validate()
|
||||
|
||||
def link_address(self):
|
||||
"""Link address based on owner"""
|
||||
if self.is_your_company_address:
|
||||
return
|
||||
|
||||
return super(ERPNextAddress, self).link_address()
|
||||
return super().link_address()
|
||||
|
||||
def update_compnay_address(self):
|
||||
for link in self.get("links"):
|
||||
@@ -26,11 +26,11 @@ class ERPNextAddress(Address):
|
||||
self.is_your_company_address = 1
|
||||
|
||||
def validate_reference(self):
|
||||
if self.is_your_company_address and not [
|
||||
row for row in self.links if row.link_doctype == "Company"
|
||||
]:
|
||||
if self.is_your_company_address and not [row for row in self.links if row.link_doctype == "Company"]:
|
||||
frappe.throw(
|
||||
_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
|
||||
_(
|
||||
"Address needs to be linked to a Company. Please add a row for Company in the Links table."
|
||||
),
|
||||
title=_("Company Not Linked"),
|
||||
)
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ def get(
|
||||
filters = frappe.parse_json(filters) or frappe.parse_json(chart.filters_json)
|
||||
|
||||
account = filters.get("account")
|
||||
company = filters.get("company")
|
||||
filters.get("company")
|
||||
|
||||
if not account and chart_name:
|
||||
frappe.throw(
|
||||
@@ -83,7 +83,6 @@ def build_result(account, dates, gl_entries):
|
||||
|
||||
# get balances in debit
|
||||
for entry in gl_entries:
|
||||
|
||||
# entry date is after the current pointer, so move the pointer forward
|
||||
while getdate(entry.posting_date) > result[date_index][0]:
|
||||
date_index += 1
|
||||
@@ -133,8 +132,6 @@ def get_dates_from_timegrain(from_date, to_date, timegrain):
|
||||
|
||||
dates = [get_period_ending(from_date, timegrain)]
|
||||
while getdate(dates[-1]) < getdate(to_date):
|
||||
date = get_period_ending(
|
||||
add_to_date(dates[-1], years=years, months=months, days=days), timegrain
|
||||
)
|
||||
date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain)
|
||||
dates.append(date)
|
||||
return dates
|
||||
|
||||
@@ -24,14 +24,10 @@ from erpnext.accounts.utils import get_account_currency
|
||||
def validate_service_stop_date(doc):
|
||||
"""Validates service_stop_date for Purchase Invoice and Sales Invoice"""
|
||||
|
||||
enable_check = (
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
old_stop_dates = {}
|
||||
old_doc = frappe.db.get_all(
|
||||
"{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"]
|
||||
)
|
||||
old_doc = frappe.db.get_all(f"{doc.doctype} Item", {"parent": doc.name}, ["name", "service_stop_date"])
|
||||
|
||||
for d in old_doc:
|
||||
old_stop_dates[d.name] = d.service_stop_date or ""
|
||||
@@ -62,16 +58,14 @@ def build_conditions(process_type, account, company):
|
||||
)
|
||||
|
||||
if account:
|
||||
conditions += "AND %s='%s'" % (deferred_account, account)
|
||||
conditions += f"AND {deferred_account}='{account}'"
|
||||
elif company:
|
||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def convert_deferred_expense_to_expense(
|
||||
deferred_process, start_date=None, end_date=None, conditions=""
|
||||
):
|
||||
def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=""):
|
||||
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
|
||||
|
||||
if not start_date:
|
||||
@@ -81,16 +75,14 @@ def convert_deferred_expense_to_expense(
|
||||
|
||||
# check for the purchase invoice for which GL entries has to be done
|
||||
invoices = frappe.db.sql_list(
|
||||
"""
|
||||
f"""
|
||||
select distinct item.parent
|
||||
from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
|
||||
where item.service_start_date<=%s and item.service_end_date>=%s
|
||||
and item.enable_deferred_expense = 1 and item.parent=p.name
|
||||
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
|
||||
{0}
|
||||
""".format(
|
||||
conditions
|
||||
),
|
||||
{conditions}
|
||||
""",
|
||||
(end_date, start_date),
|
||||
) # nosec
|
||||
|
||||
@@ -103,9 +95,7 @@ def convert_deferred_expense_to_expense(
|
||||
send_mail(deferred_process)
|
||||
|
||||
|
||||
def convert_deferred_revenue_to_income(
|
||||
deferred_process, start_date=None, end_date=None, conditions=""
|
||||
):
|
||||
def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=""):
|
||||
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
|
||||
|
||||
if not start_date:
|
||||
@@ -115,16 +105,14 @@ def convert_deferred_revenue_to_income(
|
||||
|
||||
# check for the sales invoice for which GL entries has to be done
|
||||
invoices = frappe.db.sql_list(
|
||||
"""
|
||||
f"""
|
||||
select distinct item.parent
|
||||
from `tabSales Invoice Item` item, `tabSales Invoice` p
|
||||
where item.service_start_date<=%s and item.service_end_date>=%s
|
||||
and item.enable_deferred_revenue = 1 and item.parent=p.name
|
||||
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
|
||||
{0}
|
||||
""".format(
|
||||
conditions
|
||||
),
|
||||
{conditions}
|
||||
""",
|
||||
(end_date, start_date),
|
||||
) # nosec
|
||||
|
||||
@@ -243,9 +231,7 @@ def calculate_monthly_amount(
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
|
||||
doc, item
|
||||
)
|
||||
base_amount = flt(
|
||||
item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
|
||||
)
|
||||
base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
@@ -265,17 +251,13 @@ def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, a
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
amount = flt(
|
||||
item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")
|
||||
)
|
||||
amount = flt(item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount"))
|
||||
else:
|
||||
already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
|
||||
doc, item
|
||||
)
|
||||
|
||||
base_amount = flt(
|
||||
item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
|
||||
)
|
||||
base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
|
||||
if account_currency == doc.company_currency:
|
||||
amount = base_amount
|
||||
else:
|
||||
@@ -296,26 +278,22 @@ def get_already_booked_amount(doc, item):
|
||||
|
||||
gl_entries_details = frappe.db.sql(
|
||||
"""
|
||||
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
|
||||
select sum({}) as total_credit, sum({}) as total_credit_in_account_currency, voucher_detail_no
|
||||
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
group by voucher_detail_no
|
||||
""".format(
|
||||
total_credit_debit, total_credit_debit_currency
|
||||
),
|
||||
""".format(total_credit_debit, total_credit_debit_currency),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
journal_entry_details = frappe.db.sql(
|
||||
"""
|
||||
SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no
|
||||
SELECT sum(c.{}) as total_credit, sum(c.{}) as total_credit_in_account_currency, reference_detail_no
|
||||
FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and
|
||||
p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s
|
||||
and p.docstatus < 2 group by reference_detail_no
|
||||
""".format(
|
||||
total_credit_debit, total_credit_debit_currency
|
||||
),
|
||||
""".format(total_credit_debit, total_credit_debit_currency),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -337,9 +315,7 @@ def get_already_booked_amount(doc, item):
|
||||
|
||||
|
||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = (
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
@@ -440,9 +416,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
via_journal_entry = cint(
|
||||
frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry")
|
||||
)
|
||||
submit_journal_entry = cint(
|
||||
frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries")
|
||||
)
|
||||
submit_journal_entry = cint(frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries"))
|
||||
book_deferred_entries_based_on = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "book_deferred_entries_based_on"
|
||||
)
|
||||
@@ -462,9 +436,7 @@ def process_deferred_accounting(posting_date=None):
|
||||
posting_date = today()
|
||||
|
||||
if not cint(
|
||||
frappe.db.get_singles_value(
|
||||
"Accounts Settings", "automatically_process_deferred_accounting_entry"
|
||||
)
|
||||
frappe.db.get_singles_value("Accounts Settings", "automatically_process_deferred_accounting_entry")
|
||||
):
|
||||
return
|
||||
|
||||
@@ -587,16 +559,13 @@ def book_revenue_via_journal_entry(
|
||||
deferred_process=None,
|
||||
submit="No",
|
||||
):
|
||||
|
||||
if amount == 0:
|
||||
return
|
||||
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.posting_date = posting_date
|
||||
journal_entry.company = doc.company
|
||||
journal_entry.voucher_type = (
|
||||
"Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
|
||||
)
|
||||
journal_entry.voucher_type = "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
|
||||
journal_entry.process_deferred_accounting = deferred_process
|
||||
|
||||
debit_entry = {
|
||||
@@ -645,7 +614,6 @@ def book_revenue_via_journal_entry(
|
||||
|
||||
|
||||
def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
|
||||
|
||||
if doctype == "Sales Invoice":
|
||||
credit_account, debit_account = frappe.db.get_value(
|
||||
"Sales Invoice Item",
|
||||
|
||||
@@ -88,7 +88,7 @@ class Account(NestedSet):
|
||||
if frappe.local.flags.ignore_update_nsm:
|
||||
return
|
||||
else:
|
||||
super(Account, self).on_update()
|
||||
super().on_update()
|
||||
|
||||
def onload(self):
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
@@ -218,9 +218,7 @@ class Account(NestedSet):
|
||||
|
||||
def validate_root_company_and_sync_account_to_children(self):
|
||||
# ignore validation while creating new compnay or while syncing to child companies
|
||||
if (
|
||||
frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation
|
||||
):
|
||||
if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation:
|
||||
return
|
||||
ancestors = get_root_company(self.company)
|
||||
if ancestors:
|
||||
@@ -418,7 +416,7 @@ class Account(NestedSet):
|
||||
if self.check_gle_exists():
|
||||
throw(_("Account with existing transaction can not be deleted"))
|
||||
|
||||
super(Account, self).on_trash(True)
|
||||
super().on_trash(True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -426,9 +424,8 @@ class Account(NestedSet):
|
||||
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
"""select name from tabAccount
|
||||
where is_group = 1 and docstatus != 2 and company = %s
|
||||
and %s like %s order by name limit %s offset %s"""
|
||||
% ("%s", searchfield, "%s", "%s", "%s"),
|
||||
where is_group = 1 and docstatus != 2 and company = {}
|
||||
and {} like {} order by name limit {} offset {}""".format("%s", searchfield, "%s", "%s", "%s"),
|
||||
(filters["company"], "%%%s%%" % txt, page_len, start),
|
||||
as_list=1,
|
||||
)
|
||||
@@ -594,7 +591,5 @@ def sync_update_account_number_in_child(
|
||||
if old_acc_number:
|
||||
filters["account_number"] = old_acc_number
|
||||
|
||||
for d in frappe.db.get_values(
|
||||
"Account", filters=filters, fieldname=["company", "name"], as_dict=True
|
||||
):
|
||||
for d in frappe.db.get_values("Account", filters=filters, fieldname=["company", "name"], as_dict=True):
|
||||
update_account_number(d["name"], account_name, account_number, from_descendant=True)
|
||||
|
||||
@@ -31,7 +31,6 @@ def create_charts(
|
||||
"tax_rate",
|
||||
"account_currency",
|
||||
]:
|
||||
|
||||
account_number = cstr(child.get("account_number")).strip()
|
||||
account_name, account_name_in_db = add_suffix_if_duplicate(
|
||||
account_name, account_number, accounts
|
||||
@@ -39,7 +38,9 @@ def create_charts(
|
||||
|
||||
is_group = identify_is_group(child)
|
||||
report_type = (
|
||||
"Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss"
|
||||
"Balance Sheet"
|
||||
if root_type in ["Asset", "Liability", "Equity"]
|
||||
else "Profit and Loss"
|
||||
)
|
||||
|
||||
account = frappe.get_doc(
|
||||
@@ -141,7 +142,7 @@ def get_chart(chart_template, existing_company=None):
|
||||
for fname in os.listdir(path):
|
||||
fname = frappe.as_unicode(fname)
|
||||
if fname.endswith(".json"):
|
||||
with open(os.path.join(path, fname), "r") as f:
|
||||
with open(os.path.join(path, fname)) as f:
|
||||
chart = f.read()
|
||||
if chart and json.loads(chart).get("name") == chart_template:
|
||||
return json.loads(chart).get("tree")
|
||||
@@ -173,7 +174,7 @@ def get_charts_for_country(country, with_standard=False):
|
||||
for fname in os.listdir(path):
|
||||
fname = frappe.as_unicode(fname)
|
||||
if (fname.startswith(country_code) or fname.startswith(country)) and fname.endswith(".json"):
|
||||
with open(os.path.join(path, fname), "r") as f:
|
||||
with open(os.path.join(path, fname)) as f:
|
||||
_get_chart_name(f.read())
|
||||
|
||||
# if more than one charts, returned then add the standard
|
||||
@@ -247,7 +248,13 @@ def validate_bank_account(coa, bank_account):
|
||||
|
||||
def _get_account_names(account_master):
|
||||
for account_name, child in account_master.items():
|
||||
if account_name not in ["account_number", "account_type", "root_type", "is_group", "tax_rate"]:
|
||||
if account_name not in [
|
||||
"account_number",
|
||||
"account_type",
|
||||
"root_type",
|
||||
"is_group",
|
||||
"tax_rate",
|
||||
]:
|
||||
accounts.append(account_name)
|
||||
|
||||
_get_account_names(child)
|
||||
|
||||
@@ -261,28 +261,20 @@ class TestAccount(unittest.TestCase):
|
||||
acc.insert()
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 4"}
|
||||
)
|
||||
frappe.db.exists("Account", {"account_name": "Test Group Account", "company": "_Test Company 4"})
|
||||
)
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
|
||||
)
|
||||
frappe.db.exists("Account", {"account_name": "Test Group Account", "company": "_Test Company 5"})
|
||||
)
|
||||
|
||||
# Try renaming child company account
|
||||
acc_tc_5 = frappe.db.get_value(
|
||||
"Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
|
||||
)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account"
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account")
|
||||
|
||||
# Rename child company account with allow_account_creation_against_child_company enabled
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 1
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1)
|
||||
|
||||
update_account_number(acc_tc_5, "Test Modified Account")
|
||||
self.assertTrue(
|
||||
@@ -291,9 +283,7 @@ class TestAccount(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company 5", "allow_account_creation_against_child_company", 0
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0)
|
||||
|
||||
to_delete = [
|
||||
"Test Group Account - _TC3",
|
||||
@@ -318,9 +308,7 @@ class TestAccount(unittest.TestCase):
|
||||
self.assertEqual(acc.account_currency, "INR")
|
||||
|
||||
# Make a JV against this account
|
||||
make_journal_entry(
|
||||
"Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True
|
||||
)
|
||||
make_journal_entry("Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True)
|
||||
|
||||
acc.account_currency = "USD"
|
||||
self.assertRaises(frappe.ValidationError, acc.save)
|
||||
|
||||
@@ -40,16 +40,12 @@ class AccountClosingBalance(Document):
|
||||
def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
previous_closing_entries = get_previous_closing_entries(
|
||||
company, closing_date, accounting_dimensions
|
||||
)
|
||||
previous_closing_entries = get_previous_closing_entries(company, closing_date, accounting_dimensions)
|
||||
combined_entries = closing_entries + previous_closing_entries
|
||||
|
||||
merged_entries = aggregate_with_last_account_closing_balance(
|
||||
combined_entries, accounting_dimensions
|
||||
)
|
||||
merged_entries = aggregate_with_last_account_closing_balance(combined_entries, accounting_dimensions)
|
||||
|
||||
for key, value in merged_entries.items():
|
||||
for _key, value in merged_entries.items():
|
||||
cle = frappe.new_doc("Account Closing Balance")
|
||||
cle.update(value)
|
||||
cle.update(value["dimensions"])
|
||||
|
||||
@@ -40,7 +40,8 @@ class AccountingDimension(Document):
|
||||
self.set_fieldname_and_label()
|
||||
|
||||
def validate(self):
|
||||
if self.document_type in core_doctypes_list + (
|
||||
if self.document_type in (
|
||||
*core_doctypes_list,
|
||||
"Accounting Dimension",
|
||||
"Project",
|
||||
"Cost Center",
|
||||
@@ -48,13 +49,10 @@ class AccountingDimension(Document):
|
||||
"Company",
|
||||
"Account",
|
||||
):
|
||||
|
||||
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
|
||||
frappe.throw(msg)
|
||||
|
||||
exists = frappe.db.get_value(
|
||||
"Accounting Dimension", {"document_type": self.document_type}, ["name"]
|
||||
)
|
||||
exists = frappe.db.get_value("Accounting Dimension", {"document_type": self.document_type}, ["name"])
|
||||
|
||||
if exists and self.is_new():
|
||||
frappe.throw(_("Document Type already used as a dimension"))
|
||||
@@ -113,7 +111,6 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
|
||||
repostable_doctypes = get_allowed_types_from_settings()
|
||||
|
||||
for doctype in doclist:
|
||||
|
||||
if (doc_count + 1) % 2 == 0:
|
||||
insert_after_field = "dimension_col_break"
|
||||
else:
|
||||
@@ -148,7 +145,7 @@ def add_dimension_to_budget_doctype(df, doc):
|
||||
df.update(
|
||||
{
|
||||
"insert_after": "cost_center",
|
||||
"depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type),
|
||||
"depends_on": f"eval:doc.budget_against == '{doc.document_type}'",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -182,19 +179,17 @@ def delete_accounting_dimension(doc):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
DELETE FROM `tabCustom Field`
|
||||
WHERE fieldname = %s
|
||||
AND dt IN (%s)"""
|
||||
% ("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname] + doclist),
|
||||
WHERE fieldname = {}
|
||||
AND dt IN ({})""".format("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname, *doclist]),
|
||||
)
|
||||
|
||||
frappe.db.sql(
|
||||
"""
|
||||
DELETE FROM `tabProperty Setter`
|
||||
WHERE field_name = %s
|
||||
AND doc_type IN (%s)"""
|
||||
% ("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname] + doclist),
|
||||
WHERE field_name = {}
|
||||
AND doc_type IN ({})""".format("%s", ", ".join(["%s"] * len(doclist))), # nosec
|
||||
tuple([doc.fieldname, *doclist]),
|
||||
)
|
||||
|
||||
budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options")
|
||||
@@ -243,7 +238,6 @@ def get_doctypes_with_dimensions():
|
||||
|
||||
|
||||
def get_accounting_dimensions(as_list=True, filters=None):
|
||||
|
||||
if not filters:
|
||||
filters = {"disabled": 0}
|
||||
|
||||
@@ -272,7 +266,6 @@ def get_checks_for_pl_and_bs_accounts():
|
||||
|
||||
|
||||
def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
if isinstance(dimensions, str):
|
||||
dimensions = [dimensions]
|
||||
|
||||
@@ -280,9 +273,7 @@ def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
for dimension in dimensions:
|
||||
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
|
||||
children = frappe.get_all(
|
||||
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
|
||||
)
|
||||
children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft")
|
||||
all_dimensions += [c.name for c in children]
|
||||
|
||||
return all_dimensions
|
||||
@@ -290,14 +281,10 @@ def get_dimension_with_children(doctype, dimensions):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dimensions(with_cost_center_and_project=False):
|
||||
|
||||
c = frappe.qb.DocType("Accounting Dimension Detail")
|
||||
p = frappe.qb.DocType("Accounting Dimension")
|
||||
dimension_filters = (
|
||||
frappe.qb.from_(p)
|
||||
.select(p.label, p.fieldname, p.document_type)
|
||||
.where(p.disabled == 0)
|
||||
.run(as_dict=1)
|
||||
frappe.qb.from_(p).select(p.label, p.fieldname, p.document_type).where(p.disabled == 0).run(as_dict=1)
|
||||
)
|
||||
default_dimensions = (
|
||||
frappe.qb.from_(c)
|
||||
|
||||
@@ -55,9 +55,7 @@ class TestAccountingDimensionFilter(unittest.TestCase):
|
||||
|
||||
|
||||
def create_accounting_dimension_filter():
|
||||
if not frappe.db.get_value(
|
||||
"Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}
|
||||
):
|
||||
if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension Filter",
|
||||
|
||||
@@ -84,7 +84,10 @@ class AccountingPeriod(Document):
|
||||
for doctype_for_closing in self.get_doctypes_for_closing():
|
||||
self.append(
|
||||
"closed_documents",
|
||||
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
|
||||
{
|
||||
"document_type": doctype_for_closing.document_type,
|
||||
"closed": doctype_for_closing.closed,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@ class TestAccountingPeriod(unittest.TestCase):
|
||||
ap1 = create_accounting_period(period_name="Test Accounting Period 2")
|
||||
ap1.save()
|
||||
|
||||
doc = create_sales_invoice(
|
||||
do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
|
||||
)
|
||||
doc = create_sales_invoice(do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
|
||||
self.assertRaises(ClosedAccountingPeriod, doc.save)
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
@@ -54,6 +54,7 @@ class BankAccount(Document):
|
||||
self.validate_company()
|
||||
self.validate_iban()
|
||||
self.validate_account()
|
||||
self.update_default_bank_account()
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
@@ -100,19 +101,51 @@ class BankAccount(Document):
|
||||
if to_check % 97 != 1:
|
||||
frappe.throw(_("IBAN is not valid"))
|
||||
|
||||
def update_default_bank_account(self):
|
||||
if self.is_default and not self.disabled:
|
||||
frappe.db.set_value(
|
||||
"Bank Account",
|
||||
{
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"is_company_account": self.is_company_account,
|
||||
"is_default": 1,
|
||||
"disabled": 0,
|
||||
},
|
||||
"is_default",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_bank_account(doctype, docname):
|
||||
doc = frappe.new_doc("Bank Account")
|
||||
doc.party_type = doctype
|
||||
doc.party = docname
|
||||
doc.is_default = 1
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def get_party_bank_account(party_type, party):
|
||||
return frappe.db.get_value(party_type, party, "default_bank_account")
|
||||
return frappe.db.get_value(
|
||||
"Bank Account",
|
||||
{"party_type": party_type, "party": party, "is_default": 1, "disabled": 0},
|
||||
"name",
|
||||
)
|
||||
|
||||
|
||||
def get_default_company_bank_account(company, party_type, party):
|
||||
default_company_bank_account = frappe.db.get_value(party_type, party, "default_bank_account")
|
||||
if default_company_bank_account:
|
||||
if company != frappe.get_cached_value("Bank Account", default_company_bank_account, "company"):
|
||||
default_company_bank_account = None
|
||||
|
||||
if not default_company_bank_account:
|
||||
default_company_bank_account = frappe.db.get_value(
|
||||
"Bank Account", {"company": company, "is_company_account": 1, "is_default": 1}
|
||||
)
|
||||
|
||||
return default_company_bank_account
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase):
|
||||
try:
|
||||
bank_account.validate_iban()
|
||||
except ValidationError:
|
||||
msg = "BankAccount.validate_iban() failed for valid IBAN {}".format(iban)
|
||||
msg = f"BankAccount.validate_iban() failed for valid IBAN {iban}"
|
||||
self.fail(msg=msg)
|
||||
|
||||
for not_iban in invalid_ibans:
|
||||
bank_account.iban = not_iban
|
||||
msg = "BankAccount.validate_iban() accepted invalid IBAN {}".format(not_iban)
|
||||
msg = f"BankAccount.validate_iban() accepted invalid IBAN {not_iban}"
|
||||
with self.assertRaises(ValidationError, msg=msg):
|
||||
bank_account.validate_iban()
|
||||
|
||||
@@ -127,7 +127,7 @@ def get_payment_entries_for_bank_clearance(
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select
|
||||
"Journal Entry" as payment_document, t1.name as payment_entry,
|
||||
t1.cheque_no as cheque_number, t1.cheque_date,
|
||||
@@ -141,9 +141,7 @@ def get_payment_entries_for_bank_clearance(
|
||||
and ifnull(t1.is_opening, 'No') = 'No' {condition}
|
||||
group by t2.account, t1.name
|
||||
order by t1.posting_date ASC, t1.name DESC
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -152,7 +150,7 @@ def get_payment_entries_for_bank_clearance(
|
||||
condition += "and bank_account = %(bank_account)s"
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
@@ -167,9 +165,7 @@ def get_payment_entries_for_bank_clearance(
|
||||
{condition}
|
||||
order by
|
||||
posting_date ASC, name DESC
|
||||
""".format(
|
||||
condition=condition
|
||||
),
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
"from": from_date,
|
||||
@@ -239,10 +235,7 @@ def get_payment_entries_for_bank_clearance(
|
||||
).run(as_dict=True)
|
||||
|
||||
entries = (
|
||||
list(payment_entries)
|
||||
+ list(journal_entries)
|
||||
+ list(pos_sales_invoices)
|
||||
+ list(pos_purchase_invoices)
|
||||
list(payment_entries) + list(journal_entries) + list(pos_sales_invoices) + list(pos_purchase_invoices)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
@@ -68,9 +68,7 @@ class TestBankClearance(unittest.TestCase):
|
||||
)
|
||||
loan.submit()
|
||||
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
|
||||
repayment_entry = create_repayment_entry(
|
||||
loan.name, "_Test Customer", getdate(), loan.loan_amount
|
||||
)
|
||||
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
|
||||
repayment_entry.save()
|
||||
repayment_entry.submit()
|
||||
|
||||
|
||||
@@ -81,9 +81,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
|
||||
def get_account_balance(bank_account, till_date):
|
||||
# returns account balance till the specified date
|
||||
account = frappe.db.get_value("Bank Account", bank_account, "account")
|
||||
filters = frappe._dict(
|
||||
{"account": account, "report_date": till_date, "include_pos_transactions": 1}
|
||||
)
|
||||
filters = frappe._dict({"account": account, "report_date": till_date, "include_pos_transactions": 1})
|
||||
data = get_entries(filters)
|
||||
|
||||
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
|
||||
@@ -96,10 +94,7 @@ def get_account_balance(bank_account, till_date):
|
||||
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
|
||||
|
||||
bank_bal = (
|
||||
flt(balance_as_per_system)
|
||||
- flt(total_debit)
|
||||
+ flt(total_credit)
|
||||
+ amounts_not_reflected_in_system
|
||||
flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) + amounts_not_reflected_in_system
|
||||
)
|
||||
|
||||
return bank_bal
|
||||
@@ -538,9 +533,7 @@ def check_matching(
|
||||
for query in queries:
|
||||
matching_vouchers.extend(query.run(as_dict=True))
|
||||
|
||||
return (
|
||||
sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True) if matching_vouchers else []
|
||||
)
|
||||
return sorted(matching_vouchers, key=lambda x: x["rank"], reverse=True) if matching_vouchers else []
|
||||
|
||||
|
||||
def get_queries(
|
||||
@@ -654,17 +647,13 @@ def get_bt_matching_query(exact_match, transaction):
|
||||
amount_rank = frappe.qb.terms.Case().when(amount_equality, 1).else_(0)
|
||||
amount_condition = amount_equality if exact_match else getattr(bt, field) > 0.0
|
||||
|
||||
ref_rank = (
|
||||
frappe.qb.terms.Case().when(bt.reference_number == transaction.reference_number, 1).else_(0)
|
||||
)
|
||||
ref_rank = frappe.qb.terms.Case().when(bt.reference_number == transaction.reference_number, 1).else_(0)
|
||||
unallocated_rank = (
|
||||
frappe.qb.terms.Case().when(bt.unallocated_amount == transaction.unallocated_amount, 1).else_(0)
|
||||
)
|
||||
|
||||
party_condition = (
|
||||
(bt.party_type == transaction.party_type)
|
||||
& (bt.party == transaction.party)
|
||||
& bt.party.isnotnull()
|
||||
(bt.party_type == transaction.party_type) & (bt.party == transaction.party) & bt.party.isnotnull()
|
||||
)
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
@@ -716,9 +705,7 @@ def get_pe_matching_query(
|
||||
amount_condition = amount_equality if exact_match else pe.paid_amount > 0.0
|
||||
|
||||
party_condition = (
|
||||
(pe.party_type == transaction.party_type)
|
||||
& (pe.party == transaction.party)
|
||||
& pe.party.isnotnull()
|
||||
(pe.party_type == transaction.party_type) & (pe.party == transaction.party) & pe.party.isnotnull()
|
||||
)
|
||||
party_rank = frappe.qb.terms.Case().when(party_condition, 1).else_(0)
|
||||
|
||||
@@ -749,7 +736,7 @@ def get_pe_matching_query(
|
||||
.orderby(pe.reference_date if cint(filter_by_reference_date) else pe.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
@@ -810,7 +797,7 @@ def get_je_matching_query(
|
||||
.orderby(je.cheque_date if cint(filter_by_reference_date) else je.posting_date)
|
||||
)
|
||||
|
||||
if frappe.flags.auto_reconcile_vouchers == True:
|
||||
if frappe.flags.auto_reconcile_vouchers is True:
|
||||
query = query.where(ref_condition)
|
||||
|
||||
return query
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, today
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
|
||||
auto_reconcile_vouchers,
|
||||
@@ -22,7 +21,7 @@ class TestBankReconciliationTool(AccountsTestMixin, FrappeTestCase):
|
||||
self.create_customer()
|
||||
self.clear_old_entries()
|
||||
bank_dt = qb.DocType("Bank")
|
||||
q = qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
qb.from_(bank_dt).delete().where(bank_dt.name == "HDFC").run()
|
||||
self.create_bank_account()
|
||||
|
||||
def tearDown(self):
|
||||
|
||||
@@ -45,7 +45,7 @@ class BankStatementImport(DataImport):
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BankStatementImport, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
@@ -54,7 +54,6 @@ class BankStatementImport(DataImport):
|
||||
or (doc_before_save and doc_before_save.import_file != self.import_file)
|
||||
or (doc_before_save and doc_before_save.google_sheets_url != self.google_sheets_url)
|
||||
):
|
||||
|
||||
template_options_dict = {}
|
||||
column_to_field_map = {}
|
||||
bank = frappe.get_doc("Bank", self.bank)
|
||||
@@ -69,7 +68,6 @@ class BankStatementImport(DataImport):
|
||||
self.validate_google_sheets_url()
|
||||
|
||||
def start_import(self):
|
||||
|
||||
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
|
||||
self.import_file, self.google_sheets_url
|
||||
)
|
||||
@@ -125,7 +123,7 @@ def download_errored_template(data_import_name):
|
||||
def parse_data_from_template(raw_data):
|
||||
data = []
|
||||
|
||||
for i, row in enumerate(raw_data):
|
||||
for _i, row in enumerate(raw_data):
|
||||
if all(v in INVALID_VALUES for v in row):
|
||||
# empty row
|
||||
continue
|
||||
@@ -135,9 +133,7 @@ def parse_data_from_template(raw_data):
|
||||
return data
|
||||
|
||||
|
||||
def start_import(
|
||||
data_import, bank_account, import_file_path, google_sheets_url, bank, template_options
|
||||
):
|
||||
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
|
||||
"""This method runs in background job"""
|
||||
|
||||
update_mapping_db(bank, template_options)
|
||||
@@ -148,6 +144,9 @@ def start_import(
|
||||
import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records")
|
||||
|
||||
data = parse_data_from_template(import_file.raw_data)
|
||||
# Importer expects 'Data Import' class, which has 'payload_count' attribute
|
||||
if not data_import.get("payload_count"):
|
||||
data_import.payload_count = len(data) - 1
|
||||
|
||||
if import_file_path:
|
||||
add_bank_account(data, bank_account)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
from typing import Tuple, Union
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
from rapidfuzz import fuzz, process
|
||||
@@ -19,7 +17,7 @@ class AutoMatchParty:
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self) -> Union[Tuple, None]:
|
||||
def match(self) -> tuple | None:
|
||||
result = None
|
||||
result = AutoMatchbyAccountIBAN(
|
||||
bank_party_account_number=self.bank_party_account_number,
|
||||
@@ -50,7 +48,7 @@ class AutoMatchbyAccountIBAN:
|
||||
result = self.match_account_in_party()
|
||||
return result
|
||||
|
||||
def match_account_in_party(self) -> Union[Tuple, None]:
|
||||
def match_account_in_party(self) -> tuple | None:
|
||||
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
@@ -97,7 +95,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
def get(self, key):
|
||||
return self.__dict__.get(key, None)
|
||||
|
||||
def match(self) -> Union[Tuple, None]:
|
||||
def match(self) -> tuple | None:
|
||||
# fuzzy search by customer/supplier & employee
|
||||
if not (self.bank_party_name or self.description):
|
||||
return None
|
||||
@@ -105,7 +103,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
result = self.match_party_name_desc_in_party()
|
||||
return result
|
||||
|
||||
def match_party_name_desc_in_party(self) -> Union[Tuple, None]:
|
||||
def match_party_name_desc_in_party(self) -> tuple | None:
|
||||
"""Fuzzy search party name and/or description against parties in the system"""
|
||||
result = None
|
||||
parties = get_parties_in_order(self.deposit)
|
||||
@@ -129,7 +127,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
|
||||
return result
|
||||
|
||||
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
|
||||
def fuzzy_search_and_return_result(self, party, names, field) -> tuple | None:
|
||||
skip = False
|
||||
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
|
||||
party_name, skip = self.process_fuzzy_result(result)
|
||||
@@ -142,7 +140,7 @@ class AutoMatchbyPartyNameDescription:
|
||||
party_name,
|
||||
), skip
|
||||
|
||||
def process_fuzzy_result(self, result: Union[list, None]):
|
||||
def process_fuzzy_result(self, result: list | None):
|
||||
"""
|
||||
If there are multiple valid close matches return None as result may be faulty.
|
||||
Return the result only if one accurate match stands out.
|
||||
|
||||
@@ -56,17 +56,19 @@ class BankTransaction(Document):
|
||||
Bank Transaction should be on the same currency as the Bank Account.
|
||||
"""
|
||||
if self.currency and self.bank_account:
|
||||
account = frappe.get_cached_value("Bank Account", self.bank_account, "account")
|
||||
account_currency = frappe.get_cached_value("Account", account, "account_currency")
|
||||
if account := frappe.get_cached_value("Bank Account", self.bank_account, "account"):
|
||||
account_currency = frappe.get_cached_value("Account", account, "account_currency")
|
||||
|
||||
if self.currency != account_currency:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
|
||||
).format(
|
||||
frappe.bold(self.currency), frappe.bold(self.bank_account), frappe.bold(account_currency)
|
||||
if self.currency != account_currency:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Transaction currency: {0} cannot be different from Bank Account({1}) currency: {2}"
|
||||
).format(
|
||||
frappe.bold(self.currency),
|
||||
frappe.bold(self.bank_account),
|
||||
frappe.bold(account_currency),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def set_status(self):
|
||||
if self.docstatus == 2:
|
||||
@@ -180,7 +182,7 @@ class BankTransaction(Document):
|
||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
for payment_entry in to_remove:
|
||||
self.remove(to_remove)
|
||||
self.remove(payment_entry)
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_payment_entries(self):
|
||||
@@ -235,9 +237,7 @@ def get_clearance_details(transaction, payment_entry):
|
||||
"""
|
||||
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
|
||||
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
bt_allocations = get_total_allocated_amount(
|
||||
payment_entry.payment_document, payment_entry.payment_entry
|
||||
)
|
||||
bt_allocations = get_total_allocated_amount(payment_entry.payment_document, payment_entry.payment_entry)
|
||||
|
||||
unallocated_amount = min(
|
||||
transaction.unallocated_amount,
|
||||
@@ -332,7 +332,6 @@ def get_total_allocated_amount(doctype, docname):
|
||||
|
||||
def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
|
||||
|
||||
paid_amount_field = "paid_amount"
|
||||
if payment_entry.payment_document == "Payment Entry":
|
||||
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
|
||||
@@ -371,9 +370,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Loan Repayment":
|
||||
return frappe.db.get_value(
|
||||
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
|
||||
)
|
||||
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
|
||||
|
||||
elif payment_entry.payment_document == "Bank Transaction":
|
||||
dep, wth = frappe.db.get_value(
|
||||
@@ -383,9 +380,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
|
||||
else:
|
||||
frappe.throw(
|
||||
"Please reconcile {0}: {1} manually".format(
|
||||
payment_entry.payment_document, payment_entry.payment_entry
|
||||
)
|
||||
f"Please reconcile {payment_entry.payment_document}: {payment_entry.payment_entry} manually"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -18,12 +18,12 @@ def upload_bank_statement():
|
||||
fcontent = frappe.local.uploaded_file
|
||||
fname = frappe.local.uploaded_filename
|
||||
|
||||
if frappe.safe_encode(fname).lower().endswith("csv".encode("utf-8")):
|
||||
if frappe.safe_encode(fname).lower().endswith(b"csv"):
|
||||
from frappe.utils.csvutils import read_csv_content
|
||||
|
||||
rows = read_csv_content(fcontent, False)
|
||||
|
||||
elif frappe.safe_encode(fname).lower().endswith("xlsx".encode("utf-8")):
|
||||
elif frappe.safe_encode(fname).lower().endswith(b"xlsx"):
|
||||
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
|
||||
|
||||
rows = read_xlsx_file_from_attached_file(fcontent=fcontent)
|
||||
|
||||
@@ -436,9 +436,7 @@ def add_vouchers(gl_account="_Test Bank - _TC"):
|
||||
|
||||
mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
|
||||
|
||||
if not frappe.db.get_value(
|
||||
"Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
|
||||
):
|
||||
if not frappe.db.get_value("Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}):
|
||||
mode_of_payment.append("accounts", {"company": "_Test Company", "default_account": gl_account})
|
||||
mode_of_payment.save()
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Bisect Accounting Statements", {
|
||||
onload(frm) {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
refresh(frm) {
|
||||
frm.add_custom_button(__("Bisect Left"), () => {
|
||||
frm.trigger("bisect_left");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Bisect Right"), () => {
|
||||
frm.trigger("bisect_right");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Up"), () => {
|
||||
frm.trigger("move_up");
|
||||
});
|
||||
frm.add_custom_button(__("Build Tree"), () => {
|
||||
frm.trigger("build_tree");
|
||||
});
|
||||
},
|
||||
render_heatmap(frm) {
|
||||
let bisect_heatmap = frm.get_field("bisect_heatmap").$wrapper;
|
||||
bisect_heatmap.addClass("bisect_heatmap_location");
|
||||
|
||||
// milliseconds in a day
|
||||
let msiad = 24 * 60 * 60 * 1000;
|
||||
let datapoints = {};
|
||||
let fr_dt = new Date(frm.doc.from_date).getTime();
|
||||
let to_dt = new Date(frm.doc.to_date).getTime();
|
||||
let bisect_start = new Date(frm.doc.current_from_date).getTime();
|
||||
let bisect_end = new Date(frm.doc.current_to_date).getTime();
|
||||
|
||||
for (let x = fr_dt; x <= to_dt; x += msiad) {
|
||||
let epoch_in_seconds = x / 1000;
|
||||
if (bisect_start <= x && x <= bisect_end) {
|
||||
datapoints[epoch_in_seconds] = 1.0;
|
||||
} else {
|
||||
datapoints[epoch_in_seconds] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
new frappe.Chart(".bisect_heatmap_location", {
|
||||
type: "heatmap",
|
||||
data: {
|
||||
dataPoints: datapoints,
|
||||
start: new Date(frm.doc.from_date),
|
||||
end: new Date(frm.doc.to_date),
|
||||
},
|
||||
countLabel: "Bisecting",
|
||||
discreteDomains: 1,
|
||||
});
|
||||
},
|
||||
bisect_left(frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: "bisect_left",
|
||||
freeze: true,
|
||||
freeze_message: __("Bisecting Left ..."),
|
||||
callback: (r) => {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
});
|
||||
},
|
||||
bisect_right(frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __("Bisecting Right ..."),
|
||||
method: "bisect_right",
|
||||
callback: (r) => {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
});
|
||||
},
|
||||
move_up(frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __("Moving up in tree ..."),
|
||||
method: "move_up",
|
||||
callback: (r) => {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
});
|
||||
},
|
||||
build_tree(frm) {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
freeze: true,
|
||||
freeze_message: __("Rebuilding BTree for period ..."),
|
||||
method: "build_tree",
|
||||
callback: (r) => {
|
||||
frm.trigger("render_heatmap");
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-09-15 21:28:28.054773",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_cvfg",
|
||||
"company",
|
||||
"column_break_hcam",
|
||||
"from_date",
|
||||
"column_break_qxbi",
|
||||
"to_date",
|
||||
"column_break_iwny",
|
||||
"algorithm",
|
||||
"section_break_8ph9",
|
||||
"current_node",
|
||||
"section_break_ngid",
|
||||
"bisect_heatmap",
|
||||
"section_break_hmsy",
|
||||
"bisecting_from",
|
||||
"current_from_date",
|
||||
"column_break_uqyd",
|
||||
"bisecting_to",
|
||||
"current_to_date",
|
||||
"section_break_hbyo",
|
||||
"heading_cppb",
|
||||
"p_l_summary",
|
||||
"column_break_aivo",
|
||||
"balance_sheet_summary",
|
||||
"b_s_summary",
|
||||
"column_break_gvwx",
|
||||
"difference_heading",
|
||||
"difference"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "column_break_qxbi",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "From Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "To Date"
|
||||
},
|
||||
{
|
||||
"default": "BFS",
|
||||
"fieldname": "algorithm",
|
||||
"fieldtype": "Select",
|
||||
"label": "Algorithm",
|
||||
"options": "BFS\nDFS"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iwny",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "current_node",
|
||||
"fieldtype": "Link",
|
||||
"label": "Current Node",
|
||||
"options": "Bisect Nodes"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_hmsy",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "current_from_date",
|
||||
"fieldtype": "Datetime",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "current_to_date",
|
||||
"fieldtype": "Datetime",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uqyd",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_hbyo",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "p_l_summary",
|
||||
"fieldtype": "Float",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "b_s_summary",
|
||||
"fieldtype": "Float",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "difference",
|
||||
"fieldtype": "Float",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_aivo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_gvwx",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_hcam",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ngid",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_8ph9",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bisect_heatmap",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Heatmap"
|
||||
},
|
||||
{
|
||||
"fieldname": "heading_cppb",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Profit and Loss Summary"
|
||||
},
|
||||
{
|
||||
"fieldname": "balance_sheet_summary",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Balance Sheet Summary"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_heading",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Difference"
|
||||
},
|
||||
{
|
||||
"fieldname": "bisecting_from",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Bisecting From"
|
||||
},
|
||||
{
|
||||
"fieldname": "bisecting_to",
|
||||
"fieldtype": "Heading",
|
||||
"label": "Bisecting To"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_cvfg",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-01 16:49:54.073890",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bisect Accounting Statements",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import datetime
|
||||
from collections import deque
|
||||
from math import floor
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils.data import guess_date_format
|
||||
|
||||
|
||||
class BisectAccountingStatements(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
algorithm: DF.Literal["BFS", "DFS"]
|
||||
b_s_summary: DF.Float
|
||||
company: DF.Link | None
|
||||
current_from_date: DF.Datetime | None
|
||||
current_node: DF.Link | None
|
||||
current_to_date: DF.Datetime | None
|
||||
difference: DF.Float
|
||||
from_date: DF.Datetime | None
|
||||
p_l_summary: DF.Float
|
||||
to_date: DF.Datetime | None
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
|
||||
def validate_dates(self):
|
||||
if getdate(self.from_date) > getdate(self.to_date):
|
||||
frappe.throw(
|
||||
_("From Date: {0} cannot be greater than To date: {1}").format(
|
||||
frappe.bold(self.from_date), frappe.bold(self.to_date)
|
||||
)
|
||||
)
|
||||
|
||||
def bfs(self, from_date: datetime, to_date: datetime):
|
||||
# Make Root node
|
||||
node = frappe.new_doc("Bisect Nodes")
|
||||
node.root = None
|
||||
node.period_from_date = from_date
|
||||
node.period_to_date = to_date
|
||||
node.insert()
|
||||
|
||||
period_queue = deque([node])
|
||||
while period_queue:
|
||||
cur_node = period_queue.popleft()
|
||||
delta = cur_node.period_to_date - cur_node.period_from_date
|
||||
if delta.days == 0:
|
||||
continue
|
||||
else:
|
||||
cur_floor = floor(delta.days / 2)
|
||||
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
|
||||
left_node = frappe.new_doc("Bisect Nodes")
|
||||
left_node.period_from_date = cur_node.period_from_date
|
||||
left_node.period_to_date = next_to_date
|
||||
left_node.root = cur_node.name
|
||||
left_node.generated = False
|
||||
left_node.insert()
|
||||
cur_node.left_child = left_node.name
|
||||
period_queue.append(left_node)
|
||||
|
||||
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
|
||||
right_node = frappe.new_doc("Bisect Nodes")
|
||||
right_node.period_from_date = next_from_date
|
||||
right_node.period_to_date = cur_node.period_to_date
|
||||
right_node.root = cur_node.name
|
||||
right_node.generated = False
|
||||
right_node.insert()
|
||||
cur_node.right_child = right_node.name
|
||||
period_queue.append(right_node)
|
||||
|
||||
cur_node.save()
|
||||
|
||||
def dfs(self, from_date: datetime, to_date: datetime):
|
||||
# Make Root node
|
||||
node = frappe.new_doc("Bisect Nodes")
|
||||
node.root = None
|
||||
node.period_from_date = from_date
|
||||
node.period_to_date = to_date
|
||||
node.insert()
|
||||
|
||||
period_stack = [node]
|
||||
while period_stack:
|
||||
cur_node = period_stack.pop()
|
||||
delta = cur_node.period_to_date - cur_node.period_from_date
|
||||
if delta.days == 0:
|
||||
continue
|
||||
else:
|
||||
cur_floor = floor(delta.days / 2)
|
||||
next_to_date = cur_node.period_from_date + relativedelta(days=+cur_floor)
|
||||
left_node = frappe.new_doc("Bisect Nodes")
|
||||
left_node.period_from_date = cur_node.period_from_date
|
||||
left_node.period_to_date = next_to_date
|
||||
left_node.root = cur_node.name
|
||||
left_node.generated = False
|
||||
left_node.insert()
|
||||
cur_node.left_child = left_node.name
|
||||
period_stack.append(left_node)
|
||||
|
||||
next_from_date = cur_node.period_from_date + relativedelta(days=+(cur_floor + 1))
|
||||
right_node = frappe.new_doc("Bisect Nodes")
|
||||
right_node.period_from_date = next_from_date
|
||||
right_node.period_to_date = cur_node.period_to_date
|
||||
right_node.root = cur_node.name
|
||||
right_node.generated = False
|
||||
right_node.insert()
|
||||
cur_node.right_child = right_node.name
|
||||
period_stack.append(right_node)
|
||||
|
||||
cur_node.save()
|
||||
|
||||
@frappe.whitelist()
|
||||
def build_tree(self):
|
||||
frappe.db.delete("Bisect Nodes")
|
||||
|
||||
# Convert str to datetime format
|
||||
dt_format = guess_date_format(self.from_date)
|
||||
from_date = datetime.datetime.strptime(self.from_date, dt_format)
|
||||
to_date = datetime.datetime.strptime(self.to_date, dt_format)
|
||||
|
||||
if self.algorithm == "BFS":
|
||||
self.bfs(from_date, to_date)
|
||||
|
||||
if self.algorithm == "DFS":
|
||||
self.dfs(from_date, to_date)
|
||||
|
||||
# set root as current node
|
||||
root = frappe.db.get_all("Bisect Nodes", filters={"root": ["is", "not set"]})[0]
|
||||
self.get_report_summary()
|
||||
self.current_node = root.name
|
||||
self.current_from_date = self.from_date
|
||||
self.current_to_date = self.to_date
|
||||
self.save()
|
||||
|
||||
def get_report_summary(self):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": self.current_from_date,
|
||||
"period_end_date": self.current_to_date,
|
||||
"periodicity": "Yearly",
|
||||
}
|
||||
pl_summary = frappe.get_doc("Report", "Profit and Loss Statement")
|
||||
self.p_l_summary = pl_summary.execute_script_report(filters=filters)[5]
|
||||
bs_summary = frappe.get_doc("Report", "Balance Sheet")
|
||||
self.b_s_summary = bs_summary.execute_script_report(filters=filters)[5]
|
||||
self.difference = abs(self.p_l_summary - self.b_s_summary)
|
||||
|
||||
def update_node(self):
|
||||
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
current_node.balance_sheet_summary = self.b_s_summary
|
||||
current_node.profit_loss_summary = self.p_l_summary
|
||||
current_node.difference = self.difference
|
||||
current_node.generated = True
|
||||
current_node.save()
|
||||
|
||||
def current_node_has_summary_info(self):
|
||||
"Assertion method"
|
||||
return frappe.db.get_value("Bisect Nodes", self.current_node, "generated")
|
||||
|
||||
def fetch_summary_info_from_current_node(self):
|
||||
current_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
self.p_l_summary = current_node.balance_sheet_summary
|
||||
self.b_s_summary = current_node.profit_loss_summary
|
||||
self.difference = abs(self.p_l_summary - self.b_s_summary)
|
||||
|
||||
def fetch_or_calculate(self):
|
||||
if self.current_node_has_summary_info():
|
||||
self.fetch_summary_info_from_current_node()
|
||||
else:
|
||||
self.get_report_summary()
|
||||
self.update_node()
|
||||
|
||||
@frappe.whitelist()
|
||||
def bisect_left(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
if cur_node.left_child is not None:
|
||||
lft_node = frappe.get_doc("Bisect Nodes", cur_node.left_child)
|
||||
self.current_node = cur_node.left_child
|
||||
self.current_from_date = lft_node.period_from_date
|
||||
self.current_to_date = lft_node.period_to_date
|
||||
self.fetch_or_calculate()
|
||||
self.save()
|
||||
else:
|
||||
frappe.msgprint(_("No more children on Left"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def bisect_right(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
if cur_node.right_child is not None:
|
||||
rgt_node = frappe.get_doc("Bisect Nodes", cur_node.right_child)
|
||||
self.current_node = cur_node.right_child
|
||||
self.current_from_date = rgt_node.period_from_date
|
||||
self.current_to_date = rgt_node.period_to_date
|
||||
self.fetch_or_calculate()
|
||||
self.save()
|
||||
else:
|
||||
frappe.msgprint(_("No more children on Right"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def move_up(self):
|
||||
if self.current_node is not None:
|
||||
cur_node = frappe.get_doc("Bisect Nodes", self.current_node)
|
||||
if cur_node.root is not None:
|
||||
root = frappe.get_doc("Bisect Nodes", cur_node.root)
|
||||
self.current_node = cur_node.root
|
||||
self.current_from_date = root.period_from_date
|
||||
self.current_to_date = root.period_to_date
|
||||
self.fetch_or_calculate()
|
||||
self.save()
|
||||
else:
|
||||
frappe.msgprint(_("Reached Root"))
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBisectAccountingStatements(FrappeTestCase):
|
||||
pass
|
||||
0
erpnext/accounts/doctype/bisect_nodes/__init__.py
Normal file
0
erpnext/accounts/doctype/bisect_nodes/__init__.py
Normal file
8
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js
Normal file
8
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Bisect Nodes", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
97
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json
Normal file
97
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json
Normal file
@@ -0,0 +1,97 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2023-09-27 14:56:38.112462",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"root",
|
||||
"left_child",
|
||||
"right_child",
|
||||
"period_from_date",
|
||||
"period_to_date",
|
||||
"difference",
|
||||
"balance_sheet_summary",
|
||||
"profit_loss_summary",
|
||||
"generated"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "root",
|
||||
"fieldtype": "Link",
|
||||
"label": "Root",
|
||||
"options": "Bisect Nodes"
|
||||
},
|
||||
{
|
||||
"fieldname": "left_child",
|
||||
"fieldtype": "Link",
|
||||
"label": "Left Child",
|
||||
"options": "Bisect Nodes"
|
||||
},
|
||||
{
|
||||
"fieldname": "right_child",
|
||||
"fieldtype": "Link",
|
||||
"label": "Right Child",
|
||||
"options": "Bisect Nodes"
|
||||
},
|
||||
{
|
||||
"fieldname": "period_from_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Period_from_date"
|
||||
},
|
||||
{
|
||||
"fieldname": "period_to_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Period To Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference",
|
||||
"fieldtype": "Float",
|
||||
"label": "Difference"
|
||||
},
|
||||
{
|
||||
"fieldname": "balance_sheet_summary",
|
||||
"fieldtype": "Float",
|
||||
"label": "Balance Sheet Summary"
|
||||
},
|
||||
{
|
||||
"fieldname": "profit_loss_summary",
|
||||
"fieldtype": "Float",
|
||||
"label": "Profit and Loss Summary"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "generated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Generated"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-01 17:46:12.437996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bisect Nodes",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
29
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py
Normal file
29
erpnext/accounts/doctype/bisect_nodes/bisect_nodes.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class BisectNodes(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
balance_sheet_summary: DF.Float
|
||||
difference: DF.Float
|
||||
generated: DF.Check
|
||||
left_child: DF.Link | None
|
||||
name: DF.Int | None
|
||||
period_from_date: DF.Datetime | None
|
||||
period_to_date: DF.Datetime | None
|
||||
profit_loss_summary: DF.Float
|
||||
right_child: DF.Link | None
|
||||
root: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBisectNodes(FrappeTestCase):
|
||||
pass
|
||||
@@ -70,10 +70,11 @@ class Budget(Document):
|
||||
select
|
||||
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
|
||||
where
|
||||
ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and
|
||||
b.fiscal_year=%s and b.name != %s and ba.account in (%s) """
|
||||
% ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))),
|
||||
(self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts),
|
||||
ba.parent = b.name and b.docstatus < 2 and b.company = {} and {}={} and
|
||||
b.fiscal_year={} and b.name != {} and ba.account in ({}) """.format(
|
||||
"%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))
|
||||
),
|
||||
(self.company, budget_against, self.fiscal_year, self.name, *tuple(accounts)),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -96,12 +97,14 @@ class Budget(Document):
|
||||
if account_details.is_group:
|
||||
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
|
||||
elif account_details.company != self.company:
|
||||
frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company))
|
||||
frappe.throw(
|
||||
_("Account {0} does not belongs to company {1}").format(d.account, self.company)
|
||||
)
|
||||
elif account_details.report_type != "Profit and Loss":
|
||||
frappe.throw(
|
||||
_("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
|
||||
d.account
|
||||
)
|
||||
_(
|
||||
"Budget cannot be assigned against {0}, as it's not an Income or Expense account"
|
||||
).format(d.account)
|
||||
)
|
||||
|
||||
if d.account in account_list:
|
||||
@@ -174,30 +177,24 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
and args.account
|
||||
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
|
||||
):
|
||||
|
||||
doctype = dimension.get("document_type")
|
||||
|
||||
if frappe.get_cached_value("DocType", doctype, "is_tree"):
|
||||
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
|
||||
condition = """and exists(select name from `tab%s`
|
||||
where lft<=%s and rgt>=%s and name=b.%s)""" % (
|
||||
doctype,
|
||||
lft,
|
||||
rgt,
|
||||
budget_against,
|
||||
) # nosec
|
||||
condition = f"""and exists(select name from `tab{doctype}`
|
||||
where lft<={lft} and rgt>={rgt} and name=b.{budget_against})""" # nosec
|
||||
args.is_tree = True
|
||||
else:
|
||||
condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against)))
|
||||
condition = f"and b.{budget_against}={frappe.db.escape(args.get(budget_against))}"
|
||||
args.is_tree = False
|
||||
|
||||
args.budget_against_field = budget_against
|
||||
args.budget_against_doctype = doctype
|
||||
|
||||
budget_records = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select
|
||||
b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution,
|
||||
b.{budget_against} as budget_against, ba.budget_amount, b.monthly_distribution,
|
||||
ifnull(b.applicable_on_material_request, 0) as for_material_request,
|
||||
ifnull(applicable_on_purchase_order, 0) as for_purchase_order,
|
||||
ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses,
|
||||
@@ -210,9 +207,7 @@ def validate_expense_against_budget(args, expense_amount=0):
|
||||
b.name=ba.parent and b.fiscal_year=%s
|
||||
and ba.account=%s and b.docstatus=1
|
||||
{condition}
|
||||
""".format(
|
||||
condition=condition, budget_against_field=budget_against
|
||||
),
|
||||
""",
|
||||
(args.fiscal_year, args.account),
|
||||
as_dict=True,
|
||||
) # nosec
|
||||
@@ -240,7 +235,12 @@ def validate_budget_records(args, budget_records, expense_amount):
|
||||
args["month_end_date"] = get_last_day(args.posting_date)
|
||||
|
||||
compare_expense_with_budget(
|
||||
args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount
|
||||
args,
|
||||
budget_amount,
|
||||
_("Accumulated Monthly"),
|
||||
monthly_action,
|
||||
budget.budget_against,
|
||||
amount,
|
||||
)
|
||||
|
||||
|
||||
@@ -268,9 +268,8 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
|
||||
frappe.bold(fmt_money(diff, currency=currency)),
|
||||
)
|
||||
|
||||
if (
|
||||
frappe.flags.exception_approver_role
|
||||
and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)
|
||||
if frappe.flags.exception_approver_role and frappe.flags.exception_approver_role in frappe.get_roles(
|
||||
frappe.session.user
|
||||
):
|
||||
action = "Warn"
|
||||
|
||||
@@ -316,10 +315,8 @@ def get_requested_amount(args, budget):
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
|
||||
from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and
|
||||
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and
|
||||
parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(
|
||||
condition
|
||||
),
|
||||
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {} and
|
||||
parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition),
|
||||
item_code,
|
||||
as_list=1,
|
||||
)
|
||||
@@ -332,12 +329,10 @@ def get_ordered_amount(args, budget):
|
||||
condition = get_other_condition(args, budget, "Purchase Order")
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
f""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
|
||||
from `tabPurchase Order Item` child, `tabPurchase Order` parent where
|
||||
parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt
|
||||
and parent.status != 'Closed' and {0}""".format(
|
||||
condition
|
||||
),
|
||||
and parent.status != 'Closed' and {condition}""",
|
||||
item_code,
|
||||
as_list=1,
|
||||
)
|
||||
@@ -350,7 +345,7 @@ def get_other_condition(args, budget, for_doc):
|
||||
budget_against_field = args.get("budget_against_field")
|
||||
|
||||
if budget_against_field and args.get(budget_against_field):
|
||||
condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field))
|
||||
condition += f" and child.{budget_against_field} = '{args.get(budget_against_field)}'"
|
||||
|
||||
if args.get("fiscal_year"):
|
||||
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
|
||||
@@ -358,12 +353,8 @@ def get_other_condition(args, budget, for_doc):
|
||||
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
|
||||
)
|
||||
|
||||
condition += """ and parent.%s
|
||||
between '%s' and '%s' """ % (
|
||||
date_field,
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
condition += f""" and parent.{date_field}
|
||||
between '{start_date}' and '{end_date}' """
|
||||
|
||||
return condition
|
||||
|
||||
@@ -382,21 +373,17 @@ def get_actual_expense(args):
|
||||
|
||||
args.update(lft_rgt)
|
||||
|
||||
condition2 = """and exists(select name from `tab{doctype}`
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where lft>=%(lft)s and rgt<=%(rgt)s
|
||||
and name=gle.{budget_against_field})""".format(
|
||||
doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec
|
||||
)
|
||||
and name=gle.{budget_against_field})"""
|
||||
else:
|
||||
condition2 = """and exists(select name from `tab{doctype}`
|
||||
where name=gle.{budget_against} and
|
||||
gle.{budget_against} = %({budget_against})s)""".format(
|
||||
doctype=args.budget_against_doctype, budget_against=budget_against_field
|
||||
)
|
||||
condition2 = f"""and exists(select name from `tab{args.budget_against_doctype}`
|
||||
where name=gle.{budget_against_field} and
|
||||
gle.{budget_against_field} = %({budget_against_field})s)"""
|
||||
|
||||
amount = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select sum(gle.debit) - sum(gle.credit)
|
||||
from `tabGL Entry` gle
|
||||
where
|
||||
@@ -407,9 +394,7 @@ def get_actual_expense(args):
|
||||
and gle.company=%(company)s
|
||||
and gle.docstatus=1
|
||||
{condition2}
|
||||
""".format(
|
||||
condition1=condition1, condition2=condition2
|
||||
),
|
||||
""",
|
||||
(args),
|
||||
)[0][0]
|
||||
) # nosec
|
||||
|
||||
@@ -41,9 +41,7 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -63,9 +61,7 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Cost Center")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -97,9 +93,7 @@ class TestBudget(unittest.TestCase):
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
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)
|
||||
|
||||
mr = frappe.get_doc(
|
||||
@@ -138,9 +132,7 @@ class TestBudget(unittest.TestCase):
|
||||
)
|
||||
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
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)
|
||||
|
||||
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
|
||||
@@ -158,9 +150,7 @@ class TestBudget(unittest.TestCase):
|
||||
|
||||
budget = make_budget(budget_against="Project")
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
@@ -223,7 +213,7 @@ class TestBudget(unittest.TestCase):
|
||||
if month > 9:
|
||||
month = 9
|
||||
|
||||
for i in range(month + 1):
|
||||
for _i in range(month + 1):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
@@ -237,9 +227,7 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
self.assertRaises(BudgetError, jv.cancel)
|
||||
|
||||
@@ -255,7 +243,7 @@ class TestBudget(unittest.TestCase):
|
||||
month = 9
|
||||
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
for i in range(month + 1):
|
||||
for _i in range(month + 1):
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
"_Test Bank - _TC",
|
||||
@@ -270,9 +258,7 @@ class TestBudget(unittest.TestCase):
|
||||
frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
self.assertRaises(BudgetError, jv.cancel)
|
||||
|
||||
@@ -284,9 +270,7 @@ class TestBudget(unittest.TestCase):
|
||||
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -316,9 +300,7 @@ class TestBudget(unittest.TestCase):
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
|
||||
frappe.db.set_value(
|
||||
"Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
|
||||
)
|
||||
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -423,13 +405,11 @@ def make_budget(**args):
|
||||
fiscal_year = get_fiscal_year(nowdate())[0]
|
||||
|
||||
if budget_against == "Project":
|
||||
project_name = "{0}%".format("_Test Project/" + fiscal_year)
|
||||
project_name = "{}%".format("_Test Project/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
|
||||
else:
|
||||
cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
|
||||
budget_list = frappe.get_all(
|
||||
"Budget", fields=["name"], filters={"name": ("like", cost_center_name)}
|
||||
)
|
||||
cost_center_name = "{}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
|
||||
budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", cost_center_name)})
|
||||
for d in budget_list:
|
||||
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
|
||||
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
|
||||
@@ -451,24 +431,18 @@ def make_budget(**args):
|
||||
budget.action_if_annual_budget_exceeded = "Stop"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
|
||||
budget.budget_against = budget_against
|
||||
budget.append(
|
||||
"accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000}
|
||||
)
|
||||
budget.append("accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000})
|
||||
|
||||
if args.applicable_on_material_request:
|
||||
budget.applicable_on_material_request = 1
|
||||
budget.action_if_annual_budget_exceeded_on_mr = (
|
||||
args.action_if_annual_budget_exceeded_on_mr or "Warn"
|
||||
)
|
||||
budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or "Warn"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded_on_mr = (
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn"
|
||||
)
|
||||
|
||||
if args.applicable_on_purchase_order:
|
||||
budget.applicable_on_purchase_order = 1
|
||||
budget.action_if_annual_budget_exceeded_on_po = (
|
||||
args.action_if_annual_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or "Warn"
|
||||
budget.action_if_accumulated_monthly_budget_exceeded_on_po = (
|
||||
args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
|
||||
)
|
||||
|
||||
@@ -38,9 +38,7 @@ class ChartofAccountsImporter(Document):
|
||||
|
||||
def validate(self):
|
||||
if self.import_file:
|
||||
get_coa(
|
||||
"Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1
|
||||
)
|
||||
get_coa("Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1)
|
||||
|
||||
|
||||
def validate_columns(data):
|
||||
@@ -116,7 +114,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
|
||||
file_path = file_doc.get_full_path()
|
||||
|
||||
data = []
|
||||
with open(file_path, "r") as in_file:
|
||||
with open(file_path) as in_file:
|
||||
csv_reader = list(csv.reader(in_file))
|
||||
headers = csv_reader[0]
|
||||
del csv_reader[0] # delete top row and headers row
|
||||
@@ -215,10 +213,10 @@ def build_forest(data):
|
||||
for row in data:
|
||||
account_name, parent_account, account_number, parent_account_number = row[0:4]
|
||||
if account_number:
|
||||
account_name = "{} - {}".format(account_number, account_name)
|
||||
account_name = f"{account_number} - {account_name}"
|
||||
if parent_account_number:
|
||||
parent_account_number = cstr(parent_account_number).strip()
|
||||
parent_account = "{} - {}".format(parent_account_number, parent_account)
|
||||
parent_account = f"{parent_account_number} - {parent_account}"
|
||||
|
||||
if parent_account == account_name == child:
|
||||
return [parent_account]
|
||||
@@ -230,7 +228,7 @@ def build_forest(data):
|
||||
frappe.bold(parent_account)
|
||||
)
|
||||
)
|
||||
return [child] + parent_account_list
|
||||
return [child, *parent_account_list]
|
||||
|
||||
charts_map, paths = {}, []
|
||||
|
||||
@@ -250,12 +248,12 @@ def build_forest(data):
|
||||
) = i
|
||||
|
||||
if not account_name:
|
||||
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
|
||||
error_messages.append(f"Row {line_no}: Please enter Account Name")
|
||||
|
||||
name = account_name
|
||||
if account_number:
|
||||
account_number = cstr(account_number).strip()
|
||||
account_name = "{} - {}".format(account_number, account_name)
|
||||
account_name = f"{account_number} - {account_name}"
|
||||
|
||||
charts_map[account_name] = {}
|
||||
charts_map[account_name]["account_name"] = name
|
||||
@@ -352,9 +350,9 @@ def get_template(template_type, company):
|
||||
|
||||
def get_sample_template(writer, company):
|
||||
currency = frappe.db.get_value("Company", company, "default_currency")
|
||||
with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv"), "r") as f:
|
||||
with open(os.path.join(os.path.dirname(__file__), "coa_sample_template.csv")) as f:
|
||||
for row in f:
|
||||
row = row.strip().split(",") + [currency]
|
||||
row = [*row.strip().split(","), currency]
|
||||
writer.writerow(row)
|
||||
|
||||
return writer
|
||||
@@ -463,7 +461,7 @@ def unset_existing_data(company):
|
||||
"Purchase Taxes and Charges Template",
|
||||
]:
|
||||
frappe.db.sql(
|
||||
'''delete from `tab{0}` where `company`="%s"'''.format(doctype) % (company) # nosec
|
||||
f'''delete from `tab{doctype}` where `company`="%s"''' % (company) # nosec
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -66,71 +66,71 @@ def create_or_update_cheque_print_format(template_name):
|
||||
|
||||
cheque_print.html = """
|
||||
<style>
|
||||
.print-format {
|
||||
.print-format {{
|
||||
padding: 0px;
|
||||
}
|
||||
@media screen {
|
||||
.print-format {
|
||||
}}
|
||||
@media screen {{
|
||||
.print-format {{
|
||||
padding: 0in;
|
||||
}
|
||||
}
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
<div style="position: relative; top:%(starting_position_from_top_edge)scm">
|
||||
<div style="width:%(cheque_width)scm;height:%(cheque_height)scm;">
|
||||
<span style="top:%(acc_pay_dist_from_top_edge)scm; left:%(acc_pay_dist_from_left_edge)scm;
|
||||
<div style="position: relative; top:{starting_position_from_top_edge}cm">
|
||||
<div style="width:{cheque_width}cm;height:{cheque_height}cm;">
|
||||
<span style="top:{acc_pay_dist_from_top_edge}cm; left:{acc_pay_dist_from_left_edge}cm;
|
||||
border-bottom: solid 1px;border-top:solid 1px; width:2cm;text-align: center; position: absolute;">
|
||||
%(message_to_show)s
|
||||
{message_to_show}
|
||||
</span>
|
||||
<span style="top:%(date_dist_from_top_edge)scm; left:%(date_dist_from_left_edge)scm;
|
||||
<span style="top:{date_dist_from_top_edge}cm; left:{date_dist_from_left_edge}cm;
|
||||
position: absolute;">
|
||||
{{ frappe.utils.formatdate(doc.reference_date) or '' }}
|
||||
{{{{ frappe.utils.formatdate(doc.reference_date) or '' }}}}
|
||||
</span>
|
||||
<span style="top:%(acc_no_dist_from_top_edge)scm;left:%(acc_no_dist_from_left_edge)scm;
|
||||
<span style="top:{acc_no_dist_from_top_edge}cm;left:{acc_no_dist_from_left_edge}cm;
|
||||
position: absolute; min-width: 6cm;">
|
||||
{{ doc.account_no or '' }}
|
||||
{{{{ doc.account_no or '' }}}}
|
||||
</span>
|
||||
<span style="top:%(payer_name_from_top_edge)scm;left: %(payer_name_from_left_edge)scm;
|
||||
<span style="top:{payer_name_from_top_edge}cm;left: {payer_name_from_left_edge}cm;
|
||||
position: absolute; min-width: 6cm;">
|
||||
{{doc.party_name}}
|
||||
{{{{doc.party_name}}}}
|
||||
</span>
|
||||
<span style="top:%(amt_in_words_from_top_edge)scm; left:%(amt_in_words_from_left_edge)scm;
|
||||
position: absolute; display: block; width: %(amt_in_word_width)scm;
|
||||
line-height:%(amt_in_words_line_spacing)scm; word-wrap: break-word;">
|
||||
{{frappe.utils.money_in_words(doc.base_paid_amount or doc.base_received_amount)}}
|
||||
<span style="top:{amt_in_words_from_top_edge}cm; left:{amt_in_words_from_left_edge}cm;
|
||||
position: absolute; display: block; width: {amt_in_word_width}cm;
|
||||
line-height:{amt_in_words_line_spacing}cm; word-wrap: break-word;">
|
||||
{{{{frappe.utils.money_in_words(doc.base_paid_amount or doc.base_received_amount)}}}}
|
||||
</span>
|
||||
<span style="top:%(amt_in_figures_from_top_edge)scm;left: %(amt_in_figures_from_left_edge)scm;
|
||||
<span style="top:{amt_in_figures_from_top_edge}cm;left: {amt_in_figures_from_left_edge}cm;
|
||||
position: absolute; min-width: 4cm;">
|
||||
{{doc.get_formatted("base_paid_amount") or doc.get_formatted("base_received_amount")}}
|
||||
{{{{doc.get_formatted("base_paid_amount") or doc.get_formatted("base_received_amount")}}}}
|
||||
</span>
|
||||
<span style="top:%(signatory_from_top_edge)scm;left: %(signatory_from_left_edge)scm;
|
||||
<span style="top:{signatory_from_top_edge}cm;left: {signatory_from_left_edge}cm;
|
||||
position: absolute; min-width: 6cm;">
|
||||
{{doc.company}}
|
||||
{{{{doc.company}}}}
|
||||
</span>
|
||||
</div>
|
||||
</div>""" % {
|
||||
"starting_position_from_top_edge": doc.starting_position_from_top_edge
|
||||
</div>""".format(
|
||||
starting_position_from_top_edge=doc.starting_position_from_top_edge
|
||||
if doc.cheque_size == "A4"
|
||||
else 0.0,
|
||||
"cheque_width": doc.cheque_width,
|
||||
"cheque_height": doc.cheque_height,
|
||||
"acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge,
|
||||
"acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge,
|
||||
"message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"),
|
||||
"date_dist_from_top_edge": doc.date_dist_from_top_edge,
|
||||
"date_dist_from_left_edge": doc.date_dist_from_left_edge,
|
||||
"acc_no_dist_from_top_edge": doc.acc_no_dist_from_top_edge,
|
||||
"acc_no_dist_from_left_edge": doc.acc_no_dist_from_left_edge,
|
||||
"payer_name_from_top_edge": doc.payer_name_from_top_edge,
|
||||
"payer_name_from_left_edge": doc.payer_name_from_left_edge,
|
||||
"amt_in_words_from_top_edge": doc.amt_in_words_from_top_edge,
|
||||
"amt_in_words_from_left_edge": doc.amt_in_words_from_left_edge,
|
||||
"amt_in_word_width": doc.amt_in_word_width,
|
||||
"amt_in_words_line_spacing": doc.amt_in_words_line_spacing,
|
||||
"amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge,
|
||||
"amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge,
|
||||
"signatory_from_top_edge": doc.signatory_from_top_edge,
|
||||
"signatory_from_left_edge": doc.signatory_from_left_edge,
|
||||
}
|
||||
cheque_width=doc.cheque_width,
|
||||
cheque_height=doc.cheque_height,
|
||||
acc_pay_dist_from_top_edge=doc.acc_pay_dist_from_top_edge,
|
||||
acc_pay_dist_from_left_edge=doc.acc_pay_dist_from_left_edge,
|
||||
message_to_show=doc.message_to_show if doc.message_to_show else _("Account Pay Only"),
|
||||
date_dist_from_top_edge=doc.date_dist_from_top_edge,
|
||||
date_dist_from_left_edge=doc.date_dist_from_left_edge,
|
||||
acc_no_dist_from_top_edge=doc.acc_no_dist_from_top_edge,
|
||||
acc_no_dist_from_left_edge=doc.acc_no_dist_from_left_edge,
|
||||
payer_name_from_top_edge=doc.payer_name_from_top_edge,
|
||||
payer_name_from_left_edge=doc.payer_name_from_left_edge,
|
||||
amt_in_words_from_top_edge=doc.amt_in_words_from_top_edge,
|
||||
amt_in_words_from_left_edge=doc.amt_in_words_from_left_edge,
|
||||
amt_in_word_width=doc.amt_in_word_width,
|
||||
amt_in_words_line_spacing=doc.amt_in_words_line_spacing,
|
||||
amt_in_figures_from_top_edge=doc.amt_in_figures_from_top_edge,
|
||||
amt_in_figures_from_left_edge=doc.amt_in_figures_from_left_edge,
|
||||
signatory_from_top_edge=doc.signatory_from_top_edge,
|
||||
signatory_from_left_edge=doc.signatory_from_left_edge,
|
||||
)
|
||||
|
||||
cheque_print.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@@ -34,9 +34,7 @@ class CostCenter(NestedSet):
|
||||
def autoname(self):
|
||||
from erpnext.accounts.utils import get_autoname_with_number
|
||||
|
||||
self.name = get_autoname_with_number(
|
||||
self.cost_center_number, self.cost_center_name, self.company
|
||||
)
|
||||
self.name = get_autoname_with_number(self.cost_center_number, self.cost_center_name, self.company)
|
||||
|
||||
def validate(self):
|
||||
self.validate_mandatory()
|
||||
@@ -109,14 +107,14 @@ class CostCenter(NestedSet):
|
||||
new_cost_center = get_name_with_abbr(newdn, self.company)
|
||||
|
||||
# Validate properties before merging
|
||||
super(CostCenter, self).before_rename(olddn, new_cost_center, merge, "is_group")
|
||||
super().before_rename(olddn, new_cost_center, merge, "is_group")
|
||||
if not merge:
|
||||
new_cost_center = get_name_with_number(new_cost_center, self.cost_center_number)
|
||||
|
||||
return new_cost_center
|
||||
|
||||
def after_rename(self, olddn, newdn, merge=False):
|
||||
super(CostCenter, self).after_rename(olddn, newdn, merge)
|
||||
super().after_rename(olddn, newdn, merge)
|
||||
|
||||
if not merge:
|
||||
new_cost_center = frappe.db.get_value(
|
||||
|
||||
@@ -10,7 +10,6 @@ test_records = frappe.get_test_records("Cost Center")
|
||||
|
||||
class TestCostCenter(unittest.TestCase):
|
||||
def test_cost_center_creation_against_child_node(self):
|
||||
|
||||
if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
|
||||
frappe.get_doc(test_records[1]).insert()
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class CostCenterAllocation(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CostCenterAllocation, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self._skip_from_date_validation = False
|
||||
|
||||
def validate(self):
|
||||
@@ -63,9 +63,7 @@ class CostCenterAllocation(Document):
|
||||
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
|
||||
|
||||
if total_percentage != 100:
|
||||
frappe.throw(
|
||||
_("Total percentage against cost centers should be 100"), WrongPercentageAllocation
|
||||
)
|
||||
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
|
||||
|
||||
def validate_from_date_based_on_existing_gle(self):
|
||||
# Check if GLE exists against the main cost center
|
||||
|
||||
@@ -3,22 +3,36 @@
|
||||
|
||||
frappe.ui.form.on("Currency Exchange Settings", {
|
||||
service_provider: function (frm) {
|
||||
if (frm.doc.service_provider == "exchangerate.host") {
|
||||
let result = ["result"];
|
||||
let params = {
|
||||
date: "{transaction_date}",
|
||||
from: "{from_currency}",
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, "https://api.exchangerate.host/convert", params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
symbols: "{to_currency}",
|
||||
};
|
||||
add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
|
||||
}
|
||||
frm.call({
|
||||
method: "erpnext.accounts.doctype.currency_exchange_settings.currency_exchange_settings.get_api_endpoint",
|
||||
args: {
|
||||
service_provider: frm.doc.service_provider,
|
||||
use_http: frm.doc.use_http,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r && r.message) {
|
||||
if (frm.doc.service_provider == "exchangerate.host") {
|
||||
let result = ["result"];
|
||||
let params = {
|
||||
date: "{transaction_date}",
|
||||
from: "{from_currency}",
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
symbols: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
use_http: function (frm) {
|
||||
frm.trigger("service_provider");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"disabled",
|
||||
"service_provider",
|
||||
"api_endpoint",
|
||||
"use_http",
|
||||
"access_key",
|
||||
"url",
|
||||
"column_break_3",
|
||||
@@ -91,12 +92,19 @@
|
||||
"fieldname": "access_key",
|
||||
"fieldtype": "Data",
|
||||
"label": "Access Key"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.service_provider != \"Custom\"",
|
||||
"fieldname": "use_http",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use HTTP Protocol"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-04 15:30:25.333860",
|
||||
"modified": "2024-03-18 08:32:26.895076",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
|
||||
@@ -31,6 +31,7 @@ class CurrencyExchangeSettings(Document):
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
@@ -42,7 +43,6 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
def set_parameters_and_result(self):
|
||||
if self.service_provider == "exchangerate.host":
|
||||
|
||||
if not self.access_key:
|
||||
frappe.throw(
|
||||
_("Access Key is required for Service Provider: {0}").format(
|
||||
@@ -53,7 +53,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = "https://api.exchangerate.host/convert"
|
||||
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
|
||||
self.append("result_key", {"key": "result"})
|
||||
self.append("req_params", {"key": "access_key", "value": self.access_key})
|
||||
self.append("req_params", {"key": "amount", "value": "1"})
|
||||
@@ -64,7 +64,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = "https://frankfurter.app/{transaction_date}"
|
||||
self.api_endpoint = get_api_endpoint(self.service_provider, self.use_http)
|
||||
self.append("result_key", {"key": "rates"})
|
||||
self.append("result_key", {"key": "{to_currency}"})
|
||||
self.append("req_params", {"key": "base", "value": "{from_currency}"})
|
||||
@@ -77,9 +77,7 @@ class CurrencyExchangeSettings(Document):
|
||||
transaction_date=nowdate(), to_currency="INR", from_currency="USD"
|
||||
)
|
||||
|
||||
api_url = self.api_endpoint.format(
|
||||
transaction_date=nowdate(), to_currency="INR", from_currency="USD"
|
||||
)
|
||||
api_url = self.api_endpoint.format(transaction_date=nowdate(), to_currency="INR", from_currency="USD")
|
||||
|
||||
try:
|
||||
response = requests.get(api_url, params=params)
|
||||
@@ -99,7 +97,23 @@ class CurrencyExchangeSettings(Document):
|
||||
]
|
||||
except Exception:
|
||||
frappe.throw(_("Invalid result key. Response:") + " " + response.text)
|
||||
if not isinstance(value, (int, float)):
|
||||
if not isinstance(value, int | float):
|
||||
frappe.throw(_("Returned exchange rate is neither integer not float."))
|
||||
|
||||
self.url = response.url
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "frankfurter.app/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
protocol = "http://"
|
||||
|
||||
return protocol + api
|
||||
return None
|
||||
|
||||
@@ -139,6 +139,10 @@ class Dunning(AccountsController):
|
||||
)
|
||||
row.dunning_level = len(past_dunnings) + 1
|
||||
|
||||
def on_cancel(self):
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
|
||||
def resolve_dunning(doc, state):
|
||||
"""
|
||||
|
||||
@@ -109,9 +109,7 @@ class TestDunning(FrappeTestCase):
|
||||
|
||||
def create_dunning(overdue_days, dunning_type_name=None):
|
||||
posting_date = add_days(today(), -1 * overdue_days)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=posting_date, qty=1, rate=100
|
||||
)
|
||||
sales_invoice = create_sales_invoice_against_cost_center(posting_date=posting_date, qty=1, rate=100)
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
|
||||
if dunning_type_name:
|
||||
|
||||
@@ -268,7 +268,6 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
# Handle Accounts with '0' balance in Account/Base Currency
|
||||
for d in [x for x in account_details if x.zero_balance]:
|
||||
|
||||
if d.balance != 0:
|
||||
current_exchange_rate = new_exchange_rate = 0
|
||||
|
||||
@@ -281,7 +280,8 @@ class ExchangeRateRevaluation(Document):
|
||||
new_balance_in_account_currency = 0
|
||||
|
||||
current_exchange_rate = (
|
||||
calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) or 0.0
|
||||
calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party)
|
||||
or 0.0
|
||||
)
|
||||
|
||||
gain_loss = new_balance_in_account_currency - (
|
||||
@@ -335,9 +335,7 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
revaluation_jv = self.make_jv_for_revaluation()
|
||||
if revaluation_jv:
|
||||
frappe.msgprint(
|
||||
f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}"
|
||||
)
|
||||
frappe.msgprint(f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}")
|
||||
|
||||
return {
|
||||
"revaluation_jv": revaluation_jv.name if revaluation_jv else None,
|
||||
@@ -394,7 +392,8 @@ class ExchangeRateRevaluation(Document):
|
||||
journal_account.update(
|
||||
{
|
||||
dr_or_cr: flt(
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
abs(d.get("balance_in_account_currency")),
|
||||
d.precision("balance_in_account_currency"),
|
||||
),
|
||||
reverse_dr_or_cr: 0,
|
||||
"debit": 0,
|
||||
@@ -520,7 +519,9 @@ class ExchangeRateRevaluation(Document):
|
||||
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
|
||||
),
|
||||
"cost_center": erpnext.get_default_cost_center(self.company),
|
||||
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
|
||||
"exchange_rate": flt(
|
||||
d.get("current_exchange_rate"), d.precision("current_exchange_rate")
|
||||
),
|
||||
"reference_type": "Exchange Rate Revaluation",
|
||||
"reference_name": self.name,
|
||||
}
|
||||
@@ -598,7 +599,7 @@ def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details(
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float = None
|
||||
company, posting_date, account, party_type=None, party=None, rounding_loss_allowance: float | None = None
|
||||
):
|
||||
if not (company and posting_date):
|
||||
frappe.throw(_("Company and Posting Date is mandatory"))
|
||||
@@ -611,7 +612,7 @@ def get_account_details(
|
||||
frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))
|
||||
|
||||
account_details = {}
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
erpnext.get_company_currency(company)
|
||||
|
||||
account_details = {
|
||||
"account_currency": account_currency,
|
||||
@@ -625,24 +626,22 @@ def get_account_details(
|
||||
rounding_loss_allowance=rounding_loss_allowance,
|
||||
)
|
||||
|
||||
if account_balance and (
|
||||
account_balance[0].balance or account_balance[0].balance_in_account_currency
|
||||
):
|
||||
account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
|
||||
if account_balance and (account_balance[0].balance or account_balance[0].balance_in_account_currency):
|
||||
if account_with_new_balance := ExchangeRateRevaluation.calculate_new_account_balance(
|
||||
company, posting_date, account_balance
|
||||
)
|
||||
row = account_with_new_balance[0]
|
||||
account_details.update(
|
||||
{
|
||||
"balance_in_base_currency": row["balance_in_base_currency"],
|
||||
"balance_in_account_currency": row["balance_in_account_currency"],
|
||||
"current_exchange_rate": row["current_exchange_rate"],
|
||||
"new_exchange_rate": row["new_exchange_rate"],
|
||||
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
|
||||
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
|
||||
"zero_balance": row["zero_balance"],
|
||||
"gain_loss": row["gain_loss"],
|
||||
}
|
||||
)
|
||||
):
|
||||
row = account_with_new_balance[0]
|
||||
account_details.update(
|
||||
{
|
||||
"balance_in_base_currency": row["balance_in_base_currency"],
|
||||
"balance_in_account_currency": row["balance_in_account_currency"],
|
||||
"current_exchange_rate": row["current_exchange_rate"],
|
||||
"new_exchange_rate": row["new_exchange_rate"],
|
||||
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
|
||||
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
|
||||
"zero_balance": row["zero_balance"],
|
||||
"gain_loss": row["gain_loss"],
|
||||
}
|
||||
)
|
||||
|
||||
return account_details
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, today
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
@@ -73,9 +66,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
err.extend("accounts", accounts)
|
||||
row = err.accounts[0]
|
||||
row.new_exchange_rate = 85
|
||||
row.new_balance_in_base_currency = flt(
|
||||
row.new_exchange_rate * flt(row.balance_in_account_currency)
|
||||
)
|
||||
row.new_balance_in_base_currency = flt(row.new_exchange_rate * flt(row.balance_in_account_currency))
|
||||
row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
|
||||
err.set_total_gain_loss()
|
||||
err = err.save().submit()
|
||||
@@ -127,9 +118,9 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
pe.save().submit()
|
||||
|
||||
# Cancel the auto created gain/loss JE to simulate balance only in base currency
|
||||
je = frappe.db.get_all(
|
||||
"Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
|
||||
)[0]
|
||||
je = frappe.db.get_all("Journal Entry Account", filters={"reference_name": si.name}, pluck="parent")[
|
||||
0
|
||||
]
|
||||
frappe.get_doc("Journal Entry", je).cancel()
|
||||
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
@@ -235,9 +226,9 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(flt(acc.debit, precision), 0.0)
|
||||
self.assertEqual(flt(acc.credit, precision), 0.0)
|
||||
|
||||
row = [x for x in je.accounts if x.account == self.debtors_usd][0]
|
||||
row = next(x for x in je.accounts if x.account == self.debtors_usd)
|
||||
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||
row = [x for x in je.accounts if x.account != self.debtors_usd][0]
|
||||
row = next(x for x in je.accounts if x.account != self.debtors_usd)
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
|
||||
|
||||
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||
@@ -294,5 +285,5 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
"new_balance_in_account_currency": 100.0,
|
||||
}
|
||||
|
||||
for key, val in expected_data.items():
|
||||
for key, _val in expected_data.items():
|
||||
self.assertEqual(expected_data.get(key), account_details.get(key))
|
||||
|
||||
@@ -108,9 +108,9 @@ class FiscalYear(Document):
|
||||
|
||||
if overlap:
|
||||
frappe.throw(
|
||||
_("Year start date or end date is overlapping with {0}. To avoid please set company").format(
|
||||
existing.name
|
||||
),
|
||||
_(
|
||||
"Year start date or end date is overlapping with {0}. To avoid please set company"
|
||||
).format(existing.name),
|
||||
frappe.NameError,
|
||||
)
|
||||
|
||||
@@ -126,9 +126,9 @@ def check_duplicate_fiscal_year(doc):
|
||||
not frappe.flags.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(
|
||||
fiscal_year
|
||||
)
|
||||
_(
|
||||
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
|
||||
).format(fiscal_year)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -107,13 +107,18 @@ class GLEntry(Document):
|
||||
]:
|
||||
# Update outstanding amt on against voucher
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
self.against_voucher_type
|
||||
in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.against_voucher
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_outstanding_amt(
|
||||
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
|
||||
self.account,
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.against_voucher_type,
|
||||
self.against_voucher,
|
||||
)
|
||||
|
||||
def check_mandatory(self):
|
||||
@@ -179,12 +184,13 @@ class GLEntry(Document):
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
_(
|
||||
"Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}."
|
||||
).format(dimension.label, self.account)
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -192,12 +198,13 @@ class GLEntry(Document):
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
and not self.is_cancelled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
_(
|
||||
"Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}."
|
||||
).format(dimension.label, self.account)
|
||||
)
|
||||
|
||||
def check_pl_account(self):
|
||||
@@ -245,9 +252,7 @@ class GLEntry(Document):
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
is_group, company = frappe.get_cached_value(
|
||||
"Cost Center", self.cost_center, ["is_group", "company"]
|
||||
)
|
||||
is_group, company = frappe.get_cached_value("Cost Center", self.cost_center, ["is_group", "company"])
|
||||
|
||||
if company != self.company:
|
||||
frappe.throw(
|
||||
@@ -316,7 +321,7 @@ def update_outstanding_amt(
|
||||
account, party_type, party, against_voucher_type, against_voucher, on_cancel=False
|
||||
):
|
||||
if party_type and party:
|
||||
party_condition = " and party_type={0} and party={1}".format(
|
||||
party_condition = " and party_type={} and party={}".format(
|
||||
frappe.db.escape(party_type), frappe.db.escape(party)
|
||||
)
|
||||
else:
|
||||
@@ -324,23 +329,19 @@ def update_outstanding_amt(
|
||||
|
||||
if against_voucher_type == "Sales Invoice":
|
||||
party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to")
|
||||
account_condition = "and account in ({0}, {1})".format(
|
||||
frappe.db.escape(account), frappe.db.escape(party_account)
|
||||
)
|
||||
account_condition = f"and account in ({frappe.db.escape(account)}, {frappe.db.escape(party_account)})"
|
||||
else:
|
||||
account_condition = " and account = {0}".format(frappe.db.escape(account))
|
||||
account_condition = f" and account = {frappe.db.escape(account)}"
|
||||
|
||||
# get final outstanding amt
|
||||
bal = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabGL Entry`
|
||||
where against_voucher_type=%s and against_voucher=%s
|
||||
and voucher_type != 'Invoice Discounting'
|
||||
{0} {1}""".format(
|
||||
party_condition, account_condition
|
||||
),
|
||||
{party_condition} {account_condition}""",
|
||||
(against_voucher_type, against_voucher),
|
||||
)[0][0]
|
||||
or 0.0
|
||||
@@ -351,12 +352,10 @@ def update_outstanding_amt(
|
||||
elif against_voucher_type == "Journal Entry":
|
||||
against_voucher_amount = flt(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
|
||||
and account = %s and (against_voucher is null or against_voucher='') {0}""".format(
|
||||
party_condition
|
||||
),
|
||||
and account = %s and (against_voucher is null or against_voucher='') {party_condition}""",
|
||||
(against_voucher, account),
|
||||
)[0][0]
|
||||
)
|
||||
@@ -375,7 +374,9 @@ def update_outstanding_amt(
|
||||
# Validation : Outstanding can not be negative for JV
|
||||
if bal < 0 and not on_cancel:
|
||||
frappe.throw(
|
||||
_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))
|
||||
_("Outstanding for {0} cannot be less than zero ({1})").format(
|
||||
against_voucher, fmt_money(bal)
|
||||
)
|
||||
)
|
||||
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
@@ -391,9 +392,7 @@ def update_outstanding_amt(
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
)
|
||||
frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", None, "frozen_accounts_modifier")
|
||||
|
||||
if not frozen_accounts_modifier:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
@@ -448,7 +447,7 @@ def rename_temporarily_named_docs(doctype):
|
||||
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
|
||||
newname = doc.name
|
||||
frappe.db.sql(
|
||||
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
|
||||
f"UPDATE `tab{doctype}` SET name = %s, to_rename = 0 where name = %s",
|
||||
(newname, oldname),
|
||||
auto_commit=True,
|
||||
)
|
||||
|
||||
@@ -14,9 +14,7 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
|
||||
class TestGLEntry(unittest.TestCase):
|
||||
def test_round_off_entry(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC")
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC"
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC")
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Account Cost for Goods Sold - _TC",
|
||||
@@ -73,7 +71,9 @@ class TestGLEntry(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries))
|
||||
|
||||
self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries)))
|
||||
self.assertTrue(
|
||||
all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries, strict=False))
|
||||
)
|
||||
|
||||
new_naming_series_current_value = frappe.db.sql(
|
||||
"SELECT current from tabSeries where name = %s", naming_series
|
||||
|
||||
@@ -83,9 +83,7 @@ class InvoiceDiscounting(AccountsController):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}"
|
||||
).format(
|
||||
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)
|
||||
)
|
||||
).format(record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice))
|
||||
)
|
||||
|
||||
def calculate_total_amount(self):
|
||||
@@ -105,7 +103,9 @@ class InvoiceDiscounting(AccountsController):
|
||||
self.status = status
|
||||
self.db_set("status", status)
|
||||
for d in self.invoices:
|
||||
frappe.get_doc("Sales Invoice", d.sales_invoice).set_status(update=True, update_modified=False)
|
||||
frappe.get_doc("Sales Invoice", d.sales_invoice).set_status(
|
||||
update=True, update_modified=False
|
||||
)
|
||||
else:
|
||||
self.status = "Draft"
|
||||
if self.docstatus == 1:
|
||||
|
||||
@@ -75,8 +75,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
gle = get_gl_entries("Invoice Discounting", inv_disc.name)
|
||||
|
||||
expected_gle = {inv.debit_to: [0.0, 200], self.ar_credit: [200, 0.0]}
|
||||
for i, gle in enumerate(gle):
|
||||
self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account))
|
||||
for _i, gle_value in enumerate(gle):
|
||||
self.assertEqual([gle_value.debit, gle_value.credit], expected_gle.get(gle_value.account))
|
||||
|
||||
def test_loan_on_submit(self):
|
||||
inv = create_sales_invoice(rate=300)
|
||||
@@ -92,9 +92,7 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
period=60,
|
||||
)
|
||||
self.assertEqual(inv_disc.status, "Sanctioned")
|
||||
self.assertEqual(
|
||||
inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)
|
||||
)
|
||||
self.assertEqual(inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period))
|
||||
|
||||
def test_on_disbursed(self):
|
||||
inv = create_sales_invoice(rate=500)
|
||||
@@ -262,13 +260,9 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
je_on_payment.submit()
|
||||
|
||||
self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted)
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
|
||||
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
|
||||
|
||||
inv.reload()
|
||||
self.assertEqual(inv.outstanding_amount, 0)
|
||||
@@ -304,13 +298,9 @@ class TestInvoiceDiscounting(unittest.TestCase):
|
||||
je_on_payment.submit()
|
||||
|
||||
self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid)
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
|
||||
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
|
||||
self.assertEqual(
|
||||
je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
|
||||
)
|
||||
self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
|
||||
|
||||
inv.reload()
|
||||
self.assertEqual(inv.outstanding_amount, 0)
|
||||
|
||||
@@ -32,7 +32,7 @@ class ItemTaxTemplate(Document):
|
||||
def autoname(self):
|
||||
if self.company and self.title:
|
||||
abbr = frappe.get_cached_value("Company", self.company, "abbr")
|
||||
self.name = "{0} - {1}".format(self.title, abbr)
|
||||
self.name = f"{self.title} - {abbr}"
|
||||
|
||||
def validate_tax_accounts(self):
|
||||
"""Check whether Tax Rate is not entered twice for same Tax Type"""
|
||||
|
||||
@@ -196,7 +196,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
!(frm.doc.accounts || []).length ||
|
||||
((frm.doc.accounts || []).length === 1 && !frm.doc.accounts[0].account)
|
||||
) {
|
||||
if (in_list(["Bank Entry", "Cash Entry"], frm.doc.voucher_type)) {
|
||||
if (["Bank Entry", "Cash Entry"].includes(frm.doc.voucher_type)) {
|
||||
return frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account",
|
||||
@@ -308,7 +308,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
filters: [[jvd.reference_type, "docstatus", "=", 1]],
|
||||
};
|
||||
|
||||
if (in_list(["Sales Invoice", "Purchase Invoice"], jvd.reference_type)) {
|
||||
if (["Sales Invoice", "Purchase Invoice"].includes(jvd.reference_type)) {
|
||||
out.filters.push([jvd.reference_type, "outstanding_amount", "!=", 0]);
|
||||
// Filter by cost center
|
||||
if (jvd.cost_center) {
|
||||
@@ -320,7 +320,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]);
|
||||
}
|
||||
|
||||
if (in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) {
|
||||
if (["Sales Order", "Purchase Order"].includes(jvd.reference_type)) {
|
||||
// party_type and party mandatory
|
||||
frappe.model.validate_missing(jvd, "party_type");
|
||||
frappe.model.validate_missing(jvd, "party");
|
||||
@@ -453,7 +453,10 @@ frappe.ui.form.on("Journal Entry Account", {
|
||||
}
|
||||
},
|
||||
cost_center: function (frm, dt, dn) {
|
||||
erpnext.journal_entry.set_account_details(frm, dt, dn);
|
||||
// Don't reset for Gain/Loss type journals, as it will make Debit and Credit values '0'
|
||||
if (frm.doc.voucher_type != "Exchange Gain Or Loss") {
|
||||
erpnext.journal_entry.set_account_details(frm, dt, dn);
|
||||
}
|
||||
},
|
||||
|
||||
account: function (frm, dt, dn) {
|
||||
|
||||
@@ -112,7 +112,7 @@ class JournalEntry(AccountsController):
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(JournalEntry, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self):
|
||||
if self.voucher_type == "Opening Entry":
|
||||
@@ -191,7 +191,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super(JournalEntry, self).on_cancel()
|
||||
super().on_cancel()
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
@@ -226,10 +226,7 @@ class JournalEntry(AccountsController):
|
||||
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
|
||||
|
||||
def validate_inter_company_accounts(self):
|
||||
if (
|
||||
self.voucher_type == "Inter Company Journal Entry"
|
||||
and self.inter_company_journal_entry_reference
|
||||
):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference)
|
||||
account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency")
|
||||
@@ -375,10 +372,7 @@ class JournalEntry(AccountsController):
|
||||
asset.set_status()
|
||||
|
||||
def update_inter_company_jv(self):
|
||||
if (
|
||||
self.voucher_type == "Inter Company Journal Entry"
|
||||
and self.inter_company_journal_entry_reference
|
||||
):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
frappe.db.set_value(
|
||||
"Journal Entry",
|
||||
self.inter_company_journal_entry_reference,
|
||||
@@ -406,17 +400,25 @@ class JournalEntry(AccountsController):
|
||||
if d.account == inv_disc_doc.short_term_loan and d.reference_name == inv_disc:
|
||||
if self.docstatus == 1:
|
||||
if d.credit > 0:
|
||||
_validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Sanctioned", d.idx)
|
||||
_validate_invoice_discounting_status(
|
||||
inv_disc, inv_disc_doc.status, "Sanctioned", d.idx
|
||||
)
|
||||
status = "Disbursed"
|
||||
elif d.debit > 0:
|
||||
_validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Disbursed", d.idx)
|
||||
_validate_invoice_discounting_status(
|
||||
inv_disc, inv_disc_doc.status, "Disbursed", d.idx
|
||||
)
|
||||
status = "Settled"
|
||||
else:
|
||||
if d.credit > 0:
|
||||
_validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Disbursed", d.idx)
|
||||
_validate_invoice_discounting_status(
|
||||
inv_disc, inv_disc_doc.status, "Disbursed", d.idx
|
||||
)
|
||||
status = "Sanctioned"
|
||||
elif d.debit > 0:
|
||||
_validate_invoice_discounting_status(inv_disc, inv_disc_doc.status, "Settled", d.idx)
|
||||
_validate_invoice_discounting_status(
|
||||
inv_disc, inv_disc_doc.status, "Settled", d.idx
|
||||
)
|
||||
status = "Disbursed"
|
||||
break
|
||||
if status:
|
||||
@@ -485,10 +487,7 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
self.voucher_type == "Inter Company Journal Entry"
|
||||
and self.inter_company_journal_entry_reference
|
||||
):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
frappe.db.set_value(
|
||||
"Journal Entry",
|
||||
self.inter_company_journal_entry_reference,
|
||||
@@ -510,9 +509,9 @@ class JournalEntry(AccountsController):
|
||||
if account_type in ["Receivable", "Payable"]:
|
||||
if not (d.party_type and d.party):
|
||||
frappe.throw(
|
||||
_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(
|
||||
d.idx, d.account
|
||||
)
|
||||
_(
|
||||
"Row {0}: Party Type and Party is required for Receivable / Payable account {1}"
|
||||
).format(d.idx, d.account)
|
||||
)
|
||||
elif (
|
||||
d.party_type
|
||||
@@ -577,16 +576,18 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def system_generated_gain_loss(self):
|
||||
return (
|
||||
self.voucher_type == "Exchange Gain Or Loss"
|
||||
and self.multi_currency
|
||||
and self.is_system_generated
|
||||
self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency and self.is_system_generated
|
||||
)
|
||||
|
||||
def validate_against_jv(self):
|
||||
for d in self.get("accounts"):
|
||||
if d.reference_type == "Journal Entry":
|
||||
account_root_type = frappe.get_cached_value("Account", d.account, "root_type")
|
||||
if account_root_type == "Asset" and flt(d.debit) > 0 and not self.system_generated_gain_loss():
|
||||
if (
|
||||
account_root_type == "Asset"
|
||||
and flt(d.debit) > 0
|
||||
and not self.system_generated_gain_loss()
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: For {1}, you can select reference document only if account gets credited"
|
||||
@@ -668,11 +669,13 @@ class JournalEntry(AccountsController):
|
||||
|
||||
if d.reference_type == "Purchase Order" and flt(d.credit) > 0:
|
||||
frappe.throw(
|
||||
_("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type)
|
||||
_("Row {0}: Credit entry can not be linked with a {1}").format(
|
||||
d.idx, d.reference_type
|
||||
)
|
||||
)
|
||||
|
||||
# set totals
|
||||
if not d.reference_name in self.reference_totals:
|
||||
if d.reference_name not in self.reference_totals:
|
||||
self.reference_totals[d.reference_name] = 0.0
|
||||
|
||||
if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"):
|
||||
@@ -690,7 +693,10 @@ class JournalEntry(AccountsController):
|
||||
|
||||
# check if party and account match
|
||||
if d.reference_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense") and d.reference_detail_no:
|
||||
if (
|
||||
self.voucher_type in ("Deferred Revenue", "Deferred Expense")
|
||||
and d.reference_detail_no
|
||||
):
|
||||
debit_or_credit = "Debit" if d.debit else "Credit"
|
||||
party_account = get_deferred_booking_accounts(
|
||||
d.reference_type, d.reference_detail_no, debit_or_credit
|
||||
@@ -699,7 +705,8 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
if d.reference_type == "Sales Invoice":
|
||||
party_account = (
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name)
|
||||
or against_voucher[1]
|
||||
)
|
||||
else:
|
||||
party_account = against_voucher[1]
|
||||
@@ -823,7 +830,9 @@ class JournalEntry(AccountsController):
|
||||
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
|
||||
if self.difference:
|
||||
frappe.throw(
|
||||
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
|
||||
_("Total Debit must be equal to Total Credit. The difference is {0}").format(
|
||||
self.difference
|
||||
)
|
||||
)
|
||||
|
||||
def set_total_debit_credit(self):
|
||||
@@ -887,7 +896,6 @@ class JournalEntry(AccountsController):
|
||||
and self.posting_date
|
||||
)
|
||||
):
|
||||
|
||||
ignore_exchange_rate = False
|
||||
if self.get("flags") and self.flags.get("ignore_exchange_rate"):
|
||||
ignore_exchange_rate = True
|
||||
@@ -1133,27 +1141,21 @@ class JournalEntry(AccountsController):
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def get_values(self):
|
||||
cond = (
|
||||
" and outstanding_amount <= {0}".format(self.write_off_amount)
|
||||
if flt(self.write_off_amount) > 0
|
||||
else ""
|
||||
)
|
||||
cond = f" and outstanding_amount <= {self.write_off_amount}" if flt(self.write_off_amount) > 0 else ""
|
||||
|
||||
if self.write_off_based_on == "Accounts Receivable":
|
||||
return frappe.db.sql(
|
||||
"""select name, debit_to as account, customer as party, outstanding_amount
|
||||
from `tabSales Invoice` where docstatus = 1 and company = %s
|
||||
and outstanding_amount > 0 %s"""
|
||||
% ("%s", cond),
|
||||
from `tabSales Invoice` where docstatus = 1 and company = {}
|
||||
and outstanding_amount > 0 {}""".format("%s", cond),
|
||||
self.company,
|
||||
as_dict=True,
|
||||
)
|
||||
elif self.write_off_based_on == "Accounts Payable":
|
||||
return frappe.db.sql(
|
||||
"""select name, credit_to as account, supplier as party, outstanding_amount
|
||||
from `tabPurchase Invoice` where docstatus = 1 and company = %s
|
||||
and outstanding_amount > 0 %s"""
|
||||
% ("%s", cond),
|
||||
from `tabPurchase Invoice` where docstatus = 1 and company = {}
|
||||
and outstanding_amount > 0 {}""".format("%s", cond),
|
||||
self.company,
|
||||
as_dict=True,
|
||||
)
|
||||
@@ -1262,7 +1264,7 @@ def get_payment_entry_against_order(
|
||||
"amount_field_bank": amount_field_bank,
|
||||
"amount": amount,
|
||||
"debit_in_account_currency": debit_in_account_currency,
|
||||
"remarks": "Advance Payment received against {0} {1}".format(dt, dn),
|
||||
"remarks": f"Advance Payment received against {dt} {dn}",
|
||||
"is_advance": "Yes",
|
||||
"bank_account": bank_account,
|
||||
"journal_entry": journal_entry,
|
||||
@@ -1301,7 +1303,7 @@ def get_payment_entry_against_invoice(
|
||||
"amount_field_bank": amount_field_bank,
|
||||
"amount": amount if amount else abs(ref_doc.outstanding_amount),
|
||||
"debit_in_account_currency": debit_in_account_currency,
|
||||
"remarks": "Payment received against {0} {1}. {2}".format(dt, dn, ref_doc.remarks),
|
||||
"remarks": f"Payment received against {dt} {dn}. {ref_doc.remarks}",
|
||||
"is_advance": "No",
|
||||
"bank_account": bank_account,
|
||||
"journal_entry": journal_entry,
|
||||
@@ -1327,9 +1329,7 @@ def get_payment_entry(ref_doc, args):
|
||||
)
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.update(
|
||||
{"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")}
|
||||
)
|
||||
je.update({"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")})
|
||||
|
||||
party_row = je.append(
|
||||
"accounts",
|
||||
@@ -1352,9 +1352,7 @@ def get_payment_entry(ref_doc, args):
|
||||
bank_row = je.append("accounts")
|
||||
|
||||
# Make it bank_details
|
||||
bank_account = get_default_bank_cash_account(
|
||||
ref_doc.company, "Bank", account=args.get("bank_account")
|
||||
)
|
||||
bank_account = get_default_bank_cash_account(ref_doc.company, "Bank", account=args.get("bank_account"))
|
||||
if bank_account:
|
||||
bank_row.update(bank_account)
|
||||
# Modified to include the posting date for which the exchange rate is required.
|
||||
@@ -1394,7 +1392,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
return []
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
SELECT jv.name, jv.posting_date, jv.user_remark
|
||||
FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail
|
||||
WHERE jv_detail.parent = jv.name
|
||||
@@ -1405,16 +1403,14 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
OR jv_detail.reference_type = ''
|
||||
)
|
||||
AND jv.docstatus = 1
|
||||
AND jv.`{0}` LIKE %(txt)s
|
||||
AND jv.`{searchfield}` LIKE %(txt)s
|
||||
ORDER BY jv.name DESC
|
||||
LIMIT %(limit)s offset %(offset)s
|
||||
""".format(
|
||||
searchfield
|
||||
),
|
||||
""",
|
||||
dict(
|
||||
account=filters.get("account"),
|
||||
party=cstr(filters.get("party")),
|
||||
txt="%{0}%".format(txt),
|
||||
txt=f"%{txt}%",
|
||||
offset=start,
|
||||
limit=page_len,
|
||||
),
|
||||
@@ -1436,19 +1432,15 @@ def get_outstanding(args):
|
||||
condition = " and party=%(party)s" if args.get("party") else ""
|
||||
|
||||
against_jv_amount = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
|
||||
from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {0}
|
||||
and (reference_type is null or reference_type = '')""".format(
|
||||
condition
|
||||
),
|
||||
from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {condition}
|
||||
and (reference_type is null or reference_type = '')""",
|
||||
args,
|
||||
)
|
||||
|
||||
against_jv_amount = flt(against_jv_amount[0][0]) if against_jv_amount else 0
|
||||
amount_field = (
|
||||
"credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency"
|
||||
)
|
||||
amount_field = "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency"
|
||||
return {amount_field: abs(against_jv_amount)}
|
||||
elif args.get("doctype") in ("Sales Invoice", "Purchase Invoice"):
|
||||
party_type = "Customer" if args.get("doctype") == "Sales Invoice" else "Supplier"
|
||||
@@ -1461,9 +1453,7 @@ def get_outstanding(args):
|
||||
|
||||
due_date = invoice.get("due_date")
|
||||
|
||||
exchange_rate = (
|
||||
invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
|
||||
)
|
||||
exchange_rate = invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
|
||||
|
||||
if args["doctype"] == "Sales Invoice":
|
||||
amount_field = (
|
||||
@@ -1501,9 +1491,7 @@ def get_party_account_and_currency(company, party_type, party):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_details_and_party_type(
|
||||
account, date, company, debit=None, credit=None, exchange_rate=None
|
||||
):
|
||||
def get_account_details_and_party_type(account, date, company, debit=None, credit=None, exchange_rate=None):
|
||||
"""Returns dict of account details and party type to be set in Journal Entry on selection of account."""
|
||||
if not frappe.has_permission("Account"):
|
||||
frappe.msgprint(_("No Permission"), raise_exception=1)
|
||||
|
||||
@@ -69,10 +69,8 @@ class TestJournalEntry(unittest.TestCase):
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.sql(
|
||||
"""select name from `tabJournal Entry Account`
|
||||
where reference_type = %s and reference_name = %s and {0}=400""".format(
|
||||
dr_or_cr
|
||||
),
|
||||
f"""select name from `tabJournal Entry Account`
|
||||
where reference_type = %s and reference_name = %s and {dr_or_cr}=400""",
|
||||
(submitted_voucher.doctype, submitted_voucher.name),
|
||||
)
|
||||
)
|
||||
@@ -84,9 +82,8 @@ class TestJournalEntry(unittest.TestCase):
|
||||
def advance_paid_testcase(self, base_jv, test_voucher, dr_or_cr):
|
||||
# Test advance paid field
|
||||
advance_paid = frappe.db.sql(
|
||||
"""select advance_paid from `tab%s`
|
||||
where name=%s"""
|
||||
% (test_voucher.doctype, "%s"),
|
||||
"""select advance_paid from `tab{}`
|
||||
where name={}""".format(test_voucher.doctype, "%s"),
|
||||
(test_voucher.name),
|
||||
)
|
||||
payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr)
|
||||
@@ -159,9 +156,7 @@ class TestJournalEntry(unittest.TestCase):
|
||||
jv.cancel()
|
||||
|
||||
def test_multi_currency(self):
|
||||
jv = make_journal_entry(
|
||||
"_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False
|
||||
)
|
||||
jv = make_journal_entry("_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False)
|
||||
|
||||
jv.get("accounts")[1].credit_in_account_currency = 5000
|
||||
jv.submit()
|
||||
@@ -477,9 +472,7 @@ class TestJournalEntry(unittest.TestCase):
|
||||
query = query.select(gl[field])
|
||||
|
||||
query = query.where(
|
||||
(gl.voucher_type == "Journal Entry")
|
||||
& (gl.voucher_no == self.voucher_no)
|
||||
& (gl.is_cancelled == 0)
|
||||
(gl.voucher_type == "Journal Entry") & (gl.voucher_no == self.voucher_no) & (gl.is_cancelled == 0)
|
||||
).orderby(gl.account)
|
||||
|
||||
gl_entries = query.run(as_dict=True)
|
||||
|
||||
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
0
erpnext/accounts/doctype/ledger_health/__init__.py
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
8
erpnext/accounts/doctype/ledger_health/ledger_health.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Ledger Health", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
70
erpnext/accounts/doctype/ledger_health/ledger_health.json
Normal file
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2024-03-26 17:01:47.443986",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"checked_on",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Voucher No"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "debit_credit_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Debit-Credit mismatch"
|
||||
},
|
||||
{
|
||||
"fieldname": "checked_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Checked On"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "general_and_payment_ledger_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "General and Payment Ledger mismatch"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-04-09 11:16:07.044484",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
25
erpnext/accounts/doctype/ledger_health/ledger_health.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealth(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
checked_on: DF.Datetime | None
|
||||
debit_credit_mismatch: DF.Check
|
||||
general_and_payment_ledger_mismatch: DF.Check
|
||||
name: DF.Int | None
|
||||
voucher_no: DF.Data | None
|
||||
voucher_type: DF.Data | None
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
109
erpnext/accounts/doctype/ledger_health/test_ledger_health.py
Normal file
@@ -0,0 +1,109 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import run_ledger_health_checks
|
||||
|
||||
|
||||
class TestLedgerHealth(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.configure_monitoring_tool()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def configure_monitoring_tool(self):
|
||||
monitor_settings = frappe.get_doc("Ledger Health Monitor")
|
||||
monitor_settings.enable_health_monitor = True
|
||||
monitor_settings.enable_for_last_x_days = 60
|
||||
monitor_settings.debit_credit_mismatch = True
|
||||
monitor_settings.general_and_payment_ledger_mismatch = True
|
||||
exists = [x for x in monitor_settings.companies if x.company == self.company]
|
||||
if not exists:
|
||||
monitor_settings.append("companies", {"company": self.company})
|
||||
monitor_settings.save()
|
||||
|
||||
def clear_old_entries(self):
|
||||
super().clear_old_entries()
|
||||
lh = qb.DocType("Ledger Health")
|
||||
qb.from_(lh).delete().run()
|
||||
|
||||
def create_journal(self):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = self.company
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.posting_date = nowdate()
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 10000,
|
||||
},
|
||||
)
|
||||
je.append("accounts", {"account": self.income_account, "credit_in_account_currency": 10000})
|
||||
je.save().submit()
|
||||
self.je = je
|
||||
|
||||
def test_debit_credit_mismatch(self):
|
||||
self.create_journal()
|
||||
|
||||
# manually cause debit-credit mismatch
|
||||
gle = frappe.db.get_all(
|
||||
"GL Entry", filters={"voucher_no": self.je.name, "account": self.income_account}
|
||||
)[0]
|
||||
frappe.db.set_value("GL Entry", gle.name, "credit", 8000)
|
||||
|
||||
run_ledger_health_checks()
|
||||
expected = {
|
||||
"voucher_type": self.je.doctype,
|
||||
"voucher_no": self.je.name,
|
||||
"debit_credit_mismatch": True,
|
||||
"general_and_payment_ledger_mismatch": False,
|
||||
}
|
||||
actual = frappe.db.get_all(
|
||||
"Ledger Health",
|
||||
fields=[
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual[0])
|
||||
|
||||
def test_gl_and_pl_mismatch(self):
|
||||
self.create_journal()
|
||||
|
||||
# manually cause GL and PL discrepancy
|
||||
ple = frappe.db.get_all("Payment Ledger Entry", filters={"voucher_no": self.je.name})[0]
|
||||
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", 11000)
|
||||
|
||||
run_ledger_health_checks()
|
||||
expected = {
|
||||
"voucher_type": self.je.doctype,
|
||||
"voucher_no": self.je.name,
|
||||
"debit_credit_mismatch": False,
|
||||
"general_and_payment_ledger_mismatch": True,
|
||||
}
|
||||
actual = frappe.db.get_all(
|
||||
"Ledger Health",
|
||||
fields=[
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
],
|
||||
)
|
||||
self.assertEqual(len(actual), 1)
|
||||
self.assertEqual(expected, actual[0])
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Ledger Health Monitor", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-03-27 09:38:07.427997",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable_health_monitor",
|
||||
"monitor_section",
|
||||
"monitor_for_last_x_days",
|
||||
"debit_credit_mismatch",
|
||||
"general_and_payment_ledger_mismatch",
|
||||
"section_break_xdsp",
|
||||
"companies"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_health_monitor",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Health Monitor"
|
||||
},
|
||||
{
|
||||
"fieldname": "monitor_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Configuration"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "debit_credit_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Debit-Credit Mismatch"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "general_and_payment_ledger_mismatch",
|
||||
"fieldtype": "Check",
|
||||
"label": "Discrepancy between General and Payment Ledger"
|
||||
},
|
||||
{
|
||||
"default": "60",
|
||||
"fieldname": "monitor_for_last_x_days",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Monitor for Last 'X' days",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xdsp",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Companies"
|
||||
},
|
||||
{
|
||||
"fieldname": "companies",
|
||||
"fieldtype": "Table",
|
||||
"options": "Ledger Health Monitor Company"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 10:14:16.511681",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health Monitor",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealthMonitor(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
from erpnext.accounts.doctype.ledger_health_monitor_company.ledger_health_monitor_company import (
|
||||
LedgerHealthMonitorCompany,
|
||||
)
|
||||
|
||||
companies: DF.Table[LedgerHealthMonitorCompany]
|
||||
debit_credit_mismatch: DF.Check
|
||||
enable_health_monitor: DF.Check
|
||||
general_and_payment_ledger_mismatch: DF.Check
|
||||
monitor_for_last_x_days: DF.Int
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLedgerHealthMonitor(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-03-27 10:04:45.727054",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 10:06:22.806155",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Ledger Health Monitor Company",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LedgerHealthMonitorCompany(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
@@ -83,7 +83,10 @@ class TestLedgerMerge(unittest.TestCase):
|
||||
"account": "Indirect Income - _TC",
|
||||
"merge_accounts": [
|
||||
{"account": "Indirect Test Income - _TC", "account_name": "Indirect Test Income"},
|
||||
{"account": "Administrative Test Income - _TC", "account_name": "Administrative Test Income"},
|
||||
{
|
||||
"account": "Administrative Test Income - _TC",
|
||||
"account_name": "Administrative Test Income",
|
||||
},
|
||||
],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
@@ -52,13 +52,11 @@ def get_loyalty_details(
|
||||
condition += " and expiry_date>='%s' " % expiry_date
|
||||
|
||||
loyalty_point_details = frappe.db.sql(
|
||||
"""select sum(loyalty_points) as loyalty_points,
|
||||
f"""select sum(loyalty_points) as loyalty_points,
|
||||
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
|
||||
where customer=%s and loyalty_program=%s and posting_date <= %s
|
||||
{condition}
|
||||
group by customer""".format(
|
||||
condition=condition
|
||||
),
|
||||
group by customer""",
|
||||
(customer, loyalty_program, expiry_date),
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -79,9 +77,7 @@ def get_loyalty_program_details_with_points(
|
||||
include_expired_entry=False,
|
||||
current_transaction_amount=0,
|
||||
):
|
||||
lp_details = get_loyalty_program_details(
|
||||
customer, loyalty_program, company=company, silent=silent
|
||||
)
|
||||
lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
|
||||
loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
|
||||
lp_details.update(
|
||||
get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)
|
||||
|
||||
@@ -19,9 +19,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
create_records()
|
||||
|
||||
def test_loyalty_points_earned_single_tier(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||
# create a new sales invoice
|
||||
si_original = create_sales_invoice_record()
|
||||
si_original.insert()
|
||||
@@ -69,9 +67,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
d.cancel()
|
||||
|
||||
def test_loyalty_points_earned_multiple_tier(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty")
|
||||
# assign multiple tier program to the customer
|
||||
customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
|
||||
customer.loyalty_program = frappe.get_doc(
|
||||
@@ -128,9 +124,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
|
||||
def test_cancel_sales_invoice(self):
|
||||
"""cancelling the sales invoice should cancel the earned points"""
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||
# create a new sales invoice
|
||||
si = create_sales_invoice_record()
|
||||
si.insert()
|
||||
@@ -140,7 +134,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
"Loyalty Point Entry",
|
||||
{"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer},
|
||||
)
|
||||
self.assertEqual(True, not (lpe is None))
|
||||
self.assertEqual(True, lpe is not None)
|
||||
|
||||
# cancelling sales invoice
|
||||
si.cancel()
|
||||
@@ -148,9 +142,7 @@ class TestLoyaltyProgram(unittest.TestCase):
|
||||
self.assertEqual(True, (lpe is None))
|
||||
|
||||
def test_sales_invoice_return(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
|
||||
)
|
||||
frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
|
||||
# create a new sales invoice
|
||||
si_original = create_sales_invoice_record(2)
|
||||
si_original.conversion_rate = flt(1)
|
||||
@@ -346,9 +338,7 @@ def create_records():
|
||||
).insert()
|
||||
|
||||
# create item price
|
||||
if not frappe.db.exists(
|
||||
"Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}
|
||||
):
|
||||
if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
|
||||
@@ -54,9 +54,7 @@ class MonthlyDistribution(Document):
|
||||
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
|
||||
|
||||
if flt(total, 2) != 100.0:
|
||||
frappe.throw(
|
||||
_("Percentage Allocation should be equal to 100%") + " ({0}%)".format(str(flt(total, 2)))
|
||||
)
|
||||
frappe.throw(_("Percentage Allocation should be equal to 100%") + f" ({flt(total, 2)!s}%)")
|
||||
|
||||
|
||||
def get_periodwise_distribution_data(distribution_id, period_list, periodicity):
|
||||
|
||||
@@ -83,9 +83,7 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
company = "_Test Opening Invoice Company"
|
||||
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||
|
||||
old_default_receivable_account = frappe.db.get_value(
|
||||
"Company", company, "default_receivable_account"
|
||||
)
|
||||
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", "")
|
||||
|
||||
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
|
||||
@@ -121,9 +119,7 @@ class TestOpeningInvoiceCreationTool(FrappeTestCase):
|
||||
self.assertTrue(error_log)
|
||||
|
||||
# teardown
|
||||
frappe.db.set_value(
|
||||
"Company", company, "default_receivable_account", old_default_receivable_account
|
||||
)
|
||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||
|
||||
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
@@ -169,7 +165,7 @@ def get_opening_invoice_creation_dict(**args):
|
||||
{
|
||||
"qty": 1.0,
|
||||
"outstanding_amount": 300,
|
||||
"party": args.get("party_1") or "_Test {0}".format(party),
|
||||
"party": args.get("party_1") or f"_Test {party}",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
@@ -179,7 +175,7 @@ def get_opening_invoice_creation_dict(**args):
|
||||
{
|
||||
"qty": 2.0,
|
||||
"outstanding_amount": 250,
|
||||
"party": args.get("party_2") or "_Test {0} 1".format(party),
|
||||
"party": args.get("party_2") or f"_Test {party} 1",
|
||||
"item_name": "Opening Item",
|
||||
"due_date": "2016-09-10",
|
||||
"posting_date": "2016-09-05",
|
||||
|
||||
@@ -38,7 +38,10 @@ class PartyLink(Document):
|
||||
if existing_party_link:
|
||||
frappe.throw(
|
||||
_("{} {} is already linked with {} {}").format(
|
||||
self.primary_role, bold(self.primary_party), self.secondary_role, bold(self.secondary_party)
|
||||
self.primary_role,
|
||||
bold(self.primary_party),
|
||||
self.secondary_role,
|
||||
bold(self.secondary_party),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_query("paid_from", function () {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type)
|
||||
var account_types = ["Pay", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
return {
|
||||
@@ -87,7 +87,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_query("paid_to", function () {
|
||||
frm.events.validate_company(frm);
|
||||
|
||||
var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type)
|
||||
var account_types = ["Receive", "Internal Transfer"].includes(frm.doc.payment_type)
|
||||
? ["Bank", "Cash"]
|
||||
: [frappe.boot.party_account_types[frm.doc.party_type]];
|
||||
return {
|
||||
@@ -134,7 +134,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_query("payment_term", "references", function (frm, cdt, cdn) {
|
||||
const child = locals[cdt][cdn];
|
||||
if (
|
||||
in_list(["Purchase Invoice", "Sales Invoice"], child.reference_doctype) &&
|
||||
["Purchase Invoice", "Sales Invoice"].includes(child.reference_doctype) &&
|
||||
child.reference_name
|
||||
) {
|
||||
return {
|
||||
@@ -395,10 +395,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
};
|
||||
} else if (frm.doc.party_type == "Customer") {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.customer_query",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@@ -474,6 +470,9 @@ frappe.ui.form.on("Payment Entry", {
|
||||
() => frm.events.set_dynamic_labels(frm),
|
||||
() => {
|
||||
frm.set_party_account_based_on_party = false;
|
||||
if (r.message.party_bank_account) {
|
||||
frm.set_value("party_bank_account", r.message.party_bank_account);
|
||||
}
|
||||
if (r.message.bank_account) {
|
||||
frm.set_value("bank_account", r.message.bank_account);
|
||||
}
|
||||
@@ -627,7 +626,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from) {
|
||||
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
|
||||
if (["Internal Transfer", "Pay"].includes(frm.doc.payment_type)) {
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
@@ -1046,7 +1045,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
|
||||
var allocated_positive_outstanding = paid_amount + allocated_negative_outstanding;
|
||||
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
|
||||
} else if (["Customer", "Supplier"].includes(frm.doc.party_type)) {
|
||||
total_negative_outstanding = flt(total_negative_outstanding, precision("outstanding_amount"));
|
||||
if (paid_amount > total_negative_outstanding) {
|
||||
if (total_negative_outstanding == 0) {
|
||||
@@ -1217,7 +1216,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (
|
||||
frm.doc.party_type == "Customer" &&
|
||||
!in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype)
|
||||
!["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"].includes(row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
|
||||
frappe.msgprint(
|
||||
@@ -1231,7 +1230,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
if (
|
||||
frm.doc.party_type == "Supplier" &&
|
||||
!in_list(["Purchase Order", "Purchase Invoice", "Journal Entry"], row.reference_doctype)
|
||||
!["Purchase Order", "Purchase Invoice", "Journal Entry"].includes(row.reference_doctype)
|
||||
) {
|
||||
frappe.model.set_value(row.doctype, row.name, "against_voucher_type", null);
|
||||
frappe.msgprint(
|
||||
@@ -1327,7 +1326,7 @@ frappe.ui.form.on("Payment Entry", {
|
||||
|
||||
bank_account: function (frm) {
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
|
||||
if (frm.doc.bank_account && in_list(["Pay", "Receive"], frm.doc.payment_type)) {
|
||||
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.bank_account.bank_account.get_bank_account_details",
|
||||
args: {
|
||||
@@ -1666,6 +1665,8 @@ frappe.ui.form.on("Payment Entry Reference", {
|
||||
frm.doc.payment_type == "Receive"
|
||||
? frm.doc.paid_from_account_currency
|
||||
: frm.doc.paid_to_account_currency,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
|
||||
@@ -579,6 +579,7 @@
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Payment Order Status",
|
||||
"no_copy": 1,
|
||||
"options": "Initiated\nPayment Ordered",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -821,4 +822,4 @@
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.bank_account.bank_account import (
|
||||
get_bank_account_details,
|
||||
get_default_company_bank_account,
|
||||
get_party_bank_account,
|
||||
)
|
||||
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
|
||||
@@ -52,7 +53,7 @@ class InvalidPaymentEntry(ValidationError):
|
||||
|
||||
class PaymentEntry(AccountsController):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentEntry, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if not self.is_new():
|
||||
self.setup_party_account_field()
|
||||
|
||||
@@ -112,6 +113,9 @@ class PaymentEntry(AccountsController):
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
if self.party_type not in ("Customer", "Supplier"):
|
||||
return
|
||||
|
||||
if not frappe.db.get_value(
|
||||
"Company", self.company, "book_advance_payments_in_separate_party_account"
|
||||
):
|
||||
@@ -165,7 +169,7 @@ class PaymentEntry(AccountsController):
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
super().on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.update_outstanding_amounts()
|
||||
self.update_advance_paid()
|
||||
@@ -275,9 +279,7 @@ class PaymentEntry(AccountsController):
|
||||
# If term based allocation is enabled, throw
|
||||
if (
|
||||
d.payment_term is None or d.payment_term == ""
|
||||
) and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
):
|
||||
) and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section"
|
||||
@@ -289,7 +291,9 @@ class PaymentEntry(AccountsController):
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
_("{0} {1} has already been fully paid.").format(
|
||||
_(d.reference_doctype), d.reference_name
|
||||
)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif (
|
||||
@@ -313,14 +317,14 @@ class PaymentEntry(AccountsController):
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
and self.term_based_allocation_enabled_for_reference(
|
||||
d.reference_doctype, d.reference_name
|
||||
)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
|
||||
).format(
|
||||
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
|
||||
)
|
||||
).format(d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term)
|
||||
)
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
@@ -392,20 +396,30 @@ class PaymentEntry(AccountsController):
|
||||
self,
|
||||
force: bool = False,
|
||||
update_ref_details_only_for: list | None = None,
|
||||
ref_exchange_rate: float | None = None,
|
||||
reference_exchange_details: dict | None = None,
|
||||
) -> None:
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
if update_ref_details_only_for and (
|
||||
not (d.reference_doctype, d.reference_name) in update_ref_details_only_for
|
||||
(d.reference_doctype, d.reference_name) not in update_ref_details_only_for
|
||||
):
|
||||
continue
|
||||
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype, d.reference_name, self.party_account_currency
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
self.party_account_currency,
|
||||
self.party_type,
|
||||
self.party,
|
||||
)
|
||||
if ref_exchange_rate:
|
||||
ref_details.update({"exchange_rate": ref_exchange_rate})
|
||||
|
||||
# Only update exchange rate when the reference is Journal Entry
|
||||
if (
|
||||
reference_exchange_details
|
||||
and d.reference_doctype == reference_exchange_details.reference_doctype
|
||||
and d.reference_name == reference_exchange_details.reference_name
|
||||
):
|
||||
ref_details.update({"exchange_rate": reference_exchange_details.exchange_rate})
|
||||
|
||||
for field, value in ref_details.items():
|
||||
if d.exchange_gain_loss:
|
||||
@@ -438,7 +452,9 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
if ref_doc:
|
||||
if self.paid_from_account_currency == ref_doc.currency:
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get("conversion_rate")
|
||||
self.source_exchange_rate = ref_doc.get("exchange_rate") or ref_doc.get(
|
||||
"conversion_rate"
|
||||
)
|
||||
|
||||
if not self.source_exchange_rate:
|
||||
self.source_exchange_rate = get_exchange_rate(
|
||||
@@ -475,7 +491,7 @@ class PaymentEntry(AccountsController):
|
||||
if d.reference_doctype not in valid_reference_doctypes:
|
||||
frappe.throw(
|
||||
_("Reference Doctype must be one of {0}").format(
|
||||
comma_or((_(d) for d in valid_reference_doctypes))
|
||||
comma_or(_(d) for d in valid_reference_doctypes)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -498,7 +514,8 @@ class PaymentEntry(AccountsController):
|
||||
if d.reference_doctype in frappe.get_hooks("invoice_doctypes"):
|
||||
if self.party_type == "Customer":
|
||||
ref_party_account = (
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
|
||||
get_party_account_based_on_invoice_discounting(d.reference_name)
|
||||
or ref_doc.debit_to
|
||||
)
|
||||
elif self.party_type == "Supplier":
|
||||
ref_party_account = ref_doc.credit_to
|
||||
@@ -511,7 +528,10 @@ class PaymentEntry(AccountsController):
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} {1} is associated with {2}, but Party Account is {3}").format(
|
||||
_(d.reference_doctype), d.reference_name, ref_party_account, self.party_account
|
||||
_(d.reference_doctype),
|
||||
d.reference_name,
|
||||
ref_party_account,
|
||||
self.party_account,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -522,13 +542,15 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
|
||||
if ref_doc.docstatus != 1:
|
||||
frappe.throw(_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name))
|
||||
frappe.throw(
|
||||
_("{0} {1} must be submitted").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
def get_valid_reference_doctypes(self):
|
||||
if self.party_type == "Customer":
|
||||
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
|
||||
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning", "Payment Entry")
|
||||
elif self.party_type == "Supplier":
|
||||
return ("Purchase Order", "Purchase Invoice", "Journal Entry")
|
||||
return ("Purchase Order", "Purchase Invoice", "Journal Entry", "Payment Entry")
|
||||
elif self.party_type == "Shareholder":
|
||||
return ("Journal Entry",)
|
||||
elif self.party_type == "Employee":
|
||||
@@ -693,9 +715,7 @@ class PaymentEntry(AccountsController):
|
||||
if not (is_single_currency and reference_is_multi_currency):
|
||||
return allocated_amount
|
||||
|
||||
allocated_amount = flt(
|
||||
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
|
||||
)
|
||||
allocated_amount = flt(allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount"))
|
||||
|
||||
return allocated_amount
|
||||
|
||||
@@ -758,7 +778,6 @@ class PaymentEntry(AccountsController):
|
||||
accounts = []
|
||||
for d in self.taxes:
|
||||
if d.account_head == tax_withholding_details.get("account_head"):
|
||||
|
||||
# Preserve user updated included in paid amount
|
||||
if d.included_in_paid_amount:
|
||||
tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount})
|
||||
@@ -878,7 +897,6 @@ class PaymentEntry(AccountsController):
|
||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
else:
|
||||
|
||||
# Use source/target exchange rate, so no difference amount is calculated.
|
||||
# then update exchange gain/loss amount in reference table
|
||||
# if there is an exchange gain/loss amount in reference table, submit a JE for that
|
||||
@@ -1016,7 +1034,6 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
]
|
||||
else:
|
||||
|
||||
remarks = [
|
||||
_("Amount {0} {1} {2} {3}").format(
|
||||
_(self.party_account_currency),
|
||||
@@ -1036,14 +1053,19 @@ class PaymentEntry(AccountsController):
|
||||
if d.allocated_amount:
|
||||
remarks.append(
|
||||
_("Amount {0} {1} against {2} {3}").format(
|
||||
_(self.party_account_currency), d.allocated_amount, d.reference_doctype, d.reference_name
|
||||
_(self.party_account_currency),
|
||||
d.allocated_amount,
|
||||
d.reference_doctype,
|
||||
d.reference_name,
|
||||
)
|
||||
)
|
||||
|
||||
for d in self.get("deductions"):
|
||||
if d.amount:
|
||||
remarks.append(
|
||||
_("Amount {0} {1} deducted against {2}").format(_(self.company_currency), d.amount, d.account)
|
||||
_("Amount {0} {1} deducted against {2}").format(
|
||||
_(self.company_currency), d.amount, d.account
|
||||
)
|
||||
)
|
||||
|
||||
self.set("remarks", "\n".join(remarks))
|
||||
@@ -1117,21 +1139,31 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
|
||||
cost_center = self.cost_center
|
||||
if d.reference_doctype == "Sales Invoice" and not cost_center:
|
||||
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
|
||||
cost_center = frappe.db.get_value(
|
||||
d.reference_doctype, d.reference_name, "cost_center"
|
||||
)
|
||||
|
||||
gle = party_gl_dict.copy()
|
||||
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(
|
||||
d
|
||||
)
|
||||
reverse_dr_or_cr = 0
|
||||
|
||||
if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
|
||||
payable_party_types = get_party_types_from_account_type("Payable")
|
||||
receivable_party_types = get_party_types_from_account_type("Receivable")
|
||||
if is_return and self.party_type in receivable_party_types and (self.payment_type == "Pay"):
|
||||
if (
|
||||
is_return
|
||||
and self.party_type in receivable_party_types
|
||||
and (self.payment_type == "Pay")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
elif (
|
||||
is_return and self.party_type in payable_party_types and (self.payment_type == "Receive")
|
||||
is_return
|
||||
and self.party_type in payable_party_types
|
||||
and (self.payment_type == "Receive")
|
||||
):
|
||||
reverse_dr_or_cr = 1
|
||||
|
||||
@@ -1191,9 +1223,22 @@ class PaymentEntry(AccountsController):
|
||||
"Journal Entry",
|
||||
"Sales Order",
|
||||
"Purchase Order",
|
||||
"Payment Entry",
|
||||
):
|
||||
self.add_advance_gl_for_reference(gl_entries, ref)
|
||||
|
||||
def get_dr_and_account_for_advances(self, reference):
|
||||
if reference.reference_doctype == "Sales Invoice":
|
||||
return "credit", reference.account
|
||||
|
||||
if reference.reference_doctype == "Payment Entry":
|
||||
if reference.account_type == "Receivable" and reference.payment_type == "Pay":
|
||||
return "credit", self.party_account
|
||||
else:
|
||||
return "debit", self.party_account
|
||||
|
||||
return "debit", reference.account
|
||||
|
||||
def add_advance_gl_for_reference(self, gl_entries, invoice):
|
||||
args_dict = {
|
||||
"party_type": self.party_type,
|
||||
@@ -1213,8 +1258,8 @@ class PaymentEntry(AccountsController):
|
||||
if getdate(posting_date) < getdate(self.posting_date):
|
||||
posting_date = self.posting_date
|
||||
|
||||
dr_or_cr = "credit" if invoice.reference_doctype in ["Sales Invoice", "Sales Order"] else "debit"
|
||||
args_dict["account"] = invoice.account
|
||||
dr_or_cr, account = self.get_dr_and_account_for_advances(invoice)
|
||||
args_dict["account"] = account
|
||||
args_dict[dr_or_cr] = invoice.allocated_amount
|
||||
args_dict[dr_or_cr + "_in_account_currency"] = invoice.allocated_amount
|
||||
args_dict.update(
|
||||
@@ -1596,7 +1641,8 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
return []
|
||||
elif supplier_status["hold_type"] == "Payments":
|
||||
if (
|
||||
not supplier_status["release_date"] or getdate(nowdate()) <= supplier_status["release_date"]
|
||||
not supplier_status["release_date"]
|
||||
or getdate(nowdate()) <= supplier_status["release_date"]
|
||||
):
|
||||
return []
|
||||
|
||||
@@ -1606,7 +1652,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
# Get positive outstanding sales /purchase invoices
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
condition = " and voucher_type={0} and voucher_no={1}".format(
|
||||
condition = " and voucher_type={} and voucher_no={}".format(
|
||||
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
|
||||
)
|
||||
common_filter.append(ple.voucher_type == args["voucher_type"])
|
||||
@@ -1621,7 +1667,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if args.get(dim.fieldname):
|
||||
condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname))
|
||||
condition += f" and {dim.fieldname}='{args.get(dim.fieldname)}'"
|
||||
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||
|
||||
date_fields_dict = {
|
||||
@@ -1631,21 +1677,21 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
|
||||
for fieldname, date_fields in date_fields_dict.items():
|
||||
if args.get(date_fields[0]) and args.get(date_fields[1]):
|
||||
condition += " and {0} between '{1}' and '{2}'".format(
|
||||
condition += " and {} between '{}' and '{}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
elif args.get(date_fields[0]):
|
||||
# if only from date is supplied
|
||||
condition += " and {0} >= '{1}'".format(fieldname, args.get(date_fields[0]))
|
||||
condition += f" and {fieldname} >= '{args.get(date_fields[0])}'"
|
||||
posting_and_due_date.append(ple[fieldname].gte(args.get(date_fields[0])))
|
||||
elif args.get(date_fields[1]):
|
||||
# if only to date is supplied
|
||||
condition += " and {0} <= '{1}'".format(fieldname, args.get(date_fields[1]))
|
||||
condition += f" and {fieldname} <= '{args.get(date_fields[1])}'"
|
||||
posting_and_due_date.append(ple[fieldname].lte(args.get(date_fields[1])))
|
||||
|
||||
if args.get("company"):
|
||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||
condition += " and company = {}".format(frappe.db.escape(args.get("company")))
|
||||
common_filter.append(ple.company == args.get("company"))
|
||||
|
||||
outstanding_invoices = []
|
||||
@@ -1660,7 +1706,7 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
party_account,
|
||||
[party_account],
|
||||
common_filter=common_filter,
|
||||
posting_date=posting_and_due_date,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
@@ -1761,12 +1807,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company) -> list
|
||||
return outstanding_invoices_after_split
|
||||
|
||||
|
||||
def get_currency_data(outstanding_invoices: list, company: str = None) -> dict:
|
||||
def get_currency_data(outstanding_invoices: list, company: str | None = None) -> dict:
|
||||
"""Get currency and conversion data for a list of invoices."""
|
||||
exc_rates = frappe._dict()
|
||||
company_currency = (
|
||||
frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
)
|
||||
company_currency = frappe.db.get_value("Company", company, "default_currency") if company else None
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
|
||||
@@ -1861,7 +1905,7 @@ def get_orders_to_be_billed(
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname))
|
||||
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
@@ -1994,14 +2038,16 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
party_name = frappe.db.get_value(party_type, party, _party_name)
|
||||
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
bank_account = get_party_bank_account(party_type, party)
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
|
||||
bank_account = get_default_company_bank_account(company, party_type, party)
|
||||
return {
|
||||
"party_account": party_account,
|
||||
"party_name": party_name,
|
||||
"party_account_currency": account_currency,
|
||||
"party_balance": party_balance,
|
||||
"account_balance": account_balance,
|
||||
"party_bank_account": party_bank_account,
|
||||
"bank_account": bank_account,
|
||||
}
|
||||
|
||||
@@ -2011,18 +2057,14 @@ def get_account_details(account, date, cost_center=None):
|
||||
frappe.has_permission("Payment Entry", throw=True)
|
||||
|
||||
# to check if the passed account is accessible under reference doctype Payment Entry
|
||||
account_list = frappe.get_list(
|
||||
"Account", {"name": account}, reference_doctype="Payment Entry", limit=1
|
||||
)
|
||||
account_list = frappe.get_list("Account", {"name": account}, reference_doctype="Payment Entry", limit=1)
|
||||
|
||||
# There might be some user permissions which will allow account under certain doctypes
|
||||
# except for Payment Entry, only in such case we should throw permission error
|
||||
if not account_list:
|
||||
frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account))
|
||||
|
||||
account_balance = get_balance_on(
|
||||
account, date, cost_center=cost_center, ignore_account_permission=True
|
||||
)
|
||||
account_balance = get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
|
||||
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -2039,53 +2081,75 @@ def get_company_defaults(company):
|
||||
return frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||
|
||||
|
||||
def get_outstanding_on_journal_entry(name):
|
||||
gl = frappe.qb.DocType("GL Entry")
|
||||
res = (
|
||||
frappe.qb.from_(gl)
|
||||
.select(
|
||||
Case()
|
||||
.when(
|
||||
gl.party_type == "Customer",
|
||||
Coalesce(Sum(gl.debit_in_account_currency - gl.credit_in_account_currency), 0),
|
||||
)
|
||||
.else_(Coalesce(Sum(gl.credit_in_account_currency - gl.debit_in_account_currency), 0))
|
||||
.as_("outstanding_amount")
|
||||
)
|
||||
def get_outstanding_on_journal_entry(voucher_no, party_type, party):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
|
||||
outstanding = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(Sum(ple.amount_in_account_currency))
|
||||
.where(
|
||||
(Coalesce(gl.party_type, "") != "")
|
||||
& (gl.is_cancelled == 0)
|
||||
& ((gl.voucher_no == name) | (gl.against_voucher == name))
|
||||
(ple.against_voucher_no == voucher_no)
|
||||
& (ple.party_type == party_type)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
).run()
|
||||
|
||||
outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0
|
||||
outstanding_amount = outstanding[0][0] if outstanding else 0
|
||||
|
||||
return outstanding_amount
|
||||
total = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(Sum(ple.amount_in_account_currency))
|
||||
.where(
|
||||
(ple.voucher_no == voucher_no)
|
||||
& (ple.party_type == party_type)
|
||||
& (ple.party == party)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
).run()
|
||||
|
||||
total_amount = total[0][0] if total else 0
|
||||
|
||||
return outstanding_amount, total_amount
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_reference_details(reference_doctype, reference_name, party_account_currency):
|
||||
def get_reference_details(
|
||||
reference_doctype, reference_name, party_account_currency, party_type=None, party=None
|
||||
):
|
||||
total_amount = outstanding_amount = exchange_rate = account = None
|
||||
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(
|
||||
ref_doc.company
|
||||
)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
||||
|
||||
# Only applies for Reverse Payment Entries
|
||||
account_type = None
|
||||
payment_type = None
|
||||
|
||||
if reference_doctype == "Dunning":
|
||||
total_amount = outstanding_amount = ref_doc.get("dunning_amount")
|
||||
exchange_rate = 1
|
||||
|
||||
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
|
||||
total_amount = ref_doc.get("total_amount")
|
||||
if ref_doc.multi_currency:
|
||||
exchange_rate = get_exchange_rate(
|
||||
party_account_currency, company_currency, ref_doc.posting_date
|
||||
)
|
||||
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
|
||||
outstanding_amount, total_amount = get_outstanding_on_journal_entry(
|
||||
reference_name, party_type, party
|
||||
)
|
||||
|
||||
elif reference_doctype == "Payment Entry":
|
||||
if reverse_payment_details := frappe.db.get_all(
|
||||
"Payment Entry",
|
||||
filters={"name": reference_name},
|
||||
fields=["payment_type", "party_type"],
|
||||
)[0]:
|
||||
payment_type = reverse_payment_details.payment_type
|
||||
account_type = frappe.db.get_value(
|
||||
"Party Type", reverse_payment_details.party_type, "account_type"
|
||||
)
|
||||
exchange_rate = 1
|
||||
|
||||
elif reference_doctype != "Journal Entry":
|
||||
if not total_amount:
|
||||
@@ -2131,6 +2195,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
"outstanding_amount": flt(outstanding_amount),
|
||||
"exchange_rate": flt(exchange_rate),
|
||||
"bill_no": ref_doc.get("bill_no"),
|
||||
"account_type": account_type,
|
||||
"payment_type": payment_type,
|
||||
}
|
||||
)
|
||||
if account:
|
||||
@@ -2151,9 +2217,7 @@ def get_payment_entry(
|
||||
):
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
over_billing_allowance = frappe.db.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (
|
||||
100.0 + over_billing_allowance
|
||||
):
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (100.0 + over_billing_allowance):
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt)))
|
||||
|
||||
if not party_type:
|
||||
@@ -2206,9 +2270,7 @@ def get_payment_entry(
|
||||
pe.paid_from_account_currency = (
|
||||
party_account_currency if payment_type == "Receive" else bank.account_currency
|
||||
)
|
||||
pe.paid_to_account_currency = (
|
||||
party_account_currency if payment_type == "Pay" else bank.account_currency
|
||||
)
|
||||
pe.paid_to_account_currency = party_account_currency if payment_type == "Pay" else bank.account_currency
|
||||
pe.paid_amount = paid_amount
|
||||
pe.received_amount = received_amount
|
||||
pe.letter_head = doc.get("letter_head")
|
||||
@@ -2237,7 +2299,6 @@ def get_payment_entry(
|
||||
doc.payment_terms_template,
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
):
|
||||
|
||||
for reference in get_reference_as_per_payment_terms(
|
||||
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||
):
|
||||
@@ -2421,9 +2482,7 @@ def set_paid_amount_and_received_amount(
|
||||
return paid_amount, received_amount
|
||||
|
||||
|
||||
def apply_early_payment_discount(
|
||||
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||
):
|
||||
def apply_early_payment_discount(paid_amount, received_amount, doc, party_account_currency, reference_date):
|
||||
total_discount = 0
|
||||
valid_discounts = []
|
||||
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||
@@ -2433,7 +2492,6 @@ def apply_early_payment_discount(
|
||||
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||
for term in doc.payment_schedule:
|
||||
if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
|
||||
|
||||
if term.discount_type == "Percentage":
|
||||
grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
|
||||
discount_amount = flt(grand_total) * (term.discount / 100)
|
||||
@@ -2462,9 +2520,7 @@ def apply_early_payment_discount(
|
||||
return paid_amount, received_amount, total_discount, valid_discounts
|
||||
|
||||
|
||||
def set_pending_discount_loss(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
):
|
||||
def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss, party_account_currency):
|
||||
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
|
||||
if party_account_currency != doc.company_currency:
|
||||
discount_amount = discount_amount * doc.get("conversion_rate", 1)
|
||||
@@ -2484,7 +2540,8 @@ def set_pending_discount_loss(
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * positive_negative,
|
||||
}
|
||||
)
|
||||
@@ -2507,9 +2564,7 @@ def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
|
||||
def get_total_discount_percent(doc, valid_discounts) -> float:
|
||||
"""Get total percentage and amount discount applied as a percentage."""
|
||||
total_discount_percent = (
|
||||
sum(
|
||||
discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
|
||||
)
|
||||
sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage")
|
||||
or 0.0
|
||||
)
|
||||
|
||||
@@ -2552,9 +2607,7 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
|
||||
# The same account head could be used more than once
|
||||
for tax in doc.get("taxes", []):
|
||||
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
|
||||
total_discount_percentage / 100
|
||||
)
|
||||
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (total_discount_percentage / 100)
|
||||
|
||||
account = tax.get("account_head")
|
||||
if not tax_discount_loss.get(account):
|
||||
@@ -2571,7 +2624,8 @@ def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||
"deductions",
|
||||
{
|
||||
"account": account,
|
||||
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": flt(loss, precision),
|
||||
},
|
||||
)
|
||||
@@ -2594,7 +2648,8 @@ def get_reference_as_per_payment_terms(
|
||||
if not is_multi_currency_acc:
|
||||
# If accounting is done in company currency for multi-currency transaction
|
||||
payment_term_outstanding = flt(
|
||||
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
|
||||
payment_term_outstanding * doc.get("conversion_rate"),
|
||||
payment_term.precision("payment_amount"),
|
||||
)
|
||||
|
||||
if payment_term_outstanding:
|
||||
@@ -2622,7 +2677,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
|
||||
|
||||
paid_amount = frappe.db.sql(
|
||||
"""
|
||||
f"""
|
||||
select ifnull(sum({dr_or_cr}), 0) as paid_amount
|
||||
from `tabGL Entry`
|
||||
where against_voucher_type = %s
|
||||
@@ -2632,9 +2687,7 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
and account = %s
|
||||
and due_date = %s
|
||||
and {dr_or_cr} > 0
|
||||
""".format(
|
||||
dr_or_cr=dr_or_cr
|
||||
),
|
||||
""",
|
||||
(dt, dn, party_type, party, account, due_date),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
@@ -10,7 +9,6 @@ from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
InvalidPaymentEntry,
|
||||
get_outstanding_reference_documents,
|
||||
get_payment_entry,
|
||||
get_reference_details,
|
||||
@@ -162,7 +160,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
|
||||
supplier.on_hold = 0
|
||||
supplier.save()
|
||||
except:
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
raise Exception
|
||||
@@ -469,9 +467,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry(
|
||||
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
|
||||
)
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700)
|
||||
pe.reference_no = si.name
|
||||
pe.reference_date = nowdate()
|
||||
|
||||
@@ -638,9 +634,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
self.assertEqual(
|
||||
pe.source_exchange_rate, 65.1, "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1)
|
||||
)
|
||||
self.assertEqual(pe.source_exchange_rate, 65.1, f"{pe.source_exchange_rate} is not equal to {65.1}")
|
||||
|
||||
def test_internal_transfer_usd_to_inr(self):
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
@@ -896,9 +890,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
cost_center = "_Test Cost Center for BS Account - _TC"
|
||||
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
|
||||
|
||||
pi = make_purchase_invoice_against_cost_center(
|
||||
cost_center=cost_center, credit_to="Creditors - _TC"
|
||||
)
|
||||
pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC")
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
self.assertEqual(pe.cost_center, pi.cost_center)
|
||||
@@ -939,9 +931,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
|
||||
|
||||
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=si.cost_center)
|
||||
party_balance = get_balance_on(
|
||||
party_type="Customer", party=si.customer, cost_center=si.cost_center
|
||||
)
|
||||
party_balance = get_balance_on(party_type="Customer", party=si.customer, cost_center=si.cost_center)
|
||||
party_account_balance = get_balance_on(si.debit_to, cost_center=si.cost_center)
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
@@ -1084,9 +1074,13 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.source_exchange_rate = 50
|
||||
pe.save()
|
||||
|
||||
ref_details = get_reference_details(so.doctype, so.name, pe.paid_from_account_currency)
|
||||
ref_details = get_reference_details(
|
||||
so.doctype, so.name, pe.paid_from_account_currency, "Customer", so.customer
|
||||
)
|
||||
expected_response = {
|
||||
"account": get_party_account("Customer", so.customer, so.company),
|
||||
"account_type": None, # only applies for Reverse Payment Entry
|
||||
"payment_type": None, # only applies for Reverse Payment Entry
|
||||
"total_amount": 5000.0,
|
||||
"outstanding_amount": 5000.0,
|
||||
"exchange_rate": 1.0,
|
||||
@@ -1203,7 +1197,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled
|
||||
|
||||
"""
|
||||
customer = create_customer()
|
||||
create_customer()
|
||||
create_payment_terms_template()
|
||||
|
||||
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||
@@ -1324,8 +1318,6 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.check_gl_entries()
|
||||
|
||||
def test_ledger_entries_for_advance_as_liability(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
|
||||
company = "_Test Company"
|
||||
|
||||
advance_account = create_account(
|
||||
@@ -1427,7 +1419,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.check_pl_entries()
|
||||
|
||||
# Unreconcile
|
||||
unrecon = (
|
||||
(
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
@@ -1483,9 +1475,7 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
create_payment_terms_template()
|
||||
|
||||
# SI has an earlier due date and SI2 has a later due date
|
||||
si = create_sales_invoice(
|
||||
qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4)
|
||||
)
|
||||
si = create_sales_invoice(qty=1, rate=100, customer=customer, posting_date=add_days(nowdate(), -4))
|
||||
si2 = create_sales_invoice(do_not_save=1, qty=1, rate=100, customer=customer)
|
||||
si2.payment_terms_template = "Test Receivable Template"
|
||||
si2.submit()
|
||||
@@ -1514,6 +1504,168 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(references[1].payment_term, "Basic Amount Receivable")
|
||||
self.assertEqual(references[2].payment_term, "Tax Receivable")
|
||||
|
||||
def test_reverse_payment_reconciliation(self):
|
||||
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
||||
pe = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
payment_type="Receive",
|
||||
paid_from="Debtors - _TC",
|
||||
paid_to="_Test Cash - _TC",
|
||||
)
|
||||
pe.submit()
|
||||
|
||||
reverse_pe = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
payment_type="Pay",
|
||||
paid_from="_Test Cash - _TC",
|
||||
paid_to="Debtors - _TC",
|
||||
)
|
||||
reverse_pe.submit()
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = "_Test Company"
|
||||
pr.party_type = "Customer"
|
||||
pr.party = customer
|
||||
pr.receivable_payable_account = "Debtors - _TC"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
self.assertEqual(reverse_pe.name, pr.invoices[0].invoice_number)
|
||||
self.assertEqual(pe.name, pr.payments[0].reference_name)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
def test_advance_reverse_payment_reconciliation(self):
|
||||
company = "_Test Company"
|
||||
customer = create_customer(frappe.generate_hash(length=10), "INR")
|
||||
advance_account = create_account(
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_name="Advances Received",
|
||||
company=company,
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_received_account": advance_account,
|
||||
},
|
||||
)
|
||||
# Reverse Payment(essentially an Invoice)
|
||||
reverse_pe = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
payment_type="Pay",
|
||||
paid_from="_Test Cash - _TC",
|
||||
paid_to=advance_account,
|
||||
)
|
||||
reverse_pe.save() # use save() to trigger set_liability_account()
|
||||
reverse_pe.submit()
|
||||
|
||||
# Advance Payment
|
||||
pe = create_payment_entry(
|
||||
party_type="Customer",
|
||||
party=customer,
|
||||
payment_type="Receive",
|
||||
paid_from=advance_account,
|
||||
paid_to="_Test Cash - _TC",
|
||||
)
|
||||
pe.save() # use save() to trigger set_liability_account()
|
||||
pe.submit()
|
||||
|
||||
# Partially reconcile advance against invoice
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = customer
|
||||
pr.receivable_payable_account = "Debtors - _TC"
|
||||
pr.default_advance_account = advance_account
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].allocated_amount = 400
|
||||
pr.reconcile()
|
||||
|
||||
# assert General and Payment Ledger entries post partial reconciliation
|
||||
self.expected_gle = [
|
||||
{"account": advance_account, "debit": 400.0, "credit": 0.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
||||
{"account": advance_account, "debit": 0.0, "credit": 400.0},
|
||||
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
||||
]
|
||||
self.expected_ple = [
|
||||
{
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -1000.0,
|
||||
},
|
||||
{
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": reverse_pe.name,
|
||||
"amount": -400.0,
|
||||
},
|
||||
{
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": 400.0,
|
||||
},
|
||||
]
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
# Unreconcile
|
||||
(
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
"allocations": [
|
||||
{"reference_doctype": reverse_pe.doctype, "reference_name": reverse_pe.name}
|
||||
],
|
||||
}
|
||||
)
|
||||
.save()
|
||||
.submit()
|
||||
)
|
||||
|
||||
# assert General and Payment Ledger entries post unreconciliation
|
||||
self.expected_gle = [
|
||||
{"account": advance_account, "debit": 0.0, "credit": 1000.0},
|
||||
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
|
||||
]
|
||||
self.expected_ple = [
|
||||
{
|
||||
"account": advance_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -1000.0,
|
||||
},
|
||||
]
|
||||
self.voucher_no = pe.name
|
||||
self.check_gl_entries()
|
||||
self.check_pl_entries()
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
@@ -1541,12 +1693,11 @@ def create_payment_entry(**args):
|
||||
|
||||
|
||||
def create_payment_terms_template():
|
||||
|
||||
create_payment_term("Basic Amount Receivable")
|
||||
create_payment_term("Tax Receivable")
|
||||
|
||||
if not frappe.db.exists("Payment Terms Template", "Test Receivable Template"):
|
||||
payment_term_template = frappe.get_doc(
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "Test Receivable Template",
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
"due_date",
|
||||
"bill_no",
|
||||
"payment_term",
|
||||
"account_type",
|
||||
"payment_type",
|
||||
"column_break_4",
|
||||
"total_amount",
|
||||
"outstanding_amount",
|
||||
@@ -108,12 +110,22 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Payment Type"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-08 07:40:38.487874",
|
||||
"modified": "2024-04-05 09:44:08.310593",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
||||
@@ -15,6 +15,7 @@ class PaymentEntryReference(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
account: DF.Link | None
|
||||
account_type: DF.Data | None
|
||||
allocated_amount: DF.Float
|
||||
bill_no: DF.Data | None
|
||||
due_date: DF.Date | None
|
||||
@@ -25,6 +26,7 @@ class PaymentEntryReference(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
payment_term: DF.Link | None
|
||||
payment_type: DF.Data | None
|
||||
reference_doctype: DF.Link
|
||||
reference_name: DF.DynamicLink
|
||||
total_amount: DF.Float
|
||||
|
||||
@@ -137,9 +137,9 @@ class PaymentLedgerEntry(Document):
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
_(
|
||||
"Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}."
|
||||
).format(dimension.label, self.account)
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -150,9 +150,9 @@ class PaymentLedgerEntry(Document):
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
_(
|
||||
"Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}."
|
||||
).format(dimension.label, self.account)
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -84,11 +84,14 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
self.customer = customer.name
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
@@ -112,10 +115,12 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_payment_entry(self, amount=100, posting_date=nowdate()):
|
||||
def create_payment_entry(self, amount=100, posting_date=None):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
@@ -128,9 +133,10 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def create_sales_order(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
def create_sales_order(self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False):
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
so = make_sales_order(
|
||||
company=self.company,
|
||||
transaction_date=posting_date,
|
||||
@@ -159,9 +165,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_journal_entry(
|
||||
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
|
||||
):
|
||||
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
je.company = self.company
|
||||
@@ -319,9 +323,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)
|
||||
)
|
||||
.where((ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name))
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
@@ -362,9 +364,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
)
|
||||
cr_note2.is_return = 1
|
||||
cr_note2 = cr_note2.save().submit()
|
||||
je1 = self.create_journal_entry(
|
||||
self.debit_to, self.debit_to, amount, posting_date=transaction_date
|
||||
)
|
||||
je1 = self.create_journal_entry(self.debit_to, self.debit_to, amount, posting_date=transaction_date)
|
||||
je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer"
|
||||
je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer
|
||||
je1.get("accounts")[0].reference_type = cr_note2.doctype
|
||||
@@ -419,9 +419,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name)
|
||||
)
|
||||
.where((ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name))
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
@@ -518,7 +516,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
amount = 100
|
||||
so = self.create_sales_order(qty=1, rate=amount, posting_date=transaction_date).save().submit()
|
||||
|
||||
pe = get_payment_entry(so.doctype, so.name).save().submit()
|
||||
get_payment_entry(so.doctype, so.name).save().submit()
|
||||
|
||||
so.reload()
|
||||
so.cancel()
|
||||
|
||||
@@ -90,9 +90,7 @@ def make_journal_entry(doc, supplier, mode_of_payment=None):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.payment_order = doc.name
|
||||
je.posting_date = nowdate()
|
||||
mode_of_payment_type = frappe._dict(
|
||||
frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1)
|
||||
)
|
||||
mode_of_payment_type = frappe._dict(frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1))
|
||||
|
||||
je.voucher_type = "Bank Entry"
|
||||
if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == "Cash":
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
@@ -41,9 +40,7 @@ class TestPaymentOrder(FrappeTestCase):
|
||||
payment_entry.insert()
|
||||
payment_entry.submit()
|
||||
|
||||
doc = create_payment_order_against_payment_entry(
|
||||
payment_entry, "Payment Entry", self.bank_account
|
||||
)
|
||||
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry", self.bank_account)
|
||||
reference_doc = doc.get("references")[0]
|
||||
self.assertEqual(reference_doc.reference_name, payment_entry.name)
|
||||
self.assertEqual(reference_doc.reference_doctype, "Payment Entry")
|
||||
|
||||
@@ -254,6 +254,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.data = [];
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Difference Account"),
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "allocation",
|
||||
@@ -279,6 +280,13 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
in_list_view: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Date",
|
||||
fieldname: "gain_loss_posting_date",
|
||||
label: __("Posting Date"),
|
||||
in_list_view: 1,
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
@@ -319,6 +327,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
"difference_account",
|
||||
d.difference_account
|
||||
);
|
||||
frappe.model.set_value(
|
||||
"Payment Reconciliation Allocation",
|
||||
d.docname,
|
||||
"gain_loss_posting_date",
|
||||
d.gain_loss_posting_date
|
||||
);
|
||||
});
|
||||
|
||||
this.reconcile_payment_entries();
|
||||
@@ -334,6 +348,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
reference_name: d.reference_name,
|
||||
difference_amount: d.difference_amount,
|
||||
difference_account: d.difference_account,
|
||||
gain_loss_posting_date: d.gain_loss_posting_date,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ class PaymentReconciliation(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentReconciliation, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.common_filter_conditions = []
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
@@ -286,7 +286,6 @@ class PaymentReconciliation(Document):
|
||||
self.return_invoices = self.return_invoices_query.run(as_dict=True)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
|
||||
self.build_qb_filter_conditions(get_return_invoices=True)
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@@ -340,10 +339,15 @@ class PaymentReconciliation(Document):
|
||||
|
||||
self.build_qb_filter_conditions(get_invoices=True)
|
||||
|
||||
accounts = [self.receivable_payable_account]
|
||||
|
||||
if self.default_advance_account:
|
||||
accounts.append(self.default_advance_account)
|
||||
|
||||
non_reconciled_invoices = get_outstanding_invoices(
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
accounts,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
@@ -407,9 +411,7 @@ class PaymentReconciliation(Document):
|
||||
payment_entry[0].get("reference_name")
|
||||
)
|
||||
|
||||
new_difference_amount = self.get_difference_amount(
|
||||
payment_entry[0], invoice[0], allocated_amount
|
||||
)
|
||||
new_difference_amount = self.get_difference_amount(payment_entry[0], invoice[0], allocated_amount)
|
||||
return new_difference_amount
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -441,6 +443,7 @@ class PaymentReconciliation(Document):
|
||||
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
|
||||
res.difference_account = default_exchange_gain_loss_account
|
||||
res.exchange_rate = inv.get("exchange_rate")
|
||||
res.update({"gain_loss_posting_date": pay.get("posting_date")})
|
||||
|
||||
if pay.get("amount") == 0:
|
||||
entries.append(res)
|
||||
@@ -526,9 +529,9 @@ class PaymentReconciliation(Document):
|
||||
|
||||
if running_doc:
|
||||
frappe.throw(
|
||||
_("A Reconciliation Job {0} is running for the same filters. Cannot reconcile now").format(
|
||||
get_link_to_form("Auto Reconcile", running_doc)
|
||||
)
|
||||
_(
|
||||
"A Reconciliation Job {0} is running for the same filters. Cannot reconcile now"
|
||||
).format(get_link_to_form("Auto Reconcile", running_doc))
|
||||
)
|
||||
return
|
||||
|
||||
@@ -557,6 +560,7 @@ class PaymentReconciliation(Document):
|
||||
"allocated_amount": flt(row.get("allocated_amount")),
|
||||
"difference_amount": flt(row.get("difference_amount")),
|
||||
"difference_account": row.get("difference_account"),
|
||||
"difference_posting_date": row.get("gain_loss_posting_date"),
|
||||
"cost_center": row.get("cost_center"),
|
||||
}
|
||||
)
|
||||
@@ -620,9 +624,7 @@ class PaymentReconciliation(Document):
|
||||
|
||||
invoice_exchange_map.update(purchase_invoice_map)
|
||||
|
||||
journals = [
|
||||
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"
|
||||
]
|
||||
journals = [d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Journal Entry"]
|
||||
journals.extend(
|
||||
[d.get("reference_name") for d in payments if d.get("reference_type") == "Journal Entry"]
|
||||
)
|
||||
@@ -714,7 +716,7 @@ class PaymentReconciliation(Document):
|
||||
def get_journal_filter_conditions(self):
|
||||
conditions = []
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
qb.DocType("Journal Entry Account")
|
||||
conditions.append(je.company == self.company)
|
||||
|
||||
if self.from_payment_date:
|
||||
@@ -834,7 +836,7 @@ def adjust_allocations_for_taxes(doc):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_queries_for_dimension_filters(company: str = None):
|
||||
def get_queries_for_dimension_filters(company: str | None = None):
|
||||
dimensions_with_filters = []
|
||||
for d in get_dimensions()[0]:
|
||||
filters = {}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
@@ -102,6 +101,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
"account_currency": "USD",
|
||||
"account_type": "Payable",
|
||||
},
|
||||
# 'Payable' account for capturing advance paid, under 'Assets' group
|
||||
{
|
||||
"attribute": "advance_payable_account",
|
||||
"account_name": "Advance Paid",
|
||||
"parent_account": "Current Assets - _PR",
|
||||
"account_currency": "INR",
|
||||
"account_type": "Payable",
|
||||
},
|
||||
]
|
||||
|
||||
for x in accounts:
|
||||
@@ -127,11 +134,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
setattr(self, x.attribute, acc.name)
|
||||
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
@@ -155,10 +165,13 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
)
|
||||
return sinv
|
||||
|
||||
def create_payment_entry(self, amount=100, posting_date=nowdate(), customer=None):
|
||||
def create_payment_entry(self, amount=100, posting_date=None, customer=None):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
@@ -172,11 +185,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
return payment
|
||||
|
||||
def create_purchase_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
pinv = make_purchase_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
@@ -201,11 +217,14 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
return pinv
|
||||
|
||||
def create_purchase_order(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
self, qty=1, rate=100, posting_date=None, do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
if posting_date is None:
|
||||
posting_date = nowdate()
|
||||
|
||||
pord = create_purchase_order(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
@@ -250,9 +269,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
return pr
|
||||
|
||||
def create_journal_entry(
|
||||
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
|
||||
):
|
||||
def create_journal_entry(self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
je.company = self.company
|
||||
@@ -402,7 +419,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
rate = 100
|
||||
invoices = []
|
||||
payments = []
|
||||
for i in range(5):
|
||||
for _i in range(5):
|
||||
invoices.append(self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date))
|
||||
pe = self.create_payment_entry(amount=rate, posting_date=transaction_date).save().submit()
|
||||
payments.append(pe)
|
||||
@@ -821,9 +838,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
|
||||
cr_note.cancel()
|
||||
|
||||
pay = self.create_payment_entry(
|
||||
amount=amount, posting_date=transaction_date, customer=self.customer3
|
||||
)
|
||||
pay = self.create_payment_entry(amount=amount, posting_date=transaction_date, customer=self.customer3)
|
||||
pay.paid_from = self.debtors_eur
|
||||
pay.paid_from_account_currency = "EUR"
|
||||
pay.source_exchange_rate = exchange_rate
|
||||
@@ -1025,9 +1040,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
rate = 100
|
||||
|
||||
# 'Main - PR' Cost Center
|
||||
si1 = self.create_sales_invoice(
|
||||
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
|
||||
)
|
||||
si1 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True)
|
||||
si1.cost_center = self.main_cc.name
|
||||
si1.submit()
|
||||
|
||||
@@ -1043,9 +1056,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
je1 = je1.save().submit()
|
||||
|
||||
# 'Sub - PR' Cost Center
|
||||
si2 = self.create_sales_invoice(
|
||||
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
|
||||
)
|
||||
si2 = self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True)
|
||||
si2.cost_center = self.sub_cc.name
|
||||
si2.submit()
|
||||
|
||||
@@ -1130,6 +1141,17 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 85)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
||||
|
||||
pr.reconcile()
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
# No Exchange Gain/Loss journal should be generated
|
||||
exc_gain_loss_journals = frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": si.doctype, "reference_name": si.name, "docstatus": 1},
|
||||
fields=["parent"],
|
||||
)
|
||||
self.assertEqual(exc_gain_loss_journals, [])
|
||||
|
||||
def test_reconciliation_purchase_invoice_against_return(self):
|
||||
self.supplier = "_Test Supplier USD"
|
||||
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)
|
||||
@@ -1321,6 +1343,188 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
def test_reverse_payment_against_payment_for_supplier(self):
|
||||
"""
|
||||
Reconcile a payment against a reverse payment, for a supplier.
|
||||
"""
|
||||
self.supplier = "_Test Supplier"
|
||||
amount = 4000
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.creditors
|
||||
pe.save().submit()
|
||||
|
||||
reverse_pe = self.create_payment_entry(amount=amount)
|
||||
reverse_pe.party_type = "Supplier"
|
||||
reverse_pe.party = self.supplier
|
||||
reverse_pe.payment_type = "Receive"
|
||||
reverse_pe.paid_from = self.creditors
|
||||
reverse_pe.paid_to = self.cash
|
||||
reverse_pe.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
self.assertEqual(pr.invoices[0].invoice_number, reverse_pe.name)
|
||||
self.assertEqual(pr.payments[0].reference_name, pe.name)
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pe.reload()
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].exchange_rate, 1)
|
||||
# There should not be any Exc Gain/Loss
|
||||
self.assertEqual(pe.references[0].exchange_gain_loss, 0)
|
||||
self.assertEqual(pe.references[0].reference_name, reverse_pe.name)
|
||||
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": ("in", [pe.name, reverse_pe.name]),
|
||||
},
|
||||
)
|
||||
# There should be no Exchange Gain/Loss created
|
||||
self.assertEqual(journals, [])
|
||||
|
||||
def test_advance_reverse_payment_against_payment_for_supplier(self):
|
||||
"""
|
||||
Reconcile an Advance payment against reverse payment, for a supplier.
|
||||
"""
|
||||
frappe.db.set_value(
|
||||
"Company",
|
||||
self.company,
|
||||
{
|
||||
"book_advance_payments_in_separate_party_account": 1,
|
||||
"default_advance_paid_account": self.advance_payable_account,
|
||||
},
|
||||
)
|
||||
|
||||
self.supplier = "_Test Supplier"
|
||||
amount = 4000
|
||||
|
||||
pe = self.create_payment_entry(amount=amount)
|
||||
pe.party_type = "Supplier"
|
||||
pe.party = self.supplier
|
||||
pe.payment_type = "Pay"
|
||||
pe.paid_from = self.cash
|
||||
pe.paid_to = self.advance_payable_account
|
||||
pe.save().submit()
|
||||
|
||||
reverse_pe = self.create_payment_entry(amount=amount)
|
||||
reverse_pe.party_type = "Supplier"
|
||||
reverse_pe.party = self.supplier
|
||||
reverse_pe.payment_type = "Receive"
|
||||
reverse_pe.paid_from = self.advance_payable_account
|
||||
reverse_pe.paid_to = self.cash
|
||||
reverse_pe.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||
pr.default_advance_account = self.advance_payable_account
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
self.assertEqual(pr.invoices[0].invoice_number, reverse_pe.name)
|
||||
self.assertEqual(pr.payments[0].reference_name, pe.name)
|
||||
|
||||
invoices = [invoice.as_dict() for invoice in pr.invoices]
|
||||
payments = [payment.as_dict() for payment in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pe.reload()
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].exchange_rate, 1)
|
||||
# There should not be any Exc Gain/Loss
|
||||
self.assertEqual(pe.references[0].exchange_gain_loss, 0)
|
||||
self.assertEqual(pe.references[0].reference_name, reverse_pe.name)
|
||||
|
||||
journals = frappe.db.get_all(
|
||||
"Journal Entry",
|
||||
filters={
|
||||
"voucher_type": "Exchange Gain Or Loss",
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": ("in", [pe.name, reverse_pe.name]),
|
||||
},
|
||||
)
|
||||
# There should be no Exchange Gain/Loss created
|
||||
self.assertEqual(journals, [])
|
||||
|
||||
# Assert Ledger Entries
|
||||
gl_entries = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
filters={"voucher_no": pe.name},
|
||||
fields=["account", "voucher_no", "against_voucher", "debit", "credit"],
|
||||
order_by="account, against_voucher, debit",
|
||||
)
|
||||
expected_gle = [
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": pe.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": reverse_pe.name,
|
||||
"debit": amount,
|
||||
"credit": 0.0,
|
||||
},
|
||||
{
|
||||
"account": "Cash - _PR",
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher": None,
|
||||
"debit": 0.0,
|
||||
"credit": amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(gl_entries, expected_gle)
|
||||
pl_entries = frappe.db.get_all(
|
||||
"Payment Ledger Entry",
|
||||
filters={"voucher_no": pe.name},
|
||||
fields=["account", "voucher_no", "against_voucher_no", "amount"],
|
||||
order_by="account, against_voucher_no, amount",
|
||||
)
|
||||
expected_ple = [
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": pe.name,
|
||||
"amount": amount,
|
||||
},
|
||||
{
|
||||
"account": self.advance_payable_account,
|
||||
"voucher_no": pe.name,
|
||||
"against_voucher_no": reverse_pe.name,
|
||||
"amount": -amount,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries, expected_ple)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"gain_loss_posting_date",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
|
||||
@@ -28,7 +28,7 @@ frappe.ui.form.on("Payment Request", "refresh", function (frm) {
|
||||
if (
|
||||
frm.doc.payment_request_type == "Inward" &&
|
||||
frm.doc.payment_channel !== "Phone" &&
|
||||
!in_list(["Initiated", "Paid"], frm.doc.status) &&
|
||||
!["Initiated", "Paid"].includes(frm.doc.status) &&
|
||||
!frm.doc.__islocal &&
|
||||
frm.doc.docstatus == 1
|
||||
) {
|
||||
|
||||
@@ -91,7 +91,7 @@ class PaymentRequest(Document):
|
||||
self.status = "Draft"
|
||||
self.validate_reference_document()
|
||||
self.validate_payment_request_amount()
|
||||
self.validate_currency()
|
||||
# self.validate_currency()
|
||||
self.validate_subscription_details()
|
||||
|
||||
def validate_reference_document(self):
|
||||
@@ -104,7 +104,7 @@ class PaymentRequest(Document):
|
||||
)
|
||||
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
|
||||
if not hasattr(ref_doc, "order_type") or ref_doc.order_type != "Shopping Cart":
|
||||
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
@@ -157,7 +157,7 @@ class PaymentRequest(Document):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
|
||||
if (
|
||||
hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart"
|
||||
hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart"
|
||||
) or self.flags.mute_email:
|
||||
send_mail = False
|
||||
|
||||
@@ -209,7 +209,7 @@ class PaymentRequest(Document):
|
||||
|
||||
def make_invoice(self):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":
|
||||
if hasattr(ref_doc, "order_type") and ref_doc.order_type == "Shopping Cart":
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
si = make_sales_invoice(self.reference_name, ignore_permissions=True)
|
||||
@@ -295,14 +295,10 @@ class PaymentRequest(Document):
|
||||
else:
|
||||
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
|
||||
|
||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(
|
||||
party_account
|
||||
)
|
||||
party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
|
||||
|
||||
bank_amount = self.grand_total
|
||||
if (
|
||||
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
|
||||
):
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
@@ -320,7 +316,7 @@ class PaymentRequest(Document):
|
||||
"mode_of_payment": self.mode_of_payment,
|
||||
"reference_no": self.name,
|
||||
"reference_date": nowdate(),
|
||||
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
|
||||
"remarks": "Payment Entry against {} {} via Payment Request {}".format(
|
||||
self.reference_doctype, self.reference_name, self.name
|
||||
),
|
||||
}
|
||||
@@ -334,21 +330,17 @@ class PaymentRequest(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
|
||||
amount = payment_entry.base_paid_amount
|
||||
else:
|
||||
amount = self.grand_total
|
||||
|
||||
payment_entry.received_amount = amount
|
||||
payment_entry.get("references")[0].allocated_amount = amount
|
||||
|
||||
for dimension in get_accounting_dimensions():
|
||||
payment_entry.update({dimension: self.get(dimension)})
|
||||
|
||||
if payment_entry.difference_amount:
|
||||
company_details = get_company_defaults(ref_doc.company)
|
||||
|
||||
payment_entry.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": company_details.exchange_gain_loss_account,
|
||||
"cost_center": company_details.cost_center,
|
||||
"amount": payment_entry.difference_amount,
|
||||
},
|
||||
)
|
||||
|
||||
if submit:
|
||||
payment_entry.insert(ignore_permissions=True)
|
||||
payment_entry.submit()
|
||||
@@ -443,15 +435,11 @@ def make_payment_request(**args):
|
||||
frappe.db.set_value(
|
||||
"Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False
|
||||
)
|
||||
frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
|
||||
grand_total = grand_total - loyalty_amount
|
||||
|
||||
bank_account = (
|
||||
get_party_bank_account(args.get("party_type"), args.get("party"))
|
||||
if args.get("party_type")
|
||||
else ""
|
||||
get_party_bank_account(args.get("party_type"), args.get("party")) if args.get("party_type") else ""
|
||||
)
|
||||
|
||||
draft_payment_request = frappe.db.get_value(
|
||||
@@ -471,6 +459,12 @@ def make_payment_request(**args):
|
||||
pr = frappe.get_doc("Payment Request", draft_payment_request)
|
||||
else:
|
||||
pr = frappe.new_doc("Payment Request")
|
||||
|
||||
if not args.get("payment_request_type"):
|
||||
args["payment_request_type"] = (
|
||||
"Outward" if args.get("dt") in ["Purchase Order", "Purchase Invoice"] else "Inward"
|
||||
)
|
||||
|
||||
pr.update(
|
||||
{
|
||||
"payment_gateway_account": gateway_account.get("name"),
|
||||
@@ -529,9 +523,9 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
grand_total = flt(ref_doc.outstanding_amount)
|
||||
grand_total = flt(ref_doc.grand_total)
|
||||
else:
|
||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||
grand_total = flt(ref_doc.base_grand_total) / ref_doc.conversion_rate
|
||||
elif dt == "Sales Invoice":
|
||||
for pay in ref_doc.payments:
|
||||
if pay.type == "Phone" and pay.account == payment_account:
|
||||
@@ -632,7 +626,11 @@ def update_payment_req_status(doc, method):
|
||||
|
||||
if payment_request_name:
|
||||
ref_details = get_reference_details(
|
||||
ref.reference_doctype, ref.reference_name, doc.party_account_currency
|
||||
ref.reference_doctype,
|
||||
ref.reference_name,
|
||||
doc.party_account_currency,
|
||||
doc.party_type,
|
||||
doc.party,
|
||||
)
|
||||
pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
|
||||
status = pay_req_doc.status
|
||||
|
||||
@@ -86,6 +86,8 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
dn=si_usd.name,
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
@@ -93,11 +95,56 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pe = pr.create_payment_entry()
|
||||
pr.create_payment_entry()
|
||||
pr.load_from_db()
|
||||
|
||||
self.assertEqual(pr.status, "Paid")
|
||||
|
||||
def test_multiple_payment_entry_against_purchase_invoice(self):
|
||||
purchase_invoice = make_purchase_invoice(
|
||||
customer="_Test Supplier USD",
|
||||
debit_to="_Test Payable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=50,
|
||||
)
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pr.grand_total = pr.grand_total / 2
|
||||
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
|
||||
purchase_invoice.load_from_db()
|
||||
self.assertEqual(purchase_invoice.status, "Partly Paid")
|
||||
|
||||
pr = make_payment_request(
|
||||
dt="Purchase Invoice",
|
||||
party_type="Supplier",
|
||||
party="_Test Supplier USD",
|
||||
dn=purchase_invoice.name,
|
||||
recipient_id="user@example.com",
|
||||
mute_email=1,
|
||||
payment_gateway_account="_Test Gateway - USD",
|
||||
return_doc=1,
|
||||
)
|
||||
|
||||
pr.save()
|
||||
pr.submit()
|
||||
pr.create_payment_entry()
|
||||
|
||||
purchase_invoice.load_from_db()
|
||||
self.assertEqual(purchase_invoice.status, "Paid")
|
||||
|
||||
def test_payment_entry(self):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
|
||||
@@ -158,7 +205,7 @@ class TestPaymentRequest(unittest.TestCase):
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
for _i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_gle[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_gle[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_gle[gle.account][2], gle.credit)
|
||||
|
||||
@@ -35,10 +35,11 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-04-01 15:35:18.112619",
|
||||
"modified": "2024-04-07 11:26:42.021585",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Terms Template",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -76,6 +77,15 @@
|
||||
"role": "Accounts Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"select": 1,
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
||||
@@ -36,9 +36,7 @@ class PaymentTermsTemplate(Document):
|
||||
total_portion += flt(term.get("invoice_portion", 0))
|
||||
|
||||
if flt(total_portion, 2) != 100.00:
|
||||
frappe.msgprint(
|
||||
_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red"
|
||||
)
|
||||
frappe.msgprint(_("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red")
|
||||
|
||||
def validate_terms(self):
|
||||
terms = []
|
||||
|
||||
@@ -67,7 +67,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
@@ -109,9 +110,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self
|
||||
)
|
||||
|
||||
self.year_start_date = get_fiscal_year(
|
||||
self.posting_date, self.fiscal_year, company=self.company
|
||||
)[1]
|
||||
self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1]
|
||||
|
||||
self.check_if_previous_year_closed()
|
||||
|
||||
@@ -141,7 +140,8 @@ class PeriodClosingVoucher(AccountsController):
|
||||
previous_fiscal_year = get_fiscal_year(last_year_closing, company=self.company, boolean=True)
|
||||
|
||||
if previous_fiscal_year and not frappe.db.exists(
|
||||
"GL Entry", {"posting_date": ("<=", last_year_closing), "company": self.company}
|
||||
"GL Entry",
|
||||
{"posting_date": ("<=", last_year_closing), "company": self.company, "is_cancelled": 0},
|
||||
):
|
||||
return
|
||||
|
||||
@@ -224,7 +224,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) > 0
|
||||
else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
@@ -248,7 +250,9 @@ class PeriodClosingVoucher(AccountsController):
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
"is_period_closing_voucher_entry": 1,
|
||||
},
|
||||
item=acc,
|
||||
|
||||
@@ -70,7 +70,7 @@ class POSClosingEntry(StatusUpdater):
|
||||
for key, value in pos_occurences.items():
|
||||
if len(value) > 1:
|
||||
error_list.append(
|
||||
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
|
||||
_(f"{frappe.bold(key)} is added multiple times on rows: {frappe.bold(value)}")
|
||||
)
|
||||
|
||||
if error_list:
|
||||
@@ -165,9 +165,7 @@ def get_pos_invoices(start, end, pos_profile, user):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
data = list(
|
||||
filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)
|
||||
)
|
||||
data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data))
|
||||
# need to get taxes and payments so can't avoid get_doc
|
||||
data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data]
|
||||
|
||||
@@ -238,7 +236,11 @@ def make_closing_entry_from_opening(opening_entry):
|
||||
else:
|
||||
payments.append(
|
||||
frappe._dict(
|
||||
{"mode_of_payment": p.mode_of_payment, "opening_amount": 0, "expected_amount": p.amount}
|
||||
{
|
||||
"mode_of_payment": p.mode_of_payment,
|
||||
"opening_amount": 0,
|
||||
"expected_amount": p.amount,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -62,9 +62,7 @@ class TestPOSClosingEntry(unittest.TestCase):
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
pos_inv = create_pos_invoice(
|
||||
rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1
|
||||
)
|
||||
pos_inv = create_pos_invoice(rate=3500, do_not_submit=1, item_name="Test Item", without_item_code=1)
|
||||
pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500})
|
||||
pos_inv.submit()
|
||||
|
||||
@@ -211,7 +209,7 @@ def get_test_item_qty(pos_profile):
|
||||
item_group="All Item Groups",
|
||||
)
|
||||
|
||||
test_item_qty = [item for item in test_item_pos["items"] if item["item_code"] == "_Test Item"][
|
||||
0
|
||||
].get("actual_qty")
|
||||
test_item_qty = next(item for item in test_item_pos["items"] if item["item_code"] == "_Test Item").get(
|
||||
"actual_qty"
|
||||
)
|
||||
return test_item_qty
|
||||
|
||||
@@ -775,7 +775,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "other_charges_calculation",
|
||||
"fieldtype": "Long Text",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Taxes and Charges Calculation",
|
||||
"no_copy": 1,
|
||||
"oldfieldtype": "HTML",
|
||||
@@ -1563,7 +1563,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-20 12:27:12.848149",
|
||||
"modified": "2024-03-20 16:00:34.268756",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user