mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-04 10:14:39 +00:00
Compare commits
352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd3991dd14 | ||
|
|
e586c07bdd | ||
|
|
8af6a113d1 | ||
|
|
9b9772eb14 | ||
|
|
88ecc933f7 | ||
|
|
6df9b53682 | ||
|
|
47e97e0b3d | ||
|
|
fec6e3724e | ||
|
|
668b092f6b | ||
|
|
fb7d3b7878 | ||
|
|
f6857b4698 | ||
|
|
ce601afc4e | ||
|
|
1b827f61f7 | ||
|
|
633a1703dc | ||
|
|
abfd975eb6 | ||
|
|
9106b01c04 | ||
|
|
3a0cdf30ce | ||
|
|
756ac6c587 | ||
|
|
b5b1b2da32 | ||
|
|
cdd378c518 | ||
|
|
14565ed8b1 | ||
|
|
44b290f58e | ||
|
|
d23b93a462 | ||
|
|
80f4a11d60 | ||
|
|
9caa39195c | ||
|
|
879946e2c8 | ||
|
|
bdf81a43c6 | ||
|
|
cb681e0b96 | ||
|
|
65554bdabf | ||
|
|
0d2d45f32f | ||
|
|
2631224e49 | ||
|
|
0a080efce2 | ||
|
|
48884366ea | ||
|
|
15c1af3d8a | ||
|
|
350f75877a | ||
|
|
aa7fede0dc | ||
|
|
e9f2ab5caf | ||
|
|
7076c23a4f | ||
|
|
bdfd682664 | ||
|
|
fa9fa97e05 | ||
|
|
c41e1d7d71 | ||
|
|
8f8c0a597b | ||
|
|
6f96e5dcd4 | ||
|
|
f8c58b6893 | ||
|
|
0e7840301f | ||
|
|
19f08afcef | ||
|
|
42037f9f73 | ||
|
|
d9efa662d4 | ||
|
|
ee147e62d5 | ||
|
|
bc88415e73 | ||
|
|
5463f0b137 | ||
|
|
a8fc17e0ae | ||
|
|
0575b105d0 | ||
|
|
4c8dbeddec | ||
|
|
9a8ee62d5a | ||
|
|
3a7c69fc71 | ||
|
|
dd116a3071 | ||
|
|
3f8928be5c | ||
|
|
26928b395b | ||
|
|
fe9e0c2121 | ||
|
|
77cf2e5475 | ||
|
|
2772a911ed | ||
|
|
6d121b8107 | ||
|
|
c30dda3328 | ||
|
|
b2e6fdc0cb | ||
|
|
75af689f77 | ||
|
|
3697e8f1f9 | ||
|
|
1d8050d24d | ||
|
|
897a467846 | ||
|
|
f06a8fd01c | ||
|
|
b139ec6ec0 | ||
|
|
e97eaccdfb | ||
|
|
b02ed0d9a8 | ||
|
|
4206f01c05 | ||
|
|
ab56470171 | ||
|
|
54e822d83a | ||
|
|
8af1e49e96 | ||
|
|
1783594178 | ||
|
|
762a46a5e3 | ||
|
|
19f0676592 | ||
|
|
098603dd35 | ||
|
|
9b2b46737e | ||
|
|
457846e34c | ||
|
|
5a54296686 | ||
|
|
e3c1d736ce | ||
|
|
c8e3ce48e1 | ||
|
|
38e27a68d5 | ||
|
|
e509664d4f | ||
|
|
2fb3659694 | ||
|
|
1145149f0e | ||
|
|
dd20bf931b | ||
|
|
709f94c8d3 | ||
|
|
2933c4f1c5 | ||
|
|
8133be4868 | ||
|
|
c0f9ff4995 | ||
|
|
7b6a1e5184 | ||
|
|
362003ec5f | ||
|
|
30e137e9f2 | ||
|
|
08a4781de7 | ||
|
|
14706d4326 | ||
|
|
fd09d1c4c3 | ||
|
|
afbbf26f15 | ||
|
|
f83fcf5261 | ||
|
|
5879475a00 | ||
|
|
f42225bc82 | ||
|
|
c73b76fdb6 | ||
|
|
e451916803 | ||
|
|
72255fae80 | ||
|
|
42f5888426 | ||
|
|
545f956160 | ||
|
|
1226f3294f | ||
|
|
5c38645560 | ||
|
|
9a755ca23d | ||
|
|
a5a08c9889 | ||
|
|
ed5f39c2c2 | ||
|
|
a1842103b6 | ||
|
|
4d54430010 | ||
|
|
e855866820 | ||
|
|
580641f55c | ||
|
|
816ce879f9 | ||
|
|
736c34e61a | ||
|
|
f4858fbf8a | ||
|
|
b0c042de1b | ||
|
|
c463df4fd1 | ||
|
|
33cd14f859 | ||
|
|
f5f4902494 | ||
|
|
144594b3e1 | ||
|
|
64b416b4dd | ||
|
|
4e83d0baa6 | ||
|
|
a01e2ca9ac | ||
|
|
bf6e1b67a5 | ||
|
|
148342a132 | ||
|
|
d1a91177e5 | ||
|
|
8d6034de16 | ||
|
|
d306bb080a | ||
|
|
eb3e6ff145 | ||
|
|
82c3d862ce | ||
|
|
5eb5bf7102 | ||
|
|
51d9d0a454 | ||
|
|
344c339484 | ||
|
|
bef9dd79e7 | ||
|
|
815696964a | ||
|
|
f0ae02e921 | ||
|
|
f50b4d80f1 | ||
|
|
7ad795bec5 | ||
|
|
662d9cb5aa | ||
|
|
8ac718d98c | ||
|
|
3810b02023 | ||
|
|
25b37737a2 | ||
|
|
0e39e5e868 | ||
|
|
fedde7fe3b | ||
|
|
551c96e1e5 | ||
|
|
78c34d71e2 | ||
|
|
453249d868 | ||
|
|
35ec125b34 | ||
|
|
2f3ed23a9d | ||
|
|
6597c74d6c | ||
|
|
e33fb3b242 | ||
|
|
d844a2b990 | ||
|
|
3ba2b9ed2e | ||
|
|
cca2fcec54 | ||
|
|
80230fec3e | ||
|
|
7021e3adb1 | ||
|
|
69c0d81c55 | ||
|
|
28dfc13dc6 | ||
|
|
5f381cd520 | ||
|
|
8571e6c5d2 | ||
|
|
e4ce6fa195 | ||
|
|
ef3d352a17 | ||
|
|
fc611cf86b | ||
|
|
a5489ee2ac | ||
|
|
85f3a5d318 | ||
|
|
b1473c9932 | ||
|
|
82d8379188 | ||
|
|
39b5147768 | ||
|
|
d732083166 | ||
|
|
693007adfe | ||
|
|
f43ea0d6ff | ||
|
|
f9f42c7e98 | ||
|
|
06f204a8d6 | ||
|
|
47df41fdbd | ||
|
|
878d7477bc | ||
|
|
29aa4a0222 | ||
|
|
5045ad6be6 | ||
|
|
665021270f | ||
|
|
5923a80a0f | ||
|
|
5cc3978c16 | ||
|
|
5630e8189b | ||
|
|
d121282439 | ||
|
|
3c75e55cb9 | ||
|
|
aa22cccf99 | ||
|
|
7d9c9884dc | ||
|
|
e03eaa31fe | ||
|
|
f44a79fa73 | ||
|
|
943acbfea8 | ||
|
|
e955eeeabc | ||
|
|
d5910fba44 | ||
|
|
c8622fb46f | ||
|
|
c020789bfc | ||
|
|
28cd79a040 | ||
|
|
70014028e9 | ||
|
|
79cbe1cce8 | ||
|
|
aeac43ccf9 | ||
|
|
a5e6b371ac | ||
|
|
83a1b836f9 | ||
|
|
6e03c09017 | ||
|
|
a5fde5d933 | ||
|
|
a5823547d3 | ||
|
|
00968badf5 | ||
|
|
6aabab26d8 | ||
|
|
de86e8fb95 | ||
|
|
e5048812fd | ||
|
|
237bd6e831 | ||
|
|
c2953bc3e0 | ||
|
|
9d17d3ff06 | ||
|
|
33a16086ef | ||
|
|
6e0d22c23b | ||
|
|
2ec18eb4cf | ||
|
|
87595bdb7e | ||
|
|
789dfd6774 | ||
|
|
56ef0baa9d | ||
|
|
11e4fcb058 | ||
|
|
a3568c1b27 | ||
|
|
2a48a7b427 | ||
|
|
4418862965 | ||
|
|
e5aae90078 | ||
|
|
e2818afb62 | ||
|
|
7740ceb27e | ||
|
|
3d3da75726 | ||
|
|
8c5d644671 | ||
|
|
ca9b02fb53 | ||
|
|
74abe94711 | ||
|
|
1b827e6b67 | ||
|
|
3b23fc1eba | ||
|
|
1743413e00 | ||
|
|
c11b98fa9f | ||
|
|
a2c25ed49e | ||
|
|
2ad157bd77 | ||
|
|
7cc0129302 | ||
|
|
f7e436fe71 | ||
|
|
1ab83c5c02 | ||
|
|
6faf459f97 | ||
|
|
40058c2617 | ||
|
|
78ad0eaa74 | ||
|
|
7131ff28fd | ||
|
|
33d89b4a8b | ||
|
|
fef381e8eb | ||
|
|
9e148b4277 | ||
|
|
e7ca833929 | ||
|
|
322102e56b | ||
|
|
7c4f83ed60 | ||
|
|
55da91cb34 | ||
|
|
9f090d2861 | ||
|
|
93bc1c5382 | ||
|
|
b12bd5f558 | ||
|
|
be8a22d33b | ||
|
|
757c168a8d | ||
|
|
b0b00dc869 | ||
|
|
09b92fd78c | ||
|
|
f1a1fc6c5b | ||
|
|
8f26d62b35 | ||
|
|
8e9d72bd2e | ||
|
|
7461239218 | ||
|
|
2f356dcc6c | ||
|
|
4640febc83 | ||
|
|
c53dc06f80 | ||
|
|
a981b79865 | ||
|
|
050339c88e | ||
|
|
461780da22 | ||
|
|
e8db543028 | ||
|
|
79fd38cf3f | ||
|
|
dfb1e105bd | ||
|
|
2e7043ca90 | ||
|
|
7c8194a1a8 | ||
|
|
15f5e8d4ff | ||
|
|
3738ea5794 | ||
|
|
c11aebaaae | ||
|
|
0cf6144b3f | ||
|
|
41a7d3fd60 | ||
|
|
2b0b24f7c3 | ||
|
|
f72fc73913 | ||
|
|
8a331e0f26 | ||
|
|
da913d49a7 | ||
|
|
21aea52c32 | ||
|
|
9b90323d53 | ||
|
|
994272b966 | ||
|
|
fee4cd5f40 | ||
|
|
3023dbbe95 | ||
|
|
cc21241887 | ||
|
|
66fdd1c13f | ||
|
|
5723a200c5 | ||
|
|
35c9493336 | ||
|
|
b235b95bed | ||
|
|
50abbded34 | ||
|
|
31b479d71f | ||
|
|
769736ffea | ||
|
|
b79ddbbf60 | ||
|
|
1c5e36c7b6 | ||
|
|
eaf577f078 | ||
|
|
e02ad91c39 | ||
|
|
d4a6035c83 | ||
|
|
99226d3811 | ||
|
|
a1f7e35914 | ||
|
|
915c4819b6 | ||
|
|
d817c50581 | ||
|
|
8ba1e0f31e | ||
|
|
3ad5d676ab | ||
|
|
05d24e3665 | ||
|
|
3c0cc024aa | ||
|
|
88c8c36805 | ||
|
|
7c4a9b56ff | ||
|
|
16e554dd7b | ||
|
|
3896d41e95 | ||
|
|
12625d87b0 | ||
|
|
cbb8dd6aa6 | ||
|
|
9372d46c08 | ||
|
|
dff61ab759 | ||
|
|
551190af30 | ||
|
|
2c54e763e4 | ||
|
|
6b866e24f6 | ||
|
|
be2990ec88 | ||
|
|
c0f7f7da42 | ||
|
|
b34c78c4e7 | ||
|
|
4ada090cb2 | ||
|
|
5677f25215 | ||
|
|
f1687cfb14 | ||
|
|
a00459aec3 | ||
|
|
8757435898 | ||
|
|
dcf62dc548 | ||
|
|
b835760b0b | ||
|
|
bd0c20f789 | ||
|
|
5ef98fcea1 | ||
|
|
985b232251 | ||
|
|
5e28d0234e | ||
|
|
5e03a4e9e2 | ||
|
|
e98e64f925 | ||
|
|
9ce281d008 | ||
|
|
27a3f2ce55 | ||
|
|
61858a60c2 | ||
|
|
bb5eeb6bd6 | ||
|
|
3494c9ccb6 | ||
|
|
a0df23415b | ||
|
|
8510c398a4 | ||
|
|
bae476cc99 | ||
|
|
4dfc660cc0 | ||
|
|
f47be46717 | ||
|
|
8d9305ee5f | ||
|
|
bc94358e98 | ||
|
|
5e98679f91 | ||
|
|
6819f0106d | ||
|
|
e3de229b82 | ||
|
|
3e67994cc7 |
22
CODEOWNERS
22
CODEOWNERS
@@ -3,13 +3,13 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @anandbaburajan @deepeshgarg007
|
||||
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
erpnext/loan_management/ @deepeshgarg007
|
||||
erpnext/regional @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @deepeshgarg007
|
||||
pos*
|
||||
|
||||
erpnext/buying/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
|
||||
@@ -18,12 +18,8 @@ erpnext/quality_management/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/stock/ @rohitwaghchaure @s-aga-r
|
||||
erpnext/subcontracting @rohitwaghchaure @s-aga-r
|
||||
|
||||
erpnext/crm/ @NagariaHussain
|
||||
erpnext/education/ @rutwikhdev
|
||||
erpnext/projects/ @ruchamahabal
|
||||
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
|
||||
|
||||
.github/ @ankush
|
||||
.github/ @deepeshgarg007
|
||||
pyproject.toml @ankush
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.20.3"
|
||||
__version__ = "14.25.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
@@ -120,12 +121,14 @@ def get_region(company=None):
|
||||
|
||||
You can also set global company flag in `frappe.flags.company`
|
||||
"""
|
||||
if company or frappe.flags.company:
|
||||
return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
|
||||
elif frappe.flags.country:
|
||||
return frappe.flags.country
|
||||
else:
|
||||
return frappe.get_system_settings("country")
|
||||
|
||||
if not company:
|
||||
company = frappe.local.flags.company
|
||||
|
||||
if company:
|
||||
return frappe.get_cached_value("Company", company, "country")
|
||||
|
||||
return frappe.flags.country or frappe.get_system_settings("country")
|
||||
|
||||
|
||||
def allow_regional(fn):
|
||||
@@ -136,6 +139,7 @@ def allow_regional(fn):
|
||||
def myfunction():
|
||||
pass"""
|
||||
|
||||
@functools.wraps(fn)
|
||||
def caller(*args, **kwargs):
|
||||
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
|
||||
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
"root_type",
|
||||
"report_type",
|
||||
"account_currency",
|
||||
"inter_company_account",
|
||||
"column_break1",
|
||||
"parent_account",
|
||||
"account_type",
|
||||
@@ -34,15 +33,11 @@
|
||||
{
|
||||
"fieldname": "properties",
|
||||
"fieldtype": "Section Break",
|
||||
"oldfieldtype": "Section Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break0",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -53,9 +48,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "account_name",
|
||||
"oldfieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "account_number",
|
||||
@@ -63,17 +56,13 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Account Number",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_group",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Group",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Is Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
@@ -85,9 +74,7 @@
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 1,
|
||||
"reqd": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "root_type",
|
||||
@@ -95,9 +82,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Root Type",
|
||||
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "report_type",
|
||||
@@ -105,32 +90,18 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Report Type",
|
||||
"options": "\nBalance Sheet\nProfit and Loss",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_group==0",
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "inter_company_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Inter Company Account",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
@@ -142,9 +113,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Account",
|
||||
"reqd": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"description": "Setting Account Type helps in selecting this Account in transactions.",
|
||||
@@ -154,9 +123,7 @@
|
||||
"label": "Account Type",
|
||||
"oldfieldname": "account_type",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary"
|
||||
},
|
||||
{
|
||||
"description": "Rate at which this tax is applied",
|
||||
@@ -164,9 +131,7 @@
|
||||
"fieldtype": "Float",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "tax_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
{
|
||||
"description": "If the account is frozen, entries are allowed to restricted users.",
|
||||
@@ -175,17 +140,13 @@
|
||||
"label": "Frozen",
|
||||
"oldfieldname": "freeze_account",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "No\nYes",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "No\nYes"
|
||||
},
|
||||
{
|
||||
"fieldname": "balance_must_be",
|
||||
"fieldtype": "Select",
|
||||
"label": "Balance must be",
|
||||
"options": "\nDebit\nCredit",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"options": "\nDebit\nCredit"
|
||||
},
|
||||
{
|
||||
"fieldname": "lft",
|
||||
@@ -194,9 +155,7 @@
|
||||
"label": "Lft",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rgt",
|
||||
@@ -205,9 +164,7 @@
|
||||
"label": "Rgt",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "old_parent",
|
||||
@@ -215,33 +172,27 @@
|
||||
"hidden": 1,
|
||||
"label": "Old Parent",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)",
|
||||
"fieldname": "include_in_gross",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include in gross",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Include in gross"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Disable"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-money",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-11 15:15:54.338622",
|
||||
"modified": "2023-04-11 16:08:46.983677",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Account",
|
||||
@@ -301,5 +252,6 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -201,8 +201,11 @@ class Account(NestedSet):
|
||||
)
|
||||
|
||||
def validate_account_currency(self):
|
||||
self.currency_explicitly_specified = True
|
||||
|
||||
if not self.account_currency:
|
||||
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
self.currency_explicitly_specified = False
|
||||
|
||||
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
|
||||
|
||||
@@ -248,8 +251,10 @@ class Account(NestedSet):
|
||||
{
|
||||
"company": company,
|
||||
# parent account's currency should be passed down to child account's curreny
|
||||
# if it is None, it picks it up from default company currency, which might be unintended
|
||||
"account_currency": erpnext.get_company_currency(company),
|
||||
# if currency explicitly specified by user, child will inherit. else, default currency will be used.
|
||||
"account_currency": self.account_currency
|
||||
if self.currency_explicitly_specified
|
||||
else erpnext.get_company_currency(company),
|
||||
"parent_account": parent_acc_name_map[company],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
|
||||
from erpnext.accounts.doctype.account.account import merge_account, update_account_number
|
||||
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
|
||||
|
||||
test_dependencies = ["Company"]
|
||||
|
||||
|
||||
class TestAccount(unittest.TestCase):
|
||||
def test_rename_account(self):
|
||||
@@ -188,6 +191,58 @@ class TestAccount(unittest.TestCase):
|
||||
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
|
||||
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5")
|
||||
|
||||
def test_account_currency_sync(self):
|
||||
"""
|
||||
In a parent->child company setup, child should inherit parent account currency if explicitly specified.
|
||||
"""
|
||||
|
||||
make_test_records("Company")
|
||||
|
||||
frappe.local.flags.pop("ignore_root_company_validation", None)
|
||||
|
||||
def create_bank_account():
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = "_Test Bank JPY"
|
||||
|
||||
acc.parent_account = "Temporary Accounts - _TC6"
|
||||
acc.company = "_Test Company 6"
|
||||
return acc
|
||||
|
||||
acc = create_bank_account()
|
||||
# Explicitly set currency
|
||||
acc.account_currency = "JPY"
|
||||
acc.insert()
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "_Test Bank JPY",
|
||||
"account_currency": "JPY",
|
||||
"company": "_Test Company 7",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
|
||||
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
|
||||
|
||||
acc = create_bank_account()
|
||||
# default currency is used
|
||||
acc.insert()
|
||||
self.assertTrue(
|
||||
frappe.db.exists(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "_Test Bank JPY",
|
||||
"account_currency": "USD",
|
||||
"company": "_Test Company 7",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
|
||||
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
|
||||
|
||||
def test_child_company_account_rename_sync(self):
|
||||
frappe.local.flags.pop("ignore_root_company_validation", None)
|
||||
|
||||
@@ -297,7 +352,7 @@ def _make_test_records(verbose=None):
|
||||
# fixed asset depreciation
|
||||
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
|
||||
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
|
||||
["_Test Depreciations", "Expenses", 0, None, None],
|
||||
["_Test Depreciations", "Expenses", 0, "Depreciation", None],
|
||||
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
|
||||
# Receivable / Payable Account
|
||||
["_Test Receivable", "Current Assets", 0, "Receivable", None],
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
"column_break_17",
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
"journals_section",
|
||||
"merge_similar_account_heads",
|
||||
"report_setting_section",
|
||||
"use_custom_cash_flow",
|
||||
"deferred_accounting_settings_section",
|
||||
@@ -38,6 +40,8 @@
|
||||
"show_payment_schedule_in_print",
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"section_break_jpd0",
|
||||
"auto_reconcile_payments",
|
||||
"stale_days",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
@@ -168,11 +172,6 @@
|
||||
"fieldtype": "Int",
|
||||
"label": "Stale Days"
|
||||
},
|
||||
{
|
||||
"fieldname": "report_settings_sb",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Report Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Only select this if you have set up the Cash Flow Mapper documents",
|
||||
@@ -182,6 +181,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Payment Terms from orders will be fetched into the invoices as is",
|
||||
"fieldname": "automatically_fetch_payment_terms",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Fetch Payment Terms from Order"
|
||||
@@ -355,6 +355,29 @@
|
||||
"fieldname": "book_tax_discount_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Tax Loss on Early Payment Discount"
|
||||
},
|
||||
{
|
||||
"fieldname": "journals_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Journals"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Rows with Same Account heads will be merged on Ledger",
|
||||
"fieldname": "merge_similar_account_heads",
|
||||
"fieldtype": "Check",
|
||||
"label": "Merge Similar Account Heads"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_jpd0",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Reconciliations"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "auto_reconcile_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Reconcile Payments"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -362,7 +385,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-28 09:50:20.375233",
|
||||
"modified": "2023-04-21 13:11:37.130743",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -56,7 +56,7 @@ class BankClearance(Document):
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no as cheque_number, reference_date as cheque_date,
|
||||
if(paid_from=%(account)s, paid_amount, 0) as credit,
|
||||
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
|
||||
if(paid_from=%(account)s, 0, received_amount) as debit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
|
||||
@@ -46,7 +46,7 @@ class BankTransaction(StatusUpdater):
|
||||
def add_payment_entries(self, vouchers):
|
||||
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||
if 0.0 >= self.unallocated_amount:
|
||||
frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
|
||||
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
|
||||
added = False
|
||||
for voucher in vouchers:
|
||||
@@ -114,9 +114,7 @@ class BankTransaction(StatusUpdater):
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
frappe.throw(
|
||||
frappe._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
|
||||
)
|
||||
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
self.reload()
|
||||
|
||||
@@ -178,7 +176,9 @@ def get_clearance_details(transaction, payment_entry):
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
frappe.throw(
|
||||
frappe._(f"Voucher {payment_entry.payment_entry} value is broken: {gle['amount']}")
|
||||
frappe._("Voucher {0} value is broken: {1}").format(
|
||||
payment_entry.payment_entry, gle["amount"]
|
||||
)
|
||||
)
|
||||
|
||||
unmatched_gles -= 1
|
||||
@@ -281,10 +281,13 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Journal Entry":
|
||||
return frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||
"sum(credit_in_account_currency)",
|
||||
return abs(
|
||||
frappe.db.get_value(
|
||||
"Journal Entry Account",
|
||||
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
|
||||
"sum(debit_in_account_currency-credit_in_account_currency)",
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
elif payment_entry.payment_document == "Expense Claim":
|
||||
|
||||
@@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("bank_account", "account", "account");
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement'];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -69,6 +69,7 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.set_account_and_party_balance()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_depr_entry_voucher_type()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
@@ -130,6 +131,13 @@ class JournalEntry(AccountsController):
|
||||
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
|
||||
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
|
||||
|
||||
def validate_depr_entry_voucher_type(self):
|
||||
if (
|
||||
any(d.account_type == "Depreciation" for d in self.get("accounts"))
|
||||
and self.voucher_type != "Depreciation Entry"
|
||||
):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
|
||||
for account in stock_accounts:
|
||||
@@ -233,25 +241,30 @@ class JournalEntry(AccountsController):
|
||||
self.remove(d)
|
||||
|
||||
def update_asset_value(self):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
continue
|
||||
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
|
||||
fb_idx = 1
|
||||
if self.finance_book:
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
fb_idx = fb_row.idx
|
||||
break
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation -= d.debit
|
||||
fb_row.db_update()
|
||||
else:
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation - d.debit)
|
||||
|
||||
asset.set_status()
|
||||
|
||||
@@ -316,35 +329,35 @@ class JournalEntry(AccountsController):
|
||||
if self.voucher_type != "Depreciation Entry":
|
||||
return
|
||||
|
||||
processed_assets = []
|
||||
|
||||
for d in self.get("accounts"):
|
||||
if (
|
||||
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
|
||||
d.reference_type == "Asset"
|
||||
and d.reference_name
|
||||
and d.account_type == "Depreciation"
|
||||
and d.debit
|
||||
):
|
||||
processed_assets.append(d.reference_name)
|
||||
|
||||
asset = frappe.get_doc("Asset", d.reference_name)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
fb_idx = None
|
||||
for s in asset.get("schedules"):
|
||||
if s.journal_entry == self.name:
|
||||
s.db_set("journal_entry", None)
|
||||
|
||||
idx = cint(s.finance_book_id) or 1
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation += s.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
asset.set_status()
|
||||
|
||||
fb_idx = cint(s.finance_book_id) or 1
|
||||
break
|
||||
if not fb_idx:
|
||||
fb_idx = 1
|
||||
if self.finance_book:
|
||||
for fb_row in asset.get("finance_books"):
|
||||
if fb_row.finance_book == self.finance_book:
|
||||
fb_idx = fb_row.idx
|
||||
break
|
||||
fb_row = asset.get("finance_books")[fb_idx - 1]
|
||||
fb_row.value_after_depreciation += d.debit
|
||||
fb_row.db_update()
|
||||
else:
|
||||
depr_value = d.debit or d.credit
|
||||
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
|
||||
|
||||
asset.set_status()
|
||||
asset.db_set("value_after_depreciation", asset.value_after_depreciation + d.debit)
|
||||
asset.set_status()
|
||||
|
||||
def unlink_inter_company_jv(self):
|
||||
if (
|
||||
@@ -878,6 +891,8 @@ class JournalEntry(AccountsController):
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
merge_entries = frappe.db.get_single_value("Accounts Settings", "merge_similar_account_heads")
|
||||
|
||||
gl_map = self.build_gl_map()
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
|
||||
update_outstanding = "No"
|
||||
@@ -885,7 +900,13 @@ class JournalEntry(AccountsController):
|
||||
update_outstanding = "Yes"
|
||||
|
||||
if gl_map:
|
||||
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
|
||||
make_gl_entries(
|
||||
gl_map,
|
||||
cancel=cancel,
|
||||
adv_adj=adv_adj,
|
||||
merge_entries=merge_entries,
|
||||
update_outstanding=update_outstanding,
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_balance(self, difference_account=None):
|
||||
|
||||
@@ -287,10 +287,6 @@ class TestJournalEntry(unittest.TestCase):
|
||||
jv.submit()
|
||||
|
||||
def test_inter_company_jv(self):
|
||||
frappe.db.set_value("Account", "Sales Expenses - _TC", "inter_company_account", 1)
|
||||
frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1)
|
||||
frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1)
|
||||
frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1)
|
||||
jv = make_journal_entry(
|
||||
"Sales Expenses - _TC",
|
||||
"Buildings - _TC",
|
||||
|
||||
@@ -2,6 +2,21 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Journal Entry Template", {
|
||||
onload: function(frm) {
|
||||
if(frm.is_new()) {
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
|
||||
callback: function(r){
|
||||
if(r.message) {
|
||||
frm.set_df_property("naming_series", "options", r.message.split("\n"));
|
||||
frm.set_value("naming_series", r.message.split("\n")[0]);
|
||||
frm.refresh_field("naming_series");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
refresh: function(frm) {
|
||||
frappe.model.set_default_values(frm.doc);
|
||||
|
||||
@@ -19,18 +34,6 @@ frappe.ui.form.on("Journal Entry Template", {
|
||||
|
||||
return { filters: filters };
|
||||
});
|
||||
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry_template.journal_entry_template.get_naming_series",
|
||||
callback: function(r){
|
||||
if(r.message){
|
||||
frm.set_df_property("naming_series", "options", r.message.split("\n"));
|
||||
frm.set_value("naming_series", r.message.split("\n")[0]);
|
||||
frm.refresh_field("naming_series");
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
voucher_type: function(frm) {
|
||||
var add_accounts = function(doc, r) {
|
||||
|
||||
@@ -905,7 +905,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
function(d) { return flt(d.amount) }));
|
||||
|
||||
frm.set_value("difference_amount", difference_amount - total_deductions +
|
||||
frm.doc.base_total_taxes_and_charges);
|
||||
flt(frm.doc.base_total_taxes_and_charges));
|
||||
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
},
|
||||
@@ -971,29 +971,48 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message) {
|
||||
var write_off_row = $.map(frm.doc["deductions"] || [], function(t) {
|
||||
const write_off_row = $.map(frm.doc["deductions"] || [], function(t) {
|
||||
return t.account==r.message[account] ? t : null; });
|
||||
|
||||
var row = [];
|
||||
|
||||
var difference_amount = flt(frm.doc.difference_amount,
|
||||
const difference_amount = flt(frm.doc.difference_amount,
|
||||
precision("difference_amount"));
|
||||
|
||||
if (!write_off_row.length && difference_amount) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = r.message[account];
|
||||
row.cost_center = r.message["cost_center"];
|
||||
} else {
|
||||
row = write_off_row[0];
|
||||
}
|
||||
const add_deductions = (details) => {
|
||||
let row = null;
|
||||
if (!write_off_row.length && difference_amount) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = details[account];
|
||||
row.cost_center = details["cost_center"];
|
||||
} else {
|
||||
row = write_off_row[0];
|
||||
}
|
||||
|
||||
if (row) {
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
} else {
|
||||
frappe.msgprint(__("No gain or loss in the exchange rate"))
|
||||
}
|
||||
if (row) {
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
} else {
|
||||
frappe.msgprint(__("No gain or loss in the exchange rate"))
|
||||
}
|
||||
refresh_field("deductions");
|
||||
};
|
||||
|
||||
refresh_field("deductions");
|
||||
if (!r.message[account]) {
|
||||
frappe.prompt({
|
||||
label: __("Please Specify Account"),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
}
|
||||
})
|
||||
}, (values) => {
|
||||
const details = Object.assign({}, r.message, values);
|
||||
add_deductions(details);
|
||||
}, __(frappe.unscrub(account)));
|
||||
} else {
|
||||
add_deductions(r.message);
|
||||
}
|
||||
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ class PaymentEntry(AccountsController):
|
||||
def validate(self):
|
||||
self.setup_party_account_field()
|
||||
self.set_missing_values()
|
||||
self.set_missing_ref_details()
|
||||
self.validate_payment_type()
|
||||
self.validate_party_details()
|
||||
self.set_exchange_rate()
|
||||
@@ -219,11 +220,16 @@ class PaymentEntry(AccountsController):
|
||||
else self.paid_to_account_currency
|
||||
)
|
||||
|
||||
self.set_missing_ref_details()
|
||||
|
||||
def set_missing_ref_details(self, force=False):
|
||||
def set_missing_ref_details(
|
||||
self, force: bool = False, update_ref_details_only_for: list | 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
|
||||
):
|
||||
continue
|
||||
|
||||
ref_details = get_reference_details(
|
||||
d.reference_doctype, d.reference_name, self.party_account_currency
|
||||
)
|
||||
@@ -648,6 +654,28 @@ class PaymentEntry(AccountsController):
|
||||
self.precision("base_received_amount"),
|
||||
)
|
||||
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
|
||||
# This is so there are no Exchange Gain/Loss generated for such doctypes
|
||||
|
||||
exchange_rate = 1
|
||||
if self.payment_type == "Receive":
|
||||
exchange_rate = self.source_exchange_rate
|
||||
elif self.payment_type == "Pay":
|
||||
exchange_rate = self.target_exchange_rate
|
||||
|
||||
base_allocated_amount += flt(
|
||||
flt(d.allocated_amount) * flt(exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
else:
|
||||
base_allocated_amount += flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
|
||||
return base_allocated_amount
|
||||
|
||||
def set_total_allocated_amount(self):
|
||||
if self.payment_type == "Internal Transfer":
|
||||
return
|
||||
@@ -656,9 +684,7 @@ class PaymentEntry(AccountsController):
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount:
|
||||
total_allocated_amount += flt(d.allocated_amount)
|
||||
base_total_allocated_amount += flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
base_total_allocated_amount += self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
self.total_allocated_amount = abs(total_allocated_amount)
|
||||
self.base_total_allocated_amount = abs(base_total_allocated_amount)
|
||||
@@ -875,9 +901,7 @@ class PaymentEntry(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
allocated_amount_in_company_currency = flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount")
|
||||
)
|
||||
allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
|
||||
|
||||
gle.update(
|
||||
{
|
||||
@@ -1594,17 +1618,7 @@ def get_account_details(account, date, cost_center=None):
|
||||
@frappe.whitelist()
|
||||
def get_company_defaults(company):
|
||||
fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"]
|
||||
ret = frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||
|
||||
for fieldname in fields:
|
||||
if not ret[fieldname]:
|
||||
frappe.throw(
|
||||
_("Please set default {0} in Company {1}").format(
|
||||
frappe.get_meta("Company").get_label(fieldname), company
|
||||
)
|
||||
)
|
||||
|
||||
return ret
|
||||
return frappe.get_cached_value("Company", company, fields, as_dict=1)
|
||||
|
||||
|
||||
def get_outstanding_on_journal_entry(name):
|
||||
@@ -1697,7 +1711,10 @@ def get_payment_entry(
|
||||
):
|
||||
reference_doc = None
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
|
||||
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
|
||||
):
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
|
||||
if not party_type:
|
||||
@@ -1716,6 +1733,13 @@ def get_payment_entry(
|
||||
# bank or cash
|
||||
bank = get_bank_cash_account(doc, bank_account)
|
||||
|
||||
# if default bank or cash account is not set in company master and party has default company bank account, fetch it
|
||||
if party_type in ["Customer", "Supplier"] and not bank:
|
||||
party_bank_account = get_party_bank_account(party_type, doc.get(scrub(party_type)))
|
||||
if party_bank_account:
|
||||
account = frappe.db.get_value("Bank Account", party_bank_account, "account")
|
||||
bank = get_bank_cash_account(doc, account)
|
||||
|
||||
paid_amount, received_amount = set_paid_amount_and_received_amount(
|
||||
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
||||
)
|
||||
@@ -1764,7 +1788,12 @@ def get_payment_entry(
|
||||
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
|
||||
frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date))
|
||||
else:
|
||||
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value(
|
||||
if doc.doctype in (
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Purchase Order",
|
||||
"Sales Order",
|
||||
) and frappe.get_cached_value(
|
||||
"Payment Terms Template",
|
||||
{"name": doc.payment_terms_template},
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
@@ -1816,6 +1845,7 @@ def get_payment_entry(
|
||||
|
||||
pe.setup_party_account_field()
|
||||
pe.set_missing_values()
|
||||
pe.set_missing_ref_details()
|
||||
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
@@ -1926,19 +1956,27 @@ def set_paid_amount_and_received_amount(
|
||||
paid_amount = received_amount = 0
|
||||
if party_account_currency == bank.account_currency:
|
||||
paid_amount = received_amount = abs(outstanding_amount)
|
||||
elif payment_type == "Receive":
|
||||
paid_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
received_amount = bank_amount
|
||||
else:
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
else:
|
||||
received_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
paid_amount = bank_amount
|
||||
company_currency = frappe.get_cached_value("Company", doc.get("company"), "default_currency")
|
||||
if payment_type == "Receive":
|
||||
paid_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
received_amount = bank_amount
|
||||
else:
|
||||
if company_currency != bank.account_currency:
|
||||
received_amount = paid_amount / doc.get("conversion_rate", 1)
|
||||
else:
|
||||
received_amount = paid_amount * doc.get("conversion_rate", 1)
|
||||
else:
|
||||
# if party account currency and bank currency is different then populate paid amount as well
|
||||
paid_amount = received_amount * doc.get("conversion_rate", 1)
|
||||
received_amount = abs(outstanding_amount)
|
||||
if bank_amount:
|
||||
paid_amount = bank_amount
|
||||
else:
|
||||
if company_currency != bank.account_currency:
|
||||
paid_amount = received_amount / doc.get("conversion_rate", 1)
|
||||
else:
|
||||
# if party account currency and bank currency is different then populate paid amount as well
|
||||
paid_amount = received_amount * doc.get("conversion_rate", 1)
|
||||
|
||||
return paid_amount, received_amount
|
||||
|
||||
|
||||
@@ -51,6 +51,38 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
|
||||
self.assertEqual(so_advance_paid, 0)
|
||||
|
||||
def test_payment_against_sales_order_usd_to_inr(self):
|
||||
so = make_sales_order(
|
||||
customer="_Test Customer USD", currency="USD", qty=1, rate=100, do_not_submit=True
|
||||
)
|
||||
so.conversion_rate = 50
|
||||
so.submit()
|
||||
pe = get_payment_entry("Sales Order", so.name)
|
||||
pe.source_exchange_rate = 55
|
||||
pe.received_amount = 5500
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
# there should be no difference amount
|
||||
pe.reload()
|
||||
self.assertEqual(pe.difference_amount, 0)
|
||||
self.assertEqual(pe.deductions, [])
|
||||
|
||||
expected_gle = dict(
|
||||
(d[0], d)
|
||||
for d in [["_Test Receivable USD - _TC", 0, 5500, so.name], ["Cash - _TC", 5500.0, 0, None]]
|
||||
)
|
||||
|
||||
self.validate_gl_entries(pe.name, expected_gle)
|
||||
|
||||
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
|
||||
self.assertEqual(so_advance_paid, 100)
|
||||
|
||||
pe.cancel()
|
||||
|
||||
so_advance_paid = frappe.db.get_value("Sales Order", so.name, "advance_paid")
|
||||
self.assertEqual(so_advance_paid, 0)
|
||||
|
||||
def test_payment_entry_for_blocked_supplier_invoice(self):
|
||||
supplier = frappe.get_doc("Supplier", "_Test Supplier")
|
||||
supplier.on_hold = 1
|
||||
|
||||
@@ -82,6 +82,32 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
|
||||
this.frm.change_custom_button_type('Allocate', null, 'default');
|
||||
}
|
||||
|
||||
// check for any running reconciliation jobs
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments").then((enabled) => {
|
||||
if(enabled) {
|
||||
this.frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.is_any_doc_running",
|
||||
"args": {
|
||||
for_filter: {
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party,
|
||||
receivable_payable_account: this.frm.doc.receivable_payable_account
|
||||
}
|
||||
}
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
let doc_link = frappe.utils.get_form_link("Process Payment Reconciliation", r.message, true);
|
||||
let msg = __("Payment Reconciliation Job: {0} is running for this party. Can't reconcile now.", [doc_link]);
|
||||
this.frm.dashboard.add_comment(msg, "yellow");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
company() {
|
||||
|
||||
@@ -7,9 +7,12 @@ from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import flt, getdate, nowdate, today
|
||||
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_any_doc_running,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
get_outstanding_invoices,
|
||||
@@ -304,9 +307,7 @@ class PaymentReconciliation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reconcile(self):
|
||||
self.validate_allocation()
|
||||
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
@@ -330,12 +331,35 @@ class PaymentReconciliation(Document):
|
||||
self.make_difference_entry(payment_details)
|
||||
|
||||
if entry_list:
|
||||
reconcile_against_document(entry_list)
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
||||
|
||||
if dr_or_cr_notes:
|
||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reconcile(self):
|
||||
if frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
||||
running_doc = is_any_doc_running(
|
||||
dict(
|
||||
company=self.company,
|
||||
party_type=self.party_type,
|
||||
party=self.party,
|
||||
receivable_payable_account=self.receivable_payable_account,
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
self.validate_allocation()
|
||||
self.reconcile_allocations()
|
||||
msgprint(_("Successfully Reconciled"))
|
||||
|
||||
self.get_unreconciled_entries()
|
||||
|
||||
def make_difference_entry(self, row):
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import today
|
||||
from frappe.utils import add_months, today
|
||||
|
||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
@@ -176,6 +176,23 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(pcv_gle, expected_gle)
|
||||
warehouse = frappe.db.get_value("Warehouse", {"company": company}, "name")
|
||||
|
||||
repost_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"company": company,
|
||||
"posting_date": "2020-03-15",
|
||||
"based_on": "Item and Warehouse",
|
||||
"item_code": "Test Item 1",
|
||||
"warehouse": warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, repost_doc.save)
|
||||
|
||||
repost_doc.posting_date = add_months(today(), 13)
|
||||
repost_doc.save()
|
||||
|
||||
def make_period_closing_voucher(self, submit=True):
|
||||
surplus_account = create_account()
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Process Payment Reconciliation", {
|
||||
onload: function(frm) {
|
||||
// set queries
|
||||
frm.set_query("party_type", function() {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["in", Object.keys(frappe.boot.party_account_types)],
|
||||
}
|
||||
}
|
||||
});
|
||||
frm.set_query('receivable_payable_account', function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"company": doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": frappe.boot.party_account_types[doc.party_type]
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('cost_center', function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"company": doc.company,
|
||||
"is_group": 0,
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('bank_cash_account', function(doc) {
|
||||
return {
|
||||
filters:[
|
||||
['Account', 'company', '=', doc.company],
|
||||
['Account', 'is_group', '=', 0],
|
||||
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.docstatus==1 && ['Queued', 'Paused'].find(x => x == frm.doc.status)) {
|
||||
let execute_btn = __("Start / Resume")
|
||||
|
||||
frm.add_custom_button(execute_btn, () => {
|
||||
frm.call({
|
||||
method: 'erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_job_for_doc',
|
||||
args: {
|
||||
docname: frm.doc.name
|
||||
}
|
||||
}).then(r => {
|
||||
if(!r.exc) {
|
||||
frappe.show_alert(__("Job Started"));
|
||||
frm.reload_doc();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (frm.doc.docstatus==1 && ['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
|
||||
frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.get_reconciled_count",
|
||||
args: {
|
||||
"docname": frm.docname,
|
||||
}
|
||||
}).then(r => {
|
||||
if (r.message) {
|
||||
let progress = 0;
|
||||
let description = "";
|
||||
|
||||
if (r.message.processed) {
|
||||
progress = (r.message.processed/r.message.total) * 100;
|
||||
description = r.message.processed + "/" + r.message.total + " processed";
|
||||
} else if (r.message.total == 0 && frm.doc.status == "Completed") {
|
||||
progress = 100;
|
||||
}
|
||||
|
||||
|
||||
frm.dashboard.add_progress('Reconciliation Progress', progress, description);
|
||||
}
|
||||
})
|
||||
}
|
||||
if (frm.doc.docstatus==1 && frm.doc.status == 'Running') {
|
||||
let execute_btn = __("Pause")
|
||||
|
||||
frm.add_custom_button(execute_btn, () => {
|
||||
frm.call({
|
||||
'method': "erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.pause_job_for_doc",
|
||||
args: {
|
||||
"docname": frm.docname,
|
||||
}
|
||||
}).then(r => {
|
||||
if (!r.exc) {
|
||||
frappe.show_alert(__("Job Paused"));
|
||||
frm.reload_doc()
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
},
|
||||
company(frm) {
|
||||
frm.set_value('party', '');
|
||||
frm.set_value('receivable_payable_account', '');
|
||||
},
|
||||
party_type(frm) {
|
||||
frm.set_value('party', '');
|
||||
},
|
||||
|
||||
party(frm) {
|
||||
frm.set_value('receivable_payable_account', '');
|
||||
if (!frm.doc.receivable_payable_account && frm.doc.party_type && frm.doc.party) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
party_type: frm.doc.party_type,
|
||||
party: frm.doc.party
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
frm.set_value("receivable_payable_account", r.message);
|
||||
}
|
||||
frm.refresh();
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:ACC-PPR-{#####}",
|
||||
"beta": 1,
|
||||
"creation": "2023-03-30 21:28:39.793927",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"party_type",
|
||||
"column_break_io6c",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"filter_section",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"column_break_kegk",
|
||||
"from_payment_date",
|
||||
"to_payment_date",
|
||||
"column_break_uj04",
|
||||
"cost_center",
|
||||
"bank_cash_account",
|
||||
"section_break_2n02",
|
||||
"status",
|
||||
"error_log",
|
||||
"section_break_a8yx",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "\nQueued\nRunning\nPaused\nCompleted\nPartially Reconciled\nFailed\nCancelled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Party Type",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_io6c",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Party",
|
||||
"options": "party_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "receivable_payable_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Receivable/Payable Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "filter_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Filters"
|
||||
},
|
||||
{
|
||||
"fieldname": "from_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Invoice Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Invoice Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_kegk",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "from_payment_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "From Payment Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_payment_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "To Payment Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_uj04",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "bank_cash_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Bank/Cash Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_2n02",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Status"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.error_log",
|
||||
"fieldname": "error_log",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Error Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Process Payment Reconciliation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_a8yx",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 17:19:30.912953",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company"
|
||||
}
|
||||
@@ -0,0 +1,503 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_link_to_form
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
|
||||
class ProcessPaymentReconciliation(Document):
|
||||
def validate(self):
|
||||
self.validate_receivable_payable_account()
|
||||
self.validate_bank_cash_account()
|
||||
|
||||
def validate_receivable_payable_account(self):
|
||||
if self.receivable_payable_account:
|
||||
if self.company != frappe.db.get_value("Account", self.receivable_payable_account, "company"):
|
||||
frappe.throw(
|
||||
_("Receivable/Payable Account: {0} doesn't belong to company {1}").format(
|
||||
frappe.bold(self.receivable_payable_account), frappe.bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_bank_cash_account(self):
|
||||
if self.bank_cash_account:
|
||||
if self.company != frappe.db.get_value("Account", self.bank_cash_account, "company"):
|
||||
frappe.throw(
|
||||
_("Bank/Cash Account {0} doesn't belong to company {1}").format(
|
||||
frappe.bold(self.bank_cash_account), frappe.bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
def before_save(self):
|
||||
self.status = ""
|
||||
self.error_log = ""
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("status", "Queued")
|
||||
self.db_set("error_log", None)
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
log = frappe.db.get_value(
|
||||
"Process Payment Reconciliation Log", filters={"process_pr": self.name}
|
||||
)
|
||||
if log:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Cancelled")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_reconciled_count(docname: str | None = None) -> float:
|
||||
current_status = {}
|
||||
if docname:
|
||||
reconcile_log = frappe.db.get_value(
|
||||
"Process Payment Reconciliation Log", filters={"process_pr": docname}, fieldname="name"
|
||||
)
|
||||
if reconcile_log:
|
||||
res = frappe.get_all(
|
||||
"Process Payment Reconciliation Log",
|
||||
filters={"name": reconcile_log},
|
||||
fields=["reconciled_entries", "total_allocations"],
|
||||
as_list=1,
|
||||
)
|
||||
current_status["processed"], current_status["total"] = res[0]
|
||||
|
||||
return current_status
|
||||
|
||||
|
||||
def get_pr_instance(doc: str):
|
||||
process_payment_reconciliation = frappe.get_doc("Process Payment Reconciliation", doc)
|
||||
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
fields = [
|
||||
"company",
|
||||
"party_type",
|
||||
"party",
|
||||
"receivable_payable_account",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"from_payment_date",
|
||||
"to_payment_date",
|
||||
]
|
||||
d = {}
|
||||
for field in fields:
|
||||
d[field] = process_payment_reconciliation.get(field)
|
||||
pr.update(d)
|
||||
pr.invoice_limit = 1000
|
||||
pr.payment_limit = 1000
|
||||
return pr
|
||||
|
||||
|
||||
def is_job_running(job_name: str) -> bool:
|
||||
jobs = frappe.db.get_all("RQ Job", filters={"status": ["in", ["started", "queued"]]})
|
||||
for x in jobs:
|
||||
if x.job_name == job_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def pause_job_for_doc(docname: str | None = None):
|
||||
if docname:
|
||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Paused")
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
|
||||
if log:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Paused")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def trigger_job_for_doc(docname: str | None = None):
|
||||
"""
|
||||
Trigger background job
|
||||
"""
|
||||
if not docname:
|
||||
return
|
||||
|
||||
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
||||
frappe.throw(
|
||||
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
|
||||
get_link_to_form("Accounts Settings", "Accounts Settings")
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if not is_scheduler_inactive():
|
||||
if frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Queued":
|
||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
|
||||
job_name = f"start_processing_{docname}"
|
||||
if not is_job_running(job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
|
||||
queue="long",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=docname,
|
||||
)
|
||||
|
||||
elif frappe.db.get_value("Process Payment Reconciliation", docname, "status") == "Paused":
|
||||
frappe.db.set_value("Process Payment Reconciliation", docname, "status", "Running")
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": docname})
|
||||
if log:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Running")
|
||||
|
||||
# Resume tasks for running doc
|
||||
job_name = f"start_processing_{docname}"
|
||||
if not is_job_running(job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile_based_on_filters",
|
||||
queue="long",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
doc=docname,
|
||||
)
|
||||
else:
|
||||
frappe.msgprint(_("Scheduler is Inactive. Can't trigger job now."))
|
||||
|
||||
|
||||
def trigger_reconciliation_for_queued_docs():
|
||||
"""
|
||||
Will be called from Cron Job
|
||||
Fetch queued docs and start reconciliation process for each one
|
||||
"""
|
||||
if not frappe.db.get_single_value("Accounts Settings", "auto_reconcile_payments"):
|
||||
frappe.msgprint(
|
||||
_("Auto Reconciliation of Payments has been disabled. Enable it through {0}").format(
|
||||
get_link_to_form("Accounts Settings", "Accounts Settings")
|
||||
)
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
if not is_scheduler_inactive():
|
||||
# Get all queued documents
|
||||
all_queued = frappe.db.get_all(
|
||||
"Process Payment Reconciliation",
|
||||
filters={"docstatus": 1, "status": "Queued"},
|
||||
order_by="creation desc",
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
docs_to_trigger = []
|
||||
unique_filters = set()
|
||||
queue_size = 5
|
||||
|
||||
fields = ["company", "party_type", "party", "receivable_payable_account"]
|
||||
|
||||
def get_filters_as_tuple(fields, doc):
|
||||
filters = ()
|
||||
for x in fields:
|
||||
filters += tuple(doc.get(x))
|
||||
return filters
|
||||
|
||||
for x in all_queued:
|
||||
doc = frappe.get_doc("Process Payment Reconciliation", x)
|
||||
filters = get_filters_as_tuple(fields, doc)
|
||||
if filters not in unique_filters:
|
||||
unique_filters.add(filters)
|
||||
docs_to_trigger.append(doc.name)
|
||||
if len(docs_to_trigger) == queue_size:
|
||||
break
|
||||
|
||||
# trigger reconcilation process for queue_size unique filters
|
||||
for doc in docs_to_trigger:
|
||||
trigger_job_for_doc(doc)
|
||||
|
||||
else:
|
||||
frappe.msgprint(_("Scheduler is Inactive. Can't trigger jobs now."))
|
||||
|
||||
|
||||
def reconcile_based_on_filters(doc: None | str = None) -> None:
|
||||
"""
|
||||
Identify current state of document and execute next tasks in background
|
||||
"""
|
||||
if doc:
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
|
||||
if not log:
|
||||
log = frappe.new_doc("Process Payment Reconciliation Log")
|
||||
log.process_pr = doc
|
||||
log.status = "Running"
|
||||
log = log.save()
|
||||
|
||||
job_name = f"process_{doc}_fetch_and_allocate"
|
||||
if not is_job_running(job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
else:
|
||||
res = frappe.get_all(
|
||||
"Process Payment Reconciliation Log",
|
||||
filters={"name": log},
|
||||
fields=["allocated", "reconciled"],
|
||||
as_list=1,
|
||||
)
|
||||
allocated, reconciled = res[0]
|
||||
|
||||
if not allocated:
|
||||
job_name = f"process__{doc}_fetch_and_allocate"
|
||||
if not is_job_running(job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.fetch_and_allocate",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
elif not reconciled:
|
||||
allocation = get_next_allocation(log)
|
||||
if allocation:
|
||||
reconcile_job_name = (
|
||||
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
|
||||
)
|
||||
else:
|
||||
reconcile_job_name = f"process_{doc}_reconcile"
|
||||
if not is_job_running(reconcile_job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=reconcile_job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
elif reconciled:
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
|
||||
|
||||
def get_next_allocation(log: str) -> list:
|
||||
if log:
|
||||
allocations = []
|
||||
next = frappe.db.get_all(
|
||||
"Process Payment Reconciliation Log Allocations",
|
||||
filters={"parent": log, "reconciled": 0},
|
||||
fields=["reference_type", "reference_name"],
|
||||
order_by="idx",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if next:
|
||||
allocations = frappe.db.get_all(
|
||||
"Process Payment Reconciliation Log Allocations",
|
||||
filters={
|
||||
"parent": log,
|
||||
"reconciled": 0,
|
||||
"reference_type": next[0].reference_type,
|
||||
"reference_name": next[0].reference_name,
|
||||
},
|
||||
fields=["*"],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
return allocations
|
||||
return []
|
||||
|
||||
|
||||
def fetch_and_allocate(doc: str) -> None:
|
||||
"""
|
||||
Fetch Invoices and Payments based on filters applied. FIFO ordering is used for allocation.
|
||||
"""
|
||||
|
||||
if doc:
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
|
||||
if log:
|
||||
if not frappe.db.get_value("Process Payment Reconciliation Log", log, "allocated"):
|
||||
reconcile_log = frappe.get_doc("Process Payment Reconciliation Log", log)
|
||||
|
||||
pr = get_pr_instance(doc)
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
if len(pr.invoices) > 0 and len(pr.payments) > 0:
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
for x in pr.get("allocation"):
|
||||
reconcile_log.append(
|
||||
"allocations",
|
||||
x.as_dict().update(
|
||||
{
|
||||
"parenttype": "Process Payment Reconciliation Log",
|
||||
"parent": reconcile_log.name,
|
||||
"name": None,
|
||||
"reconciled": False,
|
||||
}
|
||||
),
|
||||
)
|
||||
reconcile_log.allocated = True
|
||||
reconcile_log.total_allocations = len(reconcile_log.get("allocations"))
|
||||
reconcile_log.reconciled_entries = 0
|
||||
reconcile_log.save()
|
||||
|
||||
# generate reconcile job name
|
||||
allocation = get_next_allocation(log)
|
||||
if allocation:
|
||||
reconcile_job_name = (
|
||||
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
|
||||
)
|
||||
else:
|
||||
reconcile_job_name = f"process_{doc}_reconcile"
|
||||
|
||||
if not is_job_running(reconcile_job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=reconcile_job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
|
||||
|
||||
def reconcile(doc: None | str = None) -> None:
|
||||
if doc:
|
||||
log = frappe.db.get_value("Process Payment Reconciliation Log", filters={"process_pr": doc})
|
||||
if log:
|
||||
res = frappe.get_all(
|
||||
"Process Payment Reconciliation Log",
|
||||
filters={"name": log},
|
||||
fields=["reconciled_entries", "total_allocations"],
|
||||
as_list=1,
|
||||
limit=1,
|
||||
)
|
||||
|
||||
reconciled_entries, total_allocations = res[0]
|
||||
if reconciled_entries != total_allocations:
|
||||
try:
|
||||
# Fetch next allocation
|
||||
allocations = get_next_allocation(log)
|
||||
|
||||
pr = get_pr_instance(doc)
|
||||
|
||||
# pass allocation to PR instance
|
||||
for x in allocations:
|
||||
pr.append("allocation", x)
|
||||
|
||||
# reconcile
|
||||
pr.reconcile_allocations(skip_ref_details_update_for_pe=True)
|
||||
|
||||
# If Payment Entry, update details only for newly linked references
|
||||
# This is for performance
|
||||
if allocations[0].reference_type == "Payment Entry":
|
||||
|
||||
references = [(x.invoice_type, x.invoice_number) for x in allocations]
|
||||
pe = frappe.get_doc(allocations[0].reference_type, allocations[0].reference_name)
|
||||
pe.flags.ignore_validate_update_after_submit = True
|
||||
pe.set_missing_ref_details(update_ref_details_only_for=references)
|
||||
pe.save()
|
||||
|
||||
# Update reconciled flag
|
||||
allocation_names = [x.name for x in allocations]
|
||||
ppa = qb.DocType("Process Payment Reconciliation Log Allocations")
|
||||
qb.update(ppa).set(ppa.reconciled, True).where(ppa.name.isin(allocation_names)).run()
|
||||
|
||||
# Update reconciled count
|
||||
reconciled_count = frappe.db.count(
|
||||
"Process Payment Reconciliation Log Allocations", filters={"parent": log, "reconciled": True}
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation Log", log, "reconciled_entries", reconciled_count
|
||||
)
|
||||
|
||||
except Exception as err:
|
||||
# Update the parent doc about the exception
|
||||
frappe.db.rollback()
|
||||
|
||||
traceback = frappe.get_traceback()
|
||||
if traceback:
|
||||
message = "Traceback: <br>" + traceback
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "error_log", message)
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation",
|
||||
doc,
|
||||
"error_log",
|
||||
message,
|
||||
)
|
||||
if reconciled_entries and total_allocations and reconciled_entries < total_allocations:
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation Log", log, "status", "Partially Reconciled"
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation",
|
||||
doc,
|
||||
"status",
|
||||
"Partially Reconciled",
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Failed")
|
||||
frappe.db.set_value(
|
||||
"Process Payment Reconciliation",
|
||||
doc,
|
||||
"status",
|
||||
"Failed",
|
||||
)
|
||||
finally:
|
||||
if reconciled_entries == total_allocations:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
else:
|
||||
|
||||
if not (frappe.db.get_value("Process Payment Reconciliation", doc, "status") == "Paused"):
|
||||
# trigger next batch in job
|
||||
# generate reconcile job name
|
||||
allocation = get_next_allocation(log)
|
||||
if allocation:
|
||||
reconcile_job_name = (
|
||||
f"process_{doc}_reconcile_allocation_{allocation[0].idx}_{allocation[-1].idx}"
|
||||
)
|
||||
else:
|
||||
reconcile_job_name = f"process_{doc}_reconcile"
|
||||
|
||||
if not is_job_running(reconcile_job_name):
|
||||
job = frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.reconcile",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=reconcile_job_name,
|
||||
enqueue_after_commit=True,
|
||||
doc=doc,
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "status", "Reconciled")
|
||||
frappe.db.set_value("Process Payment Reconciliation Log", log, "reconciled", True)
|
||||
frappe.db.set_value("Process Payment Reconciliation", doc, "status", "Completed")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_any_doc_running(for_filter: str | dict | None = None) -> str | None:
|
||||
running_doc = None
|
||||
if for_filter:
|
||||
if type(for_filter) == str:
|
||||
for_filter = frappe.json.loads(for_filter)
|
||||
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation",
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"status": ["in", ["Running", "Paused"]],
|
||||
"company": for_filter.get("company"),
|
||||
"party_type": for_filter.get("party_type"),
|
||||
"party": for_filter.get("party"),
|
||||
"receivable_payable_account": for_filter.get("receivable_payable_account"),
|
||||
},
|
||||
fieldname="name",
|
||||
)
|
||||
else:
|
||||
running_doc = frappe.db.get_value(
|
||||
"Process Payment Reconciliation", filters={"docstatus": 1, "status": "Running"}
|
||||
)
|
||||
return running_doc
|
||||
@@ -0,0 +1,15 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "process_pr",
|
||||
"transactions": [
|
||||
{
|
||||
"label": _("Reconciliation Logs"),
|
||||
"items": [
|
||||
"Process Payment Reconciliation Log",
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
frappe.listview_settings['Process Payment Reconciliation'] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function(doc) {
|
||||
let colors = {
|
||||
'Queued': 'orange',
|
||||
'Paused': 'orange',
|
||||
'Completed': 'green',
|
||||
'Partially Reconciled': 'orange',
|
||||
'Running': 'blue',
|
||||
'Failed': 'red',
|
||||
};
|
||||
let status = doc.status;
|
||||
return [__(status), colors[status], 'status,=,'+status];
|
||||
},
|
||||
};
|
||||
@@ -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 TestProcessPaymentReconciliation(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Process Payment Reconciliation Log", {
|
||||
refresh(frm) {
|
||||
if (['Completed', 'Running', 'Paused', 'Partially Reconciled'].find(x => x == frm.doc.status)) {
|
||||
let progress = 0;
|
||||
if (frm.doc.reconciled_entries != 0) {
|
||||
progress = frm.doc.reconciled_entries / frm.doc.total_allocations * 100;
|
||||
} else if(frm.doc.total_allocations == 0 && frm.doc.status == "Completed"){
|
||||
progress = 100;
|
||||
}
|
||||
frm.dashboard.add_progress(__('Reconciliation Progress'), progress);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:PPR-LOG-{##}",
|
||||
"beta": 1,
|
||||
"creation": "2023-03-13 15:00:09.149681",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"process_pr",
|
||||
"section_break_fvdw",
|
||||
"status",
|
||||
"tasks_section",
|
||||
"allocated",
|
||||
"reconciled",
|
||||
"column_break_yhin",
|
||||
"total_allocations",
|
||||
"reconciled_entries",
|
||||
"section_break_4ywv",
|
||||
"error_log",
|
||||
"allocations_section",
|
||||
"allocations"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "allocations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Allocations",
|
||||
"options": "Process Payment Reconciliation Log Allocations",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "All allocations have been successfully reconciled",
|
||||
"fieldname": "reconciled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Reconciled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_allocations",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Allocations",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Invoices and Payments have been Fetched and Allocated",
|
||||
"fieldname": "allocated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allocated",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reconciled_entries",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Reconciled Entries",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tasks_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Tasks"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocations_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Allocations"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_yhin",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4ywv",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.error_log",
|
||||
"fieldname": "error_log",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Reconciliation Error Log",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "process_pr",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Parent Document",
|
||||
"options": "Process Payment Reconciliation",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_fvdw",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Status"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Running\nPaused\nReconciled\nPartially Reconciled\nFailed\nCancelled",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 17:36:26.642617",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "allocated, reconciled, total_allocations, reconciled_entries",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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 ProcessPaymentReconciliationLog(Document):
|
||||
pass
|
||||
@@ -0,0 +1,15 @@
|
||||
frappe.listview_settings['Process Payment Reconciliation Log'] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function(doc) {
|
||||
var colors = {
|
||||
'Partially Reconciled': 'orange',
|
||||
'Paused': 'orange',
|
||||
'Reconciled': 'green',
|
||||
'Failed': 'red',
|
||||
'Cancelled': 'red',
|
||||
'Running': 'blue',
|
||||
};
|
||||
let status = doc.status;
|
||||
return [__(status), colors[status], "status,=,"+status];
|
||||
},
|
||||
};
|
||||
@@ -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 TestProcessPaymentReconciliationLog(FrappeTestCase):
|
||||
pass
|
||||
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2023-03-13 13:51:27.351463",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"reference_row",
|
||||
"column_break_3",
|
||||
"invoice_type",
|
||||
"invoice_number",
|
||||
"section_break_6",
|
||||
"allocated_amount",
|
||||
"unreconciled_amount",
|
||||
"column_break_8",
|
||||
"amount",
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"column_break_7",
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
"currency",
|
||||
"reconciled"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reference Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"options": "reference_type",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Invoice Type",
|
||||
"options": "DocType",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_number",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice Number",
|
||||
"options": "invoice_type",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "unreconciled_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Unreconciled Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "is_advance",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Is Advance",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Difference Amount",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Difference Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "reconciled",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Reconciled"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-20 21:05:43.121945",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation Log Allocations",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# 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 ProcessPaymentReconciliationLogAllocations(Document):
|
||||
pass
|
||||
@@ -15,7 +15,12 @@
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||
<div>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||
{% if filters.party[0] == filters.party_name[0] %}
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party_name[0] }}</b></h5>
|
||||
{% else %}
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{ filters.party[0] }}</b></h5>
|
||||
<h5 style="float: left; margin-left:15px">{{ _("Customer Name: ") }} <b>{{filters.party_name[0] }}</b></h5>
|
||||
{% endif %}
|
||||
<h5 style="float: right;">
|
||||
{{ _("Date: ") }}
|
||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||
|
||||
@@ -34,6 +34,8 @@
|
||||
"terms_and_conditions",
|
||||
"section_break_1",
|
||||
"enable_auto_email",
|
||||
"column_break_ocfq",
|
||||
"sender",
|
||||
"section_break_18",
|
||||
"frequency",
|
||||
"filter_duration",
|
||||
@@ -284,10 +286,32 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Terms and Conditions",
|
||||
"options": "Terms and Conditions"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "include_break",
|
||||
"fieldtype": "Check",
|
||||
"label": "Page Break After Each SoA"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_net_values_in_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Net Values in Party Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "sender",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sender",
|
||||
"options": "Email Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_ocfq",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-09-06 21:00:45.732505",
|
||||
"modified": "2023-04-26 12:46:43.645455",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
||||
@@ -327,7 +327,7 @@ def send_emails(document_name, from_scheduler=False):
|
||||
queue="short",
|
||||
method=frappe.sendmail,
|
||||
recipients=recipients,
|
||||
sender=frappe.session.user,
|
||||
sender=doc.sender or frappe.session.user,
|
||||
cc=cc,
|
||||
subject=subject,
|
||||
message=message,
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "billing_email",
|
||||
"fieldtype": "Read Only",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Email"
|
||||
},
|
||||
@@ -41,7 +41,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-13 00:12:34.508086",
|
||||
"modified": "2023-04-26 13:02:41.964499",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts Customer",
|
||||
|
||||
@@ -303,7 +303,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
|
||||
apply_tds(frm) {
|
||||
var me = this;
|
||||
|
||||
me.frm.set_value("tax_withheld_vouchers", []);
|
||||
if (!me.frm.doc.apply_tds) {
|
||||
me.frm.set_value("tax_withholding_category", '');
|
||||
me.frm.set_df_property("tax_withholding_category", "hidden", 1);
|
||||
|
||||
@@ -89,6 +89,7 @@
|
||||
"column_break8",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
"use_company_roundoff_cost_center",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"total_advance",
|
||||
@@ -1368,6 +1369,7 @@
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1,
|
||||
"print_width": "50px",
|
||||
"ignore_user_permissions": 1,
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
@@ -1559,13 +1561,19 @@
|
||||
"fieldname": "only_include_allocated_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Only Include Allocated Payments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_company_roundoff_cost_center",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-03 22:57:14.074982",
|
||||
"modified": "2023-04-29 12:57:50.832598",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -978,7 +978,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
precision_loss = self.get("base_net_total") - flt(
|
||||
@@ -992,7 +992,9 @@ class PurchaseInvoice(BuyingController):
|
||||
"account": round_off_account,
|
||||
"against": self.supplier,
|
||||
"credit": precision_loss,
|
||||
"cost_center": self.cost_center or round_off_cost_center,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else self.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
@@ -1386,7 +1388,7 @@ class PurchaseInvoice(BuyingController):
|
||||
not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment
|
||||
):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
@@ -1396,7 +1398,9 @@ class PurchaseInvoice(BuyingController):
|
||||
"against": self.supplier,
|
||||
"debit_in_account_currency": self.rounding_adjustment,
|
||||
"debit": self.base_rounding_adjustment,
|
||||
"cost_center": self.cost_center or round_off_cost_center,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else (self.cost_center or round_off_cost_center),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
@@ -334,6 +334,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
}
|
||||
|
||||
make_inter_company_invoice() {
|
||||
let me = this;
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_inter_company_purchase_invoice",
|
||||
frm: me.frm
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
"column_break5",
|
||||
"grand_total",
|
||||
"rounding_adjustment",
|
||||
"use_company_roundoff_cost_center",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"total_advance",
|
||||
@@ -2134,6 +2135,12 @@
|
||||
"fieldname": "only_include_allocated_payments",
|
||||
"fieldtype": "Check",
|
||||
"label": "Only Include Allocated Payments"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_company_roundoff_cost_center",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company default Cost Center for Round off"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2146,7 +2153,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-04-03 22:55:14.206473",
|
||||
"modified": "2023-04-28 14:15:59.901154",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -1179,7 +1179,12 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if self.is_return:
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
item.finance_book,
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
@@ -1194,7 +1199,12 @@ class SalesInvoice(SellingController):
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset, item.base_net_amount, item.finance_book, self.get("doctype"), self.get("name")
|
||||
asset,
|
||||
item.base_net_amount,
|
||||
item.finance_book,
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
@@ -1450,7 +1460,7 @@ class SalesInvoice(SellingController):
|
||||
and not self.is_internal_transfer()
|
||||
):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Sales Invoice", self.name
|
||||
self.company, "Sales Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
@@ -1462,7 +1472,9 @@ class SalesInvoice(SellingController):
|
||||
self.rounding_adjustment, self.precision("rounding_adjustment")
|
||||
),
|
||||
"credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")),
|
||||
"cost_center": self.cost_center or round_off_cost_center,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else (self.cost_center or round_off_cost_center),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
|
||||
@@ -215,7 +215,7 @@ def get_tax_row_for_tds(tax_details, tax_amount):
|
||||
}
|
||||
|
||||
|
||||
def get_lower_deduction_certificate(tax_details, pan_no):
|
||||
def get_lower_deduction_certificate(company, tax_details, pan_no):
|
||||
ldc_name = frappe.db.get_value(
|
||||
"Lower Deduction Certificate",
|
||||
{
|
||||
@@ -223,6 +223,7 @@ def get_lower_deduction_certificate(tax_details, pan_no):
|
||||
"tax_withholding_category": tax_details.tax_withholding_category,
|
||||
"valid_from": (">=", tax_details.from_date),
|
||||
"valid_upto": ("<=", tax_details.to_date),
|
||||
"company": company,
|
||||
},
|
||||
"name",
|
||||
)
|
||||
@@ -255,7 +256,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
tax_amount = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
ldc = get_lower_deduction_certificate(tax_details, pan_no)
|
||||
ldc = get_lower_deduction_certificate(inv.company, tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.tax_withholding_net_total
|
||||
if ldc:
|
||||
@@ -301,7 +302,7 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
"docstatus": 1,
|
||||
}
|
||||
|
||||
if not tax_details.get("consider_party_ledger_amount") and doctype != "Sales Invoice":
|
||||
if doctype != "Sales Invoice":
|
||||
filters.update(
|
||||
{"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")}
|
||||
)
|
||||
|
||||
@@ -110,9 +110,9 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
invoices.append(pi1)
|
||||
|
||||
# Cumulative threshold is 30000
|
||||
# Threshold calculation should be on both the invoices
|
||||
# TDS should be applied only on 1000
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
|
||||
# Threshold calculation should be only on the Second invoice
|
||||
# Second didn't breach, no TDS should be applied
|
||||
self.assertEqual(pi1.taxes, [])
|
||||
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
41
erpnext/accounts/form_tour/sales_invoice/sales_invoice.json
Normal file
41
erpnext/accounts/form_tour/sales_invoice/sales_invoice.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"creation": "2023-05-23 09:58:17.235916",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"first_document": 0,
|
||||
"idx": 0,
|
||||
"include_name_field": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2023-05-23 13:10:56.227127",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Sales Invoice",
|
||||
"save_on_complete": 1,
|
||||
"steps": [
|
||||
{
|
||||
"description": "Select a customer for whom this invoice is being prepared.",
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 1,
|
||||
"is_table_field": 0,
|
||||
"label": "Customer",
|
||||
"next_step_condition": "eval: doc.customer",
|
||||
"position": "Right",
|
||||
"title": "Select Customer"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Sales Invoice Item",
|
||||
"description": "Select item that you have sold along with quantity and rate.",
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"parent_fieldname": "items",
|
||||
"position": "Top",
|
||||
"title": "Select Item"
|
||||
}
|
||||
],
|
||||
"title": "Sales Invoice"
|
||||
}
|
||||
@@ -472,7 +472,9 @@ def update_accounting_dimensions(round_off_gle):
|
||||
round_off_gle[dimension] = dimension_values.get(dimension)
|
||||
|
||||
|
||||
def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
|
||||
def get_round_off_account_and_cost_center(
|
||||
company, voucher_type, voucher_no, use_company_default=False
|
||||
):
|
||||
round_off_account, round_off_cost_center = frappe.get_cached_value(
|
||||
"Company", company, ["round_off_account", "round_off_cost_center"]
|
||||
) or [None, None]
|
||||
@@ -480,7 +482,7 @@ def get_round_off_account_and_cost_center(company, voucher_type, voucher_no):
|
||||
meta = frappe.get_meta(voucher_type)
|
||||
|
||||
# Give first preference to parent cost center for round off GLE
|
||||
if meta.has_field("cost_center"):
|
||||
if not use_company_default and meta.has_field("cost_center"):
|
||||
parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center")
|
||||
if parent_cost_center:
|
||||
round_off_cost_center = parent_cost_center
|
||||
|
||||
@@ -259,6 +259,8 @@ def set_address_details(
|
||||
)
|
||||
|
||||
if doctype in TRANSACTION_TYPES:
|
||||
# required to set correct region
|
||||
frappe.flags.company = company
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_address, shipping_address
|
||||
|
||||
@@ -114,28 +114,6 @@ def get_assets(filters):
|
||||
sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period,
|
||||
sum(results.depreciation_amount_during_the_period) as depreciation_amount_during_the_period
|
||||
from (SELECT a.asset_category,
|
||||
ifnull(sum(case when ds.schedule_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as accumulated_depreciation_as_on_from_date,
|
||||
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
|
||||
and a.disposal_date <= %(to_date)s and ds.schedule_date <= a.disposal_date then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_eliminated_during_the_period,
|
||||
ifnull(sum(case when ds.schedule_date >= %(from_date)s and ds.schedule_date <= %(to_date)s
|
||||
and (ifnull(a.disposal_date, 0) = 0 or ds.schedule_date <= a.disposal_date) then
|
||||
ds.depreciation_amount
|
||||
else
|
||||
0
|
||||
end), 0) as depreciation_amount_during_the_period
|
||||
from `tabAsset` a, `tabDepreciation Schedule` ds
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != ''
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
|
||||
gle.debit
|
||||
else
|
||||
@@ -160,7 +138,7 @@ def get_assets(filters):
|
||||
aca.parent = a.asset_category and aca.company_name = %(company)s
|
||||
join `tabCompany` company on
|
||||
company.name = %(company)s
|
||||
where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
group by a.asset_category
|
||||
union
|
||||
SELECT a.asset_category,
|
||||
|
||||
@@ -80,7 +80,7 @@ def get_entries(filters):
|
||||
payment_entries = frappe.db.sql(
|
||||
"""SELECT
|
||||
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
|
||||
if(paid_from=%(account)s, paid_amount * -1, received_amount)
|
||||
if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
|
||||
FROM
|
||||
`tabPayment Entry`
|
||||
WHERE
|
||||
|
||||
@@ -492,11 +492,26 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
||||
additional_conditions.append("cost_center in %(cost_center)s")
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
|
||||
filters.get("company_fb")
|
||||
):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
if filters.get("finance_book"):
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions.append("(finance_book in ('') OR finance_book IS NULL)")
|
||||
|
||||
if accounting_dimensions:
|
||||
for dimension in accounting_dimensions:
|
||||
|
||||
@@ -176,7 +176,8 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname": "include_default_book_entries",
|
||||
"label": __("Include Default Book Entries"),
|
||||
"fieldtype": "Check"
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_cancelled_entries",
|
||||
|
||||
@@ -244,13 +244,23 @@ def get_conditions(filters):
|
||||
if filters.get("project"):
|
||||
conditions.append("project in %(project)s")
|
||||
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("include_default_book_entries"):
|
||||
conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("include_default_book_entries"):
|
||||
if filters.get("finance_book"):
|
||||
if filters.get("company_fb") and cstr(filters.get("finance_book")) != cstr(
|
||||
filters.get("company_fb")
|
||||
):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
conditions.append("finance_book in (%(finance_book)s)")
|
||||
conditions.append("(finance_book in (%(company_fb)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
if filters.get("finance_book"):
|
||||
conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
else:
|
||||
conditions.append("(finance_book in ('') OR finance_book IS NULL)")
|
||||
|
||||
if not filters.get("show_cancelled_entries"):
|
||||
conditions.append("is_cancelled = 0")
|
||||
|
||||
@@ -24,7 +24,6 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
"root_type": "Asset",
|
||||
"report_type": "Balance Sheet",
|
||||
"account_currency": "USD",
|
||||
"inter_company_account": 0,
|
||||
"parent_account": "Bank Accounts - _TC",
|
||||
"account_type": "Bank",
|
||||
"doctype": "Account",
|
||||
|
||||
@@ -59,7 +59,7 @@ frappe.query_reports["Gross Profit"] = {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
|
||||
column._options = "Sales Invoice";
|
||||
} else {
|
||||
column._options = "Item";
|
||||
column._options = "";
|
||||
}
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ def get_columns(group_wise_columns, filters):
|
||||
"label": _("Warehouse"),
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"options": "warehouse",
|
||||
"options": "Warehouse",
|
||||
"width": 100,
|
||||
},
|
||||
"qty": {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80},
|
||||
@@ -305,7 +305,8 @@ def get_columns(group_wise_columns, filters):
|
||||
"sales_person": {
|
||||
"label": _("Sales Person"),
|
||||
"fieldname": "sales_person",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Person",
|
||||
"width": 100,
|
||||
},
|
||||
"allocated_amount": {
|
||||
@@ -326,14 +327,14 @@ def get_columns(group_wise_columns, filters):
|
||||
"label": _("Customer Group"),
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "customer",
|
||||
"options": "Customer Group",
|
||||
"width": 100,
|
||||
},
|
||||
"territory": {
|
||||
"label": _("Territory"),
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"options": "territory",
|
||||
"options": "Territory",
|
||||
"width": 100,
|
||||
},
|
||||
"monthly": {
|
||||
|
||||
@@ -19,14 +19,19 @@ def execute(filters=None):
|
||||
return _execute(filters)
|
||||
|
||||
|
||||
def _execute(filters=None, additional_table_columns=None, additional_query_columns=None):
|
||||
def _execute(
|
||||
filters=None,
|
||||
additional_table_columns=None,
|
||||
additional_query_columns=None,
|
||||
additional_conditions=None,
|
||||
):
|
||||
if not filters:
|
||||
filters = {}
|
||||
columns = get_columns(additional_table_columns, filters)
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
item_list = get_items(filters, additional_query_columns)
|
||||
item_list = get_items(filters, additional_query_columns, additional_conditions)
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
@@ -328,7 +333,7 @@ def get_columns(additional_table_columns, filters):
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
def get_conditions(filters, additional_conditions=None):
|
||||
conditions = ""
|
||||
|
||||
for opts in (
|
||||
@@ -341,6 +346,9 @@ def get_conditions(filters):
|
||||
if filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
|
||||
if additional_conditions:
|
||||
conditions += additional_conditions
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
||||
where parent=`tabSales Invoice`.name
|
||||
@@ -376,8 +384,8 @@ def get_group_by_conditions(filters, doctype):
|
||||
return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by")))
|
||||
|
||||
|
||||
def get_items(filters, additional_query_columns):
|
||||
conditions = get_conditions(filters)
|
||||
def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
conditions = get_conditions(filters, additional_conditions)
|
||||
|
||||
if additional_query_columns:
|
||||
additional_query_columns = ", " + ", ".join(additional_query_columns)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -66,12 +65,6 @@ def get_result(
|
||||
else:
|
||||
total_amount_credited += entry.credit
|
||||
|
||||
## Check if ldc is applied and show rate as per ldc
|
||||
actual_rate = (tds_deducted / total_amount_credited) * 100
|
||||
|
||||
if flt(actual_rate) < flt(rate):
|
||||
rate = actual_rate
|
||||
|
||||
if tds_deducted:
|
||||
row = {
|
||||
"pan"
|
||||
|
||||
@@ -157,12 +157,25 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
if filters.project:
|
||||
additional_conditions += " and project = %(project)s"
|
||||
|
||||
company_fb = frappe.db.get_value("Company", filters.company, "default_finance_book")
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions += (
|
||||
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
if filters.get("finance_book"):
|
||||
if company_fb and cstr(filters.get("finance_book")) != cstr(company_fb):
|
||||
frappe.throw(
|
||||
_("To use a different finance book, please uncheck 'Include Default Book Entries'")
|
||||
)
|
||||
else:
|
||||
additional_conditions += (
|
||||
" AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in (%(company_fb)s, '') OR finance_book IS NULL)"
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
if filters.get("finance_book"):
|
||||
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||
else:
|
||||
additional_conditions += " AND (finance_book in ('') OR finance_book IS NULL)"
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
@@ -174,7 +187,7 @@ def get_rootwise_opening_balances(filters, report_type):
|
||||
"year_start_date": filters.year_start_date,
|
||||
"project": filters.project,
|
||||
"finance_book": filters.finance_book,
|
||||
"company_fb": frappe.db.get_value("Company", filters.company, "default_finance_book"),
|
||||
"company_fb": company_fb,
|
||||
}
|
||||
|
||||
if accounting_dimensions:
|
||||
|
||||
@@ -420,7 +420,7 @@ def add_cc(args=None):
|
||||
return cc.name
|
||||
|
||||
|
||||
def reconcile_against_document(args): # nosemgrep
|
||||
def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
|
||||
"""
|
||||
Cancel PE or JV, Update against document, split if required and resubmit
|
||||
"""
|
||||
@@ -449,7 +449,9 @@ def reconcile_against_document(args): # nosemgrep
|
||||
if voucher_type == "Journal Entry":
|
||||
update_reference_in_journal_entry(entry, doc, do_not_save=True)
|
||||
else:
|
||||
update_reference_in_payment_entry(entry, doc, do_not_save=True)
|
||||
update_reference_in_payment_entry(
|
||||
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
|
||||
)
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
# re-submit advance entry
|
||||
@@ -586,7 +588,9 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
journal_entry.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
def update_reference_in_payment_entry(
|
||||
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
|
||||
):
|
||||
reference_details = {
|
||||
"reference_doctype": d.against_voucher_type,
|
||||
"reference_name": d.against_voucher,
|
||||
@@ -632,6 +636,13 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
|
||||
payment_entry.set_gain_or_loss(account_details=account_details)
|
||||
|
||||
payment_entry.flags.ignore_validate_update_after_submit = True
|
||||
payment_entry.setup_party_account_field()
|
||||
payment_entry.set_missing_values()
|
||||
if not skip_ref_details_update_for_pe:
|
||||
payment_entry.set_missing_ref_details()
|
||||
payment_entry.set_amounts()
|
||||
|
||||
if not do_not_save:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
|
||||
@@ -1352,10 +1363,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None)
|
||||
if wh_details.account == account and not wh_details.is_group
|
||||
]
|
||||
|
||||
total_stock_value = 0.0
|
||||
for warehouse in related_warehouses:
|
||||
value = get_stock_value_on(warehouse, posting_date)
|
||||
total_stock_value += value
|
||||
total_stock_value = get_stock_value_on(related_warehouses, posting_date)
|
||||
|
||||
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
|
||||
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
|
||||
|
||||
@@ -343,7 +343,7 @@ class Asset(AccountsController):
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_disposal:
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
from_date = self.get_from_date_for_disposal(finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book,
|
||||
depreciation_amount,
|
||||
@@ -425,7 +425,7 @@ class Asset(AccountsController):
|
||||
depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life
|
||||
skip_row = True
|
||||
|
||||
if depreciation_amount > 0:
|
||||
if flt(depreciation_amount, self.precision("gross_purchase_amount")) > 0:
|
||||
self._add_depreciation_row(
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
@@ -500,16 +500,19 @@ class Asset(AccountsController):
|
||||
|
||||
return start
|
||||
|
||||
def get_from_date(self, finance_book):
|
||||
def get_from_date_for_disposal(self, finance_book):
|
||||
if not self.get("schedules"):
|
||||
return self.available_for_use_date
|
||||
return add_months(
|
||||
getdate(self.available_for_use_date),
|
||||
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
if len(self.finance_books) == 1:
|
||||
return self.schedules[-1].schedule_date
|
||||
|
||||
from_date = ""
|
||||
for schedule in self.get("schedules"):
|
||||
if schedule.finance_book == finance_book:
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
from_date = schedule.schedule_date
|
||||
|
||||
if from_date:
|
||||
@@ -1287,9 +1290,11 @@ def get_straight_line_or_manual_depr_amount(asset, row):
|
||||
)
|
||||
# if the Depreciation Schedule is being prepared for the first time
|
||||
else:
|
||||
return (flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)) / flt(
|
||||
row.total_number_of_depreciations
|
||||
)
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
|
||||
def get_wdv_or_dd_depr_amount(
|
||||
|
||||
@@ -36,7 +36,7 @@ frappe.listview_settings['Asset'] = {
|
||||
}
|
||||
},
|
||||
onload: function(me) {
|
||||
me.page.add_action_item('Make Asset Movement', function() {
|
||||
me.page.add_action_item(__("Make Asset Movement"), function() {
|
||||
const assets = me.get_checked_items();
|
||||
frappe.call({
|
||||
method: "erpnext.assets.doctype.asset.asset.make_asset_movement",
|
||||
|
||||
@@ -126,6 +126,7 @@ def make_depreciation_entry(asset_name, date=None):
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
@@ -271,7 +272,7 @@ def scrap_asset(asset_name):
|
||||
je.company = asset.company
|
||||
je.remark = "Scrap Entry for asset {0}".format(asset_name)
|
||||
|
||||
for entry in get_gl_entries_on_asset_disposal(asset):
|
||||
for entry in get_gl_entries_on_asset_disposal(asset, date):
|
||||
entry.update({"reference_type": "Asset", "reference_name": asset_name})
|
||||
je.append("accounts", entry)
|
||||
|
||||
@@ -394,8 +395,11 @@ def disposal_happens_in_the_future(posting_date_of_disposal):
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_regain(
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
|
||||
):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -413,7 +417,7 @@ def get_gl_entries_on_asset_regain(
|
||||
"debit_in_account_currency": asset.gross_purchase_amount,
|
||||
"debit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@@ -423,7 +427,7 @@ def get_gl_entries_on_asset_regain(
|
||||
"credit_in_account_currency": accumulated_depr_amount,
|
||||
"credit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@@ -432,7 +436,7 @@ def get_gl_entries_on_asset_regain(
|
||||
profit_amount = abs(flt(value_after_depreciation)) - abs(flt(selling_amount))
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
|
||||
)
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
@@ -444,8 +448,11 @@ def get_gl_entries_on_asset_regain(
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_disposal(
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None
|
||||
asset, selling_amount=0, finance_book=None, voucher_type=None, voucher_no=None, date=None
|
||||
):
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
(
|
||||
fixed_asset_account,
|
||||
asset,
|
||||
@@ -463,7 +470,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
"credit_in_account_currency": asset.gross_purchase_amount,
|
||||
"credit": asset.gross_purchase_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@@ -473,7 +480,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
"debit_in_account_currency": accumulated_depr_amount,
|
||||
"debit": accumulated_depr_amount,
|
||||
"cost_center": depreciation_cost_center,
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
),
|
||||
@@ -482,7 +489,7 @@ def get_gl_entries_on_asset_disposal(
|
||||
profit_amount = flt(selling_amount) - flt(value_after_depreciation)
|
||||
if profit_amount:
|
||||
get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date
|
||||
)
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
@@ -516,8 +523,12 @@ def get_asset_details(asset, finance_book=None):
|
||||
|
||||
|
||||
def get_profit_gl_entries(
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center
|
||||
asset, profit_amount, gl_entries, disposal_account, depreciation_cost_center, date=None
|
||||
):
|
||||
|
||||
if not date:
|
||||
date = getdate()
|
||||
|
||||
debit_or_credit = "debit" if profit_amount < 0 else "credit"
|
||||
gl_entries.append(
|
||||
asset.get_gl_dict(
|
||||
@@ -526,7 +537,7 @@ def get_profit_gl_entries(
|
||||
"cost_center": depreciation_cost_center,
|
||||
debit_or_credit: abs(profit_amount),
|
||||
debit_or_credit + "_in_account_currency": abs(profit_amount),
|
||||
"posting_date": getdate(),
|
||||
"posting_date": date,
|
||||
},
|
||||
item=asset,
|
||||
)
|
||||
|
||||
@@ -327,6 +327,79 @@ class TestAsset(AssetSetup):
|
||||
si.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_gle_made_by_asset_sale_for_existing_asset(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-04-01",
|
||||
purchase_date="2020-04-01",
|
||||
expected_value_after_useful_life=0,
|
||||
total_number_of_depreciations=5,
|
||||
number_of_depreciations_booked=2,
|
||||
frequency_of_depreciation=12,
|
||||
depreciation_start_date="2023-03-31",
|
||||
opening_accumulated_depreciation=24000,
|
||||
gross_purchase_amount=60000,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_depr_values = [
|
||||
["2023-03-31", 12000, 36000],
|
||||
["2024-03-31", 12000, 48000],
|
||||
["2025-03-31", 12000, 60000],
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_depr_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_depr_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_depr_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
|
||||
post_depreciation_entries(date="2023-03-31")
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="Macbook Pro", asset=asset.name, qty=1, rate=40000, posting_date=getdate("2023-05-23")
|
||||
)
|
||||
asset.load_from_db()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
expected_values = [["2023-03-31", 12000, 36000], ["2023-05-23", 1742.47, 37742.47]]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertTrue(schedule.journal_entry)
|
||||
|
||||
expected_gle = (
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
37742.47,
|
||||
0.0,
|
||||
),
|
||||
(
|
||||
"_Test Fixed Asset - _TC",
|
||||
0.0,
|
||||
60000.0,
|
||||
),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
0.0,
|
||||
17742.47,
|
||||
),
|
||||
("Debtors - _TC", 40000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
@@ -649,7 +722,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]]
|
||||
expected_schedules = [["2032-12-31", 42904.11, 90000.0]]
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
|
||||
for d in asset.get("schedules")
|
||||
@@ -1453,7 +1526,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Submitted")
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 100000)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
@@ -1466,12 +1539,68 @@ class TestDepreciationBasics(AssetSetup):
|
||||
jv.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 99900)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 99900)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get("value_after_depreciation"), 100000)
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
def test_manual_depreciation_for_depreciable_asset(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-01-30",
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Submitted")
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
)
|
||||
for d in jv.accounts:
|
||||
d.reference_type = "Asset"
|
||||
d.reference_name = asset.name
|
||||
jv.voucher_type = "Depreciation Entry"
|
||||
jv.insert()
|
||||
jv.submit()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 99900)
|
||||
|
||||
jv.cancel()
|
||||
|
||||
asset.reload()
|
||||
self.assertEqual(asset.get_value_after_depreciation(), 100000)
|
||||
|
||||
def test_manual_depreciation_with_incorrect_jv_voucher_type(self):
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-01-30",
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
jv = make_journal_entry(
|
||||
"_Test Depreciations - _TC", "_Test Accumulated Depreciations - _TC", 100, save=False
|
||||
)
|
||||
for d in jv.accounts:
|
||||
d.reference_type = "Asset"
|
||||
d.reference_name = asset.name
|
||||
d.account_type = "Depreciation"
|
||||
jv.voucher_type = "Journal Entry"
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jv.insert)
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
|
||||
@@ -436,6 +436,7 @@ class AssetCapitalization(StockController):
|
||||
item.get("finance_book") or self.get("finance_book"),
|
||||
self.get("doctype"),
|
||||
self.get("name"),
|
||||
self.get("posting_date"),
|
||||
)
|
||||
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
@@ -116,7 +116,9 @@ class AssetValueAdjustment(Document):
|
||||
if d.depreciation_method in ("Straight Line", "Manual"):
|
||||
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
|
||||
total_days = date_diff(end_date, self.date)
|
||||
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
|
||||
rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
|
||||
total_days
|
||||
)
|
||||
from_date = self.date
|
||||
else:
|
||||
no_of_depreciations = len(
|
||||
|
||||
@@ -24,7 +24,7 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
"label": __("Period Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Fiscal Year", "Date Range"],
|
||||
"default": ["Fiscal Year"],
|
||||
"default": "Fiscal Year",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -75,12 +75,6 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
fieldtype: "Link",
|
||||
options: "Asset Category"
|
||||
},
|
||||
{
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book"
|
||||
},
|
||||
{
|
||||
fieldname:"cost_center",
|
||||
label: __("Cost Center"),
|
||||
@@ -96,8 +90,20 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname:"is_existing_asset",
|
||||
label: __("Is Existing Asset"),
|
||||
fieldname:"finance_book",
|
||||
label: __("Finance Book"),
|
||||
fieldtype: "Link",
|
||||
options: "Finance Book",
|
||||
depends_on: "eval: doc.filter_by_finance_book == 1",
|
||||
},
|
||||
{
|
||||
fieldname:"filter_by_finance_book",
|
||||
label: __("Filter by Finance Book"),
|
||||
fieldtype: "Check"
|
||||
},
|
||||
{
|
||||
fieldname:"only_existing_assets",
|
||||
label: __("Only existing assets"),
|
||||
fieldtype: "Check"
|
||||
},
|
||||
]
|
||||
|
||||
@@ -45,8 +45,8 @@ def get_conditions(filters):
|
||||
filters.year_end_date = getdate(fiscal_year.year_end_date)
|
||||
|
||||
conditions[date_field] = ["between", [filters.year_start_date, filters.year_end_date]]
|
||||
if filters.get("is_existing_asset"):
|
||||
conditions["is_existing_asset"] = filters.get("is_existing_asset")
|
||||
if filters.get("only_existing_assets"):
|
||||
conditions["is_existing_asset"] = filters.get("only_existing_assets")
|
||||
if filters.get("asset_category"):
|
||||
conditions["asset_category"] = filters.get("asset_category")
|
||||
if filters.get("cost_center"):
|
||||
@@ -102,19 +102,18 @@ def get_data(filters):
|
||||
]
|
||||
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
|
||||
|
||||
assets_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
assets_linked_to_fb = None
|
||||
|
||||
if filters.filter_by_finance_book:
|
||||
assets_linked_to_fb = frappe.db.get_all(
|
||||
doctype="Asset Finance Book",
|
||||
filters={"finance_book": filters.finance_book or ("is", "not set")},
|
||||
pluck="parent",
|
||||
)
|
||||
|
||||
for asset in assets_record:
|
||||
if filters.finance_book:
|
||||
if asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
else:
|
||||
if asset.calculate_depreciation and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
if assets_linked_to_fb and asset.asset_id not in assets_linked_to_fb:
|
||||
continue
|
||||
|
||||
asset_value = get_asset_value_after_depreciation(asset.asset_id, filters.finance_book)
|
||||
row = {
|
||||
@@ -172,11 +171,11 @@ def prepare_chart_data(data, filters):
|
||||
"datasets": [
|
||||
{
|
||||
"name": _("Asset Value"),
|
||||
"values": [d.get("asset_value") for d in labels_values_map.values()],
|
||||
"values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
|
||||
},
|
||||
{
|
||||
"name": _("Depreciatied Amount"),
|
||||
"values": [d.get("depreciated_amount") for d in labels_values_map.values()],
|
||||
"values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -310,7 +309,7 @@ def get_columns(filters):
|
||||
|
||||
return [
|
||||
{
|
||||
"label": _("Asset Id"),
|
||||
"label": _("Asset ID"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "asset_id",
|
||||
"options": "Asset",
|
||||
|
||||
@@ -495,6 +495,7 @@
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Items",
|
||||
"oldfieldname": "po_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Purchase Order Item",
|
||||
@@ -1100,8 +1101,7 @@
|
||||
{
|
||||
"fieldname": "before_items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Items"
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "items_col_break",
|
||||
@@ -1171,6 +1171,7 @@
|
||||
"depends_on": "is_internal_supplier",
|
||||
"fieldname": "set_from_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Set From Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
@@ -1271,7 +1272,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-28 18:59:16.322824",
|
||||
"modified": "2023-05-07 20:18:09.196799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -43,7 +43,7 @@ frappe.listview_settings['Purchase Order'] = {
|
||||
});
|
||||
|
||||
listview.page.add_action_item(__("Advance Payment"), ()=>{
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
|
||||
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Payment Entry");
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@@ -310,7 +310,6 @@
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"label": "Items",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-shopping-cart"
|
||||
},
|
||||
@@ -318,6 +317,7 @@
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Items",
|
||||
"oldfieldname": "po_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Supplier Quotation Item",
|
||||
@@ -844,7 +844,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-12 18:35:39.740974",
|
||||
"modified": "2023-04-14 16:43:41.714832",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _, throw
|
||||
from frappe import _, bold, throw
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import (
|
||||
@@ -273,8 +273,8 @@ class AccountsController(TransactionBase):
|
||||
self.validate_payment_schedule_dates()
|
||||
self.set_due_date()
|
||||
self.set_payment_schedule()
|
||||
self.validate_payment_schedule_amount()
|
||||
if not self.get("ignore_default_payment_terms_template"):
|
||||
self.validate_payment_schedule_amount()
|
||||
self.validate_due_date()
|
||||
self.validate_advance_entries()
|
||||
|
||||
@@ -392,6 +392,9 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
|
||||
def validate_inter_company_reference(self):
|
||||
if self.get("is_return"):
|
||||
return
|
||||
|
||||
if self.doctype not in ("Purchase Invoice", "Purchase Receipt"):
|
||||
return
|
||||
|
||||
@@ -405,6 +408,15 @@ class AccountsController(TransactionBase):
|
||||
msg += _("Please create purchase from internal sale or delivery document itself")
|
||||
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
|
||||
|
||||
label = "Delivery Note Item" if self.doctype == "Purchase Receipt" else "Sales Invoice Item"
|
||||
|
||||
field = frappe.scrub(label)
|
||||
|
||||
for row in self.get("items"):
|
||||
if not row.get(field):
|
||||
msg = f"At Row {row.idx}: The field {bold(label)} is mandatory for internal transfer"
|
||||
frappe.throw(_(msg), title=_("Internal Transfer Reference Missing"))
|
||||
|
||||
def disable_pricing_rule_on_internal_transfer(self):
|
||||
if not self.get("ignore_pricing_rule") and self.is_internal_transfer():
|
||||
self.ignore_pricing_rule = 1
|
||||
@@ -515,7 +527,6 @@ class AccountsController(TransactionBase):
|
||||
parent_dict.update({"customer": parent_dict.get("party_name")})
|
||||
|
||||
self.pricing_rules = []
|
||||
basic_item_details_map = {}
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.get("item_code"):
|
||||
@@ -535,17 +546,7 @@ class AccountsController(TransactionBase):
|
||||
if self.get("is_subcontracted"):
|
||||
args["is_subcontracted"] = self.is_subcontracted
|
||||
|
||||
basic_details = basic_item_details_map.get(item.item_code)
|
||||
ret, basic_item_details = get_item_details(
|
||||
args,
|
||||
self,
|
||||
for_validate=True,
|
||||
overwrite_warehouse=False,
|
||||
return_basic_details=True,
|
||||
basic_details=basic_details,
|
||||
)
|
||||
|
||||
basic_item_details_map.setdefault(item.item_code, basic_item_details)
|
||||
ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False)
|
||||
|
||||
for fieldname, value in ret.items():
|
||||
if item.meta.get_field(fieldname) and value is not None:
|
||||
@@ -1618,6 +1619,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
|
||||
grand_total = self.get("rounded_total") or self.grand_total
|
||||
automatically_fetch_payment_terms = 0
|
||||
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
|
||||
@@ -1663,19 +1665,26 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
self.append("payment_schedule", data)
|
||||
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(
|
||||
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
|
||||
)
|
||||
d.base_payment_amount = flt(
|
||||
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
elif not d.invoice_portion:
|
||||
d.base_payment_amount = flt(
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
if not (
|
||||
automatically_fetch_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
for d in self.get("payment_schedule"):
|
||||
if d.invoice_portion:
|
||||
d.payment_amount = flt(
|
||||
grand_total * flt(d.invoice_portion / 100), d.precision("payment_amount")
|
||||
)
|
||||
d.base_payment_amount = flt(
|
||||
base_grand_total * flt(d.invoice_portion / 100), d.precision("base_payment_amount")
|
||||
)
|
||||
d.outstanding = d.payment_amount
|
||||
elif not d.invoice_portion:
|
||||
d.base_payment_amount = flt(
|
||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
||||
)
|
||||
else:
|
||||
self.fetch_payment_terms_from_order(po_or_so, doctype)
|
||||
self.ignore_default_payment_terms_template = 1
|
||||
|
||||
def get_order_details(self):
|
||||
if self.doctype == "Sales Invoice":
|
||||
@@ -1728,6 +1737,10 @@ class AccountsController(TransactionBase):
|
||||
"invoice_portion": schedule.invoice_portion,
|
||||
"mode_of_payment": schedule.mode_of_payment,
|
||||
"description": schedule.description,
|
||||
"payment_amount": schedule.payment_amount,
|
||||
"base_payment_amount": schedule.base_payment_amount,
|
||||
"outstanding": schedule.outstanding,
|
||||
"paid_amount": schedule.paid_amount,
|
||||
}
|
||||
|
||||
if schedule.discount_type == "Percentage":
|
||||
@@ -1897,12 +1910,14 @@ class AccountsController(TransactionBase):
|
||||
reconcilation_entry.party = secondary_party
|
||||
reconcilation_entry.reference_type = self.doctype
|
||||
reconcilation_entry.reference_name = self.name
|
||||
reconcilation_entry.cost_center = self.cost_center
|
||||
reconcilation_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(
|
||||
self.company
|
||||
)
|
||||
|
||||
advance_entry.account = primary_account
|
||||
advance_entry.party_type = primary_party_type
|
||||
advance_entry.party = primary_party
|
||||
advance_entry.cost_center = self.cost_center
|
||||
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
advance_entry.is_advance = "Yes"
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
|
||||
@@ -576,7 +576,9 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_filtered_dimensions(
|
||||
doctype, txt, searchfield, start, page_len, filters, reference_doctype=None
|
||||
):
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
)
|
||||
@@ -617,7 +619,12 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
|
||||
query_filters.append(["name", query_selector, dimensions])
|
||||
|
||||
output = frappe.get_list(
|
||||
doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1
|
||||
doctype,
|
||||
fields=fields,
|
||||
filters=query_filters,
|
||||
or_filters=or_filters,
|
||||
as_list=1,
|
||||
reference_doctype=reference_doctype,
|
||||
)
|
||||
|
||||
return [tuple(d) for d in set(output)]
|
||||
|
||||
@@ -171,7 +171,7 @@ class SellingController(StockController):
|
||||
self.round_floats_in(sales_person)
|
||||
|
||||
sales_person.allocated_amount = flt(
|
||||
self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0,
|
||||
flt(self.amount_eligible_for_commission) * sales_person.allocated_percentage / 100.0,
|
||||
self.precision("allocated_amount", sales_person),
|
||||
)
|
||||
|
||||
|
||||
@@ -329,9 +329,10 @@ class StockController(AccountsController):
|
||||
"""Create batches if required. Called before submit"""
|
||||
for d in self.items:
|
||||
if d.get(warehouse_field) and not d.batch_no:
|
||||
has_batch_no, create_new_batch = frappe.db.get_value(
|
||||
has_batch_no, create_new_batch = frappe.get_cached_value(
|
||||
"Item", d.item_code, ["has_batch_no", "create_new_batch"]
|
||||
)
|
||||
|
||||
if has_batch_no and create_new_batch:
|
||||
d.batch_no = (
|
||||
frappe.get_doc(
|
||||
@@ -414,7 +415,7 @@ class StockController(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": d.name,
|
||||
"actual_qty": (self.docstatus == 1 and 1 or -1) * flt(d.get("stock_qty")),
|
||||
"stock_uom": frappe.db.get_value(
|
||||
"stock_uom": frappe.get_cached_value(
|
||||
"Item", args.get("item_code") or d.get("item_code"), "stock_uom"
|
||||
),
|
||||
"incoming_rate": 0,
|
||||
@@ -441,7 +442,43 @@ class StockController(AccountsController):
|
||||
if not dimension:
|
||||
continue
|
||||
|
||||
if row.get(dimension.source_fieldname):
|
||||
if self.doctype in [
|
||||
"Purchase Invoice",
|
||||
"Purchase Receipt",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
"Stock Entry",
|
||||
]:
|
||||
if (
|
||||
(
|
||||
sl_dict.actual_qty > 0
|
||||
and not self.get("is_return")
|
||||
or sl_dict.actual_qty < 0
|
||||
and self.get("is_return")
|
||||
)
|
||||
and self.doctype in ["Purchase Invoice", "Purchase Receipt"]
|
||||
) or (
|
||||
(
|
||||
sl_dict.actual_qty < 0
|
||||
and not self.get("is_return")
|
||||
or sl_dict.actual_qty > 0
|
||||
and self.get("is_return")
|
||||
)
|
||||
and self.doctype in ["Sales Invoice", "Delivery Note", "Stock Entry"]
|
||||
):
|
||||
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
|
||||
else:
|
||||
fieldname_start_with = "to"
|
||||
if self.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
fieldname_start_with = "from"
|
||||
|
||||
fieldname = f"{fieldname_start_with}_{dimension.source_fieldname}"
|
||||
sl_dict[dimension.target_fieldname] = row.get(fieldname)
|
||||
|
||||
if not sl_dict.get(dimension.target_fieldname):
|
||||
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
|
||||
|
||||
elif row.get(dimension.source_fieldname):
|
||||
sl_dict[dimension.target_fieldname] = row.get(dimension.source_fieldname)
|
||||
|
||||
if not sl_dict.get(dimension.target_fieldname) and dimension.fetch_from_parent:
|
||||
@@ -609,7 +646,7 @@ class StockController(AccountsController):
|
||||
def validate_customer_provided_item(self):
|
||||
for d in self.get("items"):
|
||||
# Customer Provided parts will have zero valuation rate
|
||||
if frappe.db.get_value("Item", d.item_code, "is_customer_provided_item"):
|
||||
if frappe.get_cached_value("Item", d.item_code, "is_customer_provided_item"):
|
||||
d.allow_zero_valuation_rate = 1
|
||||
|
||||
def set_rate_of_stock_uom(self):
|
||||
@@ -722,7 +759,7 @@ class StockController(AccountsController):
|
||||
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
||||
return message
|
||||
|
||||
def repost_future_sle_and_gle(self):
|
||||
def repost_future_sle_and_gle(self, force=False):
|
||||
args = frappe._dict(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
@@ -733,7 +770,10 @@ class StockController(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
if future_sle_exists(args) or repost_required_for_queue(self):
|
||||
if self.docstatus == 2:
|
||||
force = True
|
||||
|
||||
if force or future_sle_exists(args) or repost_required_for_queue(self):
|
||||
item_based_reposting = cint(
|
||||
frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")
|
||||
)
|
||||
@@ -859,6 +899,8 @@ def is_reposting_pending():
|
||||
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
if validate_future_sle_not_exists(args, key, sl_entries):
|
||||
return False
|
||||
@@ -903,6 +945,9 @@ def validate_future_sle_not_exists(args, key, sl_entries=None):
|
||||
item_key = (args.get("item_code"), args.get("warehouse"))
|
||||
|
||||
if not sl_entries and hasattr(frappe.local, "future_sle"):
|
||||
if key not in frappe.local.future_sle:
|
||||
return False
|
||||
|
||||
if not frappe.local.future_sle.get(key) or (
|
||||
item_key and item_key not in frappe.local.future_sle.get(key)
|
||||
):
|
||||
@@ -910,9 +955,6 @@ def validate_future_sle_not_exists(args, key, sl_entries=None):
|
||||
|
||||
|
||||
def get_cached_data(args, key):
|
||||
if not hasattr(frappe.local, "future_sle"):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
if key not in frappe.local.future_sle:
|
||||
frappe.local.future_sle[key] = frappe._dict({})
|
||||
|
||||
|
||||
@@ -689,7 +689,6 @@ class SubcontractingController(StockController):
|
||||
"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
|
||||
"serial_no": cstr(item.rejected_serial_no).strip(),
|
||||
"incoming_rate": 0.0,
|
||||
"recalculate_rate": 1,
|
||||
},
|
||||
)
|
||||
)
|
||||
@@ -741,7 +740,7 @@ class SubcontractingController(StockController):
|
||||
sco_doc = frappe.get_doc("Subcontracting Order", sco)
|
||||
sco_doc.update_status()
|
||||
|
||||
def set_missing_values_in_additional_costs(self):
|
||||
def calculate_additional_costs(self):
|
||||
self.total_additional_costs = sum(flt(item.amount) for item in self.get("additional_costs"))
|
||||
|
||||
if self.total_additional_costs:
|
||||
|
||||
@@ -976,6 +976,8 @@ def get_itemised_tax_breakup_html(doc):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_round_off_applicable_accounts(company, account_list):
|
||||
# required to set correct region
|
||||
frappe.flags.company = company
|
||||
account_list = get_regional_round_off_accounts(company, account_list)
|
||||
|
||||
return account_list
|
||||
|
||||
@@ -36,7 +36,7 @@ class TestSubcontractingController(FrappeTestCase):
|
||||
sco.remove_empty_rows()
|
||||
self.assertEqual((len_before - 1), len(sco.service_items))
|
||||
|
||||
def test_set_missing_values_in_additional_costs(self):
|
||||
def test_calculate_additional_costs(self):
|
||||
sco = get_subcontracting_order(do_not_submit=1)
|
||||
|
||||
rate_without_additional_cost = sco.items[0].rate
|
||||
|
||||
@@ -33,7 +33,6 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
def after_insert(self):
|
||||
if self.opportunity_from == "Lead":
|
||||
frappe.get_doc("Lead", self.party_name).set_status(update=True)
|
||||
self.disable_lead()
|
||||
|
||||
link_open_tasks(self.opportunity_from, self.party_name, self)
|
||||
link_open_events(self.opportunity_from, self.party_name, self)
|
||||
@@ -60,7 +59,7 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
if not self.get(field) and frappe.db.field_exists(self.opportunity_from, field):
|
||||
try:
|
||||
value = frappe.db.get_value(self.opportunity_from, self.party_name, field)
|
||||
self.db_set(field, value)
|
||||
self.set(field, value)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
@@ -119,10 +118,6 @@ class Opportunity(TransactionBase, CRMNote):
|
||||
prospect.flags.ignore_mandatory = True
|
||||
prospect.save()
|
||||
|
||||
def disable_lead(self):
|
||||
if self.opportunity_from == "Lead":
|
||||
frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1})
|
||||
|
||||
def make_new_lead_if_required(self):
|
||||
"""Set lead against new opportunity"""
|
||||
if (not self.get("party_name")) and self.contact_email:
|
||||
|
||||
@@ -315,6 +315,7 @@ class WebsiteItem(WebsiteGenerator):
|
||||
self.item_code, skip_quotation_creation=True
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def copy_specification_from_item_group(self):
|
||||
self.set("website_specifications", [])
|
||||
if self.item_group:
|
||||
|
||||
@@ -165,6 +165,7 @@
|
||||
"fieldname": "slide_3_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -214,6 +215,7 @@
|
||||
"fieldname": "slide_4_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -263,6 +265,7 @@
|
||||
"fieldname": "slide_5_content_align",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Align",
|
||||
"options": "Left\nCentre\nRight",
|
||||
"reqd": 0
|
||||
},
|
||||
{
|
||||
@@ -274,7 +277,7 @@
|
||||
}
|
||||
],
|
||||
"idx": 2,
|
||||
"modified": "2021-02-24 15:57:05.889709",
|
||||
"modified": "2023-05-12 15:03:57.604060",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Hero Slider",
|
||||
|
||||
@@ -30,10 +30,6 @@ doctype_js = {
|
||||
|
||||
override_doctype_class = {"Address": "erpnext.accounts.custom.address.ERPNextAddress"}
|
||||
|
||||
override_whitelisted_methods = {
|
||||
"frappe.www.contact.send_message": "erpnext.templates.utils.send_message"
|
||||
}
|
||||
|
||||
welcome_email = "erpnext.setup.utils.welcome_email"
|
||||
|
||||
# setup wizard
|
||||
@@ -374,6 +370,7 @@ scheduler_events = {
|
||||
"cron": {
|
||||
"0/15 * * * *": [
|
||||
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
|
||||
"erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation.trigger_reconciliation_for_queued_docs",
|
||||
],
|
||||
"0/30 * * * *": [
|
||||
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
||||
|
||||
@@ -48,7 +48,8 @@ frappe.ui.form.on("BOM", {
|
||||
return {
|
||||
query: "erpnext.manufacturing.doctype.bom.bom.item_query",
|
||||
filters: {
|
||||
"item_code": doc.item
|
||||
"include_item_in_manufacturing": 1,
|
||||
"is_fixed_asset": 0
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -411,7 +412,6 @@ frappe.ui.form.on("BOM", {
|
||||
}
|
||||
|
||||
frm.set_value("process_loss_qty", qty);
|
||||
frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -9,15 +9,14 @@
|
||||
"production_item_tab",
|
||||
"item",
|
||||
"company",
|
||||
"item_name",
|
||||
"uom",
|
||||
"quantity",
|
||||
"cb0",
|
||||
"is_active",
|
||||
"is_default",
|
||||
"allow_alternative_item",
|
||||
"set_rate_of_sub_assembly_item_based_on_bom",
|
||||
"project",
|
||||
"quantity",
|
||||
"image",
|
||||
"currency_detail",
|
||||
"rm_cost_as_per",
|
||||
@@ -27,6 +26,8 @@
|
||||
"column_break_ivyw",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
"materials_section",
|
||||
"items",
|
||||
"section_break_21",
|
||||
"operations_section_section",
|
||||
"with_operations",
|
||||
@@ -38,8 +39,6 @@
|
||||
"operating_cost_per_bom_quantity",
|
||||
"operations_section",
|
||||
"operations",
|
||||
"materials_section",
|
||||
"items",
|
||||
"scrap_section",
|
||||
"scrap_items_section",
|
||||
"scrap_items",
|
||||
@@ -59,6 +58,7 @@
|
||||
"total_cost",
|
||||
"base_total_cost",
|
||||
"more_info_tab",
|
||||
"item_name",
|
||||
"description",
|
||||
"column_break_27",
|
||||
"has_variants",
|
||||
@@ -192,6 +192,7 @@
|
||||
"options": "Quality Inspection Template"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "currency_detail",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Cost Configuration"
|
||||
@@ -417,7 +418,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "website_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
@@ -482,7 +483,7 @@
|
||||
{
|
||||
"fieldname": "section_break_21",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Operations & Materials"
|
||||
"label": "Operations"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
@@ -605,7 +606,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-13 17:31:37.504565",
|
||||
"modified": "2023-04-06 12:47:58.514795",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -1317,7 +1317,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not field in searchfields
|
||||
]
|
||||
|
||||
query_filters = {"disabled": 0, "end_of_life": (">", today())}
|
||||
query_filters = {"disabled": 0, "ifnull(end_of_life, '3099-12-31')": (">", today())}
|
||||
|
||||
or_cond_filters = {}
|
||||
if txt:
|
||||
@@ -1339,8 +1339,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not has_variants:
|
||||
query_filters["has_variants"] = 0
|
||||
|
||||
if filters and filters.get("is_stock_item"):
|
||||
query_filters["is_stock_item"] = 1
|
||||
if filters:
|
||||
for fieldname, value in filters.items():
|
||||
query_filters[fieldname] = value
|
||||
|
||||
return frappe.get_list(
|
||||
"Item",
|
||||
|
||||
@@ -698,6 +698,45 @@ class TestBOM(FrappeTestCase):
|
||||
bom.update_cost()
|
||||
self.assertFalse(bom.flags.cost_updated)
|
||||
|
||||
def test_do_not_include_manufacturing_and_fixed_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import item_query
|
||||
|
||||
if not frappe.db.exists("Asset Category", "Computers-Test"):
|
||||
doc = frappe.get_doc({"doctype": "Asset Category", "asset_category_name": "Computers-Test"})
|
||||
doc.flags.ignore_mandatory = True
|
||||
doc.insert()
|
||||
|
||||
for item_code, properties in {
|
||||
"_Test RM Item 1 Do Not Include In Manufacture": {
|
||||
"is_stock_item": 1,
|
||||
"include_item_in_manufacturing": 0,
|
||||
},
|
||||
"_Test RM Item 2 Fixed Asset Item": {
|
||||
"is_fixed_asset": 1,
|
||||
"is_stock_item": 0,
|
||||
"asset_category": "Computers-Test",
|
||||
},
|
||||
"_Test RM Item 3 Manufacture Item": {"is_stock_item": 1, "include_item_in_manufacturing": 1},
|
||||
}.items():
|
||||
make_item(item_code, properties)
|
||||
|
||||
data = item_query(
|
||||
"Item",
|
||||
txt="_Test RM Item",
|
||||
searchfield="name",
|
||||
start=0,
|
||||
page_len=20000,
|
||||
filters={"include_item_in_manufacturing": 1, "is_fixed_asset": 0},
|
||||
)
|
||||
|
||||
items = []
|
||||
for row in data:
|
||||
items.append(row[0])
|
||||
|
||||
self.assertTrue("_Test RM Item 1 Do Not Include In Manufacture" not in items)
|
||||
self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items)
|
||||
self.assertTrue("_Test RM Item 3 Manufacture Item" in items)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -439,7 +439,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-09 15:02:44.490731",
|
||||
"modified": "2023-05-23 09:56:43.826602",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -74,6 +74,52 @@ class JobCard(Document):
|
||||
self.update_sub_operation_status()
|
||||
self.validate_work_order()
|
||||
|
||||
def on_update(self):
|
||||
self.validate_job_card_qty()
|
||||
|
||||
def validate_job_card_qty(self):
|
||||
if not (self.operation_id and self.work_order):
|
||||
return
|
||||
|
||||
wo_qty = flt(frappe.get_cached_value("Work Order", self.work_order, "qty"))
|
||||
|
||||
completed_qty = flt(
|
||||
frappe.db.get_value("Work Order Operation", self.operation_id, "completed_qty")
|
||||
)
|
||||
|
||||
over_production_percentage = flt(
|
||||
frappe.db.get_single_value("Manufacturing Settings", "overproduction_percentage_for_work_order")
|
||||
)
|
||||
|
||||
wo_qty = wo_qty + (wo_qty * over_production_percentage / 100)
|
||||
|
||||
job_card_qty = frappe.get_all(
|
||||
"Job Card",
|
||||
fields=["sum(for_quantity)"],
|
||||
filters={
|
||||
"work_order": self.work_order,
|
||||
"operation_id": self.operation_id,
|
||||
"docstatus": ["!=", 2],
|
||||
},
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0
|
||||
|
||||
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
|
||||
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
|
||||
|
||||
msg = f"""
|
||||
Qty To Manufacture in the job card
|
||||
cannot be greater than Qty To Manufacture in the
|
||||
work order for the operation {bold(self.operation)}.
|
||||
<br><br><b>Solution: </b> Either you can reduce the
|
||||
Qty To Manufacture in the job card or set the
|
||||
'Overproduction Percentage For Work Order'
|
||||
in the {form_link}."""
|
||||
|
||||
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
|
||||
|
||||
def set_sub_operations(self):
|
||||
if not self.sub_operations and self.operation:
|
||||
self.sub_operations = []
|
||||
@@ -516,6 +562,7 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
def update_work_order_data(self, for_quantity, time_in_mins, wo):
|
||||
workstation_hour_rate = frappe.get_value("Workstation", self.workstation, "hour_rate")
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType("Job Card Time Log")
|
||||
|
||||
@@ -541,6 +588,7 @@ class JobCard(Document):
|
||||
if data.get("workstation") != self.workstation:
|
||||
# workstations can change in a job card
|
||||
data.workstation = self.workstation
|
||||
data.hour_rate = flt(workstation_hour_rate)
|
||||
|
||||
wo.flags.ignore_validate_update_after_submit = True
|
||||
wo.update_operation_status()
|
||||
@@ -682,7 +730,7 @@ class JobCard(Document):
|
||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
|
||||
if self.docstatus < 2:
|
||||
if self.for_quantity <= self.transferred_qty:
|
||||
if flt(self.for_quantity) <= flt(self.transferred_qty):
|
||||
self.status = "Material Transferred"
|
||||
|
||||
if self.time_logs:
|
||||
|
||||
@@ -272,6 +272,42 @@ class TestJobCard(FrappeTestCase):
|
||||
transfer_entry_2.insert()
|
||||
self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit)
|
||||
|
||||
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0})
|
||||
def test_job_card_excess_material_transfer_with_no_reference(self):
|
||||
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
|
||||
# fully transfer both RMs
|
||||
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||
row = transfer_entry_1.items[0]
|
||||
|
||||
# Add new row without reference of the job card item
|
||||
transfer_entry_1.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": row.item_code,
|
||||
"item_name": row.item_name,
|
||||
"item_group": row.item_group,
|
||||
"qty": row.qty,
|
||||
"uom": row.uom,
|
||||
"conversion_factor": row.conversion_factor,
|
||||
"stock_uom": row.stock_uom,
|
||||
"basic_rate": row.basic_rate,
|
||||
"basic_amount": row.basic_amount,
|
||||
"expense_account": row.expense_account,
|
||||
"cost_center": row.cost_center,
|
||||
"s_warehouse": row.s_warehouse,
|
||||
"t_warehouse": row.t_warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, transfer_entry_1.insert)
|
||||
|
||||
def test_job_card_partial_material_transfer(self):
|
||||
"Test partial material transfer against Job Card"
|
||||
self.transfer_material_against = "Job Card"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"column_break_4",
|
||||
"quantity",
|
||||
"uom",
|
||||
"conversion_factor",
|
||||
"projected_qty",
|
||||
"reserved_qty_for_production",
|
||||
"safety_stock",
|
||||
@@ -169,11 +170,17 @@
|
||||
"label": "Qty As Per BOM",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"label": "Conversion Factor",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-26 14:59:25.879631",
|
||||
"modified": "2023-05-03 12:43:29.895754",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Material Request Plan Item",
|
||||
|
||||
@@ -336,10 +336,6 @@ frappe.ui.form.on('Production Plan', {
|
||||
},
|
||||
|
||||
get_items_for_material_requests(frm, warehouses) {
|
||||
let set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse',
|
||||
'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty',
|
||||
'reserved_qty_for_production', 'material_request_type'];
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
|
||||
freeze: true,
|
||||
@@ -352,11 +348,11 @@ frappe.ui.form.on('Production Plan', {
|
||||
frm.set_value('mr_items', []);
|
||||
r.message.forEach(row => {
|
||||
let d = frm.add_child('mr_items');
|
||||
set_fields.forEach(field => {
|
||||
if (row[field]) {
|
||||
for (let field in row) {
|
||||
if (field !== 'name') {
|
||||
d[field] = row[field];
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
refresh_field('mr_items');
|
||||
|
||||
@@ -35,8 +35,12 @@
|
||||
"section_break_25",
|
||||
"prod_plan_references",
|
||||
"section_break_24",
|
||||
"get_sub_assembly_items",
|
||||
"combine_sub_items",
|
||||
"section_break_ucc4",
|
||||
"skip_available_sub_assembly_item",
|
||||
"column_break_igxl",
|
||||
"get_sub_assembly_items",
|
||||
"section_break_g4ip",
|
||||
"sub_assembly_items",
|
||||
"download_materials_request_plan_section_section",
|
||||
"download_materials_required",
|
||||
@@ -351,12 +355,12 @@
|
||||
{
|
||||
"fieldname": "section_break_24",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
"hide_border": 1,
|
||||
"label": "Sub Assembly Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "sub_assembly_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Assembly Items",
|
||||
"no_copy": 1,
|
||||
"options": "Production Plan Sub Assembly Item"
|
||||
},
|
||||
@@ -392,13 +396,33 @@
|
||||
"fieldname": "download_materials_request_plan_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Download Materials Request Plan Section"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System consider the projected quantity to check available or will be available sub-assembly items ",
|
||||
"fieldname": "skip_available_sub_assembly_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Skip Available Sub Assembly Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ucc4",
|
||||
"fieldtype": "Column Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_g4ip",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_igxl",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-31 10:30:48.118932",
|
||||
"modified": "2023-05-22 23:36:31.770517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -28,6 +28,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_item_details
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
|
||||
|
||||
@@ -398,9 +399,20 @@ class ProductionPlan(Document):
|
||||
self.set_status()
|
||||
self.db_set("status", self.status)
|
||||
|
||||
def on_submit(self):
|
||||
self.update_bin_qty()
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
self.delete_draft_work_order()
|
||||
self.update_bin_qty()
|
||||
|
||||
def update_bin_qty(self):
|
||||
for d in self.mr_items:
|
||||
if d.warehouse:
|
||||
bin_name = get_or_make_bin(d.item_code, d.warehouse)
|
||||
bin = frappe.get_doc("Bin", bin_name, for_update=True)
|
||||
bin.update_reserved_qty_for_production_plan()
|
||||
|
||||
def delete_draft_work_order(self):
|
||||
for d in frappe.get_all(
|
||||
@@ -575,6 +587,7 @@ class ProductionPlan(Document):
|
||||
"production_plan_sub_assembly_item": row.name,
|
||||
"bom": row.bom_no,
|
||||
"production_plan": self.name,
|
||||
"fg_item_qty": row.qty,
|
||||
}
|
||||
|
||||
for field in [
|
||||
@@ -705,7 +718,9 @@ class ProductionPlan(Document):
|
||||
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
|
||||
|
||||
bom_data = []
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
|
||||
|
||||
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
|
||||
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty, self.company, warehouse=warehouse)
|
||||
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
|
||||
sub_assembly_items_store.extend(bom_data)
|
||||
|
||||
@@ -881,7 +896,9 @@ def download_raw_materials(doc, warehouses=None):
|
||||
build_csv_response(item_list, doc.name)
|
||||
|
||||
|
||||
def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1):
|
||||
def get_exploded_items(
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=1, doc=None
|
||||
):
|
||||
bei = frappe.qb.DocType("BOM Explosion Item")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
item = frappe.qb.DocType("Item")
|
||||
@@ -1068,6 +1085,7 @@ def get_material_request_items(
|
||||
"item_code": row.item_code,
|
||||
"item_name": row.item_name,
|
||||
"quantity": required_qty / conversion_factor,
|
||||
"conversion_factor": conversion_factor,
|
||||
"required_bom_qty": total_qty,
|
||||
"stock_uom": row.get("stock_uom"),
|
||||
"warehouse": warehouse
|
||||
@@ -1257,6 +1275,12 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
include_safety_stock = doc.get("include_safety_stock")
|
||||
|
||||
so_item_details = frappe._dict()
|
||||
|
||||
sub_assembly_items = {}
|
||||
if doc.get("skip_available_sub_assembly_item"):
|
||||
for d in doc.get("sub_assembly_items"):
|
||||
sub_assembly_items.setdefault((d.get("production_item"), d.get("bom_no")), d.get("qty"))
|
||||
|
||||
for data in po_items:
|
||||
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
|
||||
data["include_exploded_items"] = 1
|
||||
@@ -1282,10 +1306,24 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
frappe.throw(_("For row {0}: Enter Planned Qty").format(data.get("idx")))
|
||||
|
||||
if bom_no:
|
||||
if data.get("include_exploded_items") and include_subcontracted_items:
|
||||
if (
|
||||
data.get("include_exploded_items")
|
||||
and doc.get("sub_assembly_items")
|
||||
and doc.get("skip_available_sub_assembly_item")
|
||||
):
|
||||
item_details = get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
company,
|
||||
bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
|
||||
elif data.get("include_exploded_items") and include_subcontracted_items:
|
||||
# fetch exploded items from BOM
|
||||
item_details = get_exploded_items(
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty
|
||||
item_details, company, bom_no, include_non_stock_items, planned_qty=planned_qty, doc=doc
|
||||
)
|
||||
else:
|
||||
item_details = get_subitems(
|
||||
@@ -1442,12 +1480,22 @@ def get_item_data(item_code):
|
||||
}
|
||||
|
||||
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, company, warehouse=None, indent=0):
|
||||
data = get_bom_children(parent=bom_no)
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
|
||||
if warehouse:
|
||||
bin_dict = get_bin_details(d, company, for_warehouse=warehouse)
|
||||
|
||||
if bin_dict and bin_dict[0].projected_qty > 0:
|
||||
if bin_dict[0].projected_qty > stock_qty:
|
||||
continue
|
||||
else:
|
||||
stock_qty = stock_qty - bin_dict[0].projected_qty
|
||||
|
||||
bom_data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
@@ -1467,10 +1515,106 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
)
|
||||
|
||||
if d.value:
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, indent=indent + 1)
|
||||
get_sub_assembly_items(d.value, bom_data, stock_qty, company, warehouse, indent=indent + 1)
|
||||
|
||||
|
||||
def set_default_warehouses(row, default_warehouses):
|
||||
for field in ["wip_warehouse", "fg_warehouse"]:
|
||||
if not row.get(field):
|
||||
row[field] = default_warehouses.get(field)
|
||||
|
||||
|
||||
def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import get_reserved_qty_for_production
|
||||
|
||||
table = frappe.qb.DocType("Production Plan")
|
||||
child = frappe.qb.DocType("Material Request Plan Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0)))
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (child.item_code == item_code)
|
||||
& (child.warehouse == warehouse)
|
||||
& (table.status.notin(["Completed", "Closed"]))
|
||||
)
|
||||
).run()
|
||||
|
||||
if not query:
|
||||
return 0.0
|
||||
|
||||
reserved_qty_for_production_plan = flt(query[0][0])
|
||||
|
||||
reserved_qty_for_production = flt(
|
||||
get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True)
|
||||
)
|
||||
|
||||
return reserved_qty_for_production_plan - reserved_qty_for_production
|
||||
|
||||
|
||||
def get_raw_materials_of_sub_assembly_items(
|
||||
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
|
||||
):
|
||||
|
||||
bei = frappe.qb.DocType("BOM Item")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
item = frappe.qb.DocType("Item")
|
||||
item_default = frappe.qb.DocType("Item Default")
|
||||
item_uom = frappe.qb.DocType("UOM Conversion Detail")
|
||||
|
||||
items = (
|
||||
frappe.qb.from_(bei)
|
||||
.join(bom)
|
||||
.on(bom.name == bei.parent)
|
||||
.join(item)
|
||||
.on(item.name == bei.item_code)
|
||||
.left_join(item_default)
|
||||
.on((item_default.parent == item.name) & (item_default.company == company))
|
||||
.left_join(item_uom)
|
||||
.on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom))
|
||||
.select(
|
||||
(IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"),
|
||||
item.item_name,
|
||||
item.name.as_("item_code"),
|
||||
bei.description,
|
||||
bei.stock_uom,
|
||||
bei.bom_no,
|
||||
item.min_order_qty,
|
||||
bei.source_warehouse,
|
||||
item.default_material_request_type,
|
||||
item.min_order_qty,
|
||||
item_default.default_warehouse,
|
||||
item.purchase_uom,
|
||||
item_uom.conversion_factor,
|
||||
item.safety_stock,
|
||||
)
|
||||
.where(
|
||||
(bei.docstatus == 1)
|
||||
& (bom.name == bom_no)
|
||||
& (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1)
|
||||
)
|
||||
.groupby(bei.item_code, bei.stock_uom)
|
||||
).run(as_dict=True)
|
||||
|
||||
for item in items:
|
||||
key = (item.item_code, item.bom_no)
|
||||
if item.bom_no and key in sub_assembly_items:
|
||||
planned_qty = flt(sub_assembly_items[key])
|
||||
get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
company,
|
||||
item.bom_no,
|
||||
include_non_stock_items,
|
||||
sub_assembly_items,
|
||||
planned_qty=planned_qty,
|
||||
)
|
||||
else:
|
||||
if not item.conversion_factor and item.purchase_uom:
|
||||
item.conversion_factor = get_uom_conversion_factor(item.item_code, item.purchase_uom)
|
||||
|
||||
item_details.setdefault(item.get("item_code"), item)
|
||||
|
||||
return item_details
|
||||
|
||||
@@ -307,6 +307,43 @@ class TestProductionPlan(FrappeTestCase):
|
||||
|
||||
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
|
||||
|
||||
def test_production_plan_for_subcontracting_po(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
bom_tree_1 = {"Test Laptop 1": {"Test Motherboard 1": {"Test Motherboard Wires 1": {}}}}
|
||||
create_nested_bom(bom_tree_1, prefix="")
|
||||
|
||||
item_doc = frappe.get_doc("Item", "Test Motherboard 1")
|
||||
company = "_Test Company"
|
||||
|
||||
item_doc.is_sub_contracted_item = 1
|
||||
for row in item_doc.item_defaults:
|
||||
if row.company == company and not row.default_supplier:
|
||||
row.default_supplier = "_Test Supplier"
|
||||
|
||||
if not item_doc.item_defaults:
|
||||
item_doc.append("item_defaults", {"company": company, "default_supplier": "_Test Supplier"})
|
||||
|
||||
item_doc.save()
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code="Test Laptop 1", planned_qty=10, use_multi_level_bom=1, do_not_submit=True
|
||||
)
|
||||
plan.get_sub_assembly_items()
|
||||
plan.set_default_supplier_for_subcontracting_order()
|
||||
plan.submit()
|
||||
|
||||
self.assertEqual(plan.sub_assembly_items[0].supplier, "_Test Supplier")
|
||||
plan.make_work_order()
|
||||
|
||||
po = frappe.db.get_value("Purchase Order Item", {"production_plan": plan.name}, "parent")
|
||||
po_doc = frappe.get_doc("Purchase Order", po)
|
||||
self.assertEqual(po_doc.supplier, "_Test Supplier")
|
||||
self.assertEqual(po_doc.items[0].qty, 10.0)
|
||||
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
|
||||
self.assertEqual(po_doc.items[0].fg_item_qty, 10.0)
|
||||
self.assertEqual(po_doc.items[0].fg_item, "Test Motherboard 1")
|
||||
|
||||
def test_production_plan_combine_subassembly(self):
|
||||
"""
|
||||
Test combining Sub assembly items belonging to the same BOM in Prod Plan.
|
||||
@@ -868,6 +905,71 @@ class TestProductionPlan(FrappeTestCase):
|
||||
for item_code in mr_items:
|
||||
self.assertTrue(item_code in validate_mr_items)
|
||||
|
||||
def test_resered_qty_for_production_plan_for_material_requests(self):
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
|
||||
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
pln = create_production_plan(item_code="Test Production Item 1")
|
||||
|
||||
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
self.assertEqual(after_qty - before_qty, 1)
|
||||
|
||||
pln = frappe.get_doc("Production Plan", pln.name)
|
||||
pln.cancel()
|
||||
|
||||
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
def test_skip_available_qty_for_sub_assembly_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
|
||||
bom_tree = {
|
||||
"Fininshed Goods1 For SUB Test": {
|
||||
"SubAssembly1 For SUB Test": {"ChildPart1 For SUB Test": {}},
|
||||
"SubAssembly2 For SUB Test": {},
|
||||
}
|
||||
}
|
||||
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=10,
|
||||
ignore_existing_ordered_qty=1,
|
||||
do_not_submit=1,
|
||||
skip_available_sub_assembly_item=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="SubAssembly1 For SUB Test",
|
||||
qty=5,
|
||||
rate=100,
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
self.assertTrue(plan.skip_available_sub_assembly_item)
|
||||
|
||||
plan.get_sub_assembly_items()
|
||||
|
||||
for row in plan.sub_assembly_items:
|
||||
if row.production_item == "SubAssembly1 For SUB Test":
|
||||
self.assertEqual(row.qty, 5)
|
||||
|
||||
mr_items = get_items_for_material_requests(plan.as_dict())
|
||||
for row in mr_items:
|
||||
row = frappe._dict(row)
|
||||
if row.item_code == "ChildPart1 For SUB Test":
|
||||
self.assertEqual(row.quantity, 5)
|
||||
|
||||
if row.item_code == "SubAssembly2 For SUB Test":
|
||||
self.assertEqual(row.quantity, 10)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
@@ -887,6 +989,7 @@ def create_production_plan(**args):
|
||||
"include_subcontracted_items": args.include_subcontracted_items or 0,
|
||||
"ignore_existing_ordered_qty": args.ignore_existing_ordered_qty or 0,
|
||||
"get_items_from": "Sales Order",
|
||||
"skip_available_sub_assembly_item": args.skip_available_sub_assembly_item or 0,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -900,6 +1003,7 @@ def create_production_plan(**args):
|
||||
"planned_qty": args.planned_qty or 1,
|
||||
"planned_start_date": args.planned_start_date or now_datetime(),
|
||||
"stock_uom": args.stock_uom or "Nos",
|
||||
"warehouse": args.warehouse,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
"uom",
|
||||
"stock_uom",
|
||||
"column_break_22",
|
||||
"description"
|
||||
"description",
|
||||
"section_break_4rxf",
|
||||
"actual_qty",
|
||||
"column_break_xfhm",
|
||||
"projected_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -183,12 +187,34 @@
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Schedule Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_4rxf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Actual Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_xfhm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "projected_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Projected Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-28 13:50:15.116082",
|
||||
"modified": "2023-05-22 17:52:34.708879",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan Sub Assembly Item",
|
||||
|
||||
@@ -1598,6 +1598,65 @@ class TestWorkOrder(FrappeTestCase):
|
||||
self.assertEqual(row.to_time, add_to_date(planned_start_date, minutes=30))
|
||||
self.assertEqual(row.workstation, workstations_to_check[index])
|
||||
|
||||
def test_job_card_extra_qty(self):
|
||||
items = [
|
||||
"Test FG Item for Scrap Item Test 1",
|
||||
"Test RM Item 1 for Scrap Item Test 1",
|
||||
"Test RM Item 2 for Scrap Item Test 1",
|
||||
]
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
for item_code in items:
|
||||
create_item(
|
||||
item_code=item_code,
|
||||
is_stock_item=1,
|
||||
is_purchase_item=1,
|
||||
opening_stock=100,
|
||||
valuation_rate=10,
|
||||
company=company,
|
||||
warehouse="Stores - TCP1",
|
||||
)
|
||||
|
||||
item = "Test FG Item for Scrap Item Test 1"
|
||||
raw_materials = ["Test RM Item 1 for Scrap Item Test 1", "Test RM Item 2 for Scrap Item Test 1"]
|
||||
if not frappe.db.get_value("BOM", {"item": item}):
|
||||
bom = make_bom(
|
||||
item=item, source_warehouse="Stores - TCP1", raw_materials=raw_materials, do_not_save=True
|
||||
)
|
||||
bom.with_operations = 1
|
||||
bom.append(
|
||||
"operations",
|
||||
{
|
||||
"operation": "_Test Operation 1",
|
||||
"workstation": "_Test Workstation 1",
|
||||
"hour_rate": 20,
|
||||
"time_in_mins": 60,
|
||||
},
|
||||
)
|
||||
|
||||
bom.submit()
|
||||
|
||||
wo_order = make_wo_order_test_record(
|
||||
item=item,
|
||||
company=company,
|
||||
planned_start_date=now(),
|
||||
qty=20,
|
||||
)
|
||||
job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name}, "name")
|
||||
job_card_doc = frappe.get_doc("Job Card", job_card)
|
||||
|
||||
# Make another Job Card for the same Work Order
|
||||
job_card2 = frappe.copy_doc(job_card_doc)
|
||||
self.assertRaises(frappe.ValidationError, job_card2.save)
|
||||
|
||||
frappe.db.set_single_value(
|
||||
"Manufacturing Settings", "overproduction_percentage_for_work_order", 100
|
||||
)
|
||||
|
||||
job_card2 = frappe.copy_doc(job_card_doc)
|
||||
job_card2.time_logs = []
|
||||
job_card2.save()
|
||||
|
||||
|
||||
def prepare_data_for_workstation_type_check():
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
|
||||
@@ -625,20 +625,18 @@ erpnext.work_order = {
|
||||
// all materials transferred for manufacturing, make this primary
|
||||
finish_btn.addClass('btn-primary');
|
||||
}
|
||||
} else {
|
||||
frappe.db.get_doc("Manufacturing Settings").then((doc) => {
|
||||
let allowance_percentage = doc.overproduction_percentage_for_work_order;
|
||||
} else if (frm.doc.__onload && frm.doc.__onload.overproduction_percentage) {
|
||||
let allowance_percentage = frm.doc.__onload.overproduction_percentage;
|
||||
|
||||
if (allowance_percentage > 0) {
|
||||
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
|
||||
if (allowance_percentage > 0) {
|
||||
let allowed_qty = frm.doc.qty + ((allowance_percentage / 100) * frm.doc.qty);
|
||||
|
||||
if ((flt(doc.produced_qty) < allowed_qty)) {
|
||||
frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
}
|
||||
if ((flt(doc.produced_qty) < allowed_qty)) {
|
||||
frm.add_custom_button(__('Finish'), function() {
|
||||
erpnext.work_order.make_se(frm, 'Manufacture');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -22,17 +22,13 @@
|
||||
"produced_qty",
|
||||
"process_loss_qty",
|
||||
"project",
|
||||
"serial_no_and_batch_for_finished_good_section",
|
||||
"has_serial_no",
|
||||
"has_batch_no",
|
||||
"column_break_17",
|
||||
"serial_no",
|
||||
"batch_size",
|
||||
"section_break_ndpq",
|
||||
"required_items",
|
||||
"work_order_configuration",
|
||||
"settings_section",
|
||||
"allow_alternative_item",
|
||||
"use_multi_level_bom",
|
||||
"column_break_18",
|
||||
"column_break_17",
|
||||
"skip_transfer",
|
||||
"from_wip_warehouse",
|
||||
"update_consumed_material_cost_in_project",
|
||||
@@ -42,9 +38,14 @@
|
||||
"column_break_12",
|
||||
"fg_warehouse",
|
||||
"scrap_warehouse",
|
||||
"serial_no_and_batch_for_finished_good_section",
|
||||
"has_serial_no",
|
||||
"has_batch_no",
|
||||
"column_break_18",
|
||||
"serial_no",
|
||||
"batch_size",
|
||||
"required_items_section",
|
||||
"materials_and_operations_tab",
|
||||
"required_items",
|
||||
"operations_section",
|
||||
"operations",
|
||||
"transfer_material_against",
|
||||
@@ -586,7 +587,11 @@
|
||||
{
|
||||
"fieldname": "materials_and_operations_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Materials & Operations"
|
||||
"label": "Operations"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_ndpq",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cogs",
|
||||
@@ -594,7 +599,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-03 14:16:35.427731",
|
||||
"modified": "2023-04-06 12:35:12.149827",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -558,12 +558,19 @@ class WorkOrder(Document):
|
||||
and self.production_plan_item
|
||||
and not self.production_plan_sub_assembly_item
|
||||
):
|
||||
qty = frappe.get_value("Production Plan Item", self.production_plan_item, "ordered_qty") or 0.0
|
||||
table = frappe.qb.DocType("Work Order")
|
||||
|
||||
if self.docstatus == 1:
|
||||
qty += self.qty
|
||||
elif self.docstatus == 2:
|
||||
qty -= self.qty
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty))
|
||||
.where(
|
||||
(table.production_plan == self.production_plan)
|
||||
& (table.production_plan_item == self.production_plan_item)
|
||||
& (table.docstatus == 1)
|
||||
)
|
||||
).run()
|
||||
|
||||
qty = flt(query[0][0]) if query else 0
|
||||
|
||||
frappe.db.set_value("Production Plan Item", self.production_plan_item, "ordered_qty", qty)
|
||||
|
||||
@@ -1476,12 +1483,14 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
|
||||
return doc
|
||||
|
||||
|
||||
def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
|
||||
def get_reserved_qty_for_production(
|
||||
item_code: str, warehouse: str, check_production_plan: bool = False
|
||||
) -> float:
|
||||
"""Get total reserved quantity for any item in specified warehouse"""
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
wo_item = frappe.qb.DocType("Work Order Item")
|
||||
|
||||
return (
|
||||
query = (
|
||||
frappe.qb.from_(wo)
|
||||
.from_(wo_item)
|
||||
.select(
|
||||
@@ -1502,7 +1511,12 @@ def get_reserved_qty_for_production(item_code: str, warehouse: str) -> float:
|
||||
| (wo_item.required_qty > wo_item.consumed_qty)
|
||||
)
|
||||
)
|
||||
).run()[0][0] or 0.0
|
||||
)
|
||||
|
||||
if check_production_plan:
|
||||
query = query.where(wo.production_plan.isnotnull())
|
||||
|
||||
return query.run()[0][0] or 0.0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user