mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-17 20:19:20 +00:00
Compare commits
467 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e7de0ac47 | ||
|
|
2277b1aff5 | ||
|
|
58c793f14e | ||
|
|
08cd08adcd | ||
|
|
35478bbf91 | ||
|
|
54ed428225 | ||
|
|
c046dad2c3 | ||
|
|
195f90232d | ||
|
|
1e52738150 | ||
|
|
cbd0a76645 | ||
|
|
552c5b5911 | ||
|
|
66b2b89bcd | ||
|
|
d804d43ed0 | ||
|
|
53bb2cf7c0 | ||
|
|
2aff16928c | ||
|
|
630dcf072f | ||
|
|
b73eb47a43 | ||
|
|
981c9c76c1 | ||
|
|
6a6398a392 | ||
|
|
325fc619dc | ||
|
|
2c9c6c3798 | ||
|
|
3a9888aad9 | ||
|
|
695ca39d84 | ||
|
|
9032d4c3d6 | ||
|
|
c01e40da3c | ||
|
|
552cb5c528 | ||
|
|
e6ccb00d4c | ||
|
|
5b481d9235 | ||
|
|
6b27b659e3 | ||
|
|
16c8b74d52 | ||
|
|
8dcb2f39c2 | ||
|
|
5d5dff9103 | ||
|
|
b529a6d00c | ||
|
|
41659a875b | ||
|
|
a61890ec2b | ||
|
|
fa04e368d3 | ||
|
|
98eeff8775 | ||
|
|
64acf179db | ||
|
|
0d5e45bb7c | ||
|
|
fc86784eb1 | ||
|
|
e1dc80b6d8 | ||
|
|
9e8bb9b235 | ||
|
|
ae90ee3f17 | ||
|
|
dab8ac7b1d | ||
|
|
ce769d3a2f | ||
|
|
d09857294c | ||
|
|
3a74968ced | ||
|
|
26725f4e53 | ||
|
|
030ce6d6a0 | ||
|
|
b46d93c709 | ||
|
|
5d01cad1d5 | ||
|
|
97b253740b | ||
|
|
94c3d66f2f | ||
|
|
a2b6e4a1c5 | ||
|
|
ebbecdba23 | ||
|
|
2263f9a477 | ||
|
|
b340d7d4f4 | ||
|
|
99b69c121e | ||
|
|
860486bb34 | ||
|
|
1693e3ef3f | ||
|
|
279cf6fe00 | ||
|
|
cdc04292f2 | ||
|
|
6788b58d1c | ||
|
|
5bfdc010f3 | ||
|
|
0e7efd75cd | ||
|
|
2d198e698a | ||
|
|
857ab70f4e | ||
|
|
ca21f16db2 | ||
|
|
5324000e2e | ||
|
|
9d2055c620 | ||
|
|
113da4f512 | ||
|
|
8d32ba9a2e | ||
|
|
23c82d410b | ||
|
|
580e825ec2 | ||
|
|
1e0532f387 | ||
|
|
8f569d9711 | ||
|
|
df820aece6 | ||
|
|
6530cfe84b | ||
|
|
c8565c47a2 | ||
|
|
f3c70a66b5 | ||
|
|
115fd48bbf | ||
|
|
918f8ca79b | ||
|
|
46ca347578 | ||
|
|
8226502956 | ||
|
|
71e537b030 | ||
|
|
8fd3e8e22e | ||
|
|
4ee4a57f72 | ||
|
|
a8ed2815a4 | ||
|
|
a2b676b340 | ||
|
|
691db5b877 | ||
|
|
7bec3d19ac | ||
|
|
3f85aa3aea | ||
|
|
9ccf4900fe | ||
|
|
bf1c606610 | ||
|
|
6c53d31f2d | ||
|
|
4de1af498b | ||
|
|
c65409c348 | ||
|
|
422aec12cb | ||
|
|
52e26b6da8 | ||
|
|
ebb62966d3 | ||
|
|
b05e2910d8 | ||
|
|
3785ffe5c9 | ||
|
|
a4ab198042 | ||
|
|
67c5249b38 | ||
|
|
af067d1c00 | ||
|
|
b4053ee0d8 | ||
|
|
9f846e2636 | ||
|
|
8d734df63b | ||
|
|
0998123e52 | ||
|
|
f2c556a6cc | ||
|
|
b41612bea8 | ||
|
|
da88196a89 | ||
|
|
bde209b077 | ||
|
|
1a278e7ca0 | ||
|
|
1637cb4168 | ||
|
|
d9e9f35230 | ||
|
|
cbc73148d3 | ||
|
|
60a18247e1 | ||
|
|
7cc0436083 | ||
|
|
f8eb48472e | ||
|
|
8074d396d0 | ||
|
|
cfbd71693b | ||
|
|
3a2d7d18a3 | ||
|
|
b55cefc54f | ||
|
|
05778bb81a | ||
|
|
a70296e9b5 | ||
|
|
bd6210a212 | ||
|
|
944c9ad0b3 | ||
|
|
bd00a484ea | ||
|
|
1427b4ac3f | ||
|
|
fd6e42e15e | ||
|
|
8b071c0d22 | ||
|
|
176ce0d4d6 | ||
|
|
dd888fc30a | ||
|
|
789adaeabe | ||
|
|
2342f8d710 | ||
|
|
8b22d9d95e | ||
|
|
626c799b60 | ||
|
|
b2f6d07c25 | ||
|
|
70d57260d6 | ||
|
|
9062b90237 | ||
|
|
40467bc26c | ||
|
|
abe599a49d | ||
|
|
9975f5fe69 | ||
|
|
dab17c194c | ||
|
|
8a3148eee6 | ||
|
|
1f79242366 | ||
|
|
3f673a6848 | ||
|
|
293f114c9d | ||
|
|
252cc89ec7 | ||
|
|
3eaccfe201 | ||
|
|
653bb1072f | ||
|
|
0a64e43e92 | ||
|
|
020db922b7 | ||
|
|
a67a11e933 | ||
|
|
8c620802f0 | ||
|
|
a31fb2ac6c | ||
|
|
a444325bd1 | ||
|
|
2f4b1341d2 | ||
|
|
7640944bb9 | ||
|
|
69c6b2f463 | ||
|
|
ef6f2389a0 | ||
|
|
1d4b97c619 | ||
|
|
0aed8c04c6 | ||
|
|
b1b6953aed | ||
|
|
b2ea5620b2 | ||
|
|
743b179b08 | ||
|
|
4553d04c38 | ||
|
|
4ca5e9eef8 | ||
|
|
7d7f929cfc | ||
|
|
085d685488 | ||
|
|
0458c548ec | ||
|
|
e5457f8bb7 | ||
|
|
46a49a134d | ||
|
|
25a9327b14 | ||
|
|
d0d38214c5 | ||
|
|
991c46d058 | ||
|
|
ca9bd8b499 | ||
|
|
199e25ec06 | ||
|
|
f38fb68d62 | ||
|
|
3a22d29d7b | ||
|
|
80642edf4f | ||
|
|
9a3e1058f6 | ||
|
|
26872c3c25 | ||
|
|
dba3f3d335 | ||
|
|
7f7c5f2381 | ||
|
|
8ee7c47fdf | ||
|
|
441a2bcf38 | ||
|
|
821f3f5884 | ||
|
|
ca70e8e9a6 | ||
|
|
70feb500f6 | ||
|
|
bf8b3d0546 | ||
|
|
07d8bc7852 | ||
|
|
b660b90adc | ||
|
|
5def006033 | ||
|
|
ade6acccfb | ||
|
|
bd795f5546 | ||
|
|
c12a560c63 | ||
|
|
ff1ca9d480 | ||
|
|
4b49080bc4 | ||
|
|
64e6b36d04 | ||
|
|
0e0360781e | ||
|
|
c62be10620 | ||
|
|
fa541a2604 | ||
|
|
5590b8d40b | ||
|
|
a32165016d | ||
|
|
57112258e6 | ||
|
|
0bec404e69 | ||
|
|
1d2fccfc0b | ||
|
|
cac9eed306 | ||
|
|
2bf12a6683 | ||
|
|
6205be5e73 | ||
|
|
38c44533b3 | ||
|
|
a6713b176b | ||
|
|
7805ccf176 | ||
|
|
a66ce02520 | ||
|
|
3c43b42a01 | ||
|
|
8ed3a0ec65 | ||
|
|
488d635dc9 | ||
|
|
08d230b3e3 | ||
|
|
f383fafb15 | ||
|
|
38124a7616 | ||
|
|
56cf5382f0 | ||
|
|
56f03aee02 | ||
|
|
841f5c24ad | ||
|
|
e8051ba180 | ||
|
|
2b7d58602d | ||
|
|
e8e09cf8ea | ||
|
|
188c633d6e | ||
|
|
1995291194 | ||
|
|
bb0d75eb78 | ||
|
|
475eada727 | ||
|
|
6cffba9a71 | ||
|
|
35f9f9f330 | ||
|
|
5b60fbbd30 | ||
|
|
2678694c5f | ||
|
|
b6a80da457 | ||
|
|
2e9a0cb01c | ||
|
|
38c1867ade | ||
|
|
a48b999af9 | ||
|
|
c082edabf4 | ||
|
|
60ec7d0fb8 | ||
|
|
7ed3c6d18a | ||
|
|
b7e4fb9d83 | ||
|
|
e49e7b621d | ||
|
|
0a67d20ff8 | ||
|
|
83c6d861eb | ||
|
|
f62e5e69b8 | ||
|
|
45bc218acb | ||
|
|
2809c46a6e | ||
|
|
15c41178d0 | ||
|
|
64950d39b5 | ||
|
|
ff1b83025a | ||
|
|
b1d40de87e | ||
|
|
2c13c4746b | ||
|
|
532031c21d | ||
|
|
cb70efb8ed | ||
|
|
ca0b4696ba | ||
|
|
4ba4da090d | ||
|
|
e3b2cc24b2 | ||
|
|
a24733791d | ||
|
|
aa94c91c12 | ||
|
|
87ffbdf129 | ||
|
|
5ef7b8c526 | ||
|
|
25cd230471 | ||
|
|
f05933e814 | ||
|
|
8df2612694 | ||
|
|
0bc98b609f | ||
|
|
02a1f815da | ||
|
|
6b6e017e36 | ||
|
|
89fcdbf56b | ||
|
|
eb2571492f | ||
|
|
55f2f1c515 | ||
|
|
673b893942 | ||
|
|
c85ce55f27 | ||
|
|
a19252e3b3 | ||
|
|
7ece6fd558 | ||
|
|
1d6e3e4e7d | ||
|
|
015f946a14 | ||
|
|
533a2dbc32 | ||
|
|
99aeb8ecd1 | ||
|
|
371030f8d4 | ||
|
|
2550b44db8 | ||
|
|
5b1674018b | ||
|
|
d8dab986fa | ||
|
|
f25e2295d0 | ||
|
|
b4fd4812cd | ||
|
|
876dec5077 | ||
|
|
36e9aae9d0 | ||
|
|
8ccb9a5ad2 | ||
|
|
85c0c16964 | ||
|
|
6a46045804 | ||
|
|
befa4bef0d | ||
|
|
f8294f1754 | ||
|
|
2d6640ac61 | ||
|
|
627b34a120 | ||
|
|
a2c82b4dc3 | ||
|
|
799119ad3e | ||
|
|
a4a0a2a0fb | ||
|
|
35fb2b8ede | ||
|
|
c844bf5547 | ||
|
|
81a16286a1 | ||
|
|
3c8534c4cc | ||
|
|
ac40b59665 | ||
|
|
3f2081b440 | ||
|
|
2db91ee67e | ||
|
|
f0eac47037 | ||
|
|
4df80c5b53 | ||
|
|
c150e5795e | ||
|
|
57282999ad | ||
|
|
623a0a932e | ||
|
|
9b06eaab78 | ||
|
|
dfda8e6241 | ||
|
|
2a5c9b469c | ||
|
|
0a0177cb9e | ||
|
|
eed144d7c9 | ||
|
|
a5ec0e4f50 | ||
|
|
ec3a226a83 | ||
|
|
19dc26ea16 | ||
|
|
e29a384f90 | ||
|
|
088bbac543 | ||
|
|
8c98f1692a | ||
|
|
182e84e94c | ||
|
|
33962ac995 | ||
|
|
3b636d5db7 | ||
|
|
70b8b3bb9e | ||
|
|
c42954bec7 | ||
|
|
eab6d69ec9 | ||
|
|
3f0bea2d9f | ||
|
|
34ed9b455f | ||
|
|
2b7abfb34b | ||
|
|
2ba1731d3f | ||
|
|
b4b8459f2c | ||
|
|
d5160c4c86 | ||
|
|
af19b81343 | ||
|
|
650d2f74ba | ||
|
|
68747b5818 | ||
|
|
38848ff43b | ||
|
|
87e8305753 | ||
|
|
a2345d467e | ||
|
|
3d8a344173 | ||
|
|
e112290728 | ||
|
|
3149785960 | ||
|
|
736cff84f2 | ||
|
|
0023476500 | ||
|
|
0c4295fb6f | ||
|
|
13371275db | ||
|
|
313e6af528 | ||
|
|
e2a0d6e5f6 | ||
|
|
25f5fb7637 | ||
|
|
22848eb4da | ||
|
|
b4df87e545 | ||
|
|
9ed40cc17d | ||
|
|
22b6760164 | ||
|
|
e559fafa83 | ||
|
|
44539f0944 | ||
|
|
a0d94c38c1 | ||
|
|
95ea9ca66b | ||
|
|
ce7ab8df9a | ||
|
|
798858bd4f | ||
|
|
c6774e35f2 | ||
|
|
2a2ae9a20c | ||
|
|
f039bfe35a | ||
|
|
1747e83cb1 | ||
|
|
de14c0838c | ||
|
|
3788d0f4f0 | ||
|
|
ba98a00c6c | ||
|
|
2c4510ed1e | ||
|
|
87f3ba5794 | ||
|
|
197d09b90a | ||
|
|
32d3fbf1e8 | ||
|
|
4ec25ac82e | ||
|
|
3dbc90a0b4 | ||
|
|
f00236e669 | ||
|
|
d4f6ca3564 | ||
|
|
10e7ae4dd3 | ||
|
|
0f00581f83 | ||
|
|
a3ddc9533a | ||
|
|
2b766bca97 | ||
|
|
38c2633594 | ||
|
|
9012a72185 | ||
|
|
249d14b072 | ||
|
|
f2fef54b83 | ||
|
|
1b1e4e4688 | ||
|
|
8f2002d419 | ||
|
|
5b643433e5 | ||
|
|
e02aaa9f1b | ||
|
|
b1816864de | ||
|
|
3f490f11d5 | ||
|
|
b545b69e4b | ||
|
|
6ba24912c3 | ||
|
|
73b8a294cf | ||
|
|
b5485dc909 | ||
|
|
292f71bcef | ||
|
|
1e4acb3703 | ||
|
|
cf66b5aa34 | ||
|
|
9d2c456668 | ||
|
|
5ed4fea3e3 | ||
|
|
b8ec3ae23a | ||
|
|
0e7f9711e1 | ||
|
|
3cad1304a0 | ||
|
|
9dc1f5f649 | ||
|
|
df6ca3af57 | ||
|
|
95db1677e2 | ||
|
|
ca9f1b6c7b | ||
|
|
e8e26a91bb | ||
|
|
dc88f7b30b | ||
|
|
ac0375fc2e | ||
|
|
5bb2e8a8ff | ||
|
|
3989a7ede6 | ||
|
|
ef719fe729 | ||
|
|
9d979e34ab | ||
|
|
080e9a3d73 | ||
|
|
79b3af6d3e | ||
|
|
29c976e9ae | ||
|
|
635fe427fe | ||
|
|
dc5b8367c5 | ||
|
|
3380deab02 | ||
|
|
8e199af118 | ||
|
|
7e3f30baad | ||
|
|
90500f0ffc | ||
|
|
32182d7cc7 | ||
|
|
4f02677d6f | ||
|
|
56bb88d281 | ||
|
|
124a0fe45d | ||
|
|
3fad90ebb9 | ||
|
|
99b6dc508e | ||
|
|
7d593dd3db | ||
|
|
c48647100f | ||
|
|
c54d995354 | ||
|
|
5c6cc1ea2a | ||
|
|
e09ee63d32 | ||
|
|
d8e5075424 | ||
|
|
d5a36fe8aa | ||
|
|
10df192275 | ||
|
|
4ebddc591f | ||
|
|
5cc6a1771d | ||
|
|
29e8801b7f | ||
|
|
d94000fecf | ||
|
|
5071cad161 | ||
|
|
0151f5f191 | ||
|
|
d139db296a | ||
|
|
3406e44b03 | ||
|
|
449fa05d7d | ||
|
|
55222468f9 | ||
|
|
f381b99b14 | ||
|
|
2c880dd609 | ||
|
|
87e297e899 | ||
|
|
6c94ca664f | ||
|
|
d911e1dab2 | ||
|
|
908f8ed462 | ||
|
|
633ccef2ff | ||
|
|
e8f8abd685 | ||
|
|
b6b5524228 | ||
|
|
76bdf7944c | ||
|
|
1999de0b75 | ||
|
|
43acfdff82 | ||
|
|
19b911120c | ||
|
|
bc07de8c12 | ||
|
|
b484db3ffd | ||
|
|
902ce45a36 | ||
|
|
bfc0044d23 | ||
|
|
c8e3da0a71 | ||
|
|
2eed8ee343 | ||
|
|
670c6dcdd7 | ||
|
|
7d607b82f1 | ||
|
|
376da8df0a |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.87.1"
|
||||
__version__ = "15.92.3"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -309,8 +309,8 @@ def get_dimensions(with_cost_center_and_project=False):
|
||||
if with_cost_center_and_project:
|
||||
dimension_filters.extend(
|
||||
[
|
||||
{"fieldname": "cost_center", "document_type": "Cost Center"},
|
||||
{"fieldname": "project", "document_type": "Project"},
|
||||
frappe._dict({"fieldname": "cost_center", "document_type": "Cost Center"}),
|
||||
frappe._dict({"fieldname": "project", "document_type": "Project"}),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@
|
||||
"reconciliation_queue_size",
|
||||
"column_break_resa",
|
||||
"exchange_gain_loss_posting_date",
|
||||
"payment_entry_settings",
|
||||
"show_account_balance",
|
||||
"show_party_balance",
|
||||
"invoicing_settings_tab",
|
||||
"accounts_transactions_settings_section",
|
||||
"over_billing_allowance",
|
||||
@@ -95,7 +98,8 @@
|
||||
"legacy_section",
|
||||
"ignore_is_opening_check_for_reporting",
|
||||
"payment_request_settings",
|
||||
"create_pr_in_draft_status"
|
||||
"create_pr_in_draft_status",
|
||||
"column_break_xrnd"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -636,6 +640,23 @@
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_entry_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Payment Entry Settings"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_account_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Account Balance"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_party_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Party Balance"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -643,7 +664,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-20 14:06:08.870427",
|
||||
"modified": "2025-11-06 17:48:07.682837",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -668,8 +689,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,10 @@ class AccountsSettings(Document):
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_account_balance: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
show_inclusive_tax_in_print: DF.Check
|
||||
show_party_balance: DF.Check
|
||||
show_payment_schedule_in_print: DF.Check
|
||||
show_taxes_as_table_in_print: DF.Check
|
||||
stale_days: DF.Int
|
||||
@@ -105,6 +107,7 @@ class AccountsSettings(Document):
|
||||
frappe.clear_cache()
|
||||
|
||||
self.validate_and_sync_auto_reconcile_config()
|
||||
self.hide_or_show_party_and_account_balance()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
@@ -112,6 +115,18 @@ class AccountsSettings(Document):
|
||||
_("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1
|
||||
)
|
||||
|
||||
def hide_or_show_party_and_account_balance(self):
|
||||
def set_property(fieldname, value):
|
||||
make_property_setter("Payment Entry", fieldname, "hidden", value, "Check")
|
||||
|
||||
if self.has_value_changed("show_party_balance"):
|
||||
set_property("party_balance", not self.show_party_balance)
|
||||
|
||||
if self.has_value_changed("show_account_balance"):
|
||||
account_fields = ["paid_from_account_balance", "paid_to_account_balance"]
|
||||
for field in account_fields:
|
||||
set_property(field, not self.show_account_balance)
|
||||
|
||||
def enable_payment_schedule_in_print(self):
|
||||
show_in_print = cint(self.show_payment_schedule_in_print)
|
||||
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
|
||||
|
||||
@@ -38,7 +38,10 @@
|
||||
"column_break_3czf",
|
||||
"bank_party_name",
|
||||
"bank_party_account_number",
|
||||
"bank_party_iban"
|
||||
"bank_party_iban",
|
||||
"extended_bank_statement_section",
|
||||
"included_fee",
|
||||
"excluded_fee"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -233,12 +236,32 @@
|
||||
{
|
||||
"fieldname": "column_break_oufv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "extended_bank_statement_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Extended Bank Statement"
|
||||
},
|
||||
{
|
||||
"fieldname": "included_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Included Fee",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"description": "On save, the Excluded Fee will be converted to an Included Fee.",
|
||||
"fieldname": "excluded_fee",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Excluded Fee",
|
||||
"non_negative": 1,
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-23 17:32:58.514807",
|
||||
"modified": "2025-12-07 20:49:18.600757",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
||||
@@ -32,6 +32,8 @@ class BankTransaction(Document):
|
||||
date: DF.Date | None
|
||||
deposit: DF.Currency
|
||||
description: DF.SmallText | None
|
||||
excluded_fee: DF.Currency
|
||||
included_fee: DF.Currency
|
||||
naming_series: DF.Literal["ACC-BTN-.YYYY.-"]
|
||||
party: DF.DynamicLink | None
|
||||
party_type: DF.Link | None
|
||||
@@ -45,9 +47,11 @@ class BankTransaction(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.handle_excluded_fee()
|
||||
self.update_allocated_amount()
|
||||
|
||||
def validate(self):
|
||||
self.validate_included_fee()
|
||||
self.validate_duplicate_references()
|
||||
self.validate_currency()
|
||||
|
||||
@@ -307,6 +311,40 @@ class BankTransaction(Document):
|
||||
|
||||
self.party_type, self.party = result
|
||||
|
||||
def validate_included_fee(self):
|
||||
"""
|
||||
The included_fee is only handled for withdrawals. An included_fee for a deposit, is not credited to the account and is
|
||||
therefore outside of the deposit value and can be larger than the deposit itself.
|
||||
"""
|
||||
|
||||
if self.included_fee and self.withdrawal:
|
||||
if self.included_fee > self.withdrawal:
|
||||
frappe.throw(_("Included fee is bigger than the withdrawal itself."))
|
||||
|
||||
def handle_excluded_fee(self):
|
||||
# Include the excluded fee on validate to handle all further processing the same
|
||||
excluded_fee = flt(self.excluded_fee)
|
||||
if excluded_fee <= 0:
|
||||
return
|
||||
|
||||
# Suppress a negative deposit (aka withdrawal), likely not intendend
|
||||
if flt(self.deposit) > 0 and (flt(self.deposit) - excluded_fee) < 0:
|
||||
frappe.throw(_("The Excluded Fee is bigger than the Deposit it is deducted from."))
|
||||
|
||||
# Enforce directionality
|
||||
if flt(self.deposit) > 0 and flt(self.withdrawal) > 0:
|
||||
frappe.throw(
|
||||
_("Only one of Deposit or Withdrawal should be non-zero when applying an Excluded Fee.")
|
||||
)
|
||||
|
||||
if flt(self.deposit) > 0:
|
||||
self.deposit = flt(self.deposit) - excluded_fee
|
||||
# A fee applied to deposit and withdrawal equal 0 become a withdrawal
|
||||
elif flt(self.withdrawal) >= 0:
|
||||
self.withdrawal = flt(self.withdrawal) + excluded_fee
|
||||
self.included_fee = flt(self.included_fee) + excluded_fee
|
||||
self.excluded_fee = 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_doctypes_for_bank_reconciliation():
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2025, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestBankTransactionFees(FrappeTestCase):
|
||||
def test_included_fee_throws(self):
|
||||
"""A fee that's part of a withdrawal cannot be bigger than the
|
||||
withdrawal itself."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 101
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.validate_included_fee)
|
||||
|
||||
def test_included_fee_allows_equal(self):
|
||||
"""A fee that's part of a withdrawal may be equal to the withdrawal
|
||||
amount (only the fee was deducted from the account)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 100
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_included_fee_allows_for_deposit(self):
|
||||
"""For deposits, a fee may be recorded separately without limiting the
|
||||
received amount."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.included_fee = 999
|
||||
|
||||
bt.validate_included_fee()
|
||||
|
||||
def test_excluded_fee_noop_when_zero(self):
|
||||
"""When there is no excluded fee to apply, the amounts should remain
|
||||
unchanged."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 5
|
||||
bt.excluded_fee = 0
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 100)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_throws_when_exceeds_deposit(self):
|
||||
"""A fee deducted from an incoming payment must not exceed the incoming
|
||||
amount (else it would be a withdrawal, a conversion we don't support)."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.excluded_fee = 11
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_throws_when_both_deposit_and_withdrawal_are_set(self):
|
||||
"""A transaction must be either incoming or outgoing when applying a
|
||||
fee, not both."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 10
|
||||
bt.withdrawal = 10
|
||||
bt.excluded_fee = 1
|
||||
|
||||
self.assertRaises(frappe.ValidationError, bt.handle_excluded_fee)
|
||||
|
||||
def test_excluded_fee_deducts_from_deposit(self):
|
||||
"""When a fee is deducted from an incoming payment, the net received
|
||||
amount decreases and the fee is tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 100
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 95)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_can_reduce_an_incoming_payment_to_zero(self):
|
||||
"""A separately-deducted fee may reduce an incoming payment to zero,
|
||||
while still tracking the fee."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 5
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 0)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_increases_outgoing_payment(self):
|
||||
"""When a separately-deducted fee is provided for an outgoing payment,
|
||||
the total money leaving increases and the fee is tracked."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 100
|
||||
bt.included_fee = 2
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 105)
|
||||
self.assertEqual(bt.included_fee, 7)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
|
||||
def test_excluded_fee_turns_zero_amount_into_withdrawal(self):
|
||||
"""If only an excluded fee is provided, it should be treated as an
|
||||
outgoing payment and the fee is then tracked as included."""
|
||||
bt = frappe.new_doc("Bank Transaction")
|
||||
bt.deposit = 0
|
||||
bt.withdrawal = 0
|
||||
bt.included_fee = 0
|
||||
bt.excluded_fee = 5
|
||||
|
||||
bt.handle_excluded_fee()
|
||||
|
||||
self.assertEqual(bt.deposit, 0)
|
||||
self.assertEqual(bt.withdrawal, 5)
|
||||
self.assertEqual(bt.included_fee, 5)
|
||||
self.assertEqual(bt.excluded_fee, 0)
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
} else if (["frankfurter.app", "frankfurter.dev"].includes(frm.doc.service_provider)) {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -104,7 +104,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-18 08:32:26.895076",
|
||||
"modified": "2025-11-25 13:03:41.896424",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -141,8 +141,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
elif self.service_provider == "frankfurter.app":
|
||||
elif self.service_provider in ("frankfurter.dev", "frankfurter.app"):
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
@@ -105,11 +105,13 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev", "frankfurter.app"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
|
||||
company_currency,
|
||||
currency=company_currency,
|
||||
)
|
||||
|
||||
if account_details:
|
||||
@@ -486,6 +486,9 @@ class ExchangeRateRevaluation(Document):
|
||||
journal_entry.posting_date = self.posting_date
|
||||
journal_entry.multi_currency = 1
|
||||
|
||||
# Prevent JE from overriding user-entered exchange rates (e.g., rate of 1)
|
||||
journal_entry.flags.ignore_exchange_rate = True
|
||||
|
||||
journal_entry_accounts = []
|
||||
for d in accounts:
|
||||
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
|
||||
|
||||
@@ -420,7 +420,7 @@ def update_against_account(voucher_type, voucher_no):
|
||||
if not entries:
|
||||
return
|
||||
company_currency = erpnext.get_company_currency(entries[0].company)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
|
||||
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in entries:
|
||||
|
||||
@@ -33,6 +33,7 @@ from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_sched
|
||||
get_depr_schedule,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.setup.utils import get_exchange_rate as _get_exchange_rate
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -273,93 +274,7 @@ class JournalEntry(AccountsController):
|
||||
)
|
||||
|
||||
def apply_tax_withholding(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
|
||||
return
|
||||
|
||||
parties = [d.party for d in self.get("accounts") if d.party]
|
||||
parties = list(set(parties))
|
||||
|
||||
if len(parties) > 1:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
account_type_map = get_account_type_map(self.company)
|
||||
party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
|
||||
doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
|
||||
debit_or_credit = (
|
||||
"debit_in_account_currency"
|
||||
if self.voucher_type == "Credit Note"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
rev_debit_or_credit = (
|
||||
"credit_in_account_currency"
|
||||
if debit_or_credit == "debit_in_account_currency"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||
|
||||
net_total = sum(
|
||||
d.get(debit_or_credit)
|
||||
for d in self.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
)
|
||||
|
||||
party_amount = sum(
|
||||
d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
|
||||
)
|
||||
|
||||
inv = frappe._dict(
|
||||
{
|
||||
party_type: parties[0],
|
||||
"doctype": doctype,
|
||||
"company": self.company,
|
||||
"posting_date": self.posting_date,
|
||||
"net_total": net_total,
|
||||
}
|
||||
)
|
||||
|
||||
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
|
||||
inv, self.tax_withholding_category
|
||||
)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.get("accounts"):
|
||||
if d.get("account") == tax_withholding_details.get("account_head"):
|
||||
d.update(
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
}
|
||||
)
|
||||
|
||||
accounts.append(d.get("account"))
|
||||
|
||||
if d.get("account") == party_account:
|
||||
d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_withholding_details.get("account_head"),
|
||||
rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
|
||||
"against_account": parties[0],
|
||||
},
|
||||
)
|
||||
|
||||
to_remove = [
|
||||
d
|
||||
for d in self.get("accounts")
|
||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
|
||||
]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
JournalEntryTaxWithholding(self).apply()
|
||||
|
||||
def update_asset_value(self):
|
||||
if self.flags.planned_depr_entry or self.voucher_type != "Depreciation Entry":
|
||||
@@ -1281,6 +1196,230 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Accounts table cannot be blank."))
|
||||
|
||||
|
||||
class JournalEntryTaxWithholding:
|
||||
def __init__(self, journal_entry):
|
||||
self.doc: JournalEntry = journal_entry
|
||||
self.party = None
|
||||
self.party_type = None
|
||||
self.party_account = None
|
||||
self.party_row = None
|
||||
self.existing_tds_rows = []
|
||||
self.precision = None
|
||||
self.has_multiple_parties = False
|
||||
|
||||
# Direction fields based on party type
|
||||
self.party_field = None # "credit" for Supplier, "debit" for Customer
|
||||
self.reverse_field = None # opposite of party_field
|
||||
|
||||
def apply(self):
|
||||
if not self._set_party_info():
|
||||
return
|
||||
|
||||
self._setup_direction_fields()
|
||||
self._reset_existing_tds()
|
||||
|
||||
if not self._should_apply_tds():
|
||||
self._cleanup_duplicate_tds_rows(None)
|
||||
return
|
||||
|
||||
if self.has_multiple_parties:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
net_total = self._calculate_net_total()
|
||||
if net_total <= 0:
|
||||
return
|
||||
|
||||
tds_details = self._get_tds_details(net_total)
|
||||
if not tds_details or not tds_details.get("tax_amount"):
|
||||
return
|
||||
|
||||
self._create_or_update_tds_row(tds_details)
|
||||
self._update_party_amount(tds_details.get("tax_amount"), is_reversal=False)
|
||||
|
||||
self._recalculate_totals()
|
||||
|
||||
def _should_apply_tds(self):
|
||||
return self.doc.apply_tds and self.doc.voucher_type in ("Debit Note", "Credit Note")
|
||||
|
||||
def _set_party_info(self):
|
||||
for row in self.doc.get("accounts"):
|
||||
if row.party_type in ("Customer", "Supplier") and row.party:
|
||||
if self.party and row.party != self.party:
|
||||
self.has_multiple_parties = True
|
||||
|
||||
if not self.party:
|
||||
self.party = row.party
|
||||
self.party_type = row.party_type
|
||||
self.party_account = row.account
|
||||
self.party_row = row
|
||||
|
||||
if row.get("is_tax_withholding_account"):
|
||||
self.existing_tds_rows.append(row)
|
||||
|
||||
return bool(self.party)
|
||||
|
||||
def _setup_direction_fields(self):
|
||||
"""
|
||||
For Supplier (TDS): party has credit, TDS reduces credit
|
||||
For Customer (TCS): party has debit, TCS increases debit
|
||||
"""
|
||||
if self.party_type == "Supplier":
|
||||
self.party_field = "credit"
|
||||
self.reverse_field = "debit"
|
||||
else: # Customer
|
||||
self.party_field = "debit"
|
||||
self.reverse_field = "credit"
|
||||
|
||||
self.precision = self.doc.precision(self.party_field, self.party_row)
|
||||
|
||||
def _reset_existing_tds(self):
|
||||
for row in self.existing_tds_rows:
|
||||
# TDS amount is always in credit (liability to government)
|
||||
tds_amount = flt(row.get("credit") - row.get("debit"), self.precision)
|
||||
if not tds_amount:
|
||||
continue
|
||||
|
||||
self._update_party_amount(tds_amount, is_reversal=True)
|
||||
|
||||
# zero_out_tds_row
|
||||
row.update(
|
||||
{
|
||||
"credit": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
def _update_party_amount(self, amount, is_reversal=False):
|
||||
amount = flt(amount, self.precision)
|
||||
amount_in_party_currency = flt(amount / self.party_row.get("exchange_rate", 1), self.precision)
|
||||
|
||||
# Determine which field the party amount is in
|
||||
active_field = self.party_field if self.party_row.get(self.party_field) else self.reverse_field
|
||||
|
||||
# If amount is in reverse field, flip the signs
|
||||
if active_field == self.reverse_field:
|
||||
amount = -amount
|
||||
amount_in_party_currency = -amount_in_party_currency
|
||||
|
||||
# Direction multiplier based on party type:
|
||||
# Customer (TCS): +1 (add to debit)
|
||||
# Supplier (TDS): -1 (subtract from credit)
|
||||
direction = 1 if self.party_type == "Customer" else -1
|
||||
|
||||
# Reversal inverts the direction
|
||||
if is_reversal:
|
||||
direction = -direction
|
||||
|
||||
adjustment = amount * direction
|
||||
adjustment_in_party_currency = amount_in_party_currency * direction
|
||||
|
||||
active_field_account_currency = f"{active_field}_in_account_currency"
|
||||
|
||||
self.party_row.update(
|
||||
{
|
||||
active_field: flt(self.party_row.get(active_field) + adjustment, self.precision),
|
||||
active_field_account_currency: flt(
|
||||
self.party_row.get(active_field_account_currency) + adjustment_in_party_currency,
|
||||
self.precision,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
def _calculate_net_total(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
account_type_map = get_account_type_map(self.doc.company)
|
||||
|
||||
return flt(
|
||||
sum(
|
||||
d.get(self.reverse_field) - d.get(self.party_field)
|
||||
for d in self.doc.get("accounts")
|
||||
if account_type_map.get(d.account) not in ("Tax", "Chargeable")
|
||||
and d.account != self.party_account
|
||||
and not d.get("is_tax_withholding_account")
|
||||
),
|
||||
self.precision,
|
||||
)
|
||||
|
||||
def _get_tds_details(self, net_total):
|
||||
return get_party_tax_withholding_details(
|
||||
frappe._dict(
|
||||
{
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"doctype": self.doc.doctype,
|
||||
"company": self.doc.company,
|
||||
"posting_date": self.doc.posting_date,
|
||||
"tax_withholding_net_total": net_total,
|
||||
"base_tax_withholding_net_total": net_total,
|
||||
"grand_total": net_total,
|
||||
}
|
||||
),
|
||||
self.doc.tax_withholding_category,
|
||||
)
|
||||
|
||||
def _create_or_update_tds_row(self, tds_details):
|
||||
tax_account = tds_details.get("account_head")
|
||||
account_currency = get_account_currency(tax_account)
|
||||
company_currency = frappe.get_cached_value("Company", self.doc.company, "default_currency")
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, self.doc.posting_date)
|
||||
|
||||
tax_amount = flt(tds_details.get("tax_amount"), self.precision)
|
||||
tax_amount_in_account_currency = flt(tax_amount / exchange_rate, self.precision)
|
||||
|
||||
# Find existing TDS row for this account
|
||||
tax_row = None
|
||||
for row in self.doc.get("accounts"):
|
||||
if row.account == tax_account and row.get("is_tax_withholding_account"):
|
||||
tax_row = row
|
||||
break
|
||||
|
||||
if not tax_row:
|
||||
tax_row = self.doc.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": tax_account,
|
||||
"account_currency": account_currency,
|
||||
"exchange_rate": exchange_rate,
|
||||
"cost_center": tds_details.get("cost_center"),
|
||||
"credit": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"is_tax_withholding_account": 1,
|
||||
},
|
||||
)
|
||||
|
||||
# TDS/TCS is always credited (liability to government)
|
||||
tax_row.update(
|
||||
{
|
||||
"credit": tax_amount,
|
||||
"credit_in_account_currency": tax_amount_in_account_currency,
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
self._cleanup_duplicate_tds_rows(tax_row)
|
||||
|
||||
def _cleanup_duplicate_tds_rows(self, current_tax_row):
|
||||
rows_to_remove = [
|
||||
row
|
||||
for row in self.doc.get("accounts")
|
||||
if row.get("is_tax_withholding_account") and row != current_tax_row
|
||||
]
|
||||
|
||||
for row in rows_to_remove:
|
||||
self.doc.remove(row)
|
||||
|
||||
def _recalculate_totals(self):
|
||||
self.doc.set_amounts_in_company_currency()
|
||||
self.doc.set_total_debit_credit()
|
||||
self.doc.set_against_account()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
|
||||
@@ -1649,8 +1788,6 @@ def get_exchange_rate(
|
||||
credit=None,
|
||||
exchange_rate=None,
|
||||
):
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
account_details = frappe.get_cached_value(
|
||||
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
|
||||
)
|
||||
@@ -1672,8 +1809,8 @@ def get_exchange_rate(
|
||||
|
||||
# The date used to retreive the exchange rate here is the date passed
|
||||
# in as an argument to this function.
|
||||
elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
elif (not flt(exchange_rate) or flt(exchange_rate) == 1) and account_currency and posting_date:
|
||||
exchange_rate = _get_exchange_rate(account_currency, company_currency, posting_date)
|
||||
else:
|
||||
exchange_rate = 1
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"reference_detail_no",
|
||||
"advance_voucher_type",
|
||||
"advance_voucher_no",
|
||||
"is_tax_withholding_account",
|
||||
"col_break3",
|
||||
"is_advance",
|
||||
"user_remark",
|
||||
@@ -282,12 +283,19 @@
|
||||
"options": "advance_voucher_type",
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_tax_withholding_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Tax Withholding Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-27 13:48:32.805100",
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -28,6 +28,7 @@ class JournalEntryAccount(Document):
|
||||
debit_in_account_currency: DF.Currency
|
||||
exchange_rate: DF.Float
|
||||
is_advance: DF.Literal["No", "Yes"]
|
||||
is_tax_withholding_account: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -592,6 +592,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
paid_from: function (frm) {
|
||||
if (frm.set_party_account_based_on_party) return;
|
||||
|
||||
frm.events.set_company_bank_account(frm);
|
||||
|
||||
frm.events.set_account_currency_and_balance(
|
||||
frm,
|
||||
frm.doc.paid_from,
|
||||
@@ -609,6 +611,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
paid_to: function (frm) {
|
||||
if (frm.set_party_account_based_on_party) return;
|
||||
|
||||
frm.events.set_company_bank_account(frm);
|
||||
|
||||
frm.events.set_account_currency_and_balance(
|
||||
frm,
|
||||
frm.doc.paid_to,
|
||||
@@ -1298,15 +1302,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const company_defaults = frappe.get_doc(":Company", frm.doc.company);
|
||||
const account =
|
||||
response.message?.[account_fieldname] ||
|
||||
company_defaults?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.cost_center = company_defaults?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
@@ -1350,6 +1353,8 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
if (frm.set_company_bank_account_based_on_coa) return;
|
||||
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
|
||||
if (frm.doc.bank_account && ["Pay", "Receive"].includes(frm.doc.payment_type)) {
|
||||
frappe.call({
|
||||
@@ -1388,6 +1393,34 @@ frappe.ui.form.on("Payment Entry", {
|
||||
}
|
||||
},
|
||||
|
||||
set_company_bank_account: function (frm) {
|
||||
if (!["Pay", "Receive"].includes(frm.doc.payment_type)) return;
|
||||
|
||||
const field = frm.doc.payment_type == "Pay" ? "paid_from" : "paid_to";
|
||||
|
||||
if (!frm.doc.company || !frm.doc[field]) return;
|
||||
|
||||
frm.set_company_bank_account_based_on_coa = true;
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
args: {
|
||||
doctype: "Bank Account",
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
account: frm.doc[field],
|
||||
disabled: 0,
|
||||
},
|
||||
fieldname: ["name"],
|
||||
},
|
||||
callback: async function (r) {
|
||||
if (r.message) await frm.set_value("bank_account", r.message.name);
|
||||
|
||||
frm.set_company_bank_account_based_on_coa = false;
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
sales_taxes_and_charges_template: function (frm) {
|
||||
frm.trigger("fetch_taxes_from_template");
|
||||
},
|
||||
|
||||
@@ -449,7 +449,7 @@ class PaymentEntry(AccountsController):
|
||||
self.contact_person = get_default_contact(self.party_type, self.party)
|
||||
|
||||
complete_contact_details(self)
|
||||
if not self.party_balance:
|
||||
if not self.party_balance and frappe.get_single_value("Accounts Settings", "show_party_balance"):
|
||||
self.party_balance = get_balance_on(
|
||||
party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
|
||||
)
|
||||
@@ -1800,7 +1800,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
|
||||
self.base_total_taxes_and_charges += tax.base_tax_amount
|
||||
self.base_total_taxes_and_charges += current_tax_amount
|
||||
|
||||
if self.get("taxes"):
|
||||
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
|
||||
@@ -2684,11 +2684,17 @@ def get_party_details(company, party_type, party, date, cost_center=None):
|
||||
|
||||
party_account = get_party_account(party_type, party, company)
|
||||
account_currency = get_account_currency(party_account)
|
||||
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
|
||||
account_balance = (
|
||||
get_balance_on(party_account, date, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
else 0
|
||||
)
|
||||
_party_name = "title" if party_type == "Shareholder" else party_type.lower() + "_name"
|
||||
party_name = frappe.db.get_value(party_type, party, _party_name)
|
||||
party_balance = get_balance_on(
|
||||
party_type=party_type, party=party, company=company, cost_center=cost_center
|
||||
party_balance = (
|
||||
get_balance_on(party_type=party_type, party=party, company=company, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_party_balance")
|
||||
else 0
|
||||
)
|
||||
if party_type in ["Customer", "Supplier"]:
|
||||
party_bank_account = get_party_bank_account(party_type, party)
|
||||
@@ -2717,7 +2723,11 @@ def get_account_details(account, date, cost_center=None):
|
||||
if not account_list:
|
||||
frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account))
|
||||
|
||||
account_balance = get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
|
||||
account_balance = (
|
||||
get_balance_on(account, date, cost_center=cost_center, ignore_account_permission=True)
|
||||
if frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
else 0
|
||||
)
|
||||
|
||||
return frappe._dict(
|
||||
{
|
||||
@@ -3529,11 +3539,18 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
|
||||
def get_party_and_account_balance(
|
||||
company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None
|
||||
):
|
||||
show_account_balance = frappe.get_single_value("Accounts Settings", "show_account_balance")
|
||||
return frappe._dict(
|
||||
{
|
||||
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
|
||||
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
|
||||
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center),
|
||||
"party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center)
|
||||
if frappe.get_single_value("Accounts Settings", "show_party_balance")
|
||||
else 0,
|
||||
"paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center)
|
||||
if show_account_balance
|
||||
else 0,
|
||||
"paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center)
|
||||
if show_account_balance
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -61,6 +61,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
};
|
||||
});
|
||||
this.frm.set_query("cost_center", "payments", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
this.frm.set_query("cost_center", "allocation", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: this.frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
refresh() {
|
||||
@@ -318,7 +334,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
options: "<b> New Journal Entry will be posted for the difference amount </b>",
|
||||
options: __(
|
||||
"New Journal Entry will be posted for the difference amount. The Posting Date can be modified."
|
||||
).bold(),
|
||||
},
|
||||
],
|
||||
primary_action: () => {
|
||||
|
||||
@@ -72,7 +72,7 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions = []
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
self.dimensions = get_dimensions()[0]
|
||||
self.dimensions = get_dimensions(with_cost_center_and_project=True)[0]
|
||||
|
||||
def load_from_db(self):
|
||||
# 'modified' attribute is required for `run_doc_method` to work properly.
|
||||
@@ -765,6 +765,14 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
for inv in dr_cr_notes:
|
||||
if (
|
||||
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
|
||||
< inv.allocated_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)
|
||||
)
|
||||
|
||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||
|
||||
reconcile_dr_or_cr = (
|
||||
|
||||
@@ -545,6 +545,9 @@ def make_payment_request(**args):
|
||||
if args.dt not in ALLOWED_DOCTYPES_FOR_PAYMENT_REQUEST:
|
||||
frappe.throw(_("Payment Requests cannot be created against: {0}").format(frappe.bold(args.dt)))
|
||||
|
||||
if args.dn and not isinstance(args.dn, str):
|
||||
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
|
||||
|
||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
@@ -850,6 +853,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
)
|
||||
|
||||
referenced_payment_requests = {pr.name: pr for pr in referenced_payment_requests}
|
||||
doc_updates = {}
|
||||
|
||||
for ref in references:
|
||||
if not ref.payment_request:
|
||||
@@ -875,7 +879,7 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
title=_("Invalid Allocated Amount"),
|
||||
)
|
||||
|
||||
# update status
|
||||
# determine status
|
||||
if new_outstanding_amount == payment_request["grand_total"]:
|
||||
status = "Initiated" if payment_request["payment_request_type"] == "Outward" else "Requested"
|
||||
elif new_outstanding_amount == 0:
|
||||
@@ -883,12 +887,15 @@ def update_payment_requests_as_per_pe_references(references=None, cancel=False):
|
||||
elif new_outstanding_amount > 0:
|
||||
status = "Partially Paid"
|
||||
|
||||
# update database
|
||||
frappe.db.set_value(
|
||||
"Payment Request",
|
||||
ref.payment_request,
|
||||
{"outstanding_amount": new_outstanding_amount, "status": status},
|
||||
)
|
||||
# prepare bulk update data
|
||||
doc_updates[ref.payment_request] = {
|
||||
"outstanding_amount": new_outstanding_amount,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
# bulk update all payment requests
|
||||
if doc_updates:
|
||||
frappe.db.bulk_update("Payment Request", doc_updates)
|
||||
|
||||
|
||||
def get_dummy_message(doc):
|
||||
|
||||
@@ -475,8 +475,15 @@ def process_gl_and_closing_entries(doc):
|
||||
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value(doc.doctype, doc.name, "gle_processing_status", "Failed")
|
||||
frappe.log_error(title=_("Period Closing Voucher {0} GL Entry Processing Failed").format(doc.name))
|
||||
frappe.db.set_value(
|
||||
doc.doctype,
|
||||
doc.name,
|
||||
{
|
||||
"error_message": str(e),
|
||||
"gle_processing_status": "Failed",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def process_cancellation(voucher_type, voucher_no):
|
||||
@@ -488,8 +495,17 @@ def process_cancellation(voucher_type, voucher_no):
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
|
||||
frappe.log_error(
|
||||
title=_("Period Closing Voucher {0} GL Entry Cancellation Failed").format(voucher_no)
|
||||
)
|
||||
frappe.db.set_value(
|
||||
voucher_type,
|
||||
voucher_no,
|
||||
{
|
||||
"error_message": str(e),
|
||||
"gle_processing_status": "Failed",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def delete_closing_entries(voucher_no):
|
||||
|
||||
@@ -18,12 +18,17 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.controllers.queries import item_query as _item_query
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||
|
||||
|
||||
class PartialPaymentValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ProductBundleStockValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -189,6 +194,9 @@ class POSInvoice(SalesInvoice):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self):
|
||||
if not self.customer:
|
||||
frappe.throw(_("Please select Customer first"))
|
||||
|
||||
if not cint(self.is_pos):
|
||||
frappe.throw(
|
||||
_("POS Invoice should have the field {0} checked.").format(frappe.bold(_("Include Payment")))
|
||||
@@ -345,34 +353,69 @@ class POSInvoice(SalesInvoice):
|
||||
):
|
||||
return
|
||||
|
||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.serial_and_batch_bundle:
|
||||
if is_negative_stock_allowed(item_code=d.item_code):
|
||||
return
|
||||
if frappe.db.exists("Product Bundle", d.item_code):
|
||||
(
|
||||
availability,
|
||||
is_stock_item,
|
||||
is_negative_stock_allowed,
|
||||
) = get_product_bundle_stock_availability(d.item_code, d.warehouse, d.stock_qty)
|
||||
|
||||
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
|
||||
else:
|
||||
availability, is_stock_item, is_negative_stock_allowed = get_stock_availability(
|
||||
d.item_code, d.warehouse
|
||||
)
|
||||
|
||||
item_code, warehouse, _qty = (
|
||||
frappe.bold(d.item_code),
|
||||
frappe.bold(d.warehouse),
|
||||
frappe.bold(d.qty),
|
||||
)
|
||||
if is_stock_item and flt(available_stock) <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
if is_negative_stock_allowed:
|
||||
continue
|
||||
|
||||
if isinstance(availability, list):
|
||||
error_msgs = []
|
||||
for item in availability:
|
||||
if flt(item["available"]) < flt(item["required"]):
|
||||
error_msgs.append(
|
||||
_("<li>Packed Item {0}: Required {1}, Available {2}</li>").format(
|
||||
frappe.bold(item["item_code"]),
|
||||
frappe.bold(flt(item["required"], 2)),
|
||||
frappe.bold(flt(item["available"], 2)),
|
||||
)
|
||||
)
|
||||
|
||||
if error_msgs:
|
||||
frappe.throw(
|
||||
_(
|
||||
"<b>Row #{0}:</b> Bundle {1} in warehouse {2} has insufficient packed items:<br><div style='margin-top: 15px;'><ul style='line-height: 0.8;'>{3}</ul></div>"
|
||||
).format(
|
||||
d.idx,
|
||||
frappe.bold(d.item_code),
|
||||
frappe.bold(d.warehouse),
|
||||
"<br>".join(error_msgs),
|
||||
),
|
||||
title=_("Insufficient Stock for Product Bundle Items"),
|
||||
exc=ProductBundleStockValidationError,
|
||||
)
|
||||
|
||||
else:
|
||||
item_code, warehouse = frappe.bold(d.item_code), frappe.bold(d.warehouse)
|
||||
if is_stock_item and flt(availability) <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Item {1} has no stock in warehouse {2}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Out of Stock"),
|
||||
)
|
||||
elif is_stock_item and flt(availability) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}.").format(
|
||||
d.idx,
|
||||
item_code,
|
||||
warehouse,
|
||||
frappe.bold(flt(availability, 2)),
|
||||
frappe.bold(flt(d.stock_qty, 2)),
|
||||
),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
def validate_serialised_or_batched_item(self):
|
||||
error_msg = []
|
||||
@@ -765,15 +808,35 @@ def get_stock_availability(item_code, warehouse):
|
||||
bin_qty = get_bin_qty(item_code, warehouse)
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
|
||||
return bin_qty - pos_sales_qty, is_stock_item
|
||||
return bin_qty - pos_sales_qty, is_stock_item, is_negative_stock_allowed(item_code=item_code)
|
||||
else:
|
||||
is_stock_item = True
|
||||
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
|
||||
return get_bundle_availability(item_code, warehouse), is_stock_item
|
||||
return get_bundle_availability(item_code, warehouse), is_stock_item, False
|
||||
else:
|
||||
is_stock_item = False
|
||||
# Is a service item or non_stock item
|
||||
return 0, is_stock_item
|
||||
return 0, is_stock_item, False
|
||||
|
||||
|
||||
def get_product_bundle_stock_availability(item_code, warehouse, item_qty):
|
||||
is_stock_item = True
|
||||
bundle = frappe.get_doc("Product Bundle", item_code)
|
||||
availabilities = []
|
||||
for bundle_item in bundle.items:
|
||||
if frappe.get_value("Item", bundle_item.item_code, "is_stock_item"):
|
||||
bin_qty = get_bin_qty(bundle_item.item_code, warehouse)
|
||||
reserved_qty = get_pos_reserved_qty(bundle_item.item_code, warehouse)
|
||||
available = bin_qty - reserved_qty
|
||||
availabilities.append(
|
||||
{
|
||||
"item_code": bundle_item.item_code,
|
||||
"required": bundle_item.qty * item_qty,
|
||||
"available": available,
|
||||
}
|
||||
)
|
||||
|
||||
return availabilities, is_stock_item, is_negative_stock_allowed(item_code=item_code)
|
||||
|
||||
|
||||
def get_bundle_availability(bundle_item_code, warehouse):
|
||||
|
||||
@@ -964,6 +964,84 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_bundle_stock_availability_validation(self):
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = "_Test Company"
|
||||
|
||||
# Create stock sub-items
|
||||
sub_item_a = "_Test Bundle SubA"
|
||||
if not frappe.db.exists("Item", sub_item_a):
|
||||
create_item(
|
||||
item_code=sub_item_a,
|
||||
is_stock_item=1,
|
||||
)
|
||||
|
||||
sub_item_b = "_Test Bundle SubB"
|
||||
if not frappe.db.exists("Item", sub_item_b):
|
||||
create_item(
|
||||
item_code=sub_item_b,
|
||||
is_stock_item=1,
|
||||
)
|
||||
|
||||
# Add initial stock: SubA=5, SubB=2
|
||||
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
|
||||
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
|
||||
|
||||
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
|
||||
bundle_item = "_Test Bundle"
|
||||
if not frappe.db.exists("Item", bundle_item):
|
||||
create_item(
|
||||
item_code=bundle_item,
|
||||
is_stock_item=0,
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Product Bundle", bundle_item):
|
||||
make_product_bundle(parent=bundle_item, items=[sub_item_a, sub_item_b])
|
||||
|
||||
# Test Case 1: Sufficient stock (bundle qty=1: requires SubA=2 (<=5), SubB=1 (<=2)) -> No error
|
||||
pos_inv_sufficient = create_pos_invoice(
|
||||
item=bundle_item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
warehouse=warehouse,
|
||||
pos_profile=self.pos_profile.name,
|
||||
do_not_save=1,
|
||||
)
|
||||
pos_inv_sufficient.append("payments", {"mode_of_payment": "Cash", "amount": 100, "default": 1})
|
||||
pos_inv_sufficient.insert()
|
||||
pos_inv_sufficient.submit()
|
||||
|
||||
pos_inv_sufficient.cancel()
|
||||
pos_inv_sufficient.delete()
|
||||
|
||||
# Test Case 2: Insufficient stock (reduce SubB to 1, bundle qty=2: requires SubB=2 >1) -> Error with details
|
||||
make_stock_entry(item_code=sub_item_b, from_warehouse=warehouse, qty=1, company=company)
|
||||
|
||||
pos_inv_insufficient = create_pos_invoice(
|
||||
item=bundle_item,
|
||||
qty=2,
|
||||
rate=100,
|
||||
warehouse=warehouse,
|
||||
pos_profile=self.pos_profile.name,
|
||||
do_not_save=1,
|
||||
)
|
||||
pos_inv_insufficient.append("payments", {"mode_of_payment": "Cash", "amount": 200, "default": 1})
|
||||
pos_inv_insufficient.save()
|
||||
self.assertRaises(ProductBundleStockValidationError, pos_inv_insufficient.submit)
|
||||
|
||||
frappe.set_user("test@example.com")
|
||||
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -41,9 +41,19 @@ class POSOpeningEntry(StatusUpdater):
|
||||
self.set_status()
|
||||
|
||||
def validate_pos_profile_and_cashier(self):
|
||||
if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
|
||||
if not frappe.db.exists("POS Profile", self.pos_profile):
|
||||
frappe.throw(_("POS Profile {} does not exist.").format(self.pos_profile))
|
||||
|
||||
pos_profile_company, pos_profile_disabled = frappe.db.get_value(
|
||||
"POS Profile", self.pos_profile, ["company", "disabled"]
|
||||
)
|
||||
|
||||
if pos_profile_disabled:
|
||||
frappe.throw(_("POS Profile {} is disabled.").format(frappe.bold(self.pos_profile)))
|
||||
|
||||
if self.company != pos_profile_company:
|
||||
frappe.throw(
|
||||
_("POS Profile {} does not belongs to company {}").format(self.pos_profile, self.company)
|
||||
_("POS Profile {} does not belong to company {}").format(self.pos_profile, self.company)
|
||||
)
|
||||
|
||||
if not cint(frappe.db.get_value("User", self.user, "enabled")):
|
||||
|
||||
@@ -70,6 +70,7 @@ class POSProfile(Document):
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
self.validate_disabled()
|
||||
self.validate_default_profile()
|
||||
self.validate_all_link_fields()
|
||||
self.validate_duplicate_groups()
|
||||
@@ -94,6 +95,21 @@ class POSProfile(Document):
|
||||
title=_("Mandatory Accounting Dimension"),
|
||||
)
|
||||
|
||||
def validate_disabled(self):
|
||||
old_doc = self.get_doc_before_save()
|
||||
|
||||
if (
|
||||
old_doc
|
||||
and self.disabled
|
||||
and old_doc.disabled != self.disabled
|
||||
and frappe.db.exists("POS Opening Entry", {"pos_profile": self.name, "status": "Open"})
|
||||
):
|
||||
frappe.throw(
|
||||
_("POS Profile {0} cannot be disabled as there are ongoing POS sessions.").format(
|
||||
frappe.bold(self.name)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_default_profile(self):
|
||||
for row in self.applicable_for_users:
|
||||
res = frappe.db.sql(
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.doctype.pos_profile.pos_profile import (
|
||||
get_child_nodes,
|
||||
@@ -38,6 +39,51 @@ class TestPOSProfile(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def test_disabled_pos_profile_creation(self):
|
||||
make_pos_profile(name="_Test POS Profile 001", disabled=1)
|
||||
|
||||
pos_profile = frappe.get_doc("POS Profile", "_Test POS Profile 001")
|
||||
|
||||
if pos_profile:
|
||||
self.assertEqual(pos_profile.disabled, 1)
|
||||
|
||||
def test_disabled_pos_profile_after_opening(self):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
|
||||
if pos_profile:
|
||||
create_opening_entry(pos_profile, test_user.name)
|
||||
self.assertEqual(pos_profile.disabled, 0)
|
||||
|
||||
pos_profile.disabled = 1
|
||||
self.assertRaises(frappe.ValidationError, pos_profile.save)
|
||||
|
||||
def test_disabled_pos_profile_after_completing_session(self):
|
||||
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import (
|
||||
make_closing_entry_from_opening,
|
||||
)
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import (
|
||||
create_opening_entry,
|
||||
)
|
||||
|
||||
test_user, pos_profile = init_user_and_profile()
|
||||
frappe.db.delete("POS Opening Entry", {"pos_profile": pos_profile.name})
|
||||
|
||||
if pos_profile:
|
||||
opening_entry = create_opening_entry(pos_profile, test_user.name)
|
||||
|
||||
closing_entry = make_closing_entry_from_opening(opening_entry)
|
||||
closing_entry.submit()
|
||||
|
||||
pos_profile.disabled = 1
|
||||
pos_profile.save()
|
||||
pos_profile.reload()
|
||||
|
||||
self.assertEqual(pos_profile.disabled, 1)
|
||||
|
||||
|
||||
def get_customers_list(pos_profile=None):
|
||||
if pos_profile is None:
|
||||
@@ -117,6 +163,7 @@ def make_pos_profile(**args):
|
||||
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
|
||||
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC",
|
||||
"location": "Block 1" if not args.do_not_set_accounting_dimension else None,
|
||||
"disabled": cint(args.disabled) or 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -243,10 +243,13 @@ def get_other_conditions(conditions, values, args):
|
||||
if group_condition:
|
||||
conditions += " and " + group_condition
|
||||
|
||||
if args.get("transaction_date"):
|
||||
date = args.get("transaction_date") or frappe.get_value(
|
||||
args.get("doctype"), args.get("name"), "posting_date", ignore=True
|
||||
)
|
||||
if date:
|
||||
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
|
||||
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
|
||||
values["transaction_date"] = args.get("transaction_date")
|
||||
values["transaction_date"] = date
|
||||
|
||||
if args.get("doctype") in [
|
||||
"Quotation",
|
||||
|
||||
@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
|
||||
|
||||
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
|
||||
setup(doc) {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
|
||||
@@ -125,8 +126,8 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if (doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||
cur_frm.add_custom_button(
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount > 0 && !cint(doc.is_return) && !doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
me.make_payment_request();
|
||||
@@ -574,17 +575,6 @@ cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function
|
||||
};
|
||||
};
|
||||
|
||||
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
if (d.cost_center) {
|
||||
var cl = doc.items || [];
|
||||
for (var i = 0; i < cl.length; i++) {
|
||||
if (!cl[i].cost_center) cl[i].cost_center = d.cost_center;
|
||||
}
|
||||
}
|
||||
refresh_field("items");
|
||||
};
|
||||
|
||||
cur_frm.fields_dict["items"].grid.get_field("project").get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Project", "status", "not in", "Completed, Cancelled"]],
|
||||
|
||||
@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
erpnext.selling.SellingController
|
||||
) {
|
||||
setup(doc) {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
this.frm.make_methods = {
|
||||
@@ -647,10 +648,6 @@ cur_frm.cscript.expense_account = function (doc, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "expense_account");
|
||||
};
|
||||
|
||||
cur_frm.cscript.cost_center = function (doc, cdt, cdn) {
|
||||
erpnext.utils.copy_value_in_all_rows(doc, cdt, cdn, "items", "cost_center");
|
||||
};
|
||||
|
||||
cur_frm.set_query("debit_to", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -1349,7 +1349,11 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
|
||||
if (
|
||||
flt(item.base_net_amount, item.precision("base_net_amount"))
|
||||
or item.is_fixed_asset
|
||||
or enable_discount_accounting
|
||||
):
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Int",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
@@ -102,7 +102,7 @@
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
@@ -199,7 +199,7 @@
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Int",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
@@ -221,7 +221,7 @@
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
@@ -324,7 +324,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-01-10 18:32:36.201124",
|
||||
"modified": "2025-12-10 08:06:40.611761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Share Balance",
|
||||
@@ -339,4 +339,4 @@
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class ShareBalance(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
amount: DF.Int
|
||||
amount: DF.Currency
|
||||
current_state: DF.Literal["", "Issued", "Purchased"]
|
||||
from_no: DF.Int
|
||||
is_company: DF.Check
|
||||
@@ -22,7 +22,7 @@ class ShareBalance(Document):
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
rate: DF.Int
|
||||
rate: DF.Currency
|
||||
share_type: DF.Link
|
||||
to_no: DF.Int
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -85,6 +85,9 @@ def get_party_details(inv):
|
||||
if inv.doctype == "Sales Invoice":
|
||||
party_type = "Customer"
|
||||
party = inv.customer
|
||||
elif inv.doctype == "Journal Entry":
|
||||
party_type = inv.party_type
|
||||
party = inv.party
|
||||
else:
|
||||
party_type = "Supplier"
|
||||
party = inv.supplier
|
||||
@@ -155,7 +158,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
party_type, parties, inv, tax_details, posting_date, pan_no
|
||||
)
|
||||
|
||||
if party_type == "Supplier":
|
||||
if party_type == "Supplier" or inv.doctype == "Journal Entry":
|
||||
tax_row = get_tax_row_for_tds(tax_details, tax_amount)
|
||||
else:
|
||||
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
|
||||
@@ -346,7 +349,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
elif party_type == "Customer":
|
||||
if tax_deducted:
|
||||
# if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
|
||||
tax_amount = 0
|
||||
if inv.doctype == "Sales Invoice":
|
||||
tax_amount = 0
|
||||
else:
|
||||
tax_amount = inv.base_tax_withholding_net_total * tax_details.rate / 100
|
||||
else:
|
||||
# if no TCS has been charged in FY,
|
||||
# then chargeable value is "prev invoices + advances - advance_adjusted" value which cross the threshold
|
||||
@@ -718,7 +724,7 @@ def get_advance_adjusted_in_invoice(inv):
|
||||
|
||||
|
||||
def get_invoice_total_without_tcs(inv, tax_details):
|
||||
tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
|
||||
tcs_tax_row = [d for d in inv.get("taxes") or [] if d.account_head == tax_details.account_head]
|
||||
tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
|
||||
|
||||
return inv.grand_total - tcs_tax_row_amount
|
||||
|
||||
@@ -848,6 +848,90 @@ class TestTaxWithholdingCategory(FrappeTestCase):
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 6000)
|
||||
self.assertEqual(payment.taxes[0].allocated_amount, 6000)
|
||||
|
||||
def test_tds_on_journal_entry_for_supplier(self):
|
||||
"""Test TDS deduction for Supplier in Debit Note"""
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
|
||||
jv = make_journal_entry_with_tax_withholding(
|
||||
party_type="Supplier",
|
||||
party="Test TDS Supplier",
|
||||
voucher_type="Debit Note",
|
||||
amount=50000,
|
||||
save=False,
|
||||
)
|
||||
jv.apply_tds = 1
|
||||
jv.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
jv.save()
|
||||
|
||||
# Again saving should not change tds amount
|
||||
jv.user_remark = "Test TDS on Journal Entry for Supplier"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
# TDS = 50000 * 10% = 5000
|
||||
self.assertEqual(len(jv.accounts), 3)
|
||||
|
||||
# Find TDS account row
|
||||
tds_row = None
|
||||
supplier_row = None
|
||||
for row in jv.accounts:
|
||||
if row.account == "TDS - _TC":
|
||||
tds_row = row
|
||||
elif row.party == "Test TDS Supplier":
|
||||
supplier_row = row
|
||||
|
||||
self.assertEqual(tds_row.credit, 5000)
|
||||
self.assertEqual(tds_row.debit, 0)
|
||||
|
||||
# Supplier amount should be reduced by TDS
|
||||
self.assertEqual(supplier_row.credit, 45000)
|
||||
jv.cancel()
|
||||
|
||||
def test_tcs_on_journal_entry_for_customer(self):
|
||||
"""Test TCS collection for Customer in Credit Note"""
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
|
||||
# Create Credit Note with amount exceeding threshold
|
||||
jv = make_journal_entry_with_tax_withholding(
|
||||
party_type="Customer",
|
||||
party="Test TCS Customer",
|
||||
voucher_type="Credit Note",
|
||||
amount=50000,
|
||||
save=False,
|
||||
)
|
||||
jv.apply_tds = 1
|
||||
jv.tax_withholding_category = "Cumulative Threshold TCS"
|
||||
jv.save()
|
||||
|
||||
# Again saving should not change tds amount
|
||||
jv.user_remark = "Test TCS on Journal Entry for Customer"
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
# Assert TCS calculation (10% on amount above threshold of 30000)
|
||||
self.assertEqual(len(jv.accounts), 3)
|
||||
|
||||
# Find TCS account row
|
||||
tcs_row = None
|
||||
customer_row = None
|
||||
for row in jv.accounts:
|
||||
if row.account == "TCS - _TC":
|
||||
tcs_row = row
|
||||
elif row.party == "Test TCS Customer":
|
||||
customer_row = row
|
||||
|
||||
# TCS should be credited (liability to government)
|
||||
self.assertEqual(tcs_row.credit, 2000) # above threshold 20000*10%
|
||||
self.assertEqual(tcs_row.debit, 0)
|
||||
|
||||
# Customer amount should be increased by TCS
|
||||
self.assertEqual(customer_row.debit, 52000)
|
||||
jv.cancel()
|
||||
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all(
|
||||
@@ -996,6 +1080,88 @@ def create_payment_entry(**args):
|
||||
return pe
|
||||
|
||||
|
||||
def make_journal_entry_with_tax_withholding(
|
||||
party_type,
|
||||
party,
|
||||
voucher_type,
|
||||
amount,
|
||||
cost_center=None,
|
||||
posting_date=None,
|
||||
save=True,
|
||||
submit=False,
|
||||
):
|
||||
"""Helper function to create Journal Entry for tax withholding"""
|
||||
if not cost_center:
|
||||
cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.posting_date = posting_date or today()
|
||||
jv.company = "_Test Company"
|
||||
jv.voucher_type = voucher_type
|
||||
jv.multi_currency = 0
|
||||
|
||||
if party_type == "Supplier":
|
||||
# Debit Note: Expense Dr, Supplier Cr
|
||||
expense_account = "Stock Received But Not Billed - _TC"
|
||||
party_account = "Creditors - _TC"
|
||||
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": expense_account,
|
||||
"cost_center": cost_center,
|
||||
"debit_in_account_currency": amount,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": party_account,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": cost_center,
|
||||
"credit_in_account_currency": amount,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
else: # Customer
|
||||
# Credit Note: Customer Dr, Income Cr
|
||||
party_account = "Debtors - _TC"
|
||||
income_account = "Sales - _TC"
|
||||
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": party_account,
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"cost_center": cost_center,
|
||||
"debit_in_account_currency": amount,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
jv.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": income_account,
|
||||
"cost_center": cost_center,
|
||||
"credit_in_account_currency": amount,
|
||||
"exchange_rate": 1,
|
||||
},
|
||||
)
|
||||
|
||||
if save or submit:
|
||||
jv.insert()
|
||||
|
||||
if submit:
|
||||
jv.submit()
|
||||
|
||||
return jv
|
||||
|
||||
|
||||
def create_records():
|
||||
# create a new suppliers
|
||||
for name in [
|
||||
|
||||
@@ -199,19 +199,20 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None, from_r
|
||||
for d in gl_map:
|
||||
cost_center = d.get("cost_center")
|
||||
|
||||
cost_center_allocation = get_cost_center_allocation_data(
|
||||
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||
)
|
||||
|
||||
if not cost_center_allocation:
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
# Validate budget against main cost center
|
||||
if not from_repost:
|
||||
validate_expense_against_budget(
|
||||
d, expense_amount=flt(d.debit, precision) - flt(d.credit, precision)
|
||||
)
|
||||
|
||||
cost_center_allocation = get_cost_center_allocation_data(
|
||||
gl_map[0]["company"], gl_map[0]["posting_date"], cost_center
|
||||
)
|
||||
if not cost_center_allocation:
|
||||
new_gl_map.append(d)
|
||||
continue
|
||||
|
||||
if d.account == round_off_account:
|
||||
d.cost_center = cost_center_allocation[0][0]
|
||||
new_gl_map.append(d)
|
||||
@@ -289,7 +290,9 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
|
||||
if not precision:
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
|
||||
)
|
||||
|
||||
# filter zero debit and credit entries
|
||||
merged_gl_map = filter(
|
||||
@@ -412,7 +415,11 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
gle.flags.notify_update = False
|
||||
gle.submit()
|
||||
|
||||
if not from_repost and gle.voucher_type != "Period Closing Voucher":
|
||||
if (
|
||||
not from_repost
|
||||
and gle.voucher_type != "Period Closing Voucher"
|
||||
and (gle.is_cancelled == 0 or gle.voucher_type == "Journal Entry")
|
||||
):
|
||||
validate_expense_against_budget(args)
|
||||
|
||||
|
||||
|
||||
@@ -26,16 +26,13 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "party_account",
|
||||
|
||||
@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
|
||||
@@ -28,16 +28,13 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
|
||||
@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimension_with_children,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import get_cost_centers_with_children
|
||||
from erpnext.accounts.utils import (
|
||||
build_qb_match_conditions,
|
||||
get_advance_payment_doctypes,
|
||||
@@ -994,11 +995,7 @@ class ReceivablePayableReport:
|
||||
self.add_accounting_dimensions_filters()
|
||||
|
||||
def get_cost_center_conditions(self):
|
||||
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
|
||||
cost_center_list = [
|
||||
center.name
|
||||
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
|
||||
]
|
||||
cost_center_list = get_cost_centers_with_children(self.filters.cost_center)
|
||||
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||
|
||||
def add_common_filters(self):
|
||||
|
||||
@@ -45,16 +45,13 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: () => {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
|
||||
@@ -119,6 +119,7 @@ def get_assets_details(assets):
|
||||
|
||||
fields = [
|
||||
"name as asset",
|
||||
"asset_name",
|
||||
"gross_purchase_amount",
|
||||
"opening_accumulated_depreciation",
|
||||
"asset_category",
|
||||
@@ -143,6 +144,12 @@ def get_columns():
|
||||
"options": "Asset",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Asset Name"),
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 140,
|
||||
},
|
||||
{
|
||||
"label": _("Depreciation Date"),
|
||||
"fieldname": "depreciation_date",
|
||||
|
||||
@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
|
||||
party_type = self.filters.party_type
|
||||
|
||||
doctype = qb.DocType(party_type)
|
||||
|
||||
party_details_fields = [
|
||||
doctype.name.as_("party"),
|
||||
f"{scrub(party_type)}_name",
|
||||
f"{scrub(party_type)}_group",
|
||||
]
|
||||
|
||||
if party_type == "Customer":
|
||||
party_details_fields.append(doctype.territory)
|
||||
|
||||
conditions = self.get_party_conditions(doctype)
|
||||
query = (
|
||||
qb.from_(doctype)
|
||||
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions))
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
@@ -153,6 +159,31 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
columns += [
|
||||
{
|
||||
"label": _("Customer Group"),
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer Group",
|
||||
},
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"options": "Territory",
|
||||
},
|
||||
]
|
||||
else:
|
||||
columns += [
|
||||
{
|
||||
"label": _("Supplier Group"),
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier Group",
|
||||
}
|
||||
]
|
||||
|
||||
columns += [
|
||||
{
|
||||
"label": _("Opening Balance"),
|
||||
@@ -213,35 +244,6 @@ class PartyLedgerSummaryReport:
|
||||
},
|
||||
]
|
||||
|
||||
# Hidden columns for handling 'User Permissions'
|
||||
if self.filters.party_type == "Customer":
|
||||
columns += [
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"options": "Territory",
|
||||
"hidden": 1,
|
||||
},
|
||||
{
|
||||
"label": _("Customer Group"),
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer Group",
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
else:
|
||||
columns += [
|
||||
{
|
||||
"label": _("Supplier Group"),
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier Group",
|
||||
"hidden": 1,
|
||||
}
|
||||
]
|
||||
|
||||
return columns
|
||||
|
||||
def get_data(self):
|
||||
|
||||
@@ -174,7 +174,7 @@ def add_solvency_ratios(
|
||||
return_on_equity_ratio = {"ratio": _("Return on Equity Ratio")}
|
||||
|
||||
for year in years:
|
||||
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
|
||||
profit_after_tax = flt(total_income.get(year)) - flt(total_expense.get(year))
|
||||
share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
|
||||
|
||||
debt_equity_ratio[year] = calculate_ratio(total_liability.get(year), share_holder_fund, precision)
|
||||
@@ -199,7 +199,7 @@ def add_turnover_ratios(data, years, period_list, filters, total_asset, net_sale
|
||||
|
||||
avg_data = {}
|
||||
for d in ["Receivable", "Payable", "Stock"]:
|
||||
avg_data[frappe.scrub(d)] = avg_ratio_balance("Receivable", period_list, precision, filters)
|
||||
avg_data[frappe.scrub(d)] = avg_ratio_balance(d, period_list, precision, filters)
|
||||
|
||||
avg_debtors, avg_creditors, avg_stock = (
|
||||
avg_data.get("receivable"),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-12-06 13:22:23",
|
||||
@@ -10,7 +10,7 @@
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-08-13 12:47:27.645023",
|
||||
"modified": "2025-11-05 15:47:59.597853",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General Ledger",
|
||||
|
||||
@@ -482,7 +482,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
|
||||
|
||||
immutable_ledger = frappe.db.get_single_value("Accounts Settings", "enable_immutable_ledger")
|
||||
|
||||
def update_value_in_dict(data, key, gle):
|
||||
def update_value_in_dict(data, key, gle, show_net_values=False):
|
||||
data[key].debit += gle.debit
|
||||
data[key].credit += gle.credit
|
||||
|
||||
@@ -493,10 +493,14 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
|
||||
data[key].debit_in_transaction_currency += gle.debit_in_transaction_currency
|
||||
data[key].credit_in_transaction_currency += gle.credit_in_transaction_currency
|
||||
|
||||
if filters.get("show_net_values_in_party_account") and account_type_map.get(data[key].account) in (
|
||||
"Receivable",
|
||||
"Payable",
|
||||
):
|
||||
if (
|
||||
filters.get("show_net_values_in_party_account")
|
||||
and account_type_map.get(data[key].account)
|
||||
in (
|
||||
"Receivable",
|
||||
"Payable",
|
||||
)
|
||||
) or show_net_values:
|
||||
net_value = data[key].debit - data[key].credit
|
||||
net_value_in_account_currency = (
|
||||
data[key].debit_in_account_currency - data[key].credit_in_account_currency
|
||||
@@ -526,11 +530,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
|
||||
|
||||
if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries):
|
||||
if not group_by_voucher_consolidated:
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "opening", gle, True)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle, True)
|
||||
|
||||
update_value_in_dict(totals, "opening", gle)
|
||||
update_value_in_dict(totals, "closing", gle)
|
||||
update_value_in_dict(totals, "opening", gle, True)
|
||||
update_value_in_dict(totals, "closing", gle, True)
|
||||
|
||||
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
|
||||
if not group_by_voucher_consolidated:
|
||||
@@ -566,6 +570,13 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map, tot
|
||||
else:
|
||||
update_value_in_dict(consolidated_gle, key, gle)
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
dimensions = [*accounting_dimensions, "cost_center", "project"]
|
||||
|
||||
for dimension in dimensions:
|
||||
if val := gle.get(dimension):
|
||||
gle[dimension] = _(val)
|
||||
|
||||
for value in consolidated_gle.values():
|
||||
update_value_in_dict(totals, "total", value)
|
||||
update_value_in_dict(totals, "closing", value)
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (
|
||||
@@ -16,7 +15,7 @@ from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register i
|
||||
get_group_by_and_display_fields,
|
||||
get_tax_accounts,
|
||||
)
|
||||
from erpnext.accounts.report.utils import get_query_columns, get_values_for_columns
|
||||
from erpnext.accounts.report.utils import get_values_for_columns
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -41,16 +40,6 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
tax_doctype="Purchase Taxes and Charges",
|
||||
)
|
||||
|
||||
scrubbed_tax_fields = {}
|
||||
|
||||
for tax in tax_columns:
|
||||
scrubbed_tax_fields.update(
|
||||
{
|
||||
tax + " Rate": frappe.scrub(tax + " Rate"),
|
||||
tax + " Amount": frappe.scrub(tax + " Amount"),
|
||||
}
|
||||
)
|
||||
|
||||
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
|
||||
|
||||
data = []
|
||||
@@ -100,8 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
{
|
||||
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
|
||||
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
|
||||
f"{tax}_rate": item_tax.get("tax_rate", 0),
|
||||
f"{tax}_amount": item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import functions as fn
|
||||
from frappe.utils import cstr, flt
|
||||
from frappe.utils import flt
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
|
||||
@@ -32,16 +32,6 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
scrubbed_tax_fields = {}
|
||||
|
||||
for tax in tax_columns:
|
||||
scrubbed_tax_fields.update(
|
||||
{
|
||||
tax + " Rate": frappe.scrub(tax + " Rate"),
|
||||
tax + " Amount": frappe.scrub(tax + " Amount"),
|
||||
}
|
||||
)
|
||||
|
||||
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
||||
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
||||
|
||||
@@ -102,8 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
{
|
||||
scrubbed_tax_fields[tax + " Rate"]: item_tax.get("tax_rate", 0),
|
||||
scrubbed_tax_fields[tax + " Amount"]: item_tax.get("tax_amount", 0),
|
||||
f"{tax}_rate": item_tax.get("tax_rate", 0),
|
||||
f"{tax}_amount": item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
if item_tax.get("is_other_charges"):
|
||||
@@ -546,9 +536,10 @@ def get_tax_accounts(
|
||||
import json
|
||||
|
||||
item_row_map = {}
|
||||
tax_columns = []
|
||||
tax_columns = {}
|
||||
invoice_item_row = {}
|
||||
itemised_tax = {}
|
||||
scrubbed_description_map = {}
|
||||
add_deduct_tax = "charge_type"
|
||||
|
||||
tax_amount_precision = (
|
||||
@@ -605,9 +596,14 @@ def get_tax_accounts(
|
||||
tax_amount,
|
||||
) in tax_details:
|
||||
description = handle_html(description)
|
||||
if description not in tax_columns and tax_amount:
|
||||
scrubbed_description = scrubbed_description_map.get(description)
|
||||
if not scrubbed_description:
|
||||
scrubbed_description = frappe.scrub(description)
|
||||
scrubbed_description_map[description] = scrubbed_description
|
||||
|
||||
if scrubbed_description not in tax_columns and tax_amount:
|
||||
# as description is text editor earlier and markup can break the column convention in reports
|
||||
tax_columns.append(description)
|
||||
tax_columns[scrubbed_description] = description
|
||||
|
||||
if item_wise_tax_detail:
|
||||
try:
|
||||
@@ -641,7 +637,7 @@ def get_tax_accounts(
|
||||
else tax_value
|
||||
)
|
||||
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
|
||||
{
|
||||
"tax_rate": tax_rate,
|
||||
"tax_amount": tax_value,
|
||||
@@ -653,7 +649,7 @@ def get_tax_accounts(
|
||||
continue
|
||||
elif charge_type == "Actual" and tax_amount:
|
||||
for d in invoice_item_row.get(parent, []):
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
itemised_tax.setdefault(d.name, {})[scrubbed_description] = frappe._dict(
|
||||
{
|
||||
"tax_rate": "NA",
|
||||
"tax_amount": flt(
|
||||
@@ -662,12 +658,14 @@ def get_tax_accounts(
|
||||
}
|
||||
)
|
||||
|
||||
tax_columns.sort()
|
||||
for desc in tax_columns:
|
||||
tax_columns_list = list(tax_columns.keys())
|
||||
tax_columns_list.sort()
|
||||
for scrubbed_desc in tax_columns_list:
|
||||
desc = tax_columns[scrubbed_desc]
|
||||
columns.append(
|
||||
{
|
||||
"label": _(desc + " Rate"),
|
||||
"fieldname": frappe.scrub(desc + " Rate"),
|
||||
"fieldname": f"{scrubbed_desc}_rate",
|
||||
"fieldtype": "Float",
|
||||
"width": 100,
|
||||
}
|
||||
@@ -676,7 +674,7 @@ def get_tax_accounts(
|
||||
columns.append(
|
||||
{
|
||||
"label": _(desc + " Amount"),
|
||||
"fieldname": frappe.scrub(desc + " Amount"),
|
||||
"fieldname": f"{scrubbed_desc}_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 100,
|
||||
@@ -714,7 +712,7 @@ def get_tax_accounts(
|
||||
},
|
||||
]
|
||||
|
||||
return itemised_tax, tax_columns
|
||||
return itemised_tax, tax_columns_list
|
||||
|
||||
|
||||
def add_total_row(
|
||||
@@ -807,5 +805,5 @@ def add_sub_total_row(item, total_row_map, group_by_value, tax_columns):
|
||||
total_row["percent_gt"] += item["percent_gt"]
|
||||
|
||||
for tax in tax_columns:
|
||||
total_row.setdefault(frappe.scrub(tax + " Amount"), 0.0)
|
||||
total_row[frappe.scrub(tax + " Amount")] += flt(item[frappe.scrub(tax + " Amount")])
|
||||
total_row.setdefault(f"{tax}_amount", 0.0)
|
||||
total_row[f"{tax}_amount"] += flt(item[f"{tax}_amount"])
|
||||
|
||||
@@ -21,6 +21,7 @@ def get_ordered_to_be_billed_data(args, filters=None):
|
||||
|
||||
doctype = frappe.qb.DocType(doctype)
|
||||
child_doctype = frappe.qb.DocType(child_tab)
|
||||
item = frappe.qb.DocType("Item")
|
||||
|
||||
docname = filters.get(args.get("reference_field"), None)
|
||||
project_field = get_project_field(doctype, child_doctype, party)
|
||||
@@ -29,6 +30,8 @@ def get_ordered_to_be_billed_data(args, filters=None):
|
||||
frappe.qb.from_(doctype)
|
||||
.inner_join(child_doctype)
|
||||
.on(doctype.name == child_doctype.parent)
|
||||
.join(item)
|
||||
.on(item.name == child_doctype.item_code)
|
||||
.select(
|
||||
doctype.name,
|
||||
doctype[args.get("date")].as_("date"),
|
||||
@@ -54,6 +57,7 @@ def get_ordered_to_be_billed_data(args, filters=None):
|
||||
& (doctype.company == filters.get("company"))
|
||||
& (doctype.posting_date <= filters.get("posting_date"))
|
||||
& (child_doctype.amount > 0)
|
||||
& (item.is_stock_item == 1)
|
||||
& (
|
||||
child_doctype.base_amount
|
||||
- Round(child_doctype.billed_amt * IfNull(doctype.conversion_rate, 1), precision)
|
||||
|
||||
@@ -1,32 +1,37 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:46:55",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:31.261299",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Purchase Invoice Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:46:55",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:49.950442",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Invoice",
|
||||
"report_name": "Purchase Invoice Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:44:21",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:03.622485",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Sales Invoice Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:44:21",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.070651",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Sales Invoice Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -43,6 +45,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
party_map = get_party_pan_map(filters.get("party_type"))
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
precision = get_currency_precision()
|
||||
|
||||
out = []
|
||||
entries = {}
|
||||
@@ -72,17 +75,28 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
|
||||
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
|
||||
if net_total_map.get((voucher_type, name)):
|
||||
|
||||
values = net_total_map.get((voucher_type, name))
|
||||
|
||||
if values:
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
base_total = min(tax_amount / (rate / 100), net_total_map.get((voucher_type, name))[0])
|
||||
# back calculate total amount from rate and tax_amount
|
||||
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
|
||||
total_amount = grand_total = base_total
|
||||
elif voucher_type == "Purchase Invoice":
|
||||
total_amount, grand_total, base_total, bill_no, bill_date = net_total_map.get(
|
||||
(voucher_type, name)
|
||||
)
|
||||
|
||||
else:
|
||||
total_amount, grand_total, base_total = net_total_map.get((voucher_type, name))
|
||||
if tax_amount and rate:
|
||||
# back calculate total amount from rate and tax_amount
|
||||
total_amount = flt((tax_amount * 100) / rate, precision=precision)
|
||||
else:
|
||||
total_amount = values[0]
|
||||
|
||||
grand_total = values[1]
|
||||
base_total = values[2]
|
||||
|
||||
if voucher_type == "Purchase Invoice":
|
||||
bill_no = values[3]
|
||||
bill_date = values[4]
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
|
||||
|
||||
@@ -47,22 +47,23 @@ frappe.query_reports["Trial Balance"] = {
|
||||
{
|
||||
fieldname: "cost_center",
|
||||
label: __("Cost Center"),
|
||||
fieldtype: "Link",
|
||||
options: "Cost Center",
|
||||
get_query: function () {
|
||||
var company = frappe.query_report.get_filter_value("company");
|
||||
return {
|
||||
doctype: "Cost Center",
|
||||
filters: {
|
||||
company: company,
|
||||
},
|
||||
};
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Cost Center", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Cost Center",
|
||||
},
|
||||
{
|
||||
fieldname: "project",
|
||||
label: __("Project"),
|
||||
fieldtype: "Link",
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
return frappe.db.get_link_options("Project", txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
});
|
||||
},
|
||||
options: "Project",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
filter_accounts,
|
||||
filter_out_zero_value_rows,
|
||||
get_cost_centers_with_children,
|
||||
set_gl_entries_by_account,
|
||||
)
|
||||
from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency
|
||||
@@ -103,10 +104,6 @@ def get_data(filters):
|
||||
|
||||
opening_balances = get_opening_balances(filters, ignore_is_opening)
|
||||
|
||||
# add filter inside list so that the query in financial_statements.py doesn't break
|
||||
if filters.project:
|
||||
filters.project = [filters.project]
|
||||
|
||||
set_gl_entries_by_account(
|
||||
filters.company,
|
||||
filters.from_date,
|
||||
@@ -270,18 +267,12 @@ def get_opening_balance(
|
||||
opening_balance = opening_balance.where(closing_balance.voucher_type != "Period Closing Voucher")
|
||||
|
||||
if filters.cost_center:
|
||||
lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"])
|
||||
cost_center = frappe.qb.DocType("Cost Center")
|
||||
opening_balance = opening_balance.where(
|
||||
closing_balance.cost_center.isin(
|
||||
frappe.qb.from_(cost_center)
|
||||
.select("name")
|
||||
.where((cost_center.lft >= lft) & (cost_center.rgt <= rgt))
|
||||
)
|
||||
closing_balance.cost_center.isin(get_cost_centers_with_children(filters.get("cost_center")))
|
||||
)
|
||||
|
||||
if filters.project:
|
||||
opening_balance = opening_balance.where(closing_balance.project == filters.project)
|
||||
opening_balance = opening_balance.where(closing_balance.project.isin(filters.project))
|
||||
|
||||
if frappe.db.count("Finance Book"):
|
||||
if filters.get("include_default_book_entries"):
|
||||
@@ -408,7 +399,7 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
}
|
||||
|
||||
for key in value_fields:
|
||||
row[key] = flt(d.get(key, 0.0), 3)
|
||||
row[key] = flt(d.get(key, 0.0))
|
||||
|
||||
if abs(row[key]) >= get_zero_cutoff(company_currency):
|
||||
# ignore zero values
|
||||
|
||||
@@ -1755,24 +1755,22 @@ def check_and_delete_linked_reports(report):
|
||||
frappe.delete_doc("Desktop Icon", icon)
|
||||
|
||||
|
||||
def create_err_and_its_journals(companies: list | None = None) -> None:
|
||||
if companies:
|
||||
for company in companies:
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = company.name
|
||||
err.posting_date = nowdate()
|
||||
err.rounding_loss_allowance = 0.0
|
||||
def create_err_and_its_journals(company: dict) -> None:
|
||||
err = frappe.new_doc("Exchange Rate Revaluation")
|
||||
err.company = company.name
|
||||
err.posting_date = nowdate()
|
||||
err.rounding_loss_allowance = 0.0
|
||||
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
if err.accounts:
|
||||
err.save().submit()
|
||||
response = err.make_jv_entries()
|
||||
err.fetch_and_calculate_accounts_data()
|
||||
if err.accounts:
|
||||
err.save().submit()
|
||||
response = err.make_jv_entries()
|
||||
|
||||
if company.submit_err_jv:
|
||||
jv = response.get("revaluation_jv", None)
|
||||
jv and frappe.get_doc("Journal Entry", jv).submit()
|
||||
jv = response.get("zero_balance_jv", None)
|
||||
jv and frappe.get_doc("Journal Entry", jv).submit()
|
||||
if company.submit_err_jv:
|
||||
jv = response.get("revaluation_jv", None)
|
||||
jv and frappe.get_doc("Journal Entry", jv).submit()
|
||||
jv = response.get("zero_balance_jv", None)
|
||||
jv and frappe.get_doc("Journal Entry", jv).submit()
|
||||
|
||||
|
||||
def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None:
|
||||
@@ -1785,7 +1783,14 @@ def _auto_create_exchange_rate_revaluation_for(frequency: str) -> None:
|
||||
filters={"auto_exchange_rate_revaluation": 1, "auto_err_frequency": frequency},
|
||||
fields=["name", "submit_err_jv"],
|
||||
)
|
||||
create_err_and_its_journals(companies)
|
||||
|
||||
if companies:
|
||||
for company in companies:
|
||||
frappe.enqueue(
|
||||
"erpnext.accounts.utils.create_err_and_its_journals",
|
||||
company=company,
|
||||
queue="long",
|
||||
)
|
||||
|
||||
|
||||
def auto_create_exchange_rate_revaluation_daily() -> None:
|
||||
|
||||
@@ -202,7 +202,7 @@ frappe.ui.form.on("Asset", {
|
||||
callback: function (r) {
|
||||
if (!r.message) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
$(".form-message").text(__("Capitalize this asset to confirm"));
|
||||
|
||||
frm.add_custom_button(__("Capitalize Asset"), function () {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
|
||||
@@ -371,7 +371,6 @@
|
||||
"label": "Other Details"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Draft",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
@@ -379,7 +378,7 @@
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
|
||||
"options": "Draft\nSubmitted\nCancelled\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt\nCapitalized\nWork In Progress",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -597,7 +596,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-10-23 22:43:33.634452",
|
||||
"modified": "2025-11-17 18:01:51.417942",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -103,6 +103,7 @@ class Asset(AccountsController):
|
||||
status: DF.Literal[
|
||||
"Draft",
|
||||
"Submitted",
|
||||
"Cancelled",
|
||||
"Partially Depreciated",
|
||||
"Fully Depreciated",
|
||||
"Sold",
|
||||
@@ -458,6 +459,7 @@ class Asset(AccountsController):
|
||||
"asset_name": self.asset_name,
|
||||
"target_location": self.location,
|
||||
"to_employee": self.custodian,
|
||||
"company": self.company,
|
||||
}
|
||||
]
|
||||
asset_movement = frappe.get_doc(
|
||||
@@ -1204,7 +1206,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
|
||||
"purchase_date": purchase_doc.get("posting_date"),
|
||||
"gross_purchase_amount": flt(first_item.base_net_amount),
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
|
||||
@@ -537,6 +537,7 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||
asset_repair.asset_doc = asset
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@ class AssetCapitalization(StockController):
|
||||
self.make_gl_entries()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.restore_consumed_asset_items()
|
||||
self.update_target_asset()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
||||
@@ -607,8 +608,12 @@ class AssetCapitalization(StockController):
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.gross_purchase_amount += total_target_asset_value
|
||||
asset_doc.purchase_amount += total_target_asset_value
|
||||
if self.docstatus == 2:
|
||||
asset_doc.gross_purchase_amount -= total_target_asset_value
|
||||
asset_doc.purchase_amount -= total_target_asset_value
|
||||
else:
|
||||
asset_doc.gross_purchase_amount += total_target_asset_value
|
||||
asset_doc.purchase_amount += total_target_asset_value
|
||||
asset_doc.set_status("Work In Progress")
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.save()
|
||||
|
||||
@@ -63,14 +63,7 @@ frappe.ui.form.on("Asset Repair", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus) {
|
||||
frm.add_custom_button(__("View General Ledger"), function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
});
|
||||
}
|
||||
frm.events.show_general_ledger(frm);
|
||||
|
||||
let sbb_field = frm.get_docfield("stock_items", "serial_and_batch_bundle");
|
||||
if (sbb_field) {
|
||||
@@ -134,6 +127,26 @@ frappe.ui.form.on("Asset Repair", {
|
||||
frm.set_value("repair_cost", 0);
|
||||
}
|
||||
},
|
||||
|
||||
show_general_ledger: (frm) => {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
__("Accounting Ledger"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||
|
||||
@@ -139,7 +139,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",null]]]",
|
||||
"link_filters": "[[\"Asset\",\"status\",\"not in\",[\"Work In Progress\",\"Capitalized\",\"Fully Depreciated\",\"Sold\",\"Scrapped\",\"Cancelled\",null]]]",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -250,7 +250,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-29 15:14:34.044564",
|
||||
"modified": "2025-11-17 18:35:54.575265",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
|
||||
@@ -6,6 +6,9 @@ from frappe import _
|
||||
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
@@ -60,6 +63,17 @@ class AssetRepair(AccountsController):
|
||||
if self.get("stock_items"):
|
||||
self.set_stock_items_cost()
|
||||
self.calculate_total_repair_cost()
|
||||
self.validate_purchase_invoice_status()
|
||||
|
||||
def validate_purchase_invoice_status(self):
|
||||
if self.purchase_invoice:
|
||||
docstatus = frappe.db.get_value("Purchase Invoice", self.purchase_invoice, "docstatus")
|
||||
if docstatus == 0:
|
||||
frappe.throw(
|
||||
_("{0} is still in Draft. Please submit it before saving the Asset Repair.").format(
|
||||
get_link_to_form("Purchase Invoice", self.purchase_invoice)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_asset(self):
|
||||
if self.asset_doc.status in ("Sold", "Fully Depreciated", "Scrapped"):
|
||||
@@ -235,6 +249,12 @@ class AssetRepair(AccountsController):
|
||||
)
|
||||
stock_entry.asset_repair = self.name
|
||||
|
||||
accounting_dimensions = {
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
**{dimension: self.get(dimension) for dimension in get_accounting_dimensions()},
|
||||
}
|
||||
|
||||
for stock_item in self.get("stock_items"):
|
||||
self.validate_serial_no(stock_item)
|
||||
|
||||
@@ -246,8 +266,7 @@ class AssetRepair(AccountsController):
|
||||
"qty": stock_item.consumed_quantity,
|
||||
"basic_rate": stock_item.valuation_rate,
|
||||
"serial_and_batch_bundle": stock_item.serial_and_batch_bundle,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
**accounting_dimensions,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -304,8 +323,8 @@ class AssetRepair(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Purchase Invoice",
|
||||
"against_voucher": self.purchase_invoice,
|
||||
"against_voucher_type": "Asset",
|
||||
"against_voucher": self.asset,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
@@ -294,6 +296,31 @@ class TestAssetRepair(unittest.TestCase):
|
||||
stock_entry = frappe.get_last_doc("Stock Entry")
|
||||
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
|
||||
|
||||
def test_gl_entries_with_capitalized_asset_repair(self):
|
||||
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
|
||||
asset_repair = create_asset_repair(
|
||||
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
|
||||
)
|
||||
asset.reload()
|
||||
|
||||
GLEntry = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(GLEntry)
|
||||
.select(Sum(GLEntry.debit_in_account_currency).as_("total_debit"))
|
||||
.where(
|
||||
(GLEntry.voucher_type == "Asset Repair")
|
||||
& (GLEntry.voucher_no == asset_repair.name)
|
||||
& (GLEntry.against_voucher_type == "Asset")
|
||||
& (GLEntry.against_voucher == asset.name)
|
||||
& (GLEntry.company == asset.company)
|
||||
& (GLEntry.is_cancelled == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
booked_value = res[0].total_debit if res else 0
|
||||
|
||||
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
|
||||
self.assertEqual(booked_value, asset_repair.repair_cost)
|
||||
|
||||
|
||||
def num_of_depreciations(asset):
|
||||
return asset.finance_books[0].total_number_of_depreciations
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
"over_transfer_allowance",
|
||||
"validate_consumed_qty",
|
||||
"section_break_xcug",
|
||||
"auto_create_subcontracting_order",
|
||||
"column_break_izrr",
|
||||
@@ -270,6 +271,14 @@
|
||||
"label": "Fixed Outgoing Email Account",
|
||||
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
|
||||
"options": "Email Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
|
||||
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
|
||||
"fieldname": "validate_consumed_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Consumed Qty (as per BOM)"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -278,7 +287,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 22:13:38.506889",
|
||||
"modified": "2025-11-20 12:59:09.925862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -44,6 +44,7 @@ class BuyingSettings(Document):
|
||||
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
|
||||
supplier_group: DF.Link | None
|
||||
use_transaction_date_exchange_rate: DF.Check
|
||||
validate_consumed_qty: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -303,6 +303,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
erpnext.buying.BuyingController
|
||||
) {
|
||||
setup() {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.frm.custom_make_buttons = {
|
||||
"Purchase Receipt": "Purchase Receipt",
|
||||
"Purchase Invoice": "Purchase Invoice",
|
||||
|
||||
@@ -76,6 +76,46 @@ class TestRequestforQuotation(FrappeTestCase):
|
||||
self.assertEqual(sq1.get("items")[0].item_code, "_Test Item")
|
||||
self.assertEqual(sq1.get("items")[0].qty, 5)
|
||||
|
||||
def test_make_supplier_quotation_with_taxes(self):
|
||||
"""Test automatic tax addition when supplier quotation is created from RFQ taxes_and_charges are set"""
|
||||
|
||||
# Create a Purchase Taxes and Charges Template for testing
|
||||
tax_template = frappe.new_doc("Purchase Taxes and Charges Template")
|
||||
tax_template.doctype = "Purchase Taxes and Charges Template"
|
||||
tax_template.title = "_Test Purchase Taxes Template for RFQ"
|
||||
tax_template.company = "_Test Company"
|
||||
tax_template.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"description": "VAT",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
tax_template.save()
|
||||
|
||||
rfq = make_request_for_quotation()
|
||||
supplier = rfq.get("suppliers")[0].supplier
|
||||
|
||||
tax_rule = frappe.new_doc("Tax Rule")
|
||||
tax_rule.company = "_Test Company"
|
||||
tax_rule.tax_type = "Purchase"
|
||||
tax_rule.supplier = supplier
|
||||
tax_rule.purchase_tax_template = tax_template.name
|
||||
tax_rule.save()
|
||||
|
||||
sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier)
|
||||
|
||||
# Verify that taxes_and_charges is set from get_party_details
|
||||
self.assertEqual(sq.taxes_and_charges, tax_template.name)
|
||||
|
||||
# Verify that taxes are automatically added
|
||||
self.assertGreaterEqual(len(sq.get("taxes")), 1)
|
||||
|
||||
tax_rule.delete()
|
||||
tax_template.delete()
|
||||
|
||||
def test_make_supplier_quotation_with_special_characters(self):
|
||||
frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1)
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
|
||||
@@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", {
|
||||
|
||||
frm.set_query("supplier_primary_contact", function (doc) {
|
||||
return {
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact",
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
|
||||
filters: {
|
||||
supplier: doc.name,
|
||||
type: "Contact",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("supplier_primary_address", function (doc) {
|
||||
return {
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
|
||||
filters: {
|
||||
link_doctype: "Supplier",
|
||||
link_name: doc.name,
|
||||
supplier: doc.name,
|
||||
type: "Address",
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -137,6 +139,14 @@ frappe.ui.form.on("Supplier", {
|
||||
// indicators
|
||||
erpnext.utils.set_party_dashboard_indicators(frm);
|
||||
}
|
||||
|
||||
frm.set_query("supplier_group", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
get_supplier_group_details: function (frm) {
|
||||
frappe.call({
|
||||
|
||||
@@ -215,19 +215,25 @@ class Supplier(TransactionBase):
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
|
||||
supplier = filters.get("supplier")
|
||||
contact = frappe.qb.DocType("Contact")
|
||||
type = filters.get("type")
|
||||
type_doctype = frappe.qb.DocType(type)
|
||||
dynamic_link = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(contact)
|
||||
query = (
|
||||
frappe.qb.from_(type_doctype)
|
||||
.join(dynamic_link)
|
||||
.on(contact.name == dynamic_link.parent)
|
||||
.select(contact.name, contact.email_id)
|
||||
.on(type_doctype.name == dynamic_link.parent)
|
||||
.select(type_doctype.name)
|
||||
.where(
|
||||
(dynamic_link.link_name == supplier)
|
||||
& (dynamic_link.link_doctype == "Supplier")
|
||||
& (contact.name.like(f"%{txt}%"))
|
||||
& (type_doctype.name.like(f"%{txt}%"))
|
||||
)
|
||||
).run(as_dict=False)
|
||||
)
|
||||
|
||||
if type == "Contact":
|
||||
query = query.select(type_doctype.email_id)
|
||||
|
||||
return query.run()
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-06-13 18:45:01",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-02-21 01:28:37.416562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Trends",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Order Trends",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"add_translate_data": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-06-13 18:45:01",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.058154",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Trends",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Purchase Order Trends",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"timeout": 0
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import (
|
||||
PURCHASE_TRANSACTION_TYPES,
|
||||
SALES_TRANSACTION_TYPES,
|
||||
get_party_account,
|
||||
get_party_account_currency,
|
||||
get_party_gle_currency,
|
||||
@@ -306,6 +308,52 @@ class AccountsController(TransactionBase):
|
||||
self.set_default_letter_head()
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_party_address_and_contact()
|
||||
self.validate_company_linked_addresses()
|
||||
|
||||
def validate_company_linked_addresses(self):
|
||||
address_fields = []
|
||||
sales_doctypes = ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice")
|
||||
purchase_doctypes = ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation")
|
||||
|
||||
if self.doctype in sales_doctypes:
|
||||
address_fields = ["dispatch_address_name", "company_address"]
|
||||
elif self.doctype in purchase_doctypes:
|
||||
address_fields = ["billing_address", "shipping_address"]
|
||||
|
||||
if not address_fields:
|
||||
return
|
||||
|
||||
# Determine if drop ship applies
|
||||
is_drop_ship = self.doctype in {
|
||||
"Purchase Order",
|
||||
"Sales Order",
|
||||
"Sales Invoice",
|
||||
} and self.is_drop_ship(self.items)
|
||||
|
||||
for field in address_fields:
|
||||
address = self.get(field)
|
||||
|
||||
if (field in ["dispatch_address_name", "shipping_address"]) and is_drop_ship:
|
||||
continue
|
||||
|
||||
if address and not frappe.db.exists(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"parent": address,
|
||||
"parenttype": "Address",
|
||||
"link_doctype": "Company",
|
||||
"link_name": self.company,
|
||||
},
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} does not belong to the Company {1}.").format(
|
||||
_(self.meta.get_label(field)), bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def is_drop_ship(items):
|
||||
return any(item.delivered_by_supplier for item in items)
|
||||
|
||||
def set_default_letter_head(self):
|
||||
if hasattr(self, "letter_head") and not self.letter_head:
|
||||
@@ -360,6 +408,24 @@ class AccountsController(TransactionBase):
|
||||
|
||||
for _doctype in repost_doctypes:
|
||||
dt = frappe.qb.DocType(_doctype)
|
||||
|
||||
cancelled_entries = (
|
||||
frappe.qb.from_(dt)
|
||||
.select(dt.parent, dt.parenttype)
|
||||
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name) & (dt.docstatus == 2))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if cancelled_entries:
|
||||
entries = "<br>".join([get_link_to_form(d.parenttype, d.parent) for d in cancelled_entries])
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"The following cancelled repost entries exist for <b>{0}</b>:<br><br>{1}<br><br>"
|
||||
"Kindly delete these entries before continuing."
|
||||
).format(self.name, entries)
|
||||
)
|
||||
|
||||
rows = (
|
||||
frappe.qb.from_(dt)
|
||||
.select(dt.name, dt.parent, dt.parenttype)
|
||||
@@ -2918,6 +2984,104 @@ class AccountsController(TransactionBase):
|
||||
x["transaction_currency"] = self.currency
|
||||
x["transaction_exchange_rate"] = self.get("conversion_rate") or 1
|
||||
|
||||
def after_mapping(self, source_doc):
|
||||
self.set_discount_amount_after_mapping(source_doc)
|
||||
|
||||
def set_discount_amount_after_mapping(self, source_doc):
|
||||
"""
|
||||
Ensures that Additional Discount Amount is not copied repeatedly
|
||||
for multiple mappings of a single source transaction.
|
||||
"""
|
||||
|
||||
# source and target doctypes should both be buying / selling
|
||||
for transaction_types in (PURCHASE_TRANSACTION_TYPES, SALES_TRANSACTION_TYPES):
|
||||
if self.doctype in transaction_types and source_doc.doctype in transaction_types:
|
||||
break
|
||||
|
||||
else:
|
||||
return
|
||||
|
||||
# ensure both doctypes have discount_amount field
|
||||
if not self.meta.get_field("discount_amount") or not source_doc.meta.get_field("discount_amount"):
|
||||
return
|
||||
|
||||
# ensure discount_amount is set in source doc
|
||||
if not source_doc.discount_amount:
|
||||
return
|
||||
|
||||
# ensure additional_discount_percentage is not set in the source doc
|
||||
if source_doc.get("additional_discount_percentage"):
|
||||
return
|
||||
|
||||
item_doctype = self.meta.get_field("items").options
|
||||
doctype_table = frappe.qb.DocType(self.doctype)
|
||||
item_table = frappe.qb.DocType(item_doctype)
|
||||
|
||||
is_same_doctype = self.doctype == source_doc.doctype
|
||||
is_return = self.get("is_return") and is_same_doctype
|
||||
|
||||
if is_same_doctype and not is_return:
|
||||
# should never happen
|
||||
# you don't map to the same doctype without it being a return
|
||||
return
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(doctype_table)
|
||||
.where(doctype_table.docstatus == 1)
|
||||
.where(doctype_table.discount_amount != 0)
|
||||
.select(Sum(doctype_table.discount_amount))
|
||||
)
|
||||
|
||||
if is_return:
|
||||
query = query.where(doctype_table.is_return == 1).where(
|
||||
doctype_table.return_against == source_doc.name
|
||||
)
|
||||
|
||||
else:
|
||||
item_meta = frappe.get_meta(item_doctype)
|
||||
reference_fieldname = next(
|
||||
(
|
||||
row.fieldname
|
||||
for row in item_meta.fields
|
||||
if row.fieldtype == "Link"
|
||||
and row.options == source_doc.doctype
|
||||
and not row.get("is_custom_field")
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not reference_fieldname:
|
||||
return
|
||||
|
||||
query = query.where(
|
||||
doctype_table.name.isin(
|
||||
frappe.qb.from_(item_table)
|
||||
.select(item_table.parent)
|
||||
.where(item_table[reference_fieldname] == source_doc.name)
|
||||
.distinct()
|
||||
)
|
||||
)
|
||||
|
||||
result = query.run()
|
||||
if not result:
|
||||
return
|
||||
|
||||
discount_already_applied = result[0][0]
|
||||
if not discount_already_applied:
|
||||
return
|
||||
|
||||
if is_return:
|
||||
# returns have negative discount
|
||||
discount_already_applied *= -1
|
||||
|
||||
discount_amount = max(source_doc.discount_amount - discount_already_applied, 0)
|
||||
if discount_amount and is_return:
|
||||
discount_amount *= -1
|
||||
|
||||
self.discount_amount = flt(discount_amount, self.precision("discount_amount"))
|
||||
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
|
||||
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
@@ -180,6 +181,12 @@ class BuyingController(SubcontractingController):
|
||||
|
||||
self.set_missing_item_details(for_validate)
|
||||
|
||||
if self.meta.get_field("taxes"):
|
||||
if self.get("taxes_and_charges") and not self.get("taxes") and not for_validate:
|
||||
taxes = get_taxes_and_charges("Purchase Taxes and Charges Template", self.taxes_and_charges)
|
||||
for tax in taxes:
|
||||
self.append("taxes", tax)
|
||||
|
||||
def set_supplier_from_item_default(self):
|
||||
if self.meta.get_field("supplier") and not self.supplier:
|
||||
for d in self.get("items"):
|
||||
|
||||
@@ -185,7 +185,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
frappe.get_meta(doc.doctype + " Item").get_field(
|
||||
"stock_qty" if doc.get("update_stock", "") else "qty"
|
||||
),
|
||||
company_currency,
|
||||
currency=company_currency,
|
||||
)
|
||||
|
||||
for column in fields:
|
||||
|
||||
@@ -95,6 +95,7 @@ class SellingController(StockController):
|
||||
# set contact and address details for customer, if they are not mentioned
|
||||
self.set_missing_lead_customer_details(for_validate=for_validate)
|
||||
self.set_price_list_and_item_details(for_validate=for_validate)
|
||||
self.set_company_contact_person()
|
||||
|
||||
def set_missing_lead_customer_details(self, for_validate=False):
|
||||
customer, lead = None, None
|
||||
@@ -137,6 +138,7 @@ class SellingController(StockController):
|
||||
lead,
|
||||
posting_date=self.get("transaction_date") or self.get("posting_date"),
|
||||
company=self.company,
|
||||
doctype=self.doctype,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -149,6 +151,13 @@ class SellingController(StockController):
|
||||
self.set_price_list_currency("Selling")
|
||||
self.set_missing_item_details(for_validate=for_validate)
|
||||
|
||||
def set_company_contact_person(self):
|
||||
"""Set the Company's Default Sales Contact as Company Contact Person."""
|
||||
if self.company and self.meta.has_field("company_contact_person") and not self.company_contact_person:
|
||||
self.company_contact_person = frappe.get_cached_value(
|
||||
"Company", self.company, "default_sales_contact"
|
||||
)
|
||||
|
||||
def remove_shipping_charge(self):
|
||||
if self.shipping_rule:
|
||||
shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)
|
||||
|
||||
@@ -86,6 +86,7 @@ status_map = {
|
||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
@@ -93,6 +94,7 @@ status_map = {
|
||||
["Draft", None],
|
||||
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
|
||||
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
|
||||
["Return", "eval:self.is_return == 1 and self.per_billed == 0 and self.docstatus == 1"],
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
[
|
||||
"Completed",
|
||||
|
||||
@@ -11,6 +11,7 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
get_auto_batch_nos,
|
||||
get_available_serial_nos,
|
||||
@@ -505,7 +506,7 @@ class SubcontractingController(StockController):
|
||||
if item.get("serial_and_batch_bundle"):
|
||||
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
|
||||
|
||||
@@ -686,7 +687,11 @@ class SubcontractingController(StockController):
|
||||
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
|
||||
row.serial_no = "\n".join(serial_nos)
|
||||
|
||||
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
|
||||
elif (
|
||||
item_details.has_batch_no
|
||||
and not row.serial_and_batch_bundle
|
||||
and (not row.batch_no or self.batch_has_not_available(row.batch_no, row.consumed_qty))
|
||||
):
|
||||
batches = get_auto_batch_nos(kwargs)
|
||||
if batches:
|
||||
consumed_qty = row.consumed_qty
|
||||
@@ -711,6 +716,11 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
consumed_qty -= d.get("qty")
|
||||
|
||||
def batch_has_not_available(self, batch_no, qty_required):
|
||||
batch_qty = get_batch_qty(batch_no, self.supplier_warehouse, consider_negative_batches=True)
|
||||
|
||||
return batch_qty < qty_required
|
||||
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
return
|
||||
@@ -849,7 +859,7 @@ class SubcontractingController(StockController):
|
||||
if self.doctype == self.subcontract_data.order_doctype or (
|
||||
self.backflush_based_on == "BOM" or self.is_return
|
||||
):
|
||||
for bom_item in self.__get_materials_from_bom(
|
||||
for bom_item in self._get_materials_from_bom(
|
||||
row.item_code, row.bom, row.get("include_exploded_items")
|
||||
):
|
||||
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import functions
|
||||
from frappe.utils import cint, flt, round_based_on_smallest_currency_fraction
|
||||
from frappe.utils.deprecations import deprecated
|
||||
|
||||
@@ -377,6 +378,9 @@ class calculate_taxes_and_totals:
|
||||
self._calculate()
|
||||
|
||||
def calculate_taxes(self):
|
||||
# reset value from earlier calculations
|
||||
self.grand_total_diff = 0
|
||||
|
||||
doc = self.doc
|
||||
if not doc.get("taxes"):
|
||||
return
|
||||
@@ -586,7 +590,7 @@ class calculate_taxes_and_totals:
|
||||
self.grand_total_diff = 0
|
||||
|
||||
def calculate_totals(self):
|
||||
grand_total_diff = getattr(self, "grand_total_diff", 0)
|
||||
grand_total_diff = self.grand_total_diff
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + grand_total_diff
|
||||
@@ -682,6 +686,41 @@ class calculate_taxes_and_totals:
|
||||
self.doc.precision("discount_amount"),
|
||||
)
|
||||
|
||||
discount_amount = self.doc.discount_amount or 0
|
||||
grand_total = self.doc.grand_total
|
||||
|
||||
if self.doc.get("is_return") and self.doc.get("return_against"):
|
||||
doctype = frappe.qb.DocType(self.doc.doctype)
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(doctype)
|
||||
.select(functions.Sum(doctype.discount_amount).as_("total_return_discount"))
|
||||
.where(
|
||||
(doctype.return_against == self.doc.return_against)
|
||||
& (doctype.is_return == 1)
|
||||
& (doctype.docstatus == 1)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
total_return_discount = abs(result[0].get("total_return_discount") or 0)
|
||||
discount_amount += total_return_discount
|
||||
|
||||
# validate that discount amount cannot exceed the total before discount
|
||||
if (
|
||||
(grand_total >= 0 and discount_amount > grand_total)
|
||||
or (grand_total < 0 and discount_amount < grand_total) # returns
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Additional Discount Amount ({discount_amount}) cannot exceed "
|
||||
"the total before such discount ({total_before_discount})"
|
||||
).format(
|
||||
discount_amount=self.doc.get_formatted("discount_amount"),
|
||||
total_before_discount=self.doc.get_formatted("grand_total"),
|
||||
),
|
||||
title=_("Invalid Discount Amount"),
|
||||
)
|
||||
|
||||
def apply_discount_amount(self):
|
||||
if self.doc.discount_amount:
|
||||
if not self.doc.apply_discount_on:
|
||||
@@ -814,12 +853,11 @@ class calculate_taxes_and_totals:
|
||||
)
|
||||
)
|
||||
|
||||
if self.doc.docstatus.is_draft():
|
||||
if self.doc.get("write_off_outstanding_amount_automatically"):
|
||||
self.doc.write_off_amount = 0
|
||||
if self.doc.get("write_off_outstanding_amount_automatically"):
|
||||
self.doc.write_off_amount = 0
|
||||
|
||||
self.calculate_outstanding_amount()
|
||||
self.calculate_write_off_amount()
|
||||
self.calculate_outstanding_amount()
|
||||
self.calculate_write_off_amount()
|
||||
|
||||
def is_internal_invoice(self):
|
||||
"""
|
||||
|
||||
@@ -16,7 +16,10 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
prepare_data_for_internal_transfer,
|
||||
)
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
@@ -2258,3 +2261,218 @@ class TestAccountsController(FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
si.contact_person = customer_contact.name
|
||||
si.save()
|
||||
|
||||
def test_discount_amount_not_mapped_repeatedly_for_sales_transactions(self):
|
||||
"""
|
||||
Test that additional discount amount is not copied repeatedly
|
||||
when creating multiple delivery notes from a single sales order with discount_amount set
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount amount
|
||||
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
|
||||
so.apply_discount_on = "Net Total"
|
||||
so.discount_amount = 100
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
# Create first delivery note from sales order (partial qty)
|
||||
dn1 = make_delivery_note(so.name)
|
||||
dn1.items[0].qty = 5
|
||||
dn1.save()
|
||||
dn1.submit()
|
||||
|
||||
# First delivery note should have full discount amount
|
||||
self.assertEqual(dn1.discount_amount, 100)
|
||||
self.assertEqual(dn1.grand_total, 400)
|
||||
|
||||
# Create second delivery note from the same sales order (remaining qty)
|
||||
dn2 = make_delivery_note(so.name)
|
||||
dn2.items[0].qty = 5
|
||||
dn2.save()
|
||||
dn2.submit()
|
||||
|
||||
# Second delivery note should have discount_amount set to 0
|
||||
# because discount was already fully applied in first delivery note
|
||||
self.assertEqual(dn2.discount_amount, 0)
|
||||
self.assertEqual(dn2.grand_total, 500)
|
||||
|
||||
def test_discount_amount_not_mapped_repeatedly_for_purchase_transactions(self):
|
||||
"""
|
||||
Test that additional discount amount is not copied repeatedly
|
||||
when creating multiple purchase receipts from a single purchase order with discount_amount set
|
||||
"""
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
|
||||
# Create a purchase order with discount amount
|
||||
po = create_purchase_order(qty=10, rate=100, do_not_submit=True)
|
||||
po.apply_discount_on = "Net Total"
|
||||
po.discount_amount = 100
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
# Create first purchase receipt from purchase order (partial qty)
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr1.items[0].qty = 5
|
||||
pr1.save()
|
||||
pr1.submit()
|
||||
|
||||
# First purchase receipt should have full discount amount
|
||||
self.assertEqual(pr1.discount_amount, 100)
|
||||
self.assertEqual(pr1.grand_total, 400)
|
||||
|
||||
# Create second purchase receipt from the same purchase order (remaining qty)
|
||||
pr2 = make_purchase_receipt(po.name)
|
||||
pr2.items[0].qty = 5
|
||||
pr2.save()
|
||||
pr2.submit()
|
||||
|
||||
# Second purchase receipt should have discount_amount set to 0
|
||||
# because discount was already fully applied in first purchase receipt
|
||||
self.assertEqual(pr2.discount_amount, 0)
|
||||
self.assertEqual(pr2.grand_total, 500)
|
||||
|
||||
def test_discount_amount_partial_application_in_mapped_transactions(self):
|
||||
"""
|
||||
Test that discount amount is partially applied when some discount
|
||||
has already been used in previous mapped transactions
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount amount
|
||||
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
|
||||
so.apply_discount_on = "Net Total"
|
||||
so.discount_amount = 200
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
self.assertEqual(so.discount_amount, 200)
|
||||
self.assertEqual(so.grand_total, 800)
|
||||
|
||||
# Create first invoice with partial discount (manually set lower discount)
|
||||
si1 = make_sales_invoice(so.name)
|
||||
si1.items[0].qty = 5
|
||||
si1.discount_amount = 50 # Partial discount application
|
||||
si1.save()
|
||||
si1.submit()
|
||||
|
||||
self.assertEqual(si1.discount_amount, 50)
|
||||
self.assertEqual(si1.grand_total, 450)
|
||||
|
||||
# Create second invoice from the same sales order
|
||||
si2 = make_sales_invoice(so.name)
|
||||
si2.items[0].qty = 5
|
||||
si2.save()
|
||||
si2.submit()
|
||||
|
||||
# Second invoice should have remaining discount (200 - 50 = 150)
|
||||
self.assertEqual(si2.discount_amount, 150)
|
||||
self.assertEqual(si2.grand_total, 350)
|
||||
|
||||
def test_discount_amount_not_mapped_when_percentage_is_set(self):
|
||||
"""
|
||||
Test that discount amount is not adjusted when additional_discount_percentage
|
||||
is set in the source document (as it will be recalculated based on percentage)
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
# Create a sales order with discount percentage instead of amount
|
||||
so = make_sales_order(qty=10, rate=100, do_not_submit=True)
|
||||
so.apply_discount_on = "Net Total"
|
||||
so.additional_discount_percentage = 10 # 10% discount
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
self.assertEqual(so.discount_amount, 100) # 10% of 1000
|
||||
self.assertEqual(so.grand_total, 900)
|
||||
|
||||
# Create delivery note from sales order
|
||||
dn = make_delivery_note(so.name)
|
||||
dn.items[0].qty = 5
|
||||
dn.save()
|
||||
|
||||
# Delivery note should have discount amount recalculated based on percentage
|
||||
# and not affected by the repeated mapping logic
|
||||
self.assertEqual(dn.additional_discount_percentage, 10)
|
||||
self.assertEqual(dn.discount_amount, 50) # 10% of 500
|
||||
|
||||
def test_discount_amount_for_multiple_returns(self):
|
||||
"""
|
||||
Test that discount amount is correctly adjusted when multiple return invoices
|
||||
are created against the same original invoice to prevent over-returning discount
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
# Create original sales invoice with discount
|
||||
si = create_sales_invoice(qty=10, rate=100, do_not_submit=True)
|
||||
si.apply_discount_on = "Net Total"
|
||||
si.discount_amount = 100
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# Create first return - Frappe will copy full discount by default, we need to adjust it
|
||||
return_si_1 = make_sales_return(si.name)
|
||||
return_si_1.items[0].qty = -6 # Return 6 out of 10 items
|
||||
# Manually set discount to match the proportion (60% of discount)
|
||||
return_si_1.discount_amount = -60
|
||||
return_si_1.save()
|
||||
return_si_1.submit()
|
||||
|
||||
self.assertEqual(return_si_1.discount_amount, -60)
|
||||
|
||||
# Create second return for remaining items
|
||||
return_si_2 = make_sales_return(si.name)
|
||||
return_si_2.items[0].qty = -4 # Return remaining 4 out of 10 items
|
||||
return_si_2.save()
|
||||
|
||||
# Second return should only get remaining discount (100 - 60 = 40)
|
||||
self.assertEqual(return_si_2.discount_amount, -40)
|
||||
|
||||
def test_company_linked_address(self):
|
||||
from erpnext.crm.doctype.prospect.test_prospect import make_address
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
company_address = make_address(
|
||||
address_title="Company", address_type="Shipping", address_line1="100", city="Mumbai"
|
||||
)
|
||||
company_address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
|
||||
company_address.save()
|
||||
|
||||
customer_shipping = make_address(
|
||||
address_title="Customer", address_type="Shipping", address_line1="10"
|
||||
)
|
||||
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
|
||||
customer_shipping.save()
|
||||
|
||||
supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad")
|
||||
supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
|
||||
supplier_billing.save()
|
||||
|
||||
po = create_purchase_order(do_not_save=True)
|
||||
po.shipping_address = customer_shipping.name
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
po.shipping_address = company_address.name
|
||||
po.save()
|
||||
|
||||
po.billing_address = supplier_billing.name
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
po.billing_address = company_address.name
|
||||
po.reload()
|
||||
po.save()
|
||||
|
||||
si = make_sales_order(do_not_save=1, do_not_submit=1)
|
||||
si.dispatch_address_name = supplier_billing.name
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
si.items[0].delivered_by_supplier = 1
|
||||
si.items[0].supplier = "_Test Supplier"
|
||||
si.save()
|
||||
|
||||
po = create_purchase_order(do_not_save=True)
|
||||
po.shipping_address = customer_shipping.name
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
po.items[0].delivered_by_supplier = 1
|
||||
po.save()
|
||||
|
||||
@@ -191,6 +191,9 @@ def get_data(filters, conditions):
|
||||
des[j + inc] = row1[0][j]
|
||||
|
||||
data.append(des)
|
||||
|
||||
total_row = calculate_total_row(data1, conditions["columns"])
|
||||
data.append(total_row)
|
||||
else:
|
||||
data = frappe.db.sql(
|
||||
""" select {} from `tab{}` t1, `tab{} Item` t2 {}
|
||||
@@ -214,9 +217,32 @@ def get_data(filters, conditions):
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
total_row = calculate_total_row(data, conditions["columns"])
|
||||
data.append(total_row)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def calculate_total_row(data, columns):
|
||||
def wrap_in_quotes(label):
|
||||
return f"'{label}'"
|
||||
|
||||
total_values = {}
|
||||
for i, col in enumerate(columns):
|
||||
if "Float" in col or "Currency/currency" in col:
|
||||
total_values[i] = 0
|
||||
|
||||
for row in data:
|
||||
for i in total_values.keys():
|
||||
total_values[i] += row[i] if row[i] is not None else 0
|
||||
|
||||
total_row = [wrap_in_quotes(_("Total"))]
|
||||
for i in range(1, len(columns)):
|
||||
total_row.append(total_values.get(i, None))
|
||||
|
||||
return total_row
|
||||
|
||||
|
||||
def get_mon(dt):
|
||||
return getdate(dt).strftime("%b")
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ def send_mail(entry, email_campaign):
|
||||
subject=frappe.render_template(email_template.get("subject"), context),
|
||||
content=frappe.render_template(email_template.response_, context),
|
||||
sender=sender,
|
||||
recipients=recipient_list,
|
||||
bcc=recipient_list,
|
||||
communication_medium="Email",
|
||||
sent_or_received="Sent",
|
||||
send_email=True,
|
||||
|
||||
@@ -432,7 +432,7 @@ def _set_missing_values(source, target):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lead_details(lead, posting_date=None, company=None):
|
||||
def get_lead_details(lead, posting_date=None, company=None, doctype=None):
|
||||
if not lead:
|
||||
return {}
|
||||
|
||||
@@ -454,7 +454,7 @@ def get_lead_details(lead, posting_date=None, company=None):
|
||||
}
|
||||
)
|
||||
|
||||
set_address_details(out, lead, "Lead", company=company)
|
||||
set_address_details(out, lead, "Lead", doctype=doctype, company=company)
|
||||
|
||||
taxes_and_charges = set_taxes(
|
||||
None,
|
||||
|
||||
@@ -340,10 +340,7 @@ doc_events = {
|
||||
"User": {
|
||||
"after_insert": "frappe.contacts.doctype.contact.contact.update_contact",
|
||||
"validate": "erpnext.setup.doctype.employee.employee.validate_employee_role",
|
||||
"on_update": [
|
||||
"erpnext.setup.doctype.employee.employee.update_user_permissions",
|
||||
"erpnext.portal.utils.set_default_role",
|
||||
],
|
||||
"on_update": "erpnext.portal.utils.set_default_role",
|
||||
},
|
||||
"Communication": {
|
||||
"on_update": [
|
||||
@@ -415,29 +412,29 @@ scheduler_events = {
|
||||
"0/15 * * * *": [
|
||||
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
|
||||
],
|
||||
"0/30 * * * *": [
|
||||
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
||||
],
|
||||
"0/30 * * * *": [],
|
||||
# Hourly but offset by 30 minutes
|
||||
"30 * * * *": [
|
||||
"erpnext.accounts.doctype.gl_entry.gl_entry.rename_gle_sle_docs",
|
||||
],
|
||||
# Daily but offset by 45 minutes
|
||||
"45 0 * * *": [
|
||||
"erpnext.stock.reorder_item.reorder_item",
|
||||
],
|
||||
"45 0 * * *": [],
|
||||
},
|
||||
"hourly": [
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
],
|
||||
"hourly_long": [
|
||||
"hourly_long": [],
|
||||
"hourly_maintenance": [
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
|
||||
"erpnext.utilities.bulk_transaction.retry",
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
||||
],
|
||||
"daily": [
|
||||
"daily": [],
|
||||
"daily_long": [],
|
||||
"daily_maintenance": [
|
||||
"erpnext.support.doctype.issue.issue.auto_close_tickets",
|
||||
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
|
||||
"erpnext.controllers.accounts_controller.update_invoice_status",
|
||||
@@ -461,17 +458,16 @@ scheduler_events = {
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_daily",
|
||||
"erpnext.accounts.utils.run_ledger_health_checks",
|
||||
"erpnext.assets.doctype.asset_maintenance_log.asset_maintenance_log.update_asset_maintenance_log_status",
|
||||
],
|
||||
"weekly": [
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||
],
|
||||
"daily_long": [
|
||||
"erpnext.stock.reorder_item.reorder_item",
|
||||
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
|
||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
|
||||
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
|
||||
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
|
||||
],
|
||||
"weekly": [
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||
],
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_monthly",
|
||||
|
||||
@@ -389,10 +389,12 @@ frappe.ui.form.on("BOM", {
|
||||
);
|
||||
|
||||
has_template_rm.forEach((d) => {
|
||||
let bom_qty = dialog.fields_dict.qty?.value || 1;
|
||||
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
item_code: d.item_code,
|
||||
variant_item_code: "",
|
||||
qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1),
|
||||
qty: flt(d.qty / frm.doc.quantity) * flt(bom_qty),
|
||||
source_warehouse: d.source_warehouse,
|
||||
operation: d.operation,
|
||||
});
|
||||
|
||||
@@ -548,12 +548,14 @@
|
||||
{
|
||||
"fieldname": "process_loss_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "% Process Loss"
|
||||
"label": "% Process Loss",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "process_loss_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Process Loss Qty",
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -591,7 +593,6 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.track_semi_finished_goods === 0",
|
||||
"fieldname": "fg_based_operating_cost",
|
||||
"fieldtype": "Check",
|
||||
"label": "Finished Goods based Operating Cost"
|
||||
@@ -640,7 +641,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-29 17:43:12.966753",
|
||||
"modified": "2025-11-19 16:17:15.925156",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -465,7 +465,7 @@ class BOM(WebsiteGenerator):
|
||||
)
|
||||
)
|
||||
|
||||
def get_rm_rate(self, arg):
|
||||
def get_rm_rate(self, arg, notify=True):
|
||||
"""Get raw material rate as per selected method, if bom exists takes bom cost"""
|
||||
rate = 0
|
||||
if not self.rm_cost_as_per:
|
||||
@@ -491,7 +491,7 @@ class BOM(WebsiteGenerator):
|
||||
),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
elif notify:
|
||||
frappe.msgprint(
|
||||
_("{0} not found for item {1}").format(self.rm_cost_as_per, arg["item_code"]),
|
||||
alert=True,
|
||||
@@ -796,11 +796,14 @@ class BOM(WebsiteGenerator):
|
||||
"stock_uom": d.stock_uom,
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"sourced_by_supplier": d.sourced_by_supplier,
|
||||
}
|
||||
},
|
||||
notify=False,
|
||||
)
|
||||
|
||||
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
||||
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
|
||||
d.amount = flt(
|
||||
flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount")
|
||||
)
|
||||
d.base_amount = d.amount * flt(self.conversion_rate)
|
||||
d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
|
||||
self.quantity, self.precision("quantity")
|
||||
@@ -823,7 +826,10 @@ class BOM(WebsiteGenerator):
|
||||
d.base_rate = flt(d.rate, d.precision("rate")) * flt(
|
||||
self.conversion_rate, self.precision("conversion_rate")
|
||||
)
|
||||
d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty"))
|
||||
d.amount = flt(
|
||||
flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")),
|
||||
d.precision("amount"),
|
||||
)
|
||||
d.base_amount = flt(d.amount, d.precision("amount")) * flt(
|
||||
self.conversion_rate, self.precision("conversion_rate")
|
||||
)
|
||||
|
||||
@@ -38,6 +38,15 @@ frappe.ui.form.on("Job Card", {
|
||||
return doc.status === "Complete" ? "green" : "orange";
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query("employee", () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
status: "Active",
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
|
||||
@@ -178,17 +178,12 @@ class JobCard(Document):
|
||||
|
||||
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"))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Qty To Manufacture in the job card cannot be greater than Qty To Manufacture in the work order for the operation {0}. <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 {1}."
|
||||
).format(bold(self.operation), form_link),
|
||||
title=_("Extra Job Card Quantity"),
|
||||
)
|
||||
|
||||
def set_sub_operations(self):
|
||||
if not self.sub_operations and self.operation:
|
||||
@@ -605,7 +600,7 @@ class JobCard(Document):
|
||||
op_row.employee.append(time_log.employee)
|
||||
if time_log.time_in_mins:
|
||||
op_row.completed_time += time_log.time_in_mins
|
||||
op_row.completed_qty += time_log.completed_qty
|
||||
op_row.completed_qty += flt(time_log.completed_qty)
|
||||
|
||||
for row in self.sub_operations:
|
||||
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
|
||||
@@ -1064,14 +1059,16 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
if row.completed_qty < current_operation_qty:
|
||||
msg = f"""The completed quantity {bold(current_operation_qty)}
|
||||
of an operation {bold(self.operation)} cannot be greater
|
||||
than the completed quantity {bold(row.completed_qty)}
|
||||
of a previous operation
|
||||
{bold(row.operation)}.
|
||||
"""
|
||||
|
||||
frappe.throw(_(msg))
|
||||
frappe.throw(
|
||||
_(
|
||||
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
|
||||
).format(
|
||||
bold(current_operation_qty),
|
||||
bold(self.operation),
|
||||
bold(row.completed_qty),
|
||||
bold(row.operation),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_work_order(self):
|
||||
if self.is_work_order_closed():
|
||||
|
||||
@@ -1627,7 +1627,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
"min_order_qty": item_master.min_order_qty,
|
||||
"default_material_request_type": item_master.default_material_request_type,
|
||||
"qty": planned_qty or 1,
|
||||
"is_sub_contracted": item_master.is_subcontracted_item,
|
||||
"is_sub_contracted": item_master.is_sub_contracted_item,
|
||||
"item_code": item_master.name,
|
||||
"description": item_master.description,
|
||||
"stock_uom": item_master.stock_uom,
|
||||
|
||||
@@ -209,7 +209,7 @@ frappe.ui.form.on("Work Order", {
|
||||
if (not_completed && not_completed.length) {
|
||||
frm.add_custom_button(__("Create Job Card"), () => {
|
||||
frm.trigger("make_job_card");
|
||||
}).addClass("btn-primary");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,8 @@ frappe.ui.form.on("Work Order", {
|
||||
if (
|
||||
frm.doc.docstatus === 1 &&
|
||||
["Closed", "Completed"].includes(frm.doc.status) &&
|
||||
frm.doc.produced_qty > 0
|
||||
frm.doc.produced_qty > 0 &&
|
||||
frm.doc.produced_qty > frm.doc.disassembled_qty
|
||||
) {
|
||||
frm.add_custom_button(
|
||||
__("Disassemble Order"),
|
||||
@@ -253,7 +254,7 @@ frappe.ui.form.on("Work Order", {
|
||||
if (non_consumed_items && non_consumed_items.length) {
|
||||
frm.add_custom_button(__("Return Components"), function () {
|
||||
frm.trigger("create_stock_return_entry");
|
||||
}).addClass("btn-primary");
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -402,11 +403,14 @@ frappe.ui.form.on("Work Order", {
|
||||
erpnext.work_order
|
||||
.show_prompt_for_qty_input(frm, "Disassemble")
|
||||
.then((data) => {
|
||||
if (flt(data.qty) <= 0) {
|
||||
frappe.msgprint(__("Disassemble Qty cannot be less than or equal to <b>0</b>."));
|
||||
return;
|
||||
}
|
||||
return frappe.xcall("erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry", {
|
||||
work_order_id: frm.doc.name,
|
||||
purpose: "Disassemble",
|
||||
qty: data.qty,
|
||||
target_warehouse: data.target_warehouse,
|
||||
});
|
||||
})
|
||||
.then((stock_entry) => {
|
||||
@@ -863,24 +867,6 @@ erpnext.work_order = {
|
||||
},
|
||||
];
|
||||
|
||||
if (purpose === "Disassemble") {
|
||||
fields.push({
|
||||
fieldtype: "Link",
|
||||
options: "Warehouse",
|
||||
fieldname: "target_warehouse",
|
||||
label: __("Target Warehouse"),
|
||||
default: frm.doc.source_warehouse || frm.doc.wip_warehouse,
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
frm.qty_prompt = frappe.prompt(
|
||||
fields,
|
||||
|
||||
@@ -979,14 +979,14 @@ class WorkOrder(Document):
|
||||
|
||||
for d in self.get("operations"):
|
||||
precision = d.precision("completed_qty")
|
||||
qty = flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision)
|
||||
qty = flt(flt(d.completed_qty, precision) + flt(d.process_loss_qty, precision), precision)
|
||||
if not qty:
|
||||
d.status = "Pending"
|
||||
elif flt(qty) < flt(self.qty):
|
||||
elif qty < flt(self.qty, precision):
|
||||
d.status = "Work in Progress"
|
||||
elif flt(qty) == flt(self.qty):
|
||||
elif qty == flt(self.qty, precision):
|
||||
d.status = "Completed"
|
||||
elif flt(qty) <= max_allowed_qty_for_wo:
|
||||
elif qty <= flt(max_allowed_qty_for_wo, precision):
|
||||
d.status = "Completed"
|
||||
else:
|
||||
frappe.throw(_("Completed Qty cannot be greater than 'Qty to Manufacture'"))
|
||||
@@ -1373,6 +1373,13 @@ def make_work_order(bom_no, item, qty=0, project=None, variant_items=None, use_m
|
||||
|
||||
item_details = get_item_details(item, project)
|
||||
|
||||
if frappe.db.get_value("Item", item, "variant_of"):
|
||||
if variant_bom := frappe.db.get_value(
|
||||
"BOM",
|
||||
{"item": item, "is_default": 1, "docstatus": 1},
|
||||
):
|
||||
bom_no = variant_bom
|
||||
|
||||
wo_doc = frappe.new_doc("Work Order")
|
||||
wo_doc.production_item = item
|
||||
wo_doc.update(item_details)
|
||||
@@ -1502,7 +1509,7 @@ def make_stock_entry(work_order_id, purpose, qty=None, target_warehouse=None):
|
||||
stock_entry.to_warehouse = target_warehouse or work_order.source_warehouse
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
stock_entry.get_items(qty, work_order.production_item)
|
||||
stock_entry.get_items()
|
||||
|
||||
if purpose != "Disassemble":
|
||||
stock_entry.set_serial_no_batch_for_finished_good()
|
||||
|
||||
@@ -151,10 +151,9 @@ def get_column(filters):
|
||||
},
|
||||
{
|
||||
"label": _("Document Type"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "document_type",
|
||||
"width": 150,
|
||||
"options": "DocType",
|
||||
},
|
||||
{
|
||||
"label": _("Document Name"),
|
||||
|
||||
@@ -318,7 +318,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2024-10-21 14:13:38.777556",
|
||||
"modified": "2025-11-24 11:11:28.343568",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing",
|
||||
@@ -336,7 +336,7 @@
|
||||
"doc_view": "List",
|
||||
"label": "Learn Manufacturing",
|
||||
"type": "URL",
|
||||
"url": "https://school.frappe.io/lms/courses/manufacturing?utm_source=in_app"
|
||||
"url": "https://school.frappe.io/lms/courses/production-planning-and-execution"
|
||||
},
|
||||
{
|
||||
"color": "Grey",
|
||||
|
||||
@@ -424,3 +424,6 @@ erpnext.patches.v15_0.update_uae_zero_rated_fetch
|
||||
erpnext.patches.v15_0.update_fieldname_in_accounting_dimension_filter
|
||||
erpnext.patches.v15_0.set_asset_status_if_not_already_set
|
||||
erpnext.patches.v15_0.toggle_legacy_controller_for_period_closing
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "show_party_balance", 1)
|
||||
execute:frappe.db.set_single_value("Accounts Settings", "show_account_balance", 1)
|
||||
erpnext.patches.v16_0.update_currency_exchange_settings_for_frankfurter #2025-12-11
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
settings_meta = frappe.get_meta("Currency Exchange Settings")
|
||||
settings = frappe.get_doc("Currency Exchange Settings")
|
||||
|
||||
if (
|
||||
"frankfurter.dev" not in settings_meta.get_options("service_provider").split("\n")
|
||||
or settings.service_provider != "frankfurter.app"
|
||||
):
|
||||
return
|
||||
|
||||
settings.service_provider = "frankfurter.dev"
|
||||
settings.set_parameters_and_result()
|
||||
settings.flags.ignore_validate = True
|
||||
settings.save()
|
||||
@@ -17,6 +17,10 @@ class CircularReferenceError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class ParentIsGroupError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class Task(NestedSet):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
@@ -83,6 +87,7 @@ class Task(NestedSet):
|
||||
self.update_depends_on()
|
||||
self.validate_dependencies_for_template_task()
|
||||
self.validate_completed_on()
|
||||
self.validate_parent_is_group()
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("exp_start_date", "exp_end_date")
|
||||
@@ -153,20 +158,36 @@ class Task(NestedSet):
|
||||
def validate_parent_template_task(self):
|
||||
if self.parent_task:
|
||||
if not frappe.db.get_value("Task", self.parent_task, "is_template"):
|
||||
parent_task_format = f"""<a href="/app/task/{self.parent_task}">{self.parent_task}</a>"""
|
||||
frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
|
||||
frappe.throw(
|
||||
_("Parent Task {0} is not a Template Task").format(
|
||||
get_link_to_form("Task", self.parent_task)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_depends_on_tasks(self):
|
||||
if self.depends_on:
|
||||
for task in self.depends_on:
|
||||
if not frappe.db.get_value("Task", task.task, "is_template"):
|
||||
dependent_task_format = f"""<a href="/app/task/{task.task}">{task.task}</a>"""
|
||||
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
|
||||
frappe.throw(
|
||||
_("Dependent Task {0} is not a Template Task").format(
|
||||
get_link_to_form("Task", task.task)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_completed_on(self):
|
||||
if self.completed_on and getdate(self.completed_on) > getdate():
|
||||
frappe.throw(_("Completed On cannot be greater than Today"))
|
||||
|
||||
def validate_parent_is_group(self):
|
||||
if self.parent_task:
|
||||
if not frappe.db.get_value("Task", self.parent_task, "is_group"):
|
||||
frappe.throw(
|
||||
_("Parent Task {0} must be a Group Task").format(
|
||||
get_link_to_form("Task", self.parent_task)
|
||||
),
|
||||
ParentIsGroupError,
|
||||
)
|
||||
|
||||
def update_depends_on(self):
|
||||
depends_on_tasks = ""
|
||||
for d in self.depends_on:
|
||||
|
||||
@@ -6,7 +6,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate, nowdate
|
||||
|
||||
from erpnext.projects.doctype.task.task import CircularReferenceError
|
||||
from erpnext.projects.doctype.task.task import CircularReferenceError, ParentIsGroupError
|
||||
|
||||
|
||||
class TestTask(unittest.TestCase):
|
||||
@@ -109,6 +109,20 @@ class TestTask(unittest.TestCase):
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
|
||||
|
||||
def test_parent_task_must_be_group(self):
|
||||
parent_task = create_task(
|
||||
subject="_Test Parent Task Non Group",
|
||||
is_group=0,
|
||||
)
|
||||
|
||||
child_task = create_task(
|
||||
subject="_Test Child Task",
|
||||
parent_task=parent_task.name,
|
||||
save=False,
|
||||
)
|
||||
|
||||
self.assertRaises(ParentIsGroupError, child_task.save)
|
||||
|
||||
|
||||
def create_task(
|
||||
subject,
|
||||
|
||||
@@ -21,6 +21,7 @@ frappe.ui.form.on("Timesheet", {
|
||||
filters: {
|
||||
project: child.project,
|
||||
status: ["!=", "Cancelled"],
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -285,7 +285,7 @@ class Timesheet(Document):
|
||||
if data.activity_type or data.is_billable:
|
||||
rate = get_activity_cost(self.employee, data.activity_type)
|
||||
hours = data.billing_hours or 0
|
||||
costing_hours = data.billing_hours or data.hours or 0
|
||||
costing_hours = data.hours or 0
|
||||
if rate:
|
||||
data.billing_rate = (
|
||||
flt(rate.get("billing_rate")) if flt(data.billing_rate) == 0 else data.billing_rate
|
||||
|
||||
@@ -75,13 +75,27 @@ def get_chart_data(data):
|
||||
delay = delay + 1
|
||||
else:
|
||||
on_track = on_track + 1
|
||||
|
||||
labels = []
|
||||
datasets = []
|
||||
colors = []
|
||||
|
||||
if on_track:
|
||||
labels.append(_("On Track"))
|
||||
datasets.append(on_track)
|
||||
colors.append("#84D5BA")
|
||||
if delay:
|
||||
labels.append(_("Delayed"))
|
||||
datasets.append(delay)
|
||||
colors.append("#CB4B5F")
|
||||
|
||||
charts = {
|
||||
"data": {
|
||||
"labels": [_("On Track"), _("Delayed")],
|
||||
"datasets": [{"name": _("Delayed"), "values": [on_track, delay]}],
|
||||
"labels": labels,
|
||||
"datasets": [{"name": _("Delayed"), "values": datasets}],
|
||||
},
|
||||
"type": "percentage",
|
||||
"colors": ["#84D5BA", "#CB4B5F"],
|
||||
"colors": colors,
|
||||
}
|
||||
return charts
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("erpnext.buying");
|
||||
// cur_frm.add_fetch('project', 'cost_center', 'cost_center');
|
||||
|
||||
erpnext.buying = {
|
||||
setup_buying_controller: function() {
|
||||
@@ -11,6 +10,7 @@ erpnext.buying = {
|
||||
super.setup();
|
||||
this.toggle_enable_for_stock_uom("allow_to_edit_stock_uom_qty_for_purchase");
|
||||
this.frm.email_field = "contact_email";
|
||||
this.frm.add_fetch("project", "cost_center", "cost_center");
|
||||
}
|
||||
|
||||
onload(doc, cdt, cdn) {
|
||||
@@ -171,15 +171,13 @@ erpnext.buying = {
|
||||
shipping_address: this.frm.doc.shipping_address
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!this.frm.doc.billing_address)
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
if (!r.message) return;
|
||||
|
||||
if (
|
||||
!frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") ||
|
||||
this.frm.doc.shipping_address
|
||||
)
|
||||
return;
|
||||
this.frm.set_value("shipping_address", r.message.shipping_address || "");
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
|
||||
if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) {
|
||||
this.frm.set_value("shipping_address", r.message.shipping_address || "");
|
||||
}
|
||||
},
|
||||
});
|
||||
erpnext.utils.set_letter_head(this.frm)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user