mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-27 14:38:18 +00:00
Compare commits
418 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2597eaad51 | ||
|
|
39aaefc202 | ||
|
|
75344e9e82 | ||
|
|
d39072a689 | ||
|
|
f4a1f04566 | ||
|
|
01b8ae3e11 | ||
|
|
b80e10e15e | ||
|
|
25852879f6 | ||
|
|
d16061f1bc | ||
|
|
b35a6c2e73 | ||
|
|
5884b71b0a | ||
|
|
bcd56abb62 | ||
|
|
db70d2e4df | ||
|
|
098cbcde10 | ||
|
|
1eda22c2bd | ||
|
|
fffd3a785c | ||
|
|
88c16c8378 | ||
|
|
40c2b3c0f6 | ||
|
|
46f751e403 | ||
|
|
a21b82b238 | ||
|
|
f7536f645b | ||
|
|
ddf6eab013 | ||
|
|
3e4c331962 | ||
|
|
4b1c1d33b0 | ||
|
|
14088ee7ac | ||
|
|
6151a496e7 | ||
|
|
237915dc03 | ||
|
|
d09207ab82 | ||
|
|
d9cd09b24a | ||
|
|
0a28fb3ae1 | ||
|
|
d50c727f89 | ||
|
|
40c8201302 | ||
|
|
979c594e98 | ||
|
|
7b9f2626f8 | ||
|
|
64956ab59c | ||
|
|
78635ebe99 | ||
|
|
d6afb9b10a | ||
|
|
468ca2bde1 | ||
|
|
1d14ba1639 | ||
|
|
a270c02bb4 | ||
|
|
28aa21bf83 | ||
|
|
119195c6fa | ||
|
|
32c0532dec | ||
|
|
46e784d094 | ||
|
|
c0ce34e12c | ||
|
|
eaf5494502 | ||
|
|
04d74ad6eb | ||
|
|
2de04b8a46 | ||
|
|
974755b224 | ||
|
|
5e767ea595 | ||
|
|
f5bd85b4dc | ||
|
|
3fcf6cfef7 | ||
|
|
461bc1733f | ||
|
|
812ca37055 | ||
|
|
540a8540d6 | ||
|
|
d96590c4d9 | ||
|
|
90e4f9026d | ||
|
|
defa1d4a76 | ||
|
|
ff11429941 | ||
|
|
b3f0e2a00d | ||
|
|
7707a79d44 | ||
|
|
94900cb8b8 | ||
|
|
c1be262357 | ||
|
|
65d8a176a6 | ||
|
|
f8ab56ecc9 | ||
|
|
488ea7f994 | ||
|
|
6ea3d56972 | ||
|
|
e2c8dc5386 | ||
|
|
9c243e8dd0 | ||
|
|
4f39dfd642 | ||
|
|
af86fd3cb4 | ||
|
|
5a3bc27e2c | ||
|
|
562f93e75c | ||
|
|
e0f1e757f3 | ||
|
|
572d8530b6 | ||
|
|
0fa8cc76f5 | ||
|
|
9e10dec903 | ||
|
|
7b64f88734 | ||
|
|
7acd435835 | ||
|
|
16fe458b92 | ||
|
|
4c2dba98da | ||
|
|
e17b5dfe61 | ||
|
|
c09c5999dc | ||
|
|
a7bf55b4bf | ||
|
|
c912df95cb | ||
|
|
915315ef1b | ||
|
|
526dc68c72 | ||
|
|
9771ed4c57 | ||
|
|
57815a07ac | ||
|
|
b9c8e8d478 | ||
|
|
526ffc1176 | ||
|
|
239728e4d9 | ||
|
|
48d211f8a0 | ||
|
|
761caba8e8 | ||
|
|
3bc9190795 | ||
|
|
3720a8d5c9 | ||
|
|
03f09222cf | ||
|
|
f232024fa4 | ||
|
|
fd336e8d4b | ||
|
|
c384564314 | ||
|
|
ca6872c768 | ||
|
|
a5d1afe304 | ||
|
|
a85aeb2f9b | ||
|
|
158e290580 | ||
|
|
0e770c0bbd | ||
|
|
d262a65b00 | ||
|
|
ab9d960aa8 | ||
|
|
eec8cf8a71 | ||
|
|
284ccd1def | ||
|
|
3aafed0659 | ||
|
|
30fe711c44 | ||
|
|
ac6c06daf9 | ||
|
|
b63b5320f2 | ||
|
|
433dec8a6c | ||
|
|
81244a84e7 | ||
|
|
7a7c4a03f0 | ||
|
|
a6cf31edad | ||
|
|
d6693c9b79 | ||
|
|
56ffd52335 | ||
|
|
53e3bfbf22 | ||
|
|
db9dc86694 | ||
|
|
1c444ef822 | ||
|
|
f702a71126 | ||
|
|
908e185cfe | ||
|
|
2a70203cab | ||
|
|
85c4cc3e1b | ||
|
|
62280c285f | ||
|
|
5737d2afa3 | ||
|
|
0e00ab8865 | ||
|
|
05d614eb04 | ||
|
|
6d476604df | ||
|
|
0d527ac8ea | ||
|
|
0612f1c941 | ||
|
|
dbed426725 | ||
|
|
8497d1f8cf | ||
|
|
034d460ae1 | ||
|
|
fd94cd0e7c | ||
|
|
db251c6e11 | ||
|
|
b037dae529 | ||
|
|
b73d9700d0 | ||
|
|
73d347f456 | ||
|
|
3e7d2c6f11 | ||
|
|
4ebc23752e | ||
|
|
ef6fd7dcb5 | ||
|
|
e3e9d7b19e | ||
|
|
a4aaf67b2b | ||
|
|
d425e90ef7 | ||
|
|
79b04826d9 | ||
|
|
8c7100df04 | ||
|
|
1ffd814f92 | ||
|
|
c6e7cf13b5 | ||
|
|
55a0603356 | ||
|
|
abe433cfa7 | ||
|
|
49648b5c6e | ||
|
|
b59dc173b8 | ||
|
|
7454db2b3e | ||
|
|
fcfadf9dea | ||
|
|
098f6fd0d2 | ||
|
|
fd8fac7d40 | ||
|
|
b7fd9aea6a | ||
|
|
c5796fed4a | ||
|
|
2b25059315 | ||
|
|
dfbb3e97a8 | ||
|
|
ee22347d64 | ||
|
|
4d418d40db | ||
|
|
a6dd07802a | ||
|
|
94972da845 | ||
|
|
c2e67599f5 | ||
|
|
bcc542b1f9 | ||
|
|
ed428ceb1c | ||
|
|
93ebec90ef | ||
|
|
0b1746a4c8 | ||
|
|
c71557f432 | ||
|
|
c142a2be9c | ||
|
|
3c77653508 | ||
|
|
c572a019b4 | ||
|
|
260d87a80c | ||
|
|
e0f5ae2d4c | ||
|
|
37e750e877 | ||
|
|
3148816451 | ||
|
|
1ee03f41f2 | ||
|
|
c2f2331d49 | ||
|
|
5af5de3315 | ||
|
|
9b49a27af6 | ||
|
|
5764f5ec80 | ||
|
|
e9ae156323 | ||
|
|
54fdce648e | ||
|
|
180e232eb0 | ||
|
|
ba4a99b22c | ||
|
|
9100428f1b | ||
|
|
bcc52090c9 | ||
|
|
2ca9b75aa6 | ||
|
|
68c79a4a79 | ||
|
|
1950e82d1e | ||
|
|
6898d70382 | ||
|
|
624d1d4759 | ||
|
|
bf6b5b7b7f | ||
|
|
24d9e2c5a9 | ||
|
|
7b2e4832aa | ||
|
|
a7e8f31f56 | ||
|
|
d5a250a254 | ||
|
|
be598108b6 | ||
|
|
68bac20198 | ||
|
|
072ab8d5f3 | ||
|
|
2574c4c18c | ||
|
|
bc9f3a38ce | ||
|
|
f9e5ac6b60 | ||
|
|
8131af51a3 | ||
|
|
17dc1681b0 | ||
|
|
62340babb8 | ||
|
|
6846f02cea | ||
|
|
10b40836a9 | ||
|
|
9b0d4ab4da | ||
|
|
74e71f3868 | ||
|
|
44620884c1 | ||
|
|
bd76ef29cc | ||
|
|
dced21faf2 | ||
|
|
d5cc51b426 | ||
|
|
3581906033 | ||
|
|
90169a39bb | ||
|
|
86f8bd403d | ||
|
|
c1f2991694 | ||
|
|
8ce541bf46 | ||
|
|
0c0f1a6591 | ||
|
|
e2b6179b17 | ||
|
|
96872d314d | ||
|
|
3c6a120c5c | ||
|
|
067a57d4c3 | ||
|
|
814c17aafe | ||
|
|
5f82db200e | ||
|
|
f5f40dbcc3 | ||
|
|
15054a1c10 | ||
|
|
2e26a7fa0b | ||
|
|
08e155cec5 | ||
|
|
658d219aa1 | ||
|
|
6f164cb183 | ||
|
|
4568ed3fb4 | ||
|
|
470a9b38b1 | ||
|
|
0766c0ea43 | ||
|
|
9d73faddfe | ||
|
|
d5e25153f8 | ||
|
|
94afc32a8d | ||
|
|
97df39a630 | ||
|
|
6e5738e95c | ||
|
|
6147541f58 | ||
|
|
6d726e4b64 | ||
|
|
8fed606357 | ||
|
|
a587b6a57c | ||
|
|
51fb7affeb | ||
|
|
6ac2f510a6 | ||
|
|
2a78f47f02 | ||
|
|
8e59fe90cc | ||
|
|
bf7b9c10dc | ||
|
|
2984f79a69 | ||
|
|
f2e1482f63 | ||
|
|
0458930d9e | ||
|
|
b2a8af5ba6 | ||
|
|
45eed5fd81 | ||
|
|
435c852b54 | ||
|
|
bba0a6d950 | ||
|
|
51d5885454 | ||
|
|
cc6fbde463 | ||
|
|
b712467049 | ||
|
|
eabaef2f0b | ||
|
|
f90e984833 | ||
|
|
f6a05ec85e | ||
|
|
5ba38bcf32 | ||
|
|
39f8d35414 | ||
|
|
25b169059d | ||
|
|
717c5b25eb | ||
|
|
e5282a48ae | ||
|
|
516ad9021b | ||
|
|
86c628521e | ||
|
|
6ad84d66cc | ||
|
|
a2a6460c6c | ||
|
|
76a19076bf | ||
|
|
fdaa0b8b58 | ||
|
|
7278166b2c | ||
|
|
e5eb5406da | ||
|
|
9657c6431d | ||
|
|
7ad770a83a | ||
|
|
5396b141dd | ||
|
|
4ffb161ec8 | ||
|
|
b37fc6676e | ||
|
|
965fcf8b71 | ||
|
|
61ac18069b | ||
|
|
3ae5de7b11 | ||
|
|
2420122f0e | ||
|
|
c5826f4132 | ||
|
|
09ba9808de | ||
|
|
71248ff40b | ||
|
|
bfe0443530 | ||
|
|
eda4462e5f | ||
|
|
0ba965aae6 | ||
|
|
07eb5c714a | ||
|
|
245bc7d2fb | ||
|
|
773e56808a | ||
|
|
cb17dbd616 | ||
|
|
943e2c00bc | ||
|
|
0866c03e20 | ||
|
|
4078e252c2 | ||
|
|
caa03efbe1 | ||
|
|
4b2ac626c5 | ||
|
|
ae733cd7ad | ||
|
|
cec539629b | ||
|
|
6b286ae03d | ||
|
|
e30b2f1d04 | ||
|
|
d59d30c50a | ||
|
|
946c3554b1 | ||
|
|
bb1a655efb | ||
|
|
dada7c4aa8 | ||
|
|
dabf2c7027 | ||
|
|
76760c2ee3 | ||
|
|
b3bcfd5a64 | ||
|
|
377435fa7d | ||
|
|
82bcb62b21 | ||
|
|
7032197f97 | ||
|
|
ef801d6bb4 | ||
|
|
bb8e5adadc | ||
|
|
397f39e271 | ||
|
|
2fffc9448b | ||
|
|
5a1c61f4d9 | ||
|
|
266658cda1 | ||
|
|
dcf4ac66bb | ||
|
|
813f0fa8c9 | ||
|
|
58d9694173 | ||
|
|
1bf608f835 | ||
|
|
5eb4baccd6 | ||
|
|
ae0acd9ba1 | ||
|
|
97a4a5f1cc | ||
|
|
3bafa360b2 | ||
|
|
de571c9266 | ||
|
|
e7e67902e9 | ||
|
|
800e38453b | ||
|
|
38939005ca | ||
|
|
bce77b6117 | ||
|
|
5a3c027432 | ||
|
|
ee7602e752 | ||
|
|
656b1bcede | ||
|
|
e2a1a7a36d | ||
|
|
283b118773 | ||
|
|
fd48fb49b9 | ||
|
|
a57a9d7548 | ||
|
|
49dca6016c | ||
|
|
0422117003 | ||
|
|
6d32089d9e | ||
|
|
c3626d67ca | ||
|
|
2888b68334 | ||
|
|
f7f10a5930 | ||
|
|
3fcda3a414 | ||
|
|
f06e20d993 | ||
|
|
2fc3e30f9f | ||
|
|
41c7890a6d | ||
|
|
d9f1b0be77 | ||
|
|
fd7f6fd7f3 | ||
|
|
33d48c5575 | ||
|
|
f7332258e7 | ||
|
|
d782c52b76 | ||
|
|
be4fe38998 | ||
|
|
ce7101f555 | ||
|
|
56dd37c9e6 | ||
|
|
460291990a | ||
|
|
80976ae466 | ||
|
|
9a28d4ef5a | ||
|
|
c432506912 | ||
|
|
852c200ee0 | ||
|
|
a66854d16d | ||
|
|
72bb3e26cd | ||
|
|
128c2bf8b9 | ||
|
|
7aa46af0c3 | ||
|
|
631363632b | ||
|
|
3e3c489178 | ||
|
|
79c3bc9bcd | ||
|
|
c6682f130c | ||
|
|
c425944bdf | ||
|
|
0935b181bf | ||
|
|
bc50b94a87 | ||
|
|
02f16715e0 | ||
|
|
f0ab9b6e76 | ||
|
|
d2dc0a4c9a | ||
|
|
e8b46d9815 | ||
|
|
d6333c1562 | ||
|
|
53e18a9beb | ||
|
|
0576752d3b | ||
|
|
e3a2b310d8 | ||
|
|
97a6610c0c | ||
|
|
9ec30319e4 | ||
|
|
e511503597 | ||
|
|
ca79f6478a | ||
|
|
9ece276e76 | ||
|
|
4fe968961a | ||
|
|
f2a77d178d | ||
|
|
dbbba7262b | ||
|
|
85d18fa7a4 | ||
|
|
11a6c5b394 | ||
|
|
e12871b408 | ||
|
|
a75c2cb4a0 | ||
|
|
c18ed0862e | ||
|
|
8bddefb18b | ||
|
|
55448b7437 | ||
|
|
ac02af476a | ||
|
|
0a1b532f69 | ||
|
|
383648fb59 | ||
|
|
13239a9dee | ||
|
|
2c13b2cc22 | ||
|
|
4da44e2c3f | ||
|
|
a503460bd5 | ||
|
|
78a3701f4c | ||
|
|
eedb93b2d6 | ||
|
|
ea3042bc74 | ||
|
|
b977366dcd | ||
|
|
0a4198718b | ||
|
|
8bdbb24d73 | ||
|
|
e57f3fe727 | ||
|
|
68099a9b5c | ||
|
|
ed42d54989 | ||
|
|
b740846b68 | ||
|
|
273029d0f0 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.97.0"
|
||||
__version__ = "15.103.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
"description": "Payment Terms from orders will be fetched into the invoices as is",
|
||||
"fieldname": "automatically_fetch_payment_terms",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Fetch Payment Terms from Order"
|
||||
"label": "Automatically Fetch Payment Terms from Order/Quotation"
|
||||
},
|
||||
{
|
||||
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
|
||||
@@ -307,7 +307,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Learn about <a href=\"https://docs.frappe.io/erpnext/user/manual/en/common_party_accounting\">Common Party</a>",
|
||||
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
|
||||
"fieldname": "enable_common_party_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
@@ -671,7 +671,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-26 19:46:55.093717",
|
||||
"modified": "2026-03-06 14:49:11.467716",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@@ -701,4 +701,4 @@
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,7 @@ def get_default_company_bank_account(company, party_type, party):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bank_account_details(bank_account):
|
||||
frappe.has_permission("Bank Account", doc=bank_account, ptype="read", throw=True)
|
||||
return frappe.get_cached_value(
|
||||
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
|
||||
)
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate
|
||||
from pypika import Order
|
||||
|
||||
@@ -136,65 +138,162 @@ def get_payment_entries_for_bank_clearance(
|
||||
):
|
||||
entries = []
|
||||
|
||||
condition = ""
|
||||
pe_condition = ""
|
||||
journal_entry = frappe.qb.DocType("Journal Entry")
|
||||
journal_entry_account = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
journal_entry_query = (
|
||||
frappe.qb.from_(journal_entry_account)
|
||||
.inner_join(journal_entry)
|
||||
.on(journal_entry_account.parent == journal_entry.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("payment_document"),
|
||||
journal_entry.name.as_("payment_entry"),
|
||||
journal_entry.cheque_no.as_("cheque_number"),
|
||||
journal_entry.cheque_date,
|
||||
Sum(journal_entry_account.debit_in_account_currency).as_("debit"),
|
||||
Sum(journal_entry_account.credit_in_account_currency).as_("credit"),
|
||||
journal_entry.posting_date,
|
||||
journal_entry_account.against_account,
|
||||
journal_entry.clearance_date,
|
||||
journal_entry_account.account_currency,
|
||||
)
|
||||
.where(
|
||||
(journal_entry_account.account == account)
|
||||
& (journal_entry.docstatus == 1)
|
||||
& (journal_entry.posting_date >= from_date)
|
||||
& (journal_entry.posting_date <= to_date)
|
||||
& (journal_entry.is_opening == "No")
|
||||
)
|
||||
)
|
||||
|
||||
if not include_reconciled_entries:
|
||||
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
|
||||
pe_condition = "and (pe.clearance_date IS NULL or pe.clearance_date='0000-00-00')"
|
||||
journal_entry_query = journal_entry_query.where(
|
||||
(journal_entry.clearance_date.isnull()) | (journal_entry.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Journal Entry" as payment_document, t1.name as payment_entry,
|
||||
t1.cheque_no as cheque_number, t1.cheque_date,
|
||||
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
|
||||
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
|
||||
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
|
||||
and ifnull(t1.is_opening, 'No') = 'No' {condition}
|
||||
group by t2.account, t1.name
|
||||
order by t1.posting_date ASC, t1.name DESC
|
||||
""",
|
||||
{"account": account, "from": from_date, "to": to_date},
|
||||
as_dict=1,
|
||||
journal_entries = (
|
||||
journal_entry_query.groupby(journal_entry_account.account, journal_entry.name)
|
||||
.orderby(journal_entry.posting_date)
|
||||
.orderby(journal_entry.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
company = frappe.qb.DocType("Company")
|
||||
payment_entry_query = (
|
||||
frappe.qb.from_(pe)
|
||||
.join(company)
|
||||
.on(pe.company == company.name)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("payment_document"),
|
||||
pe.name.as_("payment_entry"),
|
||||
pe.reference_no.as_("cheque_number"),
|
||||
pe.reference_date.as_("cheque_date"),
|
||||
(
|
||||
Case()
|
||||
.when(
|
||||
pe.paid_from == account,
|
||||
(
|
||||
pe.paid_amount
|
||||
+ (
|
||||
Case()
|
||||
.when(
|
||||
(pe.payment_type == "Pay")
|
||||
& (company.default_currency == pe.paid_from_account_currency),
|
||||
pe.base_total_taxes_and_charges,
|
||||
)
|
||||
.else_(pe.total_taxes_and_charges)
|
||||
)
|
||||
),
|
||||
)
|
||||
.else_(0)
|
||||
).as_("credit"),
|
||||
(
|
||||
Case()
|
||||
.when(pe.paid_from == account, 0)
|
||||
.else_(
|
||||
pe.received_amount
|
||||
+ (
|
||||
Case()
|
||||
.when(
|
||||
company.default_currency == pe.paid_to_account_currency,
|
||||
pe.base_total_taxes_and_charges,
|
||||
)
|
||||
.else_(pe.total_taxes_and_charges)
|
||||
)
|
||||
)
|
||||
).as_("debit"),
|
||||
pe.posting_date,
|
||||
Coalesce(pe.party, Case().when(pe.paid_from == account, pe.paid_to).else_(pe.paid_from)).as_(
|
||||
"against_account"
|
||||
),
|
||||
pe.clearance_date,
|
||||
(
|
||||
Case()
|
||||
.when(pe.paid_to == account, pe.paid_to_account_currency)
|
||||
.else_(pe.paid_from_account_currency)
|
||||
).as_("account_currency"),
|
||||
)
|
||||
.where(
|
||||
((pe.paid_from == account) | (pe.paid_to == account))
|
||||
& (pe.docstatus == 1)
|
||||
& (pe.posting_date >= from_date)
|
||||
& (pe.posting_date <= to_date)
|
||||
)
|
||||
)
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""
|
||||
select
|
||||
"Payment Entry" as payment_document, pe.name as payment_entry,
|
||||
pe.reference_no as cheque_number, pe.reference_date as cheque_date,
|
||||
if(pe.paid_from=%(account)s, pe.paid_amount + if(pe.payment_type = 'Pay' and c.default_currency = pe.paid_from_account_currency, pe.base_total_taxes_and_charges, pe.total_taxes_and_charges) , 0) as credit,
|
||||
if(pe.paid_from=%(account)s, 0, pe.received_amount + pe.total_taxes_and_charges) as debit,
|
||||
pe.posting_date, ifnull(pe.party,if(pe.paid_from=%(account)s,pe.paid_to,pe.paid_from)) as against_account, pe.clearance_date,
|
||||
if(pe.paid_to=%(account)s, pe.paid_to_account_currency, pe.paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry` as pe
|
||||
join `tabCompany` c on c.name = pe.company
|
||||
where
|
||||
(pe.paid_from=%(account)s or pe.paid_to=%(account)s) and pe.docstatus=1
|
||||
and pe.posting_date >= %(from)s and pe.posting_date <= %(to)s
|
||||
{pe_condition}
|
||||
order by
|
||||
pe.posting_date ASC, pe.name DESC
|
||||
""",
|
||||
{
|
||||
"account": account,
|
||||
"from": from_date,
|
||||
"to": to_date,
|
||||
},
|
||||
as_dict=1,
|
||||
if not include_reconciled_entries:
|
||||
payment_entry_query = payment_entry_query.where(
|
||||
(pe.clearance_date.isnull()) | (pe.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
payment_entries = (payment_entry_query.orderby(pe.posting_date).orderby(pe.name, order=Order.desc)).run(
|
||||
as_dict=True
|
||||
)
|
||||
|
||||
pos_sales_invoices, pos_purchase_invoices = [], []
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
|
||||
paid_purchase_invoices_query = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.paid_amount.as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.bill_no.as_("cheque_number"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("debit"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == account)
|
||||
& (pi.posting_date >= from_date)
|
||||
& (pi.posting_date <= to_date)
|
||||
)
|
||||
)
|
||||
|
||||
if not include_reconciled_entries:
|
||||
paid_purchase_invoices_query = paid_purchase_invoices_query.where(
|
||||
(pi.clearance_date.isnull()) | (pi.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
paid_purchase_invoices = (
|
||||
paid_purchase_invoices_query.orderby(pi.posting_date).orderby(pi.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
pos_sales_invoices = []
|
||||
|
||||
if include_pos_transactions:
|
||||
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
|
||||
pos_sales_invoices = (
|
||||
pos_sales_invoices_query = (
|
||||
frappe.qb.from_(si_payment)
|
||||
.inner_join(si)
|
||||
.on(si_payment.parent == si.name)
|
||||
@@ -217,38 +316,22 @@ def get_payment_entries_for_bank_clearance(
|
||||
& (si.posting_date >= from_date)
|
||||
& (si.posting_date <= to_date)
|
||||
)
|
||||
.orderby(si.posting_date)
|
||||
.orderby(si.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
)
|
||||
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
if not include_reconciled_entries:
|
||||
pos_sales_invoices_query = pos_sales_invoices_query.where(
|
||||
(si_payment.clearance_date.isnull()) | (si_payment.clearance_date == "0000-00-00")
|
||||
)
|
||||
|
||||
pos_purchase_invoices = (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.paid_amount.as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("debit"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.cash_bank_account == account)
|
||||
& (pi.posting_date >= from_date)
|
||||
& (pi.posting_date <= to_date)
|
||||
)
|
||||
.orderby(pi.posting_date)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
pos_sales_invoices = (
|
||||
pos_sales_invoices_query.orderby(si.posting_date).orderby(si.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
entries = (
|
||||
list(payment_entries) + list(journal_entries) + list(pos_sales_invoices) + list(pos_purchase_invoices)
|
||||
list(payment_entries)
|
||||
+ list(journal_entries)
|
||||
+ list(pos_sales_invoices)
|
||||
+ list(paid_purchase_invoices)
|
||||
)
|
||||
|
||||
return entries
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Bank Statement Import", {
|
||||
onload(frm) {
|
||||
frm.set_query("bank_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
setup(frm) {
|
||||
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
|
||||
frm.import_in_progress = false;
|
||||
|
||||
@@ -136,6 +136,8 @@ class BankTransaction(Document):
|
||||
self.set_status()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ["GL Entry"]
|
||||
|
||||
for payment_entry in self.payment_entries:
|
||||
self.delink_payment_entry(payment_entry)
|
||||
|
||||
@@ -370,11 +372,12 @@ def get_clearance_details(transaction, payment_entry, bt_allocations, gl_entries
|
||||
("unallocated_amount", "bank_account"),
|
||||
as_dict=True,
|
||||
)
|
||||
bt_bank_account = frappe.db.get_value("Bank Account", bt.bank_account, "account")
|
||||
|
||||
if bt.bank_account != gl_bank_account:
|
||||
if bt_bank_account != gl_bank_account:
|
||||
frappe.throw(
|
||||
_("Bank Account {} in Bank Transaction {} is not matching with Bank Account {}").format(
|
||||
bt.bank_account, payment_entry.payment_entry, gl_bank_account
|
||||
bt_bank_account, payment_entry.payment_entry, gl_bank_account
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe import _
|
||||
from frappe import _, cint
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, add_years, cstr, getdate
|
||||
|
||||
@@ -33,23 +33,11 @@ class FiscalYear(Document):
|
||||
self.validate_dates()
|
||||
self.validate_overlap()
|
||||
|
||||
if not self.is_new():
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select year_start_date, year_end_date
|
||||
from `tabFiscal Year` where name=%s""",
|
||||
(self.name),
|
||||
)
|
||||
def on_update(self):
|
||||
frappe.cache().delete_key("fiscal_years")
|
||||
|
||||
if year_start_end_dates:
|
||||
if (
|
||||
getdate(self.year_start_date) != year_start_end_dates[0][0]
|
||||
or getdate(self.year_end_date) != year_start_end_dates[0][1]
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
|
||||
)
|
||||
)
|
||||
def on_trash(self):
|
||||
frappe.cache().delete_key("fiscal_years")
|
||||
|
||||
def validate_dates(self):
|
||||
self.validate_from_to_dates("year_start_date", "year_end_date")
|
||||
@@ -66,28 +54,20 @@ class FiscalYear(Document):
|
||||
frappe.exceptions.InvalidDates,
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
check_duplicate_fiscal_year(self)
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def on_trash(self):
|
||||
frappe.cache().delete_value("fiscal_years")
|
||||
|
||||
def validate_overlap(self):
|
||||
existing_fiscal_years = frappe.db.sql(
|
||||
"""select name from `tabFiscal Year`
|
||||
where (
|
||||
(%(year_start_date)s between year_start_date and year_end_date)
|
||||
or (%(year_end_date)s between year_start_date and year_end_date)
|
||||
or (year_start_date between %(year_start_date)s and %(year_end_date)s)
|
||||
or (year_end_date between %(year_start_date)s and %(year_end_date)s)
|
||||
) and name!=%(name)s""",
|
||||
{
|
||||
"year_start_date": self.year_start_date,
|
||||
"year_end_date": self.year_end_date,
|
||||
"name": self.name or "No Name",
|
||||
},
|
||||
as_dict=True,
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
name = self.name or self.year
|
||||
|
||||
existing_fiscal_years = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where(
|
||||
(fy.year_start_date <= self.year_end_date)
|
||||
& (fy.year_end_date >= self.year_start_date)
|
||||
& (fy.name != name)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if existing_fiscal_years:
|
||||
@@ -110,37 +90,30 @@ class FiscalYear(Document):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Year start date or end date is overlapping with {0}. To avoid please set company"
|
||||
).format(existing.name),
|
||||
).format(frappe.get_desk_link("Fiscal Year", existing.name, open_in_new_tab=True)),
|
||||
frappe.NameError,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_duplicate_fiscal_year(doc):
|
||||
year_start_end_dates = frappe.db.sql(
|
||||
"""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
|
||||
(doc.name),
|
||||
)
|
||||
for fiscal_year, ysd, yed in year_start_end_dates:
|
||||
if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
|
||||
not frappe.flags.in_test
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}"
|
||||
).format(fiscal_year)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def auto_create_fiscal_year():
|
||||
for d in frappe.db.sql(
|
||||
"""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
|
||||
):
|
||||
fy = frappe.qb.DocType("Fiscal Year")
|
||||
|
||||
# Skipped auto-creating Short Year, as it has very rare use case.
|
||||
# Reference: https://www.irs.gov/businesses/small-businesses-self-employed/tax-years (US)
|
||||
follow_up_date = add_days(getdate(), days=3)
|
||||
fiscal_year = (
|
||||
frappe.qb.from_(fy)
|
||||
.select(fy.name)
|
||||
.where((fy.year_end_date == follow_up_date) & (fy.is_short_year == 0))
|
||||
.run()
|
||||
)
|
||||
|
||||
for d in fiscal_year:
|
||||
try:
|
||||
current_fy = frappe.get_doc("Fiscal Year", d[0])
|
||||
|
||||
new_fy = frappe.copy_doc(current_fy, ignore_no_copy=False)
|
||||
new_fy = frappe.new_doc("Fiscal Year")
|
||||
new_fy.disabled = cint(current_fy.disabled)
|
||||
|
||||
new_fy.year_start_date = add_days(current_fy.year_end_date, 1)
|
||||
new_fy.year_end_date = add_years(current_fy.year_end_date, 1)
|
||||
@@ -148,6 +121,10 @@ def auto_create_fiscal_year():
|
||||
start_year = cstr(new_fy.year_start_date.year)
|
||||
end_year = cstr(new_fy.year_end_date.year)
|
||||
new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
|
||||
|
||||
for row in current_fy.companies:
|
||||
new_fy.append("companies", {"company": row.company})
|
||||
|
||||
new_fy.auto_created = 1
|
||||
|
||||
new_fy.insert(ignore_permissions=True)
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 18:01:53.495929",
|
||||
"modified": "2026-02-20 23:02:26.193606",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year Company",
|
||||
@@ -30,4 +31,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ class FiscalYearCompany(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
company: DF.Link | None
|
||||
company: DF.Link
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -214,6 +214,8 @@ class JournalEntry(AccountsController):
|
||||
def on_cancel(self):
|
||||
# References for this Journal are removed on the `on_cancel` event in accounts_controller
|
||||
super().on_cancel()
|
||||
|
||||
from_doc_events = getattr(self, "ignore_linked_doctypes", ())
|
||||
self.ignore_linked_doctypes = (
|
||||
"GL Entry",
|
||||
"Stock Ledger Entry",
|
||||
@@ -226,6 +228,10 @@ class JournalEntry(AccountsController):
|
||||
"Unreconcile Payment Entries",
|
||||
"Advance Payment Ledger Entry",
|
||||
)
|
||||
|
||||
if from_doc_events and from_doc_events != self.ignore_linked_doctypes:
|
||||
self.ignore_linked_doctypes = self.ignore_linked_doctypes + from_doc_events
|
||||
|
||||
self.make_gl_entries(1)
|
||||
self.unlink_advance_entry_reference()
|
||||
self.unlink_asset_reference()
|
||||
@@ -263,6 +269,9 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Journal Entry type should be set as Depreciation Entry for asset depreciation"))
|
||||
|
||||
def validate_stock_accounts(self):
|
||||
if not erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
return
|
||||
|
||||
stock_accounts = get_stock_accounts(self.company, accounts=self.accounts)
|
||||
for account in stock_accounts:
|
||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry\nBank Transaction",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@@ -198,7 +198,7 @@
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance', 'Bank Transaction'])",
|
||||
"fieldname": "reference_due_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Reference Due Date",
|
||||
@@ -295,7 +295,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-27 12:23:33.157655",
|
||||
"modified": "2026-02-19 17:01:22.642454",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
||||
@@ -55,6 +55,7 @@ class JournalEntryAccount(Document):
|
||||
"Fees",
|
||||
"Full and Final Statement",
|
||||
"Payment Entry",
|
||||
"Bank Transaction",
|
||||
]
|
||||
user_remark: DF.SmallText | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import escape_html, flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -84,6 +84,11 @@ class OpeningInvoiceCreationTool(Document):
|
||||
)
|
||||
prepare_invoice_summary(doctype, invoices)
|
||||
|
||||
invoices_summary_companies = list(invoices_summary.keys())
|
||||
|
||||
for company in invoices_summary_companies:
|
||||
invoices_summary[escape_html(company)] = invoices_summary.pop(company)
|
||||
|
||||
return invoices_summary, max_count
|
||||
|
||||
def validate_company(self):
|
||||
|
||||
@@ -516,12 +516,16 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("contact_email", "");
|
||||
frm.set_value("contact_person", "");
|
||||
}
|
||||
|
||||
if (frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) {
|
||||
if (!frm.doc.posting_date) {
|
||||
frappe.msgprint(__("Please select Posting Date before selecting Party"));
|
||||
frm.set_value("party", "");
|
||||
return;
|
||||
}
|
||||
|
||||
erpnext.utils.get_employee_contact_details(frm);
|
||||
|
||||
frm.set_party_account_based_on_party = true;
|
||||
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
@@ -1465,16 +1469,15 @@ frappe.ui.form.on("Payment Entry", {
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
// set taxes table
|
||||
if (r.message) {
|
||||
for (let tax of r.message) {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.add_child("taxes", tax);
|
||||
let taxes = r.message;
|
||||
taxes.forEach((tax) => {
|
||||
if (tax.charge_type === "On Net Total") {
|
||||
tax.charge_type = "On Paid Amount";
|
||||
}
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
});
|
||||
frm.set_value("taxes", taxes);
|
||||
frm.events.apply_taxes(frm);
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1092,20 +1092,32 @@ class PaymentEntry(AccountsController):
|
||||
self.base_paid_amount + deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.source_exchange_rate
|
||||
flt(
|
||||
(
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
),
|
||||
self.precision("unallocated_amount"),
|
||||
)
|
||||
/ self.source_exchange_rate
|
||||
)
|
||||
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
|
||||
self.base_received_amount - deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.target_exchange_rate
|
||||
flt(
|
||||
(
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
),
|
||||
self.precision("unallocated_amount"),
|
||||
)
|
||||
/ self.target_exchange_rate
|
||||
)
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
exchange_gain_loss = flt(
|
||||
@@ -2544,14 +2556,9 @@ def get_orders_to_be_billed(
|
||||
if not voucher_type:
|
||||
return []
|
||||
|
||||
# Add cost center condition
|
||||
doc = frappe.get_doc({"doctype": voucher_type})
|
||||
condition = ""
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
condition = ""
|
||||
active_dimensions = get_dimensions(True)[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += f" and {dim.fieldname}='{filters.get(dim.fieldname)}'"
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@@ -59,7 +60,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-05 16:07:47.307971",
|
||||
"modified": "2026-03-11 14:26:11.312950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
|
||||
@@ -536,7 +536,7 @@ class PaymentRequest(Document):
|
||||
row_number += TO_SKIP_NEW_ROW
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def make_payment_request(**args):
|
||||
"""Make payment request"""
|
||||
|
||||
@@ -548,6 +548,9 @@ def make_payment_request(**args):
|
||||
if args.dn and not isinstance(args.dn, str):
|
||||
frappe.throw(_("Invalid parameter. 'dn' should be of type str"))
|
||||
|
||||
frappe.has_permission("Payment Request", "create", throw=True)
|
||||
frappe.has_permission(args.dt, "read", args.dn, throw=True)
|
||||
|
||||
ref_doc = args.ref_doc or frappe.get_doc(args.dt, args.dn)
|
||||
if not args.get("company"):
|
||||
args.company = ref_doc.company
|
||||
@@ -822,7 +825,7 @@ def get_print_format_list(ref_doctype):
|
||||
return {"print_format": print_format_list}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def resend_payment_email(docname):
|
||||
return frappe.get_doc("Payment Request", docname).send_email()
|
||||
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
"sec_warehouse",
|
||||
"set_warehouse",
|
||||
"items_section",
|
||||
"update_stock",
|
||||
"scan_barcode",
|
||||
"last_scanned_warehouse",
|
||||
"items",
|
||||
@@ -574,7 +573,6 @@
|
||||
"label": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"fieldname": "set_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Source Warehouse",
|
||||
@@ -588,15 +586,6 @@
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-shopping-cart"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Stock",
|
||||
"oldfieldname": "update_stock",
|
||||
"oldfieldtype": "Check",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "scan_barcode",
|
||||
"fieldtype": "Data",
|
||||
@@ -1582,7 +1571,7 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 22:22:31.471752",
|
||||
"modified": "2026-02-22 04:18:50.691218",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1627,6 +1616,7 @@
|
||||
"role": "All"
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
@@ -1635,4 +1625,4 @@
|
||||
"timeline_field": "customer",
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,6 @@ class POSInvoice(SalesInvoice):
|
||||
total_taxes_and_charges: DF.Currency
|
||||
update_billed_amount_in_delivery_note: DF.Check
|
||||
update_billed_amount_in_sales_order: DF.Check
|
||||
update_stock: DF.Check
|
||||
write_off_account: DF.Link | None
|
||||
write_off_amount: DF.Currency
|
||||
write_off_cost_center: DF.Link | None
|
||||
@@ -652,7 +651,6 @@ class POSInvoice(SalesInvoice):
|
||||
"tax_category",
|
||||
"ignore_pricing_rule",
|
||||
"company_address",
|
||||
"update_stock",
|
||||
):
|
||||
if not for_validate:
|
||||
self.set(fieldname, profile.get(fieldname))
|
||||
|
||||
@@ -1101,7 +1101,6 @@ def create_pos_invoice(**args):
|
||||
|
||||
pos_inv = frappe.new_doc("POS Invoice")
|
||||
pos_inv.update(args)
|
||||
pos_inv.update_stock = 1
|
||||
pos_inv.is_pos = 1
|
||||
pos_inv.pos_profile = args.pos_profile or pos_profile.name
|
||||
|
||||
|
||||
@@ -146,6 +146,7 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
sales_invoice.is_consolidated = 1
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.update_stock = 1
|
||||
|
||||
if not sales_invoice.posting_date:
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
@@ -174,6 +175,7 @@ class POSInvoiceMergeLog(Document):
|
||||
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 1
|
||||
credit_note.update_stock = 1
|
||||
credit_note.posting_date = getdate(self.posting_date)
|
||||
credit_note.posting_time = get_time(self.posting_time)
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"validate_stock_on_save",
|
||||
"print_receipt_on_order_complete",
|
||||
"column_break_16",
|
||||
"update_stock",
|
||||
"ignore_pricing_rule",
|
||||
"allow_rate_change",
|
||||
"allow_discount_change",
|
||||
@@ -297,7 +296,6 @@
|
||||
"options": "Print Format"
|
||||
},
|
||||
{
|
||||
"depends_on": "update_stock",
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
@@ -312,14 +310,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Pricing Rule"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "update_stock",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Update Stock",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "hide_unavailable_items",
|
||||
@@ -432,7 +422,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2025-04-14 15:58:20.497426",
|
||||
"modified": "2026-02-22 04:17:03.308876",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -61,7 +61,6 @@ class POSProfile(Document):
|
||||
tax_category: DF.Link | None
|
||||
taxes_and_charges: DF.Link | None
|
||||
tc_name: DF.Link | None
|
||||
update_stock: DF.Check
|
||||
validate_stock_on_save: DF.Check
|
||||
warehouse: DF.Link
|
||||
write_off_account: DF.Link
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Apply On",
|
||||
"options": "\nItem Code\nItem Group\nBrand\nTransaction",
|
||||
"options": "Item Code\nItem Group\nBrand\nTransaction",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -657,7 +657,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 11:40:07.096854",
|
||||
"modified": "2026-02-17 12:24:07.553505",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
@@ -719,4 +719,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ class PricingRule(Document):
|
||||
apply_discount_on: DF.Literal["Grand Total", "Net Total"]
|
||||
apply_discount_on_rate: DF.Check
|
||||
apply_multiple_pricing_rules: DF.Check
|
||||
apply_on: DF.Literal["", "Item Code", "Item Group", "Brand", "Transaction"]
|
||||
apply_on: DF.Literal["Item Code", "Item Group", "Brand", "Transaction"]
|
||||
apply_recursion_over: DF.Float
|
||||
apply_rule_on_other: DF.Literal["", "Item Code", "Item Group", "Brand"]
|
||||
brands: DF.Table[PricingRuleBrand]
|
||||
@@ -346,8 +346,7 @@ def apply_pricing_rule(args, doc=None):
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not args.transaction_type:
|
||||
set_transaction_type(args)
|
||||
set_transaction_type(args)
|
||||
|
||||
# list of dictionaries
|
||||
out = []
|
||||
@@ -683,23 +682,23 @@ def remove_pricing_rules(item_list):
|
||||
return out
|
||||
|
||||
|
||||
def set_transaction_type(args):
|
||||
if args.transaction_type:
|
||||
def set_transaction_type(pricing_ctx: frappe._dict) -> None:
|
||||
if pricing_ctx.transaction_type in ["buying", "selling"]:
|
||||
return
|
||||
if args.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
args.transaction_type = "selling"
|
||||
elif args.doctype in (
|
||||
if pricing_ctx.doctype in ("Opportunity", "Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
pricing_ctx.transaction_type = "selling"
|
||||
elif pricing_ctx.doctype in (
|
||||
"Material Request",
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
):
|
||||
args.transaction_type = "buying"
|
||||
elif args.customer:
|
||||
args.transaction_type = "selling"
|
||||
pricing_ctx.transaction_type = "buying"
|
||||
elif pricing_ctx.customer:
|
||||
pricing_ctx.transaction_type = "selling"
|
||||
else:
|
||||
args.transaction_type = "buying"
|
||||
pricing_ctx.transaction_type = "buying"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -243,8 +243,10 @@ def get_other_conditions(conditions, values, args):
|
||||
if group_condition:
|
||||
conditions += " and " + group_condition
|
||||
|
||||
date = args.get("transaction_date") or frappe.get_value(
|
||||
args.get("doctype"), args.get("name"), "posting_date", ignore=True
|
||||
date = (
|
||||
args.get("transaction_date")
|
||||
or args.get("posting_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')
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
||||
"depends_on": "eval:parent.apply_on == 'Brand'",
|
||||
"fieldname": "brand",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
@@ -91,7 +91,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-24 14:48:59.649168",
|
||||
"modified": "2026-02-17 12:17:13.073587",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule Brand",
|
||||
@@ -107,4 +107,4 @@
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "eval:parent.apply_on == 'Item Code'",
|
||||
"depends_on": "eval:parent.apply_on == 'Item Group'",
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
@@ -91,7 +91,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-03-24 14:48:59.649168",
|
||||
"modified": "2026-02-17 12:16:57.778471",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule Item Group",
|
||||
@@ -107,4 +107,4 @@
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date",
|
||||
"label": "Posting Date",
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
"print_hide": 1,
|
||||
@@ -382,7 +382,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "bill_no",
|
||||
"collapsible_depends_on": "posting_date",
|
||||
"fieldname": "supplier_invoice_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Supplier Invoice"
|
||||
@@ -1660,7 +1660,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-05 20:45:16.964500",
|
||||
"modified": "2026-03-17 20:44:00.221219",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -612,12 +612,13 @@ class PurchaseInvoice(BuyingController):
|
||||
frappe.db.set_value(self.doctype, self.name, "against_expense_account", self.against_expense_account)
|
||||
|
||||
def po_required(self):
|
||||
if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes":
|
||||
if frappe.get_value(
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "po_required") == "Yes"
|
||||
and not self.is_internal_transfer()
|
||||
and not frappe.db.get_value(
|
||||
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order"
|
||||
):
|
||||
return
|
||||
|
||||
)
|
||||
):
|
||||
for d in self.get("items"):
|
||||
if not d.purchase_order:
|
||||
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
|
||||
@@ -728,9 +729,10 @@ class PurchaseInvoice(BuyingController):
|
||||
for item in self.get("items"):
|
||||
if item.purchase_receipt:
|
||||
frappe.throw(
|
||||
_("Stock cannot be updated against Purchase Receipt {0}").format(
|
||||
item.purchase_receipt
|
||||
)
|
||||
_(
|
||||
"Stock cannot be updated for Purchase Invoice {0} because a Purchase Receipt {1} has already been created for this transaction. Please disable the 'Update Stock' checkbox in the Purchase Invoice and save the invoice."
|
||||
).format(self.name, item.purchase_receipt),
|
||||
title=_("Stock Update Not Allowed"),
|
||||
)
|
||||
|
||||
def validate_for_repost(self):
|
||||
@@ -1729,10 +1731,6 @@ class PurchaseInvoice(BuyingController):
|
||||
project_doc.db_update()
|
||||
|
||||
def validate_supplier_invoice(self):
|
||||
if self.bill_date:
|
||||
if getdate(self.bill_date) > getdate(self.posting_date):
|
||||
frappe.throw(_("Supplier Invoice Date cannot be greater than Posting Date"))
|
||||
|
||||
if self.bill_no:
|
||||
if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")):
|
||||
fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True)
|
||||
|
||||
@@ -373,7 +373,7 @@
|
||||
"fieldtype": "Date",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Date",
|
||||
"label": "Posting Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "posting_date",
|
||||
"oldfieldtype": "Date",
|
||||
|
||||
@@ -804,11 +804,9 @@ class SalesInvoice(SellingController):
|
||||
if self.pos_profile:
|
||||
pos = frappe.get_doc("POS Profile", self.pos_profile)
|
||||
|
||||
if not self.get("payments") and not for_validate:
|
||||
update_multi_mode_option(self, pos)
|
||||
|
||||
if pos:
|
||||
if not for_validate:
|
||||
update_multi_mode_option(self, pos)
|
||||
self.tax_category = pos.get("tax_category")
|
||||
|
||||
if not for_validate and not self.customer:
|
||||
@@ -854,9 +852,6 @@ class SalesInvoice(SellingController):
|
||||
if selling_price_list:
|
||||
self.set("selling_price_list", selling_price_list)
|
||||
|
||||
if not for_validate:
|
||||
self.update_stock = cint(pos.get("update_stock"))
|
||||
|
||||
# set pos values in items
|
||||
for item in self.get("items"):
|
||||
if item.get("item_code"):
|
||||
@@ -1097,7 +1092,9 @@ class SalesInvoice(SellingController):
|
||||
d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0
|
||||
|
||||
def update_packing_list(self):
|
||||
if cint(self.update_stock) == 1:
|
||||
if self.doctype == "POS Invoice" or (
|
||||
self.doctype == "Sales Invoice" and cint(self.update_stock) == 1
|
||||
):
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
|
||||
make_packing_list(self)
|
||||
@@ -1200,6 +1197,9 @@ class SalesInvoice(SellingController):
|
||||
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
||||
|
||||
def process_asset_depreciation(self):
|
||||
if self.is_internal_transfer():
|
||||
return
|
||||
|
||||
if (self.is_return and self.docstatus == 2) or (not self.is_return and self.docstatus == 1):
|
||||
self.depreciate_asset_on_sale()
|
||||
else:
|
||||
@@ -2516,7 +2516,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"doctype": target_doctype,
|
||||
"postprocess": update_details,
|
||||
"set_target_warehouse": "set_from_warehouse",
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"],
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
|
||||
},
|
||||
doctype + " Item": item_field_map,
|
||||
},
|
||||
@@ -2745,6 +2745,8 @@ def update_multi_mode_option(doc, pos_profile):
|
||||
payment.account = payment_mode.default_account
|
||||
payment.type = payment_mode.type
|
||||
|
||||
mop_refetched = bool(doc.payments)
|
||||
|
||||
doc.set("payments", [])
|
||||
invalid_modes = []
|
||||
mode_of_payments = [d.mode_of_payment for d in pos_profile.get("payments")]
|
||||
@@ -2766,6 +2768,11 @@ def update_multi_mode_option(doc, pos_profile):
|
||||
msg = _("Please set default Cash or Bank account in Mode of Payments {}")
|
||||
frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account"))
|
||||
|
||||
if mop_refetched:
|
||||
frappe.msgprint(
|
||||
_("Payment methods refreshed. Please review before proceeding."), indicator="orange", alert=True
|
||||
)
|
||||
|
||||
|
||||
def get_all_mode_of_payments(doc):
|
||||
return frappe.db.sql(
|
||||
|
||||
@@ -4835,6 +4835,33 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
|
||||
self.assertEqual(stock_ledger_entry.incoming_rate, 0.0)
|
||||
|
||||
def test_inter_company_transaction_cost_center(self):
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
debit_to="Debtors - WP",
|
||||
warehouse="Stores - WP",
|
||||
income_account="Sales - WP",
|
||||
expense_account="Cost of Goods Sold - WP",
|
||||
parent_cost_center="Main - WP",
|
||||
cost_center="Main - WP",
|
||||
currency="USD",
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
si.selling_price_list = "_Test Price List Rest of the World"
|
||||
si.submit()
|
||||
|
||||
cost_center = frappe.db.get_value("Company", "_Test Company 1", "cost_center")
|
||||
frappe.db.set_value("Company", "_Test Company 1", "cost_center", None)
|
||||
|
||||
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
|
||||
|
||||
self.assertEqual(target_doc.cost_center, None)
|
||||
self.assertEqual(target_doc.items[0].cost_center, None)
|
||||
|
||||
frappe.db.set_value("Company", "_Test Company 1", "cost_center", cost_center)
|
||||
|
||||
|
||||
def make_item_for_si(item_code, properties=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@@ -840,6 +840,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Incoming Rate (Costing)",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1
|
||||
},
|
||||
@@ -983,7 +984,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-12 16:33:55.503777",
|
||||
"modified": "2026-02-23 14:37:14.853941",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -993,4 +994,4 @@
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.doctype == 'Sales Invoice'",
|
||||
"depends_on": "eval: [\"POS Invoice\", \"Sales Invoice\"].includes(parent.doctype)",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
@@ -85,7 +85,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-23 16:20:06.436979",
|
||||
"modified": "2026-02-16 20:46:34.592604",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Payment",
|
||||
@@ -95,4 +95,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +152,9 @@ class ShippingRule(Document):
|
||||
frappe.throw(_("Shipping rule only applicable for Buying"))
|
||||
|
||||
shipping_charge["doctype"] = "Purchase Taxes and Charges"
|
||||
shipping_charge["category"] = "Valuation and Total"
|
||||
shipping_charge["category"] = (
|
||||
"Valuation and Total" if doc.get_stock_items() or doc.get_asset_items() else "Total"
|
||||
)
|
||||
shipping_charge["add_deduct_tax"] = "Add"
|
||||
|
||||
existing_shipping_charge = doc.get("taxes", filters=shipping_charge)
|
||||
|
||||
@@ -804,12 +804,19 @@ def validate_against_pcv(is_opening, posting_date, company):
|
||||
title=_("Invalid Opening Entry"),
|
||||
)
|
||||
|
||||
last_pcv_date = frappe.db.get_value(
|
||||
"Period Closing Voucher", {"docstatus": 1, "company": company}, "max(period_end_date)"
|
||||
)
|
||||
# Local import so you don't have to touch file-level imports
|
||||
from frappe.query_builder.functions import Max
|
||||
|
||||
pcv = frappe.qb.DocType("Period Closing Voucher")
|
||||
|
||||
last_pcv_date = (
|
||||
frappe.qb.from_(pcv)
|
||||
.select(Max(pcv.period_end_date))
|
||||
.where((pcv.docstatus == 1) & (pcv.company == company))
|
||||
).run(pluck=True)[0]
|
||||
|
||||
if last_pcv_date and getdate(posting_date) <= getdate(last_pcv_date):
|
||||
message = _("Books have been closed till the period ending on {0}").format(formatdate(last_pcv_date))
|
||||
message = _("Books have been closed till the period ending on {0}.").format(formatdate(last_pcv_date))
|
||||
message += "</br >"
|
||||
message += _("You cannot create/amend any accounting entries till this date.")
|
||||
frappe.throw(message, title=_("Period Closed"))
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<h4>{{ _("New Fiscal Year - {0}").format(doc.name) }}</h4>
|
||||
|
||||
<p>{{ _("A new fiscal year has been automatically created.") }}</p>
|
||||
|
||||
<p>{{ _("Fiscal Year Details") }}</p>
|
||||
|
||||
<table style="margin-bottom: 1rem; width: 70%">
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("Year Name") }}</td>
|
||||
<td>{{ doc.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("Start Date") }}</td>
|
||||
<td>{{ frappe.format_value(doc.year_start_date) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="font-weight:bold; width: 40%">{{ _("End Date") }}</td>
|
||||
<td>{{ frappe.format_value(doc.year_end_date) }}</td>
|
||||
</tr>
|
||||
{% if doc.companies|length > 0 %}
|
||||
<tr>
|
||||
<td style="vertical-align: top; font-weight: bold; width: 40%" rowspan="{{ doc.companies|length }}">
|
||||
{% if doc.companies|length < 2 %}
|
||||
{{ _("Company") }}
|
||||
{% else %}
|
||||
{{ _("Companies") }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ doc.companies[0].company }}</td>
|
||||
</tr>
|
||||
{% for idx in range(1, doc.companies|length) %}
|
||||
<tr>
|
||||
<td>{{ doc.companies[idx].company }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</table>
|
||||
|
||||
{% if doc.disabled %}
|
||||
<p>{{ _("The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.") }}</p>
|
||||
{% endif %}
|
||||
|
||||
<p>{{ _("Please review the {0} configuration and complete any required financial setup activities.").format(frappe.utils.get_link_to_form("Fiscal Year", doc.name, frappe.bold("Fiscal Year"))) }}</p>
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"attach_print": 0,
|
||||
"channel": "Email",
|
||||
"condition": "doc.auto_created",
|
||||
"condition": "doc.auto_created == 1",
|
||||
"creation": "2018-04-25 14:19:05.440361",
|
||||
"days_in_advance": 0,
|
||||
"docstatus": 0,
|
||||
@@ -11,19 +11,22 @@
|
||||
"event": "New",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"message": "<h3>{{_(\"Fiscal Year\")}}</h3>\n\n<p>{{ _(\"New fiscal year created :- \") }} {{ doc.name }}</p>",
|
||||
"modified": "2018-04-25 14:30:38.588534",
|
||||
"message": "<h4>{{ _(\"New Fiscal Year - {0}\").format(doc.name) }}</h4>\n\n<p>{{ _(\"A new fiscal year has been automatically created.\") }}</p>\n\n<p>{{ _(\"Fiscal Year Details\") }}</p>\n\n<table style=\"margin-bottom: 1rem; width: 70%\">\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Year Name\") }}</td>\n <td>{{ doc.name }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"Start Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_start_date) }}</td>\n </tr>\n <tr>\n <td style=\"font-weight:bold; width: 40%\">{{ _(\"End Date\") }}</td>\n <td>{{ frappe.format_value(doc.year_end_date) }}</td>\n </tr>\n {% if doc.companies|length > 0 %}\n <tr>\n <td style=\"vertical-align: top; font-weight: bold; width: 40%\" rowspan=\"{{ doc.companies|length }}\">\n {% if doc.companies|length < 2 %}\n {{ _(\"Company\") }}\n {% else %}\n {{ _(\"Companies\") }}\n {% endif %}\n </td>\n <td>{{ doc.companies[0].company }}</td>\n </tr>\n {% for idx in range(1, doc.companies|length) %}\n <tr>\n <td>{{ doc.companies[idx].company }}</td>\n </tr>\n {% endfor %}\n {% endif %}\n</table>\n\n{% if doc.disabled %}\n<p>{{ _(\"The fiscal year has been automatically created in a Disabled state to maintain consistency with the previous fiscal year's status.\") }}</p>\n{% endif %}\n\n<p>{{ _(\"Please review the {0} configuration and complete any required financial setup activities.\").format(frappe.utils.get_link_to_form(\"Fiscal Year\", doc.name, frappe.bold(\"Fiscal Year\"))) }}</p>",
|
||||
"message_type": "HTML",
|
||||
"modified": "2026-02-21 15:59:07.775679",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Notification for new fiscal year",
|
||||
"owner": "Administrator",
|
||||
"recipients": [
|
||||
{
|
||||
"email_by_role": "Accounts User"
|
||||
"receiver_by_role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"email_by_role": "Accounts Manager"
|
||||
"receiver_by_role": "Accounts User"
|
||||
}
|
||||
],
|
||||
"subject": "Notification for new fiscal year {{ doc.name }}"
|
||||
"send_system_notification": 0,
|
||||
"send_to_all_assignees": 0,
|
||||
"subject": "{{ _(\"New Fiscal Year {0} - Review Required\").format(doc.name) }}"
|
||||
}
|
||||
@@ -7,18 +7,16 @@ from frappe import _, msgprint, qb, scrub
|
||||
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Abs, Count, Date, Sum
|
||||
from frappe.query_builder.functions import Abs, Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_years,
|
||||
cint,
|
||||
cstr,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_last_day,
|
||||
get_timestamp,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
@@ -302,19 +300,9 @@ def complete_contact_details(party_details):
|
||||
contact_details = frappe._dict()
|
||||
|
||||
if party_details.party_type == "Employee":
|
||||
contact_details = frappe.db.get_value(
|
||||
"Employee",
|
||||
party_details.party,
|
||||
[
|
||||
"employee_name as contact_display",
|
||||
"prefered_email as contact_email",
|
||||
"cell_number as contact_mobile",
|
||||
"designation as contact_designation",
|
||||
"department as contact_department",
|
||||
],
|
||||
as_dict=True,
|
||||
)
|
||||
from erpnext.setup.doctype.employee.employee import _get_contact_details as get_employee_contact
|
||||
|
||||
contact_details = get_employee_contact(party_details.party)
|
||||
contact_details.update({"contact_person": None, "contact_phone": None})
|
||||
elif party_details.contact_person:
|
||||
contact_details = frappe.db.get_value(
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"align_labels_right": 0,
|
||||
"creation": "2016-05-05 17:16:18.564460",
|
||||
"custom_format": 1,
|
||||
"disabled": 0,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Default",
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Monospace;\n\t\tline-height: 200%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n<p class=\"text-center\">\n\t{{ company }}<br>\n\t{{ __(\"POS No : \") }} {{ offline_pos_name }}<br>\n</p>\n<p>\n\t<b>{{ __(\"Customer\") }}:</b> {{ customer }}<br>\n</p>\n\n<p>\n\t<b>{{ __(\"Date\") }}:</b> {{ dateutil.global_date_format(posting_date) }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ __(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ __(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{% for item in items %}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_name }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ format_number(item.qty, null,precision(\"difference\")) }}<br>@ {{ format_currency(item.rate, currency) }}</td>\n\t\t\t<td class=\"text-right\">{{ format_currency(item.amount, currency) }}</td>\n\t\t</tr>\n\t\t{% endfor %}\n\t</tbody>\n</table>\n\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ __(\"Net Total\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% for row in taxes %}\n\t\t{% if not row.included_in_print_rate %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t{{ row.description }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(row.tax_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t{% endfor %}\n\t\t{% if discount_amount %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ __(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(discount_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{% endif %}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(grand_total, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ format_currency(paid_amount, currency) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ __(\"Qty Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ qty_total }}\n\t\t\t</td>\n\t\t</tr>\n\t</tbody>\n</table>\n\n\n<hr>\n<p>{{ terms }}</p>\n<p class=\"text-center\">{{ __(\"Thank you, please visit again.\") }}</p>",
|
||||
"idx": 0,
|
||||
"line_breaks": 0,
|
||||
"modified": "2019-09-05 17:20:30.726659",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Point of Sale",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "JS",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 0,
|
||||
"standard": "Yes"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.report.accounts_payable.accounts_payable import execute
|
||||
@@ -57,3 +57,66 @@ class TestAccountsPayable(AccountsTestMixin, FrappeTestCase):
|
||||
if not do_not_submit:
|
||||
pi = pi.submit()
|
||||
return pi
|
||||
|
||||
def test_payment_terms_template_filters(self):
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
|
||||
payment_term1 = frappe.get_doc(
|
||||
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
|
||||
).insert()
|
||||
payment_term2 = frappe.get_doc(
|
||||
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
|
||||
).insert()
|
||||
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "_Test 50-50",
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term": payment_term1.name,
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 15,
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term": payment_term2.name,
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 30,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
template.insert()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"based_on_payment_terms": 1,
|
||||
"payment_terms_template": template.name,
|
||||
"ageing_based_on": "Posting Date",
|
||||
}
|
||||
|
||||
pi = self.create_purchase_invoice(do_not_submit=True)
|
||||
pi.payment_terms_template = template.name
|
||||
schedule = get_payment_terms(template.name)
|
||||
pi.set("payment_schedule", [])
|
||||
|
||||
for row in schedule:
|
||||
row["due_date"] = add_days(pi.posting_date, row.get("credit_days", 0))
|
||||
pi.append("payment_schedule", row)
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
self.assertEqual([pi.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||
|
||||
@@ -1029,9 +1029,8 @@ class ReceivablePayableReport:
|
||||
self,
|
||||
):
|
||||
self.customer = qb.DocType("Customer")
|
||||
|
||||
if self.filters.get("customer_group"):
|
||||
groups = get_customer_group_with_children(self.filters.customer_group)
|
||||
groups = get_party_group_with_children("Customer", self.filters.customer_group)
|
||||
customers = (
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
@@ -1043,14 +1042,18 @@ class ReceivablePayableReport:
|
||||
self.get_hierarchical_filters("Territory", "territory")
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
|
||||
)
|
||||
customer_ptt = self.ple.party.isin(
|
||||
qb.from_(self.customer)
|
||||
.select(self.customer.name)
|
||||
.where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
|
||||
)
|
||||
|
||||
si_ptt = self.add_payment_term_template_filters("Sales Invoice")
|
||||
|
||||
sales_ptt = self.ple.against_voucher_no.isin(si_ptt)
|
||||
|
||||
self.qb_selection_filter.append(Criterion.any([customer_ptt, sales_ptt]))
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
@@ -1075,14 +1078,53 @@ class ReceivablePayableReport:
|
||||
)
|
||||
|
||||
if self.filters.get("payment_terms_template"):
|
||||
self.qb_selection_filter.append(
|
||||
self.ple.party.isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.payment_terms == self.filters.get("supplier_group"))
|
||||
)
|
||||
supplier_ptt = self.ple.party.isin(
|
||||
qb.from_(supplier)
|
||||
.select(supplier.name)
|
||||
.where(supplier.payment_terms == self.filters.get("payment_terms_template"))
|
||||
)
|
||||
|
||||
pi_ptt = self.add_payment_term_template_filters("Purchase Invoice")
|
||||
|
||||
purchase_ptt = self.ple.against_voucher_no.isin(pi_ptt)
|
||||
|
||||
self.qb_selection_filter.append(Criterion.any([supplier_ptt, purchase_ptt]))
|
||||
|
||||
def add_payment_term_template_filters(self, dtype):
|
||||
voucher_type = qb.DocType(dtype)
|
||||
|
||||
ptt = (
|
||||
qb.from_(voucher_type)
|
||||
.select(voucher_type.name)
|
||||
.where(voucher_type.payment_terms_template == self.filters.get("payment_terms_template"))
|
||||
.where(voucher_type.company == self.filters.company)
|
||||
)
|
||||
|
||||
if dtype == "Purchase Invoice":
|
||||
party = "Supplier"
|
||||
party_group_type = "supplier_group"
|
||||
acc_type = "credit_to"
|
||||
else:
|
||||
party = "Customer"
|
||||
party_group_type = "customer_group"
|
||||
acc_type = "debit_to"
|
||||
|
||||
if self.filters.get(party_group_type):
|
||||
party_groups = get_party_group_with_children(party, self.filters.get(party_group_type))
|
||||
ptt = ptt.where((voucher_type[party_group_type]).isin(party_groups))
|
||||
|
||||
if self.filters.party:
|
||||
ptt = ptt.where((voucher_type[party.lower()]).isin(self.filters.party))
|
||||
|
||||
if self.filters.cost_center:
|
||||
cost_centers = get_cost_centers_with_children(self.filters.cost_center)
|
||||
ptt = ptt.where(voucher_type.cost_center.isin(cost_centers))
|
||||
|
||||
if self.filters.party_account:
|
||||
ptt = ptt.where(voucher_type[acc_type] == self.filters.party_account)
|
||||
|
||||
return ptt
|
||||
|
||||
def get_hierarchical_filters(self, doctype, key):
|
||||
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
|
||||
|
||||
@@ -1320,20 +1362,26 @@ class ReceivablePayableReport:
|
||||
self.err_journals = [x[0] for x in results] if results else []
|
||||
|
||||
|
||||
def get_customer_group_with_children(customer_groups):
|
||||
if not isinstance(customer_groups, list):
|
||||
customer_groups = [d.strip() for d in customer_groups.strip().split(",") if d]
|
||||
def get_party_group_with_children(party, party_groups):
|
||||
if party not in ("Customer", "Supplier"):
|
||||
return []
|
||||
|
||||
all_customer_groups = []
|
||||
for d in customer_groups:
|
||||
if frappe.db.exists("Customer Group", d):
|
||||
lft, rgt = frappe.db.get_value("Customer Group", d, ["lft", "rgt"])
|
||||
children = frappe.get_all("Customer Group", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
|
||||
all_customer_groups += [c.name for c in children]
|
||||
group_dtype = f"{party} Group"
|
||||
if not isinstance(party_groups, list):
|
||||
party_groups = [d.strip() for d in party_groups.strip().split(",") if d]
|
||||
|
||||
all_party_groups = []
|
||||
for d in party_groups:
|
||||
if frappe.db.exists(group_dtype, d):
|
||||
lft, rgt = frappe.db.get_value(group_dtype, d, ["lft", "rgt"])
|
||||
children = frappe.get_all(
|
||||
group_dtype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, pluck="name"
|
||||
)
|
||||
all_party_groups += children
|
||||
else:
|
||||
frappe.throw(_("Customer Group: {0} does not exist").format(d))
|
||||
frappe.throw(_("{0}: {1} does not exist").format(group_dtype, d))
|
||||
|
||||
return list(set(all_customer_groups))
|
||||
return list(set(all_party_groups))
|
||||
|
||||
|
||||
class InitSQLProceduresForAR:
|
||||
|
||||
@@ -1139,3 +1139,66 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(len(report[1]), 1)
|
||||
row = report[1][0]
|
||||
self.assertEqual(expected_data_after_payment, [row.voucher_no, row.cost_center, row.outstanding])
|
||||
|
||||
def test_payment_terms_template_filters(self):
|
||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||
|
||||
payment_term1 = frappe.get_doc(
|
||||
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 15 Days"}
|
||||
).insert()
|
||||
payment_term2 = frappe.get_doc(
|
||||
{"doctype": "Payment Term", "payment_term_name": "_Test 50% on 30 Days"}
|
||||
).insert()
|
||||
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "_Test 50-50",
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term": payment_term1.name,
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 15,
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term": payment_term2.name,
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 30,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
template.insert()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range": "30, 60, 90, 120",
|
||||
"based_on_payment_terms": 1,
|
||||
"payment_terms_template": template.name,
|
||||
"ageing_based_on": "Posting Date",
|
||||
}
|
||||
|
||||
si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si.payment_terms_template = template.name
|
||||
schedule = get_payment_terms(template.name)
|
||||
si.set("payment_schedule", [])
|
||||
|
||||
for row in schedule:
|
||||
row["due_date"] = add_days(si.posting_date, row.get("credit_days", 0))
|
||||
si.append("payment_schedule", row)
|
||||
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
report = execute(filters)
|
||||
row = report[1][0]
|
||||
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
self.assertEqual([si.name, payment_term1.payment_term_name], [row.voucher_no, row.payment_term])
|
||||
|
||||
@@ -96,7 +96,7 @@ def execute(filters=None):
|
||||
filters.periodicity, period_list, filters.accumulated_values, company=filters.company
|
||||
)
|
||||
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity, currency)
|
||||
chart = get_chart_data(filters, period_list, asset, liability, equity, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
@@ -225,18 +225,19 @@ def get_report_summary(
|
||||
], (net_asset - net_liability + net_equity)
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, asset, liability, equity, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
def get_chart_data(filters, chart_columns, asset, liability, equity, currency):
|
||||
labels = [col.get("label") for col in chart_columns]
|
||||
|
||||
asset_data, liability_data, equity_data = [], [], []
|
||||
|
||||
for p in columns[2:]:
|
||||
for col in chart_columns:
|
||||
key = col.get("key") or col.get("fieldname")
|
||||
if asset:
|
||||
asset_data.append(asset[-2].get(p.get("fieldname")))
|
||||
asset_data.append(asset[-2].get(key))
|
||||
if liability:
|
||||
liability_data.append(liability[-2].get(p.get("fieldname")))
|
||||
liability_data.append(liability[-2].get(key))
|
||||
if equity:
|
||||
equity_data.append(equity[-2].get(p.get("fieldname")))
|
||||
equity_data.append(equity[-2].get(key))
|
||||
|
||||
datasets = []
|
||||
if asset_data:
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import getdate, nowdate
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import getdate
|
||||
from pypika import Order
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -48,17 +51,6 @@ def get_columns():
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
|
||||
if filters.get("from_date"):
|
||||
conditions += " and posting_date>=%(from_date)s"
|
||||
if filters.get("to_date"):
|
||||
conditions += " and posting_date<=%(to_date)s"
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_entries(filters):
|
||||
entries = []
|
||||
|
||||
@@ -73,41 +65,90 @@ def get_entries(filters):
|
||||
|
||||
return sorted(
|
||||
entries,
|
||||
key=lambda k: k[2].strftime("%H%M%S") or getdate(nowdate()),
|
||||
key=lambda k: getdate(k[2]),
|
||||
)
|
||||
|
||||
|
||||
def get_entries_for_bank_clearance_summary(filters):
|
||||
entries = []
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
journal_entries = frappe.db.sql(
|
||||
f"""SELECT
|
||||
"Journal Entry", jv.name, jv.posting_date, jv.cheque_no,
|
||||
jv.clearance_date, jvd.against_account, jvd.debit - jvd.credit
|
||||
FROM
|
||||
`tabJournal Entry Account` jvd, `tabJournal Entry` jv
|
||||
WHERE
|
||||
jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {conditions}
|
||||
order by posting_date DESC, jv.name DESC""",
|
||||
filters,
|
||||
as_list=1,
|
||||
)
|
||||
journal_entries = (
|
||||
frappe.qb.from_(jea)
|
||||
.inner_join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("payment_document"),
|
||||
je.name.as_("payment_entry"),
|
||||
je.posting_date,
|
||||
je.cheque_no,
|
||||
je.clearance_date,
|
||||
jea.against_account,
|
||||
jea.debit_in_account_currency - jea.credit_in_account_currency,
|
||||
)
|
||||
.where(
|
||||
(jea.account == filters.account)
|
||||
& (je.docstatus == 1)
|
||||
& (je.posting_date >= filters.from_date)
|
||||
& (je.posting_date <= filters.to_date)
|
||||
& ((je.is_opening == "No") | (je.is_opening.isnull()))
|
||||
)
|
||||
.orderby(je.posting_date, order=Order.desc)
|
||||
.orderby(je.name, order=Order.desc)
|
||||
).run(as_list=True)
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
f"""SELECT
|
||||
"Payment Entry", name, posting_date, reference_no, clearance_date, party,
|
||||
if(paid_from=%(account)s, ((paid_amount * -1) - total_taxes_and_charges) , received_amount)
|
||||
FROM
|
||||
`tabPayment Entry`
|
||||
WHERE
|
||||
docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {conditions}
|
||||
order by posting_date DESC, name DESC""",
|
||||
filters,
|
||||
as_list=1,
|
||||
)
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
payment_entries = (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("payment_document"),
|
||||
pe.name.as_("payment_entry"),
|
||||
pe.posting_date,
|
||||
pe.reference_no.as_("cheque_no"),
|
||||
pe.clearance_date,
|
||||
pe.party.as_("against_account"),
|
||||
Case()
|
||||
.when(
|
||||
(pe.paid_from == filters.account),
|
||||
((pe.paid_amount * -1) - pe.total_taxes_and_charges),
|
||||
)
|
||||
.else_(pe.received_amount),
|
||||
)
|
||||
.where((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
|
||||
.where(
|
||||
(pe.docstatus == 1)
|
||||
& (pe.posting_date >= filters.from_date)
|
||||
& (pe.posting_date <= filters.to_date)
|
||||
)
|
||||
.orderby(pe.posting_date, order=Order.desc)
|
||||
.orderby(pe.name, order=Order.desc)
|
||||
).run(as_list=True)
|
||||
|
||||
entries = journal_entries + payment_entries
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
purchase_invoices = (
|
||||
frappe.qb.from_(pi)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.posting_date,
|
||||
pi.bill_no.as_("cheque_no"),
|
||||
pi.clearance_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
(pi.paid_amount * -1).as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == filters.account)
|
||||
& (pi.posting_date >= filters.from_date)
|
||||
& (pi.posting_date <= filters.to_date)
|
||||
)
|
||||
.orderby(pi.posting_date, order=Order.desc)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
).run(as_list=True)
|
||||
|
||||
entries = journal_entries + payment_entries + purchase_invoices
|
||||
|
||||
return entries
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika import Order
|
||||
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
|
||||
@@ -123,73 +127,143 @@ def get_entries_for_bank_reconciliation_statement(filters):
|
||||
|
||||
payment_entries = get_payment_entries(filters)
|
||||
|
||||
purchase_invoices = get_purchase_invoices(filters)
|
||||
|
||||
pos_entries = []
|
||||
if filters.include_pos_transactions:
|
||||
pos_entries = get_pos_entries(filters)
|
||||
|
||||
return list(journal_entries) + list(payment_entries) + list(pos_entries)
|
||||
return list(journal_entries) + list(payment_entries) + list(pos_entries) + list(purchase_invoices)
|
||||
|
||||
|
||||
def get_journal_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select "Journal Entry" as payment_document, jv.posting_date,
|
||||
jv.name as payment_entry, jvd.debit_in_account_currency as debit,
|
||||
jvd.credit_in_account_currency as credit, jvd.against_account,
|
||||
jv.cheque_no as reference_no, jv.cheque_date as ref_date, jv.clearance_date, jvd.account_currency
|
||||
from
|
||||
`tabJournal Entry Account` jvd, `tabJournal Entry` jv
|
||||
where jvd.parent = jv.name and jv.docstatus=1
|
||||
and jvd.account = %(account)s and jv.posting_date <= %(report_date)s
|
||||
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
|
||||
and ifnull(jv.is_opening, 'No') = 'No'
|
||||
and jv.company = %(company)s """,
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
return (
|
||||
frappe.qb.from_(jea)
|
||||
.join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("payment_document"),
|
||||
je.name.as_("payment_entry"),
|
||||
je.posting_date,
|
||||
jea.debit_in_account_currency.as_("debit"),
|
||||
jea.credit_in_account_currency.as_("credit"),
|
||||
jea.against_account,
|
||||
je.cheque_no.as_("reference_no"),
|
||||
je.cheque_date.as_("ref_date"),
|
||||
je.clearance_date,
|
||||
jea.account_currency,
|
||||
)
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (jea.account == filters.account)
|
||||
& (je.posting_date <= filters.report_date)
|
||||
& (je.clearance_date.isnull() | (je.clearance_date > filters.report_date))
|
||||
& (je.company == filters.company)
|
||||
& ((je.is_opening.isnull()) | (je.is_opening == "No"))
|
||||
)
|
||||
.orderby(je.posting_date)
|
||||
.orderby(je.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_payment_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Payment Entry" as payment_document, name as payment_entry,
|
||||
reference_no, reference_date as ref_date,
|
||||
if(paid_to=%(account)s, received_amount_after_tax, 0) as debit,
|
||||
if(paid_from=%(account)s, paid_amount_after_tax, 0) as credit,
|
||||
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
|
||||
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date <= %(report_date)s
|
||||
and ifnull(clearance_date, '4000-01-01') > %(report_date)s
|
||||
and company = %(company)s
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
return (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("payment_document"),
|
||||
pe.name.as_("payment_entry"),
|
||||
pe.reference_no.as_("reference_no"),
|
||||
pe.reference_date.as_("ref_date"),
|
||||
Case().when(pe.paid_to == filters.account, pe.received_amount_after_tax).else_(0).as_("debit"),
|
||||
Case().when(pe.paid_from == filters.account, pe.paid_amount_after_tax).else_(0).as_("credit"),
|
||||
pe.posting_date,
|
||||
Coalesce(
|
||||
pe.party, Case().when(pe.paid_from == filters.account, pe.paid_to).else_(pe.paid_from)
|
||||
).as_("against_account"),
|
||||
pe.clearance_date,
|
||||
(
|
||||
Case()
|
||||
.when(pe.paid_to == filters.account, pe.paid_to_account_currency)
|
||||
.else_(pe.paid_from_account_currency)
|
||||
).as_("account_currency"),
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus == 1)
|
||||
& ((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
|
||||
& (pe.posting_date <= filters.report_date)
|
||||
& (pe.clearance_date.isnull() | (pe.clearance_date > filters.report_date))
|
||||
& (pe.company == filters.company)
|
||||
)
|
||||
.orderby(pe.posting_date)
|
||||
.orderby(pe.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_purchase_invoices(filters):
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
return (
|
||||
frappe.qb.from_(pi)
|
||||
.inner_join(acc)
|
||||
.on(pi.cash_bank_account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Purchase Invoice").as_("payment_document"),
|
||||
pi.name.as_("payment_entry"),
|
||||
pi.bill_no.as_("reference_no"),
|
||||
pi.posting_date.as_("ref_date"),
|
||||
Case().when(pi.paid_amount < 0, pi.paid_amount * -1).else_(0).as_("debit"),
|
||||
Case().when(pi.paid_amount > 0, pi.paid_amount).else_(0).as_("credit"),
|
||||
pi.posting_date,
|
||||
pi.supplier.as_("against_account"),
|
||||
pi.clearance_date,
|
||||
acc.account_currency,
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == filters.account)
|
||||
& (pi.posting_date <= filters.report_date)
|
||||
& (pi.clearance_date.isnull() | (pi.clearance_date > filters.report_date))
|
||||
& (pi.company == filters.company)
|
||||
)
|
||||
.orderby(pi.posting_date)
|
||||
.orderby(pi.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_pos_entries(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
|
||||
si.posting_date, si.debit_to as against_account, sip.clearance_date,
|
||||
account.account_currency, 0 as credit
|
||||
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
|
||||
where
|
||||
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
|
||||
and account.name = sip.account and si.posting_date <= %(report_date)s and
|
||||
ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s
|
||||
and si.company = %(company)s
|
||||
order by
|
||||
si.posting_date ASC, si.name DESC
|
||||
""",
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
si = frappe.qb.DocType("Sales Invoice")
|
||||
si_payment = frappe.qb.DocType("Sales Invoice Payment")
|
||||
acc = frappe.qb.DocType("Account")
|
||||
return (
|
||||
frappe.qb.from_(si_payment)
|
||||
.join(si)
|
||||
.on(si_payment.parent == si.name)
|
||||
.join(acc)
|
||||
.on(si_payment.account == acc.name)
|
||||
.select(
|
||||
ConstantColumn("Sales Invoice").as_("payment_document"),
|
||||
si.name.as_("payment_entry"),
|
||||
si_payment.amount.as_("debit"),
|
||||
si.posting_date,
|
||||
si.debit_to.as_("against_account"),
|
||||
si_payment.clearance_date,
|
||||
acc.account_currency,
|
||||
ConstantColumn(0).as_("credit"),
|
||||
)
|
||||
.where(
|
||||
(si_payment.account == filters.account)
|
||||
& (si.docstatus == 1)
|
||||
& (si.posting_date <= filters.report_date)
|
||||
& (si_payment.clearance_date.isnull() | (si_payment.clearance_date > filters.report_date))
|
||||
& (si.company == filters.company)
|
||||
)
|
||||
.orderby(si.posting_date)
|
||||
.orderby(si_payment.name, order=Order.desc)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system(filters):
|
||||
@@ -205,30 +279,66 @@ def get_amounts_not_reflected_in_system(filters):
|
||||
|
||||
|
||||
def get_amounts_not_reflected_in_system_for_bank_reconciliation_statement(filters):
|
||||
je_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency)
|
||||
from `tabJournal Entry Account` jvd, `tabJournal Entry` jv
|
||||
where jvd.parent = jv.name and jv.docstatus=1 and jvd.account=%(account)s
|
||||
and jv.posting_date > %(report_date)s and jv.clearance_date <= %(report_date)s
|
||||
and ifnull(jv.is_opening, 'No') = 'No' """,
|
||||
filters,
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
|
||||
je_amount = (
|
||||
frappe.qb.from_(jea)
|
||||
.inner_join(je)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (jea.account == filters.account)
|
||||
& (je.posting_date > filters.report_date)
|
||||
& (je.clearance_date <= filters.report_date)
|
||||
& (je.company == filters.company)
|
||||
& ((je.is_opening.isnull()) | (je.is_opening == "No"))
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
je_amount = flt(je_amount[0].amount) if je_amount else 0.0
|
||||
|
||||
je_amount = flt(je_amount[0][0]) if je_amount else 0.0
|
||||
|
||||
pe_amount = frappe.db.sql(
|
||||
"""
|
||||
select sum(if(paid_from=%(account)s, paid_amount, received_amount))
|
||||
from `tabPayment Entry`
|
||||
where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
|
||||
and posting_date > %(report_date)s and clearance_date <= %(report_date)s""",
|
||||
filters,
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
pe_amount = (
|
||||
frappe.qb.from_(pe)
|
||||
.select(
|
||||
Sum(Case().when(pe.paid_from == filters.account, pe.paid_amount).else_(pe.received_amount)).as_(
|
||||
"amount"
|
||||
),
|
||||
)
|
||||
.where(
|
||||
((pe.paid_from == filters.account) | (pe.paid_to == filters.account))
|
||||
& (pe.docstatus == 1)
|
||||
& (pe.posting_date > filters.report_date)
|
||||
& (pe.clearance_date <= filters.report_date)
|
||||
& (pe.company == filters.company)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
pe_amount = flt(pe_amount[0].amount) if pe_amount else 0.0
|
||||
|
||||
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
|
||||
pi = frappe.qb.DocType("Purchase Invoice")
|
||||
pi_amount = (
|
||||
frappe.qb.from_(pi)
|
||||
.select(
|
||||
Sum(pi.paid_amount).as_("amount"),
|
||||
)
|
||||
.where(
|
||||
(pi.docstatus == 1)
|
||||
& (pi.is_paid == 1)
|
||||
& (pi.cash_bank_account == filters.account)
|
||||
& (pi.posting_date > filters.report_date)
|
||||
& (pi.clearance_date <= filters.report_date)
|
||||
& (pi.company == filters.company)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return je_amount + pe_amount
|
||||
pi_amount = flt(pi_amount[0].amount) if pi_amount else 0.0
|
||||
|
||||
return je_amount + pe_amount + pi_amount
|
||||
|
||||
|
||||
def get_balance_row(label, amount, account_currency):
|
||||
|
||||
@@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, formatdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.controllers.trends import get_period_date_ranges, get_period_month_ranges
|
||||
|
||||
|
||||
@@ -15,6 +16,8 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
if filters.get("budget_against_filter"):
|
||||
dimensions = filters.get("budget_against_filter")
|
||||
@@ -35,6 +38,21 @@ def execute(filters=None):
|
||||
return columns, data, None, chart
|
||||
|
||||
|
||||
def validate_filters(filters):
|
||||
validate_budget_dimensions(filters)
|
||||
|
||||
|
||||
def validate_budget_dimensions(filters):
|
||||
dimensions = [d.get("document_type") for d in get_dimensions(with_cost_center_and_project=True)[0]]
|
||||
if filters.get("budget_against") and filters.get("budget_against") not in dimensions:
|
||||
frappe.throw(
|
||||
title=_("Invalid Accounting Dimension"),
|
||||
msg=_("{0} is not a valid Accounting Dimension.").format(
|
||||
frappe.bold(filters.get("budget_against"))
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation):
|
||||
for account, monthwise_data in dimension_items.items():
|
||||
row = [dimension, account]
|
||||
|
||||
@@ -139,7 +139,7 @@ def execute(filters=None):
|
||||
True,
|
||||
)
|
||||
|
||||
chart = get_chart_data(columns, data, company_currency)
|
||||
chart = get_chart_data(period_list, data, company_currency)
|
||||
|
||||
report_summary = get_report_summary(summary_data, company_currency)
|
||||
|
||||
@@ -411,13 +411,12 @@ def get_report_summary(summary_data, currency):
|
||||
return report_summary
|
||||
|
||||
|
||||
def get_chart_data(columns, data, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
print(data)
|
||||
def get_chart_data(period_list, data, currency):
|
||||
labels = [period.get("label") for period in period_list]
|
||||
datasets = [
|
||||
{
|
||||
"name": section.get("section").replace("'", ""),
|
||||
"values": [section.get(d.get("fieldname")) for d in columns[2:]],
|
||||
"values": [section.get(period.get("key")) for period in period_list],
|
||||
}
|
||||
for section in data
|
||||
if section.get("parent_section") is None and section.get("currency")
|
||||
|
||||
@@ -48,22 +48,25 @@ def execute(filters=None):
|
||||
return columns, data, message, chart
|
||||
|
||||
fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year"))
|
||||
companies_column, companies = get_companies(filters)
|
||||
columns = get_columns(companies_column, filters)
|
||||
company_list, companies = get_companies(filters)
|
||||
company_columns = get_company_columns(company_list, filters)
|
||||
columns = get_columns(company_columns)
|
||||
|
||||
if filters.get("report") == "Balance Sheet":
|
||||
data, message, chart, report_summary = get_balance_sheet_data(
|
||||
fiscal_year, companies, columns, filters
|
||||
fiscal_year, companies, company_columns, filters
|
||||
)
|
||||
elif filters.get("report") == "Profit and Loss Statement":
|
||||
data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters)
|
||||
data, message, chart, report_summary = get_profit_loss_data(
|
||||
fiscal_year, companies, company_columns, filters
|
||||
)
|
||||
else:
|
||||
data, report_summary = get_cash_flow_data(fiscal_year, companies, filters)
|
||||
|
||||
return columns, data, message, chart, report_summary
|
||||
|
||||
|
||||
def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
def get_balance_sheet_data(fiscal_year, companies, company_columns, filters):
|
||||
asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters)
|
||||
|
||||
liability = get_data(companies, "Liability", "Credit", fiscal_year, filters=filters)
|
||||
@@ -116,7 +119,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
|
||||
True,
|
||||
)
|
||||
|
||||
chart = get_chart_data(filters, columns, asset, liability, equity, company_currency)
|
||||
chart = get_chart_data(filters, company_columns, asset, liability, equity, company_currency)
|
||||
|
||||
return data, message, chart, report_summary
|
||||
|
||||
@@ -164,7 +167,7 @@ def get_root_account_name(root_type, company):
|
||||
return root_account[0][0]
|
||||
|
||||
|
||||
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
def get_profit_loss_data(fiscal_year, companies, company_columns, filters):
|
||||
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
|
||||
company_currency = get_company_currency(filters)
|
||||
|
||||
@@ -174,7 +177,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||
if net_profit_loss:
|
||||
data.append(net_profit_loss)
|
||||
|
||||
chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss, company_currency)
|
||||
chart = get_pl_chart_data(filters, company_columns, income, expense, net_profit_loss, company_currency)
|
||||
|
||||
report_summary, primitive_summary = get_pl_summary(
|
||||
companies, "", income, expense, net_profit_loss, company_currency, filters, True
|
||||
@@ -279,7 +282,30 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
|
||||
return data
|
||||
|
||||
|
||||
def get_columns(companies, filters):
|
||||
def get_company_columns(companies, filters):
|
||||
company_columns = []
|
||||
for company in companies:
|
||||
apply_currency_formatter = 1 if not filters.presentation_currency else 0
|
||||
currency = filters.presentation_currency
|
||||
if not currency:
|
||||
currency = erpnext.get_company_currency(company)
|
||||
|
||||
company_columns.append(
|
||||
{
|
||||
"fieldname": company,
|
||||
"label": f"{company} ({currency})",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150,
|
||||
"apply_currency_formatter": apply_currency_formatter,
|
||||
"company_name": company,
|
||||
}
|
||||
)
|
||||
|
||||
return company_columns
|
||||
|
||||
|
||||
def get_columns(company_columns):
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "account",
|
||||
@@ -297,23 +323,7 @@ def get_columns(companies, filters):
|
||||
},
|
||||
]
|
||||
|
||||
for company in companies:
|
||||
apply_currency_formatter = 1 if not filters.presentation_currency else 0
|
||||
currency = filters.presentation_currency
|
||||
if not currency:
|
||||
currency = erpnext.get_company_currency(company)
|
||||
|
||||
columns.append(
|
||||
{
|
||||
"fieldname": company,
|
||||
"label": f"{company} ({currency})",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150,
|
||||
"apply_currency_formatter": apply_currency_formatter,
|
||||
"company_name": company,
|
||||
}
|
||||
)
|
||||
columns.extend(company_columns)
|
||||
|
||||
return columns
|
||||
|
||||
|
||||
@@ -48,6 +48,9 @@ class Deferred_Item:
|
||||
Generate report data for output
|
||||
"""
|
||||
ret_data = frappe._dict({"name": self.item_name})
|
||||
ret_data.service_start_date = self.service_start_date
|
||||
ret_data.service_end_date = self.service_end_date
|
||||
ret_data.amount = self.base_net_amount
|
||||
for period in self.period_total:
|
||||
ret_data[period.key] = period.total
|
||||
ret_data.indent = 1
|
||||
@@ -205,6 +208,9 @@ class Deferred_Invoice:
|
||||
for item in self.uniq_items:
|
||||
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
|
||||
|
||||
# roll-up amount from all deferred items
|
||||
self.amount_total = sum(item.base_net_amount for item in self.items)
|
||||
|
||||
def calculate_invoice_revenue_expense_for_period(self):
|
||||
"""
|
||||
calculate deferred revenue/expense for all items in invoice
|
||||
@@ -232,7 +238,7 @@ class Deferred_Invoice:
|
||||
generate report data for invoice, includes invoice total
|
||||
"""
|
||||
ret_data = []
|
||||
inv_total = frappe._dict({"name": self.name})
|
||||
inv_total = frappe._dict({"name": self.name, "amount": self.amount_total})
|
||||
for x in self.period_total:
|
||||
inv_total[x.key] = x.total
|
||||
inv_total.indent = 0
|
||||
@@ -386,6 +392,24 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
def get_columns(self):
|
||||
columns = []
|
||||
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Service Start Date"),
|
||||
"fieldname": "service_start_date",
|
||||
"fieldtype": "Date",
|
||||
"read_only": 1,
|
||||
}
|
||||
)
|
||||
columns.append(
|
||||
{
|
||||
"label": _("Service End Date"),
|
||||
"fieldname": "service_end_date",
|
||||
"fieldtype": "Date",
|
||||
"read_only": 1,
|
||||
}
|
||||
)
|
||||
columns.append({"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "read_only": 1})
|
||||
|
||||
for period in self.period_list:
|
||||
columns.append(
|
||||
{
|
||||
@@ -415,6 +439,8 @@ class Deferred_Revenue_and_Expense_Report:
|
||||
elif self.filters.type == "Expense":
|
||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||
|
||||
total_row["amount"] = sum(inv.amount_total for inv in self.deferred_invoices)
|
||||
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
total_row[period.key] = self.period_total[idx].total
|
||||
ret.append(total_row)
|
||||
|
||||
@@ -37,6 +37,20 @@ function get_filters() {
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "party_type",
|
||||
label: __("Party Type"),
|
||||
fieldtype: "Link",
|
||||
options: "Party Type",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
fieldname: "party",
|
||||
label: __("Party"),
|
||||
fieldtype: "Dynamic Link",
|
||||
options: "party_type",
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
fieldname: "voucher_no",
|
||||
label: __("Voucher No"),
|
||||
|
||||
@@ -68,6 +68,12 @@ class General_Payment_Ledger_Comparison:
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
if self.filters.party_type:
|
||||
filter_criterion.append(gle.party_type.eq(self.filters.party_type))
|
||||
|
||||
if self.filters.party:
|
||||
filter_criterion.append(gle.party.eq(self.filters.party))
|
||||
|
||||
if acc_type == "receivable":
|
||||
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
|
||||
else:
|
||||
@@ -111,6 +117,12 @@ class General_Payment_Ledger_Comparison:
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
if self.filters.party_type:
|
||||
filter_criterion.append(ple.party_type.eq(self.filters.party_type))
|
||||
|
||||
if self.filters.party:
|
||||
filter_criterion.append(ple.party.eq(self.filters.party))
|
||||
|
||||
self.account_types[acc_type].ple = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
|
||||
@@ -649,7 +649,7 @@ class GrossProfitGenerator:
|
||||
new_row = row
|
||||
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
|
||||
else:
|
||||
new_row.qty += flt(row.qty)
|
||||
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
|
||||
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
|
||||
|
||||
new_row = self.set_average_rate(new_row)
|
||||
@@ -659,11 +659,17 @@ class GrossProfitGenerator:
|
||||
if i == 0:
|
||||
new_row = row
|
||||
else:
|
||||
new_row.qty += flt(row.qty)
|
||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||
new_row.qty = flt((new_row.qty + row.qty), self.float_precision)
|
||||
new_row.buying_amount = flt(
|
||||
(new_row.buying_amount + row.buying_amount), self.currency_precision
|
||||
)
|
||||
new_row.base_amount = flt(
|
||||
(new_row.base_amount + row.base_amount), self.currency_precision
|
||||
)
|
||||
if self.filters.get("group_by") == "Sales Person":
|
||||
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
|
||||
new_row.allocated_amount = flt(
|
||||
(new_row.allocated_amount + row.allocated_amount), self.currency_precision
|
||||
)
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
|
||||
|
||||
@@ -439,6 +439,7 @@ class TestGrossProfit(FrappeTestCase):
|
||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
sinv.is_return = 1
|
||||
sinv.items[0].allow_zero_valuation_rate = 1
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
|
||||
@@ -31,6 +31,7 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
|
||||
item_list = get_items(filters, additional_table_columns)
|
||||
aii_account_map = get_aii_accounts()
|
||||
default_taxes = {}
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(
|
||||
item_list,
|
||||
@@ -39,6 +40,9 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
doctype="Purchase Invoice",
|
||||
tax_doctype="Purchase Taxes and Charges",
|
||||
)
|
||||
for tax in tax_columns:
|
||||
default_taxes[f"{tax}_rate"] = 0
|
||||
default_taxes[f"{tax}_amount"] = 0
|
||||
|
||||
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
|
||||
|
||||
@@ -85,6 +89,8 @@ def _execute(filters=None, additional_table_columns=None):
|
||||
}
|
||||
|
||||
total_tax = 0
|
||||
row.update(default_taxes.copy())
|
||||
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
|
||||
@@ -29,8 +29,12 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
item_list = get_items(filters, additional_table_columns, additional_conditions)
|
||||
default_taxes = {}
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
for tax in tax_columns:
|
||||
default_taxes[f"{tax}_rate"] = 0
|
||||
default_taxes[f"{tax}_amount"] = 0
|
||||
|
||||
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)
|
||||
@@ -88,6 +92,8 @@ def _execute(filters=None, additional_table_columns=None, additional_conditions=
|
||||
|
||||
total_tax = 0
|
||||
total_other_charges = 0
|
||||
row.update(default_taxes.copy())
|
||||
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
|
||||
@@ -17,9 +17,11 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
def create_sales_invoice(self, item=None, taxes=None, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
item=item or self.item,
|
||||
item_name=item or self.item,
|
||||
description=item or self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
@@ -30,6 +32,19 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
price_list_rate=100,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
for tax in taxes or []:
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": tax["account_head"],
|
||||
"cost_center": self.cost_center,
|
||||
"description": tax["description"],
|
||||
"rate": tax["rate"],
|
||||
},
|
||||
)
|
||||
|
||||
si = si.save()
|
||||
if not do_not_submit:
|
||||
si = si.submit()
|
||||
@@ -63,3 +78,50 @@ class TestItemWiseSalesRegister(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
report_output = {k: v for k, v in report[1][0].items() if k in expected_result}
|
||||
self.assertDictEqual(report_output, expected_result)
|
||||
|
||||
def test_grouped_report_handles_different_tax_descriptions(self):
|
||||
self.create_item(item_name="_Test Item Tax Description A")
|
||||
first_item = self.item
|
||||
self.create_item(item_name="_Test Item Tax Description B")
|
||||
second_item = self.item
|
||||
|
||||
first_tax_description = "Tax Description A"
|
||||
second_tax_description = "Tax Description B"
|
||||
first_tax_amount_field = f"{frappe.scrub(first_tax_description)}_amount"
|
||||
second_tax_amount_field = f"{frappe.scrub(second_tax_description)}_amount"
|
||||
|
||||
self.create_sales_invoice(
|
||||
item=first_item,
|
||||
taxes=[
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"description": first_tax_description,
|
||||
"rate": 5,
|
||||
}
|
||||
],
|
||||
)
|
||||
self.create_sales_invoice(
|
||||
item=second_item,
|
||||
taxes=[
|
||||
{
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"description": second_tax_description,
|
||||
"rate": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"from_date": today(),
|
||||
"to_date": today(),
|
||||
"company": self.company,
|
||||
"group_by": "Customer",
|
||||
}
|
||||
)
|
||||
_, data, _, _, _, _ = execute(filters)
|
||||
|
||||
grand_total_row = next(row for row in data if row.get("bold") and row.get("item_code") == "Total")
|
||||
|
||||
self.assertEqual(grand_total_row[first_tax_amount_field], 5.0)
|
||||
self.assertEqual(grand_total_row[second_tax_amount_field], 2.0)
|
||||
|
||||
@@ -22,7 +22,7 @@ frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
||||
fieldname: "accumulated_values",
|
||||
label: __("Accumulated Values"),
|
||||
fieldtype: "Check",
|
||||
default: 1,
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
fieldname: "include_default_book_entries",
|
||||
|
||||
@@ -62,7 +62,7 @@ def execute(filters=None):
|
||||
currency = filters.presentation_currency or frappe.get_cached_value(
|
||||
"Company", filters.company, "default_currency"
|
||||
)
|
||||
chart = get_chart_data(filters, columns, income, expense, net_profit_loss, currency)
|
||||
chart = get_chart_data(filters, period_list, income, expense, net_profit_loss, currency)
|
||||
|
||||
report_summary, primitive_summary = get_report_summary(
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
@@ -158,18 +158,20 @@ def get_net_profit_loss(income, expense, period_list, company, currency=None, co
|
||||
return net_profit_loss
|
||||
|
||||
|
||||
def get_chart_data(filters, columns, income, expense, net_profit_loss, currency):
|
||||
labels = [d.get("label") for d in columns[4:]]
|
||||
def get_chart_data(filters, chart_columns, income, expense, net_profit_loss, currency):
|
||||
labels = [col.get("label") for col in chart_columns]
|
||||
|
||||
income_data, expense_data, net_profit = [], [], []
|
||||
|
||||
for p in columns[4:]:
|
||||
for col in chart_columns:
|
||||
key = col.get("key") or col.get("fieldname")
|
||||
|
||||
if income:
|
||||
income_data.append(income[-2].get(p.get("fieldname")))
|
||||
income_data.append(income[-2].get(key))
|
||||
if expense:
|
||||
expense_data.append(expense[-2].get(p.get("fieldname")))
|
||||
expense_data.append(expense[-2].get(key))
|
||||
if net_profit_loss:
|
||||
net_profit.append(net_profit_loss.get(p.get("fieldname")))
|
||||
net_profit.append(net_profit_loss.get(key))
|
||||
|
||||
datasets = []
|
||||
if income_data:
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
@@ -33,11 +34,19 @@ def execute(filters=None):
|
||||
|
||||
def get_accounts_data(based_on, company):
|
||||
if based_on == "Cost Center":
|
||||
return frappe.db.sql(
|
||||
"""select name, parent_cost_center as parent_account, cost_center_name as account_name, lft, rgt
|
||||
from `tabCost Center` where company=%s order by name""",
|
||||
company,
|
||||
as_dict=True,
|
||||
cc = qb.DocType("Cost Center")
|
||||
return (
|
||||
qb.from_(cc)
|
||||
.select(
|
||||
cc.name,
|
||||
cc.parent_cost_center.as_("parent_account"),
|
||||
cc.cost_center_name.as_("account_name"),
|
||||
cc.lft,
|
||||
cc.rgt,
|
||||
)
|
||||
.where(cc.company.eq(company))
|
||||
.orderby(cc.name)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
elif based_on == "Project":
|
||||
return frappe.get_all("Project", fields=["name"], filters={"company": company}, order_by="name")
|
||||
@@ -206,27 +215,38 @@ def set_gl_entries_by_account(
|
||||
company, from_date, to_date, based_on, gl_entries_by_account, ignore_closing_entries=False
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
additional_conditions = []
|
||||
gl = qb.DocType("GL Entry")
|
||||
acc = qb.DocType("Account")
|
||||
|
||||
conditions = []
|
||||
conditions.append(gl.company.eq(company))
|
||||
conditions.append(gl[based_on].notnull())
|
||||
conditions.append(gl.is_cancelled.eq(0))
|
||||
|
||||
if from_date and to_date:
|
||||
conditions.append(gl.posting_date.between(from_date, to_date))
|
||||
elif from_date and not to_date:
|
||||
conditions.append(gl.posting_date.gte(from_date))
|
||||
elif not from_date and to_date:
|
||||
conditions.append(gl.posting_date.lte(to_date))
|
||||
|
||||
if ignore_closing_entries:
|
||||
additional_conditions.append("and voucher_type !='Period Closing Voucher'")
|
||||
conditions.append(gl.voucher_type.ne("Period Closing Voucher"))
|
||||
|
||||
if from_date:
|
||||
additional_conditions.append("and posting_date >= %(from_date)s")
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select posting_date, {based_on} as based_on, debit, credit,
|
||||
is_opening, (select root_type from `tabAccount` where name = account) as type
|
||||
from `tabGL Entry` where company=%(company)s
|
||||
{additional_conditions}
|
||||
and posting_date <= %(to_date)s
|
||||
and {based_on} is not null
|
||||
and is_cancelled = 0
|
||||
order by {based_on}, posting_date""".format(
|
||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
||||
),
|
||||
{"company": company, "from_date": from_date, "to_date": to_date},
|
||||
as_dict=True,
|
||||
root_subquery = qb.from_(acc).select(acc.root_type).where(acc.name.eq(gl.account))
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
.select(
|
||||
gl.posting_date,
|
||||
gl[based_on].as_("based_on"),
|
||||
gl.debit,
|
||||
gl.credit,
|
||||
gl.is_opening,
|
||||
root_subquery.as_("type"),
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(gl[based_on], gl.posting_date)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
for entry in gl_entries:
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"idx": 4,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:49.950442",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:35:39.703838",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Trends",
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.070651",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:36:13.725601",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Trends",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt, getdate
|
||||
from pypika import Tuple
|
||||
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
|
||||
@@ -19,18 +20,14 @@ def execute(filters=None):
|
||||
|
||||
validate_filters(filters)
|
||||
(
|
||||
tds_docs,
|
||||
tds_accounts,
|
||||
tax_category_map,
|
||||
journal_entry_party_map,
|
||||
net_total_map,
|
||||
) = get_tds_docs(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
|
||||
res = get_result(
|
||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map
|
||||
)
|
||||
res = get_result(filters, tds_accounts, tax_category_map, net_total_map)
|
||||
return columns, res
|
||||
|
||||
|
||||
@@ -41,27 +38,23 @@ def validate_filters(filters):
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
|
||||
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, net_total_map):
|
||||
party_map = get_party_pan_map(filters.get("party_type"))
|
||||
def get_result(filters, tds_accounts, tax_category_map, net_total_map):
|
||||
party_names = {v.party for v in net_total_map.values() if v.party}
|
||||
party_map = get_party_pan_map(filters.get("party_type"), party_names)
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
gle_map = get_gle_map(net_total_map)
|
||||
precision = get_currency_precision()
|
||||
|
||||
out = []
|
||||
entries = {}
|
||||
for name, details in gle_map.items():
|
||||
for (voucher_type, name), details in gle_map.items():
|
||||
for entry in details:
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
tax_amount, total_amount, grand_total, base_total, base_tax_withholding_net_total = 0, 0, 0, 0, 0
|
||||
tax_withholding_category, rate = None, None
|
||||
bill_no, bill_date = "", ""
|
||||
party = entry.party or entry.against
|
||||
posting_date = entry.posting_date
|
||||
voucher_type = entry.voucher_type
|
||||
|
||||
if voucher_type == "Journal Entry":
|
||||
party_list = journal_entry_party_map.get(name)
|
||||
if party_list:
|
||||
party = party_list[0]
|
||||
values = net_total_map.get((voucher_type, name))
|
||||
party = values.party if values else (entry.party or entry.against)
|
||||
|
||||
if entry.account in tds_accounts.keys():
|
||||
tax_amount += entry.credit - entry.debit
|
||||
@@ -76,29 +69,35 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
|
||||
rate = get_tax_withholding_rates(tax_rate_map.get(tax_withholding_category, []), posting_date)
|
||||
|
||||
values = net_total_map.get((voucher_type, name))
|
||||
|
||||
if values:
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
# back calculate total amount from rate and tax_amount
|
||||
base_total = min(flt(tax_amount / (rate / 100), precision=precision), values[0])
|
||||
base_total = min(
|
||||
flt(tax_amount / (rate / 100), precision=precision),
|
||||
values.base_tax_withholding_net_total,
|
||||
)
|
||||
total_amount = grand_total = base_total
|
||||
base_tax_withholding_net_total = total_amount
|
||||
|
||||
else:
|
||||
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]
|
||||
total_amount = values.base_tax_withholding_net_total
|
||||
|
||||
grand_total = values[1]
|
||||
base_total = values[2]
|
||||
grand_total = values.grand_total
|
||||
base_total = values.base_total
|
||||
base_tax_withholding_net_total = total_amount
|
||||
|
||||
if voucher_type == "Purchase Invoice":
|
||||
bill_no = values[3]
|
||||
bill_date = values[4]
|
||||
base_tax_withholding_net_total = values.base_tax_withholding_net_total
|
||||
bill_no = values.bill_no
|
||||
bill_date = values.bill_date
|
||||
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
base_tax_withholding_net_total = total_amount
|
||||
|
||||
if tax_amount:
|
||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
||||
@@ -125,6 +124,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
"base_tax_withholding_net_total": base_tax_withholding_net_total,
|
||||
"base_total": base_total,
|
||||
"tax_amount": tax_amount,
|
||||
"transaction_date": posting_date,
|
||||
@@ -141,14 +141,17 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_
|
||||
else:
|
||||
entries[key] = row
|
||||
out = list(entries.values())
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"]))
|
||||
out.sort(key=lambda x: (x["section_code"], x["transaction_date"], x["ref_no"]))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def get_party_pan_map(party_type):
|
||||
def get_party_pan_map(party_type, party_names):
|
||||
party_map = frappe._dict()
|
||||
|
||||
if not party_names:
|
||||
return party_map
|
||||
|
||||
fields = ["name", "tax_withholding_category"]
|
||||
if party_type == "Supplier":
|
||||
fields += ["supplier_type", "supplier_name"]
|
||||
@@ -158,7 +161,7 @@ def get_party_pan_map(party_type):
|
||||
if frappe.db.has_column(party_type, "pan"):
|
||||
fields.append("pan")
|
||||
|
||||
party_details = frappe.db.get_all(party_type, fields=fields)
|
||||
party_details = frappe.db.get_all(party_type, filters={"name": ("in", list(party_names))}, fields=fields)
|
||||
|
||||
for party in party_details:
|
||||
party.party_type = party_type
|
||||
@@ -167,22 +170,33 @@ def get_party_pan_map(party_type):
|
||||
return party_map
|
||||
|
||||
|
||||
def get_gle_map(documents):
|
||||
# create gle_map of the form
|
||||
# {"purchase_invoice": list of dict of all gle created for this invoice}
|
||||
def get_gle_map(net_total_map):
|
||||
if not net_total_map:
|
||||
return {}
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
voucher_pairs = list(net_total_map.keys())
|
||||
|
||||
rows = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(
|
||||
gle.credit,
|
||||
gle.debit,
|
||||
gle.account,
|
||||
gle.voucher_no,
|
||||
gle.posting_date,
|
||||
gle.voucher_type,
|
||||
gle.against,
|
||||
gle.party,
|
||||
gle.party_type,
|
||||
)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(Tuple(gle.voucher_type, gle.voucher_no).isin(voucher_pairs))
|
||||
).run(as_dict=True)
|
||||
|
||||
gle_map = {}
|
||||
|
||||
gle = frappe.db.get_all(
|
||||
"GL Entry",
|
||||
{"voucher_no": ["in", documents], "is_cancelled": 0},
|
||||
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
|
||||
)
|
||||
|
||||
for d in gle:
|
||||
if d.voucher_no not in gle_map:
|
||||
gle_map[d.voucher_no] = [d]
|
||||
else:
|
||||
gle_map[d.voucher_no].append(d)
|
||||
for d in rows:
|
||||
gle_map.setdefault((d.voucher_type, d.voucher_no), []).append(d)
|
||||
|
||||
return gle_map
|
||||
|
||||
@@ -252,14 +266,14 @@ def get_columns(filters):
|
||||
"width": 60,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"label": _("Tax Withholding Net Total"),
|
||||
"fieldname": "base_tax_withholding_net_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Base Total"),
|
||||
"fieldname": "base_total",
|
||||
"label": _("Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
@@ -270,10 +284,16 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total"),
|
||||
"label": _("Grand Total (Company Currency)"),
|
||||
"fieldname": "base_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Grand Total (Transaction Currency)"),
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
"width": 170,
|
||||
},
|
||||
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 130},
|
||||
{
|
||||
@@ -296,14 +316,9 @@ def get_columns(filters):
|
||||
|
||||
|
||||
def get_tds_docs(filters):
|
||||
tds_documents = []
|
||||
purchase_invoices = []
|
||||
sales_invoices = []
|
||||
payment_entries = []
|
||||
journal_entries = []
|
||||
vouchers = frappe._dict()
|
||||
tax_category_map = frappe._dict()
|
||||
net_total_map = frappe._dict()
|
||||
journal_entry_party_map = frappe._dict()
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
|
||||
_tds_accounts = frappe.get_all(
|
||||
@@ -322,35 +337,14 @@ def get_tds_docs(filters):
|
||||
tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True)
|
||||
|
||||
for d in tds_docs:
|
||||
if d.voucher_type == "Purchase Invoice":
|
||||
purchase_invoices.append(d.voucher_no)
|
||||
if d.voucher_type == "Sales Invoice":
|
||||
sales_invoices.append(d.voucher_no)
|
||||
elif d.voucher_type == "Payment Entry":
|
||||
payment_entries.append(d.voucher_no)
|
||||
elif d.voucher_type == "Journal Entry":
|
||||
journal_entries.append(d.voucher_no)
|
||||
vouchers.setdefault(d.voucher_type, set()).add(d.voucher_no)
|
||||
|
||||
tds_documents.append(d.voucher_no)
|
||||
|
||||
if purchase_invoices:
|
||||
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, net_total_map)
|
||||
|
||||
if sales_invoices:
|
||||
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, net_total_map)
|
||||
|
||||
if payment_entries:
|
||||
get_doc_info(payment_entries, "Payment Entry", tax_category_map, net_total_map)
|
||||
|
||||
if journal_entries:
|
||||
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
|
||||
get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
|
||||
for voucher_type, docs in vouchers.items():
|
||||
get_doc_info(docs, voucher_type, tax_category_map, net_total_map, filters)
|
||||
|
||||
return (
|
||||
tds_documents,
|
||||
tds_accounts,
|
||||
tax_category_map,
|
||||
journal_entry_party_map,
|
||||
net_total_map,
|
||||
)
|
||||
|
||||
@@ -361,11 +355,16 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
_("No {0} Accounts found for this company.").format(frappe.bold(_("Tax Withholding"))),
|
||||
title=_("Accounts Missing Error"),
|
||||
)
|
||||
|
||||
invoice_voucher = "Purchase Invoice" if filters.get("party_type") == "Supplier" else "Sales Invoice"
|
||||
voucher_types = {"Payment Entry", "Journal Entry", invoice_voucher}
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select("voucher_no", "voucher_type", "against", "party")
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(gle.voucher_type.isin(voucher_types))
|
||||
)
|
||||
|
||||
if filters.get("from_date"):
|
||||
@@ -391,25 +390,27 @@ def get_tds_docs_query(filters, bank_accounts, tds_accounts):
|
||||
return query
|
||||
|
||||
|
||||
def get_journal_entry_party_map(journal_entries):
|
||||
def get_journal_entry_party_map(journal_entries, party_type):
|
||||
journal_entry_party_map = {}
|
||||
for d in frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"parent": ("in", journal_entries),
|
||||
"party_type": ("in", ("Supplier", "Customer")),
|
||||
"party_type": party_type,
|
||||
"party": ("is", "set"),
|
||||
},
|
||||
["parent", "party"],
|
||||
):
|
||||
if d.parent not in journal_entry_party_map:
|
||||
journal_entry_party_map[d.parent] = []
|
||||
journal_entry_party_map[d.parent].append(d.party)
|
||||
journal_entry_party_map.setdefault(d.parent, []).append(d.party)
|
||||
|
||||
return journal_entry_party_map
|
||||
|
||||
|
||||
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None, filters=None):
|
||||
journal_entry_party_map = {}
|
||||
party_type = filters.get("party_type") if filters else None
|
||||
party = filters.get("party") if filters else None
|
||||
|
||||
common_fields = ["name"]
|
||||
fields_dict = {
|
||||
"Purchase Invoice": [
|
||||
@@ -419,37 +420,81 @@ def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
|
||||
"base_total",
|
||||
"bill_no",
|
||||
"bill_date",
|
||||
"supplier",
|
||||
],
|
||||
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
|
||||
"Sales Invoice": ["base_net_total", "grand_total", "base_total", "customer"],
|
||||
"Payment Entry": [
|
||||
"tax_withholding_category",
|
||||
"paid_amount",
|
||||
"paid_amount_after_tax",
|
||||
"base_paid_amount",
|
||||
"party",
|
||||
"party_type",
|
||||
],
|
||||
"Journal Entry": ["tax_withholding_category", "total_debit"],
|
||||
}
|
||||
party_field = {
|
||||
"Purchase Invoice": "supplier",
|
||||
"Sales Invoice": "customer",
|
||||
"Payment Entry": "party",
|
||||
}
|
||||
|
||||
entries = frappe.get_all(
|
||||
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
|
||||
)
|
||||
doc_filters = {"name": ("in", vouchers)}
|
||||
|
||||
if party and party_field.get(doctype):
|
||||
doc_filters[party_field[doctype]] = party
|
||||
|
||||
if doctype == "Payment Entry":
|
||||
doc_filters["party_type"] = party_type
|
||||
|
||||
entries = frappe.get_all(doctype, filters=doc_filters, fields=common_fields + fields_dict[doctype])
|
||||
|
||||
if doctype == "Journal Entry":
|
||||
journal_entry_party_map = get_journal_entry_party_map(vouchers, party_type=party_type)
|
||||
|
||||
for entry in entries:
|
||||
tax_category_map[(doctype, entry.name)] = entry.tax_withholding_category
|
||||
|
||||
value = frappe._dict(
|
||||
party=None,
|
||||
party_type=party_type,
|
||||
base_tax_withholding_net_total=0,
|
||||
grand_total=0,
|
||||
base_total=0,
|
||||
bill_no="",
|
||||
bill_date="",
|
||||
)
|
||||
|
||||
if doctype == "Purchase Invoice":
|
||||
value = [
|
||||
entry.base_tax_withholding_net_total,
|
||||
entry.grand_total,
|
||||
entry.base_total,
|
||||
entry.bill_no,
|
||||
entry.bill_date,
|
||||
]
|
||||
value.party = entry.supplier
|
||||
value.party_type = "Supplier"
|
||||
value.base_tax_withholding_net_total = entry.base_tax_withholding_net_total
|
||||
value.grand_total = entry.grand_total
|
||||
value.base_total = entry.base_total
|
||||
value.bill_no = entry.bill_no
|
||||
value.bill_date = entry.bill_date
|
||||
elif doctype == "Sales Invoice":
|
||||
value = [entry.base_net_total, entry.grand_total, entry.base_total]
|
||||
value.party = entry.customer
|
||||
value.party_type = "Customer"
|
||||
value.base_tax_withholding_net_total = entry.base_net_total
|
||||
value.grand_total = entry.grand_total
|
||||
value.base_total = entry.base_total
|
||||
elif doctype == "Payment Entry":
|
||||
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
|
||||
value.party = entry.party
|
||||
value.party_type = entry.party_type
|
||||
value.base_tax_withholding_net_total = entry.paid_amount
|
||||
value.grand_total = entry.paid_amount_after_tax
|
||||
value.base_total = entry.base_paid_amount
|
||||
else:
|
||||
value = [entry.total_debit] * 3
|
||||
party_list = journal_entry_party_map.get(entry.name, [])
|
||||
if party and party in party_list:
|
||||
value.party = party
|
||||
elif party_list:
|
||||
value.party = sorted(party_list)[0]
|
||||
value.party_type = party_type
|
||||
value.base_tax_withholding_net_total = entry.total_debit
|
||||
value.grand_total = entry.total_debit
|
||||
value.base_total = entry.total_debit
|
||||
|
||||
net_total_map[(doctype, entry.name)] = value
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
result = execute(filters)[1]
|
||||
expected_values = [
|
||||
# Check for JV totals using back calculation logic
|
||||
[jv.name, "TCS", 0.075, -10000.0, -7.5, -10000.0],
|
||||
[pe.name, "TCS", 0.075, 2550, 0.53, 2550.53],
|
||||
[si.name, "TCS", 0.075, 1000, 0.52, 1000.52],
|
||||
[jv.name, "TCS", 0.075, -10000.0, -10000.0, -7.5, -10000.0],
|
||||
[pe.name, "TCS", 0.075, 706.67, 2550.0, 0.53, 2550.53],
|
||||
[si.name, "TCS", 0.075, 693.33, 1000.0, 0.52, 1000.52],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
@@ -55,8 +55,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
frappe._dict(company="_Test Company", party_type="Supplier", from_date=today(), to_date=today())
|
||||
)[1]
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 1", 10, 5000, 500, 5500],
|
||||
[inv_2.name, "TDS - 2", 20, 5000, 1000, 6000],
|
||||
[inv_1.name, "TDS - 1", 10, 5000, 5000, 500, 5500],
|
||||
[inv_2.name, "TDS - 2", 20, 5000, 5000, 1000, 6000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
@@ -107,8 +107,8 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
)[1]
|
||||
|
||||
expected_values = [
|
||||
[inv_1.name, "TDS - 3", 10.0, 5000, 500, 4500],
|
||||
[inv_2.name, "TDS - 3", 20.0, 5000, 1000, 4000],
|
||||
[inv_1.name, "TDS - 3", 10.0, 5000, 5000, 500, 4500],
|
||||
[inv_2.name, "TDS - 3", 20.0, 5000, 5000, 1000, 4000],
|
||||
]
|
||||
self.check_expected_values(result, expected_values)
|
||||
|
||||
@@ -120,6 +120,7 @@ class TestTaxWithholdingDetails(AccountsTestMixin, FrappeTestCase):
|
||||
voucher.ref_no,
|
||||
voucher.section_code,
|
||||
voucher.rate,
|
||||
voucher.base_tax_withholding_net_total,
|
||||
voucher.base_total,
|
||||
voucher.tax_amount,
|
||||
voucher.grand_total,
|
||||
|
||||
@@ -20,16 +20,12 @@ def execute(filters=None):
|
||||
|
||||
columns = get_columns(filters)
|
||||
(
|
||||
tds_docs,
|
||||
tds_accounts,
|
||||
tax_category_map,
|
||||
journal_entry_party_map,
|
||||
invoice_total_map,
|
||||
net_total_map,
|
||||
) = get_tds_docs(filters)
|
||||
|
||||
res = get_result(
|
||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_total_map
|
||||
)
|
||||
res = get_result(filters, tds_accounts, tax_category_map, net_total_map)
|
||||
final_result = group_by_party_and_category(res, filters)
|
||||
|
||||
return columns, final_result
|
||||
@@ -128,7 +124,7 @@ def get_columns(filters):
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"label": _("Total Taxable Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
|
||||
@@ -345,7 +345,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, show_net
|
||||
prepare_opening_closing(d)
|
||||
|
||||
|
||||
def calculate_total_row(accounts, company_currency):
|
||||
def calculate_total_row(data, company_currency, show_group_accounts=True):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
@@ -362,10 +362,16 @@ def calculate_total_row(accounts, company_currency):
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
if not d.parent_account:
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
def sum_value_fields(row):
|
||||
for field in value_fields:
|
||||
total_row[field] += row[field]
|
||||
|
||||
for d in data:
|
||||
if not show_group_accounts:
|
||||
sum_value_fields(d)
|
||||
|
||||
elif show_group_accounts and not d.get("parent_account"):
|
||||
sum_value_fields(d)
|
||||
|
||||
return total_row
|
||||
|
||||
@@ -409,11 +415,13 @@ def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
|
||||
if not filters.get("show_group_accounts"):
|
||||
data = hide_group_accounts(data)
|
||||
|
||||
total_row = calculate_total_row(
|
||||
data, company_currency, show_group_accounts=filters.get("show_group_accounts")
|
||||
)
|
||||
|
||||
data.extend([{}, total_row])
|
||||
|
||||
return data
|
||||
|
||||
@@ -454,7 +454,8 @@ def _build_dimensions_dict_for_exc_gain_loss(
|
||||
dimensions_dict = frappe._dict()
|
||||
if entry and active_dimensions:
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
|
||||
if entry_dimension := entry.get(dim.fieldname):
|
||||
dimensions_dict[dim.fieldname] = entry_dimension
|
||||
return dimensions_dict
|
||||
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
company: function (frm) {
|
||||
frm.trigger("set_dynamic_labels");
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
|
||||
@@ -87,6 +88,7 @@ frappe.ui.form.on("Asset", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
frm.trigger("set_dynamic_labels");
|
||||
frappe.ui.form.trigger("Asset", "is_existing_asset");
|
||||
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
|
||||
|
||||
@@ -221,6 +223,10 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
},
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
frm.set_currency_labels(["gross_purchase_amount"], erpnext.get_currency(frm.doc.company));
|
||||
},
|
||||
|
||||
set_depr_posting_failure_alert: function (frm) {
|
||||
const alert = `
|
||||
<div class="row">
|
||||
|
||||
@@ -229,7 +229,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Net Purchase Amount",
|
||||
"mandatory_depends_on": "eval:(!doc.is_composite_asset || doc.docstatus==1)",
|
||||
"options": "Company:company:default_currency",
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval: doc.is_composite_asset"
|
||||
},
|
||||
{
|
||||
@@ -597,7 +597,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2025-12-23 16:01:10.195932",
|
||||
"modified": "2026-03-13 12:15:25.734623",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -111,16 +111,12 @@ frappe.ui.form.on("Asset Repair", {
|
||||
purchase_invoice: function (frm) {
|
||||
if (frm.doc.purchase_invoice) {
|
||||
frappe.call({
|
||||
method: "frappe.client.get_value",
|
||||
method: "erpnext.assets.doctype.asset_repair.asset_repair.get_repair_cost_for_purchase_invoice",
|
||||
args: {
|
||||
doctype: "Purchase Invoice",
|
||||
fieldname: "base_net_total",
|
||||
filters: { name: frm.doc.purchase_invoice },
|
||||
purchase_invoice: frm.doc.purchase_invoice,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
frm.set_value("repair_cost", r.message.base_net_total);
|
||||
}
|
||||
frm.set_value("repair_cost", r.message || 0);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -135,7 +131,7 @@ frappe.ui.form.on("Asset Repair", {
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.posting_date,
|
||||
from_date: moment(frm.doc.completion_date).format("YYYY-MM-DD"),
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
|
||||
|
||||
import erpnext
|
||||
@@ -308,9 +309,14 @@ class AssetRepair(AccountsController):
|
||||
if flt(self.repair_cost) <= 0:
|
||||
return
|
||||
|
||||
pi_expense_account = (
|
||||
frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account
|
||||
)
|
||||
expense_accounts = _get_expense_accounts_for_purchase_invoice(self.purchase_invoice)
|
||||
|
||||
if not expense_accounts:
|
||||
frappe.throw(
|
||||
_("No expense accounts found for Purchase Invoice {0}").format(self.purchase_invoice)
|
||||
)
|
||||
|
||||
pi_expense_account = expense_accounts[0]
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
@@ -473,3 +479,84 @@ class AssetRepair(AccountsController):
|
||||
def get_downtime(failure_date, completion_date):
|
||||
downtime = time_diff_in_hours(completion_date, failure_date)
|
||||
return round(downtime, 2)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_repair_cost_for_purchase_invoice(purchase_invoice: str) -> float:
|
||||
"""
|
||||
Get the total repair cost from GL entries for a purchase invoice.
|
||||
Only considers expense accounts for non-stock, non-fixed-asset items.
|
||||
"""
|
||||
if not purchase_invoice:
|
||||
return 0.0
|
||||
|
||||
frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True)
|
||||
|
||||
expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice)
|
||||
|
||||
if not expense_accounts:
|
||||
return 0.0
|
||||
|
||||
return _get_total_expense_amount(purchase_invoice, expense_accounts)
|
||||
|
||||
|
||||
def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]:
|
||||
"""
|
||||
Get expense accounts for non-stock items from the purchase invoice.
|
||||
"""
|
||||
pi_items = frappe.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": purchase_invoice},
|
||||
fields=["item_code", "expense_account", "is_fixed_asset"],
|
||||
)
|
||||
|
||||
if not pi_items:
|
||||
return []
|
||||
|
||||
# Get list of stock item codes from the invoice
|
||||
item_codes = {item.item_code for item in pi_items if item.item_code}
|
||||
stock_items = set()
|
||||
if item_codes:
|
||||
stock_items = set(
|
||||
frappe.db.get_all(
|
||||
"Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name"
|
||||
)
|
||||
)
|
||||
|
||||
expense_accounts = set()
|
||||
|
||||
for item in pi_items:
|
||||
# Skip stock items - they use warehouse accounts
|
||||
if item.item_code and item.item_code in stock_items:
|
||||
continue
|
||||
|
||||
# Skip fixed assets - they use asset accounts
|
||||
if item.is_fixed_asset:
|
||||
continue
|
||||
|
||||
# Use expense account from Purchase Invoice Item
|
||||
if item.expense_account:
|
||||
expense_accounts.add(item.expense_account)
|
||||
|
||||
return list(expense_accounts)
|
||||
|
||||
|
||||
def _get_total_expense_amount(purchase_invoice: str, expense_accounts: list[str]) -> float:
|
||||
"""Get the total expense amount from GL entries for a purchase invoice and accounts."""
|
||||
if not expense_accounts:
|
||||
return 0.0
|
||||
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
|
||||
result = (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total"))
|
||||
.where(
|
||||
(gl_entry.voucher_type == "Purchase Invoice")
|
||||
& (gl_entry.voucher_no == purchase_invoice)
|
||||
& (gl_entry.account.isin(expense_accounts))
|
||||
& (gl_entry.is_cancelled == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return flt(result[0].total) if result else 0.0
|
||||
|
||||
@@ -8,6 +8,7 @@ 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.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.assets.doctype.asset.asset import (
|
||||
get_asset_account,
|
||||
get_asset_value_after_depreciation,
|
||||
@@ -21,6 +22,7 @@ from erpnext.assets.doctype.asset.test_asset import (
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_repair.asset_repair import get_repair_cost_for_purchase_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_serial_nos_from_bundle,
|
||||
@@ -321,6 +323,59 @@ class TestAssetRepair(unittest.TestCase):
|
||||
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
|
||||
self.assertEqual(booked_value, asset_repair.repair_cost)
|
||||
|
||||
def test_repair_cost_fetches_only_service_item_amount(self):
|
||||
"""Test that repair cost only includes service (non-stock) item amounts from purchase invoice."""
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
warehouse = "Stores - TCP1"
|
||||
|
||||
service_item = create_item(
|
||||
"_Test Service Item for Repair",
|
||||
is_stock_item=0,
|
||||
warehouse=warehouse,
|
||||
company=company,
|
||||
)
|
||||
|
||||
stock_item = create_item(
|
||||
"_Test Stock Item for Repair",
|
||||
is_stock_item=1,
|
||||
warehouse=warehouse,
|
||||
company=company,
|
||||
)
|
||||
|
||||
service_expense_account = "Miscellaneous Expenses - TCP1"
|
||||
cost_center = frappe.db.get_value("Company", company, "cost_center")
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item_code=service_item.name,
|
||||
qty=1,
|
||||
rate=500,
|
||||
expense_account=service_expense_account,
|
||||
cost_center=cost_center,
|
||||
warehouse=warehouse,
|
||||
update_stock=0,
|
||||
do_not_submit=1,
|
||||
company=company,
|
||||
)
|
||||
|
||||
pi.update_stock = 1
|
||||
pi.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": stock_item.name,
|
||||
"qty": 2,
|
||||
"rate": 300,
|
||||
"warehouse": warehouse,
|
||||
"cost_center": cost_center,
|
||||
},
|
||||
)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
repair_cost = get_repair_cost_for_purchase_invoice(pi.name)
|
||||
|
||||
self.assertEqual(repair_cost, 500)
|
||||
|
||||
|
||||
def num_of_depreciations(asset):
|
||||
return asset.finance_books[0].total_number_of_depreciations
|
||||
@@ -411,6 +466,7 @@ def create_asset_repair(**args):
|
||||
if asset.calculate_depreciation:
|
||||
asset_repair.increase_in_asset_life = 12
|
||||
pi = make_purchase_invoice(
|
||||
item=args.item or "_Test Non Stock Item",
|
||||
company=asset.company,
|
||||
expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"),
|
||||
cost_center=asset_repair.cost_center,
|
||||
|
||||
@@ -769,10 +769,6 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
items_on_form_rendered() {
|
||||
set_schedule_date(this.frm);
|
||||
}
|
||||
|
||||
schedule_date() {
|
||||
set_schedule_date(this.frm);
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
|
||||
@@ -196,6 +196,9 @@ class PurchaseOrder(BuyingController):
|
||||
self.set_has_unit_price_items()
|
||||
self.flags.allow_zero_qty = self.has_unit_price_items
|
||||
|
||||
if self.is_subcontracted:
|
||||
self.status_updater[0]["source_field"] = "fg_item_qty"
|
||||
|
||||
def validate(self):
|
||||
super().validate()
|
||||
|
||||
@@ -823,18 +826,18 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
target.set_payment_schedule()
|
||||
target.credit_to = get_party_account("Supplier", source.supplier, source.company)
|
||||
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
def get_billed_qty(po_item_name):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
table = frappe.qb.DocType("Purchase Invoice Item")
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.select(Sum(table.qty).as_("qty"))
|
||||
.where((table.docstatus == 1) & (table.po_detail == po_item_name))
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
@@ -874,7 +877,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount))
|
||||
"condition": lambda doc: (
|
||||
doc.base_amount == 0
|
||||
or abs(doc.billed_amt) < abs(doc.amount)
|
||||
or doc.qty > flt(get_billed_qty(doc.name))
|
||||
)
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
@@ -909,6 +916,8 @@ def get_list_context(context=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_status(status, name):
|
||||
frappe.has_permission("Purchase Order", "submit", name, throw=True)
|
||||
|
||||
po = frappe.get_doc("Purchase Order", name)
|
||||
po.update_status(status)
|
||||
po.update_delivered_qty_in_sales_order()
|
||||
|
||||
@@ -1346,6 +1346,35 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
self.assertEqual(pi_2.status, "Paid")
|
||||
self.assertEqual(po.status, "Completed")
|
||||
|
||||
@change_settings("Buying Settings", {"maintain_same_rate": 0})
|
||||
def test_purchase_order_over_billing_missing_item(self):
|
||||
item1 = make_item(
|
||||
"_Test Item for Overbilling",
|
||||
).name
|
||||
|
||||
item2 = make_item(
|
||||
"_Test Item for Overbilling 2",
|
||||
).name
|
||||
|
||||
po = create_purchase_order(qty=10, rate=1000, item_code=item1, do_not_save=1)
|
||||
po.append("items", {"item_code": item2, "qty": 5, "rate": 20, "warehouse": "_Test Warehouse - _TC"})
|
||||
po.taxes = []
|
||||
po.insert()
|
||||
po.submit()
|
||||
|
||||
pi1 = make_pi_from_po(po.name)
|
||||
pi1.items[0].qty = 8
|
||||
pi1.items[0].rate = 1250
|
||||
pi1.remove(pi1.items[1])
|
||||
pi1.insert()
|
||||
pi1.submit()
|
||||
|
||||
self.assertEqual(pi1.grand_total, 10000.0)
|
||||
self.assertTrue(len(pi1.items) == 1)
|
||||
|
||||
pi2 = make_pi_from_po(po.name)
|
||||
self.assertEqual(len(pi2.items), 2)
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
|
||||
@@ -165,14 +165,10 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
},
|
||||
|
||||
show_supplier_quotation_comparison(frm) {
|
||||
const today = new Date();
|
||||
const oneMonthAgo = new Date(today);
|
||||
oneMonthAgo.setMonth(today.getMonth() - 1);
|
||||
|
||||
frappe.route_options = {
|
||||
company: frm.doc.company,
|
||||
from_date: moment(oneMonthAgo).format("YYYY-MM-DD"),
|
||||
to_date: moment(today).format("YYYY-MM-DD"),
|
||||
from_date: moment(frm.doc.transaction_date).format("YYYY-MM-DD"),
|
||||
to_date: moment(new Date()).format("YYYY-MM-DD"),
|
||||
request_for_quotation: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("query-report", "Supplier Quotation Comparison");
|
||||
@@ -250,10 +246,17 @@ frappe.ui.form.on("Request for Quotation", {
|
||||
"subject",
|
||||
])
|
||||
.then((r) => {
|
||||
frm.set_value(
|
||||
"message_for_supplier",
|
||||
r.message.use_html ? r.message.response_html : r.message.response
|
||||
);
|
||||
if (r.message.use_html) {
|
||||
frm.set_value({
|
||||
mfs_html: r.message.response_html,
|
||||
use_html: 1,
|
||||
});
|
||||
} else {
|
||||
frm.set_value({
|
||||
message_for_supplier: r.message.response,
|
||||
use_html: 0,
|
||||
});
|
||||
}
|
||||
frm.set_value("subject", r.message.subject);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,9 @@
|
||||
"send_document_print",
|
||||
"sec_break_email_2",
|
||||
"subject",
|
||||
"use_html",
|
||||
"message_for_supplier",
|
||||
"mfs_html",
|
||||
"terms_section_break",
|
||||
"incoterm",
|
||||
"named_place",
|
||||
@@ -142,12 +144,13 @@
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Please supply the specified items at the best possible rates",
|
||||
"depends_on": "eval:doc.use_html == 0",
|
||||
"fieldname": "message_for_supplier",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Message for Supplier",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval:doc.use_html == 0",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -324,6 +327,22 @@
|
||||
"label": "Subject",
|
||||
"not_nullable": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "eval:doc.use_html == 1",
|
||||
"fieldname": "mfs_html",
|
||||
"fieldtype": "Code",
|
||||
"label": "Message for Supplier",
|
||||
"mandatory_depends_on": "eval:doc.use_html == 1",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_html",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Use HTML"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -331,7 +350,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-01-05 14:27:33.329810",
|
||||
"modified": "2026-03-01 23:38:48.079274",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
||||
@@ -47,7 +47,8 @@ class RequestforQuotation(BuyingController):
|
||||
incoterm: DF.Link | None
|
||||
items: DF.Table[RequestforQuotationItem]
|
||||
letter_head: DF.Link | None
|
||||
message_for_supplier: DF.TextEditor
|
||||
message_for_supplier: DF.TextEditor | None
|
||||
mfs_html: DF.Code | None
|
||||
named_place: DF.Data | None
|
||||
naming_series: DF.Literal["PUR-RFQ-.YYYY.-"]
|
||||
opportunity: DF.Link | None
|
||||
@@ -61,6 +62,7 @@ class RequestforQuotation(BuyingController):
|
||||
tc_name: DF.Link | None
|
||||
terms: DF.TextEditor | None
|
||||
transaction_date: DF.Date
|
||||
use_html: DF.Check
|
||||
vendor: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
@@ -100,8 +102,16 @@ class RequestforQuotation(BuyingController):
|
||||
["use_html", "response", "response_html", "subject"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not self.message_for_supplier:
|
||||
self.message_for_supplier = data.response_html if data.use_html else data.response
|
||||
|
||||
self.use_html = data.use_html
|
||||
|
||||
if data.use_html:
|
||||
if not self.mfs_html:
|
||||
self.mfs_html = data.response_html
|
||||
else:
|
||||
if not self.message_for_supplier:
|
||||
self.message_for_supplier = data.response
|
||||
|
||||
if not self.subject:
|
||||
self.subject = data.subject
|
||||
|
||||
@@ -304,7 +314,10 @@ class RequestforQuotation(BuyingController):
|
||||
else:
|
||||
sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None
|
||||
|
||||
rendered_message = frappe.render_template(self.message_for_supplier, doc_args)
|
||||
message_template = self.mfs_html if self.use_html else self.message_for_supplier
|
||||
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-ssti
|
||||
rendered_message = frappe.render_template(message_template, doc_args)
|
||||
|
||||
subject_source = (
|
||||
self.subject
|
||||
or frappe.get_value("Email Template", self.email_template, "subject")
|
||||
|
||||
@@ -30,6 +30,14 @@ erpnext.buying.SupplierQuotationController = class SupplierQuotationController e
|
||||
cur_frm.add_custom_button(__("Purchase Order"), this.make_purchase_order, __("Create"));
|
||||
cur_frm.page.set_inner_btn_group_as_primary(__("Create"));
|
||||
cur_frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
|
||||
this.frm.add_custom_button(__("Update Items"), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
frm: this.frm,
|
||||
child_docname: "items",
|
||||
cannot_add_row: false,
|
||||
});
|
||||
});
|
||||
} else if (this.frm.doc.docstatus === 0) {
|
||||
erpnext.set_unit_price_items_note(this.frm);
|
||||
|
||||
|
||||
@@ -345,3 +345,15 @@ def set_expired_status():
|
||||
""",
|
||||
(nowdate()),
|
||||
)
|
||||
|
||||
|
||||
def get_purchased_items(supplier_quotation: str):
|
||||
return frappe._dict(
|
||||
frappe.get_all(
|
||||
"Purchase Order Item",
|
||||
filters={"supplier_quotation": supplier_quotation, "docstatus": 1},
|
||||
fields=["supplier_quotation_item", "sum(qty)"],
|
||||
group_by="supplier_quotation_item",
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -2,15 +2,131 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||
|
||||
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_update_child_supplier_quotation_add_item(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.submit()
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": sq.items[0].rate,
|
||||
"qty": 5,
|
||||
"docname": sq.items[0].name,
|
||||
},
|
||||
{
|
||||
"item_code": "_Test Item 2",
|
||||
"rate": 300,
|
||||
"qty": 3,
|
||||
},
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
sq.reload()
|
||||
self.assertEqual(sq.get("items")[0].qty, 5)
|
||||
self.assertEqual(sq.get("items")[1].rate, 300)
|
||||
|
||||
def test_update_supplier_quotation_child_rate(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.submit()
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": 300,
|
||||
"qty": sq.items[0].qty,
|
||||
"docname": sq.items[0].name,
|
||||
},
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
sq.reload()
|
||||
self.assertEqual(sq.get("items")[0].rate, 300)
|
||||
po = make_purchase_order(sq.name)
|
||||
po.schedule_date = add_days(today(), 1)
|
||||
po.submit()
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": 20,
|
||||
"qty": sq.items[0].qty,
|
||||
"docname": sq.items[0].name,
|
||||
},
|
||||
]
|
||||
)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||
)
|
||||
|
||||
def test_update_supplier_quotation_child_remove_item(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.submit()
|
||||
po = make_purchase_order(sq.name)
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": sq.items[0].rate,
|
||||
"qty": sq.items[0].qty,
|
||||
"docname": sq.items[0].name,
|
||||
},
|
||||
{
|
||||
"item_code": "_Test Item 2",
|
||||
"rate": 300,
|
||||
"qty": 3,
|
||||
},
|
||||
]
|
||||
)
|
||||
po.get("items")[0].schedule_date = add_days(today(), 1)
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
po.submit()
|
||||
sq.reload()
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": "_Test Item 2",
|
||||
"rate": 300,
|
||||
"qty": 3,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
frappe.db.savepoint("before_cancel")
|
||||
# check if item having purchase order can be removed
|
||||
self.assertRaises(
|
||||
frappe.LinkExistsError, update_child_qty_rate, "Supplier Quotation", trans_item, sq.name
|
||||
)
|
||||
frappe.db.rollback(save_point="before_cancel")
|
||||
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": sq.items[0].item_code,
|
||||
"rate": sq.items[0].rate,
|
||||
"qty": sq.items[0].qty,
|
||||
"docname": sq.items[0].name,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
update_child_qty_rate("Supplier Quotation", trans_item, sq.name)
|
||||
sq.reload()
|
||||
self.assertEqual(len(sq.get("items")), 1)
|
||||
|
||||
def test_supplier_quotation_qty(self):
|
||||
sq = frappe.copy_doc(test_records[0])
|
||||
sq.items[0].qty = 0
|
||||
|
||||
@@ -165,7 +165,7 @@ def get_data(filters):
|
||||
"cost_center": po.cost_center,
|
||||
"project": po.project,
|
||||
"requesting_site": po.warehouse,
|
||||
"requestor": po.owner,
|
||||
"requestor": mr_record.get("owner", po.owner),
|
||||
"material_request_no": po.material_request,
|
||||
"item_code": po.item_code,
|
||||
"quantity": flt(po.qty),
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2025-11-05 11:55:50.058154",
|
||||
"letter_head": null,
|
||||
"modified": "2026-03-13 17:36:05.561765",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Trends",
|
||||
|
||||
@@ -41,6 +41,7 @@ def get_columns(filters):
|
||||
"fieldname": "transferred_qty",
|
||||
"width": 200,
|
||||
},
|
||||
{"label": _("Returned Quantity"), "fieldtype": "Float", "fieldname": "returned_qty", "width": 150},
|
||||
{"label": _("Pending Quantity"), "fieldtype": "Float", "fieldname": "p_qty", "width": 150},
|
||||
]
|
||||
|
||||
@@ -50,7 +51,7 @@ def get_data(filters):
|
||||
|
||||
data = []
|
||||
for row in order_rm_item_details:
|
||||
transferred_qty = row.get("transferred_qty") or 0
|
||||
transferred_qty = (row.get("transferred_qty") or 0) - (row.get("returned_qty") or 0)
|
||||
if transferred_qty < row.get("reqd_qty", 0):
|
||||
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
|
||||
row.p_qty = pending_qty if pending_qty > 0 else 0
|
||||
@@ -86,6 +87,7 @@ def get_order_items_to_supply(filters):
|
||||
f"`tab{supplied_items_table}`.rm_item_code as rm_item_code",
|
||||
f"`tab{supplied_items_table}`.required_qty as reqd_qty",
|
||||
f"`tab{supplied_items_table}`.supplied_qty as transferred_qty",
|
||||
f"`tab{supplied_items_table}`.returned_qty as returned_qty",
|
||||
],
|
||||
filters=record_filters,
|
||||
)
|
||||
|
||||
@@ -2297,6 +2297,16 @@ class AccountsController(TransactionBase):
|
||||
|
||||
return stock_items
|
||||
|
||||
def get_asset_items(self):
|
||||
asset_items = []
|
||||
item_codes = list(set(item.item_code for item in self.get("items")))
|
||||
if item_codes:
|
||||
asset_items = frappe.db.get_values(
|
||||
"Item", {"name": ["in", item_codes], "is_fixed_asset": 1}, pluck="name", cache=True
|
||||
)
|
||||
|
||||
return asset_items
|
||||
|
||||
def calculate_total_advance_from_ledger(self):
|
||||
adv = frappe.qb.DocType("Advance Payment Ledger Entry")
|
||||
return (
|
||||
@@ -2502,13 +2512,14 @@ class AccountsController(TransactionBase):
|
||||
grand_total = self.get("rounded_total") or self.grand_total
|
||||
automatically_fetch_payment_terms = 0
|
||||
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
|
||||
grand_total = grand_total - flt(self.write_off_amount)
|
||||
if self.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
|
||||
po_or_so, doctype, fieldname = self.get_order_details()
|
||||
automatically_fetch_payment_terms = cint(
|
||||
frappe.db.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
|
||||
)
|
||||
if self.doctype != "Sales Order":
|
||||
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
|
||||
grand_total = grand_total - flt(self.write_off_amount)
|
||||
|
||||
if self.get("total_advance"):
|
||||
if party_account_currency == self.company_currency:
|
||||
@@ -2524,7 +2535,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
if not self.get("payment_schedule"):
|
||||
if (
|
||||
self.doctype in ["Sales Invoice", "Purchase Invoice"]
|
||||
self.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
|
||||
and automatically_fetch_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
@@ -2579,17 +2590,23 @@ class AccountsController(TransactionBase):
|
||||
self.ignore_default_payment_terms_template = 1
|
||||
|
||||
def get_order_details(self):
|
||||
if not self.get("items"):
|
||||
return None, None, None
|
||||
if self.doctype == "Sales Invoice":
|
||||
po_or_so = self.get("items")[0].get("sales_order")
|
||||
po_or_so_doctype = "Sales Order"
|
||||
po_or_so_doctype_name = "sales_order"
|
||||
|
||||
prev_doc = self.get("items")[0].get("sales_order")
|
||||
prev_doctype = "Sales Order"
|
||||
prev_doctype_name = "sales_order"
|
||||
elif self.doctype == "Purchase Invoice":
|
||||
prev_doc = self.get("items")[0].get("purchase_order")
|
||||
prev_doctype = "Purchase Order"
|
||||
prev_doctype_name = "purchase_order"
|
||||
elif self.doctype == "Sales Order":
|
||||
prev_doc = self.get("items")[0].get("prevdoc_docname")
|
||||
prev_doctype = "Quotation"
|
||||
prev_doctype_name = "prevdoc_docname"
|
||||
else:
|
||||
po_or_so = self.get("items")[0].get("purchase_order")
|
||||
po_or_so_doctype = "Purchase Order"
|
||||
po_or_so_doctype_name = "purchase_order"
|
||||
|
||||
return po_or_so, po_or_so_doctype, po_or_so_doctype_name
|
||||
return None, None, None
|
||||
return prev_doc, prev_doctype, prev_doctype_name
|
||||
|
||||
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
|
||||
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
|
||||
@@ -2685,7 +2702,9 @@ class AccountsController(TransactionBase):
|
||||
|
||||
for d in self.get("payment_schedule"):
|
||||
d.validate_from_to_dates("discount_date", "due_date")
|
||||
if self.doctype == "Sales Order" and getdate(d.due_date) < getdate(self.transaction_date):
|
||||
if self.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
|
||||
self.transaction_date
|
||||
):
|
||||
frappe.throw(
|
||||
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
|
||||
d.idx
|
||||
@@ -3678,7 +3697,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
||||
|
||||
if child_doctype == "Purchase Order Item":
|
||||
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
|
||||
# Initialized value will update in parent validation
|
||||
child_item.base_rate = 1
|
||||
child_item.base_amount = 1
|
||||
@@ -3696,7 +3715,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
||||
return child_item
|
||||
|
||||
|
||||
def validate_child_on_delete(row, parent):
|
||||
def validate_child_on_delete(row, parent, ordered_item=None):
|
||||
"""Check if partially transacted item (row) is being deleted."""
|
||||
if parent.doctype == "Sales Order":
|
||||
if flt(row.delivered_qty):
|
||||
@@ -3724,13 +3743,17 @@ def validate_child_on_delete(row, parent):
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
|
||||
if flt(row.billed_amt):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
||||
row.idx, row.item_code
|
||||
if parent.doctype in ["Purchase Order", "Sales Order"]:
|
||||
if flt(row.billed_amt):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot delete item {1} which has already been billed.").format(
|
||||
row.idx, row.item_code
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if parent.doctype == "Quotation":
|
||||
if ordered_item.get(row.name):
|
||||
frappe.throw(_("Cannot delete an item which has been ordered"))
|
||||
|
||||
|
||||
def update_bin_on_delete(row, doctype):
|
||||
@@ -3756,7 +3779,7 @@ def update_bin_on_delete(row, doctype):
|
||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||
|
||||
|
||||
def validate_and_delete_children(parent, data) -> bool:
|
||||
def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
|
||||
deleted_children = []
|
||||
updated_item_names = [d.get("docname") for d in data]
|
||||
for item in parent.items:
|
||||
@@ -3764,7 +3787,7 @@ def validate_and_delete_children(parent, data) -> bool:
|
||||
deleted_children.append(item)
|
||||
|
||||
for d in deleted_children:
|
||||
validate_child_on_delete(d, parent)
|
||||
validate_child_on_delete(d, parent, ordered_item)
|
||||
d.cancel()
|
||||
d.delete()
|
||||
|
||||
@@ -3773,16 +3796,19 @@ def validate_and_delete_children(parent, data) -> bool:
|
||||
|
||||
# need to update ordered qty in Material Request first
|
||||
# bin uses Material Request Items to recalculate & update
|
||||
parent.update_prevdoc_status()
|
||||
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
if parent.doctype not in ["Quotation", "Supplier Quotation"]:
|
||||
parent.update_prevdoc_status()
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
return bool(deleted_children)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
|
||||
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
|
||||
|
||||
def check_doc_permissions(doc, perm_type="create"):
|
||||
try:
|
||||
doc.check_permission(perm_type)
|
||||
@@ -3821,7 +3847,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
)
|
||||
|
||||
def get_new_child_item(item_row):
|
||||
child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
|
||||
child_doctype = parent_doctype + " Item"
|
||||
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
|
||||
|
||||
def is_allowed_zero_qty():
|
||||
@@ -3831,20 +3857,51 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
return frappe.db.get_single_value("Buying Settings", "allow_zero_qty_in_purchase_order") or False
|
||||
return False
|
||||
|
||||
def validate_quantity(child_item, new_data):
|
||||
def validate_quantity_and_rate(child_item, new_data):
|
||||
if not flt(new_data.get("qty")) and not is_allowed_zero_qty():
|
||||
frappe.throw(
|
||||
_("Row #{0}: Quantity for Item {1} cannot be zero.").format(
|
||||
_("Row #{0}:Quantity for Item {1} cannot be zero.").format(
|
||||
new_data.get("idx"), frappe.bold(new_data.get("item_code"))
|
||||
),
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
if parent_doctype == "Sales Order" and flt(new_data.get("qty")) < flt(child_item.delivered_qty):
|
||||
frappe.throw(_("Cannot set quantity less than delivered quantity"))
|
||||
qty_limits = {
|
||||
"Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")),
|
||||
"Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")),
|
||||
}
|
||||
|
||||
if parent_doctype == "Purchase Order" and flt(new_data.get("qty")) < flt(child_item.received_qty):
|
||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||
if parent_doctype in qty_limits:
|
||||
qty_field, error_message = qty_limits[parent_doctype]
|
||||
if flt(new_data.get("qty")) < flt(child_item.get(qty_field)):
|
||||
frappe.throw(
|
||||
_("Row #{0}:").format(new_data.get("idx"))
|
||||
+ error_message.format(frappe.bold(new_data.get("item_code"))),
|
||||
title=_("Invalid Qty"),
|
||||
)
|
||||
|
||||
if parent_doctype in ["Quotation", "Supplier Quotation"]:
|
||||
if (parent_doctype == "Quotation" and not ordered_items) or (
|
||||
parent_doctype == "Supplier Quotation" and not purchased_items
|
||||
):
|
||||
return
|
||||
|
||||
qty_to_check = (
|
||||
ordered_items.get(child_item.name)
|
||||
if parent_doctype == "Quotation"
|
||||
else purchased_items.get(child_item.name)
|
||||
)
|
||||
|
||||
if qty_to_check:
|
||||
if not rate_unchanged:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot update rate as item {0} is already ordered or purchased against this quotation"
|
||||
).format(frappe.bold(new_data.get("item_code")))
|
||||
)
|
||||
|
||||
if flt(new_data.get("qty")) < qty_to_check:
|
||||
frappe.throw(_("Cannot reduce quantity than ordered or purchased quantity"))
|
||||
|
||||
def should_update_supplied_items(doc) -> bool:
|
||||
"""Subcontracted PO can allow following changes *after submit*:
|
||||
@@ -3888,7 +3945,6 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
||||
|
||||
data = json.loads(trans_items)
|
||||
|
||||
any_qty_changed = False # updated to true if any item's qty changes
|
||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||
any_conversion_factor_changed = False
|
||||
@@ -3896,7 +3952,16 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_doc_permissions(parent, "write")
|
||||
_removed_items = validate_and_delete_children(parent, data)
|
||||
|
||||
if parent_doctype == "Quotation":
|
||||
ordered_items = get_ordered_items(parent.name)
|
||||
_removed_items = validate_and_delete_children(parent, data, ordered_items)
|
||||
elif parent_doctype == "Supplier Quotation":
|
||||
purchased_items = get_purchased_items(parent.name)
|
||||
_removed_items = validate_and_delete_children(parent, data, purchased_items)
|
||||
else:
|
||||
_removed_items = validate_and_delete_children(parent, data)
|
||||
|
||||
items_added_or_removed |= _removed_items
|
||||
|
||||
for d in data:
|
||||
@@ -3936,7 +4001,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||
date_unchanged = (
|
||||
prev_date == getdate(new_date) if prev_date and new_date else False
|
||||
(prev_date == getdate(new_date) if prev_date and new_date else False)
|
||||
if parent_doctype not in ["Quotation", "Supplier Quotation"]
|
||||
else None
|
||||
) # in case of delivery note etc
|
||||
if (
|
||||
rate_unchanged
|
||||
@@ -3948,7 +4015,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
):
|
||||
continue
|
||||
|
||||
validate_quantity(child_item, d)
|
||||
validate_quantity_and_rate(child_item, d)
|
||||
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
@@ -3972,18 +4040,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
rate_unchanged = prev_rate == new_rate
|
||||
if not rate_unchanged and not child_item.get("qty") and is_allowed_zero_qty():
|
||||
frappe.throw(_("Rate of '{}' items cannot be changed").format(frappe.bold(_("Unit Price"))))
|
||||
|
||||
# Amount cannot be lesser than billed amount, except for negative amounts
|
||||
row_rate = flt(d.get("rate"), rate_precision)
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
||||
).format(child_item.idx, child_item.item_code)
|
||||
|
||||
if parent_doctype in ["Purchase Order", "Sales Order"]:
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot set Rate if the billed amount is greater than the amount for Item {1}."
|
||||
).format(child_item.idx, child_item.item_code)
|
||||
)
|
||||
else:
|
||||
child_item.rate = row_rate
|
||||
else:
|
||||
child_item.rate = row_rate
|
||||
|
||||
@@ -4002,6 +4073,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
flt(d.get("conversion_factor"), conv_fac_precision) or conversion_factor
|
||||
)
|
||||
|
||||
if child_item.get("total_weight") and child_item.get("weight_per_unit"):
|
||||
child_item.total_weight = flt(
|
||||
child_item.weight_per_unit * child_item.qty * child_item.conversion_factor,
|
||||
child_item.precision("total_weight"),
|
||||
)
|
||||
|
||||
if d.get("delivery_date") and parent_doctype == "Sales Order":
|
||||
child_item.delivery_date = d.get("delivery_date")
|
||||
|
||||
@@ -4011,26 +4088,27 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
if d.get("bom_no") and parent_doctype == "Sales Order":
|
||||
child_item.bom_no = d.get("bom_no")
|
||||
|
||||
if flt(child_item.price_list_rate):
|
||||
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
||||
# if rate is greater than price_list_rate, set margin
|
||||
# or set discount
|
||||
child_item.discount_percentage = 0
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.discount_percentage = flt(
|
||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||
child_item.precision("discount_percentage"),
|
||||
)
|
||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
if parent_doctype in ["Sales Order", "Purchase Order"]:
|
||||
if flt(child_item.price_list_rate):
|
||||
if flt(child_item.rate) > flt(child_item.price_list_rate):
|
||||
# if rate is greater than price_list_rate, set margin
|
||||
# or set discount
|
||||
child_item.discount_percentage = 0
|
||||
child_item.margin_type = "Amount"
|
||||
child_item.margin_rate_or_amount = flt(
|
||||
child_item.rate - child_item.price_list_rate,
|
||||
child_item.precision("margin_rate_or_amount"),
|
||||
)
|
||||
child_item.rate_with_margin = child_item.rate
|
||||
else:
|
||||
child_item.discount_percentage = flt(
|
||||
(1 - flt(child_item.rate) / flt(child_item.price_list_rate)) * 100.0,
|
||||
child_item.precision("discount_percentage"),
|
||||
)
|
||||
child_item.discount_amount = flt(child_item.price_list_rate) - flt(child_item.rate)
|
||||
child_item.margin_type = ""
|
||||
child_item.margin_rate_or_amount = 0
|
||||
child_item.rate_with_margin = 0
|
||||
|
||||
child_item.flags.ignore_validate_update_after_submit = True
|
||||
if new_child_flag:
|
||||
@@ -4038,7 +4116,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
child_item.idx = len(parent.items) + 1
|
||||
child_item.insert()
|
||||
else:
|
||||
child_item.save()
|
||||
child_item.save(ignore_permissions=True)
|
||||
|
||||
parent.reload()
|
||||
parent.flags.ignore_validate_update_after_submit = True
|
||||
@@ -4052,13 +4130,15 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.doctype, parent.company, parent.base_grand_total
|
||||
)
|
||||
|
||||
parent.set_payment_schedule()
|
||||
if parent_doctype != "Supplier Quotation":
|
||||
parent.set_payment_schedule()
|
||||
if parent_doctype == "Purchase Order":
|
||||
parent.set_tax_withholding()
|
||||
parent.validate_minimum_order_qty()
|
||||
parent.validate_budget()
|
||||
if parent.is_against_so():
|
||||
parent.update_status_updater()
|
||||
else:
|
||||
elif parent_doctype == "Sales Order":
|
||||
parent.check_credit_limit()
|
||||
|
||||
# reset index of child table
|
||||
@@ -4091,7 +4171,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
elif parent_doctype == "Sales Order":
|
||||
parent.validate_selling_price()
|
||||
parent.validate_for_duplicate_items()
|
||||
parent.validate_warehouse()
|
||||
@@ -4103,9 +4183,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.reload()
|
||||
validate_workflow_conditions(parent)
|
||||
|
||||
parent.update_blanket_order()
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
if parent_doctype in ["Purchase Order", "Sales Order"]:
|
||||
parent.update_blanket_order()
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
|
||||
parent.validate_uom_is_integer("uom", "qty")
|
||||
parent.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
|
||||
@@ -327,7 +327,7 @@ class BuyingController(SubcontractingController):
|
||||
last_item_idx = d.idx
|
||||
|
||||
total_valuation_amount = sum(
|
||||
flt(d.base_tax_amount_after_discount_amount)
|
||||
flt(d.base_tax_amount_after_discount_amount) * (-1 if d.get("add_deduct_tax") == "Deduct" else 1)
|
||||
for d in self.get("taxes")
|
||||
if d.category in ["Valuation", "Valuation and Total"]
|
||||
)
|
||||
@@ -626,7 +626,9 @@ class BuyingController(SubcontractingController):
|
||||
or self.is_return
|
||||
or (self.is_internal_transfer() and self.docstatus == 2)
|
||||
else self.get_package_for_target_warehouse(
|
||||
d, type_of_transaction=type_of_transaction
|
||||
d,
|
||||
type_of_transaction=type_of_transaction,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
),
|
||||
},
|
||||
@@ -714,7 +716,22 @@ class BuyingController(SubcontractingController):
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
|
||||
def get_package_for_target_warehouse(self, item, warehouse=None, type_of_transaction=None) -> str:
|
||||
def get_package_for_target_warehouse(
|
||||
self, item, warehouse=None, type_of_transaction=None, via_landed_cost_voucher=None
|
||||
) -> str:
|
||||
if via_landed_cost_voucher and item.get("warehouse"):
|
||||
if sabb := frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
{
|
||||
"voucher_detail_no": item.name,
|
||||
"warehouse": item.get("warehouse"),
|
||||
"docstatus": 1,
|
||||
"is_cancelled": 0,
|
||||
},
|
||||
"name",
|
||||
):
|
||||
return sabb
|
||||
|
||||
if not item.serial_and_batch_bundle:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -360,13 +360,13 @@ def copy_attributes_to_variant(item, variant):
|
||||
else:
|
||||
if item.variant_based_on == "Item Attribute":
|
||||
if variant.attributes:
|
||||
attributes_description = item.description + " "
|
||||
attributes_description = item.description or ""
|
||||
for d in variant.attributes:
|
||||
attributes_description += (
|
||||
"<div>" + d.attribute + ": " + cstr(d.attribute_value) + "</div>"
|
||||
)
|
||||
|
||||
if attributes_description not in variant.description:
|
||||
if attributes_description not in (variant.description or ""):
|
||||
variant.description = attributes_description
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from frappe.utils import cint, nowdate, today, unique
|
||||
from pypika import Order
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import build_qb_match_conditions
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
|
||||
|
||||
@@ -608,34 +609,37 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
|
||||
# income account can be any Credit account,
|
||||
# but can also be a Asset account with account_type='Income Account' in special circumstances.
|
||||
# Hence the first condition is an "OR"
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
dt = "Account"
|
||||
|
||||
acc = qb.DocType(dt)
|
||||
condition = [
|
||||
(acc.report_type.eq("Profit and Loss") | acc.account_type.isin(["Income Account", "Temporary"])),
|
||||
acc.is_group.eq(0),
|
||||
acc.disabled.eq(0),
|
||||
]
|
||||
if txt:
|
||||
condition.append(acc.name.like(f"%{txt}%"))
|
||||
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
condition.append(acc.company.eq(filters.get("company")))
|
||||
|
||||
condition += " and tabAccount.disabled = %(disabled)s"
|
||||
user_perms = build_qb_match_conditions(dt)
|
||||
condition.extend(user_perms)
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""select tabAccount.name from `tabAccount`
|
||||
where (tabAccount.report_type = "Profit and Loss"
|
||||
or tabAccount.account_type in ("Income Account", "Temporary"))
|
||||
and tabAccount.is_group=0
|
||||
and tabAccount.`{searchfield}` LIKE %(txt)s
|
||||
{condition} {get_match_cond(doctype)}
|
||||
order by idx desc, name""",
|
||||
{
|
||||
"txt": "%" + txt + "%",
|
||||
"company": filters.get("company", ""),
|
||||
"disabled": cint(filters.get("disabled", 0)),
|
||||
},
|
||||
return (
|
||||
qb.from_(acc)
|
||||
.select(acc.name)
|
||||
.where(Criterion.all(condition))
|
||||
.orderby(acc.idx, order=Order.desc)
|
||||
.orderby(acc.name)
|
||||
.run()
|
||||
)
|
||||
|
||||
|
||||
@@ -696,26 +700,38 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters,
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
dt = "Account"
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""select tabAccount.name from `tabAccount`
|
||||
where (tabAccount.report_type = "Profit and Loss"
|
||||
or tabAccount.account_type in ("Expense Account", "Fixed Asset", "Temporary", "Asset Received But Not Billed", "Capital Work in Progress"))
|
||||
and tabAccount.is_group=0
|
||||
and tabAccount.disabled = 0
|
||||
and tabAccount.{searchfield} LIKE %(txt)s
|
||||
{condition} {get_match_cond(doctype)}""",
|
||||
{"company": filters.get("company", ""), "txt": "%" + txt + "%"},
|
||||
)
|
||||
acc = qb.DocType(dt)
|
||||
condition = [
|
||||
(
|
||||
acc.report_type.eq("Profit and Loss")
|
||||
| acc.account_type.isin(
|
||||
[
|
||||
"Expense Account",
|
||||
"Fixed Asset",
|
||||
"Temporary",
|
||||
"Asset Received But Not Billed",
|
||||
"Capital Work in Progress",
|
||||
]
|
||||
)
|
||||
),
|
||||
acc.is_group.eq(0),
|
||||
acc.disabled.eq(0),
|
||||
]
|
||||
if txt:
|
||||
condition.append(acc.name.like(f"%{txt}%"))
|
||||
|
||||
if filters.get("company"):
|
||||
condition.append(acc.company.eq(filters.get("company")))
|
||||
|
||||
user_perms = build_qb_match_conditions(dt)
|
||||
condition.extend(user_perms)
|
||||
|
||||
return qb.from_(acc).select(acc.name).where(Criterion.all(condition)).run()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -976,3 +992,26 @@ def get_item_uom_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
limit_page_length=page_len,
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
|
||||
table = frappe.qb.DocType(doctype)
|
||||
child_table = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child_table)
|
||||
.on((table.name == child_table.parent) & (child_table.parenttype == doctype))
|
||||
.select(table.name)
|
||||
.where(
|
||||
(child_table.link_name == filters.get("warehouse"))
|
||||
& (table.disabled == 0)
|
||||
& (child_table.link_doctype == "Warehouse")
|
||||
& (table.name.like(f"%{txt}%"))
|
||||
)
|
||||
.offset(start)
|
||||
.limit(page_len)
|
||||
)
|
||||
return query.run(as_list=1)
|
||||
|
||||
@@ -531,7 +531,6 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None, return_agai
|
||||
target_doc.against_sales_order = source_doc.against_sales_order
|
||||
target_doc.against_sales_invoice = source_doc.against_sales_invoice
|
||||
target_doc.so_detail = source_doc.so_detail
|
||||
target_doc.si_detail = source_doc.si_detail
|
||||
target_doc.expense_account = source_doc.expense_account
|
||||
target_doc.dn_detail = source_doc.name
|
||||
if default_warehouse_for_sales_return:
|
||||
@@ -936,7 +935,14 @@ def get_serial_batches_based_on_bundle(doctype, field, _bundle_ids):
|
||||
|
||||
if doctype == "Packed Item":
|
||||
if key is None:
|
||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||
key = frappe.get_cached_value(
|
||||
"Packed Item",
|
||||
{"parent_detail_docname": row.voucher_detail_no, "item_code": row.item_code},
|
||||
field,
|
||||
)
|
||||
if key is None:
|
||||
key = frappe.get_cached_value("Packed Item", row.voucher_detail_no, field)
|
||||
|
||||
if row.voucher_type == "Delivery Note":
|
||||
key = frappe.get_cached_value("Delivery Note Item", key, "dn_detail")
|
||||
elif row.voucher_type == "Sales Invoice":
|
||||
|
||||
@@ -318,9 +318,10 @@ class SellingController(StockController):
|
||||
if is_internal_customer or not is_stock_item:
|
||||
continue
|
||||
|
||||
if item.get("incoming_rate") and item.base_net_rate < (
|
||||
rate_field = "valuation_rate" if self.doctype in ["Sales Order", "Quotation"] else "incoming_rate"
|
||||
if item.get(rate_field) and item.base_net_rate < (
|
||||
valuation_rate := flt(
|
||||
item.incoming_rate * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||
item.get(rate_field) * (item.conversion_factor or 1), item.precision("base_net_rate")
|
||||
)
|
||||
):
|
||||
throw_message(
|
||||
@@ -483,10 +484,37 @@ class SellingController(StockController):
|
||||
sales_order.update_reserved_qty(so_item_rows)
|
||||
|
||||
def set_incoming_rate(self):
|
||||
def reset_incoming_rate():
|
||||
old_item = next(
|
||||
(
|
||||
item
|
||||
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
|
||||
if item.name == d.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if old_item:
|
||||
old_qty = flt(old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty"))
|
||||
if (
|
||||
old_item.item_code != d.item_code
|
||||
or old_item.warehouse != d.warehouse
|
||||
or old_qty != qty
|
||||
or old_item.serial_no != d.serial_no
|
||||
or get_serial_nos(old_item.serial_and_batch_bundle)
|
||||
!= get_serial_nos(d.serial_and_batch_bundle)
|
||||
or old_item.batch_no != d.batch_no
|
||||
or get_batch_nos(old_item.serial_and_batch_bundle)
|
||||
!= get_batch_nos(d.serial_and_batch_bundle)
|
||||
):
|
||||
d.incoming_rate = 0
|
||||
|
||||
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||
return
|
||||
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos
|
||||
if self.doctype == "Sales Invoice" and not self.update_stock and not self.is_internal_transfer():
|
||||
return
|
||||
|
||||
from erpnext.stock.serial_batch_bundle import get_batch_nos, get_serial_nos
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
@@ -495,6 +523,8 @@ class SellingController(StockController):
|
||||
"Selling Settings", "set_zero_rate_for_expired_batch"
|
||||
)
|
||||
|
||||
is_standalone = self.is_return and not self.return_against
|
||||
|
||||
old_doc = self.get_doc_before_save()
|
||||
items = self.get("items") + (self.get("packed_items") or [])
|
||||
for d in items:
|
||||
@@ -526,27 +556,7 @@ class SellingController(StockController):
|
||||
qty = flt(d.get("stock_qty") or d.get("actual_qty") or d.get("qty"))
|
||||
|
||||
if old_doc:
|
||||
old_item = next(
|
||||
(
|
||||
item
|
||||
for item in (old_doc.get("items") + (old_doc.get("packed_items") or []))
|
||||
if item.name == d.name
|
||||
),
|
||||
None,
|
||||
)
|
||||
if old_item:
|
||||
old_qty = flt(
|
||||
old_item.get("stock_qty") or old_item.get("actual_qty") or old_item.get("qty")
|
||||
)
|
||||
if (
|
||||
old_item.item_code != d.item_code
|
||||
or old_item.warehouse != d.warehouse
|
||||
or old_qty != qty
|
||||
or old_item.batch_no != d.batch_no
|
||||
or get_batch_nos(old_item.serial_and_batch_bundle)
|
||||
!= get_batch_nos(d.serial_and_batch_bundle)
|
||||
):
|
||||
d.incoming_rate = 0
|
||||
reset_incoming_rate()
|
||||
|
||||
if (
|
||||
not d.incoming_rate
|
||||
@@ -565,11 +575,12 @@ class SellingController(StockController):
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"voucher_detail_no": d.name,
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation"),
|
||||
"allow_zero_valuation": d.get("allow_zero_valuation_rate"),
|
||||
"batch_no": d.batch_no,
|
||||
"serial_no": d.serial_no,
|
||||
},
|
||||
raise_error_if_no_rate=False,
|
||||
raise_error_if_no_rate=is_standalone,
|
||||
fallbacks=not is_standalone,
|
||||
)
|
||||
|
||||
if (
|
||||
|
||||
@@ -111,7 +111,7 @@ status_map = {
|
||||
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
|
||||
[
|
||||
"Ordered",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture']",
|
||||
"eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type in ['Purchase', 'Manufacture', 'Subcontracting']",
|
||||
],
|
||||
[
|
||||
"Transferred",
|
||||
@@ -234,8 +234,8 @@ class StatusUpdater(Document):
|
||||
self.global_amount_allowance = None
|
||||
|
||||
for args in self.status_updater:
|
||||
if "target_ref_field" not in args:
|
||||
# if target_ref_field is not specified, the programmer does not want to validate qty / amount
|
||||
if "target_ref_field" not in args or args.get("validate_qty") is False:
|
||||
# if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation
|
||||
continue
|
||||
|
||||
# get unique transactions to update
|
||||
|
||||
@@ -57,6 +57,8 @@ class StockController(AccountsController):
|
||||
|
||||
if not self.get("is_return"):
|
||||
self.validate_inspection()
|
||||
|
||||
self.validate_warehouse_of_sabb()
|
||||
self.validate_serialized_batch()
|
||||
self.clean_serial_nos()
|
||||
self.validate_customer_provided_item()
|
||||
@@ -65,6 +67,45 @@ class StockController(AccountsController):
|
||||
self.validate_putaway_capacity()
|
||||
self.reset_conversion_factor()
|
||||
|
||||
def validate_warehouse_of_sabb(self):
|
||||
if self.is_internal_transfer():
|
||||
return
|
||||
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
|
||||
for row in self.items:
|
||||
if not row.get("serial_and_batch_bundle"):
|
||||
continue
|
||||
|
||||
sabb_details = frappe.db.get_value(
|
||||
"Serial and Batch Bundle",
|
||||
row.serial_and_batch_bundle,
|
||||
["type_of_transaction", "warehouse", "has_serial_no"],
|
||||
as_dict=True,
|
||||
)
|
||||
if not sabb_details:
|
||||
continue
|
||||
|
||||
if sabb_details.type_of_transaction != "Outward":
|
||||
continue
|
||||
|
||||
warehouse = row.get("warehouse") or row.get("s_warehouse")
|
||||
if sabb_details.warehouse != warehouse:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Warehouse {1} does not match with the warehouse {2} in Serial and Batch Bundle {3}."
|
||||
).format(row.idx, warehouse, sabb_details.warehouse, row.serial_and_batch_bundle)
|
||||
)
|
||||
|
||||
if self.doctype == "Stock Reconciliation":
|
||||
continue
|
||||
|
||||
if sabb_details.has_serial_no and doc_before_save and doc_before_save.get("items"):
|
||||
prev_row = doc_before_save.get("items", {"idx": row.idx})
|
||||
if prev_row and prev_row[0].serial_and_batch_bundle != row.serial_and_batch_bundle:
|
||||
sabb_doc = frappe.get_doc("Serial and Batch Bundle", row.serial_and_batch_bundle)
|
||||
sabb_doc.validate_serial_no_status()
|
||||
|
||||
def reset_conversion_factor(self):
|
||||
for row in self.get("items"):
|
||||
if row.uom != row.stock_uom:
|
||||
@@ -1094,6 +1135,16 @@ class StockController(AccountsController):
|
||||
continue
|
||||
|
||||
if qi_required: # validate row only if inspection is required on item level
|
||||
if self.doctype in [
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
] and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
):
|
||||
return
|
||||
|
||||
self.validate_qi_presence(row)
|
||||
if self.docstatus == 1:
|
||||
self.validate_qi_submission(row)
|
||||
@@ -1101,16 +1152,6 @@ class StockController(AccountsController):
|
||||
|
||||
def validate_qi_presence(self, row):
|
||||
"""Check if QI is present on row level. Warn on save and stop on submit if missing."""
|
||||
if self.doctype in [
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
"Sales Invoice",
|
||||
"Delivery Note",
|
||||
] and frappe.db.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
):
|
||||
return
|
||||
|
||||
if not row.quality_inspection:
|
||||
msg = _("Row #{0}: Quality Inspection is required for Item {1}").format(
|
||||
row.idx, frappe.bold(row.item_code)
|
||||
@@ -1591,7 +1632,7 @@ def get_gl_entries_for_preview(doctype, docname, fields):
|
||||
|
||||
def get_columns(raw_columns, fields):
|
||||
return [
|
||||
{"name": d.get("label"), "editable": False, "width": 110}
|
||||
{"name": d.get("label"), "editable": False, "width": 110, "fieldtype": d.get("fieldtype")}
|
||||
for d in raw_columns
|
||||
if not d.get("hidden") and d.get("fieldname") in fields
|
||||
]
|
||||
@@ -1661,7 +1702,7 @@ def check_item_quality_inspection(doctype, items):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
def make_quality_inspections(company, doctype, docname, items):
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
@@ -1680,6 +1721,7 @@ def make_quality_inspections(doctype, docname, items):
|
||||
|
||||
quality_inspection = frappe.get_doc(
|
||||
{
|
||||
"company": company,
|
||||
"doctype": "Quality Inspection",
|
||||
"inspection_type": "Incoming",
|
||||
"inspected_by": frappe.session.user,
|
||||
|
||||
@@ -989,6 +989,12 @@ class SubcontractingController(StockController):
|
||||
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
|
||||
return
|
||||
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
|
||||
== "BOM"
|
||||
):
|
||||
return
|
||||
|
||||
for row in self.get(self.raw_material_table):
|
||||
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
|
||||
if not self.__transferred_items or not self.__transferred_items.get(key):
|
||||
@@ -1297,6 +1303,55 @@ def make_rm_stock_entry(
|
||||
if target_doc and target_doc.get("items"):
|
||||
target_doc.items = []
|
||||
|
||||
def post_process(source_doc, target_doc):
|
||||
target_doc.purpose = "Send to Subcontractor"
|
||||
|
||||
if order_doctype == "Purchase Order":
|
||||
target_doc.purchase_order = source_doc.name
|
||||
else:
|
||||
target_doc.subcontracting_order = source_doc.name
|
||||
|
||||
target_doc.set_stock_entry_type()
|
||||
|
||||
for fg_item_code in fg_item_code_list:
|
||||
for rm_item in rm_items:
|
||||
if (
|
||||
rm_item.get("main_item_code") == fg_item_code
|
||||
or rm_item.get("item_code") == fg_item_code
|
||||
):
|
||||
rm_item_code = rm_item.get("rm_item_code")
|
||||
|
||||
items_dict = {
|
||||
rm_item_code: {
|
||||
rm_detail_field: rm_item.get("name"),
|
||||
"item_name": rm_item.get("item_name")
|
||||
or item_wh.get(rm_item_code, {}).get("item_name", ""),
|
||||
"description": item_wh.get(rm_item_code, {}).get("description", ""),
|
||||
"qty": rm_item.get("qty")
|
||||
or max(
|
||||
rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0
|
||||
),
|
||||
"from_warehouse": rm_item.get("warehouse")
|
||||
or rm_item.get("reserve_warehouse"),
|
||||
"to_warehouse": source_doc.supplier_warehouse,
|
||||
"stock_uom": rm_item.get("stock_uom"),
|
||||
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
|
||||
"main_item_code": fg_item_code,
|
||||
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
|
||||
"allow_alternative_item"
|
||||
),
|
||||
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
|
||||
"serial_no": rm_item.get("serial_no")
|
||||
if rm_item.get("use_serial_batch_fields")
|
||||
else None,
|
||||
"batch_no": rm_item.get("batch_no")
|
||||
if rm_item.get("use_serial_batch_fields")
|
||||
else None,
|
||||
}
|
||||
}
|
||||
|
||||
target_doc.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
stock_entry = get_mapped_doc(
|
||||
order_doctype,
|
||||
subcontract_order.name,
|
||||
@@ -1317,53 +1372,9 @@ def make_rm_stock_entry(
|
||||
},
|
||||
target_doc,
|
||||
ignore_child_tables=True,
|
||||
postprocess=post_process,
|
||||
)
|
||||
|
||||
stock_entry.purpose = "Send to Subcontractor"
|
||||
|
||||
if order_doctype == "Purchase Order":
|
||||
stock_entry.purchase_order = subcontract_order.name
|
||||
else:
|
||||
stock_entry.subcontracting_order = subcontract_order.name
|
||||
|
||||
stock_entry.set_stock_entry_type()
|
||||
|
||||
for fg_item_code in fg_item_code_list:
|
||||
for rm_item in rm_items:
|
||||
if (
|
||||
rm_item.get("main_item_code") == fg_item_code
|
||||
or rm_item.get("item_code") == fg_item_code
|
||||
):
|
||||
rm_item_code = rm_item.get("rm_item_code")
|
||||
items_dict = {
|
||||
rm_item_code: {
|
||||
rm_detail_field: rm_item.get("name"),
|
||||
"item_name": rm_item.get("item_name")
|
||||
or item_wh.get(rm_item_code, {}).get("item_name", ""),
|
||||
"description": item_wh.get(rm_item_code, {}).get("description", ""),
|
||||
"qty": rm_item.get("qty")
|
||||
or max(rm_item.get("required_qty") - rm_item.get("total_supplied_qty"), 0),
|
||||
"from_warehouse": rm_item.get("warehouse")
|
||||
or rm_item.get("reserve_warehouse"),
|
||||
"to_warehouse": subcontract_order.supplier_warehouse,
|
||||
"stock_uom": rm_item.get("stock_uom"),
|
||||
"serial_and_batch_bundle": rm_item.get("serial_and_batch_bundle"),
|
||||
"main_item_code": fg_item_code,
|
||||
"allow_alternative_item": item_wh.get(rm_item_code, {}).get(
|
||||
"allow_alternative_item"
|
||||
),
|
||||
"use_serial_batch_fields": rm_item.get("use_serial_batch_fields"),
|
||||
"serial_no": rm_item.get("serial_no")
|
||||
if rm_item.get("use_serial_batch_fields")
|
||||
else None,
|
||||
"batch_no": rm_item.get("batch_no")
|
||||
if rm_item.get("use_serial_batch_fields")
|
||||
else None,
|
||||
}
|
||||
}
|
||||
|
||||
stock_entry.add_to_stock_entry_detail(items_dict)
|
||||
|
||||
if target_doc:
|
||||
return stock_entry
|
||||
else:
|
||||
@@ -1395,6 +1406,8 @@ def add_items_in_ste(ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_deta
|
||||
def make_return_stock_entry_for_subcontract(
|
||||
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
|
||||
):
|
||||
rm_detail_field = "po_detail" if order_doctype == "Purchase Order" else "sco_rm_detail"
|
||||
|
||||
def post_process(source_doc, target_doc):
|
||||
target_doc.purpose = "Material Transfer"
|
||||
|
||||
@@ -1405,6 +1418,21 @@ def make_return_stock_entry_for_subcontract(
|
||||
|
||||
target_doc.company = source_doc.company
|
||||
target_doc.is_return = 1
|
||||
for _key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if item_details := value.get("item_details"):
|
||||
item_details["serial_and_batch_bundle"] = None
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(target_doc, value, qty, rm_details, rm_detail_field, batch_no)
|
||||
else:
|
||||
add_items_in_ste(target_doc, value, value.qty, rm_details, rm_detail_field)
|
||||
|
||||
target_doc.set_stock_entry_type()
|
||||
|
||||
ste_doc = get_mapped_doc(
|
||||
order_doctype,
|
||||
@@ -1419,27 +1447,6 @@ def make_return_stock_entry_for_subcontract(
|
||||
postprocess=post_process,
|
||||
)
|
||||
|
||||
if order_doctype == "Purchase Order":
|
||||
rm_detail_field = "po_detail"
|
||||
else:
|
||||
rm_detail_field = "sco_rm_detail"
|
||||
|
||||
for _key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if item_details := value.get("item_details"):
|
||||
item_details["serial_and_batch_bundle"] = None
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(ste_doc, value, qty, rm_details, rm_detail_field, batch_no)
|
||||
else:
|
||||
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
|
||||
|
||||
ste_doc.set_stock_entry_type()
|
||||
|
||||
return ste_doc
|
||||
|
||||
|
||||
|
||||
@@ -602,6 +602,11 @@ class calculate_taxes_and_totals:
|
||||
else:
|
||||
self.grand_total_diff = 0
|
||||
|
||||
# Apply rounding adjustment to grand_total_for_distributing_discount
|
||||
# to prevent precision errors during discount distribution
|
||||
if hasattr(self, "grand_total_for_distributing_discount") and not self.discount_amount_applied:
|
||||
self.grand_total_for_distributing_discount += self.grand_total_diff
|
||||
|
||||
def calculate_totals(self):
|
||||
grand_total_diff = self.grand_total_diff
|
||||
|
||||
|
||||
@@ -59,3 +59,41 @@ class TestTaxesAndTotals(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertEqual(so.total, 1500)
|
||||
self.assertAlmostEqual(so.net_total, 1272.73, places=2)
|
||||
self.assertEqual(so.grand_total, 1400)
|
||||
|
||||
def test_100_percent_discount_with_inclusive_tax(self):
|
||||
"""Test that 100% discount with inclusive taxes results in zero net_total"""
|
||||
so = make_sales_order(do_not_save=1)
|
||||
so.apply_discount_on = "Grand Total"
|
||||
so.items[0].qty = 2
|
||||
so.items[0].rate = 1300
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Account VAT",
|
||||
"included_in_print_rate": True,
|
||||
"rate": 9,
|
||||
},
|
||||
)
|
||||
so.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Service Tax - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "Account Service Tax",
|
||||
"included_in_print_rate": True,
|
||||
"rate": 9,
|
||||
},
|
||||
)
|
||||
so.save()
|
||||
|
||||
# Apply 100% discount
|
||||
so.discount_amount = 2600
|
||||
calculate_taxes_and_totals(so)
|
||||
|
||||
# net_total should be exactly 0, not 0.01
|
||||
self.assertEqual(so.net_total, 0)
|
||||
self.assertEqual(so.grand_total, 0)
|
||||
|
||||
@@ -55,6 +55,14 @@ def validate_filters(filters):
|
||||
if filters.get("based_on") == filters.get("group_by"):
|
||||
frappe.throw(_("'Based On' and 'Group By' can not be same"))
|
||||
|
||||
if filters.get("period_based_on") and filters.period_based_on not in ["bill_date", "posting_date"]:
|
||||
frappe.throw(
|
||||
msg=_("{0} can be either {1} or {2}.").format(
|
||||
frappe.bold("Period based On"), frappe.bold("Posting Date"), frappe.bold("Billing Date")
|
||||
),
|
||||
title=_("Invalid Filter"),
|
||||
)
|
||||
|
||||
|
||||
def get_data(filters, conditions):
|
||||
data = []
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-12-07 10:44:22.587047",
|
||||
"modified": "2026-03-25 19:27:19.162421",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Contract Template",
|
||||
@@ -75,43 +75,34 @@
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,8 +204,22 @@ def send_mail(entry, email_campaign):
|
||||
|
||||
# called from hooks on doc_event Email Unsubscribe
|
||||
def unsubscribe_recipient(unsubscribe, method):
|
||||
if unsubscribe.reference_doctype == "Email Campaign":
|
||||
frappe.db.set_value("Email Campaign", unsubscribe.reference_name, "status", "Unsubscribed")
|
||||
if unsubscribe.reference_doctype != "Email Campaign":
|
||||
return
|
||||
|
||||
email_campaign = frappe.get_doc("Email Campaign", unsubscribe.reference_name)
|
||||
|
||||
if email_campaign.email_campaign_for == "Email Group":
|
||||
if unsubscribe.email:
|
||||
frappe.db.set_value(
|
||||
"Email Group Member",
|
||||
{"email_group": email_campaign.recipient, "email": unsubscribe.email},
|
||||
"unsubscribed",
|
||||
1,
|
||||
)
|
||||
else:
|
||||
# For Lead or Contact
|
||||
frappe.db.set_value("Email Campaign", email_campaign.name, "status", "Unsubscribed")
|
||||
|
||||
|
||||
# called through hooks to update email campaign status daily
|
||||
|
||||
@@ -1,96 +1,48 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:market_segment",
|
||||
"beta": 0,
|
||||
"creation": "2018-10-01 09:59:14.479509",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:market_segment",
|
||||
"creation": "2018-10-01 09:59:14.479509",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"market_segment"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "market_segment",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Market Segment",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"fieldname": "market_segment",
|
||||
"fieldtype": "Data",
|
||||
"label": "Market Segment",
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-10-01 09:59:14.479509",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Market Segment",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2025-12-17 12:09:34.687368",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Market Segment",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1,
|
||||
"translated_doctype": 1
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user