mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-07 15:12:51 +00:00
Merge pull request #31647 from frappe/version-13-hotfix
chore: weekly version-13 release
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"book_asset_depreciation_entry_automatically",
|
"book_asset_depreciation_entry_automatically",
|
||||||
"unlink_advance_payment_on_cancelation_of_order",
|
"unlink_advance_payment_on_cancelation_of_order",
|
||||||
"enable_common_party_accounting",
|
"enable_common_party_accounting",
|
||||||
|
"allow_multi_currency_invoices_against_single_party_account",
|
||||||
"post_change_gl_entries",
|
"post_change_gl_entries",
|
||||||
"enable_discount_accounting",
|
"enable_discount_accounting",
|
||||||
"tax_settings_section",
|
"tax_settings_section",
|
||||||
@@ -276,14 +277,21 @@
|
|||||||
"fieldname": "enable_common_party_accounting",
|
"fieldname": "enable_common_party_accounting",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Enable Common Party Accounting"
|
"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",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-11 17:42:36.427699",
|
"modified": "2022-07-11 13:37:50.605141",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class GLEntry(Document):
|
|||||||
self.validate_and_set_fiscal_year()
|
self.validate_and_set_fiscal_year()
|
||||||
self.pl_must_have_cost_center()
|
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.check_mandatory()
|
||||||
self.validate_cost_center()
|
self.validate_cost_center()
|
||||||
self.check_pl_account()
|
self.check_pl_account()
|
||||||
@@ -52,7 +52,7 @@ class GLEntry(Document):
|
|||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
adv_adj = self.flags.adv_adj
|
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_account_details(adv_adj)
|
||||||
self.validate_dimensions_for_pl_and_bs()
|
self.validate_dimensions_for_pl_and_bs()
|
||||||
self.validate_allowed_dimensions()
|
self.validate_allowed_dimensions()
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ from erpnext.accounts.utils import (
|
|||||||
get_stock_and_account_balance,
|
get_stock_and_account_balance,
|
||||||
)
|
)
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
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):
|
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||||
@@ -935,15 +938,12 @@ class JournalEntry(AccountsController):
|
|||||||
def validate_expense_claim(self):
|
def validate_expense_claim(self):
|
||||||
for d in self.accounts:
|
for d in self.accounts:
|
||||||
if d.reference_type == "Expense Claim":
|
if d.reference_type == "Expense Claim":
|
||||||
sanctioned_amount, reimbursed_amount = frappe.db.get_value(
|
outstanding_amt = get_outstanding_amount_for_claim(d.reference_name)
|
||||||
"Expense Claim", d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed")
|
if d.debit > outstanding_amt:
|
||||||
)
|
|
||||||
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
|
|
||||||
if d.debit > pending_amount:
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}"
|
"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, pending_amount)
|
).format(d.idx, d.reference_name, outstanding_amt)
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_credit_debit_note(self):
|
def validate_credit_debit_note(self):
|
||||||
|
|||||||
@@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
|||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
btn: $(btn_primary),
|
btn: $(btn_primary),
|
||||||
method: "make_invoices",
|
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,
|
get_supplier_block_status,
|
||||||
validate_taxes_and_charges,
|
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
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
@@ -1649,12 +1652,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||||
bill_no = ref_doc.get("bill_no")
|
bill_no = ref_doc.get("bill_no")
|
||||||
elif reference_doctype == "Expense Claim":
|
elif reference_doctype == "Expense Claim":
|
||||||
outstanding_amount = (
|
outstanding_amount = get_outstanding_amount_for_claim(ref_doc)
|
||||||
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"))
|
|
||||||
)
|
|
||||||
elif reference_doctype == "Employee Advance":
|
elif reference_doctype == "Employee Advance":
|
||||||
outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
|
outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
|
||||||
if party_account_currency != ref_doc.currency:
|
if party_account_currency != ref_doc.currency:
|
||||||
|
|||||||
@@ -10,10 +10,11 @@
|
|||||||
"fiscal_year",
|
"fiscal_year",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"company",
|
"company",
|
||||||
"cost_center_wise_pnl",
|
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"closing_account_head",
|
"closing_account_head",
|
||||||
"remarks"
|
"remarks",
|
||||||
|
"gle_processing_status",
|
||||||
|
"error_message"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -86,17 +87,26 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"depends_on": "eval:doc.docstatus!=0",
|
||||||
"fieldname": "cost_center_wise_pnl",
|
"fieldname": "gle_processing_status",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Select",
|
||||||
"label": "Book Cost Center Wise Profit/Loss"
|
"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",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-20 15:27:37.210458",
|
"modified": "2022-07-20 14:51:04.714154",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Period Closing Voucher",
|
"name": "Period Closing Voucher",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from frappe.utils import flt
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
get_dimensions,
|
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import get_account_currency
|
from erpnext.accounts.utils import get_account_currency
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
@@ -20,13 +19,28 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
self.validate_posting_date()
|
self.validate_posting_date()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
self.db_set("gle_processing_status", "In Progress")
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
self.db_set("gle_processing_status", "In Progress")
|
||||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
gle_count = frappe.db.count(
|
||||||
|
"GL Entry",
|
||||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
{"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):
|
def validate_account_head(self):
|
||||||
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
|
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):
|
def make_gl_entries(self):
|
||||||
gl_entries = self.get_gl_entries()
|
gl_entries = self.get_gl_entries()
|
||||||
if gl_entries:
|
if gl_entries:
|
||||||
from erpnext.accounts.general_ledger import make_gl_entries
|
if len(gl_entries) > 5000:
|
||||||
|
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
|
||||||
make_gl_entries(gl_entries)
|
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):
|
def get_gl_entries(self):
|
||||||
gl_entries = []
|
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):
|
if flt(acc.bal_in_company_currency):
|
||||||
gl_entries.append(
|
gl_entries.append(self.get_gle_for_pl_account(acc))
|
||||||
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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if gl_entries:
|
# closing liability account
|
||||||
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
|
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
|
||||||
gl_entries += gle_for_net_pl_bal
|
if flt(acc.bal_in_company_currency):
|
||||||
|
gl_entries.append(self.get_gle_for_closing_account(acc))
|
||||||
|
|
||||||
return gl_entries
|
return gl_entries
|
||||||
|
|
||||||
def get_pnl_gl_entry(self, pl_accounts):
|
def get_gle_for_pl_account(self, acc):
|
||||||
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
gl_entry = self.get_gl_dict(
|
||||||
gl_entries = []
|
{
|
||||||
|
"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:
|
def get_gle_for_closing_account(self, acc):
|
||||||
if flt(acc.bal_in_company_currency):
|
gl_entry = self.get_gl_dict(
|
||||||
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": acc.cost_center,
|
||||||
"account": self.closing_account_head,
|
"finance_book": acc.finance_book,
|
||||||
"cost_center": cost_center,
|
"account_currency": acc.account_currency,
|
||||||
"finance_book": acc.finance_book,
|
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||||
"account_currency": acc.account_currency,
|
if flt(acc.bal_in_account_currency) > 0
|
||||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
else 0,
|
||||||
if flt(acc.bal_in_account_currency) > 0
|
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||||
else 0,
|
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||||
"debit": abs(flt(acc.bal_in_company_currency))
|
if flt(acc.bal_in_account_currency) < 0
|
||||||
if flt(acc.bal_in_company_currency) > 0
|
else 0,
|
||||||
else 0,
|
"credit": 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
|
item=acc,
|
||||||
else 0,
|
)
|
||||||
"credit": abs(flt(acc.bal_in_company_currency))
|
self.update_default_dimensions(gl_entry, acc)
|
||||||
if flt(acc.bal_in_company_currency) < 0
|
return gl_entry
|
||||||
else 0,
|
|
||||||
},
|
|
||||||
item=acc,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.update_default_dimensions(gl_entry)
|
def update_default_dimensions(self, gl_entry, acc):
|
||||||
|
|
||||||
gl_entries.append(gl_entry)
|
|
||||||
|
|
||||||
return gl_entries
|
|
||||||
|
|
||||||
def update_default_dimensions(self, gl_entry):
|
|
||||||
if not self.accounting_dimensions:
|
if not self.accounting_dimensions:
|
||||||
self.accounting_dimensions = get_accounting_dimensions()
|
self.accounting_dimensions = get_accounting_dimensions()
|
||||||
|
|
||||||
_, default_dimensions = get_dimensions()
|
|
||||||
for dimension in self.accounting_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"""
|
"""Get balance for dimension-wise pl accounts"""
|
||||||
|
|
||||||
dimension_fields = ["t1.cost_center", "t1.finance_book"]
|
dimension_fields = ["t1.cost_center", "t1.finance_book"]
|
||||||
@@ -159,20 +163,56 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
for dimension in self.accounting_dimensions:
|
for dimension in self.accounting_dimensions:
|
||||||
dimension_fields.append("t1.{0}".format(dimension))
|
dimension_fields.append("t1.{0}".format(dimension))
|
||||||
|
|
||||||
|
if group_by_account:
|
||||||
|
dimension_fields.append("t1.account")
|
||||||
|
|
||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select
|
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_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
|
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||||
from `tabGL Entry` t1, `tabAccount` t2
|
from `tabGL Entry` t1, `tabAccount` t2
|
||||||
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
|
where
|
||||||
and t2.docstatus < 2 and t2.company = %s
|
t1.is_cancelled = 0
|
||||||
and t1.posting_date between %s and %s
|
and t1.account = t2.name
|
||||||
group by t1.account, {dimension_fields}
|
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(
|
""".format(
|
||||||
dimension_fields=", ".join(dimension_fields)
|
dimension_fields=", ".join(dimension_fields)
|
||||||
),
|
),
|
||||||
(self.company, self.get("year_start_date"), self.posting_date),
|
(self.company, self.get("year_start_date"), self.posting_date),
|
||||||
as_dict=1,
|
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 = (
|
expected_gle = (
|
||||||
("Cost of Goods Sold - TPC", 0.0, 600.0),
|
("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),
|
("Sales - TPC", 400.0, 0.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -59,7 +59,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
""",
|
""",
|
||||||
(pcv.name),
|
(pcv.name),
|
||||||
)
|
)
|
||||||
|
pcv.reload()
|
||||||
|
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||||
self.assertEqual(pcv_gle, expected_gle)
|
self.assertEqual(pcv_gle, expected_gle)
|
||||||
|
|
||||||
def test_cost_center_wise_posting(self):
|
def test_cost_center_wise_posting(self):
|
||||||
@@ -94,7 +95,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
pcv = self.make_period_closing_voucher(submit=False)
|
pcv = self.make_period_closing_voucher(submit=False)
|
||||||
pcv.cost_center_wise_pnl = 1
|
|
||||||
pcv.save()
|
pcv.save()
|
||||||
pcv.submit()
|
pcv.submit()
|
||||||
surplus_account = pcv.closing_account_head
|
surplus_account = pcv.closing_account_head
|
||||||
@@ -117,6 +117,16 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(pcv_gle, expected_gle)
|
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):
|
def test_period_closing_with_finance_book_entries(self):
|
||||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||||
|
|
||||||
|
|||||||
@@ -148,6 +148,7 @@ def update_net_values(entry):
|
|||||||
def merge_similar_entries(gl_map, precision=None):
|
def merge_similar_entries(gl_map, precision=None):
|
||||||
merged_gl_map = []
|
merged_gl_map = []
|
||||||
accounting_dimensions = get_accounting_dimensions()
|
accounting_dimensions = get_accounting_dimensions()
|
||||||
|
|
||||||
for entry in gl_map:
|
for entry in gl_map:
|
||||||
# if there is already an entry in this account then just add it
|
# if there is already an entry in this account then just add it
|
||||||
# to that entry
|
# 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.from_repost = from_repost
|
||||||
gle.flags.adv_adj = adv_adj
|
gle.flags.adv_adj = adv_adj
|
||||||
gle.flags.update_outstanding = update_outstanding or "Yes"
|
gle.flags.update_outstanding = update_outstanding or "Yes"
|
||||||
|
gle.flags.notify_update = False
|
||||||
gle.submit()
|
gle.submit()
|
||||||
|
|
||||||
if not from_repost:
|
if not from_repost and gle.voucher_type != "Period Closing Voucher":
|
||||||
validate_expense_against_budget(args)
|
validate_expense_against_budget(args)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ from frappe import _
|
|||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
validate_filters(filters)
|
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)
|
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
|
return columns, res
|
||||||
|
|
||||||
|
|
||||||
@@ -22,10 +22,11 @@ def validate_filters(filters):
|
|||||||
frappe.throw(_("From Date must be before To Date"))
|
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()
|
supplier_map = get_supplier_pan_map()
|
||||||
tax_rate_map = get_tax_rate_map(filters)
|
tax_rate_map = get_tax_rate_map(filters)
|
||||||
gle_map = get_gle_map(tds_docs)
|
gle_map = get_gle_map(tds_docs)
|
||||||
|
print(journal_entry_party_map)
|
||||||
|
|
||||||
out = []
|
out = []
|
||||||
for name, details in gle_map.items():
|
for name, details in gle_map.items():
|
||||||
@@ -38,6 +39,11 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
|||||||
posting_date = entry.posting_date
|
posting_date = entry.posting_date
|
||||||
voucher_type = entry.voucher_type
|
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:
|
if not tax_withholding_category:
|
||||||
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
|
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
|
||||||
rate = tax_rate_map.get(tax_withholding_category)
|
rate = tax_rate_map.get(tax_withholding_category)
|
||||||
@@ -176,6 +182,7 @@ def get_tds_docs(filters):
|
|||||||
journal_entries = []
|
journal_entries = []
|
||||||
tax_category_map = {}
|
tax_category_map = {}
|
||||||
or_filters = {}
|
or_filters = {}
|
||||||
|
journal_entry_party_map = {}
|
||||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||||
|
|
||||||
tds_accounts = frappe.get_all(
|
tds_accounts = frappe.get_all(
|
||||||
@@ -218,9 +225,24 @@ def get_tds_docs(filters):
|
|||||||
get_tax_category_map(payment_entries, "Payment Entry", tax_category_map)
|
get_tax_category_map(payment_entries, "Payment Entry", tax_category_map)
|
||||||
|
|
||||||
if journal_entries:
|
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)
|
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):
|
def get_tax_category_map(vouchers, doctype, tax_category_map):
|
||||||
|
|||||||
@@ -38,7 +38,6 @@
|
|||||||
"purchase_date",
|
"purchase_date",
|
||||||
"section_break_23",
|
"section_break_23",
|
||||||
"calculate_depreciation",
|
"calculate_depreciation",
|
||||||
"allow_monthly_depreciation",
|
|
||||||
"column_break_33",
|
"column_break_33",
|
||||||
"opening_accumulated_depreciation",
|
"opening_accumulated_depreciation",
|
||||||
"number_of_depreciations_booked",
|
"number_of_depreciations_booked",
|
||||||
@@ -454,13 +453,6 @@
|
|||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "calculate_depreciation",
|
|
||||||
"fieldname": "allow_monthly_depreciation",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Allow Monthly Depreciation"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"collapsible_depends_on": "is_existing_asset",
|
"collapsible_depends_on": "is_existing_asset",
|
||||||
@@ -503,7 +495,7 @@
|
|||||||
"link_fieldname": "asset"
|
"link_fieldname": "asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2022-01-30 20:19:24.680027",
|
"modified": "2022-07-20 16:22:44.437579",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
|
|||||||
@@ -353,61 +353,16 @@ class Asset(AccountsController):
|
|||||||
skip_row = True
|
skip_row = True
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
if depreciation_amount > 0:
|
||||||
# With monthly depreciation, each depreciation is divided by months remaining until next date
|
self.append(
|
||||||
if self.allow_monthly_depreciation:
|
"schedules",
|
||||||
# month range is 1 to 12
|
{
|
||||||
# In pro rata case, for first and last depreciation, month range would be different
|
"schedule_date": schedule_date,
|
||||||
if (has_pro_rata and n == 0 and not self.number_of_depreciations_booked) or (
|
"depreciation_amount": depreciation_amount,
|
||||||
has_pro_rata and n == cint(number_of_pending_depreciations) - 1
|
"depreciation_method": finance_book.depreciation_method,
|
||||||
):
|
"finance_book": finance_book.finance_book,
|
||||||
month_range = months
|
"finance_book_id": finance_book.idx,
|
||||||
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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||||
# JE: Journal Entry, FB: Finance Book
|
# 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))
|
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):
|
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
|
||||||
days = date_diff(to_date, from_date)
|
days = date_diff(to_date, from_date)
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ class TestAsset(AssetSetup):
|
|||||||
def test_is_fixed_asset_set(self):
|
def test_is_fixed_asset_set(self):
|
||||||
asset = create_asset(is_existing_asset=1)
|
asset = create_asset(is_existing_asset=1)
|
||||||
doc = frappe.new_doc("Purchase Invoice")
|
doc = frappe.new_doc("Purchase Invoice")
|
||||||
|
doc.company = "_Test Company"
|
||||||
doc.supplier = "_Test Supplier"
|
doc.supplier = "_Test Supplier"
|
||||||
doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name})
|
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)
|
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||||
|
|
||||||
def test_monthly_depreciation_by_wdv_method(self):
|
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(
|
asset = create_asset(
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
available_for_use_date="2022-02-15",
|
available_for_use_date="2022-02-15",
|
||||||
@@ -715,12 +718,12 @@ class TestDepreciationMethods(AssetSetup):
|
|||||||
)
|
)
|
||||||
|
|
||||||
expected_schedules = [
|
expected_schedules = [
|
||||||
["2022-02-28", 645.0, 645.0],
|
["2022-02-28", 647.25, 647.25],
|
||||||
["2022-03-31", 1206.8, 1851.8],
|
["2022-03-31", 1210.71, 1857.96],
|
||||||
["2022-04-30", 1051.12, 2902.92],
|
["2022-04-30", 1053.99, 2911.95],
|
||||||
["2022-05-31", 915.52, 3818.44],
|
["2022-05-31", 917.55, 3829.5],
|
||||||
["2022-06-30", 797.42, 4615.86],
|
["2022-06-30", 798.77, 4628.27],
|
||||||
["2022-07-15", 384.14, 5000.0],
|
["2022-07-15", 371.73, 5000.0],
|
||||||
]
|
]
|
||||||
|
|
||||||
schedules = [
|
schedules = [
|
||||||
@@ -731,8 +734,8 @@ class TestDepreciationMethods(AssetSetup):
|
|||||||
]
|
]
|
||||||
for d in asset.get("schedules")
|
for d in asset.get("schedules")
|
||||||
]
|
]
|
||||||
|
|
||||||
self.assertEqual(schedules, expected_schedules)
|
self.assertEqual(schedules, expected_schedules)
|
||||||
|
frappe.db.set_default("float_precision", existing_precision)
|
||||||
|
|
||||||
|
|
||||||
class TestDepreciationBasics(AssetSetup):
|
class TestDepreciationBasics(AssetSetup):
|
||||||
|
|||||||
@@ -1470,8 +1470,15 @@ class AccountsController(TransactionBase):
|
|||||||
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
|
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
|
||||||
)
|
)
|
||||||
party_account_currency = get_account_currency(party_account)
|
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(
|
frappe.throw(
|
||||||
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
||||||
frappe.bold(party_account), party_account_currency, self.currency
|
frappe.bold(party_account), party_account_currency, self.currency
|
||||||
|
|||||||
@@ -355,6 +355,8 @@ class Subcontracting:
|
|||||||
rm_obj.purchase_order = item_row.purchase_order
|
rm_obj.purchase_order = item_row.purchase_order
|
||||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
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):
|
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)
|
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
|
||||||
|
|
||||||
|
|||||||
@@ -483,12 +483,12 @@ scheduler_events = {
|
|||||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
"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.support.doctype.issue.issue.set_service_level_agreement_variance",
|
||||||
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
|
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
|
||||||
],
|
],
|
||||||
"hourly_long": [
|
"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": [
|
"daily": [
|
||||||
"erpnext.support.doctype.issue.issue.auto_close_tickets",
|
"erpnext.support.doctype.issue.issue.auto_close_tickets",
|
||||||
|
|||||||
@@ -813,7 +813,7 @@
|
|||||||
"idx": 24,
|
"idx": 24,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-17 11:31:37.730760",
|
"modified": "2022-07-18 20:03:43.188705",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee",
|
"name": "Employee",
|
||||||
|
|||||||
@@ -349,7 +349,9 @@ def get_employee_email(employee_doc):
|
|||||||
|
|
||||||
def get_holiday_list_for_employee(employee, raise_exception=True):
|
def get_holiday_list_for_employee(employee, raise_exception=True):
|
||||||
if employee:
|
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:
|
else:
|
||||||
holiday_list = ""
|
holiday_list = ""
|
||||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Employee",
|
"label": "Employee",
|
||||||
"options": "Employee",
|
"options": "Employee",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "employee.employee_name",
|
"fetch_from": "employee.employee_name",
|
||||||
@@ -48,7 +49,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Shift",
|
"label": "Shift",
|
||||||
"options": "Shift Type",
|
"options": "Shift Type",
|
||||||
"read_only": 1
|
"read_only": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
@@ -107,7 +109,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-07-08 11:02:32.660986",
|
"modified": "2022-07-19 15:38:41.767539",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee Checkin",
|
"name": "Employee Checkin",
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ def mark_attendance_and_link_log(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
elif attendance_status in ("Present", "Absent", "Half Day"):
|
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(
|
duplicate = frappe.db.exists(
|
||||||
"Attendance",
|
"Attendance",
|
||||||
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
|
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
|
||||||
@@ -144,7 +144,7 @@ def mark_attendance_and_link_log(
|
|||||||
"attendance_date": attendance_date,
|
"attendance_date": attendance_date,
|
||||||
"status": attendance_status,
|
"status": attendance_status,
|
||||||
"working_hours": working_hours,
|
"working_hours": working_hours,
|
||||||
"company": employee_doc.company,
|
"company": company,
|
||||||
"shift": shift,
|
"shift": shift,
|
||||||
"late_entry": late_entry,
|
"late_entry": late_entry,
|
||||||
"early_exit": early_exit,
|
"early_exit": early_exit,
|
||||||
|
|||||||
@@ -339,6 +339,30 @@ def update_reimbursed_amount(doc, amount):
|
|||||||
frappe.db.set_value("Expense Claim", doc.name, "status", doc.status)
|
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()
|
@frappe.whitelist()
|
||||||
def make_bank_entry(dt, dn):
|
def make_bank_entry(dt, dn):
|
||||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
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:
|
if not default_bank_cash_account:
|
||||||
default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash")
|
default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash")
|
||||||
|
|
||||||
payable_amount = (
|
payable_amount = get_outstanding_amount_for_claim(expense_claim)
|
||||||
flt(expense_claim.total_sanctioned_amount)
|
|
||||||
- flt(expense_claim.total_amount_reimbursed)
|
|
||||||
- flt(expense_claim.total_advance_amount)
|
|
||||||
)
|
|
||||||
|
|
||||||
je = frappe.new_doc("Journal Entry")
|
je = frappe.new_doc("Journal Entry")
|
||||||
je.voucher_type = "Bank Entry"
|
je.voucher_type = "Bank Entry"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import flt, nowdate, random_string
|
from frappe.utils import flt, nowdate, random_string
|
||||||
|
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
@@ -14,9 +15,18 @@ test_dependencies = ["Employee"]
|
|||||||
company_name = "_Test Company 3"
|
company_name = "_Test Company 3"
|
||||||
|
|
||||||
|
|
||||||
class TestExpenseClaim(unittest.TestCase):
|
class TestExpenseClaim(FrappeTestCase):
|
||||||
def tearDown(self):
|
def setUp(self):
|
||||||
frappe.db.rollback()
|
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):
|
def test_total_expense_claim_for_project(self):
|
||||||
frappe.db.sql("""delete from `tabTask`""")
|
frappe.db.sql("""delete from `tabTask`""")
|
||||||
@@ -58,12 +68,7 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
payable_account, 300, 200, company_name, "Travel Expenses - _TC3"
|
payable_account, 300, 200, company_name, "Travel Expenses - _TC3"
|
||||||
)
|
)
|
||||||
|
|
||||||
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
|
je = make_journal_entry(expense_claim)
|
||||||
je = frappe.get_doc(je_dict)
|
|
||||||
je.posting_date = nowdate()
|
|
||||||
je.cheque_no = random_string(5)
|
|
||||||
je.cheque_date = nowdate()
|
|
||||||
je.submit()
|
|
||||||
|
|
||||||
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
|
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
|
||||||
self.assertEqual(expense_claim.status, "Paid")
|
self.assertEqual(expense_claim.status, "Paid")
|
||||||
@@ -272,6 +277,24 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
self.assertEqual(outstanding_amount, 0)
|
self.assertEqual(outstanding_amount, 0)
|
||||||
self.assertEqual(total_amount_reimbursed, 5500)
|
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):
|
def get_payable_account(company):
|
||||||
return frappe.get_cached_value("Company", company, "default_payable_account")
|
return frappe.get_cached_value("Company", company, "default_payable_account")
|
||||||
@@ -370,3 +393,14 @@ def make_payment_entry(expense_claim, payable_account, amt):
|
|||||||
pe.references[0].allocated_amount = amt
|
pe.references[0].allocated_amount = amt
|
||||||
pe.insert()
|
pe.insert()
|
||||||
pe.submit()
|
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:
|
if date is None:
|
||||||
date = today()
|
date = today()
|
||||||
if holiday_list:
|
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:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -25,7 +25,8 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Employee",
|
"label": "Employee",
|
||||||
"options": "Employee",
|
"options": "Employee",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "employee.employee_name",
|
"fetch_from": "employee.employee_name",
|
||||||
@@ -48,7 +49,8 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Shift Type",
|
"label": "Shift Type",
|
||||||
"options": "Shift Type",
|
"options": "Shift Type",
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
@@ -105,7 +107,7 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-15 14:27:54.310773",
|
"modified": "2022-07-19 15:27:54.310773",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Shift Assignment",
|
"name": "Shift Assignment",
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ def get_employee_shift(
|
|||||||
"""
|
"""
|
||||||
if for_date is None:
|
if for_date is None:
|
||||||
for_date = nowdate()
|
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_type_name = None
|
||||||
shift_assignment_details = frappe.db.get_value(
|
shift_assignment_details = frappe.db.get_value(
|
||||||
"Shift Assignment",
|
"Shift Assignment",
|
||||||
@@ -187,7 +187,7 @@ def get_employee_shift(
|
|||||||
if not shift_type_name and consider_default_shift:
|
if not shift_type_name and consider_default_shift:
|
||||||
shift_type_name = default_shift
|
shift_type_name = default_shift
|
||||||
if shift_type_name:
|
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:
|
if not holiday_list_name:
|
||||||
holiday_list_name = get_holiday_list_for_employee(employee, False)
|
holiday_list_name = get_holiday_list_for_employee(employee, False)
|
||||||
if holiday_list_name and is_holiday(holiday_list_name, for_date):
|
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
|
return None
|
||||||
if not for_date:
|
if not for_date:
|
||||||
for_date = nowdate()
|
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
|
start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
|
||||||
for_date = (
|
for_date = (
|
||||||
for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else 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.
|
"""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.
|
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"]
|
"Employee", employee, ["date_of_joining", "relieving_date", "creation"]
|
||||||
)
|
)
|
||||||
if not date_of_joining:
|
if not date_of_joining:
|
||||||
@@ -156,21 +156,19 @@ class ShiftType(Document):
|
|||||||
if not from_date:
|
if not from_date:
|
||||||
del filters["start_date"]
|
del filters["start_date"]
|
||||||
|
|
||||||
assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True)
|
assigned_employees = frappe.get_all("Shift Assignment", filters, pluck="employee")
|
||||||
assigned_employees = [x[0] for x in assigned_employees]
|
|
||||||
|
|
||||||
if consider_default_shift:
|
if consider_default_shift:
|
||||||
filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
|
filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
|
||||||
default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True)
|
default_shift_employees = frappe.get_all("Employee", filters, pluck="name")
|
||||||
default_shift_employees = [x[0] for x in default_shift_employees]
|
|
||||||
return list(set(assigned_employees + default_shift_employees))
|
return list(set(assigned_employees + default_shift_employees))
|
||||||
return assigned_employees
|
return assigned_employees
|
||||||
|
|
||||||
|
|
||||||
def process_auto_attendance_for_all_shifts():
|
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:
|
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()
|
doc.process_auto_attendance()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,10 @@
|
|||||||
"total_payment",
|
"total_payment",
|
||||||
"total_principal_paid",
|
"total_principal_paid",
|
||||||
"written_off_amount",
|
"written_off_amount",
|
||||||
|
"refund_amount",
|
||||||
|
"debit_adjustment_amount",
|
||||||
|
"credit_adjustment_amount",
|
||||||
|
"is_npa",
|
||||||
"column_break_19",
|
"column_break_19",
|
||||||
"total_interest_payable",
|
"total_interest_payable",
|
||||||
"total_amount_paid",
|
"total_amount_paid",
|
||||||
@@ -379,12 +383,39 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "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,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-03-10 11:50:31.957360",
|
"modified": "2022-06-30 12:04:13.728880",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan",
|
"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
|
||||||
@@ -35,12 +35,15 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "loan",
|
"fieldname": "loan",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Loan",
|
"label": "Loan",
|
||||||
"options": "Loan"
|
"options": "Loan"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "posting_date",
|
"fieldname": "posting_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Posting Date"
|
"label": "Posting Date"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -75,6 +78,8 @@
|
|||||||
"fetch_from": "loan.applicant",
|
"fetch_from": "loan.applicant",
|
||||||
"fieldname": "applicant",
|
"fieldname": "applicant",
|
||||||
"fieldtype": "Dynamic Link",
|
"fieldtype": "Dynamic Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Applicant",
|
"label": "Applicant",
|
||||||
"options": "applicant_type"
|
"options": "applicant_type"
|
||||||
},
|
},
|
||||||
@@ -158,8 +163,11 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "accrual_type",
|
"fieldname": "accrual_type",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
|
"in_filter": 1,
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Accrual Type",
|
"label": "Accrual Type",
|
||||||
"options": "Regular\nRepayment\nDisbursement"
|
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "penalty_amount",
|
"fieldname": "penalty_amount",
|
||||||
@@ -185,10 +193,11 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-19 18:26:38.871889",
|
"modified": "2022-06-30 11:51:31.911794",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan Interest Accrual",
|
"name": "Loan Interest Accrual",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -225,5 +234,6 @@
|
|||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Loan 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
|
||||||
@@ -387,15 +387,19 @@ class LoanRepayment(AccountsController):
|
|||||||
|
|
||||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||||
gle_map = []
|
gle_map = []
|
||||||
|
|
||||||
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
|
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
|
self.shortfall_amount, self.against_loan
|
||||||
)
|
)
|
||||||
elif self.shortfall_amount:
|
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:
|
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:
|
if self.repay_from_salary:
|
||||||
payment_account = self.payroll_payable_account
|
payment_account = self.payroll_payable_account
|
||||||
@@ -446,7 +450,7 @@ class LoanRepayment(AccountsController):
|
|||||||
"debit_in_account_currency": self.amount_paid,
|
"debit_in_account_currency": self.amount_paid,
|
||||||
"against_voucher_type": "Loan",
|
"against_voucher_type": "Loan",
|
||||||
"against_voucher": self.against_loan,
|
"against_voucher": self.against_loan,
|
||||||
"remarks": remarks,
|
"remarks": _(remarks),
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"posting_date": getdate(self.posting_date),
|
"posting_date": getdate(self.posting_date),
|
||||||
}
|
}
|
||||||
@@ -464,7 +468,7 @@ class LoanRepayment(AccountsController):
|
|||||||
"credit_in_account_currency": self.amount_paid,
|
"credit_in_account_currency": self.amount_paid,
|
||||||
"against_voucher_type": "Loan",
|
"against_voucher_type": "Loan",
|
||||||
"against_voucher": self.against_loan,
|
"against_voucher": self.against_loan,
|
||||||
"remarks": remarks,
|
"remarks": _(remarks),
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"posting_date": getdate(self.posting_date),
|
"posting_date": getdate(self.posting_date),
|
||||||
}
|
}
|
||||||
@@ -624,16 +628,22 @@ def get_pending_principal_amount(loan):
|
|||||||
if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
|
if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
|
||||||
pending_principal_amount = (
|
pending_principal_amount = (
|
||||||
flt(loan.total_payment)
|
flt(loan.total_payment)
|
||||||
|
+ flt(loan.debit_adjustment_amount)
|
||||||
|
- flt(loan.credit_adjustment_amount)
|
||||||
- flt(loan.total_principal_paid)
|
- flt(loan.total_principal_paid)
|
||||||
- flt(loan.total_interest_payable)
|
- flt(loan.total_interest_payable)
|
||||||
- flt(loan.written_off_amount)
|
- flt(loan.written_off_amount)
|
||||||
|
+ flt(loan.refund_amount)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
pending_principal_amount = (
|
pending_principal_amount = (
|
||||||
flt(loan.disbursed_amount)
|
flt(loan.disbursed_amount)
|
||||||
|
+ flt(loan.debit_adjustment_amount)
|
||||||
|
- flt(loan.credit_adjustment_amount)
|
||||||
- flt(loan.total_principal_paid)
|
- flt(loan.total_principal_paid)
|
||||||
- flt(loan.total_interest_payable)
|
- flt(loan.total_interest_payable)
|
||||||
- flt(loan.written_off_amount)
|
- flt(loan.written_off_amount)
|
||||||
|
+ flt(loan.refund_amount)
|
||||||
)
|
)
|
||||||
|
|
||||||
return pending_principal_amount
|
return pending_principal_amount
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"loan",
|
"loan",
|
||||||
|
"applicant_type",
|
||||||
|
"applicant",
|
||||||
"status",
|
"status",
|
||||||
"column_break_3",
|
"column_break_3",
|
||||||
"shortfall_time",
|
"shortfall_time",
|
||||||
@@ -23,6 +25,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "loan",
|
"fieldname": "loan",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"in_standard_filter": 1,
|
||||||
"label": "Loan ",
|
"label": "Loan ",
|
||||||
"options": "Loan",
|
"options": "Loan",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
@@ -91,17 +95,35 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "shortfall_percentage",
|
"fieldname": "shortfall_percentage",
|
||||||
"fieldtype": "Percent",
|
"fieldtype": "Percent",
|
||||||
|
"in_list_view": 1,
|
||||||
"label": "Shortfall Percentage",
|
"label": "Shortfall Percentage",
|
||||||
"read_only": 1
|
"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,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-01 08:13:43.263772",
|
"modified": "2022-06-30 11:57:09.378089",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Loan Security Shortfall",
|
"name": "Loan Security Shortfall",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -132,5 +154,6 @@
|
|||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -54,17 +54,18 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Accrual Type",
|
"label": "Accrual Type",
|
||||||
"options": "Regular\nRepayment\nDisbursement",
|
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-06 13:28:51.478909",
|
"modified": "2022-06-29 11:19:33.203088",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Loan Management",
|
"module": "Loan Management",
|
||||||
"name": "Process Loan Interest Accrual",
|
"name": "Process Loan Interest Accrual",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@@ -98,5 +99,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -444,6 +444,7 @@ class BOM(WebsiteGenerator):
|
|||||||
and self.is_active
|
and self.is_active
|
||||||
):
|
):
|
||||||
frappe.db.set(self, "is_default", 1)
|
frappe.db.set(self, "is_default", 1)
|
||||||
|
frappe.db.set_value("Item", self.item, "default_bom", self.name)
|
||||||
else:
|
else:
|
||||||
frappe.db.set(self, "is_default", 0)
|
frappe.db.set(self, "is_default", 0)
|
||||||
item = frappe.get_doc("Item", self.item)
|
item = frappe.get_doc("Item", self.item)
|
||||||
|
|||||||
@@ -519,6 +519,42 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
new_bom.delete()
|
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"):
|
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})
|
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||||
|
|||||||
@@ -369,3 +369,4 @@ erpnext.patches.v13_0.update_employee_advance_status
|
|||||||
erpnext.patches.v13_0.job_card_status_on_hold
|
erpnext.patches.v13_0.job_card_status_on_hold
|
||||||
erpnext.patches.v13_0.add_cost_center_in_loans
|
erpnext.patches.v13_0.add_cost_center_in_loans
|
||||||
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
|
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
|
||||||
|
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
|
||||||
|
|||||||
@@ -33,7 +33,10 @@ def execute():
|
|||||||
"insert_after": insert_after_field,
|
"insert_after": insert_after_field,
|
||||||
}
|
}
|
||||||
|
|
||||||
create_custom_field(doctype, df, ignore_validate=True)
|
try:
|
||||||
frappe.clear_cache(doctype=doctype)
|
create_custom_field(doctype, df, ignore_validate=True)
|
||||||
|
frappe.clear_cache(doctype=doctype)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
count += 1
|
count += 1
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -144,7 +144,7 @@ class GSTR3BReport(Document):
|
|||||||
def get_inward_nil_exempt(self, state):
|
def get_inward_nil_exempt(self, state):
|
||||||
inward_nil_exempt = frappe.db.sql(
|
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
|
i.base_amount, i.is_nil_exempt, i.is_non_gst
|
||||||
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||||
WHERE p.docstatus = 1 and p.name = i.parent
|
WHERE p.docstatus = 1 and p.name = i.parent
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ def validate_eligibility(doc):
|
|||||||
"SEZ",
|
"SEZ",
|
||||||
"Overseas",
|
"Overseas",
|
||||||
"Deemed Export",
|
"Deemed Export",
|
||||||
|
"UIN Holders",
|
||||||
]
|
]
|
||||||
company_transaction = doc.get("billing_address_gstin") == doc.get("company_gstin")
|
company_transaction = doc.get("billing_address_gstin") == doc.get("company_gstin")
|
||||||
|
|
||||||
@@ -130,9 +131,7 @@ def read_json(name):
|
|||||||
|
|
||||||
def get_transaction_details(invoice):
|
def get_transaction_details(invoice):
|
||||||
supply_type = ""
|
supply_type = ""
|
||||||
if (
|
if invoice.gst_category in ("Registered Regular", "Registered Composition", "UIN Holders"):
|
||||||
invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition"
|
|
||||||
):
|
|
||||||
supply_type = "B2B"
|
supply_type = "B2B"
|
||||||
elif invoice.gst_category == "SEZ":
|
elif invoice.gst_category == "SEZ":
|
||||||
if invoice.export_type == "Without Payment of Tax":
|
if invoice.export_type == "Without Payment of Tax":
|
||||||
@@ -148,15 +147,18 @@ def get_transaction_details(invoice):
|
|||||||
supply_type = "DEXP"
|
supply_type = "DEXP"
|
||||||
|
|
||||||
if not supply_type:
|
if not supply_type:
|
||||||
rr, rc, sez, overseas, export = (
|
rr, rc, sez, overseas, export, uin = (
|
||||||
bold("Registered Regular"),
|
bold("Registered Regular"),
|
||||||
bold("Registered Composition"),
|
bold("Registered Composition"),
|
||||||
bold("SEZ"),
|
bold("SEZ"),
|
||||||
bold("Overseas"),
|
bold("Overseas"),
|
||||||
bold("Deemed Export"),
|
bold("Deemed Export"),
|
||||||
|
bold("UIN Holders"),
|
||||||
)
|
)
|
||||||
frappe.throw(
|
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"),
|
title=_("Invalid Supply Type"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -272,45 +274,43 @@ def get_item_list(invoice):
|
|||||||
item.description = sanitize_for_json(d.item_name)
|
item.description = sanitize_for_json(d.item_name)
|
||||||
|
|
||||||
item.qty = abs(item.qty)
|
item.qty = abs(item.qty)
|
||||||
|
item_qty = item.qty
|
||||||
|
|
||||||
|
item.discount_amount = abs(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
|
||||||
|
|
||||||
hide_discount_in_einvoice = cint(
|
hide_discount_in_einvoice = cint(
|
||||||
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
|
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
|
||||||
)
|
)
|
||||||
|
|
||||||
if hide_discount_in_einvoice:
|
if hide_discount_in_einvoice:
|
||||||
if flt(item.qty) != 0.0:
|
item.unit_rate = item.taxable_value / item_qty
|
||||||
item.unit_rate = abs(item.taxable_value / item.qty)
|
item.gross_amount = item.taxable_value
|
||||||
else:
|
|
||||||
item.unit_rate = abs(item.taxable_value)
|
|
||||||
item.gross_amount = abs(item.taxable_value)
|
|
||||||
item.taxable_value = abs(item.taxable_value)
|
|
||||||
item.discount_amount = 0
|
item.discount_amount = 0
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if invoice.get("apply_discount_on") and (abs(invoice.get("base_discount_amount") or 0.0) > 0.0):
|
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.
|
# TODO: need to handle case when tax included in basic rate is checked.
|
||||||
item.discount_amount = (item.discount_amount * item.qty) + (
|
item.discount_amount = (item.discount_amount * item_qty) + (
|
||||||
abs(item.base_amount) - abs(item.base_net_amount)
|
abs(item.base_amount) - abs(item.base_net_amount)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
item.discount_amount = item.discount_amount * item.qty
|
item.discount_amount = item.discount_amount * item_qty
|
||||||
|
|
||||||
if invoice.get("is_return") or invoice.get("is_debit_note"):
|
try:
|
||||||
item.unit_rate = (abs(item.taxable_value) + item.discount_amount) / (
|
item.unit_rate = (item.taxable_value + item.discount_amount) / item_qty
|
||||||
1 if (item.qty == 0) else 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."),
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
try:
|
|
||||||
item.unit_rate = abs(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 = abs(item.taxable_value) + item.discount_amount
|
item.gross_amount = item.taxable_value + item.discount_amount
|
||||||
item.taxable_value = abs(item.taxable_value)
|
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.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N"
|
||||||
item.serial_no = ""
|
item.serial_no = ""
|
||||||
|
|
||||||
|
|||||||
@@ -714,7 +714,7 @@ def get_custom_fields():
|
|||||||
insert_after="customer",
|
insert_after="customer",
|
||||||
no_copy=1,
|
no_copy=1,
|
||||||
print_hide=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(
|
dict(
|
||||||
fieldname="irn_cancelled",
|
fieldname="irn_cancelled",
|
||||||
|
|||||||
@@ -40,13 +40,12 @@ def validate_gstin_for_india(doc, method):
|
|||||||
|
|
||||||
gst_category = []
|
gst_category = []
|
||||||
|
|
||||||
if hasattr(doc, "gst_category"):
|
if len(doc.links):
|
||||||
if len(doc.links):
|
link_doctype = doc.links[0].get("link_doctype")
|
||||||
link_doctype = doc.links[0].get("link_doctype")
|
link_name = doc.links[0].get("link_name")
|
||||||
link_name = doc.links[0].get("link_name")
|
|
||||||
|
|
||||||
if link_doctype in ["Customer", "Supplier"]:
|
if link_doctype in ["Customer", "Supplier"]:
|
||||||
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
|
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
|
||||||
|
|
||||||
doc.gstin = doc.gstin.upper().strip()
|
doc.gstin = doc.gstin.upper().strip()
|
||||||
if not doc.gstin or doc.gstin == "NA":
|
if not doc.gstin or doc.gstin == "NA":
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_to_date, flt, now
|
from frappe.utils import add_days, add_to_date, flt, now
|
||||||
|
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
@@ -120,6 +120,61 @@ class TestLandedCostVoucher(FrappeTestCase):
|
|||||||
expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}"
|
expected_values[gle.account][1], gle.credit, msg=f"incorrect credit for {gle.account}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_landed_cost_voucher_stock_impact(self):
|
||||||
|
"Test impact of LCV on future stock balances."
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
|
||||||
|
item = make_item("LCV Stock Item", {"is_stock_item": 1})
|
||||||
|
warehouse = "Stores - _TC"
|
||||||
|
|
||||||
|
pr1 = make_purchase_receipt(
|
||||||
|
item_code=item.name,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=500,
|
||||||
|
rate=80,
|
||||||
|
posting_date=add_days(frappe.utils.nowdate(), -2),
|
||||||
|
)
|
||||||
|
pr2 = make_purchase_receipt(
|
||||||
|
item_code=item.name,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=100,
|
||||||
|
rate=80,
|
||||||
|
posting_date=frappe.utils.nowdate(),
|
||||||
|
)
|
||||||
|
|
||||||
|
last_sle = frappe.db.get_value( # SLE of second PR
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{
|
||||||
|
"voucher_type": pr2.doctype,
|
||||||
|
"voucher_no": pr2.name,
|
||||||
|
"item_code": item.name,
|
||||||
|
"warehouse": warehouse,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
},
|
||||||
|
fieldname=["qty_after_transaction", "stock_value"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_landed_cost_voucher("Purchase Receipt", pr1.name, pr1.company)
|
||||||
|
|
||||||
|
last_sle_after_landed_cost = frappe.db.get_value( # SLE of second PR after LCV's effect
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
{
|
||||||
|
"voucher_type": pr2.doctype,
|
||||||
|
"voucher_no": pr2.name,
|
||||||
|
"item_code": item.name,
|
||||||
|
"warehouse": warehouse,
|
||||||
|
"is_cancelled": 0,
|
||||||
|
},
|
||||||
|
fieldname=["qty_after_transaction", "stock_value"],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
last_sle.qty_after_transaction, last_sle_after_landed_cost.qty_after_transaction
|
||||||
|
)
|
||||||
|
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
|
||||||
|
|
||||||
def test_landed_cost_voucher_against_purchase_invoice(self):
|
def test_landed_cost_voucher_against_purchase_invoice(self):
|
||||||
|
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
@@ -219,11 +274,11 @@ class TestLandedCostVoucher(FrappeTestCase):
|
|||||||
landed costs, this should be allowed for serial nos too.
|
landed costs, this should be allowed for serial nos too.
|
||||||
|
|
||||||
Case:
|
Case:
|
||||||
- receipt a serial no @ X rate
|
- receipt a serial no @ X rate
|
||||||
- delivery the serial no @ X rate
|
- delivery the serial no @ X rate
|
||||||
- add LCV to receipt X + Y
|
- add LCV to receipt X + Y
|
||||||
- LCV should be successful
|
- LCV should be successful
|
||||||
- delivery should reflect X+Y valuation.
|
- delivery should reflect X+Y valuation.
|
||||||
"""
|
"""
|
||||||
serial_no = "LCV_TEST_SR_NO"
|
serial_no = "LCV_TEST_SR_NO"
|
||||||
item_code = "_Test Serialized Item"
|
item_code = "_Test Serialized Item"
|
||||||
|
|||||||
@@ -94,27 +94,26 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
|
|||||||
if not args.get("posting_date"):
|
if not args.get("posting_date"):
|
||||||
args["posting_date"] = nowdate()
|
args["posting_date"] = nowdate()
|
||||||
|
|
||||||
if args.get("is_cancelled") and via_landed_cost_voucher:
|
if not (args.get("is_cancelled") and via_landed_cost_voucher):
|
||||||
return
|
# Reposts only current voucher SL Entries
|
||||||
|
# Updates valuation rate, stock value, stock queue for current transaction
|
||||||
# Reposts only current voucher SL Entries
|
update_entries_after(
|
||||||
# Updates valuation rate, stock value, stock queue for current transaction
|
{
|
||||||
update_entries_after(
|
"item_code": args.get("item_code"),
|
||||||
{
|
"warehouse": args.get("warehouse"),
|
||||||
"item_code": args.get("item_code"),
|
"posting_date": args.get("posting_date"),
|
||||||
"warehouse": args.get("warehouse"),
|
"posting_time": args.get("posting_time"),
|
||||||
"posting_date": args.get("posting_date"),
|
"voucher_type": args.get("voucher_type"),
|
||||||
"posting_time": args.get("posting_time"),
|
"voucher_no": args.get("voucher_no"),
|
||||||
"voucher_type": args.get("voucher_type"),
|
"sle_id": args.get("name"),
|
||||||
"voucher_no": args.get("voucher_no"),
|
"creation": args.get("creation"),
|
||||||
"sle_id": args.get("name"),
|
},
|
||||||
"creation": args.get("creation"),
|
allow_negative_stock=allow_negative_stock,
|
||||||
},
|
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||||
allow_negative_stock=allow_negative_stock,
|
)
|
||||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
|
||||||
)
|
|
||||||
|
|
||||||
# update qty in future sle and Validate negative qty
|
# update qty in future sle and Validate negative qty
|
||||||
|
# For LCV: update future balances with -ve LCV SLE, which will be balanced by +ve LCV SLE
|
||||||
update_qty_in_future_sle(args, allow_negative_stock)
|
update_qty_in_future_sle(args, allow_negative_stock)
|
||||||
|
|
||||||
|
|
||||||
@@ -208,8 +207,14 @@ def repost_future_sle(
|
|||||||
allow_negative_stock=None,
|
allow_negative_stock=None,
|
||||||
via_landed_cost_voucher=False,
|
via_landed_cost_voucher=False,
|
||||||
):
|
):
|
||||||
if not args and voucher_type and voucher_no:
|
if not args:
|
||||||
args = get_items_to_be_repost(voucher_type, voucher_no, doc)
|
args = [] # set args to empty list if None to avoid enumerate error
|
||||||
|
|
||||||
|
items_to_be_repost = get_items_to_be_repost(
|
||||||
|
voucher_type=voucher_type, voucher_no=voucher_no, doc=doc
|
||||||
|
)
|
||||||
|
if items_to_be_repost:
|
||||||
|
args = items_to_be_repost
|
||||||
|
|
||||||
distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
|
distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
|
||||||
affected_transactions = get_affected_transactions(doc)
|
affected_transactions = get_affected_transactions(doc)
|
||||||
@@ -276,17 +281,21 @@ def update_args_in_repost_item_valuation(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
|
def get_items_to_be_repost(voucher_type=None, voucher_no=None, doc=None):
|
||||||
|
items_to_be_repost = []
|
||||||
if doc and doc.items_to_be_repost:
|
if doc and doc.items_to_be_repost:
|
||||||
return json.loads(doc.items_to_be_repost) or []
|
items_to_be_repost = json.loads(doc.items_to_be_repost) or []
|
||||||
|
|
||||||
return frappe.db.get_all(
|
if not items_to_be_repost and voucher_type and voucher_no:
|
||||||
"Stock Ledger Entry",
|
items_to_be_repost = frappe.db.get_all(
|
||||||
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
|
"Stock Ledger Entry",
|
||||||
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
|
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
|
||||||
order_by="creation asc",
|
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
|
||||||
group_by="item_code, warehouse",
|
order_by="creation asc",
|
||||||
)
|
group_by="item_code, warehouse",
|
||||||
|
)
|
||||||
|
|
||||||
|
return items_to_be_repost
|
||||||
|
|
||||||
|
|
||||||
def get_distinct_item_warehouse(args=None, doc=None):
|
def get_distinct_item_warehouse(args=None, doc=None):
|
||||||
@@ -486,7 +495,8 @@ class update_entries_after(object):
|
|||||||
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
|
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
|
||||||
return entries_to_fix
|
return entries_to_fix
|
||||||
else:
|
else:
|
||||||
self.append_future_sle_for_dependant(dependant_sle, entries_to_fix)
|
self.initialize_previous_data(dependant_sle)
|
||||||
|
self.update_distinct_item_warehouses(dependant_sle)
|
||||||
return entries_to_fix
|
return entries_to_fix
|
||||||
|
|
||||||
def update_distinct_item_warehouses(self, dependant_sle):
|
def update_distinct_item_warehouses(self, dependant_sle):
|
||||||
@@ -504,14 +514,6 @@ class update_entries_after(object):
|
|||||||
self.distinct_item_warehouses[key] = val
|
self.distinct_item_warehouses[key] = val
|
||||||
self.new_items_found = True
|
self.new_items_found = True
|
||||||
|
|
||||||
def append_future_sle_for_dependant(self, dependant_sle, entries_to_fix):
|
|
||||||
self.initialize_previous_data(dependant_sle)
|
|
||||||
self.distinct_item_warehouses[(self.item_code, dependant_sle.warehouse)] = frappe._dict(
|
|
||||||
{"sle": dependant_sle}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.new_items_found = True
|
|
||||||
|
|
||||||
def process_sle(self, sle):
|
def process_sle(self, sle):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
|||||||
@@ -898,6 +898,7 @@ ERPNext Settings,ERPNext-Einstellungen,
|
|||||||
Earliest,Frühestens,
|
Earliest,Frühestens,
|
||||||
Earnest Money,Anzahlung,
|
Earnest Money,Anzahlung,
|
||||||
Earning,Einkommen,
|
Earning,Einkommen,
|
||||||
|
Earnings & Deductions,Verdienste & Abzüge,
|
||||||
Edit,Bearbeiten,
|
Edit,Bearbeiten,
|
||||||
Edit Publishing Details,Bearbeitungsdetails bearbeiten,
|
Edit Publishing Details,Bearbeitungsdetails bearbeiten,
|
||||||
"Edit in full page for more options like assets, serial nos, batches etc.","Bearbeiten Sie in Vollansicht für weitere Optionen wie Vermögenswerte, Seriennummern, Chargen usw.",
|
"Edit in full page for more options like assets, serial nos, batches etc.","Bearbeiten Sie in Vollansicht für weitere Optionen wie Vermögenswerte, Seriennummern, Chargen usw.",
|
||||||
|
|||||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user