mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-23 19:19:40 +00:00
Compare commits
355 Commits
v13.35.0
...
remove_ret
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9bf74e589 | ||
|
|
2fca8b541e | ||
|
|
7fc460bb32 | ||
|
|
e2e69dced7 | ||
|
|
5e49b6ea0f | ||
|
|
2e3445fad9 | ||
|
|
3c10e5066a | ||
|
|
e8d2e49155 | ||
|
|
5690e9771a | ||
|
|
ec6cac8043 | ||
|
|
0329642116 | ||
|
|
3c3e3cfcf8 | ||
|
|
7c4f5fa5c5 | ||
|
|
367e05f808 | ||
|
|
2dcb35da33 | ||
|
|
94732479f5 | ||
|
|
b4dec4630d | ||
|
|
c415a47d25 | ||
|
|
1e0cc65c61 | ||
|
|
37dbc7043a | ||
|
|
0e88496607 | ||
|
|
ef86b437cb | ||
|
|
b7b0076743 | ||
|
|
ef5dd1d693 | ||
|
|
a53b40ba93 | ||
|
|
e78a7679a4 | ||
|
|
6a5beecb36 | ||
|
|
2b900e2f0e | ||
|
|
bc14dbbcad | ||
|
|
f4e53a5c91 | ||
|
|
3f99522764 | ||
|
|
81dd9722fe | ||
|
|
9c5d335360 | ||
|
|
c5c28615f5 | ||
|
|
ab32c09ff9 | ||
|
|
f2e63bc491 | ||
|
|
882aa96973 | ||
|
|
d29a033c09 | ||
|
|
b3125a56ed | ||
|
|
adfc57487b | ||
|
|
ac320e4d55 | ||
|
|
3256e2b8b7 | ||
|
|
b4ec4ccc56 | ||
|
|
a1826f215a | ||
|
|
181c0acf99 | ||
|
|
638d6b7177 | ||
|
|
f5132411eb | ||
|
|
e177f1e51b | ||
|
|
6f2a567c95 | ||
|
|
bb41d8bc47 | ||
|
|
d79aacd1cb | ||
|
|
44c3a322d3 | ||
|
|
5707118d58 | ||
|
|
8964612b8e | ||
|
|
514d280ee7 | ||
|
|
b10a2b87b6 | ||
|
|
3664a12bb9 | ||
|
|
0605cbf26b | ||
|
|
25d2847881 | ||
|
|
66cb3fd63f | ||
|
|
d22104ad40 | ||
|
|
0527393b09 | ||
|
|
741d6fcb9a | ||
|
|
1870dbf9a8 | ||
|
|
69a9724422 | ||
|
|
814dd36a3e | ||
|
|
17ad96998e | ||
|
|
20919c8626 | ||
|
|
56d8962e40 | ||
|
|
0a3ac82232 | ||
|
|
b736df3f0b | ||
|
|
ea995de4a3 | ||
|
|
b7c94e38a6 | ||
|
|
a3698524ca | ||
|
|
0f1f67dcae | ||
|
|
570729b73e | ||
|
|
a46dca57cb | ||
|
|
821c1cddfc | ||
|
|
8217c6dd9f | ||
|
|
fc030a7de9 | ||
|
|
1157bf887f | ||
|
|
e64e812679 | ||
|
|
6025df97ef | ||
|
|
4793adfefd | ||
|
|
7686c9e450 | ||
|
|
074d484d3c | ||
|
|
332e86af7e | ||
|
|
8eded437d2 | ||
|
|
bef7f6114d | ||
|
|
4095a3dae2 | ||
|
|
0b40117bfd | ||
|
|
09de1cba92 | ||
|
|
51a40ad2fc | ||
|
|
261405cec1 | ||
|
|
1ac9d7f43e | ||
|
|
69b3145be4 | ||
|
|
5e1d367d7a | ||
|
|
10f0baf6ad | ||
|
|
20694eb509 | ||
|
|
c2b8d1bd9a | ||
|
|
49bf05c1c2 | ||
|
|
2cf2885470 | ||
|
|
edfaf99388 | ||
|
|
3a7a53ab72 | ||
|
|
34537c9dc2 | ||
|
|
154adcbb58 | ||
|
|
6a9d13ff28 | ||
|
|
7c85b487cd | ||
|
|
20e9599fc1 | ||
|
|
b4c992dd4d | ||
|
|
d46ce05dcd | ||
|
|
6bc6681f92 | ||
|
|
75d57c1f77 | ||
|
|
fc7285cc85 | ||
|
|
23a441252b | ||
|
|
474f34c04c | ||
|
|
6606028062 | ||
|
|
f58427bffe | ||
|
|
89f889068e | ||
|
|
9263d5ff4d | ||
|
|
4df197a282 | ||
|
|
b495969eb8 | ||
|
|
5f4760985c | ||
|
|
4a61840e39 | ||
|
|
ca48b9de54 | ||
|
|
baab96d02c | ||
|
|
f5b967a947 | ||
|
|
06b1bcbf59 | ||
|
|
aa8a063103 | ||
|
|
3f46b2a0ce | ||
|
|
b52962751c | ||
|
|
b94154f00a | ||
|
|
90ed14a055 | ||
|
|
b65bce8f98 | ||
|
|
66578a58cf | ||
|
|
476ded2972 | ||
|
|
ffb1196516 | ||
|
|
a452143782 | ||
|
|
6405a9378d | ||
|
|
2471ca58ee | ||
|
|
a4828407d0 | ||
|
|
36130c6292 | ||
|
|
4775c42593 | ||
|
|
b0917aaf8a | ||
|
|
d349a337e1 | ||
|
|
ba5ee1ca96 | ||
|
|
41b8563b7a | ||
|
|
7ee75ff762 | ||
|
|
3274e7681d | ||
|
|
7ddb332f47 | ||
|
|
2a615af00a | ||
|
|
ba8dc8925f | ||
|
|
14891ccf18 | ||
|
|
15654aae8b | ||
|
|
d1de4b027c | ||
|
|
a7d66fa352 | ||
|
|
8dea238d12 | ||
|
|
acbed434c2 | ||
|
|
9751f1060e | ||
|
|
0569f8a857 | ||
|
|
63715bf229 | ||
|
|
63cd4349a6 | ||
|
|
42b395916d | ||
|
|
0057c10a7d | ||
|
|
3c8c5d01fb | ||
|
|
d820757359 | ||
|
|
14e59c86aa | ||
|
|
9e16f4e412 | ||
|
|
6d269a4d89 | ||
|
|
4e8b39ab3f | ||
|
|
9925eb9982 | ||
|
|
301d199ece | ||
|
|
6c574fbf33 | ||
|
|
80981d025f | ||
|
|
a420d0242c | ||
|
|
809d5caf80 | ||
|
|
6f442d14cf | ||
|
|
a17acfdaa2 | ||
|
|
3673dea03b | ||
|
|
fb3725752f | ||
|
|
16c94d292c | ||
|
|
0e46b33ee3 | ||
|
|
df716fbd0c | ||
|
|
0dbfb1589e | ||
|
|
473a43b6b1 | ||
|
|
1a3b3b96c2 | ||
|
|
e9e53a74c9 | ||
|
|
26aef4fb1c | ||
|
|
b145fe3b3e | ||
|
|
d83c869d73 | ||
|
|
08c69c7a76 | ||
|
|
25da4d28ff | ||
|
|
f57d2fadbc | ||
|
|
9e7edd677a | ||
|
|
bc7494278d | ||
|
|
c442b4aef1 | ||
|
|
ef9b25cf08 | ||
|
|
2f2d3de306 | ||
|
|
13639682ce | ||
|
|
c01adae8a5 | ||
|
|
8ae691d9dc | ||
|
|
a95c011a93 | ||
|
|
42f3592e95 | ||
|
|
0602848caa | ||
|
|
9985d28571 | ||
|
|
31824c2280 | ||
|
|
18d93f8398 | ||
|
|
c9443123f9 | ||
|
|
0fdec8fac8 | ||
|
|
6fbb06a878 | ||
|
|
89348c1bb4 | ||
|
|
598dbc93ac | ||
|
|
51a4ef3069 | ||
|
|
49fb177f86 | ||
|
|
b0aae8a297 | ||
|
|
e310347ca4 | ||
|
|
9b1544aa14 | ||
|
|
a8da5f4566 | ||
|
|
0e469f6d95 | ||
|
|
b393c230bd | ||
|
|
32c1bb61de | ||
|
|
4f023757de | ||
|
|
d2d25e17bb | ||
|
|
a843e784e6 | ||
|
|
8a6432ec3f | ||
|
|
b97d30aad0 | ||
|
|
bc7cfe6919 | ||
|
|
85802b0f97 | ||
|
|
dc67d39ce6 | ||
|
|
529a47bc88 | ||
|
|
2381b81aac | ||
|
|
dbf245c687 | ||
|
|
5cdc267aee | ||
|
|
95e1021caf | ||
|
|
7d50f8798a | ||
|
|
dd751c6e92 | ||
|
|
69087b8790 | ||
|
|
763787b0a5 | ||
|
|
2ae3bd9fed | ||
|
|
1998223ee3 | ||
|
|
794fd0819f | ||
|
|
1d69ce1932 | ||
|
|
8e23c6ad69 | ||
|
|
32ba8c7d6d | ||
|
|
4d67695294 | ||
|
|
50fbfd9839 | ||
|
|
e36c74ac44 | ||
|
|
45e25b2d56 | ||
|
|
55399f2e58 | ||
|
|
a5cd81c73c | ||
|
|
52c8e0a6ea | ||
|
|
616e3c66b9 | ||
|
|
a6e97ea9ab | ||
|
|
2c5e117f01 | ||
|
|
a65dd58444 | ||
|
|
04b077a89f | ||
|
|
1180135067 | ||
|
|
eae4b06a80 | ||
|
|
b0efb98237 | ||
|
|
e7e1847ec9 | ||
|
|
a5dae9264d | ||
|
|
284095106c | ||
|
|
8eb9aaafe9 | ||
|
|
6938025952 | ||
|
|
6e6b55ece0 | ||
|
|
8c9035d914 | ||
|
|
cfb11f4b84 | ||
|
|
feffc3af27 | ||
|
|
ca9c6e1651 | ||
|
|
f0ecdbef5a | ||
|
|
31930a16fa | ||
|
|
4f55fef782 | ||
|
|
72e8c02ae0 | ||
|
|
f02596242c | ||
|
|
431d79f516 | ||
|
|
711501be5e | ||
|
|
abb7ac5a0b | ||
|
|
85c554eab5 | ||
|
|
903e42fbdd | ||
|
|
12d3d6ab49 | ||
|
|
26497f9c0e | ||
|
|
6a7549d4b2 | ||
|
|
90c751f648 | ||
|
|
1090862296 | ||
|
|
425abcdea7 | ||
|
|
144057f7e3 | ||
|
|
9631ffd215 | ||
|
|
9a3c84663f | ||
|
|
2066e5a53a | ||
|
|
b147ce4206 | ||
|
|
0d6beed546 | ||
|
|
844758a27c | ||
|
|
1f4d434675 | ||
|
|
01d6df45d0 | ||
|
|
8c7b836de8 | ||
|
|
9fd1fad259 | ||
|
|
dfa0638baf | ||
|
|
0a5bc86d1b | ||
|
|
bdd15895ff | ||
|
|
82539fc18a | ||
|
|
cb80bcd0ee | ||
|
|
84f260e1d8 | ||
|
|
023c5db3bc | ||
|
|
0b3958d692 | ||
|
|
c2a7dc7754 | ||
|
|
8237d94b11 | ||
|
|
0713a240a7 | ||
|
|
61b2a1c3cb | ||
|
|
94be7a95f7 | ||
|
|
7ba0326bdd | ||
|
|
86af1ef6ed | ||
|
|
b995338d0c | ||
|
|
0856e14c13 | ||
|
|
ff5ee2bbac | ||
|
|
a36e8a83d8 | ||
|
|
d81e7e2421 | ||
|
|
8181d61293 | ||
|
|
548aac5b11 | ||
|
|
de69fee6aa | ||
|
|
c3e6d7ba29 | ||
|
|
d5ce780e87 | ||
|
|
a2125e694d | ||
|
|
0d94653c5b | ||
|
|
d9723a12c4 | ||
|
|
d24cb01a9d | ||
|
|
ab214bcdfc | ||
|
|
751fbd6794 | ||
|
|
36566c1d14 | ||
|
|
28e4e4320e | ||
|
|
1133062bfc | ||
|
|
a28c7cf094 | ||
|
|
1c686c732e | ||
|
|
66c5290dee | ||
|
|
9ac9c46dab | ||
|
|
faa489bc73 | ||
|
|
b17850ac3e | ||
|
|
7d36bdf292 | ||
|
|
b331c462ef | ||
|
|
b0e17dea2a | ||
|
|
be3b010b44 | ||
|
|
45799f51b3 | ||
|
|
bac854628f | ||
|
|
6d04bafb04 | ||
|
|
c4f39c7b8b | ||
|
|
2045df19f9 | ||
|
|
07b80c295d | ||
|
|
a8c3882400 | ||
|
|
2a432c22d4 | ||
|
|
776e807ade | ||
|
|
6bda2a0865 | ||
|
|
01beb6f391 | ||
|
|
d182137ed1 | ||
|
|
fecf567e92 | ||
|
|
96bd493a6f | ||
|
|
def622c13e |
@@ -10,7 +10,7 @@
|
||||
"@semantic-release/release-notes-generator",
|
||||
[
|
||||
"@semantic-release/exec", {
|
||||
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" erpnext/__init__.py'
|
||||
"prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" erpnext/__init__.py'
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
@@ -65,6 +65,8 @@ GNU/General Public License (see [license.txt](license.txt))
|
||||
|
||||
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
|
||||
|
||||
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.35.0"
|
||||
__version__ = "13.36.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
"post_change_gl_entries",
|
||||
"enable_discount_accounting",
|
||||
"tax_settings_section",
|
||||
@@ -276,14 +277,21 @@
|
||||
"fieldname": "enable_common_party_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
}
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
|
||||
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow multi-currency invoices against single party account"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-11 17:42:36.427699",
|
||||
"modified": "2022-07-11 13:37:50.605141",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -43,7 +43,7 @@ class GLEntry(Document):
|
||||
self.validate_and_set_fiscal_year()
|
||||
self.pl_must_have_cost_center()
|
||||
|
||||
if not self.flags.from_repost:
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
self.check_mandatory()
|
||||
self.validate_cost_center()
|
||||
self.check_pl_account()
|
||||
@@ -52,7 +52,7 @@ class GLEntry(Document):
|
||||
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost:
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
self.validate_allowed_dimensions()
|
||||
@@ -358,7 +358,7 @@ def update_outstanding_amt(
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
|
||||
|
||||
# Didn't use db_set for optimisation purpose
|
||||
# Didn't use db_set for optimization purpose
|
||||
ref_doc.outstanding_amount = bal
|
||||
frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)
|
||||
|
||||
|
||||
@@ -25,7 +25,10 @@ from erpnext.accounts.utils import (
|
||||
get_stock_and_account_balance,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import (
|
||||
get_outstanding_amount_for_claim,
|
||||
update_reimbursed_amount,
|
||||
)
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -935,15 +938,12 @@ class JournalEntry(AccountsController):
|
||||
def validate_expense_claim(self):
|
||||
for d in self.accounts:
|
||||
if d.reference_type == "Expense Claim":
|
||||
sanctioned_amount, reimbursed_amount = frappe.db.get_value(
|
||||
"Expense Claim", d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed")
|
||||
)
|
||||
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
|
||||
if d.debit > pending_amount:
|
||||
outstanding_amt = get_outstanding_amount_for_claim(d.reference_name)
|
||||
if d.debit > outstanding_amt:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}"
|
||||
).format(d.idx, d.reference_name, pending_amount)
|
||||
"Row No {0}: Amount cannot be greater than the Outstanding Amount against Expense Claim {1}. Outstanding Amount is {2}"
|
||||
).format(d.idx, d.reference_name, outstanding_amt)
|
||||
)
|
||||
|
||||
def validate_credit_debit_note(self):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Journal Entry Template", {
|
||||
setup: function(frm) {
|
||||
refresh: function(frm) {
|
||||
frappe.model.set_default_values(frm.doc);
|
||||
|
||||
frm.set_query("account" ,"accounts", function(){
|
||||
|
||||
@@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
||||
doc: frm.doc,
|
||||
btn: $(btn_primary),
|
||||
method: "make_invoices",
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
|
||||
freeze: 1,
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
||||
callback: function(r) {
|
||||
if (r.message.length == 1) {
|
||||
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
|
||||
} else if (r.message.length < 50) {
|
||||
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ from erpnext.controllers.accounts_controller import (
|
||||
get_supplier_block_status,
|
||||
validate_taxes_and_charges,
|
||||
)
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import (
|
||||
get_outstanding_amount_for_claim,
|
||||
update_reimbursed_amount,
|
||||
)
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
@@ -302,7 +305,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def validate_reference_documents(self):
|
||||
if self.party_type == "Student":
|
||||
valid_reference_doctypes = "Fees"
|
||||
valid_reference_doctypes = ("Fees", "Journal Entry")
|
||||
elif self.party_type == "Customer":
|
||||
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
|
||||
elif self.party_type == "Supplier":
|
||||
@@ -1649,12 +1652,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||
bill_no = ref_doc.get("bill_no")
|
||||
elif reference_doctype == "Expense Claim":
|
||||
outstanding_amount = (
|
||||
flt(ref_doc.get("total_sanctioned_amount"))
|
||||
+ flt(ref_doc.get("total_taxes_and_charges"))
|
||||
- flt(ref_doc.get("total_amount_reimbursed"))
|
||||
- flt(ref_doc.get("total_advance_amount"))
|
||||
)
|
||||
outstanding_amount = get_outstanding_amount_for_claim(ref_doc)
|
||||
elif reference_doctype == "Employee Advance":
|
||||
outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
|
||||
if party_account_currency != ref_doc.currency:
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
"fiscal_year",
|
||||
"amended_from",
|
||||
"company",
|
||||
"cost_center_wise_pnl",
|
||||
"column_break1",
|
||||
"closing_account_head",
|
||||
"remarks"
|
||||
"remarks",
|
||||
"gle_processing_status",
|
||||
"error_message"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -86,17 +87,26 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "cost_center_wise_pnl",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Cost Center Wise Profit/Loss"
|
||||
"depends_on": "eval:doc.docstatus!=0",
|
||||
"fieldname": "gle_processing_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "GL Entry Processing Status",
|
||||
"options": "In Progress\nCompleted\nFailed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.gle_processing_status=='Failed'",
|
||||
"fieldname": "error_message",
|
||||
"fieldtype": "Text",
|
||||
"label": "Error Message",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-20 15:27:37.210458",
|
||||
"modified": "2022-07-20 14:51:04.714154",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Period Closing Voucher",
|
||||
|
||||
@@ -8,7 +8,6 @@ from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimensions,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
@@ -20,13 +19,28 @@ class PeriodClosingVoucher(AccountsController):
|
||||
self.validate_posting_date()
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
gle_count = frappe.db.count(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
)
|
||||
if gle_count > 5000:
|
||||
frappe.enqueue(
|
||||
make_reverse_gl_entries,
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||
)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
def validate_account_head(self):
|
||||
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
|
||||
@@ -67,90 +81,80 @@ class PeriodClosingVoucher(AccountsController):
|
||||
def make_gl_entries(self):
|
||||
gl_entries = self.get_gl_entries()
|
||||
if gl_entries:
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
make_gl_entries(gl_entries)
|
||||
if len(gl_entries) > 5000:
|
||||
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_entries(gl_entries)
|
||||
|
||||
def get_gl_entries(self):
|
||||
gl_entries = []
|
||||
pl_accounts = self.get_pl_balances()
|
||||
|
||||
for acc in pl_accounts:
|
||||
# pl account
|
||||
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) > 0
|
||||
else 0,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
)
|
||||
gl_entries.append(self.get_gle_for_pl_account(acc))
|
||||
|
||||
if gl_entries:
|
||||
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
|
||||
gl_entries += gle_for_net_pl_bal
|
||||
# closing liability account
|
||||
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gle_for_closing_account(acc))
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_pnl_gl_entry(self, pl_accounts):
|
||||
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
||||
gl_entries = []
|
||||
def get_gle_for_pl_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
for acc in pl_accounts:
|
||||
if flt(acc.bal_in_company_currency):
|
||||
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) > 0
|
||||
else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
def get_gle_for_closing_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
self.update_default_dimensions(gl_entry)
|
||||
|
||||
gl_entries.append(gl_entry)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def update_default_dimensions(self, gl_entry):
|
||||
def update_default_dimensions(self, gl_entry, acc):
|
||||
if not self.accounting_dimensions:
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
_, default_dimensions = get_dimensions()
|
||||
for dimension in self.accounting_dimensions:
|
||||
gl_entry.update({dimension: default_dimensions.get(self.company, {}).get(dimension)})
|
||||
gl_entry.update({dimension: acc.get(dimension)})
|
||||
|
||||
def get_pl_balances(self):
|
||||
def get_pl_balances_based_on_dimensions(self, group_by_account=False):
|
||||
"""Get balance for dimension-wise pl accounts"""
|
||||
|
||||
dimension_fields = ["t1.cost_center", "t1.finance_book"]
|
||||
@@ -159,20 +163,56 @@ class PeriodClosingVoucher(AccountsController):
|
||||
for dimension in self.accounting_dimensions:
|
||||
dimension_fields.append("t1.{0}".format(dimension))
|
||||
|
||||
if group_by_account:
|
||||
dimension_fields.append("t1.account")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
t1.account, t2.account_currency, {dimension_fields},
|
||||
t2.account_currency,
|
||||
{dimension_fields},
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2 and t2.company = %s
|
||||
and t1.posting_date between %s and %s
|
||||
group by t1.account, {dimension_fields}
|
||||
where
|
||||
t1.is_cancelled = 0
|
||||
and t1.account = t2.name
|
||||
and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2
|
||||
and t2.company = %s
|
||||
and t1.posting_date between %s and %s
|
||||
group by {dimension_fields}
|
||||
""".format(
|
||||
dimension_fields=", ".join(dimension_fields)
|
||||
),
|
||||
(self.company, self.get("year_start_date"), self.posting_date),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def process_gl_entries(gl_entries):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
frappe.db.set_value(
|
||||
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
|
||||
)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value(
|
||||
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
|
||||
)
|
||||
|
||||
|
||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
try:
|
||||
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
|
||||
|
||||
@@ -49,7 +49,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
expected_gle = (
|
||||
("Cost of Goods Sold - TPC", 0.0, 600.0),
|
||||
(surplus_account, 600.0, 400.0),
|
||||
(surplus_account, 200.0, 0.0),
|
||||
("Sales - TPC", 400.0, 0.0),
|
||||
)
|
||||
|
||||
@@ -59,7 +59,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
""",
|
||||
(pcv.name),
|
||||
)
|
||||
|
||||
pcv.reload()
|
||||
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||
self.assertEqual(pcv_gle, expected_gle)
|
||||
|
||||
def test_cost_center_wise_posting(self):
|
||||
@@ -94,7 +95,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
)
|
||||
|
||||
pcv = self.make_period_closing_voucher(submit=False)
|
||||
pcv.cost_center_wise_pnl = 1
|
||||
pcv.save()
|
||||
pcv.submit()
|
||||
surplus_account = pcv.closing_account_head
|
||||
@@ -117,6 +117,16 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
self.assertEqual(pcv_gle, expected_gle)
|
||||
|
||||
pcv.reload()
|
||||
pcv.cancel()
|
||||
|
||||
self.assertFalse(
|
||||
frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": pcv.name, "is_cancelled": 0},
|
||||
)
|
||||
)
|
||||
|
||||
def test_period_closing_with_finance_book_entries(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
|
||||
|
||||
@@ -36,6 +36,15 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
});
|
||||
|
||||
set_html_data(frm);
|
||||
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if (!frm.doc.posting_date) {
|
||||
frm.set_value("posting_date", frappe.datetime.nowdate());
|
||||
}
|
||||
if (!frm.doc.posting_time) {
|
||||
frm.set_value("posting_time", frappe.datetime.now_time());
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"period_end_date",
|
||||
"column_break_3",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"pos_opening_entry",
|
||||
"status",
|
||||
"section_break_5",
|
||||
@@ -51,7 +52,6 @@
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Period End Date",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -219,6 +219,13 @@
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Error",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
@@ -228,10 +235,11 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-20 16:19:25.340565",
|
||||
"modified": "2022-08-01 11:37:14.991228",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -278,5 +286,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -15,6 +15,9 @@ from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
class POSClosingEntry(StatusUpdater):
|
||||
def validate(self):
|
||||
self.posting_date = self.posting_date or frappe.utils.nowdate()
|
||||
self.posting_time = self.posting_time or frappe.utils.nowtime()
|
||||
|
||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||
|
||||
|
||||
@@ -222,9 +222,6 @@ class POSInvoice(SalesInvoice):
|
||||
allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
|
||||
|
||||
for d in self.get("items"):
|
||||
is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item"))
|
||||
if is_service_item:
|
||||
return
|
||||
if d.serial_no:
|
||||
self.validate_pos_reserved_serial_nos(d)
|
||||
self.validate_delivered_serial_nos(d)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"merge_invoices_based_on",
|
||||
"column_break_3",
|
||||
"pos_closing_entry",
|
||||
@@ -105,12 +106,19 @@
|
||||
"label": "Customer Group",
|
||||
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
|
||||
"options": "Customer Group"
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-14 11:17:19.001142",
|
||||
"modified": "2022-08-01 11:36:42.456429",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
@@ -173,5 +181,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.utils import flt, getdate, nowdate
|
||||
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
@@ -79,6 +79,7 @@ class POSInvoiceMergeLog(Document):
|
||||
if sales:
|
||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||
|
||||
self.flags.ignore_validate_update_after_submit = True
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
@@ -99,6 +100,7 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice.is_consolidated = 1
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.posting_time = get_time(self.posting_time)
|
||||
sales_invoice.save()
|
||||
sales_invoice.submit()
|
||||
|
||||
@@ -115,6 +117,7 @@ class POSInvoiceMergeLog(Document):
|
||||
credit_note.is_consolidated = 1
|
||||
credit_note.set_posting_time = 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?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
@@ -219,6 +222,9 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.taxes_and_charges = None
|
||||
invoice.ignore_pricing_rule = 1
|
||||
invoice.customer = self.customer
|
||||
invoice.disable_rounded_total = cint(
|
||||
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
|
||||
)
|
||||
|
||||
if self.merge_invoices_based_on == "Customer Group":
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
@@ -399,6 +405,9 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
merge_log.posting_date = (
|
||||
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
|
||||
)
|
||||
merge_log.posting_time = (
|
||||
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
|
||||
)
|
||||
merge_log.customer = customer
|
||||
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
|
||||
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
"currency",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"write_off_limit",
|
||||
"account_for_change_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break_23",
|
||||
"income_account",
|
||||
"expense_account",
|
||||
@@ -358,6 +360,21 @@
|
||||
"fieldname": "validate_stock_on_save",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Stock on Save"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Auto write off precision loss while consolidation",
|
||||
"fieldname": "write_off_limit",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Write Off Limit",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the consolidated invoices will have rounded total disabled",
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -385,7 +402,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2022-03-21 13:29:28.480533",
|
||||
"modified": "2022-08-10 12:57:06.241439",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -34,4 +34,4 @@ class ProcessDeferredAccounting(Document):
|
||||
filters={"against_voucher_type": self.doctype, "against_voucher": self.name},
|
||||
)
|
||||
|
||||
make_gl_entries(gl_entries=gl_entries, cancel=1)
|
||||
make_gl_entries(gl_map=gl_entries, cancel=1)
|
||||
|
||||
@@ -57,3 +57,16 @@ class TestProcessDeferredAccounting(unittest.TestCase):
|
||||
]
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-10")
|
||||
|
||||
def test_pda_submission_and_cancellation(self):
|
||||
pda = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Process Deferred Accounting",
|
||||
posting_date="2019-01-01",
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-01-31",
|
||||
type="Income",
|
||||
)
|
||||
)
|
||||
pda.submit()
|
||||
pda.cancel()
|
||||
|
||||
@@ -539,7 +539,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
},
|
||||
|
||||
add_custom_buttons: function(frm) {
|
||||
if (frm.doc.per_received < 100) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
||||
frm.add_custom_button(__('Purchase Receipt'), () => {
|
||||
frm.events.make_purchase_receipt(frm);
|
||||
}, __('Create'));
|
||||
|
||||
@@ -158,6 +158,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if tds_category and not for_validate:
|
||||
self.apply_tds = 1
|
||||
self.tax_withholding_category = tds_category
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
super(PurchaseInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
@@ -566,7 +567,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.make_supplier_gl_entry(gl_entries)
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
if self.check_asset_cwip_enabled():
|
||||
self.get_asset_gl_entry(gl_entries)
|
||||
@@ -793,7 +793,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if not item.is_fixed_asset:
|
||||
dummy, amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting)
|
||||
dummy, amount = self.get_amount_and_base_amount(item, None)
|
||||
else:
|
||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
||||
|
||||
@@ -1109,7 +1109,7 @@ class PurchaseInvoice(BuyingController):
|
||||
valuation_tax = {}
|
||||
|
||||
for tax in self.get("taxes"):
|
||||
amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting)
|
||||
amount, base_amount = self.get_tax_amounts(tax, None)
|
||||
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
|
||||
@@ -1688,4 +1688,6 @@ def make_purchase_receipt(source_name, target_doc=None):
|
||||
target_doc,
|
||||
)
|
||||
|
||||
doc.set_onload("ignore_price_list", True)
|
||||
|
||||
return doc
|
||||
|
||||
@@ -304,59 +304,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
|
||||
def test_purchase_invoice_with_discount_accounting_enabled(self):
|
||||
enable_discount_accounting()
|
||||
|
||||
discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
pi = make_purchase_invoice(discount_account=discount_account, rate=45)
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()],
|
||||
["Creditors - _TC", 0.0, 225.0, nowdate()],
|
||||
["Discount Account - _TC", 0.0, 25.0, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
enable_discount_accounting(enable=0)
|
||||
|
||||
def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self):
|
||||
enable_discount_accounting()
|
||||
additional_discount_account = create_account(
|
||||
account_name="Discount Account",
|
||||
parent_account="Indirect Expenses - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC")
|
||||
pi.apply_discount_on = "Grand Total"
|
||||
pi.additional_discount_account = additional_discount_account
|
||||
pi.additional_discount_percentage = 10
|
||||
pi.disable_rounded_total = 1
|
||||
pi.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "Test",
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()],
|
||||
["_Test Account VAT - _TC", 25.0, 0.0, nowdate()],
|
||||
["Creditors - _TC", 0.0, 247.5, nowdate()],
|
||||
["Discount Account - _TC", 0.0, 27.5, nowdate()],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
def test_purchase_invoice_change_naming_series(self):
|
||||
pi = frappe.copy_doc(test_records[1])
|
||||
pi.insert()
|
||||
|
||||
@@ -480,9 +480,13 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
|
||||
is_cash_or_non_trade_discount() {
|
||||
this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount);
|
||||
this.frm.set_df_property("additional_discount_account", "reqd", this.frm.doc.is_cash_or_non_trade_discount);
|
||||
|
||||
if (!this.frm.doc.is_cash_or_non_trade_discount) {
|
||||
this.frm.set_value("additional_discount_account", "");
|
||||
}
|
||||
|
||||
this.calculate_taxes_and_totals();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||
"depends_on": "eval: doc.is_return",
|
||||
"fieldname": "update_billed_amount_in_sales_order",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
@@ -2046,7 +2046,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-06-16 16:22:44.870575",
|
||||
"modified": "2022-07-11 17:43:56.435382",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -1065,22 +1065,6 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
)
|
||||
|
||||
if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.additional_discount_account,
|
||||
"against": self.debit_to,
|
||||
"debit": self.base_discount_amount,
|
||||
"debit_in_account_currency": self.discount_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
},
|
||||
self.currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
for tax in self.get("taxes"):
|
||||
amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting)
|
||||
@@ -2173,13 +2157,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
|
||||
source_document_warehouse_field = "target_warehouse"
|
||||
target_document_warehouse_field = "from_warehouse"
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
else:
|
||||
source_doc = frappe.get_doc(doctype, source_name)
|
||||
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
|
||||
source_document_warehouse_field = "from_warehouse"
|
||||
target_document_warehouse_field = "target_warehouse"
|
||||
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
received_items = {}
|
||||
|
||||
validate_inter_company_transaction(source_doc, doctype)
|
||||
details = get_inter_company_details(source_doc, doctype)
|
||||
|
||||
@@ -2623,20 +2623,34 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
def test_einvoice_discounts(self):
|
||||
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
|
||||
|
||||
frappe.db.set_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice", False)
|
||||
|
||||
# Normal Itemized Discount
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = ""
|
||||
si.items[0].discount_amount = 4000
|
||||
si.items[1].discount_amount = 300
|
||||
si.items[0].price_list_rate = 12
|
||||
si.items[0].discount_percentage = 16.6666666667
|
||||
si.items[0].rate = 10
|
||||
|
||||
si.items[1].price_list_rate = 15
|
||||
si.items[1].discount_amount = 5
|
||||
si.items[1].rate = 10
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 4000)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 300)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 2100)
|
||||
self.assertEqual(einvoice["ItemList"][2]["Discount"], 222)
|
||||
self.assertEqual(einvoice["ItemList"][3]["Discount"], 5555)
|
||||
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
|
||||
|
||||
# Invoice Discount on net total
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = "Net Total"
|
||||
@@ -2646,10 +2660,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 316.83)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 83.17)
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 253.61)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 66.57)
|
||||
self.assertEqual(einvoice["ItemList"][2]["Discount"], 243.11)
|
||||
self.assertEqual(einvoice["ItemList"][3]["Discount"], 5613.71)
|
||||
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
|
||||
|
||||
# Invoice Discount on grand total (Itemized Discount)
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = "Grand Total"
|
||||
@@ -2659,10 +2680,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 268.5)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 70.48)
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 214.93)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 56.42)
|
||||
self.assertEqual(einvoice["ItemList"][2]["Discount"], 239.89)
|
||||
self.assertEqual(einvoice["ItemList"][3]["Discount"], 5604.75)
|
||||
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
|
||||
|
||||
# Invoice Discount on grand total (Cash/Non-Trade Discount)
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = "Grand Total"
|
||||
@@ -2675,8 +2703,133 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][2]["Discount"], 222.0)
|
||||
self.assertEqual(einvoice["ItemList"][3]["Discount"], 5555.0)
|
||||
self.assertEqual(einvoice["ValDtls"]["Discount"], 400)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
|
||||
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = ""
|
||||
si.items[1].price_list_rate = 15
|
||||
si.items[1].discount_amount = -5
|
||||
si.items[1].rate = 20
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
|
||||
|
||||
def test_einvoice_without_discounts(self):
|
||||
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
|
||||
|
||||
frappe.db.set_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice", True)
|
||||
|
||||
# Normal Itemized Discount
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = ""
|
||||
si.items[0].price_list_rate = 12
|
||||
si.items[0].discount_percentage = 16.6666666667
|
||||
si.items[0].rate = 10
|
||||
|
||||
si.items[1].price_list_rate = 15
|
||||
si.items[1].discount_amount = 5
|
||||
si.items[1].rate = 10
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][2]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][3]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 10)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 10)
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 18)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5)
|
||||
|
||||
# Invoice Discount on net total
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = "Net Total"
|
||||
si.discount_amount = 400
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][2]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][3]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 11.87)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 14.84)
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 17.81)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 4.95)
|
||||
|
||||
# Invoice Discount on grand total (Itemized Discount)
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = "Grand Total"
|
||||
si.discount_amount = 400
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][2]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][3]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ValDtls"]["Discount"], 0)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 11.89)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 14.87)
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 17.84)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 4.96)
|
||||
|
||||
# Invoice Discount on grand total (Cash/Non-Trade Discount)
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = "Grand Total"
|
||||
si.is_cash_or_non_trade_discount = 1
|
||||
si.discount_amount = 400
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][2]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][3]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ValDtls"]["Discount"], 400)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15)
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 18)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5)
|
||||
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = ""
|
||||
si.items[1].price_list_rate = 15
|
||||
si.items[1].discount_amount = -5
|
||||
si.items[1].rate = 20
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
@@ -3276,6 +3429,36 @@ def get_sales_invoice_for_e_invoice():
|
||||
},
|
||||
)
|
||||
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"uom": "Nos",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 111,
|
||||
"price_list_rate": 20,
|
||||
"discount_percentage": 10,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item 2",
|
||||
"uom": "Nos",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 1111,
|
||||
"price_list_rate": 10,
|
||||
"rate": 5,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
return si
|
||||
|
||||
|
||||
|
||||
@@ -279,7 +279,6 @@
|
||||
"label": "Discount (%) on Price List Rate with Margin",
|
||||
"oldfieldname": "adj_rate",
|
||||
"oldfieldtype": "Float",
|
||||
"precision": "2",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@@ -842,7 +841,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-24 14:41:36.392560",
|
||||
"modified": "2022-08-26 12:06:31.205417",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
||||
@@ -309,7 +309,6 @@ def get_advance_vouchers(
|
||||
"is_cancelled": 0,
|
||||
"party_type": party_type,
|
||||
"party": ["in", parties],
|
||||
"against_voucher": ["is", "not set"],
|
||||
}
|
||||
|
||||
if company:
|
||||
|
||||
@@ -148,6 +148,7 @@ def update_net_values(entry):
|
||||
def merge_similar_entries(gl_map, precision=None):
|
||||
merged_gl_map = []
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
for entry in gl_map:
|
||||
# if there is already an entry in this account then just add it
|
||||
# to that entry
|
||||
@@ -229,9 +230,10 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
gle.flags.from_repost = from_repost
|
||||
gle.flags.adv_adj = adv_adj
|
||||
gle.flags.update_outstanding = update_outstanding or "Yes"
|
||||
gle.flags.notify_update = False
|
||||
gle.submit()
|
||||
|
||||
if not from_repost:
|
||||
if not from_repost and gle.voucher_type != "Period Closing Voucher":
|
||||
validate_expense_against_budget(args)
|
||||
|
||||
|
||||
|
||||
@@ -208,7 +208,7 @@ def set_address_details(
|
||||
)
|
||||
|
||||
if company_address:
|
||||
party_details.update({"company_address": company_address})
|
||||
party_details.company_address = company_address
|
||||
else:
|
||||
party_details.update(get_company_address(company))
|
||||
|
||||
@@ -220,12 +220,37 @@ def set_address_details(
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]:
|
||||
if party_details.company_address:
|
||||
party_details["shipping_address"] = shipping_address or party_details["company_address"]
|
||||
party_details.shipping_address_display = get_address_display(party_details["shipping_address"])
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "shipping_address", party_details.shipping_address)
|
||||
{
|
||||
"shipping_address": shipping_address,
|
||||
"shipping_address_display": get_address_display(shipping_address),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address),
|
||||
}
|
||||
)
|
||||
|
||||
if party_details.company_address:
|
||||
# billing address
|
||||
party_details.update(
|
||||
{
|
||||
"billing_address": party_details.company_address,
|
||||
"billing_address_display": (
|
||||
party_details.company_address_display or get_address_display(party_details.company_address)
|
||||
),
|
||||
**get_fetch_values(doctype, "billing_address", party_details.company_address),
|
||||
}
|
||||
)
|
||||
|
||||
# shipping address - if not already set
|
||||
if not party_details.shipping_address:
|
||||
party_details.update(
|
||||
{
|
||||
"shipping_address": party_details.billing_address,
|
||||
"shipping_address_display": party_details.billing_address_display,
|
||||
**get_fetch_values(doctype, "shipping_address", party_details.billing_address),
|
||||
}
|
||||
)
|
||||
|
||||
get_regional_address_details(party_details, doctype, company)
|
||||
|
||||
return party_details.get(billing_address_field), party_details.shipping_address_name
|
||||
|
||||
@@ -535,7 +535,11 @@ def get_accounts(root_type, companies):
|
||||
):
|
||||
if account.account_name not in added_accounts:
|
||||
accounts.append(account)
|
||||
added_accounts.append(account.account_name)
|
||||
if account.account_number:
|
||||
account_key = account.account_number + "-" + account.account_name
|
||||
else:
|
||||
account_key = account.account_name
|
||||
added_accounts.append(account_key)
|
||||
|
||||
return accounts
|
||||
|
||||
|
||||
@@ -44,14 +44,14 @@ frappe.query_reports["Gross Profit"] = {
|
||||
"parent_field": "parent_invoice",
|
||||
"initial_depth": 3,
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
|
||||
if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) {
|
||||
column._options = "Sales Invoice";
|
||||
} else {
|
||||
column._options = "Item";
|
||||
}
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (data && (data.indent == 0.0 || row[1].content == "Total")) {
|
||||
if (data && (data.indent == 0.0 || (row[1] && row[1].content == "Total"))) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
value = $value.wrap("<p></p>").parent().html();
|
||||
|
||||
@@ -561,7 +561,7 @@ class GrossProfitGenerator(object):
|
||||
previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0
|
||||
|
||||
if previous_stock_value:
|
||||
return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
||||
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
|
||||
else:
|
||||
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
|
||||
else:
|
||||
|
||||
@@ -14,9 +14,9 @@ def execute(filters=None):
|
||||
filters.naming_series = frappe.db.get_single_value("Buying Settings", "supp_master_name")
|
||||
|
||||
columns = get_columns(filters)
|
||||
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
|
||||
tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters)
|
||||
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map)
|
||||
final_result = group_by_supplier_and_category(res)
|
||||
|
||||
return columns, final_result
|
||||
|
||||
@@ -8,11 +8,11 @@ from frappe import _
|
||||
|
||||
def execute(filters=None):
|
||||
validate_filters(filters)
|
||||
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
|
||||
tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map)
|
||||
return columns, res
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ def validate_filters(filters):
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
|
||||
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map):
|
||||
supplier_map = get_supplier_pan_map()
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
@@ -38,6 +38,11 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||
posting_date = entry.posting_date
|
||||
voucher_type = entry.voucher_type
|
||||
|
||||
if voucher_type == "Journal Entry":
|
||||
suppliers = journal_entry_party_map.get(name)
|
||||
if suppliers:
|
||||
supplier = suppliers[0]
|
||||
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
@@ -176,6 +181,7 @@ def get_tds_docs(filters):
|
||||
journal_entries = []
|
||||
tax_category_map = {}
|
||||
or_filters = {}
|
||||
journal_entry_party_map = {}
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
|
||||
tds_accounts = frappe.get_all(
|
||||
@@ -218,9 +224,24 @@ def get_tds_docs(filters):
|
||||
get_tax_category_map(payment_entries, "Payment Entry", tax_category_map)
|
||||
|
||||
if journal_entries:
|
||||
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
|
||||
get_tax_category_map(journal_entries, "Journal Entry", tax_category_map)
|
||||
|
||||
return tds_documents, tds_accounts, tax_category_map
|
||||
return tds_documents, tds_accounts, tax_category_map, journal_entry_party_map
|
||||
|
||||
|
||||
def get_journal_entry_party_map(journal_entries):
|
||||
journal_entry_party_map = {}
|
||||
for d in frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"parent": ("in", journal_entries), "party_type": "Supplier", "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)
|
||||
|
||||
return journal_entry_party_map
|
||||
|
||||
|
||||
def get_tax_category_map(vouchers, doctype, tax_category_map):
|
||||
|
||||
@@ -818,6 +818,31 @@ def get_held_invoices(party_type, party):
|
||||
return held_invoices
|
||||
|
||||
|
||||
def remove_return_pos_invoices(party_type, party, invoice_list):
|
||||
if invoice_list:
|
||||
|
||||
if party_type == "Customer":
|
||||
sinv = frappe.qb.DocType("Sales Invoice")
|
||||
return_pos = (
|
||||
frappe.qb.from_(sinv)
|
||||
.select(sinv.name)
|
||||
.where((sinv.is_pos == 1) & (sinv.docstatus == 1) & (sinv.is_return == 1))
|
||||
.run()
|
||||
)
|
||||
|
||||
if return_pos:
|
||||
return_pos = [x[0] for x in return_pos]
|
||||
else:
|
||||
return invoice_list
|
||||
|
||||
# remove pos return invoices from invoice_list
|
||||
for idx, inv in enumerate(invoice_list, 0):
|
||||
if inv.voucher_no in return_pos:
|
||||
del invoice_list[idx]
|
||||
|
||||
return invoice_list
|
||||
|
||||
|
||||
def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
|
||||
outstanding_invoices = []
|
||||
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
|
||||
@@ -868,6 +893,8 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
invoice_list = remove_return_pos_invoices(party_type, party, invoice_list)
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select against_voucher_type, against_voucher,
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"purchase_date",
|
||||
"section_break_23",
|
||||
"calculate_depreciation",
|
||||
"allow_monthly_depreciation",
|
||||
"column_break_33",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
@@ -454,13 +453,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "calculate_depreciation",
|
||||
"fieldname": "allow_monthly_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Monthly Depreciation"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "is_existing_asset",
|
||||
@@ -503,7 +495,7 @@
|
||||
"link_fieldname": "asset"
|
||||
}
|
||||
],
|
||||
"modified": "2022-01-30 20:19:24.680027",
|
||||
"modified": "2022-07-20 16:22:44.437579",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -545,4 +537,4 @@
|
||||
"sort_order": "DESC",
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -353,61 +353,16 @@ class Asset(AccountsController):
|
||||
skip_row = True
|
||||
|
||||
if depreciation_amount > 0:
|
||||
# With monthly depreciation, each depreciation is divided by months remaining until next date
|
||||
if self.allow_monthly_depreciation:
|
||||
# month range is 1 to 12
|
||||
# In pro rata case, for first and last depreciation, month range would be different
|
||||
if (has_pro_rata and n == 0 and not self.number_of_depreciations_booked) or (
|
||||
has_pro_rata and n == cint(number_of_pending_depreciations) - 1
|
||||
):
|
||||
month_range = months
|
||||
else:
|
||||
month_range = finance_book.frequency_of_depreciation
|
||||
|
||||
for r in range(month_range):
|
||||
if has_pro_rata and n == 0 and not self.number_of_depreciations_booked:
|
||||
# For first entry of monthly depr
|
||||
if r == 0:
|
||||
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1
|
||||
per_day_amt = depreciation_amount / days
|
||||
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
|
||||
depreciation_amount -= depreciation_amount_for_current_month
|
||||
date = monthly_schedule_date
|
||||
amount = depreciation_amount_for_current_month
|
||||
else:
|
||||
date = add_months(monthly_schedule_date, r)
|
||||
amount = depreciation_amount / (month_range - 1)
|
||||
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(
|
||||
month_range
|
||||
) - 1:
|
||||
# For last entry of monthly depr
|
||||
date = last_schedule_date
|
||||
amount = depreciation_amount / month_range
|
||||
else:
|
||||
date = add_months(monthly_schedule_date, r)
|
||||
amount = depreciation_amount / month_range
|
||||
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": date,
|
||||
"depreciation_amount": amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx,
|
||||
},
|
||||
)
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx,
|
||||
},
|
||||
)
|
||||
|
||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||
# JE: Journal Entry, FB: Finance Book
|
||||
@@ -853,7 +808,7 @@ class Asset(AccountsController):
|
||||
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
|
||||
|
||||
return 100 * (1 - flt(depreciation_rate, float_precision))
|
||||
return flt((100 * (1 - depreciation_rate)), float_precision)
|
||||
|
||||
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
|
||||
days = date_diff(to_date, from_date)
|
||||
|
||||
@@ -145,6 +145,7 @@ class TestAsset(AssetSetup):
|
||||
def test_is_fixed_asset_set(self):
|
||||
asset = create_asset(is_existing_asset=1)
|
||||
doc = frappe.new_doc("Purchase Invoice")
|
||||
doc.company = "_Test Company"
|
||||
doc.supplier = "_Test Supplier"
|
||||
doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name})
|
||||
|
||||
@@ -702,6 +703,8 @@ class TestDepreciationMethods(AssetSetup):
|
||||
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||
|
||||
def test_monthly_depreciation_by_wdv_method(self):
|
||||
existing_precision = frappe.db.get_default("float_precision")
|
||||
frappe.db.set_default("float_precision", 3)
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2022-02-15",
|
||||
@@ -715,12 +718,12 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2022-02-28", 645.0, 645.0],
|
||||
["2022-03-31", 1206.8, 1851.8],
|
||||
["2022-04-30", 1051.12, 2902.92],
|
||||
["2022-05-31", 915.52, 3818.44],
|
||||
["2022-06-30", 797.42, 4615.86],
|
||||
["2022-07-15", 384.14, 5000.0],
|
||||
["2022-02-28", 647.25, 647.25],
|
||||
["2022-03-31", 1210.71, 1857.96],
|
||||
["2022-04-30", 1053.99, 2911.95],
|
||||
["2022-05-31", 917.55, 3829.5],
|
||||
["2022-06-30", 798.77, 4628.27],
|
||||
["2022-07-15", 371.73, 5000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
@@ -731,8 +734,8 @@ class TestDepreciationMethods(AssetSetup):
|
||||
]
|
||||
for d in asset.get("schedules")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
frappe.db.set_default("float_precision", existing_precision)
|
||||
|
||||
|
||||
class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
@@ -15,9 +15,12 @@ frappe.ui.form.on("Request for Quotation",{
|
||||
frm.fields_dict["suppliers"].grid.get_field("contact").get_query = function(doc, cdt, cdn) {
|
||||
let d = locals[cdt][cdn];
|
||||
return {
|
||||
query: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_supplier_contacts",
|
||||
filters: {'supplier': d.supplier}
|
||||
}
|
||||
query: "frappe.contacts.doctype.contact.contact.contact_query",
|
||||
filters: {
|
||||
link_doctype: "Supplier",
|
||||
link_name: d.supplier || ""
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ class RequestforQuotation(BuyingController):
|
||||
|
||||
def get_link(self):
|
||||
# RFQ link for supplier portal
|
||||
return get_url("/rfq/" + self.name)
|
||||
return get_url("/app/request-for-quotation/" + self.name)
|
||||
|
||||
def update_supplier_part_no(self, supplier):
|
||||
self.vendor = supplier
|
||||
@@ -181,12 +181,20 @@ class RequestforQuotation(BuyingController):
|
||||
doc_args = self.as_dict()
|
||||
doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
|
||||
|
||||
# Get Contact Full Name
|
||||
supplier_name = None
|
||||
if data.get("contact"):
|
||||
contact_name = frappe.db.get_value(
|
||||
"Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
|
||||
)
|
||||
supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
|
||||
|
||||
args = {
|
||||
"update_password_link": update_password_link,
|
||||
"message": frappe.render_template(self.message_for_supplier, doc_args),
|
||||
"rfq_link": rfq_link,
|
||||
"user_fullname": full_name,
|
||||
"supplier_name": data.get("supplier_name"),
|
||||
"supplier_name": supplier_name or data.get("supplier_name"),
|
||||
"supplier_salutation": self.salutation or "Dear Mx.",
|
||||
}
|
||||
|
||||
@@ -279,18 +287,6 @@ def get_list_context(context=None):
|
||||
return list_context
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql(
|
||||
"""select `tabContact`.name from `tabContact`, `tabDynamic Link`
|
||||
where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s
|
||||
and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent
|
||||
limit %(start)s, %(page_len)s""",
|
||||
{"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None):
|
||||
def postprocess(source, target_doc):
|
||||
|
||||
@@ -65,7 +65,6 @@ class TestRequestforQuotation(FrappeTestCase):
|
||||
)
|
||||
sq.submit()
|
||||
|
||||
frappe.form_dict = frappe.local("form_dict")
|
||||
frappe.form_dict.name = rfq.name
|
||||
|
||||
self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True)
|
||||
|
||||
@@ -1105,17 +1105,17 @@ class AccountsController(TransactionBase):
|
||||
frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting")
|
||||
)
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
dr_or_cr = "credit"
|
||||
rev_dr_cr = "debit"
|
||||
supplier_or_customer = self.supplier
|
||||
|
||||
else:
|
||||
dr_or_cr = "debit"
|
||||
rev_dr_cr = "credit"
|
||||
supplier_or_customer = self.customer
|
||||
|
||||
if enable_discount_accounting:
|
||||
if self.doctype == "Purchase Invoice":
|
||||
dr_or_cr = "credit"
|
||||
rev_dr_cr = "debit"
|
||||
supplier_or_customer = self.supplier
|
||||
|
||||
else:
|
||||
dr_or_cr = "debit"
|
||||
rev_dr_cr = "credit"
|
||||
supplier_or_customer = self.customer
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.get("discount_amount") and item.get("discount_account"):
|
||||
discount_amount = item.discount_amount * item.qty
|
||||
@@ -1169,18 +1169,22 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
if self.get("discount_amount") and self.get("additional_discount_account"):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.additional_discount_account,
|
||||
"against": supplier_or_customer,
|
||||
dr_or_cr: self.discount_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
if (
|
||||
(enable_discount_accounting or self.get("is_cash_or_non_trade_discount"))
|
||||
and self.get("additional_discount_account")
|
||||
and self.get("discount_amount")
|
||||
):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.additional_discount_account,
|
||||
"against": supplier_or_customer,
|
||||
dr_or_cr: self.discount_amount,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
|
||||
from erpnext.controllers.status_updater import get_allowance_for
|
||||
@@ -1470,8 +1474,15 @@ class AccountsController(TransactionBase):
|
||||
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
|
||||
)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
allow_multi_currency_invoices_against_single_party_account = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "allow_multi_currency_invoices_against_single_party_account"
|
||||
)
|
||||
|
||||
if not party_gle_currency and (party_account_currency != self.currency):
|
||||
if (
|
||||
not party_gle_currency
|
||||
and (party_account_currency != self.currency)
|
||||
and not allow_multi_currency_invoices_against_single_party_account
|
||||
):
|
||||
frappe.throw(
|
||||
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
||||
frappe.bold(party_account), party_account_currency, self.currency
|
||||
|
||||
@@ -86,6 +86,7 @@ class BuyingController(StockController, Subcontracting):
|
||||
company=self.company,
|
||||
party_address=self.get("supplier_address"),
|
||||
shipping_address=self.get("shipping_address"),
|
||||
company_address=self.get("billing_address"),
|
||||
fetch_payment_terms_template=not self.get("ignore_default_payment_terms_template"),
|
||||
ignore_permissions=self.flags.ignore_permissions,
|
||||
)
|
||||
@@ -299,9 +300,14 @@ class BuyingController(StockController, Subcontracting):
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
rate = flt(outgoing_rate * d.conversion_factor, d.precision("rate"))
|
||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), "rate")
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
d.precision("rate"),
|
||||
)
|
||||
|
||||
if self.is_internal_transfer():
|
||||
if rate != d.rate:
|
||||
|
||||
@@ -18,8 +18,9 @@ from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Employee"
|
||||
conditions = []
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
fields = get_fields(doctype, ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabEmployee`
|
||||
@@ -49,7 +50,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Lead", ["name", "lead_name", "company_name"])
|
||||
doctype = "Lead"
|
||||
fields = get_fields(doctype, ["name", "lead_name", "company_name"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabLead`
|
||||
@@ -77,6 +79,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Customer"
|
||||
conditions = []
|
||||
cust_master_name = frappe.defaults.get_user_default("cust_master_name")
|
||||
|
||||
@@ -85,9 +88,9 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
else:
|
||||
fields = ["name", "customer_name", "customer_group", "territory"]
|
||||
|
||||
fields = get_fields("Customer", fields)
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
searchfields = frappe.get_meta("Customer").get_search_fields()
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
return frappe.db.sql(
|
||||
@@ -116,6 +119,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Supplier"
|
||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
||||
|
||||
if supp_master_name == "Supplier Name":
|
||||
@@ -123,7 +127,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
else:
|
||||
fields = ["name", "supplier_name", "supplier_group"]
|
||||
|
||||
fields = get_fields("Supplier", fields)
|
||||
fields = get_fields(doctype, fields)
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {field} from `tabSupplier`
|
||||
@@ -147,6 +151,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Account"
|
||||
company_currency = erpnext.get_company_currency(filters.get("company"))
|
||||
|
||||
def get_accounts(with_account_type_filter):
|
||||
@@ -197,13 +202,14 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
|
||||
doctype = "Item"
|
||||
conditions = []
|
||||
|
||||
if isinstance(filters, str):
|
||||
filters = json.loads(filters)
|
||||
|
||||
# Get searchfields from meta and use in Item Link field query
|
||||
meta = frappe.get_meta("Item", cached=True)
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
# these are handled separately
|
||||
@@ -257,7 +263,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
filters.pop("supplier", None)
|
||||
|
||||
description_cond = ""
|
||||
if frappe.db.count("Item", cache=True) < 50000:
|
||||
if frappe.db.count(doctype, cache=True) < 50000:
|
||||
# scan description only if items are less than 50000
|
||||
description_cond = "or tabItem.description LIKE %(txt)s"
|
||||
return frappe.db.sql(
|
||||
@@ -300,8 +306,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "BOM"
|
||||
conditions = []
|
||||
fields = get_fields("BOM", ["name", "item"])
|
||||
fields = get_fields(doctype, ["name", "item"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields}
|
||||
@@ -331,6 +338,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Project"
|
||||
cond = ""
|
||||
if filters and filters.get("customer"):
|
||||
cond = """(`tabProject`.customer = %s or
|
||||
@@ -338,9 +346,9 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
frappe.db.escape(filters.get("customer"))
|
||||
)
|
||||
|
||||
fields = get_fields("Project", ["name", "project_name"])
|
||||
searchfields = frappe.get_meta("Project").get_search_fields()
|
||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||
fields = get_fields(doctype, ["name", "project_name"])
|
||||
searchfields = frappe.get_meta(doctype).get_search_fields()
|
||||
searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabProject`
|
||||
@@ -366,7 +374,8 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len, filters, as_dict):
|
||||
fields = get_fields("Delivery Note", ["name", "customer", "posting_date"])
|
||||
doctype = "Delivery Note"
|
||||
fields = get_fields(doctype, ["name", "customer", "posting_date"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
@@ -402,6 +411,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Batch"
|
||||
cond = ""
|
||||
if filters.get("posting_date"):
|
||||
cond = "and (batch.expiry_date is null or batch.expiry_date >= %(posting_date)s)"
|
||||
@@ -420,7 +430,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("is_return"):
|
||||
having_clause = ""
|
||||
|
||||
meta = frappe.get_meta("Batch", cached=True)
|
||||
meta = frappe.get_meta(doctype, cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
search_columns = ""
|
||||
@@ -496,6 +506,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
doctype = "Account"
|
||||
filter_list = []
|
||||
|
||||
if isinstance(filters, dict):
|
||||
@@ -514,7 +525,7 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters):
|
||||
filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
|
||||
|
||||
return frappe.desk.reportview.execute(
|
||||
"Account",
|
||||
doctype,
|
||||
filters=filter_list,
|
||||
fields=["name", "parent_account"],
|
||||
limit_start=start,
|
||||
@@ -553,6 +564,7 @@ def get_income_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
@@ -628,6 +640,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
doctype = "Account"
|
||||
condition = ""
|
||||
if filters.get("company"):
|
||||
condition += "and tabAccount.company = %(company)s"
|
||||
@@ -650,6 +663,7 @@ def get_expense_account(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
# Should be used when item code is passed in filters.
|
||||
doctype = "Warehouse"
|
||||
conditions, bin_conditions = [], []
|
||||
filter_dict = get_doctype_wise_filters(filters)
|
||||
|
||||
|
||||
@@ -614,13 +614,13 @@ class SellingController(StockController):
|
||||
stock_items = [d.item_code, d.description, d.warehouse, ""]
|
||||
non_stock_items = [d.item_code, d.description]
|
||||
|
||||
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
|
||||
duplicate_items_msg += "<br><br>"
|
||||
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
|
||||
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
|
||||
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
|
||||
duplicate_items_msg += "<br><br>"
|
||||
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
|
||||
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if stock_items in check_list:
|
||||
frappe.throw(duplicate_items_msg)
|
||||
else:
|
||||
|
||||
@@ -33,6 +33,10 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BatchExpiredError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class StockController(AccountsController):
|
||||
def validate(self):
|
||||
super(StockController, self).validate()
|
||||
@@ -74,6 +78,10 @@ class StockController(AccountsController):
|
||||
def validate_serialized_batch(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
is_material_issue = False
|
||||
if self.doctype == "Stock Entry" and self.purpose == "Material Issue":
|
||||
is_material_issue = True
|
||||
|
||||
for d in self.get("items"):
|
||||
if hasattr(d, "serial_no") and hasattr(d, "batch_no") and d.serial_no and d.batch_no:
|
||||
serial_nos = frappe.get_all(
|
||||
@@ -90,6 +98,9 @@ class StockController(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
if is_material_issue:
|
||||
continue
|
||||
|
||||
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
|
||||
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")
|
||||
|
||||
@@ -97,7 +108,8 @@ class StockController(AccountsController):
|
||||
frappe.throw(
|
||||
_("Row #{0}: The batch {1} has already expired.").format(
|
||||
d.idx, get_link_to_form("Batch", d.get("batch_no"))
|
||||
)
|
||||
),
|
||||
BatchExpiredError,
|
||||
)
|
||||
|
||||
def clean_serial_nos(self):
|
||||
|
||||
@@ -355,6 +355,8 @@ class Subcontracting:
|
||||
rm_obj.purchase_order = item_row.purchase_order
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
rm_obj.amount = flt(rm_obj.required_qty) * flt(rm_obj.rate)
|
||||
|
||||
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
|
||||
|
||||
|
||||
@@ -37,6 +37,12 @@ class calculate_taxes_and_totals(object):
|
||||
self.set_discount_amount()
|
||||
self.apply_discount_amount()
|
||||
|
||||
# Update grand total as per cash and non trade discount
|
||||
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
|
||||
self.doc.grand_total -= self.doc.discount_amount
|
||||
self.doc.base_grand_total -= self.doc.base_discount_amount
|
||||
self.set_rounded_total()
|
||||
|
||||
self.calculate_shipping_charges()
|
||||
|
||||
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
@@ -500,9 +506,6 @@ class calculate_taxes_and_totals(object):
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
|
||||
self.doc.grand_total -= self.doc.discount_amount
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment),
|
||||
@@ -597,16 +600,16 @@ class calculate_taxes_and_totals(object):
|
||||
if not self.doc.apply_discount_on:
|
||||
frappe.throw(_("Please select Apply Discount On"))
|
||||
|
||||
self.doc.base_discount_amount = flt(
|
||||
self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount")
|
||||
)
|
||||
|
||||
if self.doc.apply_discount_on == "Grand Total" and self.doc.get(
|
||||
"is_cash_or_non_trade_discount"
|
||||
):
|
||||
self.discount_amount_applied = True
|
||||
return
|
||||
|
||||
self.doc.base_discount_amount = flt(
|
||||
self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount")
|
||||
)
|
||||
|
||||
total_for_discount_amount = self.get_total_for_discount_amount()
|
||||
taxes = self.doc.get("taxes")
|
||||
net_total = 0
|
||||
@@ -767,6 +770,18 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.precision("outstanding_amount"),
|
||||
)
|
||||
|
||||
if (
|
||||
self.doc.doctype == "Sales Invoice"
|
||||
and self.doc.get("is_pos")
|
||||
and self.doc.get("pos_profile")
|
||||
and self.doc.get("is_consolidated")
|
||||
):
|
||||
write_off_limit = flt(
|
||||
frappe.db.get_value("POS Profile", self.doc.pos_profile, "write_off_limit")
|
||||
)
|
||||
if write_off_limit and abs(self.doc.outstanding_amount) <= write_off_limit:
|
||||
self.doc.write_off_outstanding_amount_automatically = 1
|
||||
|
||||
if (
|
||||
self.doc.doctype == "Sales Invoice"
|
||||
and self.doc.get("is_pos")
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "website_item.image",
|
||||
"fetch_from": "website_item.website_image",
|
||||
"fieldname": "website_item_image",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Website Item Image",
|
||||
@@ -75,7 +75,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-13 21:02:19.031652",
|
||||
"modified": "2022-06-28 16:44:24.718728",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Recommended Items",
|
||||
@@ -83,5 +83,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -30,10 +30,6 @@ frappe.ui.form.on('Website Item', {
|
||||
}, __("View"));
|
||||
},
|
||||
|
||||
image: () => {
|
||||
refresh_field("image_view");
|
||||
},
|
||||
|
||||
copy_from_item_group: (frm) => {
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"column_break_11",
|
||||
"description",
|
||||
"brand",
|
||||
"image",
|
||||
"display_section",
|
||||
"website_image",
|
||||
"website_image_alt",
|
||||
@@ -113,8 +112,11 @@
|
||||
{
|
||||
"description": "Item Image (if not slideshow)",
|
||||
"fieldname": "website_image",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Website Image"
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Website Image",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"description": "Image Alternative Text",
|
||||
@@ -188,14 +190,6 @@
|
||||
"options": "Item Group",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Image",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "published",
|
||||
@@ -348,13 +342,14 @@
|
||||
}
|
||||
],
|
||||
"has_web_view": 1,
|
||||
"image_field": "image",
|
||||
"image_field": "website_image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-02 13:08:41.942726",
|
||||
"modified": "2022-06-28 17:10:30.613251",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Website Item",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -410,6 +405,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "web_item_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, List, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from erpnext.stock.doctype.item.item import Item
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -116,11 +120,6 @@ class WebsiteItem(WebsiteGenerator):
|
||||
if frappe.flags.in_import:
|
||||
return
|
||||
|
||||
auto_set_website_image = False
|
||||
if not self.website_image and self.image:
|
||||
auto_set_website_image = True
|
||||
self.website_image = self.image
|
||||
|
||||
if not self.website_image:
|
||||
return
|
||||
|
||||
@@ -137,18 +136,16 @@ class WebsiteItem(WebsiteGenerator):
|
||||
file_doc = file_doc[0]
|
||||
|
||||
if not file_doc:
|
||||
if not auto_set_website_image:
|
||||
frappe.msgprint(
|
||||
_("Website Image {0} attached to Item {1} cannot be found").format(
|
||||
self.website_image, self.name
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Website Image {0} attached to Item {1} cannot be found").format(
|
||||
self.website_image, self.name
|
||||
)
|
||||
)
|
||||
|
||||
self.website_image = None
|
||||
|
||||
elif file_doc.is_private:
|
||||
if not auto_set_website_image:
|
||||
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
||||
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
||||
|
||||
self.website_image = None
|
||||
|
||||
@@ -159,9 +156,8 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
if not self.is_new() and self.website_image != frappe.db.get_value(
|
||||
self.doctype, self.name, "website_image"
|
||||
):
|
||||
db_website_image = frappe.db.get_value(self.doctype, self.name, "website_image")
|
||||
if not self.is_new() and self.website_image != db_website_image:
|
||||
self.thumbnail = None
|
||||
|
||||
if self.website_image and not self.thumbnail:
|
||||
@@ -437,7 +433,9 @@ def check_if_user_is_customer(user=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_website_item(doc, save=True):
|
||||
def make_website_item(doc: "Item", save: bool = True) -> Union["WebsiteItem", List[str]]:
|
||||
"Make Website Item from Item. Used via Form UI or patch."
|
||||
|
||||
if not doc:
|
||||
return
|
||||
|
||||
@@ -457,7 +455,6 @@ def make_website_item(doc, save=True):
|
||||
"item_group",
|
||||
"stock_uom",
|
||||
"brand",
|
||||
"image",
|
||||
"has_variants",
|
||||
"variant_of",
|
||||
"description",
|
||||
@@ -465,6 +462,10 @@ def make_website_item(doc, save=True):
|
||||
for field in fields_to_map:
|
||||
website_item.update({field: doc.get(field)})
|
||||
|
||||
# Needed for publishing/mapping via Form UI only
|
||||
if not frappe.flags.in_migrate and (doc.get("image") and not website_item.website_image):
|
||||
website_item.website_image = doc.get("image")
|
||||
|
||||
if not save:
|
||||
return website_item
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
frappe.listview_settings['Website Item'] = {
|
||||
add_fields: ["item_name", "web_item_name", "published", "image", "has_variants", "variant_of"],
|
||||
add_fields: ["item_name", "web_item_name", "published", "website_image", "has_variants", "variant_of"],
|
||||
filters: [["published", "=", "1"]],
|
||||
|
||||
get_indicator: function(doc) {
|
||||
|
||||
@@ -20,7 +20,15 @@ def add_to_wishlist(item_code):
|
||||
web_item_data = frappe.db.get_value(
|
||||
"Website Item",
|
||||
{"item_code": item_code},
|
||||
["image", "website_warehouse", "name", "web_item_name", "item_name", "item_group", "route"],
|
||||
[
|
||||
"website_image",
|
||||
"website_warehouse",
|
||||
"name",
|
||||
"web_item_name",
|
||||
"item_name",
|
||||
"item_group",
|
||||
"route",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -30,7 +38,7 @@ def add_to_wishlist(item_code):
|
||||
"item_group": web_item_data.get("item_group"),
|
||||
"website_item": web_item_data.get("name"),
|
||||
"web_item_name": web_item_data.get("web_item_name"),
|
||||
"image": web_item_data.get("image"),
|
||||
"image": web_item_data.get("website_image"),
|
||||
"warehouse": web_item_data.get("website_warehouse"),
|
||||
"route": web_item_data.get("route"),
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ class ProductQuery:
|
||||
"variant_of",
|
||||
"has_variants",
|
||||
"item_group",
|
||||
"image",
|
||||
"web_long_description",
|
||||
"short_description",
|
||||
"route",
|
||||
|
||||
@@ -35,7 +35,7 @@ erpnext.ProductGrid = class {
|
||||
}
|
||||
|
||||
get_image_html(item, title) {
|
||||
let image = item.website_image || item.image;
|
||||
let image = item.website_image;
|
||||
|
||||
if (image) {
|
||||
return `
|
||||
|
||||
@@ -35,7 +35,7 @@ erpnext.ProductList = class {
|
||||
}
|
||||
|
||||
get_image_html(item, title, settings) {
|
||||
let image = item.website_image || item.image;
|
||||
let image = item.website_image;
|
||||
let wishlist_enabled = !item.has_variants && settings.enable_wishlist;
|
||||
let image_html = ``;
|
||||
|
||||
|
||||
@@ -199,16 +199,32 @@ def get_fee_components(fee_structure):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fee_schedule(program, student_category=None):
|
||||
def get_fee_schedule(program, student_category=None, academic_year=None):
|
||||
"""Returns Fee Schedule.
|
||||
|
||||
:param program: Program.
|
||||
:param student_category: Student Category
|
||||
:param student_category: Student Category.
|
||||
:param academic_year: Academic Year.
|
||||
"""
|
||||
fs = frappe.get_all(
|
||||
"Program Fee",
|
||||
fields=["academic_term", "fee_structure", "due_date", "amount"],
|
||||
filters={"parent": program, "student_category": student_category},
|
||||
filters = {}
|
||||
if program:
|
||||
filters = {"program": program}
|
||||
|
||||
if student_category:
|
||||
filters["student_category"] = student_category
|
||||
|
||||
if academic_year:
|
||||
filters["academic_year"] = academic_year
|
||||
|
||||
fs = frappe.db.get_list(
|
||||
"Fee Schedule",
|
||||
filters=filters,
|
||||
fields=[
|
||||
"academic_term",
|
||||
"fee_structure",
|
||||
"student_category",
|
||||
"due_date",
|
||||
"total_amount as amount",
|
||||
],
|
||||
order_by="idx",
|
||||
)
|
||||
return fs
|
||||
|
||||
@@ -60,12 +60,15 @@ frappe.ui.form.on('Program Enrollment', {
|
||||
method: 'erpnext.education.api.get_fee_schedule',
|
||||
args: {
|
||||
'program': frm.doc.program,
|
||||
'student_category': frm.doc.student_category
|
||||
'student_category': frm.doc.student_category,
|
||||
'academic_year': frm.doc.academic_year
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
cur_frm.clear_table("fees");
|
||||
frm.refresh_fields('fees');
|
||||
frm.set_value('fees' ,r.message);
|
||||
frm.events.get_courses(frm);
|
||||
frm.refresh_fields('fees');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -76,6 +79,10 @@ frappe.ui.form.on('Program Enrollment', {
|
||||
frappe.ui.form.trigger('Program Enrollment', 'program');
|
||||
},
|
||||
|
||||
academic_year: function() {
|
||||
frappe.ui.form.trigger('Program Enrollment', 'program');
|
||||
},
|
||||
|
||||
get_courses: function(frm) {
|
||||
frm.set_value('courses',[]);
|
||||
frappe.call({
|
||||
|
||||
@@ -105,6 +105,8 @@ class ProgramEnrollment(Document):
|
||||
"academic_term": d.academic_term,
|
||||
"fee_structure": d.fee_structure,
|
||||
"program": self.program,
|
||||
"student_batch": self.student_batch_name,
|
||||
"student_category": self.student_category,
|
||||
"due_date": d.due_date,
|
||||
"student_name": self.student_name,
|
||||
"program_enrollment": self.name,
|
||||
|
||||
@@ -37,11 +37,26 @@ def handle_end_call(**kwargs):
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def handle_missed_call(**kwargs):
|
||||
update_call_log(kwargs, "Missed")
|
||||
status = ""
|
||||
call_type = kwargs.get("CallType")
|
||||
dial_call_status = kwargs.get("DialCallStatus")
|
||||
|
||||
if call_type == "incomplete" and dial_call_status == "no-answer":
|
||||
status = "No Answer"
|
||||
elif call_type == "client-hangup" and dial_call_status == "canceled":
|
||||
status = "Canceled"
|
||||
elif call_type == "incomplete" and dial_call_status == "failed":
|
||||
status = "Failed"
|
||||
|
||||
update_call_log(kwargs, status)
|
||||
|
||||
|
||||
def update_call_log(call_payload, status="Ringing", call_log=None):
|
||||
call_log = call_log or get_call_log(call_payload)
|
||||
|
||||
# for a new sid, call_log and get_call_log will be empty so create a new log
|
||||
if not call_log:
|
||||
call_log = create_call_log(call_payload)
|
||||
if call_log:
|
||||
call_log.status = status
|
||||
call_log.to = call_payload.get("DialWhomNumber")
|
||||
@@ -53,16 +68,9 @@ def update_call_log(call_payload, status="Ringing", call_log=None):
|
||||
|
||||
|
||||
def get_call_log(call_payload):
|
||||
call_log = frappe.get_all(
|
||||
"Call Log",
|
||||
{
|
||||
"id": call_payload.get("CallSid"),
|
||||
},
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if call_log:
|
||||
return frappe.get_doc("Call Log", call_log[0].name)
|
||||
call_log_id = call_payload.get("CallSid")
|
||||
if frappe.db.exists("Call Log", call_log_id):
|
||||
return frappe.get_doc("Call Log", call_log_id)
|
||||
|
||||
|
||||
def create_call_log(call_payload):
|
||||
|
||||
@@ -483,12 +483,12 @@ scheduler_events = {
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
||||
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
|
||||
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
|
||||
],
|
||||
"hourly_long": [
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
||||
],
|
||||
"daily": [
|
||||
"erpnext.support.doctype.issue.issue.auto_close_tickets",
|
||||
@@ -589,6 +589,7 @@ accounting_dimension_doctypes = [
|
||||
"Shipping Rule",
|
||||
"Landed Cost Item",
|
||||
"Asset Value Adjustment",
|
||||
"Asset Repair",
|
||||
"Loyalty Program",
|
||||
"Fee Schedule",
|
||||
"Fee Structure",
|
||||
|
||||
@@ -19,7 +19,7 @@ from erpnext.hr.doctype.attendance.attendance import (
|
||||
mark_attendance,
|
||||
)
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
|
||||
from erpnext.hr.tests.test_utils import get_first_sunday
|
||||
|
||||
test_records = frappe.get_test_records("Attendance")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-03-07 09:04:18",
|
||||
"creation": "2022-02-21 11:54:09.632218",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
@@ -813,11 +813,12 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-06-17 11:31:37.730760",
|
||||
"modified": "2022-08-20 13:44:37.088519",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.permissions import (
|
||||
remove_user_permission,
|
||||
set_user_permission_if_allowed,
|
||||
)
|
||||
from frappe.utils import add_years, cstr, getdate, today, validate_email_address
|
||||
from frappe.utils import add_days, add_years, cstr, getdate, today, validate_email_address
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
from erpnext.utilities.transaction_base import delete_events
|
||||
@@ -64,6 +64,8 @@ class Employee(NestedSet):
|
||||
if existing_user_id:
|
||||
remove_user_permission("Employee", self.name, existing_user_id)
|
||||
|
||||
self.update_to_date_in_work_history()
|
||||
|
||||
def after_rename(self, old, new, merge):
|
||||
self.db_set("employee", new)
|
||||
|
||||
@@ -166,6 +168,18 @@ class Employee(NestedSet):
|
||||
user.flags.ignore_permissions = True
|
||||
user.add_roles("Expense Approver")
|
||||
|
||||
def update_to_date_in_work_history(self):
|
||||
if not self.internal_work_history:
|
||||
return
|
||||
|
||||
for idx, row in enumerate(self.internal_work_history):
|
||||
if not row.from_date or idx == 0:
|
||||
continue
|
||||
|
||||
self.internal_work_history[idx - 1].to_date = add_days(row.from_date, -1)
|
||||
|
||||
self.internal_work_history[-1].to_date = None
|
||||
|
||||
def validate_date(self):
|
||||
if self.date_of_birth and getdate(self.date_of_birth) > getdate(today()):
|
||||
throw(_("Date of Birth cannot be greater than today."))
|
||||
@@ -349,7 +363,9 @@ def get_employee_email(employee_doc):
|
||||
|
||||
def get_holiday_list_for_employee(employee, raise_exception=True):
|
||||
if employee:
|
||||
holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"])
|
||||
holiday_list, company = frappe.get_cached_value(
|
||||
"Employee", employee, ["holiday_list", "company"]
|
||||
)
|
||||
else:
|
||||
holiday_list = ""
|
||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Employee",
|
||||
"options": "Employee",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.employee_name",
|
||||
@@ -48,7 +49,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Shift",
|
||||
"options": "Shift Type",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
@@ -107,7 +109,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-07-08 11:02:32.660986",
|
||||
"modified": "2022-07-19 15:38:41.767539",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Checkin",
|
||||
|
||||
@@ -131,7 +131,7 @@ def mark_attendance_and_link_log(
|
||||
return None
|
||||
|
||||
elif attendance_status in ("Present", "Absent", "Half Day"):
|
||||
employee_doc = frappe.get_doc("Employee", employee)
|
||||
company = frappe.get_cached_value("Employee", employee, "company")
|
||||
duplicate = frappe.db.exists(
|
||||
"Attendance",
|
||||
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
|
||||
@@ -144,7 +144,7 @@ def mark_attendance_and_link_log(
|
||||
"attendance_date": attendance_date,
|
||||
"status": attendance_status,
|
||||
"working_hours": working_hours,
|
||||
"company": employee_doc.company,
|
||||
"company": company,
|
||||
"shift": shift,
|
||||
"late_entry": late_entry,
|
||||
"early_exit": early_exit,
|
||||
|
||||
@@ -80,12 +80,14 @@ class TestEmployeeTransfer(unittest.TestCase):
|
||||
department = ["Accounts - TC", "Management - TC"]
|
||||
designation = ["Accountant", "Manager"]
|
||||
dt = [getdate("01-10-2021"), getdate()]
|
||||
to_date = [add_days(dt[1], -1), None]
|
||||
|
||||
employee = frappe.get_doc("Employee", employee)
|
||||
for data in employee.internal_work_history:
|
||||
self.assertEqual(data.department, department[count])
|
||||
self.assertEqual(data.designation, designation[count])
|
||||
self.assertEqual(data.from_date, dt[count])
|
||||
self.assertEqual(data.to_date, to_date[count])
|
||||
count = count + 1
|
||||
|
||||
transfer.cancel()
|
||||
@@ -95,6 +97,7 @@ class TestEmployeeTransfer(unittest.TestCase):
|
||||
self.assertEqual(data.designation, designation[0])
|
||||
self.assertEqual(data.department, department[0])
|
||||
self.assertEqual(data.from_date, dt[0])
|
||||
self.assertEqual(data.to_date, None)
|
||||
|
||||
|
||||
def create_company():
|
||||
|
||||
@@ -254,9 +254,11 @@ frappe.ui.form.on("Expense Claim", {
|
||||
}, __("View"));
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus===1 && !cint(frm.doc.is_paid) && cint(frm.doc.grand_total) > 0
|
||||
&& (cint(frm.doc.total_amount_reimbursed) < cint(frm.doc.total_sanctioned_amount))
|
||||
&& frappe.model.can_create("Payment Entry")) {
|
||||
if (
|
||||
frm.doc.docstatus === 1
|
||||
&& frm.doc.status !== "Paid"
|
||||
&& frappe.model.can_create("Payment Entry")
|
||||
) {
|
||||
frm.add_custom_button(__('Payment'),
|
||||
function() { frm.events.make_payment_entry(frm); }, __('Create'));
|
||||
}
|
||||
|
||||
@@ -305,12 +305,12 @@ class ExpenseClaim(AccountsController):
|
||||
|
||||
if self.total_advance_amount:
|
||||
precision = self.precision("total_advance_amount")
|
||||
if flt(self.total_advance_amount, precision) > flt(self.total_claimed_amount, precision):
|
||||
frappe.throw(_("Total advance amount cannot be greater than total claimed amount"))
|
||||
amount_with_taxes = flt(
|
||||
(flt(self.total_sanctioned_amount, precision) + flt(self.total_taxes_and_charges, precision)),
|
||||
precision,
|
||||
)
|
||||
|
||||
if self.total_sanctioned_amount and flt(self.total_advance_amount, precision) > flt(
|
||||
self.total_sanctioned_amount, precision
|
||||
):
|
||||
if flt(self.total_advance_amount, precision) > amount_with_taxes:
|
||||
frappe.throw(_("Total advance amount cannot be greater than total sanctioned amount"))
|
||||
|
||||
def validate_sanctioned_amount(self):
|
||||
@@ -339,6 +339,30 @@ def update_reimbursed_amount(doc, amount):
|
||||
frappe.db.set_value("Expense Claim", doc.name, "status", doc.status)
|
||||
|
||||
|
||||
def get_outstanding_amount_for_claim(claim):
|
||||
if isinstance(claim, str):
|
||||
claim = frappe.db.get_value(
|
||||
"Expense Claim",
|
||||
claim,
|
||||
(
|
||||
"total_sanctioned_amount",
|
||||
"total_taxes_and_charges",
|
||||
"total_amount_reimbursed",
|
||||
"total_advance_amount",
|
||||
),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
outstanding_amt = (
|
||||
flt(claim.total_sanctioned_amount)
|
||||
+ flt(claim.total_taxes_and_charges)
|
||||
- flt(claim.total_amount_reimbursed)
|
||||
- flt(claim.total_advance_amount)
|
||||
)
|
||||
|
||||
return outstanding_amt
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_bank_entry(dt, dn):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
@@ -348,11 +372,7 @@ def make_bank_entry(dt, dn):
|
||||
if not default_bank_cash_account:
|
||||
default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash")
|
||||
|
||||
payable_amount = (
|
||||
flt(expense_claim.total_sanctioned_amount)
|
||||
- flt(expense_claim.total_amount_reimbursed)
|
||||
- flt(expense_claim.total_advance_amount)
|
||||
)
|
||||
payable_amount = get_outstanding_amount_for_claim(expense_claim)
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Bank Entry"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, nowdate, random_string
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
@@ -14,9 +15,18 @@ test_dependencies = ["Employee"]
|
||||
company_name = "_Test Company 3"
|
||||
|
||||
|
||||
class TestExpenseClaim(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
class TestExpenseClaim(FrappeTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.get_value("Cost Center", {"company": company_name}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "_Test Cost Center 3",
|
||||
"parent_cost_center": "_Test Company 3 - _TC3",
|
||||
"is_group": 0,
|
||||
"company": company_name,
|
||||
}
|
||||
).insert()
|
||||
|
||||
def test_total_expense_claim_for_project(self):
|
||||
frappe.db.sql("""delete from `tabTask`""")
|
||||
@@ -58,12 +68,7 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
payable_account, 300, 200, company_name, "Travel Expenses - _TC3"
|
||||
)
|
||||
|
||||
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
|
||||
je = frappe.get_doc(je_dict)
|
||||
je.posting_date = nowdate()
|
||||
je.cheque_no = random_string(5)
|
||||
je.cheque_date = nowdate()
|
||||
je.submit()
|
||||
je = make_journal_entry(expense_claim)
|
||||
|
||||
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
|
||||
self.assertEqual(expense_claim.status, "Paid")
|
||||
@@ -109,6 +114,40 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
self.assertEqual(claim.grand_total, 0)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_advance_amount_allocation_against_claim_with_taxes(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
make_employee_advance,
|
||||
make_payment_entry,
|
||||
)
|
||||
|
||||
frappe.db.delete("Employee Advance")
|
||||
|
||||
payable_account = get_payable_account("_Test Company")
|
||||
taxes = generate_taxes("_Test Company")
|
||||
claim = make_expense_claim(
|
||||
payable_account,
|
||||
700,
|
||||
700,
|
||||
"_Test Company",
|
||||
"Travel Expenses - _TC",
|
||||
do_not_submit=True,
|
||||
taxes=taxes,
|
||||
)
|
||||
claim.save()
|
||||
|
||||
advance = make_employee_advance(claim.employee)
|
||||
pe = make_payment_entry(advance)
|
||||
pe.submit()
|
||||
|
||||
# claim for already paid out advances
|
||||
claim = get_advances_for_claim(claim, advance.name, 763)
|
||||
claim.save()
|
||||
claim.submit()
|
||||
|
||||
self.assertEqual(claim.grand_total, 0)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_expense_claim_partially_paid_via_advance(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
@@ -272,17 +311,36 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
self.assertEqual(total_amount_reimbursed, 5500)
|
||||
|
||||
def test_journal_entry_against_expense_claim(self):
|
||||
payable_account = get_payable_account(company_name)
|
||||
taxes = generate_taxes()
|
||||
expense_claim = make_expense_claim(
|
||||
payable_account,
|
||||
300,
|
||||
200,
|
||||
company_name,
|
||||
"Travel Expenses - _TC3",
|
||||
do_not_submit=True,
|
||||
taxes=taxes,
|
||||
)
|
||||
expense_claim.submit()
|
||||
|
||||
je = make_journal_entry(expense_claim)
|
||||
|
||||
self.assertEqual(je.accounts[0].debit_in_account_currency, expense_claim.grand_total)
|
||||
|
||||
|
||||
def get_payable_account(company):
|
||||
return frappe.get_cached_value("Company", company, "default_payable_account")
|
||||
|
||||
|
||||
def generate_taxes():
|
||||
def generate_taxes(company=None):
|
||||
company = company or company_name
|
||||
parent_account = frappe.db.get_value(
|
||||
"Account", {"company": company_name, "is_group": 1, "account_type": "Tax"}, "name"
|
||||
"Account", filters={"account_name": "Duties and Taxes", "company": company}
|
||||
)
|
||||
account = create_account(
|
||||
company=company_name,
|
||||
company=company,
|
||||
account_name="Output Tax CGST",
|
||||
account_type="Tax",
|
||||
parent_account=parent_account,
|
||||
@@ -370,3 +428,14 @@ def make_payment_entry(expense_claim, payable_account, amt):
|
||||
pe.references[0].allocated_amount = amt
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
|
||||
def make_journal_entry(expense_claim):
|
||||
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
|
||||
je = frappe.get_doc(je_dict)
|
||||
je.posting_date = nowdate()
|
||||
je.cheque_no = random_string(5)
|
||||
je.cheque_date = nowdate()
|
||||
je.submit()
|
||||
|
||||
return je
|
||||
|
||||
@@ -115,6 +115,8 @@ def is_holiday(holiday_list, date=None):
|
||||
if date is None:
|
||||
date = today()
|
||||
if holiday_list:
|
||||
return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date)))
|
||||
return bool(
|
||||
frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True)
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -94,8 +94,8 @@ class Interview(Document):
|
||||
@frappe.whitelist()
|
||||
def reschedule_interview(self, scheduled_on, from_time, to_time):
|
||||
original_date = self.scheduled_on
|
||||
from_time = self.from_time
|
||||
to_time = self.to_time
|
||||
original_from_time = self.from_time
|
||||
original_to_time = self.to_time
|
||||
|
||||
self.db_set({"scheduled_on": scheduled_on, "from_time": from_time, "to_time": to_time})
|
||||
self.notify_update()
|
||||
@@ -107,7 +107,12 @@ class Interview(Document):
|
||||
recipients=recipients,
|
||||
subject=_("Interview: {0} Rescheduled").format(self.name),
|
||||
message=_("Your Interview session is rescheduled from {0} {1} - {2} to {3} {4} - {5}").format(
|
||||
original_date, from_time, to_time, self.scheduled_on, self.from_time, self.to_time
|
||||
original_date,
|
||||
original_from_time,
|
||||
original_to_time,
|
||||
self.scheduled_on,
|
||||
self.from_time,
|
||||
self.to_time,
|
||||
),
|
||||
reference_doctype=self.doctype,
|
||||
reference_name=self.name,
|
||||
|
||||
@@ -8,7 +8,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.utils import add_days, getdate, nowtime
|
||||
from frappe.utils import add_days, get_time, getdate, nowtime
|
||||
|
||||
from erpnext.hr.doctype.designation.test_designation import create_designation
|
||||
from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
|
||||
@@ -26,18 +26,23 @@ class TestInterview(unittest.TestCase):
|
||||
def test_notification_on_rescheduling(self):
|
||||
job_applicant = create_job_applicant()
|
||||
interview = create_interview_and_dependencies(
|
||||
job_applicant.name, scheduled_on=add_days(getdate(), -4)
|
||||
job_applicant.name,
|
||||
scheduled_on=add_days(getdate(), -4),
|
||||
from_time="10:00:00",
|
||||
to_time="11:00:00",
|
||||
)
|
||||
|
||||
previous_scheduled_date = interview.scheduled_on
|
||||
frappe.db.sql("DELETE FROM `tabEmail Queue`")
|
||||
|
||||
interview.reschedule_interview(
|
||||
add_days(getdate(previous_scheduled_date), 2), from_time=nowtime(), to_time=nowtime()
|
||||
add_days(getdate(previous_scheduled_date), 2), from_time="11:00:00", to_time="12:00:00"
|
||||
)
|
||||
interview.reload()
|
||||
|
||||
self.assertEqual(interview.scheduled_on, add_days(getdate(previous_scheduled_date), 2))
|
||||
self.assertEqual(get_time(interview.from_time), get_time("11:00:00"))
|
||||
self.assertEqual(get_time(interview.to_time), get_time("12:00:00"))
|
||||
|
||||
notification = frappe.get_all(
|
||||
"Email Queue", filters={"message": ("like", "%Your Interview session is rescheduled from%")}
|
||||
|
||||
@@ -52,7 +52,7 @@ frappe.ui.form.on("Leave Application", {
|
||||
make_dashboard: function(frm) {
|
||||
var leave_details;
|
||||
let lwps;
|
||||
if (frm.doc.employee && frm.doc.from_date) {
|
||||
if (frm.doc.employee) {
|
||||
frappe.call({
|
||||
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
|
||||
async: false,
|
||||
|
||||
@@ -33,6 +33,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
create_assignment_for_multiple_employees,
|
||||
)
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
from erpnext.hr.tests.test_utils import get_first_sunday
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_holiday_list,
|
||||
make_leave_application,
|
||||
@@ -1105,23 +1106,6 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
|
||||
allocate_leave.submit()
|
||||
|
||||
|
||||
def get_first_sunday(holiday_list, for_date=None):
|
||||
date = for_date or getdate()
|
||||
month_start_date = get_first_day(date)
|
||||
month_end_date = get_last_day(date)
|
||||
first_sunday = frappe.db.sql(
|
||||
"""
|
||||
select holiday_date from `tabHoliday`
|
||||
where parent = %s
|
||||
and holiday_date between %s and %s
|
||||
order by holiday_date
|
||||
""",
|
||||
(holiday_list, month_start_date, month_end_date),
|
||||
)[0][0]
|
||||
|
||||
return first_sunday
|
||||
|
||||
|
||||
def make_policy_assignment(employee, leave_type, leave_period):
|
||||
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
|
||||
frappe.get_doc(
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Employee",
|
||||
"options": "Employee",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.employee_name",
|
||||
@@ -48,7 +49,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Shift Type",
|
||||
"options": "Shift Type",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
@@ -105,7 +107,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-15 14:27:54.310773",
|
||||
"modified": "2022-07-19 15:27:54.310773",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Shift Assignment",
|
||||
|
||||
@@ -169,7 +169,7 @@ def get_employee_shift(
|
||||
"""
|
||||
if for_date is None:
|
||||
for_date = nowdate()
|
||||
default_shift = frappe.db.get_value("Employee", employee, "default_shift")
|
||||
default_shift = frappe.get_cached_value("Employee", employee, "default_shift")
|
||||
shift_type_name = None
|
||||
shift_assignment_details = frappe.db.get_value(
|
||||
"Shift Assignment",
|
||||
@@ -187,7 +187,7 @@ def get_employee_shift(
|
||||
if not shift_type_name and consider_default_shift:
|
||||
shift_type_name = default_shift
|
||||
if shift_type_name:
|
||||
holiday_list_name = frappe.db.get_value("Shift Type", shift_type_name, "holiday_list")
|
||||
holiday_list_name = frappe.get_cached_value("Shift Type", shift_type_name, "holiday_list")
|
||||
if not holiday_list_name:
|
||||
holiday_list_name = get_holiday_list_for_employee(employee, False)
|
||||
if holiday_list_name and is_holiday(holiday_list_name, for_date):
|
||||
@@ -294,7 +294,18 @@ def get_shift_details(shift_type_name, for_date=None):
|
||||
return None
|
||||
if not for_date:
|
||||
for_date = nowdate()
|
||||
shift_type = frappe.get_doc("Shift Type", shift_type_name)
|
||||
shift_type = frappe.get_cached_value(
|
||||
"Shift Type",
|
||||
shift_type_name,
|
||||
[
|
||||
"name",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"begin_check_in_before_shift_start_time",
|
||||
"allow_check_out_after_shift_end_time",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
|
||||
for_date = (
|
||||
for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date
|
||||
|
||||
@@ -107,7 +107,7 @@ class ShiftType(Document):
|
||||
"""Marks Absents for the given employee on working days in this shift which have no attendance marked.
|
||||
The Absent is marked starting from 'process_attendance_after' or employee creation date.
|
||||
"""
|
||||
date_of_joining, relieving_date, employee_creation = frappe.db.get_value(
|
||||
date_of_joining, relieving_date, employee_creation = frappe.get_cached_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date", "creation"]
|
||||
)
|
||||
if not date_of_joining:
|
||||
@@ -156,21 +156,19 @@ class ShiftType(Document):
|
||||
if not from_date:
|
||||
del filters["start_date"]
|
||||
|
||||
assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True)
|
||||
assigned_employees = [x[0] for x in assigned_employees]
|
||||
assigned_employees = frappe.get_all("Shift Assignment", filters, pluck="employee")
|
||||
|
||||
if consider_default_shift:
|
||||
filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
|
||||
default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True)
|
||||
default_shift_employees = [x[0] for x in default_shift_employees]
|
||||
default_shift_employees = frappe.get_all("Employee", filters, pluck="name")
|
||||
return list(set(assigned_employees + default_shift_employees))
|
||||
return assigned_employees
|
||||
|
||||
|
||||
def process_auto_attendance_for_all_shifts():
|
||||
shift_list = frappe.get_all("Shift Type", "name", {"enable_auto_attendance": "1"}, as_list=True)
|
||||
shift_list = frappe.get_all("Shift Type", filters={"enable_auto_attendance": "1"}, pluck="name")
|
||||
for shift in shift_list:
|
||||
doc = frappe.get_doc("Shift Type", shift[0])
|
||||
doc = frappe.get_cached_doc("Shift Type", shift)
|
||||
doc.process_auto_attendance()
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ frappe.ui.form.on(cur_frm.doctype, {
|
||||
};
|
||||
});
|
||||
},
|
||||
onload: function(frm){
|
||||
if(frm.doc.__islocal){
|
||||
if(frm.doctype == "Employee Promotion"){
|
||||
onload: function(frm) {
|
||||
if (frm.doc.__islocal && !frm.doc.amended_from) {
|
||||
if (frm.doctype == "Employee Promotion") {
|
||||
frm.doc.promotion_details = [];
|
||||
}else if (frm.doctype == "Employee Transfer") {
|
||||
} else if (frm.doctype == "Employee Transfer") {
|
||||
frm.doc.transfer_details = [];
|
||||
}
|
||||
}
|
||||
@@ -106,12 +106,12 @@ var render_dynamic_field = function(d, fieldtype, options, fieldname) {
|
||||
|
||||
var add_to_details = function(frm, d, table) {
|
||||
let data = d.data;
|
||||
if(data.fieldname){
|
||||
if(validate_duplicate(frm, table, data.fieldname)){
|
||||
if (data.fieldname) {
|
||||
if (validate_duplicate(frm, table, data.fieldname)) {
|
||||
frappe.show_alert({message:__("Property already added"), indicator:'orange'});
|
||||
return false;
|
||||
}
|
||||
if(data.current == data.new){
|
||||
if (data.current == data.new) {
|
||||
frappe.show_alert({message:__("Nothing to change"), indicator:'orange'});
|
||||
d.get_primary_btn().attr('disabled', false);
|
||||
return false;
|
||||
@@ -123,12 +123,14 @@ var add_to_details = function(frm, d, table) {
|
||||
new: data.new
|
||||
});
|
||||
frm.refresh_field(table);
|
||||
frm.fields_dict[table].grid.wrapper.find(".grid-add-row").hide();
|
||||
|
||||
d.fields_dict.field_html.$wrapper.html("");
|
||||
d.set_value("property", "");
|
||||
d.set_value('current', "");
|
||||
frappe.show_alert({message:__("Added to details"),indicator:'green'});
|
||||
d.data = {};
|
||||
}else {
|
||||
} else {
|
||||
frappe.show_alert({message:__("Value missing"),indicator:'red'});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,13 +9,11 @@ from frappe.utils import add_days, add_months, flt, get_year_ending, get_year_st
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_first_sunday,
|
||||
make_allocation_record,
|
||||
)
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
|
||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
from erpnext.hr.report.employee_leave_balance.employee_leave_balance import execute
|
||||
from erpnext.hr.tests.test_utils import get_first_sunday
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_holiday_list,
|
||||
make_leave_application,
|
||||
|
||||
@@ -22,9 +22,9 @@ def execute(filters=None):
|
||||
|
||||
def get_columns(leave_types):
|
||||
columns = [
|
||||
_("Employee") + ":Link.Employee:150",
|
||||
_("Employee") + ":Link/Employee:150",
|
||||
_("Employee Name") + "::200",
|
||||
_("Department") + "::150",
|
||||
_("Department") + ":Link/Department:150",
|
||||
]
|
||||
|
||||
for leave_type in leave_types:
|
||||
|
||||
@@ -9,12 +9,10 @@ from frappe.utils import add_days, flt, get_year_ending, get_year_start, getdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_first_sunday,
|
||||
make_allocation_record,
|
||||
)
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
|
||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
|
||||
from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary import execute
|
||||
from erpnext.hr.tests.test_utils import get_first_sunday
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_holiday_list,
|
||||
make_leave_application,
|
||||
|
||||
19
erpnext/hr/tests/test_utils.py
Normal file
19
erpnext/hr/tests/test_utils.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import frappe
|
||||
from frappe.utils import get_first_day, get_last_day, getdate
|
||||
|
||||
|
||||
def get_first_sunday(holiday_list="Salary Slip Test Holiday List", for_date=None):
|
||||
date = for_date or getdate()
|
||||
month_start_date = get_first_day(date)
|
||||
month_end_date = get_last_day(date)
|
||||
first_sunday = frappe.db.sql(
|
||||
"""
|
||||
select holiday_date from `tabHoliday`
|
||||
where parent = %s
|
||||
and holiday_date between %s and %s
|
||||
order by holiday_date
|
||||
""",
|
||||
(holiday_list, month_start_date, month_end_date),
|
||||
)[0][0]
|
||||
|
||||
return first_sunday
|
||||
@@ -224,6 +224,7 @@ def delete_employee_work_history(details, employee, date):
|
||||
filters["from_date"] = date
|
||||
if filters:
|
||||
frappe.db.delete("Employee Internal Work History", filters)
|
||||
employee.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"posting_date",
|
||||
"status",
|
||||
"repay_from_salary",
|
||||
"manually_update_paid_amount_in_salary_slip",
|
||||
"section_break_8",
|
||||
"loan_type",
|
||||
"loan_amount",
|
||||
@@ -48,9 +49,13 @@
|
||||
"total_payment",
|
||||
"total_principal_paid",
|
||||
"written_off_amount",
|
||||
"refund_amount",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"column_break_19",
|
||||
"total_interest_payable",
|
||||
"total_amount_paid",
|
||||
"is_npa",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
@@ -379,16 +384,50 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "refund_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Refund amount",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_adjustment_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Adjustment Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_adjustment_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Adjustment Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Mark Loan as a Nonperforming asset",
|
||||
"fieldname": "is_npa",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is NPA"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"depends_on": "repay_from_salary",
|
||||
"fieldname": "manually_update_paid_amount_in_salary_slip",
|
||||
"fieldtype": "Check",
|
||||
"label": "Manually Update Paid Amount in Salary Slip"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-10 11:50:31.957360",
|
||||
"modified": "2022-09-13 02:05:25.017190",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -414,6 +453,5 @@
|
||||
"search_fields": "posting_date",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Balance Adjustment', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "LM-ADJ-.#####",
|
||||
"creation": "2022-06-28 14:48:47.736269",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan",
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"posting_date",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"section_break_9",
|
||||
"adjustment_account",
|
||||
"column_break_11",
|
||||
"adjustment_type",
|
||||
"amount",
|
||||
"reference_number",
|
||||
"remarks",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan",
|
||||
"options": "Loan",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant_type",
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Applicant ",
|
||||
"options": "applicant_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Posting Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Adjustment Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Balance Adjustment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Balance Adjustment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "adjustment_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Adjustment Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "adjustment_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Adjustment Type",
|
||||
"options": "Credit Adjustment\nDebit Adjustment",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Data",
|
||||
"label": "Remarks"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-08 16:48:54.480066",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Balance Adjustment",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
process_loan_interest_accrual_for_demand_loans,
|
||||
)
|
||||
|
||||
|
||||
class LoanBalanceAdjustment(AccountsController):
|
||||
"""
|
||||
Add credit/debit adjustments to loan ledger.
|
||||
"""
|
||||
|
||||
def validate(self):
|
||||
if self.amount == 0:
|
||||
frappe.throw(_("Amount cannot be zero"))
|
||||
if self.amount < 0:
|
||||
frappe.throw(_("Amount cannot be negative"))
|
||||
self.set_missing_values()
|
||||
|
||||
def on_submit(self):
|
||||
self.set_status_and_amounts()
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status_and_amounts(cancel=1)
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
|
||||
def set_missing_values(self):
|
||||
if not self.posting_date:
|
||||
self.posting_date = nowdate()
|
||||
|
||||
if not self.cost_center:
|
||||
self.cost_center = erpnext.get_default_cost_center(self.company)
|
||||
|
||||
def set_status_and_amounts(self, cancel=0):
|
||||
loan_details = frappe.db.get_value(
|
||||
"Loan",
|
||||
self.loan,
|
||||
[
|
||||
"loan_amount",
|
||||
"credit_adjustment_amount",
|
||||
"debit_adjustment_amount",
|
||||
"total_payment",
|
||||
"total_principal_paid",
|
||||
"total_interest_payable",
|
||||
"status",
|
||||
"is_term_loan",
|
||||
"is_secured_loan",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if cancel:
|
||||
adjustment_amount = self.get_values_on_cancel(loan_details)
|
||||
else:
|
||||
adjustment_amount = self.get_values_on_submit(loan_details)
|
||||
|
||||
if self.adjustment_type == "Credit Adjustment":
|
||||
adj_field = "credit_adjustment_amount"
|
||||
elif self.adjustment_type == "Debit Adjustment":
|
||||
adj_field = "debit_adjustment_amount"
|
||||
|
||||
frappe.db.set_value("Loan", self.loan, {adj_field: adjustment_amount})
|
||||
|
||||
def get_values_on_cancel(self, loan_details):
|
||||
if self.adjustment_type == "Credit Adjustment":
|
||||
adjustment_amount = loan_details.credit_adjustment_amount - self.amount
|
||||
elif self.adjustment_type == "Debit Adjustment":
|
||||
adjustment_amount = loan_details.debit_adjustment_amount - self.amount
|
||||
|
||||
return adjustment_amount
|
||||
|
||||
def get_values_on_submit(self, loan_details):
|
||||
if self.adjustment_type == "Credit Adjustment":
|
||||
adjustment_amount = loan_details.credit_adjustment_amount + self.amount
|
||||
elif self.adjustment_type == "Debit Adjustment":
|
||||
adjustment_amount = loan_details.debit_adjustment_amount + self.amount
|
||||
|
||||
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
|
||||
process_loan_interest_accrual_for_demand_loans(
|
||||
posting_date=add_days(self.posting_date, -1),
|
||||
loan=self.loan,
|
||||
accrual_type=self.adjustment_type,
|
||||
)
|
||||
|
||||
return adjustment_amount
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gle_map = []
|
||||
loan_account = frappe.db.get_value("Loan", self.loan, "loan_account")
|
||||
remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan)
|
||||
if self.reference_number:
|
||||
remarks += " with reference no. {}".format(self.reference_number)
|
||||
|
||||
loan_entry = {
|
||||
"account": loan_account,
|
||||
"against": self.adjustment_account,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.loan,
|
||||
"remarks": _(remarks),
|
||||
"cost_center": self.cost_center,
|
||||
"party_type": self.applicant_type,
|
||||
"party": self.applicant,
|
||||
"posting_date": self.posting_date,
|
||||
}
|
||||
company_entry = {
|
||||
"account": self.adjustment_account,
|
||||
"against": loan_account,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.loan,
|
||||
"remarks": _(remarks),
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.posting_date,
|
||||
}
|
||||
if self.adjustment_type == "Credit Adjustment":
|
||||
loan_entry["credit"] = self.amount
|
||||
loan_entry["credit_in_account_currency"] = self.amount
|
||||
|
||||
company_entry["debit"] = self.amount
|
||||
company_entry["debit_in_account_currency"] = self.amount
|
||||
|
||||
elif self.adjustment_type == "Debit Adjustment":
|
||||
loan_entry["debit"] = self.amount
|
||||
loan_entry["debit_in_account_currency"] = self.amount
|
||||
|
||||
company_entry["credit"] = self.amount
|
||||
company_entry["credit_in_account_currency"] = self.amount
|
||||
|
||||
gle_map.append(self.get_gl_dict(loan_entry))
|
||||
|
||||
gle_map.append(self.get_gl_dict(company_entry))
|
||||
|
||||
if gle_map:
|
||||
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLoanBalanceAdjustment(FrappeTestCase):
|
||||
pass
|
||||
@@ -163,11 +163,11 @@
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.disbursement_account",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "disbursement_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Disbursement Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
@@ -185,7 +185,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-17 18:23:44.157598",
|
||||
"modified": "2022-08-04 17:16:04.922444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Disbursement",
|
||||
|
||||
@@ -209,6 +209,9 @@ def get_disbursal_amount(loan, on_current_security_price=0):
|
||||
"loan_amount",
|
||||
"disbursed_amount",
|
||||
"total_payment",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"total_principal_paid",
|
||||
"total_interest_payable",
|
||||
"status",
|
||||
|
||||
@@ -35,12 +35,15 @@
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Loan",
|
||||
"options": "Loan"
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Posting Date"
|
||||
},
|
||||
{
|
||||
@@ -75,6 +78,8 @@
|
||||
"fetch_from": "loan.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type"
|
||||
},
|
||||
@@ -158,8 +163,11 @@
|
||||
{
|
||||
"fieldname": "accrual_type",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Accrual Type",
|
||||
"options": "Regular\nRepayment\nDisbursement"
|
||||
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund"
|
||||
},
|
||||
{
|
||||
"fieldname": "penalty_amount",
|
||||
@@ -185,10 +193,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 18:26:38.871889",
|
||||
"modified": "2022-06-30 11:51:31.911794",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Interest Accrual",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -225,5 +234,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -135,7 +135,11 @@ def calculate_accrual_amount_for_demand_loans(
|
||||
def make_accrual_interest_entry_for_demand_loans(
|
||||
posting_date, process_loan_interest, open_loans=None, loan_type=None, accrual_type="Regular"
|
||||
):
|
||||
query_filters = {"status": ("in", ["Disbursed", "Partially Disbursed"]), "docstatus": 1}
|
||||
query_filters = {
|
||||
"status": ("in", ["Disbursed", "Partially Disbursed"]),
|
||||
"docstatus": 1,
|
||||
"is_term_loan": 0,
|
||||
}
|
||||
|
||||
if loan_type:
|
||||
query_filters.update({"loan_type": loan_type})
|
||||
@@ -147,6 +151,9 @@ def make_accrual_interest_entry_for_demand_loans(
|
||||
"name",
|
||||
"total_payment",
|
||||
"total_amount_paid",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"loan_account",
|
||||
"interest_income_account",
|
||||
"loan_amount",
|
||||
@@ -229,6 +236,7 @@ def get_term_loans(date, term_loan=None, loan_type=None):
|
||||
AND l.is_term_loan =1
|
||||
AND rs.payment_date <= %s
|
||||
AND rs.is_accrued=0 {0}
|
||||
AND rs.principal_amount > 0
|
||||
AND l.status = 'Disbursed'
|
||||
ORDER BY rs.payment_date""".format(
|
||||
condition
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Refund', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user