mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 12:25:09 +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",
|
||||
"unlink_advance_payment_on_cancelation_of_order",
|
||||
"enable_common_party_accounting",
|
||||
"allow_multi_currency_invoices_against_single_party_account",
|
||||
"post_change_gl_entries",
|
||||
"enable_discount_accounting",
|
||||
"tax_settings_section",
|
||||
@@ -276,14 +277,21 @@
|
||||
"fieldname": "enable_common_party_accounting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Common Party Accounting"
|
||||
}
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
|
||||
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow multi-currency invoices against single party account"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-11 17:42:36.427699",
|
||||
"modified": "2022-07-11 13:37:50.605141",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -43,7 +43,7 @@ class GLEntry(Document):
|
||||
self.validate_and_set_fiscal_year()
|
||||
self.pl_must_have_cost_center()
|
||||
|
||||
if not self.flags.from_repost:
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
self.check_mandatory()
|
||||
self.validate_cost_center()
|
||||
self.check_pl_account()
|
||||
@@ -52,7 +52,7 @@ class GLEntry(Document):
|
||||
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost:
|
||||
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
self.validate_allowed_dimensions()
|
||||
|
||||
@@ -25,7 +25,10 @@ from erpnext.accounts.utils import (
|
||||
get_stock_and_account_balance,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import (
|
||||
get_outstanding_amount_for_claim,
|
||||
update_reimbursed_amount,
|
||||
)
|
||||
|
||||
|
||||
class StockAccountInvalidTransaction(frappe.ValidationError):
|
||||
@@ -935,15 +938,12 @@ class JournalEntry(AccountsController):
|
||||
def validate_expense_claim(self):
|
||||
for d in self.accounts:
|
||||
if d.reference_type == "Expense Claim":
|
||||
sanctioned_amount, reimbursed_amount = frappe.db.get_value(
|
||||
"Expense Claim", d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed")
|
||||
)
|
||||
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
|
||||
if d.debit > pending_amount:
|
||||
outstanding_amt = get_outstanding_amount_for_claim(d.reference_name)
|
||||
if d.debit > outstanding_amt:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}"
|
||||
).format(d.idx, d.reference_name, pending_amount)
|
||||
"Row No {0}: Amount cannot be greater than the Outstanding Amount against Expense Claim {1}. Outstanding Amount is {2}"
|
||||
).format(d.idx, d.reference_name, outstanding_amt)
|
||||
)
|
||||
|
||||
def validate_credit_debit_note(self):
|
||||
|
||||
@@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
|
||||
doc: frm.doc,
|
||||
btn: $(btn_primary),
|
||||
method: "make_invoices",
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
|
||||
freeze: 1,
|
||||
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
|
||||
callback: function(r) {
|
||||
if (r.message.length == 1) {
|
||||
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
|
||||
} else if (r.message.length < 50) {
|
||||
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,7 +30,10 @@ from erpnext.controllers.accounts_controller import (
|
||||
get_supplier_block_status,
|
||||
validate_taxes_and_charges,
|
||||
)
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
|
||||
from erpnext.hr.doctype.expense_claim.expense_claim import (
|
||||
get_outstanding_amount_for_claim,
|
||||
update_reimbursed_amount,
|
||||
)
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
@@ -1649,12 +1652,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
||||
outstanding_amount = ref_doc.get("outstanding_amount")
|
||||
bill_no = ref_doc.get("bill_no")
|
||||
elif reference_doctype == "Expense Claim":
|
||||
outstanding_amount = (
|
||||
flt(ref_doc.get("total_sanctioned_amount"))
|
||||
+ flt(ref_doc.get("total_taxes_and_charges"))
|
||||
- flt(ref_doc.get("total_amount_reimbursed"))
|
||||
- flt(ref_doc.get("total_advance_amount"))
|
||||
)
|
||||
outstanding_amount = get_outstanding_amount_for_claim(ref_doc)
|
||||
elif reference_doctype == "Employee Advance":
|
||||
outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
|
||||
if party_account_currency != ref_doc.currency:
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
"fiscal_year",
|
||||
"amended_from",
|
||||
"company",
|
||||
"cost_center_wise_pnl",
|
||||
"column_break1",
|
||||
"closing_account_head",
|
||||
"remarks"
|
||||
"remarks",
|
||||
"gle_processing_status",
|
||||
"error_message"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -86,17 +87,26 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "cost_center_wise_pnl",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Cost Center Wise Profit/Loss"
|
||||
"depends_on": "eval:doc.docstatus!=0",
|
||||
"fieldname": "gle_processing_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "GL Entry Processing Status",
|
||||
"options": "In Progress\nCompleted\nFailed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.gle_processing_status=='Failed'",
|
||||
"fieldname": "error_message",
|
||||
"fieldtype": "Text",
|
||||
"label": "Error Message",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-20 15:27:37.210458",
|
||||
"modified": "2022-07-20 14:51:04.714154",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Period Closing Voucher",
|
||||
|
||||
@@ -8,7 +8,6 @@ from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimensions,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
@@ -20,13 +19,28 @@ class PeriodClosingVoucher(AccountsController):
|
||||
self.validate_posting_date()
|
||||
|
||||
def on_submit(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("gle_processing_status", "In Progress")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
gle_count = frappe.db.count(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
|
||||
)
|
||||
if gle_count > 5000:
|
||||
frappe.enqueue(
|
||||
make_reverse_gl_entries,
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||
)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
|
||||
|
||||
def validate_account_head(self):
|
||||
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
|
||||
@@ -67,90 +81,80 @@ class PeriodClosingVoucher(AccountsController):
|
||||
def make_gl_entries(self):
|
||||
gl_entries = self.get_gl_entries()
|
||||
if gl_entries:
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
make_gl_entries(gl_entries)
|
||||
if len(gl_entries) > 5000:
|
||||
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_entries(gl_entries)
|
||||
|
||||
def get_gl_entries(self):
|
||||
gl_entries = []
|
||||
pl_accounts = self.get_pl_balances()
|
||||
|
||||
for acc in pl_accounts:
|
||||
# pl account
|
||||
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) > 0
|
||||
else 0,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
)
|
||||
gl_entries.append(self.get_gle_for_pl_account(acc))
|
||||
|
||||
if gl_entries:
|
||||
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
|
||||
gl_entries += gle_for_net_pl_bal
|
||||
# closing liability account
|
||||
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
|
||||
if flt(acc.bal_in_company_currency):
|
||||
gl_entries.append(self.get_gle_for_closing_account(acc))
|
||||
|
||||
return gl_entries
|
||||
|
||||
def get_pnl_gl_entry(self, pl_accounts):
|
||||
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
||||
gl_entries = []
|
||||
def get_gle_for_pl_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"account": acc.account,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
for acc in pl_accounts:
|
||||
if flt(acc.bal_in_company_currency):
|
||||
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) > 0
|
||||
else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency))
|
||||
if flt(acc.bal_in_company_currency) < 0
|
||||
else 0,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
def get_gle_for_closing_account(self, acc):
|
||||
gl_entry = self.get_gl_dict(
|
||||
{
|
||||
"account": self.closing_account_head,
|
||||
"cost_center": acc.cost_center,
|
||||
"finance_book": acc.finance_book,
|
||||
"account_currency": acc.account_currency,
|
||||
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) > 0
|
||||
else 0,
|
||||
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
|
||||
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
|
||||
if flt(acc.bal_in_account_currency) < 0
|
||||
else 0,
|
||||
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
|
||||
},
|
||||
item=acc,
|
||||
)
|
||||
self.update_default_dimensions(gl_entry, acc)
|
||||
return gl_entry
|
||||
|
||||
self.update_default_dimensions(gl_entry)
|
||||
|
||||
gl_entries.append(gl_entry)
|
||||
|
||||
return gl_entries
|
||||
|
||||
def update_default_dimensions(self, gl_entry):
|
||||
def update_default_dimensions(self, gl_entry, acc):
|
||||
if not self.accounting_dimensions:
|
||||
self.accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
_, default_dimensions = get_dimensions()
|
||||
for dimension in self.accounting_dimensions:
|
||||
gl_entry.update({dimension: default_dimensions.get(self.company, {}).get(dimension)})
|
||||
gl_entry.update({dimension: acc.get(dimension)})
|
||||
|
||||
def get_pl_balances(self):
|
||||
def get_pl_balances_based_on_dimensions(self, group_by_account=False):
|
||||
"""Get balance for dimension-wise pl accounts"""
|
||||
|
||||
dimension_fields = ["t1.cost_center", "t1.finance_book"]
|
||||
@@ -159,20 +163,56 @@ class PeriodClosingVoucher(AccountsController):
|
||||
for dimension in self.accounting_dimensions:
|
||||
dimension_fields.append("t1.{0}".format(dimension))
|
||||
|
||||
if group_by_account:
|
||||
dimension_fields.append("t1.account")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
t1.account, t2.account_currency, {dimension_fields},
|
||||
t2.account_currency,
|
||||
{dimension_fields},
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2 and t2.company = %s
|
||||
and t1.posting_date between %s and %s
|
||||
group by t1.account, {dimension_fields}
|
||||
where
|
||||
t1.is_cancelled = 0
|
||||
and t1.account = t2.name
|
||||
and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2
|
||||
and t2.company = %s
|
||||
and t1.posting_date between %s and %s
|
||||
group by {dimension_fields}
|
||||
""".format(
|
||||
dimension_fields=", ".join(dimension_fields)
|
||||
),
|
||||
(self.company, self.get("year_start_date"), self.posting_date),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def process_gl_entries(gl_entries):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
frappe.db.set_value(
|
||||
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
|
||||
)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value(
|
||||
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
|
||||
)
|
||||
|
||||
|
||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
try:
|
||||
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")
|
||||
|
||||
@@ -49,7 +49,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
expected_gle = (
|
||||
("Cost of Goods Sold - TPC", 0.0, 600.0),
|
||||
(surplus_account, 600.0, 400.0),
|
||||
(surplus_account, 200.0, 0.0),
|
||||
("Sales - TPC", 400.0, 0.0),
|
||||
)
|
||||
|
||||
@@ -59,7 +59,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
""",
|
||||
(pcv.name),
|
||||
)
|
||||
|
||||
pcv.reload()
|
||||
self.assertEqual(pcv.gle_processing_status, "Completed")
|
||||
self.assertEqual(pcv_gle, expected_gle)
|
||||
|
||||
def test_cost_center_wise_posting(self):
|
||||
@@ -94,7 +95,6 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
)
|
||||
|
||||
pcv = self.make_period_closing_voucher(submit=False)
|
||||
pcv.cost_center_wise_pnl = 1
|
||||
pcv.save()
|
||||
pcv.submit()
|
||||
surplus_account = pcv.closing_account_head
|
||||
@@ -117,6 +117,16 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
self.assertEqual(pcv_gle, expected_gle)
|
||||
|
||||
pcv.reload()
|
||||
pcv.cancel()
|
||||
|
||||
self.assertFalse(
|
||||
frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{"voucher_type": "Period Closing Voucher", "voucher_no": pcv.name, "is_cancelled": 0},
|
||||
)
|
||||
)
|
||||
|
||||
def test_period_closing_with_finance_book_entries(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ def update_net_values(entry):
|
||||
def merge_similar_entries(gl_map, precision=None):
|
||||
merged_gl_map = []
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
|
||||
for entry in gl_map:
|
||||
# if there is already an entry in this account then just add it
|
||||
# to that entry
|
||||
@@ -229,9 +230,10 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
gle.flags.from_repost = from_repost
|
||||
gle.flags.adv_adj = adv_adj
|
||||
gle.flags.update_outstanding = update_outstanding or "Yes"
|
||||
gle.flags.notify_update = False
|
||||
gle.submit()
|
||||
|
||||
if not from_repost:
|
||||
if not from_repost and gle.voucher_type != "Period Closing Voucher":
|
||||
validate_expense_against_budget(args)
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,11 @@ from frappe import _
|
||||
|
||||
def execute(filters=None):
|
||||
validate_filters(filters)
|
||||
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
|
||||
tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map)
|
||||
return columns, res
|
||||
|
||||
|
||||
@@ -22,10 +22,11 @@ def validate_filters(filters):
|
||||
frappe.throw(_("From Date must be before To Date"))
|
||||
|
||||
|
||||
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map):
|
||||
supplier_map = get_supplier_pan_map()
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
print(journal_entry_party_map)
|
||||
|
||||
out = []
|
||||
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
|
||||
voucher_type = entry.voucher_type
|
||||
|
||||
if voucher_type == "Journal Entry":
|
||||
suppliers = journal_entry_party_map.get(name)
|
||||
if suppliers:
|
||||
supplier = suppliers[0]
|
||||
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
@@ -176,6 +182,7 @@ def get_tds_docs(filters):
|
||||
journal_entries = []
|
||||
tax_category_map = {}
|
||||
or_filters = {}
|
||||
journal_entry_party_map = {}
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
|
||||
tds_accounts = frappe.get_all(
|
||||
@@ -218,9 +225,24 @@ def get_tds_docs(filters):
|
||||
get_tax_category_map(payment_entries, "Payment Entry", tax_category_map)
|
||||
|
||||
if journal_entries:
|
||||
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
|
||||
get_tax_category_map(journal_entries, "Journal Entry", tax_category_map)
|
||||
|
||||
return tds_documents, tds_accounts, tax_category_map
|
||||
return tds_documents, tds_accounts, tax_category_map, journal_entry_party_map
|
||||
|
||||
|
||||
def get_journal_entry_party_map(journal_entries):
|
||||
journal_entry_party_map = {}
|
||||
for d in frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
{"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
|
||||
["parent", "party"],
|
||||
):
|
||||
if d.parent not in journal_entry_party_map:
|
||||
journal_entry_party_map[d.parent] = []
|
||||
journal_entry_party_map[d.parent].append(d.party)
|
||||
|
||||
return journal_entry_party_map
|
||||
|
||||
|
||||
def get_tax_category_map(vouchers, doctype, tax_category_map):
|
||||
|
||||
@@ -38,7 +38,6 @@
|
||||
"purchase_date",
|
||||
"section_break_23",
|
||||
"calculate_depreciation",
|
||||
"allow_monthly_depreciation",
|
||||
"column_break_33",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
@@ -454,13 +453,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "calculate_depreciation",
|
||||
"fieldname": "allow_monthly_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Monthly Depreciation"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "is_existing_asset",
|
||||
@@ -503,7 +495,7 @@
|
||||
"link_fieldname": "asset"
|
||||
}
|
||||
],
|
||||
"modified": "2022-01-30 20:19:24.680027",
|
||||
"modified": "2022-07-20 16:22:44.437579",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -545,4 +537,4 @@
|
||||
"sort_order": "DESC",
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -353,61 +353,16 @@ class Asset(AccountsController):
|
||||
skip_row = True
|
||||
|
||||
if depreciation_amount > 0:
|
||||
# With monthly depreciation, each depreciation is divided by months remaining until next date
|
||||
if self.allow_monthly_depreciation:
|
||||
# month range is 1 to 12
|
||||
# In pro rata case, for first and last depreciation, month range would be different
|
||||
if (has_pro_rata and n == 0 and not self.number_of_depreciations_booked) or (
|
||||
has_pro_rata and n == cint(number_of_pending_depreciations) - 1
|
||||
):
|
||||
month_range = months
|
||||
else:
|
||||
month_range = finance_book.frequency_of_depreciation
|
||||
|
||||
for r in range(month_range):
|
||||
if has_pro_rata and n == 0 and not self.number_of_depreciations_booked:
|
||||
# For first entry of monthly depr
|
||||
if r == 0:
|
||||
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + 1
|
||||
per_day_amt = depreciation_amount / days
|
||||
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
|
||||
depreciation_amount -= depreciation_amount_for_current_month
|
||||
date = monthly_schedule_date
|
||||
amount = depreciation_amount_for_current_month
|
||||
else:
|
||||
date = add_months(monthly_schedule_date, r)
|
||||
amount = depreciation_amount / (month_range - 1)
|
||||
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(
|
||||
month_range
|
||||
) - 1:
|
||||
# For last entry of monthly depr
|
||||
date = last_schedule_date
|
||||
amount = depreciation_amount / month_range
|
||||
else:
|
||||
date = add_months(monthly_schedule_date, r)
|
||||
amount = depreciation_amount / month_range
|
||||
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": date,
|
||||
"depreciation_amount": amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx,
|
||||
},
|
||||
)
|
||||
else:
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx,
|
||||
},
|
||||
)
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
"finance_book_id": finance_book.idx,
|
||||
},
|
||||
)
|
||||
|
||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||
# JE: Journal Entry, FB: Finance Book
|
||||
@@ -853,7 +808,7 @@ class Asset(AccountsController):
|
||||
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
|
||||
|
||||
return 100 * (1 - flt(depreciation_rate, float_precision))
|
||||
return flt((100 * (1 - depreciation_rate)), float_precision)
|
||||
|
||||
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
|
||||
days = date_diff(to_date, from_date)
|
||||
|
||||
@@ -145,6 +145,7 @@ class TestAsset(AssetSetup):
|
||||
def test_is_fixed_asset_set(self):
|
||||
asset = create_asset(is_existing_asset=1)
|
||||
doc = frappe.new_doc("Purchase Invoice")
|
||||
doc.company = "_Test Company"
|
||||
doc.supplier = "_Test Supplier"
|
||||
doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name})
|
||||
|
||||
@@ -702,6 +703,8 @@ class TestDepreciationMethods(AssetSetup):
|
||||
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||
|
||||
def test_monthly_depreciation_by_wdv_method(self):
|
||||
existing_precision = frappe.db.get_default("float_precision")
|
||||
frappe.db.set_default("float_precision", 3)
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2022-02-15",
|
||||
@@ -715,12 +718,12 @@ class TestDepreciationMethods(AssetSetup):
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2022-02-28", 645.0, 645.0],
|
||||
["2022-03-31", 1206.8, 1851.8],
|
||||
["2022-04-30", 1051.12, 2902.92],
|
||||
["2022-05-31", 915.52, 3818.44],
|
||||
["2022-06-30", 797.42, 4615.86],
|
||||
["2022-07-15", 384.14, 5000.0],
|
||||
["2022-02-28", 647.25, 647.25],
|
||||
["2022-03-31", 1210.71, 1857.96],
|
||||
["2022-04-30", 1053.99, 2911.95],
|
||||
["2022-05-31", 917.55, 3829.5],
|
||||
["2022-06-30", 798.77, 4628.27],
|
||||
["2022-07-15", 371.73, 5000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
@@ -731,8 +734,8 @@ class TestDepreciationMethods(AssetSetup):
|
||||
]
|
||||
for d in asset.get("schedules")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
frappe.db.set_default("float_precision", existing_precision)
|
||||
|
||||
|
||||
class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
@@ -1470,8 +1470,15 @@ class AccountsController(TransactionBase):
|
||||
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
|
||||
)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
allow_multi_currency_invoices_against_single_party_account = frappe.db.get_singles_value(
|
||||
"Accounts Settings", "allow_multi_currency_invoices_against_single_party_account"
|
||||
)
|
||||
|
||||
if not party_gle_currency and (party_account_currency != self.currency):
|
||||
if (
|
||||
not party_gle_currency
|
||||
and (party_account_currency != self.currency)
|
||||
and not allow_multi_currency_invoices_against_single_party_account
|
||||
):
|
||||
frappe.throw(
|
||||
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
||||
frappe.bold(party_account), party_account_currency, self.currency
|
||||
|
||||
@@ -355,6 +355,8 @@ class Subcontracting:
|
||||
rm_obj.purchase_order = item_row.purchase_order
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
rm_obj.amount = flt(rm_obj.required_qty) * flt(rm_obj.rate)
|
||||
|
||||
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
|
||||
|
||||
|
||||
@@ -483,12 +483,12 @@ scheduler_events = {
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
||||
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
|
||||
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
|
||||
],
|
||||
"hourly_long": [
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
|
||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
||||
],
|
||||
"daily": [
|
||||
"erpnext.support.doctype.issue.issue.auto_close_tickets",
|
||||
|
||||
@@ -813,7 +813,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-06-17 11:31:37.730760",
|
||||
"modified": "2022-07-18 20:03:43.188705",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
|
||||
@@ -349,7 +349,9 @@ def get_employee_email(employee_doc):
|
||||
|
||||
def get_holiday_list_for_employee(employee, raise_exception=True):
|
||||
if employee:
|
||||
holiday_list, company = frappe.db.get_value("Employee", employee, ["holiday_list", "company"])
|
||||
holiday_list, company = frappe.get_cached_value(
|
||||
"Employee", employee, ["holiday_list", "company"]
|
||||
)
|
||||
else:
|
||||
holiday_list = ""
|
||||
company = frappe.db.get_value("Global Defaults", None, "default_company")
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Employee",
|
||||
"options": "Employee",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.employee_name",
|
||||
@@ -48,7 +49,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Shift",
|
||||
"options": "Shift Type",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
@@ -107,7 +109,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-07-08 11:02:32.660986",
|
||||
"modified": "2022-07-19 15:38:41.767539",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Checkin",
|
||||
|
||||
@@ -131,7 +131,7 @@ def mark_attendance_and_link_log(
|
||||
return None
|
||||
|
||||
elif attendance_status in ("Present", "Absent", "Half Day"):
|
||||
employee_doc = frappe.get_doc("Employee", employee)
|
||||
company = frappe.get_cached_value("Employee", employee, "company")
|
||||
duplicate = frappe.db.exists(
|
||||
"Attendance",
|
||||
{"employee": employee, "attendance_date": attendance_date, "docstatus": ("!=", "2")},
|
||||
@@ -144,7 +144,7 @@ def mark_attendance_and_link_log(
|
||||
"attendance_date": attendance_date,
|
||||
"status": attendance_status,
|
||||
"working_hours": working_hours,
|
||||
"company": employee_doc.company,
|
||||
"company": company,
|
||||
"shift": shift,
|
||||
"late_entry": late_entry,
|
||||
"early_exit": early_exit,
|
||||
|
||||
@@ -339,6 +339,30 @@ def update_reimbursed_amount(doc, amount):
|
||||
frappe.db.set_value("Expense Claim", doc.name, "status", doc.status)
|
||||
|
||||
|
||||
def get_outstanding_amount_for_claim(claim):
|
||||
if isinstance(claim, str):
|
||||
claim = frappe.db.get_value(
|
||||
"Expense Claim",
|
||||
claim,
|
||||
(
|
||||
"total_sanctioned_amount",
|
||||
"total_taxes_and_charges",
|
||||
"total_amount_reimbursed",
|
||||
"total_advance_amount",
|
||||
),
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
outstanding_amt = (
|
||||
flt(claim.total_sanctioned_amount)
|
||||
+ flt(claim.total_taxes_and_charges)
|
||||
- flt(claim.total_amount_reimbursed)
|
||||
- flt(claim.total_advance_amount)
|
||||
)
|
||||
|
||||
return outstanding_amt
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_bank_entry(dt, dn):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||
@@ -348,11 +372,7 @@ def make_bank_entry(dt, dn):
|
||||
if not default_bank_cash_account:
|
||||
default_bank_cash_account = get_default_bank_cash_account(expense_claim.company, "Cash")
|
||||
|
||||
payable_amount = (
|
||||
flt(expense_claim.total_sanctioned_amount)
|
||||
- flt(expense_claim.total_amount_reimbursed)
|
||||
- flt(expense_claim.total_advance_amount)
|
||||
)
|
||||
payable_amount = get_outstanding_amount_for_claim(expense_claim)
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Bank Entry"
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, nowdate, random_string
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
@@ -14,9 +15,18 @@ test_dependencies = ["Employee"]
|
||||
company_name = "_Test Company 3"
|
||||
|
||||
|
||||
class TestExpenseClaim(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
class TestExpenseClaim(FrappeTestCase):
|
||||
def setUp(self):
|
||||
if not frappe.db.get_value("Cost Center", {"company": company_name}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Cost Center",
|
||||
"cost_center_name": "_Test Cost Center 3",
|
||||
"parent_cost_center": "_Test Company 3 - _TC3",
|
||||
"is_group": 0,
|
||||
"company": company_name,
|
||||
}
|
||||
).insert()
|
||||
|
||||
def test_total_expense_claim_for_project(self):
|
||||
frappe.db.sql("""delete from `tabTask`""")
|
||||
@@ -58,12 +68,7 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
payable_account, 300, 200, company_name, "Travel Expenses - _TC3"
|
||||
)
|
||||
|
||||
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
|
||||
je = frappe.get_doc(je_dict)
|
||||
je.posting_date = nowdate()
|
||||
je.cheque_no = random_string(5)
|
||||
je.cheque_date = nowdate()
|
||||
je.submit()
|
||||
je = make_journal_entry(expense_claim)
|
||||
|
||||
expense_claim = frappe.get_doc("Expense Claim", expense_claim.name)
|
||||
self.assertEqual(expense_claim.status, "Paid")
|
||||
@@ -272,6 +277,24 @@ class TestExpenseClaim(unittest.TestCase):
|
||||
self.assertEqual(outstanding_amount, 0)
|
||||
self.assertEqual(total_amount_reimbursed, 5500)
|
||||
|
||||
def test_journal_entry_against_expense_claim(self):
|
||||
payable_account = get_payable_account(company_name)
|
||||
taxes = generate_taxes()
|
||||
expense_claim = make_expense_claim(
|
||||
payable_account,
|
||||
300,
|
||||
200,
|
||||
company_name,
|
||||
"Travel Expenses - _TC3",
|
||||
do_not_submit=True,
|
||||
taxes=taxes,
|
||||
)
|
||||
expense_claim.submit()
|
||||
|
||||
je = make_journal_entry(expense_claim)
|
||||
|
||||
self.assertEqual(je.accounts[0].debit_in_account_currency, expense_claim.grand_total)
|
||||
|
||||
|
||||
def get_payable_account(company):
|
||||
return frappe.get_cached_value("Company", company, "default_payable_account")
|
||||
@@ -370,3 +393,14 @@ def make_payment_entry(expense_claim, payable_account, amt):
|
||||
pe.references[0].allocated_amount = amt
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
|
||||
def make_journal_entry(expense_claim):
|
||||
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
|
||||
je = frappe.get_doc(je_dict)
|
||||
je.posting_date = nowdate()
|
||||
je.cheque_no = random_string(5)
|
||||
je.cheque_date = nowdate()
|
||||
je.submit()
|
||||
|
||||
return je
|
||||
|
||||
@@ -115,6 +115,8 @@ def is_holiday(holiday_list, date=None):
|
||||
if date is None:
|
||||
date = today()
|
||||
if holiday_list:
|
||||
return bool(frappe.get_all("Holiday List", dict(name=holiday_list, holiday_date=date)))
|
||||
return bool(
|
||||
frappe.db.exists("Holiday", {"parent": holiday_list, "holiday_date": date}, cache=True)
|
||||
)
|
||||
else:
|
||||
return False
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Employee",
|
||||
"options": "Employee",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.employee_name",
|
||||
@@ -48,7 +49,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Shift Type",
|
||||
"options": "Shift Type",
|
||||
"reqd": 1
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
@@ -105,7 +107,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-15 14:27:54.310773",
|
||||
"modified": "2022-07-19 15:27:54.310773",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Shift Assignment",
|
||||
|
||||
@@ -169,7 +169,7 @@ def get_employee_shift(
|
||||
"""
|
||||
if for_date is None:
|
||||
for_date = nowdate()
|
||||
default_shift = frappe.db.get_value("Employee", employee, "default_shift")
|
||||
default_shift = frappe.get_cached_value("Employee", employee, "default_shift")
|
||||
shift_type_name = None
|
||||
shift_assignment_details = frappe.db.get_value(
|
||||
"Shift Assignment",
|
||||
@@ -187,7 +187,7 @@ def get_employee_shift(
|
||||
if not shift_type_name and consider_default_shift:
|
||||
shift_type_name = default_shift
|
||||
if shift_type_name:
|
||||
holiday_list_name = frappe.db.get_value("Shift Type", shift_type_name, "holiday_list")
|
||||
holiday_list_name = frappe.get_cached_value("Shift Type", shift_type_name, "holiday_list")
|
||||
if not holiday_list_name:
|
||||
holiday_list_name = get_holiday_list_for_employee(employee, False)
|
||||
if holiday_list_name and is_holiday(holiday_list_name, for_date):
|
||||
@@ -294,7 +294,18 @@ def get_shift_details(shift_type_name, for_date=None):
|
||||
return None
|
||||
if not for_date:
|
||||
for_date = nowdate()
|
||||
shift_type = frappe.get_doc("Shift Type", shift_type_name)
|
||||
shift_type = frappe.get_cached_value(
|
||||
"Shift Type",
|
||||
shift_type_name,
|
||||
[
|
||||
"name",
|
||||
"start_time",
|
||||
"end_time",
|
||||
"begin_check_in_before_shift_start_time",
|
||||
"allow_check_out_after_shift_end_time",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
|
||||
for_date = (
|
||||
for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date
|
||||
|
||||
@@ -107,7 +107,7 @@ class ShiftType(Document):
|
||||
"""Marks Absents for the given employee on working days in this shift which have no attendance marked.
|
||||
The Absent is marked starting from 'process_attendance_after' or employee creation date.
|
||||
"""
|
||||
date_of_joining, relieving_date, employee_creation = frappe.db.get_value(
|
||||
date_of_joining, relieving_date, employee_creation = frappe.get_cached_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date", "creation"]
|
||||
)
|
||||
if not date_of_joining:
|
||||
@@ -156,21 +156,19 @@ class ShiftType(Document):
|
||||
if not from_date:
|
||||
del filters["start_date"]
|
||||
|
||||
assigned_employees = frappe.get_all("Shift Assignment", "employee", filters, as_list=True)
|
||||
assigned_employees = [x[0] for x in assigned_employees]
|
||||
assigned_employees = frappe.get_all("Shift Assignment", filters, pluck="employee")
|
||||
|
||||
if consider_default_shift:
|
||||
filters = {"default_shift": self.name, "status": ["!=", "Inactive"]}
|
||||
default_shift_employees = frappe.get_all("Employee", "name", filters, as_list=True)
|
||||
default_shift_employees = [x[0] for x in default_shift_employees]
|
||||
default_shift_employees = frappe.get_all("Employee", filters, pluck="name")
|
||||
return list(set(assigned_employees + default_shift_employees))
|
||||
return assigned_employees
|
||||
|
||||
|
||||
def process_auto_attendance_for_all_shifts():
|
||||
shift_list = frappe.get_all("Shift Type", "name", {"enable_auto_attendance": "1"}, as_list=True)
|
||||
shift_list = frappe.get_all("Shift Type", filters={"enable_auto_attendance": "1"}, pluck="name")
|
||||
for shift in shift_list:
|
||||
doc = frappe.get_doc("Shift Type", shift[0])
|
||||
doc = frappe.get_cached_doc("Shift Type", shift)
|
||||
doc.process_auto_attendance()
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
"total_payment",
|
||||
"total_principal_paid",
|
||||
"written_off_amount",
|
||||
"refund_amount",
|
||||
"debit_adjustment_amount",
|
||||
"credit_adjustment_amount",
|
||||
"is_npa",
|
||||
"column_break_19",
|
||||
"total_interest_payable",
|
||||
"total_amount_paid",
|
||||
@@ -379,12 +383,39 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "refund_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Refund amount",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_adjustment_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Credit Adjustment Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "debit_adjustment_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Debit Adjustment Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Mark Loan as a Nonperforming asset",
|
||||
"fieldname": "is_npa",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is NPA"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-10 11:50:31.957360",
|
||||
"modified": "2022-06-30 12:04:13.728880",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Loan Balance Adjustment', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "LM-ADJ-.#####",
|
||||
"creation": "2022-06-28 14:48:47.736269",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan",
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"posting_date",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"section_break_9",
|
||||
"adjustment_account",
|
||||
"column_break_11",
|
||||
"adjustment_type",
|
||||
"amount",
|
||||
"reference_number",
|
||||
"remarks",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan",
|
||||
"options": "Loan",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant_type",
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Applicant ",
|
||||
"options": "applicant_type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Posting Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Adjustment Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Reference Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Balance Adjustment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Loan Balance Adjustment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "adjustment_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Adjustment Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "adjustment_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Adjustment Type",
|
||||
"options": "Credit Adjustment\nDebit Adjustment",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "remarks",
|
||||
"fieldtype": "Data",
|
||||
"label": "Remarks"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-08 16:48:54.480066",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Balance Adjustment",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Loan Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.controllers.accounts_controller import AccountsController
|
||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
process_loan_interest_accrual_for_demand_loans,
|
||||
)
|
||||
|
||||
|
||||
class LoanBalanceAdjustment(AccountsController):
|
||||
"""
|
||||
Add credit/debit adjustments to loan ledger.
|
||||
"""
|
||||
|
||||
def validate(self):
|
||||
if self.amount == 0:
|
||||
frappe.throw(_("Amount cannot be zero"))
|
||||
if self.amount < 0:
|
||||
frappe.throw(_("Amount cannot be negative"))
|
||||
self.set_missing_values()
|
||||
|
||||
def on_submit(self):
|
||||
self.set_status_and_amounts()
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status_and_amounts(cancel=1)
|
||||
self.make_gl_entries(cancel=1)
|
||||
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||
|
||||
def set_missing_values(self):
|
||||
if not self.posting_date:
|
||||
self.posting_date = nowdate()
|
||||
|
||||
if not self.cost_center:
|
||||
self.cost_center = erpnext.get_default_cost_center(self.company)
|
||||
|
||||
def set_status_and_amounts(self, cancel=0):
|
||||
loan_details = frappe.db.get_value(
|
||||
"Loan",
|
||||
self.loan,
|
||||
[
|
||||
"loan_amount",
|
||||
"credit_adjustment_amount",
|
||||
"debit_adjustment_amount",
|
||||
"total_payment",
|
||||
"total_principal_paid",
|
||||
"total_interest_payable",
|
||||
"status",
|
||||
"is_term_loan",
|
||||
"is_secured_loan",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if cancel:
|
||||
adjustment_amount = self.get_values_on_cancel(loan_details)
|
||||
else:
|
||||
adjustment_amount = self.get_values_on_submit(loan_details)
|
||||
|
||||
if self.adjustment_type == "Credit Adjustment":
|
||||
adj_field = "credit_adjustment_amount"
|
||||
elif self.adjustment_type == "Debit Adjustment":
|
||||
adj_field = "debit_adjustment_amount"
|
||||
|
||||
frappe.db.set_value("Loan", self.loan, {adj_field: adjustment_amount})
|
||||
|
||||
def get_values_on_cancel(self, loan_details):
|
||||
if self.adjustment_type == "Credit Adjustment":
|
||||
adjustment_amount = loan_details.credit_adjustment_amount - self.amount
|
||||
elif self.adjustment_type == "Debit Adjustment":
|
||||
adjustment_amount = loan_details.debit_adjustment_amount - self.amount
|
||||
|
||||
return adjustment_amount
|
||||
|
||||
def get_values_on_submit(self, loan_details):
|
||||
if self.adjustment_type == "Credit Adjustment":
|
||||
adjustment_amount = loan_details.credit_adjustment_amount + self.amount
|
||||
elif self.adjustment_type == "Debit Adjustment":
|
||||
adjustment_amount = loan_details.debit_adjustment_amount + self.amount
|
||||
|
||||
if loan_details.status in ("Disbursed", "Partially Disbursed") and not loan_details.is_term_loan:
|
||||
process_loan_interest_accrual_for_demand_loans(
|
||||
posting_date=add_days(self.posting_date, -1),
|
||||
loan=self.loan,
|
||||
accrual_type=self.adjustment_type,
|
||||
)
|
||||
|
||||
return adjustment_amount
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gle_map = []
|
||||
loan_account = frappe.db.get_value("Loan", self.loan, "loan_account")
|
||||
remarks = "{} against loan {}".format(self.adjustment_type.capitalize(), self.loan)
|
||||
if self.reference_number:
|
||||
remarks += "with reference no. {}".format(self.reference_number)
|
||||
|
||||
loan_entry = {
|
||||
"account": loan_account,
|
||||
"against": self.adjustment_account,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.loan,
|
||||
"remarks": _(remarks),
|
||||
"cost_center": self.cost_center,
|
||||
"party_type": self.applicant_type,
|
||||
"party": self.applicant,
|
||||
"posting_date": self.posting_date,
|
||||
}
|
||||
company_entry = {
|
||||
"account": self.adjustment_account,
|
||||
"against": loan_account,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.loan,
|
||||
"remarks": _(remarks),
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.posting_date,
|
||||
}
|
||||
if self.adjustment_type == "Credit Adjustment":
|
||||
loan_entry["credit"] = self.amount
|
||||
loan_entry["credit_in_account_currency"] = self.amount
|
||||
|
||||
company_entry["debit"] = self.amount
|
||||
company_entry["debit_in_account_currency"] = self.amount
|
||||
|
||||
elif self.adjustment_type == "Debit Adjustment":
|
||||
loan_entry["debit"] = self.amount
|
||||
loan_entry["debit_in_account_currency"] = self.amount
|
||||
|
||||
company_entry["credit"] = self.amount
|
||||
company_entry["credit_in_account_currency"] = self.amount
|
||||
|
||||
gle_map.append(self.get_gl_dict(loan_entry))
|
||||
|
||||
gle_map.append(self.get_gl_dict(company_entry))
|
||||
|
||||
if gle_map:
|
||||
make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestLoanBalanceAdjustment(FrappeTestCase):
|
||||
pass
|
||||
@@ -35,12 +35,15 @@
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Loan",
|
||||
"options": "Loan"
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Posting Date"
|
||||
},
|
||||
{
|
||||
@@ -75,6 +78,8 @@
|
||||
"fetch_from": "loan.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type"
|
||||
},
|
||||
@@ -158,8 +163,11 @@
|
||||
{
|
||||
"fieldname": "accrual_type",
|
||||
"fieldtype": "Select",
|
||||
"in_filter": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Accrual Type",
|
||||
"options": "Regular\nRepayment\nDisbursement"
|
||||
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund"
|
||||
},
|
||||
{
|
||||
"fieldname": "penalty_amount",
|
||||
@@ -185,10 +193,11 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 18:26:38.871889",
|
||||
"modified": "2022-06-30 11:51:31.911794",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Interest Accrual",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -225,5 +234,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -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):
|
||||
gle_map = []
|
||||
|
||||
if self.shortfall_amount and self.amount_paid > self.shortfall_amount:
|
||||
remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(
|
||||
remarks = "Shortfall repayment of {0}.<br>Repayment against loan {1}".format(
|
||||
self.shortfall_amount, self.against_loan
|
||||
)
|
||||
elif self.shortfall_amount:
|
||||
remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount)
|
||||
remarks = "Shortfall repayment of {0} against loan {1}".format(
|
||||
self.shortfall_amount, self.against_loan
|
||||
)
|
||||
else:
|
||||
remarks = _("Repayment against Loan: ") + self.against_loan
|
||||
remarks = "Repayment against loan " + self.against_loan
|
||||
|
||||
if self.reference_number:
|
||||
remarks += "with reference no. {}".format(self.reference_number)
|
||||
|
||||
if self.repay_from_salary:
|
||||
payment_account = self.payroll_payable_account
|
||||
@@ -446,7 +450,7 @@ class LoanRepayment(AccountsController):
|
||||
"debit_in_account_currency": self.amount_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.against_loan,
|
||||
"remarks": remarks,
|
||||
"remarks": _(remarks),
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
}
|
||||
@@ -464,7 +468,7 @@ class LoanRepayment(AccountsController):
|
||||
"credit_in_account_currency": self.amount_paid,
|
||||
"against_voucher_type": "Loan",
|
||||
"against_voucher": self.against_loan,
|
||||
"remarks": remarks,
|
||||
"remarks": _(remarks),
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
}
|
||||
@@ -624,16 +628,22 @@ def get_pending_principal_amount(loan):
|
||||
if loan.status in ("Disbursed", "Closed") or loan.disbursed_amount >= loan.loan_amount:
|
||||
pending_principal_amount = (
|
||||
flt(loan.total_payment)
|
||||
+ flt(loan.debit_adjustment_amount)
|
||||
- flt(loan.credit_adjustment_amount)
|
||||
- flt(loan.total_principal_paid)
|
||||
- flt(loan.total_interest_payable)
|
||||
- flt(loan.written_off_amount)
|
||||
+ flt(loan.refund_amount)
|
||||
)
|
||||
else:
|
||||
pending_principal_amount = (
|
||||
flt(loan.disbursed_amount)
|
||||
+ flt(loan.debit_adjustment_amount)
|
||||
- flt(loan.credit_adjustment_amount)
|
||||
- flt(loan.total_principal_paid)
|
||||
- flt(loan.total_interest_payable)
|
||||
- flt(loan.written_off_amount)
|
||||
+ flt(loan.refund_amount)
|
||||
)
|
||||
|
||||
return pending_principal_amount
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"loan",
|
||||
"applicant_type",
|
||||
"applicant",
|
||||
"status",
|
||||
"column_break_3",
|
||||
"shortfall_time",
|
||||
@@ -23,6 +25,8 @@
|
||||
{
|
||||
"fieldname": "loan",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Loan ",
|
||||
"options": "Loan",
|
||||
"read_only": 1
|
||||
@@ -91,17 +95,35 @@
|
||||
{
|
||||
"fieldname": "shortfall_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Shortfall Percentage",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant_type",
|
||||
"fieldname": "applicant_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Applicant Type",
|
||||
"options": "Employee\nMember\nCustomer"
|
||||
},
|
||||
{
|
||||
"fetch_from": "loan.applicant",
|
||||
"fieldname": "applicant",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Applicant",
|
||||
"options": "applicant_type"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-01 08:13:43.263772",
|
||||
"modified": "2022-06-30 11:57:09.378089",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Security Shortfall",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -132,5 +154,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -54,17 +54,18 @@
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Accrual Type",
|
||||
"options": "Regular\nRepayment\nDisbursement",
|
||||
"options": "Regular\nRepayment\nDisbursement\nCredit Adjustment\nDebit Adjustment\nRefund",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-06 13:28:51.478909",
|
||||
"modified": "2022-06-29 11:19:33.203088",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Process Loan Interest Accrual",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -98,5 +99,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -444,6 +444,7 @@ class BOM(WebsiteGenerator):
|
||||
and self.is_active
|
||||
):
|
||||
frappe.db.set(self, "is_default", 1)
|
||||
frappe.db.set_value("Item", self.item, "default_bom", self.name)
|
||||
else:
|
||||
frappe.db.set(self, "is_default", 0)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
|
||||
@@ -519,6 +519,42 @@ class TestBOM(FrappeTestCase):
|
||||
|
||||
new_bom.delete()
|
||||
|
||||
def test_set_default_bom_for_item_having_single_bom(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1})
|
||||
bom_item = make_item(properties={"is_stock_item": 1})
|
||||
|
||||
# Step 1: Create BOM
|
||||
bom = frappe.new_doc("BOM")
|
||||
bom.item = fg_item.item_code
|
||||
bom.quantity = 1
|
||||
bom.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": bom_item.item_code,
|
||||
"qty": 1,
|
||||
"uom": bom_item.stock_uom,
|
||||
"stock_uom": bom_item.stock_uom,
|
||||
"rate": 100.0,
|
||||
},
|
||||
)
|
||||
bom.save()
|
||||
bom.submit()
|
||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||
|
||||
# Step 2: Uncheck is_active field
|
||||
bom.is_active = 0
|
||||
bom.save()
|
||||
bom.reload()
|
||||
self.assertIsNone(frappe.get_value("Item", fg_item.item_code, "default_bom"))
|
||||
|
||||
# Step 3: Check is_active field
|
||||
bom.is_active = 1
|
||||
bom.save()
|
||||
bom.reload()
|
||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -368,4 +368,5 @@ erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
erpnext.patches.v13_0.update_employee_advance_status
|
||||
erpnext.patches.v13_0.job_card_status_on_hold
|
||||
erpnext.patches.v13_0.add_cost_center_in_loans
|
||||
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
|
||||
erpnext.patches.v13_0.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,
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
try:
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
count += 1
|
||||
|
||||
@@ -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):
|
||||
inward_nil_exempt = frappe.db.sql(
|
||||
"""
|
||||
SELECT p.place_of_supply, p.supplier_address,
|
||||
SELECT p.name, p.place_of_supply, p.supplier_address, p.gst_category,
|
||||
i.base_amount, i.is_nil_exempt, i.is_non_gst
|
||||
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||
WHERE p.docstatus = 1 and p.name = i.parent
|
||||
|
||||
@@ -65,6 +65,7 @@ def validate_eligibility(doc):
|
||||
"SEZ",
|
||||
"Overseas",
|
||||
"Deemed Export",
|
||||
"UIN Holders",
|
||||
]
|
||||
company_transaction = doc.get("billing_address_gstin") == doc.get("company_gstin")
|
||||
|
||||
@@ -130,9 +131,7 @@ def read_json(name):
|
||||
|
||||
def get_transaction_details(invoice):
|
||||
supply_type = ""
|
||||
if (
|
||||
invoice.gst_category == "Registered Regular" or invoice.gst_category == "Registered Composition"
|
||||
):
|
||||
if invoice.gst_category in ("Registered Regular", "Registered Composition", "UIN Holders"):
|
||||
supply_type = "B2B"
|
||||
elif invoice.gst_category == "SEZ":
|
||||
if invoice.export_type == "Without Payment of Tax":
|
||||
@@ -148,15 +147,18 @@ def get_transaction_details(invoice):
|
||||
supply_type = "DEXP"
|
||||
|
||||
if not supply_type:
|
||||
rr, rc, sez, overseas, export = (
|
||||
rr, rc, sez, overseas, export, uin = (
|
||||
bold("Registered Regular"),
|
||||
bold("Registered Composition"),
|
||||
bold("SEZ"),
|
||||
bold("Overseas"),
|
||||
bold("Deemed Export"),
|
||||
bold("UIN Holders"),
|
||||
)
|
||||
frappe.throw(
|
||||
_("GST category should be one of {}, {}, {}, {}, {}").format(rr, rc, sez, overseas, export),
|
||||
_("GST category should be one of {}, {}, {}, {}, {}, {}").format(
|
||||
rr, rc, sez, overseas, export, uin
|
||||
),
|
||||
title=_("Invalid Supply Type"),
|
||||
)
|
||||
|
||||
@@ -272,45 +274,43 @@ def get_item_list(invoice):
|
||||
item.description = sanitize_for_json(d.item_name)
|
||||
|
||||
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(
|
||||
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
|
||||
)
|
||||
|
||||
if hide_discount_in_einvoice:
|
||||
if flt(item.qty) != 0.0:
|
||||
item.unit_rate = abs(item.taxable_value / item.qty)
|
||||
else:
|
||||
item.unit_rate = abs(item.taxable_value)
|
||||
item.gross_amount = abs(item.taxable_value)
|
||||
item.taxable_value = abs(item.taxable_value)
|
||||
item.unit_rate = item.taxable_value / item_qty
|
||||
item.gross_amount = item.taxable_value
|
||||
item.discount_amount = 0
|
||||
|
||||
else:
|
||||
if invoice.get("apply_discount_on") and (abs(invoice.get("base_discount_amount") or 0.0) > 0.0):
|
||||
# TODO: need to handle case when tax included in basic rate is checked.
|
||||
item.discount_amount = (item.discount_amount * item.qty) + (
|
||||
item.discount_amount = (item.discount_amount * item_qty) + (
|
||||
abs(item.base_amount) - abs(item.base_net_amount)
|
||||
)
|
||||
else:
|
||||
item.discount_amount = item.discount_amount * item.qty
|
||||
item.discount_amount = item.discount_amount * item_qty
|
||||
|
||||
if invoice.get("is_return") or invoice.get("is_debit_note"):
|
||||
item.unit_rate = (abs(item.taxable_value) + item.discount_amount) / (
|
||||
1 if (item.qty == 0) else item.qty
|
||||
try:
|
||||
item.unit_rate = (item.taxable_value + item.discount_amount) / item_qty
|
||||
except ZeroDivisionError:
|
||||
# This will never run but added as safety measure
|
||||
frappe.throw(
|
||||
title=_("Error: Qty is Zero"),
|
||||
msg=_("Quantity can't be zero unless it's Credit/Debit Note."),
|
||||
)
|
||||
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.taxable_value = abs(item.taxable_value)
|
||||
item.gross_amount = item.taxable_value + item.discount_amount
|
||||
item.taxable_value = item.taxable_value
|
||||
item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N"
|
||||
item.serial_no = ""
|
||||
|
||||
|
||||
@@ -714,7 +714,7 @@ def get_custom_fields():
|
||||
insert_after="customer",
|
||||
no_copy=1,
|
||||
print_hide=1,
|
||||
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0',
|
||||
depends_on='eval:in_list(["Registered Regular", "Registered Composition", "SEZ", "Overseas", "Deemed Export", "UIN Holders"], doc.gst_category) && doc.irn_cancelled === 0',
|
||||
),
|
||||
dict(
|
||||
fieldname="irn_cancelled",
|
||||
|
||||
@@ -40,13 +40,12 @@ def validate_gstin_for_india(doc, method):
|
||||
|
||||
gst_category = []
|
||||
|
||||
if hasattr(doc, "gst_category"):
|
||||
if len(doc.links):
|
||||
link_doctype = doc.links[0].get("link_doctype")
|
||||
link_name = doc.links[0].get("link_name")
|
||||
if len(doc.links):
|
||||
link_doctype = doc.links[0].get("link_doctype")
|
||||
link_name = doc.links[0].get("link_name")
|
||||
|
||||
if link_doctype in ["Customer", "Supplier"]:
|
||||
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
|
||||
if link_doctype in ["Customer", "Supplier"]:
|
||||
gst_category = frappe.db.get_value(link_doctype, {"name": link_name}, ["gst_category"])
|
||||
|
||||
doc.gstin = doc.gstin.upper().strip()
|
||||
if not doc.gstin or doc.gstin == "NA":
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
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.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}"
|
||||
)
|
||||
|
||||
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):
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
@@ -219,11 +274,11 @@ class TestLandedCostVoucher(FrappeTestCase):
|
||||
landed costs, this should be allowed for serial nos too.
|
||||
|
||||
Case:
|
||||
- receipt a serial no @ X rate
|
||||
- delivery the serial no @ X rate
|
||||
- add LCV to receipt X + Y
|
||||
- LCV should be successful
|
||||
- delivery should reflect X+Y valuation.
|
||||
- receipt a serial no @ X rate
|
||||
- delivery the serial no @ X rate
|
||||
- add LCV to receipt X + Y
|
||||
- LCV should be successful
|
||||
- delivery should reflect X+Y valuation.
|
||||
"""
|
||||
serial_no = "LCV_TEST_SR_NO"
|
||||
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"):
|
||||
args["posting_date"] = nowdate()
|
||||
|
||||
if 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
|
||||
update_entries_after(
|
||||
{
|
||||
"item_code": args.get("item_code"),
|
||||
"warehouse": args.get("warehouse"),
|
||||
"posting_date": args.get("posting_date"),
|
||||
"posting_time": args.get("posting_time"),
|
||||
"voucher_type": args.get("voucher_type"),
|
||||
"voucher_no": args.get("voucher_no"),
|
||||
"sle_id": args.get("name"),
|
||||
"creation": args.get("creation"),
|
||||
},
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
if not (args.get("is_cancelled") and via_landed_cost_voucher):
|
||||
# Reposts only current voucher SL Entries
|
||||
# Updates valuation rate, stock value, stock queue for current transaction
|
||||
update_entries_after(
|
||||
{
|
||||
"item_code": args.get("item_code"),
|
||||
"warehouse": args.get("warehouse"),
|
||||
"posting_date": args.get("posting_date"),
|
||||
"posting_time": args.get("posting_time"),
|
||||
"voucher_type": args.get("voucher_type"),
|
||||
"voucher_no": args.get("voucher_no"),
|
||||
"sle_id": args.get("name"),
|
||||
"creation": args.get("creation"),
|
||||
},
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -208,8 +207,14 @@ def repost_future_sle(
|
||||
allow_negative_stock=None,
|
||||
via_landed_cost_voucher=False,
|
||||
):
|
||||
if not args and voucher_type and voucher_no:
|
||||
args = get_items_to_be_repost(voucher_type, voucher_no, doc)
|
||||
if not args:
|
||||
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)
|
||||
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:
|
||||
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(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
|
||||
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
|
||||
order_by="creation asc",
|
||||
group_by="item_code, warehouse",
|
||||
)
|
||||
if not items_to_be_repost and voucher_type and voucher_no:
|
||||
items_to_be_repost = frappe.db.get_all(
|
||||
"Stock Ledger Entry",
|
||||
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
|
||||
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
|
||||
order_by="creation asc",
|
||||
group_by="item_code, warehouse",
|
||||
)
|
||||
|
||||
return items_to_be_repost
|
||||
|
||||
|
||||
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:
|
||||
return entries_to_fix
|
||||
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
|
||||
|
||||
def update_distinct_item_warehouses(self, dependant_sle):
|
||||
@@ -504,14 +514,6 @@ class update_entries_after(object):
|
||||
self.distinct_item_warehouses[key] = val
|
||||
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):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
|
||||
@@ -898,6 +898,7 @@ ERPNext Settings,ERPNext-Einstellungen,
|
||||
Earliest,Frühestens,
|
||||
Earnest Money,Anzahlung,
|
||||
Earning,Einkommen,
|
||||
Earnings & Deductions,Verdienste & Abzüge,
|
||||
Edit,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.",
|
||||
|
||||
|
Can't render this file because it is too large.
|
Reference in New Issue
Block a user