mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-13 18:28:48 +00:00
Compare commits
200 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c15cff05b0 | ||
|
|
b80526f226 | ||
|
|
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 | ||
|
|
84e6cead56 | ||
|
|
590e48fa1d | ||
|
|
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'
|
||||
}
|
||||
],
|
||||
[
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.35.0"
|
||||
__version__ = "13.36.3"
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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, getdate, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
@@ -219,6 +219,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
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"account_for_change_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break_23",
|
||||
"income_account",
|
||||
"expense_account",
|
||||
@@ -358,6 +359,13 @@
|
||||
"fieldname": "validate_stock_on_save",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Stock on Save"
|
||||
},
|
||||
{
|
||||
"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 +393,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2022-03-21 13:29:28.480533",
|
||||
"modified": "2022-07-21 11:16:46.911173",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -2173,13 +2173,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
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.",
|
||||
}
|
||||
|
||||
|
||||
@@ -1470,8 +1470,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
|
||||
|
||||
@@ -299,9 +299,10 @@ 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 = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
|
||||
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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-06-17 11:31:37.730760",
|
||||
"modified": "2022-07-18 20:03:43.188705",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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.reload()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
"total_payment",
|
||||
"total_principal_paid",
|
||||
"written_off_amount",
|
||||
"refund_amount",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"is_npa",
|
||||
"column_break_19",
|
||||
"total_interest_payable",
|
||||
"total_amount_paid",
|
||||
@@ -379,12 +383,39 @@
|
||||
"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"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-10 11:50:31.957360",
|
||||
"modified": "2022-06-30 12:04:13.728880",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -147,6 +147,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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
// }
|
||||
});
|
||||
176
erpnext/loan_management/doctype/loan_refund/loan_refund.json
Normal file
176
erpnext/loan_management/doctype/loan_refund/loan_refund.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "LM-RF-.#####",
|
||||
"creation": "2022-06-24 15:51:03.165498",
|
||||
"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",
|
||||
"refund_account",
|
||||
"column_break_11",
|
||||
"refund_amount",
|
||||
"reference_number",
|
||||
"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": "Refund Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "refund_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Refund Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "refund_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Refund Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Refund",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Refund",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Number"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-24 16:13:48.793486",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Refund",
|
||||
"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
|
||||
}
|
||||
97
erpnext/loan_management/doctype/loan_refund/loan_refund.py
Normal file
97
erpnext/loan_management/doctype/loan_refund/loan_refund.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# 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 getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||
get_pending_principal_amount,
|
||||
)
|
||||
|
||||
|
||||
class LoanRefund(AccountsController):
|
||||
"""
|
||||
Add refund if total repayment is more than that is owed.
|
||||
"""
|
||||
|
||||
def validate(self):
|
||||
self.set_missing_values()
|
||||
self.validate_refund_amount()
|
||||
|
||||
def set_missing_values(self):
|
||||
if not self.cost_center:
|
||||
self.cost_center = erpnext.get_default_cost_center(self.company)
|
||||
|
||||
def validate_refund_amount(self):
|
||||
loan = frappe.get_doc("Loan", self.loan)
|
||||
pending_amount = get_pending_principal_amount(loan)
|
||||
if pending_amount >= 0:
|
||||
frappe.throw(_("No excess amount to refund."))
|
||||
else:
|
||||
excess_amount = pending_amount * -1
|
||||
|
||||
if self.refund_amount > excess_amount:
|
||||
frappe.throw(_("Refund amount cannot be greater than excess amount {0}").format(excess_amount))
|
||||
|
||||
def on_submit(self):
|
||||
self.update_outstanding_amount()
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_outstanding_amount(cancel=1)
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
self.make_gl_entries(cancel=1)
|
||||
|
||||
def update_outstanding_amount(self, cancel=0):
|
||||
refund_amount = frappe.db.get_value("Loan", self.loan, "refund_amount")
|
||||
|
||||
if cancel:
|
||||
refund_amount -= self.refund_amount
|
||||
else:
|
||||
refund_amount += self.refund_amount
|
||||
|
||||
frappe.db.set_value("Loan", self.loan, "refund_amount", refund_amount)
|
||||
|
||||
def make_gl_entries(self, cancel=0):
|
||||
gl_entries = []
|
||||
loan_details = frappe.get_doc("Loan", self.loan)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.refund_account,
|
||||
"against": loan_details.loan_account,
|
||||
"credit": self.refund_amount,
|
||||
"credit_in_account_currency": self.refund_amount,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.loan,
|
||||
"remarks": _("Against Loan:") + self.loan,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": loan_details.loan_account,
|
||||
"party_type": loan_details.applicant_type,
|
||||
"party": loan_details.applicant,
|
||||
"against": self.refund_account,
|
||||
"debit": self.refund_amount,
|
||||
"debit_in_account_currency": self.refund_amount,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.loan,
|
||||
"remarks": _("Against Loan:") + self.loan,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
make_gl_entries(gl_entries, cancel=cancel, 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 TestLoanRefund(FrappeTestCase):
|
||||
pass
|
||||
@@ -281,11 +281,11 @@
|
||||
},
|
||||
{
|
||||
"fetch_from": "against_loan.payment_account",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "payment_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Repayment Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
@@ -311,7 +311,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-18 19:10:07.742298",
|
||||
"modified": "2022-08-04 17:13:51.964203",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Repayment",
|
||||
@@ -353,4 +353,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,9 @@ class LoanRepayment(AccountsController):
|
||||
"status",
|
||||
"is_secured_loan",
|
||||
"total_payment",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"loan_amount",
|
||||
"disbursed_amount",
|
||||
"total_interest_payable",
|
||||
@@ -387,15 +390,19 @@ class LoanRepayment(AccountsController):
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gle_map = []
|
||||
|
||||
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
|
||||
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(
|
||||
remarks = "Shortfall repayment of {0}.<br>Repayment against loan {1}".format(
|
||||
self.shortfall_amount, self.against_loan
|
||||
)
|
||||
elif self.shortfall_amount:
|
||||
remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount)
|
||||
remarks = "Shortfall repayment of {0} against loan {1}".format(
|
||||
self.shortfall_amount, self.against_loan
|
||||
)
|
||||
else:
|
||||
remarks = _("Repayment against Loan: ") + self.against_loan
|
||||
remarks = "Repayment against loan " + self.against_loan
|
||||
|
||||
if self.reference_number:
|
||||
remarks += " with reference no. {}".format(self.reference_number)
|
||||
|
||||
if self.repay_from_salary:
|
||||
payment_account = self.payroll_payable_account
|
||||
@@ -446,7 +453,7 @@ class LoanRepayment(AccountsController):
|
||||
"debit_in_account_currency": self.amount_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.against_loan,
|
||||
"remarks": remarks,
|
||||
"remarks": _(remarks),
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
}
|
||||
@@ -464,7 +471,7 @@ class LoanRepayment(AccountsController):
|
||||
"credit_in_account_currency": self.amount_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.against_loan,
|
||||
"remarks": remarks,
|
||||
"remarks": _(remarks),
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
}
|
||||
@@ -624,16 +631,22 @@ def get_pending_principal_amount(loan):
|
||||
if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
|
||||
pending_principal_amount = (
|
||||
flt(loan.total_payment)
|
||||
+ flt(loan.debit_adjustment_amount)
|
||||
- flt(loan.credit_adjustment_amount)
|
||||
- flt(loan.total_principal_paid)
|
||||
- flt(loan.total_interest_payable)
|
||||
- flt(loan.written_off_amount)
|
||||
+ flt(loan.refund_amount)
|
||||
)
|
||||
else:
|
||||
pending_principal_amount = (
|
||||
flt(loan.disbursed_amount)
|
||||
+ flt(loan.debit_adjustment_amount)
|
||||
- flt(loan.credit_adjustment_amount)
|
||||
- flt(loan.total_principal_paid)
|
||||
- flt(loan.total_interest_payable)
|
||||
- flt(loan.written_off_amount)
|
||||
+ flt(loan.refund_amount)
|
||||
)
|
||||
|
||||
return pending_principal_amount
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan",
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"status",
|
||||
"column_break_3",
|
||||
"shortfall_time",
|
||||
@@ -23,6 +25,8 @@
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Loan ",
|
||||
"options": "Loan",
|
||||
"read_only": 1
|
||||
@@ -91,17 +95,35 @@
|
||||
{
|
||||
"fieldname": "shortfall_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Shortfall Percentage",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant_type",
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-01 08:13:43.263772",
|
||||
"modified": "2022-06-30 11:57:09.378089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Shortfall",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -132,5 +154,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -58,6 +58,9 @@ class LoanSecurityUnpledge(Document):
|
||||
self.loan,
|
||||
[
|
||||
"total_payment",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"total_principal_paid",
|
||||
"loan_amount",
|
||||
"total_interest_payable",
|
||||
|
||||
@@ -9,6 +9,9 @@ from frappe.utils import cint, flt, getdate
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||
get_pending_principal_amount,
|
||||
)
|
||||
|
||||
|
||||
class LoanWriteOff(AccountsController):
|
||||
@@ -22,16 +25,26 @@ class LoanWriteOff(AccountsController):
|
||||
|
||||
def validate_write_off_amount(self):
|
||||
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value(
|
||||
|
||||
loan_details = frappe.get_value(
|
||||
"Loan",
|
||||
self.loan,
|
||||
["total_payment", "total_principal_paid", "total_interest_payable", "written_off_amount"],
|
||||
[
|
||||
"total_payment",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"refund_amount",
|
||||
"total_principal_paid",
|
||||
"loan_amount",
|
||||
"total_interest_payable",
|
||||
"written_off_amount",
|
||||
"disbursed_amount",
|
||||
"status",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
pending_principal_amount = flt(
|
||||
flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount),
|
||||
precision,
|
||||
)
|
||||
pending_principal_amount = flt(get_pending_principal_amount(loan_details), precision)
|
||||
|
||||
if self.write_off_amount > pending_principal_amount:
|
||||
frappe.throw(_("Write off amount cannot be greater than pending principal amount"))
|
||||
|
||||
@@ -54,17 +54,18 @@
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Accrual Type",
|
||||
"options": "Regular\nRepayment\nDisbursement",
|
||||
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-06 13:28:51.478909",
|
||||
"modified": "2022-06-29 11:19:33.203088",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Process Loan Interest Accrual",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -98,5 +99,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -444,6 +444,7 @@ class BOM(WebsiteGenerator):
|
||||
and self.is_active
|
||||
):
|
||||
frappe.db.set(self, "is_default", 1)
|
||||
frappe.db.set_value("Item", self.item, "default_bom", self.name)
|
||||
else:
|
||||
frappe.db.set(self, "is_default", 0)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
|
||||
@@ -519,6 +519,42 @@ class TestBOM(FrappeTestCase):
|
||||
|
||||
new_bom.delete()
|
||||
|
||||
def test_set_default_bom_for_item_having_single_bom(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1})
|
||||
bom_item = make_item(properties={"is_stock_item": 1})
|
||||
|
||||
# Step 1: Create BOM
|
||||
bom = frappe.new_doc("BOM")
|
||||
bom.item = fg_item.item_code
|
||||
bom.quantity = 1
|
||||
bom.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": 1,
|
||||
"uom": bom_item.stock_uom,
|
||||
"stock_uom": bom_item.stock_uom,
|
||||
"rate": 100.0,
|
||||
},
|
||||
)
|
||||
bom.save()
|
||||
bom.submit()
|
||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||
|
||||
# Step 2: Uncheck is_active field
|
||||
bom.is_active = 0
|
||||
bom.save()
|
||||
bom.reload()
|
||||
self.assertIsNone(frappe.get_value("Item", fg_item.item_code, "default_bom"))
|
||||
|
||||
# Step 3: Check is_active field
|
||||
bom.is_active = 1
|
||||
bom.save()
|
||||
bom.reload()
|
||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -21,13 +21,18 @@ frappe.ui.form.on('Member', {
|
||||
|
||||
// custom buttons
|
||||
frm.add_custom_button(__('Accounting Ledger'), function() {
|
||||
frappe.set_route('query-report', 'General Ledger',
|
||||
{party_type:'Member', party:frm.doc.name});
|
||||
if (frm.doc.customer) {
|
||||
frappe.set_route('query-report', 'General Ledger', {party_type: 'Customer', party: frm.doc.customer});
|
||||
} else {
|
||||
frappe.set_route('query-report', 'General Ledger', {party_type: 'Member', party: frm.doc.name});
|
||||
}
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Accounts Receivable'), function() {
|
||||
frappe.set_route('query-report', 'Accounts Receivable', {member:frm.doc.name});
|
||||
});
|
||||
if (frm.doc.customer) {
|
||||
frm.add_custom_button(__('Accounts Receivable'), function() {
|
||||
frappe.set_route('query-report', 'Accounts Receivable', {customer: frm.doc.customer});
|
||||
});
|
||||
}
|
||||
|
||||
if (!frm.doc.customer) {
|
||||
frm.add_custom_button(__('Create Customer'), () => {
|
||||
|
||||
@@ -368,3 +368,8 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
erpnext.patches.v13_0.update_employee_advance_status
|
||||
erpnext.patches.v13_0.job_card_status_on_hold
|
||||
erpnext.patches.v13_0.add_cost_center_in_loans
|
||||
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
|
||||
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
|
||||
erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
|
||||
def execute():
|
||||
accounting_dimensions = frappe.db.get_all(
|
||||
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||
)
|
||||
|
||||
if not accounting_dimensions:
|
||||
return
|
||||
|
||||
for d in accounting_dimensions:
|
||||
doctype = "Asset Repair"
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
continue
|
||||
|
||||
df = {
|
||||
"fieldname": d.fieldname,
|
||||
"label": d.label,
|
||||
"fieldtype": "Link",
|
||||
"options": d.document_type,
|
||||
"insert_after": "accounting_dimensions_section",
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
@@ -33,7 +33,10 @@ def execute():
|
||||
"insert_after": insert_after_field,
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
try:
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
count += 1
|
||||
|
||||
@@ -17,7 +17,6 @@ def execute():
|
||||
"item_group",
|
||||
"stock_uom",
|
||||
"brand",
|
||||
"image",
|
||||
"has_variants",
|
||||
"variant_of",
|
||||
"description",
|
||||
@@ -30,6 +29,7 @@ def execute():
|
||||
"website_warehouse",
|
||||
"web_long_description",
|
||||
"website_content",
|
||||
"website_image",
|
||||
"thumbnail",
|
||||
]
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if not frappe.db.has_column("Asset", "allow_monthly_depreciation"):
|
||||
return
|
||||
|
||||
assets = frappe.get_all("Asset", filters={"allow_monthly_depreciation": 1})
|
||||
for d in assets:
|
||||
print(d.name)
|
||||
asset_doc = frappe.get_doc("Asset", d.name)
|
||||
for i in asset_doc.get("finance_books"):
|
||||
if i.frequency_of_depreciation != 1:
|
||||
i.total_number_of_depreciations *= i.frequency_of_depreciation
|
||||
i.frequency_of_depreciation = 1
|
||||
i.db_update()
|
||||
29
erpnext/patches/v13_0/reset_corrupt_defaults.py
Normal file
29
erpnext/patches/v13_0/reset_corrupt_defaults.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
This patch is needed to fix parent incorrectly set as `__2fa` because of
|
||||
https://github.com/frappe/frappe/commit/a822092211533ff17ff9b92dd86f6f868ed63e2e
|
||||
"""
|
||||
|
||||
for doctype in (
|
||||
"Accounts Settings",
|
||||
"Stock Settings",
|
||||
"Selling Settings",
|
||||
"Buying Settings",
|
||||
"CRM Settings",
|
||||
"Global Defaults",
|
||||
"Healthcare Settings",
|
||||
"Education Settings",
|
||||
):
|
||||
try:
|
||||
frappe.get_single(doctype).save()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
pos_profile = frappe.get_last_doc("POS Profile")
|
||||
pos_profile.set_defaults()
|
||||
except Exception:
|
||||
pass
|
||||
16
erpnext/patches/v13_0/show_hr_payroll_deprecation_warning.py
Normal file
16
erpnext/patches/v13_0/show_hr_payroll_deprecation_warning.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if "hrms" in frappe.get_installed_apps():
|
||||
return
|
||||
|
||||
click.secho(
|
||||
"HR and Payroll modules have been moved to a separate app"
|
||||
" and will be removed from ERPNext in Version 14."
|
||||
" Please install the HRMS app when upgrading to Version 14"
|
||||
" to continue using the HR and Payroll modules:\n"
|
||||
"https://github.com/frappe/hrms",
|
||||
fg="yellow",
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if not frappe.db.exists("Company", {"country": "India"}):
|
||||
return
|
||||
|
||||
click.secho(
|
||||
"India-specific regional features have been moved to a separate app"
|
||||
" and will be removed from ERPNext in Version 14."
|
||||
" Please install India Compliance after upgrading to Version 14:\n"
|
||||
"https://github.com/resilient-tech/india-compliance",
|
||||
fg="yellow",
|
||||
)
|
||||
@@ -622,11 +622,23 @@ class SalarySlip(TransactionBase):
|
||||
self.add_tax_components(payroll_period)
|
||||
|
||||
def add_structure_components(self, component_type):
|
||||
data = self.get_data_for_eval()
|
||||
data, default_data = self.get_data_for_eval()
|
||||
timesheet_component = frappe.db.get_value(
|
||||
"Salary Structure", self.salary_structure, "salary_component"
|
||||
)
|
||||
|
||||
for struct_row in self._salary_structure_doc.get(component_type):
|
||||
if self.salary_slip_based_on_timesheet and struct_row.salary_component == timesheet_component:
|
||||
continue
|
||||
|
||||
amount = self.eval_condition_and_formula(struct_row, data)
|
||||
if amount is not None and struct_row.statistical_component == 0:
|
||||
self.update_component_row(struct_row, amount, component_type)
|
||||
if (
|
||||
amount or (struct_row.amount_based_on_formula and amount is not None)
|
||||
) and struct_row.statistical_component == 0:
|
||||
default_amount = self.eval_condition_and_formula(struct_row, default_data)
|
||||
self.update_component_row(
|
||||
struct_row, amount, component_type, data=data, default_amount=default_amount
|
||||
)
|
||||
|
||||
def get_data_for_eval(self):
|
||||
"""Returns data for evaluating formula"""
|
||||
@@ -670,11 +682,15 @@ class SalarySlip(TransactionBase):
|
||||
for sc in salary_components:
|
||||
data.setdefault(sc.salary_component_abbr, 0)
|
||||
|
||||
# shallow copy of data to store default amounts (without payment days) for tax calculation
|
||||
default_data = data.copy()
|
||||
|
||||
for key in ("earnings", "deductions"):
|
||||
for d in self.get(key):
|
||||
default_data[d.abbr] = d.default_amount
|
||||
data[d.abbr] = d.amount
|
||||
|
||||
return data
|
||||
return data, default_data
|
||||
|
||||
def eval_condition_and_formula(self, d, data):
|
||||
try:
|
||||
@@ -780,7 +796,14 @@ class SalarySlip(TransactionBase):
|
||||
self.update_component_row(tax_row, tax_amount, "deductions")
|
||||
|
||||
def update_component_row(
|
||||
self, component_data, amount, component_type, additional_salary=None, is_recurring=0
|
||||
self,
|
||||
component_data,
|
||||
amount,
|
||||
component_type,
|
||||
additional_salary=None,
|
||||
is_recurring=0,
|
||||
data=None,
|
||||
default_amount=None,
|
||||
):
|
||||
component_row = None
|
||||
for d in self.get(component_type):
|
||||
@@ -841,7 +864,7 @@ class SalarySlip(TransactionBase):
|
||||
additional_salary.deduct_full_tax_on_selected_payroll_date
|
||||
)
|
||||
else:
|
||||
component_row.default_amount = amount
|
||||
component_row.default_amount = default_amount or amount
|
||||
component_row.additional_amount = 0
|
||||
component_row.deduct_full_tax_on_selected_payroll_date = (
|
||||
component_data.deduct_full_tax_on_selected_payroll_date
|
||||
@@ -850,6 +873,8 @@ class SalarySlip(TransactionBase):
|
||||
component_row.amount = amount
|
||||
|
||||
self.update_component_amount_based_on_payment_days(component_row)
|
||||
if data:
|
||||
data[component_row.abbr] = component_row.amount
|
||||
|
||||
def update_component_amount_based_on_payment_days(self, component_row):
|
||||
joining_date, relieving_date = self.get_joining_and_relieving_dates()
|
||||
@@ -1272,7 +1297,7 @@ class SalarySlip(TransactionBase):
|
||||
)[0].total_amount
|
||||
|
||||
def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab):
|
||||
data = self.get_data_for_eval()
|
||||
data, default_data = self.get_data_for_eval()
|
||||
data.update({"annual_taxable_earning": annual_taxable_earning})
|
||||
tax_amount = 0
|
||||
for slab in tax_slab.slabs:
|
||||
@@ -1350,23 +1375,22 @@ class SalarySlip(TransactionBase):
|
||||
self.total_interest_amount = 0
|
||||
self.total_principal_amount = 0
|
||||
|
||||
if not self.get("loans"):
|
||||
for loan in self.get_loan_details():
|
||||
self.set("loans", [])
|
||||
for loan in self.get_loan_details():
|
||||
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
|
||||
|
||||
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
|
||||
|
||||
if amounts["interest_amount"] or amounts["payable_principal_amount"]:
|
||||
self.append(
|
||||
"loans",
|
||||
{
|
||||
"loan": loan.name,
|
||||
"total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"],
|
||||
"interest_amount": amounts["interest_amount"],
|
||||
"principal_amount": amounts["payable_principal_amount"],
|
||||
"loan_account": loan.loan_account,
|
||||
"interest_income_account": loan.interest_income_account,
|
||||
},
|
||||
)
|
||||
if amounts["interest_amount"] or amounts["payable_principal_amount"]:
|
||||
self.append(
|
||||
"loans",
|
||||
{
|
||||
"loan": loan.name,
|
||||
"total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"],
|
||||
"interest_amount": amounts["interest_amount"],
|
||||
"principal_amount": amounts["payable_principal_amount"],
|
||||
"loan_account": loan.loan_account,
|
||||
"interest_income_account": loan.interest_income_account,
|
||||
},
|
||||
)
|
||||
|
||||
for payment in self.get("loans"):
|
||||
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -35,13 +35,12 @@ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_detail
|
||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
|
||||
|
||||
class TestSalarySlip(unittest.TestCase):
|
||||
class TestSalarySlip(FrappeTestCase):
|
||||
def setUp(self):
|
||||
setup_test()
|
||||
frappe.flags.pop("via_payroll_entry", None)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@@ -372,13 +371,19 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1)
|
||||
|
||||
# gross pay calculation based on attendance (payment days)
|
||||
gross_pay = 78100 - (
|
||||
(78000 / (days_in_month - no_of_holidays))
|
||||
* flt(salary_slip.leave_without_pay + salary_slip.absent_days)
|
||||
# component calculation based on attendance (payment days)
|
||||
amount, precision = None, None
|
||||
|
||||
for row in salary_slip.earnings:
|
||||
if row.salary_component == "Basic Salary":
|
||||
amount = row.amount
|
||||
precision = row.precision("amount")
|
||||
break
|
||||
expected_amount = flt(
|
||||
(50000 * salary_slip.payment_days / salary_slip.total_working_days), precision
|
||||
)
|
||||
|
||||
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
|
||||
self.assertEqual(amount, expected_amount)
|
||||
|
||||
@change_settings("Payroll Settings", {"payroll_based_on": "Attendance"})
|
||||
def test_component_amount_dependent_on_another_payment_days_based_component(self):
|
||||
@@ -919,6 +924,41 @@ class TestSalarySlip(unittest.TestCase):
|
||||
# undelete fixture data
|
||||
frappe.db.rollback()
|
||||
|
||||
@change_settings(
|
||||
"Payroll Settings",
|
||||
{
|
||||
"payroll_based_on": "Attendance",
|
||||
"consider_unmarked_attendance_as": "Present",
|
||||
"include_holidays_in_total_working_days": True,
|
||||
},
|
||||
)
|
||||
def test_default_amount(self):
|
||||
# Special Allowance (SA) uses another component Basic (BS) in it's formula : BD * .5
|
||||
# Basic has "Depends on Payment Days" enabled
|
||||
# Test default amount for SA is based on default amount for BS (irrespective of PD)
|
||||
# Test amount for SA is based on amount for BS (based on PD)
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
month_start_date = get_first_day(nowdate())
|
||||
joining_date = add_days(month_start_date, 3)
|
||||
employee = make_employee("test_tax_for_mid_joinee@salary.com", date_of_joining=joining_date)
|
||||
|
||||
salary_structure = make_salary_structure(
|
||||
"Stucture to test tax",
|
||||
"Monthly",
|
||||
test_tax=True,
|
||||
from_date=joining_date,
|
||||
employee=employee,
|
||||
)
|
||||
|
||||
ss = make_salary_slip(salary_structure.name, employee=employee)
|
||||
|
||||
# default amount for SA (special allowance = BS*0.5) should be based on default amount for basic
|
||||
self.assertEqual(ss.earnings[2].default_amount, 25000)
|
||||
self.assertEqual(
|
||||
ss.earnings[2].amount, flt(ss.earnings[0].amount * 0.5, ss.earnings[0].precision("amount"))
|
||||
)
|
||||
|
||||
def test_tax_for_recurring_additional_salary(self):
|
||||
frappe.db.sql("""delete from `tabPayroll Period`""")
|
||||
frappe.db.sql("""delete from `tabSalary Component`""")
|
||||
@@ -981,6 +1021,16 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_do_not_show_statistical_component_in_slip(self):
|
||||
make_employee("test_statistical_component@salary.com")
|
||||
new_ss = make_employee_salary_slip(
|
||||
"test_statistical_component@salary.com",
|
||||
"Monthly",
|
||||
"Test Payment Based On Attendence",
|
||||
)
|
||||
components = [row.salary_component for row in new_ss.get("earnings")]
|
||||
self.assertNotIn("Statistical Component", components)
|
||||
|
||||
def make_activity_for_employee(self):
|
||||
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
|
||||
activity_type.billing_rate = 50
|
||||
@@ -1038,7 +1088,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, po
|
||||
def make_salary_component(salary_components, test_tax, company_list=None):
|
||||
for salary_component in salary_components:
|
||||
if frappe.db.exists("Salary Component", salary_component["salary_component"]):
|
||||
continue
|
||||
frappe.delete_doc("Salary Component", salary_component["salary_component"], force=True)
|
||||
|
||||
if test_tax:
|
||||
if salary_component["type"] == "Earning":
|
||||
@@ -1119,8 +1169,16 @@ def make_earning_salary_component(
|
||||
"formula": "BS*.5",
|
||||
"type": "Earning",
|
||||
"amount_based_on_formula": 1,
|
||||
"depends_on_payment_days": 0,
|
||||
},
|
||||
{"salary_component": "Leave Encashment", "abbr": "LE", "type": "Earning"},
|
||||
{
|
||||
"salary_component": "Statistical Component",
|
||||
"abbr": "SC",
|
||||
"type": "Earning",
|
||||
"statistical_component": 1,
|
||||
"amount": 500,
|
||||
},
|
||||
]
|
||||
if include_flexi_benefits:
|
||||
data.extend(
|
||||
@@ -1418,6 +1476,10 @@ def setup_test():
|
||||
"Salary Slip",
|
||||
"Attendance",
|
||||
"Additional Salary",
|
||||
"Employee Tax Exemption Declaration",
|
||||
"Employee Tax Exemption Proof Submission",
|
||||
"Employee Benefit Claim",
|
||||
"Salary Structure Assignment",
|
||||
]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -18,6 +19,8 @@ class SalaryStructure(Document):
|
||||
self.strip_condition_and_formula_fields()
|
||||
self.validate_max_benefits_with_flexi()
|
||||
self.validate_component_based_on_tax_slab()
|
||||
self.validate_payment_days_based_dependent_component()
|
||||
self.validate_timesheet_component()
|
||||
|
||||
def set_missing_values(self):
|
||||
overwritten_fields = [
|
||||
@@ -58,6 +61,50 @@ class SalaryStructure(Document):
|
||||
if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet:
|
||||
frappe.throw(_("Net pay cannot be negative"))
|
||||
|
||||
def validate_payment_days_based_dependent_component(self):
|
||||
abbreviations = self.get_component_abbreviations()
|
||||
for component_type in ("earnings", "deductions"):
|
||||
for row in self.get(component_type):
|
||||
if (
|
||||
row.formula
|
||||
and row.depends_on_payment_days
|
||||
# check if the formula contains any of the payment days components
|
||||
and any(re.search(r"\b" + abbr + r"\b", row.formula) for abbr in abbreviations)
|
||||
):
|
||||
message = _("Row #{0}: The {1} Component has the options {2} and {3} enabled.").format(
|
||||
row.idx,
|
||||
frappe.bold(row.salary_component),
|
||||
frappe.bold("Amount based on formula"),
|
||||
frappe.bold("Depends On Payment Days"),
|
||||
)
|
||||
message += "<br><br>" + _(
|
||||
"Disable {0} for the {1} component, to prevent the amount from being deducted twice, as its formula already uses a payment-days-based component."
|
||||
).format(
|
||||
frappe.bold("Depends On Payment Days"), frappe.bold(row.salary_component)
|
||||
)
|
||||
frappe.throw(message, title=_("Payment Days Dependency"))
|
||||
|
||||
def get_component_abbreviations(self):
|
||||
abbr = [d.abbr for d in self.earnings if d.depends_on_payment_days]
|
||||
abbr += [d.abbr for d in self.deductions if d.depends_on_payment_days]
|
||||
|
||||
return abbr
|
||||
|
||||
def validate_timesheet_component(self):
|
||||
if not self.salary_slip_based_on_timesheet:
|
||||
return
|
||||
|
||||
for component in self.earnings:
|
||||
if component.salary_component == self.salary_component:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row #{0}: Timesheet amount will overwrite the Earning component amount for the Salary Component {1}"
|
||||
).format(self.idx, frappe.bold(self.salary_component)),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
break
|
||||
|
||||
def strip_condition_and_formula_fields(self):
|
||||
# remove whitespaces from condition and formula fields
|
||||
for row in self.earnings:
|
||||
|
||||
@@ -150,6 +150,7 @@ def make_salary_structure(
|
||||
currency=erpnext.get_default_currency(),
|
||||
payroll_period=None,
|
||||
include_flexi_benefits=False,
|
||||
base=None,
|
||||
):
|
||||
if test_tax:
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
||||
@@ -200,6 +201,7 @@ def make_salary_structure(
|
||||
company=company,
|
||||
currency=currency,
|
||||
payroll_period=payroll_period,
|
||||
base=base,
|
||||
)
|
||||
|
||||
return salary_structure_doc
|
||||
|
||||
@@ -16,7 +16,7 @@ class Homepage(Document):
|
||||
def setup_items(self):
|
||||
for d in frappe.get_all(
|
||||
"Website Item",
|
||||
fields=["name", "item_name", "description", "image", "route"],
|
||||
fields=["name", "item_name", "description", "website_image", "route"],
|
||||
filters={"published": 1},
|
||||
limit=3,
|
||||
):
|
||||
@@ -31,7 +31,7 @@ class Homepage(Document):
|
||||
item_code=d.name,
|
||||
item_name=d.item_name,
|
||||
description=d.description,
|
||||
image=d.image,
|
||||
image=d.website_image,
|
||||
route=d.route,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2122,7 +2122,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
"qty": item.qty,
|
||||
"description": item.description,
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no
|
||||
"batch_no": item.batch_no,
|
||||
"sample_size": item.sample_quantity
|
||||
});
|
||||
dialog_items.grid.refresh();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
"section_break_2",
|
||||
"sandbox_mode",
|
||||
"applicable_from",
|
||||
"column_break_4",
|
||||
"dont_show_discounts_in_e_invoice",
|
||||
"section_break_7",
|
||||
"credentials",
|
||||
"advanced_settings_section",
|
||||
"client_id",
|
||||
@@ -80,12 +83,28 @@
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Enabling this will directly report net rates in e-Invoice post discounts",
|
||||
"fieldname": "dont_show_discounts_in_e_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Don't show discounts in e-Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-16 19:50:28.029517",
|
||||
"modified": "2022-07-17 14:57:50.783517",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "E Invoice Settings",
|
||||
|
||||
@@ -144,7 +144,7 @@ class GSTR3BReport(Document):
|
||||
def get_inward_nil_exempt(self, state):
|
||||
inward_nil_exempt = frappe.db.sql(
|
||||
"""
|
||||
SELECT p.place_of_supply, p.supplier_address,
|
||||
SELECT p.name, p.place_of_supply, p.supplier_address, p.gst_category,
|
||||
i.base_amount, i.is_nil_exempt, i.is_non_gst
|
||||
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||
WHERE p.docstatus = 1 and p.name = i.parent
|
||||
|
||||
@@ -65,6 +65,7 @@ def validate_eligibility(doc):
|
||||
"SEZ",
|
||||
"Overseas",
|
||||
"Deemed Export",
|
||||
"UIN Holders",
|
||||
]
|
||||
company_transaction = doc.get("billing_address_gstin") == doc.get("company_gstin")
|
||||
|
||||
@@ -130,9 +131,7 @@ def read_json(name):
|
||||
|
||||
def get_transaction_details(invoice):
|
||||
supply_type = ""
|
||||
if (
|
||||
invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition"
|
||||
):
|
||||
if invoice.gst_category in ("Registered Regular", "Registered Composition", "UIN Holders"):
|
||||
supply_type = "B2B"
|
||||
elif invoice.gst_category == "SEZ":
|
||||
if invoice.export_type == "Without Payment of Tax":
|
||||
@@ -148,15 +147,18 @@ def get_transaction_details(invoice):
|
||||
supply_type = "DEXP"
|
||||
|
||||
if not supply_type:
|
||||
rr, rc, sez, overseas, export = (
|
||||
rr, rc, sez, overseas, export, uin = (
|
||||
bold("Registered Regular"),
|
||||
bold("Registered Composition"),
|
||||
bold("SEZ"),
|
||||
bold("Overseas"),
|
||||
bold("Deemed Export"),
|
||||
bold("UIN Holders"),
|
||||
)
|
||||
frappe.throw(
|
||||
_("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export),
|
||||
_("GST category should be one of {}, {}, {}, {}, {}, {}").format(
|
||||
rr, rc, sez, overseas, export, uin
|
||||
),
|
||||
title=_("Invalid Supply Type"),
|
||||
)
|
||||
|
||||
@@ -263,6 +265,10 @@ def get_overseas_address_details(address_name):
|
||||
def get_item_list(invoice):
|
||||
item_list = []
|
||||
|
||||
hide_discount_in_einvoice = cint(
|
||||
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
|
||||
)
|
||||
|
||||
for d in invoice.items:
|
||||
einvoice_item_schema = read_json("einv_item_template")
|
||||
item = frappe._dict({})
|
||||
@@ -272,14 +278,38 @@ def get_item_list(invoice):
|
||||
item.description = sanitize_for_json(d.item_name)
|
||||
|
||||
item.qty = abs(item.qty)
|
||||
item_qty = item.qty
|
||||
|
||||
if invoice.get("apply_discount_on"):
|
||||
item.discount_amount = item.base_amount - item.base_net_amount
|
||||
|
||||
item.unit_rate = abs(item.taxable_value - item.discount_amount) / item.qty
|
||||
|
||||
item.gross_amount = abs(item.taxable_value) + item.discount_amount
|
||||
item.taxable_value = abs(item.taxable_value)
|
||||
|
||||
if invoice.get("is_return") or invoice.get("is_debit_note"):
|
||||
item_qty = item_qty or 1
|
||||
|
||||
if hide_discount_in_einvoice or invoice.is_internal_customer or item.discount_amount < 0:
|
||||
item.unit_rate = item.taxable_value / item_qty
|
||||
item.gross_amount = item.taxable_value
|
||||
item.discount_amount = 0
|
||||
|
||||
else:
|
||||
if invoice.get("apply_discount_on") and (abs(invoice.get("base_discount_amount") or 0.0) > 0.0):
|
||||
# TODO: need to handle case when tax included in basic rate is checked.
|
||||
item.discount_amount = (item.discount_amount * item_qty) + (
|
||||
abs(item.base_amount) - abs(item.base_net_amount)
|
||||
)
|
||||
else:
|
||||
item.discount_amount = item.discount_amount * item_qty
|
||||
|
||||
try:
|
||||
item.unit_rate = (item.taxable_value + item.discount_amount) / item_qty
|
||||
except ZeroDivisionError:
|
||||
# This will never run but added as safety measure
|
||||
frappe.throw(
|
||||
title=_("Error: Qty is Zero"),
|
||||
msg=_("Quantity can't be zero unless it's Credit/Debit Note."),
|
||||
)
|
||||
|
||||
item.gross_amount = item.taxable_value + item.discount_amount
|
||||
item.taxable_value = item.taxable_value
|
||||
item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N"
|
||||
item.serial_no = ""
|
||||
|
||||
|
||||
@@ -714,7 +714,7 @@ def get_custom_fields():
|
||||
insert_after="customer",
|
||||
no_copy=1,
|
||||
print_hide=1,
|
||||
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0',
|
||||
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export", "UIN Holders"], doc.gst_category) && doc.irn_cancelled === 0',
|
||||
),
|
||||
dict(
|
||||
fieldname="irn_cancelled",
|
||||
|
||||
@@ -40,13 +40,12 @@ def validate_gstin_for_india(doc, method):
|
||||
|
||||
gst_category = []
|
||||
|
||||
if hasattr(doc, "gst_category"):
|
||||
if len(doc.links):
|
||||
link_doctype = doc.links[0].get("link_doctype")
|
||||
link_name = doc.links[0].get("link_name")
|
||||
if len(doc.links):
|
||||
link_doctype = doc.links[0].get("link_doctype")
|
||||
link_name = doc.links[0].get("link_name")
|
||||
|
||||
if link_doctype in ["Customer", "Supplier"]:
|
||||
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
|
||||
if link_doctype in ["Customer", "Supplier"]:
|
||||
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
|
||||
|
||||
doc.gstin = doc.gstin.upper().strip()
|
||||
if not doc.gstin or doc.gstin == "NA":
|
||||
@@ -581,7 +580,7 @@ def get_ewb_data(dt, dn):
|
||||
|
||||
if dt == "Delivery Note":
|
||||
data.subSupplyType = 1
|
||||
elif doc.gst_category in ["Registered Regular", "SEZ"]:
|
||||
elif doc.gst_category in ["Unregistered", "Registered Regular", "SEZ"]:
|
||||
data.subSupplyType = 1
|
||||
elif doc.gst_category in ["Overseas", "Deemed Export"]:
|
||||
data.subSupplyType = 3
|
||||
|
||||
@@ -23,24 +23,31 @@ def _execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
columns = get_columns()
|
||||
output_gst_accounts = get_output_gst_accounts(filters.company)
|
||||
|
||||
company_currency = erpnext.get_company_currency(filters.company)
|
||||
item_list = get_items(filters)
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
itemised_tax, tax_columns = get_tax_accounts(
|
||||
item_list, columns, company_currency, output_gst_accounts
|
||||
)
|
||||
|
||||
data = []
|
||||
added_item = []
|
||||
for d in item_list:
|
||||
if (d.parent, d.item_code) not in added_item:
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate]
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty]
|
||||
total_tax = 0
|
||||
tax_rate = 0
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||
if item_tax.get("is_gst_tax"):
|
||||
tax_rate += flt(item_tax.get("tax_rate", 0))
|
||||
total_tax += flt(item_tax.get("tax_amount", 0))
|
||||
|
||||
row += [d.base_net_amount + total_tax]
|
||||
row += [d.base_net_amount]
|
||||
row += [tax_rate]
|
||||
row += [d.taxable_value + total_tax]
|
||||
row += [d.taxable_value]
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||
row += [item_tax.get("tax_amount", 0)]
|
||||
@@ -51,6 +58,40 @@ def _execute(filters=None):
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_output_gst_accounts(company):
|
||||
|
||||
accounts = frappe.qb.DocType("Account")
|
||||
gst_accounts = frappe.qb.DocType("GST Account")
|
||||
|
||||
accounts_query = (
|
||||
frappe.qb.from_(accounts)
|
||||
.select(accounts.name)
|
||||
.where((accounts.account_type == "Tax") & (accounts.root_type == "Liability"))
|
||||
)
|
||||
|
||||
gst_accounts_query = (
|
||||
frappe.qb.from_(gst_accounts)
|
||||
.select(
|
||||
gst_accounts.cgst_account,
|
||||
gst_accounts.sgst_account,
|
||||
gst_accounts.igst_account,
|
||||
gst_accounts.utgst_account,
|
||||
gst_accounts.cess_account,
|
||||
)
|
||||
.where((gst_accounts.is_reverse_charge_account == 0) & (gst_accounts.company == company))
|
||||
)
|
||||
|
||||
gst_accounts_list = [
|
||||
account for sublist in gst_accounts_query.run() for account in sublist if account
|
||||
]
|
||||
|
||||
tax_accounts_list = [account[0] for account in accounts_query.run() if account]
|
||||
|
||||
output_tax_list = [account for account in gst_accounts_list if account in tax_accounts_list]
|
||||
|
||||
return output_tax_list
|
||||
|
||||
|
||||
def get_columns():
|
||||
columns = [
|
||||
{
|
||||
@@ -99,34 +140,35 @@ def get_items(filters):
|
||||
match_conditions = " and {0} ".format(match_conditions)
|
||||
|
||||
items = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
f"""
|
||||
SELECT
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.stock_uom,
|
||||
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
|
||||
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
|
||||
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
||||
sum(`tabSales Invoice Item`.stock_qty) AS stock_qty,
|
||||
sum(`tabSales Invoice Item`.taxable_value) AS taxable_value,
|
||||
sum(`tabSales Invoice Item`.base_price_list_rate) AS base_price_list_rate,
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code,
|
||||
`tabGST HSN Code`.description,
|
||||
json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail,
|
||||
concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate
|
||||
from
|
||||
`tabSales Invoice`,
|
||||
`tabSales Invoice Item`,
|
||||
`tabGST HSN Code`,
|
||||
`tabSales Taxes and Charges`
|
||||
where
|
||||
`tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
|
||||
and `tabSales Invoice`.docstatus = 1
|
||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
||||
group by
|
||||
`tabGST HSN Code`.description
|
||||
FROM
|
||||
`tabSales Invoice`
|
||||
INNER JOIN `tabSales Invoice Item` ON `tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
INNER JOIN `tabGST HSN Code` ON `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name
|
||||
WHERE
|
||||
`tabSales Invoice`.docstatus = 1
|
||||
AND `tabSales Invoice Item`.gst_hsn_code IS NOT NULL
|
||||
{conditions}
|
||||
GROUP BY
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code
|
||||
"""
|
||||
% (conditions, match_conditions),
|
||||
`tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.uom
|
||||
ORDER BY
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.uom
|
||||
""".format(
|
||||
conditions=conditions
|
||||
),
|
||||
filters,
|
||||
as_dict=1,
|
||||
)
|
||||
@@ -138,6 +180,7 @@ def get_tax_accounts(
|
||||
item_list,
|
||||
columns,
|
||||
company_currency,
|
||||
output_gst_accounts,
|
||||
doctype="Sales Invoice",
|
||||
tax_doctype="Sales Taxes and Charges",
|
||||
):
|
||||
@@ -177,7 +220,7 @@ def get_tax_accounts(
|
||||
|
||||
for parent, account_head, item_wise_tax_detail, tax_amount in tax_details:
|
||||
|
||||
if account_head not in tax_columns and tax_amount:
|
||||
if account_head in output_gst_accounts and account_head not in tax_columns and tax_amount:
|
||||
# as description is text editor earlier and markup can break the column convention in reports
|
||||
tax_columns.append(account_head)
|
||||
|
||||
@@ -190,29 +233,40 @@ def get_tax_accounts(
|
||||
continue
|
||||
itemised_tax.setdefault(item_code, frappe._dict())
|
||||
if isinstance(tax_data, list):
|
||||
tax_rate = 0
|
||||
is_gst_tax = 0
|
||||
if account_head in output_gst_accounts:
|
||||
is_gst_tax = 1
|
||||
tax_rate = tax_data[0]
|
||||
tax_amount = tax_data[1]
|
||||
else:
|
||||
tax_rate = 0
|
||||
tax_amount = 0
|
||||
|
||||
for d in item_row_map.get(parent, {}).get(item_code, []):
|
||||
item_tax_amount = tax_amount
|
||||
if item_tax_amount:
|
||||
itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict(
|
||||
{"tax_amount": flt(item_tax_amount, tax_amount_precision)}
|
||||
{
|
||||
"tax_rate": flt(tax_rate, 2),
|
||||
"is_gst_tax": is_gst_tax,
|
||||
"tax_amount": flt(item_tax_amount, tax_amount_precision),
|
||||
}
|
||||
)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
tax_columns.sort()
|
||||
for account_head in tax_columns:
|
||||
columns.append(
|
||||
{
|
||||
"label": account_head,
|
||||
"fieldname": frappe.scrub(account_head),
|
||||
"fieldtype": "Float",
|
||||
"width": 110,
|
||||
}
|
||||
)
|
||||
if account_head in output_gst_accounts:
|
||||
columns.append(
|
||||
{
|
||||
"label": account_head,
|
||||
"fieldname": frappe.scrub(account_head),
|
||||
"fieldtype": "Float",
|
||||
"width": 110,
|
||||
}
|
||||
)
|
||||
|
||||
return itemised_tax, tax_columns
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user