diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json index baab628b210..ea427aa7d80 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json @@ -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", diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py index e590beb0261..1a574edfe86 100644 --- a/erpnext/accounts/doctype/gl_entry/gl_entry.py +++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py @@ -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() diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index c46b185e60a..8cc20038770 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -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): diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js index 9f22ab0d76c..7eb5c4234d1 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js @@ -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])); + } + } }); }); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 2728c37d5b7..d8ce89c59d2 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -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: diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json index 84c941ecc10..54a76b34196 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json @@ -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", diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index 5a86376199c..866a94d04b1 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -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") diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index a944a373832..93869ed6c04 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -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'") diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index dcf09940ed0..7525369d4f6 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -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) diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py index 30ea5a9e4e1..16e0ac1de60 100644 --- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py +++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py @@ -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): diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 411e1efe8ea..04e9c32f379 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -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 -} +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 4d63a285534..1ce815ae2bd 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -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) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 9bfafaf8657..13475f34c39 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -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): diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index dce6d7525d0..4eee74b2cbd 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -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 diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py index 70830882efa..2ed963cf91f 100644 --- a/erpnext/controllers/subcontracting.py +++ b/erpnext/controllers/subcontracting.py @@ -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) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 386ac0a2888..cf1714a25e1 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -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", diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index d592a9c79e2..ed45e5288c6 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -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", diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index d24e7038422..8aaae5956b0 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -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") diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.json b/erpnext/hr/doctype/employee_checkin/employee_checkin.json index d34316dc0f3..19ef1b3c0ea 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.json +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.json @@ -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", diff --git a/erpnext/hr/doctype/employee_checkin/employee_checkin.py b/erpnext/hr/doctype/employee_checkin/employee_checkin.py index 58618b6358c..8b0a60e20e9 100644 --- a/erpnext/hr/doctype/employee_checkin/employee_checkin.py +++ b/erpnext/hr/doctype/employee_checkin/employee_checkin.py @@ -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, diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 89d86c1bc7c..ab0e62b378e 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -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" diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index 9b3d53a2105..62df1e02900 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -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 diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index ea32ba744d8..1cf44536b59 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -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 diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.json b/erpnext/hr/doctype/shift_assignment/shift_assignment.json index ce2a10f229f..c2df00be628 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.json +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.json @@ -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", diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py index 868be6ef719..483d0fb1c41 100644 --- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py +++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py @@ -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 diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py index 2000eeb5443..791c791ad07 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.py +++ b/erpnext/hr/doctype/shift_type/shift_type.py @@ -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() diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json index 61bbf712610..0e8feba2f12 100644 --- a/erpnext/loan_management/doctype/loan/loan.json +++ b/erpnext/loan_management/doctype/loan/loan.json @@ -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", diff --git a/erpnext/loan_management/doctype/loan_balance_adjustment/__init__.py b/erpnext/loan_management/doctype/loan_balance_adjustment/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.js b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.js new file mode 100644 index 00000000000..8aec63ad75f --- /dev/null +++ b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.js @@ -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) { + + // } +}); diff --git a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.json b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.json new file mode 100644 index 00000000000..80c3389ba14 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py new file mode 100644 index 00000000000..0a576d69692 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_balance_adjustment/loan_balance_adjustment.py @@ -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) diff --git a/erpnext/loan_management/doctype/loan_balance_adjustment/test_loan_balance_adjustment.py b/erpnext/loan_management/doctype/loan_balance_adjustment/test_loan_balance_adjustment.py new file mode 100644 index 00000000000..7658d7b215d --- /dev/null +++ b/erpnext/loan_management/doctype/loan_balance_adjustment/test_loan_balance_adjustment.py @@ -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 diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json index 30e2328442a..08dc98c8304 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json @@ -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 } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_refund/__init__.py b/erpnext/loan_management/doctype/loan_refund/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/loan_management/doctype/loan_refund/loan_refund.js b/erpnext/loan_management/doctype/loan_refund/loan_refund.js new file mode 100644 index 00000000000..f108bf7a281 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_refund/loan_refund.js @@ -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) { + + // } +}); diff --git a/erpnext/loan_management/doctype/loan_refund/loan_refund.json b/erpnext/loan_management/doctype/loan_refund/loan_refund.json new file mode 100644 index 00000000000..f78e55e9f94 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_refund/loan_refund.json @@ -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 +} \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_refund/loan_refund.py b/erpnext/loan_management/doctype/loan_refund/loan_refund.py new file mode 100644 index 00000000000..d7ab54ca974 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_refund/loan_refund.py @@ -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) diff --git a/erpnext/loan_management/doctype/loan_refund/test_loan_refund.py b/erpnext/loan_management/doctype/loan_refund/test_loan_refund.py new file mode 100644 index 00000000000..de2f9e13727 --- /dev/null +++ b/erpnext/loan_management/doctype/loan_refund/test_loan_refund.py @@ -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 diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index d3840bfb2e2..18803504eb1 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -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}.
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 diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json index 99b5c72b2db..d4007cb62dd 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json @@ -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 } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json index 828df2e35f7..7fc4736216d 100644 --- a/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json +++ b/erpnext/loan_management/doctype/process_loan_interest_accrual/process_loan_interest_accrual.json @@ -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 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index e9602312df9..dd6c802aab4 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -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) diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py index 5ea5d1b8a45..edb97937f02 100644 --- a/erpnext/manufacturing/doctype/bom/test_bom.py +++ b/erpnext/manufacturing/doctype/bom/test_bom.py @@ -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}) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index bdc43dd094a..8e8e2b25299 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -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 \ No newline at end of file +erpnext.patches.v13_0.show_india_localisation_deprecation_warning +erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation diff --git a/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py b/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py index 7e6e8200852..8e6bce66fe4 100644 --- a/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py +++ b/erpnext/patches/v13_0/create_accounting_dimensions_in_orders.py @@ -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 diff --git a/erpnext/patches/v13_0/fix_number_and_frequency_for_monthly_depreciation.py b/erpnext/patches/v13_0/fix_number_and_frequency_for_monthly_depreciation.py new file mode 100644 index 00000000000..fe645c81b88 --- /dev/null +++ b/erpnext/patches/v13_0/fix_number_and_frequency_for_monthly_depreciation.py @@ -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() diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py index be062fcff85..379579b904f 100644 --- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py @@ -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 diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index b0f837b51e7..bf304bc10dc 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -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 = "" diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 82d734d845c..5c3239913c7 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -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", diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 378f39a7e21..160eebca6e7 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -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": diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 1af99534516..790d284a3dc 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -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" diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py index 068cc12ed91..4e64b7b1833 100644 --- a/erpnext/stock/stock_ledger.py +++ b/erpnext/stock/stock_ledger.py @@ -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 diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index f99c8ec3765..4251b0cb3a5 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -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.",