diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js index 2fa1d53c60c..2f53f7b640d 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js @@ -15,6 +15,17 @@ frappe.ui.form.on('Accounting Dimension', { }; }); + frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) { + let d = locals[cdt][cdn]; + return { + filters: { + company: d.company, + root_type: ["in", ["Asset", "Liability"]], + is_group: 0 + } + } + }); + if (!frm.is_new()) { frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () { frappe.set_route("List", frm.doc.document_type); diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 15c84d462f1..cfe5e6e8009 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -39,6 +39,8 @@ class AccountingDimension(Document): if not self.is_new(): self.validate_document_type_change() + self.validate_dimension_defaults() + def validate_document_type_change(self): doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type") if doctype_before_save != self.document_type: @@ -46,6 +48,14 @@ class AccountingDimension(Document): message += _("Please create a new Accounting Dimension if required.") frappe.throw(message) + def validate_dimension_defaults(self): + companies = [] + for default in self.get("dimension_defaults"): + if default.company not in companies: + companies.append(default.company) + else: + frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company))) + def after_insert(self): if frappe.flags.in_test: make_dimension_in_accounting_doctypes(doc=self) diff --git a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json index e9e1f43f990..7b6120a583b 100644 --- a/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json +++ b/erpnext/accounts/doctype/accounting_dimension_detail/accounting_dimension_detail.json @@ -8,7 +8,10 @@ "reference_document", "default_dimension", "mandatory_for_bs", - "mandatory_for_pl" + "mandatory_for_pl", + "column_break_lqns", + "automatically_post_balancing_accounting_entry", + "offsetting_account" ], "fields": [ { @@ -50,6 +53,23 @@ "fieldtype": "Check", "in_list_view": 1, "label": "Mandatory For Profit and Loss Account" + }, + { + "default": "0", + "fieldname": "automatically_post_balancing_accounting_entry", + "fieldtype": "Check", + "label": "Automatically post balancing accounting entry" + }, + { + "fieldname": "offsetting_account", + "fieldtype": "Link", + "label": "Offsetting Account", + "mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry", + "options": "Account" + }, + { + "fieldname": "column_break_lqns", + "fieldtype": "Column Break" } ], "istable": 1, diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 302acc4f1f7..199068529d4 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry"); frappe.ui.form.on("Journal Entry", { setup: function(frm) { frm.add_fetch("bank_account", "account", "account"); - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger", 'Asset', 'Asset Movement']; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger', 'Asset', 'Asset Movement', 'Repost Accounting Ledger']; }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index f6898026134..4328635aaa6 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -96,6 +96,8 @@ class JournalEntry(AccountsController): "Payment Ledger Entry", "Repost Payment Ledger", "Repost Payment Ledger Items", + "Repost Accounting Ledger", + "Repost Accounting Ledger Items", ) self.make_gl_entries(1) self.update_advance_paid() diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 3c2fb1dd0ed..cd788a896a8 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges"; frappe.ui.form.on('Payment Entry', { onload: function(frm) { - frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Journal Entry", "Repost Payment Ledger"]; + frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger']; if(frm.doc.__islocal) { if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 379903dade3..127768071a0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -105,6 +105,8 @@ class PaymentEntry(AccountsController): "Payment Ledger Entry", "Repost Payment Ledger", "Repost Payment Ledger Items", + "Repost Accounting Ledger", + "Repost Accounting Ledger Items", ) super(PaymentEntry, self).on_cancel() self.make_gl_entries(cancel=1) @@ -185,84 +187,87 @@ class PaymentEntry(AccountsController): return False def validate_allocated_amount_with_latest_data(self): - latest_references = get_outstanding_reference_documents( - { - "posting_date": self.posting_date, - "company": self.company, - "party_type": self.party_type, - "payment_type": self.payment_type, - "party": self.party, - "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, - "get_outstanding_invoices": True, - "get_orders_to_be_billed": True, - } - ) + if self.references: + uniq_vouchers = set([(x.reference_doctype, x.reference_name) for x in self.references]) + vouchers = [frappe._dict({"voucher_type": x[0], "voucher_no": x[1]}) for x in uniq_vouchers] + latest_references = get_outstanding_reference_documents( + { + "posting_date": self.posting_date, + "company": self.company, + "party_type": self.party_type, + "payment_type": self.payment_type, + "party": self.party, + "party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to, + "get_outstanding_invoices": True, + "get_orders_to_be_billed": True, + "vouchers": vouchers, + } + ) - # Group latest_references by (voucher_type, voucher_no) - latest_lookup = {} - for d in latest_references: - d = frappe._dict(d) - latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d + # Group latest_references by (voucher_type, voucher_no) + latest_lookup = {} + for d in latest_references: + d = frappe._dict(d) + latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d - for idx, d in enumerate(self.get("references"), start=1): - latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() + for idx, d in enumerate(self.get("references"), start=1): + latest = latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict() - # If term based allocation is enabled, throw - if ( - d.payment_term is None or d.payment_term == "" - ) and self.term_based_allocation_enabled_for_reference( - d.reference_doctype, d.reference_name - ): - frappe.throw( - _( - "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" - ).format(frappe.bold(d.reference_name), frappe.bold(idx)) - ) - - # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key - latest = latest.get(d.payment_term) or latest.get(None) - - # The reference has already been fully paid - if not latest: - frappe.throw( - _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) - ) - # The reference has already been partly paid - elif latest.outstanding_amount < latest.invoice_amount and flt( - d.outstanding_amount, d.precision("outstanding_amount") - ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): - - frappe.throw( - _( - "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." - ).format(_(d.reference_doctype), d.reference_name) - ) - - fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.") - - if ( - d.payment_term - and ( - (flt(d.allocated_amount)) > 0 - and latest.payment_term_outstanding - and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) - ) - and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) - ): - frappe.throw( - _( - "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" - ).format( - d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term + # If term based allocation is enabled, throw + if ( + d.payment_term is None or d.payment_term == "" + ) and self.term_based_allocation_enabled_for_reference( + d.reference_doctype, d.reference_name + ): + frappe.throw( + _( + "{0} has Payment Term based allocation enabled. Select a Payment Term for Row #{1} in Payment References section" + ).format(frappe.bold(d.reference_name), frappe.bold(idx)) ) - ) - if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): - frappe.throw(fail_message.format(d.idx)) + # if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key + latest = latest.get(d.payment_term) or latest.get(None) - # Check for negative outstanding invoices as well - if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): - frappe.throw(fail_message.format(d.idx)) + # The reference has already been fully paid + if not latest: + frappe.throw( + _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) + ) + # The reference has already been partly paid + elif latest.outstanding_amount < latest.invoice_amount and flt( + d.outstanding_amount, d.precision("outstanding_amount") + ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")): + frappe.throw( + _( + "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." + ).format(_(d.reference_doctype), d.reference_name) + ) + + fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.") + + if ( + d.payment_term + and ( + (flt(d.allocated_amount)) > 0 + and latest.payment_term_outstanding + and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding)) + ) + and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name) + ): + frappe.throw( + _( + "Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}" + ).format( + d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term + ) + ) + + if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): + frappe.throw(fail_message.format(d.idx)) + + # Check for negative outstanding invoices as well + if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): + frappe.throw(fail_message.format(d.idx)) def delink_advance_entry_references(self): for reference in self.references: @@ -1463,6 +1468,7 @@ def get_outstanding_reference_documents(args): min_outstanding=args.get("outstanding_amt_greater_than"), max_outstanding=args.get("outstanding_amt_less_than"), accounting_dimensions=accounting_dimensions_filter, + vouchers=args.get("vouchers") or None, ) outstanding_invoices = split_invoices_based_on_payment_terms( diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index b6708ce24b1..cc3ec26066f 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -385,59 +385,6 @@ class PaymentReconciliation(Document): self.get_unreconciled_entries() - def make_difference_entry(self, row): - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Exchange Gain Or Loss" - journal_entry.company = self.company - journal_entry.posting_date = nowdate() - journal_entry.multi_currency = 1 - - party_account_currency = frappe.get_cached_value( - "Account", self.receivable_payable_account, "account_currency" - ) - difference_account_currency = frappe.get_cached_value( - "Account", row.difference_account, "account_currency" - ) - - # Account Currency has balance - dr_or_cr = "debit" if self.party_type == "Customer" else "credit" - reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit" - - journal_account = frappe._dict( - { - "account": self.receivable_payable_account, - "party_type": self.party_type, - "party": self.party, - "account_currency": party_account_currency, - "exchange_rate": 0, - "cost_center": erpnext.get_default_cost_center(self.company), - "reference_type": row.against_voucher_type, - "reference_name": row.against_voucher, - dr_or_cr: flt(row.difference_amount), - dr_or_cr + "_in_account_currency": 0, - } - ) - - journal_entry.append("accounts", journal_account) - - journal_account = frappe._dict( - { - "account": row.difference_account, - "account_currency": difference_account_currency, - "exchange_rate": 1, - "cost_center": erpnext.get_default_cost_center(self.company), - reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount), - reverse_dr_or_cr: flt(row.difference_amount), - } - ) - - journal_entry.append("accounts", journal_account) - - journal_entry.save() - journal_entry.submit() - - return journal_entry - def get_payment_details(self, row, dr_or_cr): return frappe._dict( { @@ -603,16 +550,6 @@ class PaymentReconciliation(Document): def reconcile_dr_cr_note(dr_cr_notes, company): - def get_difference_row(inv): - if inv.difference_amount != 0 and inv.difference_account: - difference_row = { - "account": inv.difference_account, - inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0, - reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0, - "cost_center": erpnext.get_default_cost_center(company), - } - return difference_row - for inv in dr_cr_notes: voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note" diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js index cced37589ba..720b549ccb3 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js @@ -130,6 +130,7 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex args: { "pos_profile": frm.pos_profile }, callback: ({ message: profile }) => { this.update_customer_groups_settings(profile?.customer_groups); + this.frm.set_value("company", profile?.company); }, }); } diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index aea801af9dd..0ff230bb18b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -54,6 +54,7 @@ class POSInvoice(SalesInvoice): self.validate_pos() self.validate_payment_amount() self.validate_loyalty_transaction() + self.validate_company_with_pos_company() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code @@ -370,6 +371,14 @@ class POSInvoice(SalesInvoice): if total_amount_in_payments and total_amount_in_payments < invoice_total: frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total)) + def validate_company_with_pos_company(self): + if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): + frappe.throw( + _("Company {} does not match with POS Profile Company {}").format( + self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company") + ) + ) + def validate_loyalty_transaction(self): if self.redeem_loyalty_points and ( not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center @@ -448,6 +457,7 @@ class POSInvoice(SalesInvoice): profile = {} if self.pos_profile: profile = frappe.get_doc("POS Profile", self.pos_profile) + self.company = profile.get("company") if not self.get("payments") and not for_validate: update_multi_mode_option(self, profile) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ab7884d5209..5c82cf99438 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger"]; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice', "Repost Payment Ledger", "Repost Accounting Ledger"]; if(!this.frm.doc.__islocal) { // show credit_to in print format diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index cefb502ede1..6161e5b36ee 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1422,6 +1422,8 @@ class PurchaseInvoice(BuyingController): "Repost Item Valuation", "Repost Payment Ledger", "Repost Payment Ledger Items", + "Repost Accounting Ledger", + "Repost Accounting Ledger Items", "Payment Ledger Entry", "Tax Withheld Vouchers", ) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index f60c83dcf5c..0f8e77952cf 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1771,23 +1771,101 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): 0, ) + def test_offsetting_entries_for_accounting_dimensions(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.report.trial_balance.test_trial_balance import ( + clear_dimension_defaults, + create_accounting_dimension, + disable_dimension, + ) -def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql( - """select account, debit, credit, posting_date - from `tabGL Entry` - where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s - order by posting_date asc, account asc""", - (voucher_no, posting_date), - as_dict=1, + create_account( + account_name="Offsetting", + company="_Test Company", + parent_account="Temporary Accounts - _TC", + ) + + create_accounting_dimension(company="_Test Company", offsetting_account="Offsetting - _TC") + + branch1 = frappe.new_doc("Branch") + branch1.branch = "Location 1" + branch1.insert(ignore_if_duplicate=True) + branch2 = frappe.new_doc("Branch") + branch2.branch = "Location 2" + branch2.insert(ignore_if_duplicate=True) + + pi = make_purchase_invoice( + company="_Test Company", + customer="_Test Supplier", + do_not_save=True, + do_not_submit=True, + rate=1000, + price_list_rate=1000, + qty=1, + ) + pi.branch = branch1.branch + pi.items[0].branch = branch2.branch + pi.save() + pi.submit() + + expected_gle = [ + ["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate(), branch2.branch], + ["Creditors - _TC", 0.0, 1000, nowdate(), branch1.branch], + ["Offsetting - _TC", 1000, 0.0, nowdate(), branch1.branch], + ["Offsetting - _TC", 0.0, 1000, nowdate(), branch2.branch], + ] + + check_gl_entries( + self, + pi.name, + expected_gle, + nowdate(), + voucher_type="Purchase Invoice", + additional_columns=["branch"], + ) + clear_dimension_defaults("Branch") + disable_dimension() + + +def check_gl_entries( + doc, + voucher_no, + expected_gle, + posting_date, + voucher_type="Purchase Invoice", + additional_columns=None, +): + gl = frappe.qb.DocType("GL Entry") + query = ( + frappe.qb.from_(gl) + .select(gl.account, gl.debit, gl.credit, gl.posting_date) + .where( + (gl.voucher_type == voucher_type) + & (gl.voucher_no == voucher_no) + & (gl.posting_date >= posting_date) + & (gl.is_cancelled == 0) + ) + .orderby(gl.posting_date, gl.account, gl.creation) ) + if additional_columns: + for col in additional_columns: + query = query.select(gl[col]) + + gl_entries = query.run(as_dict=True) + for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) doc.assertEqual(expected_gle[i][1], gle.debit) doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + if additional_columns: + j = 4 + for col in additional_columns: + doc.assertEqual(expected_gle[i][j], gle[col]) + j += 1 + def create_tax_witholding_category(category_name, company, account): from erpnext.accounts.utils import get_fiscal_year diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html new file mode 100644 index 00000000000..2dec8f753f2 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html @@ -0,0 +1,44 @@ + + + + + + {% for col in gl_columns%} + + {% endfor %} + + + + {% for col in gl_columns%} + + {% endfor %} + + +{% for gl in gl_data%} +{% if gl["old"]%} + +{% else %} + +{% endif %} + {% for col in gl_columns %} + + {% endfor %} + +{% endfor %} +
{{ col.label }}
+ {{ gl[col.fieldname] }} +
diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js new file mode 100644 index 00000000000..3a87a380d19 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.js @@ -0,0 +1,50 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Repost Accounting Ledger", { + setup: function(frm) { + frm.fields_dict['vouchers'].grid.get_field('voucher_type').get_query = function(doc) { + return { + filters: { + name: ['in', ['Purchase Invoice', 'Sales Invoice', 'Payment Entry', 'Journal Entry']], + } + } + } + + frm.fields_dict['vouchers'].grid.get_field('voucher_no').get_query = function(doc) { + if (doc.company) { + return { + filters: { + company: doc.company, + docstatus: 1 + } + } + } + } + }, + + refresh: function(frm) { + frm.add_custom_button(__('Show Preview'), () => { + frm.call({ + method: 'generate_preview', + doc: frm.doc, + freeze: true, + freeze_message: __('Generating Preview'), + callback: function(r) { + if (r && r.message) { + let content = r.message; + let opts = { + title: "Preview", + subtitle: "preview", + content: content, + print_settings: {orientation: "landscape"}, + columns: [], + data: [], + } + frappe.render_grid(opts); + } + } + }); + }); + } +}); diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json new file mode 100644 index 00000000000..8d56c9bb11d --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "format:ACC-REPOST-{#####}", + "creation": "2023-07-04 13:07:32.923675", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "column_break_vpup", + "delete_cancelled_entries", + "section_break_metl", + "vouchers", + "amended_from" + ], + "fields": [ + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Repost Accounting Ledger", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "vouchers", + "fieldtype": "Table", + "label": "Vouchers", + "options": "Repost Accounting Ledger Items" + }, + { + "fieldname": "column_break_vpup", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_metl", + "fieldtype": "Section Break" + }, + { + "default": "0", + "fieldname": "delete_cancelled_entries", + "fieldtype": "Check", + "label": "Delete Cancelled Ledger Entries" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2023-07-27 15:47:58.975034", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Accounting Ledger", + "naming_rule": "Expression", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py new file mode 100644 index 00000000000..4cf2ed2f46c --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.py @@ -0,0 +1,183 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +import frappe +from frappe import _, qb +from frappe.model.document import Document +from frappe.utils.data import comma_and + + +class RepostAccountingLedger(Document): + def __init__(self, *args, **kwargs): + super(RepostAccountingLedger, self).__init__(*args, **kwargs) + self._allowed_types = set( + ["Purchase Invoice", "Sales Invoice", "Payment Entry", "Journal Entry"] + ) + + def validate(self): + self.validate_vouchers() + self.validate_for_closed_fiscal_year() + self.validate_for_deferred_accounting() + + def validate_for_deferred_accounting(self): + sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"] + docs_with_deferred_revenue = frappe.db.get_all( + "Sales Invoice Item", + filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True}, + fields=["parent"], + as_list=1, + ) + + purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"] + docs_with_deferred_expense = frappe.db.get_all( + "Purchase Invoice Item", + filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1}, + fields=["parent"], + as_list=1, + ) + + if docs_with_deferred_revenue or docs_with_deferred_expense: + frappe.throw( + _("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format( + frappe.bold( + comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]) + ) + ) + ) + + def validate_for_closed_fiscal_year(self): + if self.vouchers: + latest_pcv = ( + frappe.db.get_all( + "Period Closing Voucher", + filters={"company": self.company}, + order_by="posting_date desc", + pluck="posting_date", + limit=1, + ) + or None + ) + if not latest_pcv: + return + + for vtype in self._allowed_types: + if names := [x.voucher_no for x in self.vouchers if x.voucher_type == vtype]: + latest_voucher = frappe.db.get_all( + vtype, + filters={"name": ["in", names]}, + pluck="posting_date", + order_by="posting_date desc", + limit=1, + )[0] + if latest_voucher and latest_pcv[0] >= latest_voucher: + frappe.throw(_("Cannot Resubmit Ledger entries for vouchers in Closed fiscal year.")) + + def validate_vouchers(self): + if self.vouchers: + # Validate voucher types + voucher_types = set([x.voucher_type for x in self.vouchers]) + if disallowed_types := voucher_types.difference(self._allowed_types): + frappe.throw( + _("{0} types are not allowed. Only {1} are.").format( + frappe.bold(comma_and(list(disallowed_types))), + frappe.bold(comma_and(list(self._allowed_types))), + ) + ) + + def get_existing_ledger_entries(self): + vouchers = [x.voucher_no for x in self.vouchers] + gl = qb.DocType("GL Entry") + existing_gles = ( + qb.from_(gl) + .select(gl.star) + .where((gl.voucher_no.isin(vouchers)) & (gl.is_cancelled == 0)) + .run(as_dict=True) + ) + self.gles = frappe._dict({}) + + for gle in existing_gles: + self.gles.setdefault((gle.voucher_type, gle.voucher_no), frappe._dict({})).setdefault( + "existing", [] + ).append(gle.update({"old": True})) + + def generate_preview_data(self): + self.gl_entries = [] + self.get_existing_ledger_entries() + for x in self.vouchers: + doc = frappe.get_doc(x.voucher_type, x.voucher_no) + if doc.doctype in ["Payment Entry", "Journal Entry"]: + gle_map = doc.build_gl_map() + else: + gle_map = doc.get_gl_entries() + + old_entries = self.gles.get((x.voucher_type, x.voucher_no)) + if old_entries: + self.gl_entries.extend(old_entries.existing) + self.gl_entries.extend(gle_map) + + @frappe.whitelist() + def generate_preview(self): + from erpnext.accounts.report.general_ledger.general_ledger import get_columns as get_gl_columns + + gl_columns = [] + gl_data = [] + + self.generate_preview_data() + if self.gl_entries: + filters = {"company": self.company, "include_dimensions": 1} + for x in get_gl_columns(filters): + if x["fieldname"] == "gl_entry": + x["fieldname"] = "name" + gl_columns.append(x) + + gl_data = self.gl_entries + rendered_page = frappe.render_template( + "erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.html", + {"gl_columns": gl_columns, "gl_data": gl_data}, + ) + + return rendered_page + + def on_submit(self): + job_name = "repost_accounting_ledger_" + self.name + frappe.enqueue( + method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost", + account_repost_doc=self.name, + is_async=True, + job_name=job_name, + ) + frappe.msgprint(_("Repost has started in the background")) + + +@frappe.whitelist() +def start_repost(account_repost_doc=str) -> None: + if account_repost_doc: + repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc) + + if repost_doc.docstatus == 1: + # Prevent repost on invoices with deferred accounting + repost_doc.validate_for_deferred_accounting() + + for x in repost_doc.vouchers: + doc = frappe.get_doc(x.voucher_type, x.voucher_no) + + if repost_doc.delete_cancelled_entries: + frappe.db.delete("GL Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name}) + frappe.db.delete( + "Payment Ledger Entry", filters={"voucher_type": doc.doctype, "voucher_no": doc.name} + ) + + if doc.doctype in ["Sales Invoice", "Purchase Invoice"]: + if not repost_doc.delete_cancelled_entries: + doc.docstatus = 2 + doc.make_gl_entries_on_cancel() + + doc.docstatus = 1 + doc.make_gl_entries() + + elif doc.doctype in ["Payment Entry", "Journal Entry"]: + if not repost_doc.delete_cancelled_entries: + doc.make_gl_entries(1) + doc.make_gl_entries() + + frappe.db.commit() diff --git a/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py new file mode 100644 index 00000000000..0e75dd2e3e1 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger/test_repost_accounting_ledger.py @@ -0,0 +1,202 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +import frappe +from frappe import qb +from frappe.query_builder.functions import Sum +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import add_days, nowdate, today + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request +from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import start_repost +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin +from erpnext.accounts.utils import get_fiscal_year + + +class TestRepostAccountingLedger(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.create_company() + self.create_customer() + self.create_item() + + def teadDown(self): + frappe.db.rollback() + + def test_01_basic_functions(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + ) + + preq = frappe.get_doc( + make_payment_request( + dt=si.doctype, + dn=si.name, + payment_request_type="Inward", + party_type="Customer", + party=si.customer, + ) + ) + preq.save().submit() + + # Test Validation Error + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.delete_cancelled_entries = True + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.append( + "vouchers", {"voucher_type": preq.doctype, "voucher_no": preq.name} + ) # this should throw validation error + self.assertRaises(frappe.ValidationError, ral.save) + ral.vouchers.pop() + preq.cancel() + preq.delete() + + pe = get_payment_entry(si.doctype, si.name) + pe.save().submit() + ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) + ral.save() + + # manually set an incorrect debit amount in DB + gle = frappe.db.get_all("GL Entry", filters={"voucher_no": si.name, "account": self.debit_to}) + frappe.db.set_value("GL Entry", gle[0], "debit", 90) + + gl = qb.DocType("GL Entry") + res = ( + qb.from_(gl) + .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit")) + .where((gl.voucher_no == si.name) & (gl.is_cancelled == 0)) + .run() + ) + + # Assert incorrect ledger balance + self.assertNotEqual(res[0], (si.name, 100, 100)) + + # Submit repost document + ral.save().submit() + + # background jobs don't run on test cases. Manually triggering repost function. + start_repost(ral.name) + + res = ( + qb.from_(gl) + .select(gl.voucher_no, Sum(gl.debit).as_("debit"), Sum(gl.credit).as_("credit")) + .where((gl.voucher_no == si.name) & (gl.is_cancelled == 0)) + .run() + ) + + # Ledger should reflect correct amount post repost + self.assertEqual(res[0], (si.name, 100, 100)) + + def test_02_deferred_accounting_valiations(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + do_not_submit=True, + ) + si.items[0].enable_deferred_revenue = True + si.items[0].deferred_revenue_account = self.deferred_revenue + si.items[0].service_start_date = nowdate() + si.items[0].service_end_date = add_days(nowdate(), 90) + si.save().submit() + + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + self.assertRaises(frappe.ValidationError, ral.save) + + @change_settings("Accounts Settings", {"delete_linked_ledger_entries": 1}) + def test_04_pcv_validation(self): + # Clear old GL entries so PCV can be submitted. + gl = frappe.qb.DocType("GL Entry") + qb.from_(gl).delete().where(gl.company == self.company).run() + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + ) + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": today(), + "posting_date": today(), + "company": self.company, + "fiscal_year": get_fiscal_year(today(), company=self.company)[0], + "cost_center": self.cost_center, + "closing_account_head": self.retained_earnings, + "remarks": "test", + } + ) + pcv.save().submit() + + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + self.assertRaises(frappe.ValidationError, ral.save) + + pcv.reload() + pcv.cancel() + pcv.delete() + + def test_03_deletion_flag_and_preview_function(self): + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=100, + ) + + pe = get_payment_entry(si.doctype, si.name) + pe.save().submit() + + # without deletion flag set + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.delete_cancelled_entries = False + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) + ral.save() + + # assert preview data is generated + preview = ral.generate_preview() + self.assertIsNotNone(preview) + + ral.save().submit() + + # background jobs don't run on test cases. Manually triggering repost function. + start_repost(ral.name) + + self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) + self.assertIsNotNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) + + # with deletion flag set + ral = frappe.new_doc("Repost Accounting Ledger") + ral.company = self.company + ral.delete_cancelled_entries = True + ral.append("vouchers", {"voucher_type": si.doctype, "voucher_no": si.name}) + ral.append("vouchers", {"voucher_type": pe.doctype, "voucher_no": pe.name}) + ral.save().submit() + + start_repost(ral.name) + self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": si.name, "is_cancelled": 1})) + self.assertIsNone(frappe.db.exists("GL Entry", {"voucher_no": pe.name, "is_cancelled": 1})) diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json new file mode 100644 index 00000000000..4a2041f88c6 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.json @@ -0,0 +1,40 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-07-04 14:14:01.243848", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_no" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher No", + "options": "voucher_type" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2023-07-04 14:15:51.165584", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Repost Accounting Ledger Items", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py new file mode 100644 index 00000000000..9221f447355 --- /dev/null +++ b/erpnext/accounts/doctype/repost_accounting_ledger_items/repost_accounting_ledger_items.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RepostAccountingLedgerItems(Document): + pass diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index df3db37bc65..d6977d39a9f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e super.onload(); this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', - 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger"]; + 'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"]; if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index fb60dd58724..5fc711d893b 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -714,6 +714,7 @@ "fieldtype": "Table", "hide_days": 1, "hide_seconds": 1, + "label": "Items", "oldfieldname": "entries", "oldfieldtype": "Table", "options": "Sales Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index ab629913cd4..3f9fe0441d1 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -399,6 +399,8 @@ class SalesInvoice(SellingController): "Repost Item Valuation", "Repost Payment Ledger", "Repost Payment Ledger Items", + "Repost Accounting Ledger", + "Repost Accounting Ledger Items", "Payment Ledger Entry", ) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 5d4035ee65e..ed6d9dbe026 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -28,6 +28,7 @@ def make_gl_entries( ): if gl_map: if not cancel: + make_acc_dimensions_offsetting_entry(gl_map) validate_accounting_period(gl_map) validate_disabled_accounts(gl_map) gl_map = process_gl_map(gl_map, merge_entries) @@ -51,6 +52,63 @@ def make_gl_entries( make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) +def make_acc_dimensions_offsetting_entry(gl_map): + accounting_dimensions_to_offset = get_accounting_dimensions_for_offsetting_entry( + gl_map, gl_map[0].company + ) + no_of_dimensions = len(accounting_dimensions_to_offset) + if no_of_dimensions == 0: + return + + offsetting_entries = [] + + for gle in gl_map: + for dimension in accounting_dimensions_to_offset: + offsetting_entry = gle.copy() + debit = flt(gle.credit) / no_of_dimensions if gle.credit != 0 else 0 + credit = flt(gle.debit) / no_of_dimensions if gle.debit != 0 else 0 + offsetting_entry.update( + { + "account": dimension.offsetting_account, + "debit": debit, + "credit": credit, + "debit_in_account_currency": debit, + "credit_in_account_currency": credit, + "remarks": _("Offsetting for Accounting Dimension") + " - {0}".format(dimension.name), + "against_voucher": None, + } + ) + offsetting_entry["against_voucher_type"] = None + offsetting_entries.append(offsetting_entry) + + gl_map += offsetting_entries + + +def get_accounting_dimensions_for_offsetting_entry(gl_map, company): + acc_dimension = frappe.qb.DocType("Accounting Dimension") + dimension_detail = frappe.qb.DocType("Accounting Dimension Detail") + + acc_dimensions = ( + frappe.qb.from_(acc_dimension) + .inner_join(dimension_detail) + .on(acc_dimension.name == dimension_detail.parent) + .select(acc_dimension.fieldname, acc_dimension.name, dimension_detail.offsetting_account) + .where( + (acc_dimension.disabled == 0) + & (dimension_detail.company == company) + & (dimension_detail.automatically_post_balancing_accounting_entry == 1) + ) + ).run(as_dict=True) + + accounting_dimensions_to_offset = [] + for acc_dimension in acc_dimensions: + values = set([entry.get(acc_dimension.fieldname) for entry in gl_map]) + if len(values) > 1: + accounting_dimensions_to_offset.append(acc_dimension) + + return accounting_dimensions_to_offset + + def validate_disabled_accounts(gl_map): accounts = [d.account for d in gl_map if d.account] diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 3aea40316b5..0c51e727c92 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -14,7 +14,7 @@ from frappe.contacts.doctype.address.address import ( from frappe.contacts.doctype.contact.contact import get_contact_details from frappe.core.doctype.user_permission.user_permission import get_permitted_documents from frappe.model.utils import get_fetch_values -from frappe.query_builder.functions import Date, Sum +from frappe.query_builder.functions import Abs, Date, Sum from frappe.utils import ( add_days, add_months, @@ -884,35 +884,34 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]: def get_partywise_advanced_payment_amount( - party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None + party_type, posting_date=None, future_payment=0, company=None, party=None ): - gle = frappe.qb.DocType("GL Entry") + ple = frappe.qb.DocType("Payment Ledger Entry") query = ( - frappe.qb.from_(gle) - .select(gle.party) + frappe.qb.from_(ple) + .select(ple.party, Abs(Sum(ple.amount).as_("amount"))) .where( - (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0) + (ple.party_type.isin(party_type)) + & (ple.amount < 0) + & (ple.against_voucher_no == ple.voucher_no) + & (ple.delinked == 0) ) - .groupby(gle.party) + .groupby(ple.party) ) - if account_type == "Receivable": - query = query.select(Sum(gle.credit).as_("amount")) - else: - query = query.select(Sum(gle.debit).as_("amount")) if posting_date: if future_payment: - query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date)) + query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date)) else: - query = query.where(gle.posting_date <= posting_date) + query = query.where(ple.posting_date <= posting_date) if company: - query = query.where(gle.company == company) + query = query.where(ple.company == company) if party: - query = query.where(gle.party == party) + query = query.where(ple.party == party) - data = query.run(as_dict=True) + data = query.run() if data: return frappe._dict(data) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index f78a84086a9..751063ad8e6 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -214,8 +214,8 @@ class ReceivablePayableReport(object): for party_type in self.party_type: if self.filters.get(scrub(party_type)): amount = ple.amount_in_account_currency - else: - amount = ple.amount + else: + amount = ple.amount amount_in_account_currency = ple.amount_in_account_currency # update voucher @@ -1090,7 +1090,10 @@ class ReceivablePayableReport(object): .where( (je.company == self.filters.company) & (je.posting_date.lte(self.filters.report_date)) - & (je.voucher_type == "Exchange Rate Revaluation") + & ( + (je.voucher_type == "Exchange Rate Revaluation") + | (je.voucher_type == "Exchange Gain Or Loss") + ) ) .run() ) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index da4c9dabbf6..cffc87895ef 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -50,13 +50,12 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.filters.show_future_payments, self.filters.company, party=party, - account_type=self.account_type, ) or {} ) if self.filters.show_gl_balance: - gl_balance_map = get_gl_balance(self.filters.report_date) + gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company) for party, party_dict in self.party_total.items(): if party_dict.outstanding == 0: @@ -233,12 +232,12 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.add_column(label="Total Amount Due", fieldname="total_due") -def get_gl_balance(report_date): +def get_gl_balance(report_date, company): return frappe._dict( frappe.db.get_all( "GL Entry", fields=["party", "sum(debit - credit)"], - filters={"posting_date": ("<=", report_date), "is_cancelled": 0}, + filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company}, group_by="party", as_list=1, ) diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py new file mode 100644 index 00000000000..3ee35a114d1 --- /dev/null +++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py @@ -0,0 +1,203 @@ +import unittest + +import frappe +from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils import today + +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute +from erpnext.accounts.test.accounts_mixin import AccountsTestMixin + + +class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase): + def setUp(self): + self.maxDiff = None + self.create_company() + self.create_customer() + self.create_item() + self.clear_old_entries() + + def tearDown(self): + frappe.db.rollback() + + def test_01_receivable_summary_output(self): + """ + Test for Invoices, Paid, Advance and Outstanding + """ + filters = { + "company": self.company, + "customer": self.customer, + "posting_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=200, + price_list_rate=200, + ) + + customer_group, customer_territory = frappe.db.get_all( + "Customer", + filters={"name": self.customer}, + fields=["customer_group", "territory"], + as_list=True, + )[0] + + report = execute(filters) + rpt_output = report[1] + expected_data = { + "party_type": "Customer", + "advance": 0, + "party": self.customer, + "invoiced": 200.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 200.0, + "range1": 200.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 200.0, + "future_amount": 0.0, + "sales_person": [], + "currency": si.currency, + "territory": customer_territory, + "customer_group": customer_group, + } + + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # simulate advance payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 50 + pe.references[0].allocated_amount = 0 # this essitially removes the reference + pe.save().submit() + + # update expected data with advance + expected_data.update( + { + "advance": 50.0, + "outstanding": 150.0, + "range1": 150.0, + "total_due": 150.0, + } + ) + + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # make partial payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 125 + pe.references[0].allocated_amount = 125 + pe.save().submit() + + # update expected data after advance and partial payment + expected_data.update( + {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0} + ) + + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + @change_settings("Selling Settings", {"cust_master_name": "Naming Series"}) + def test_02_various_filters_and_output(self): + filters = { + "company": self.company, + "customer": self.customer, + "posting_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + } + + si = create_sales_invoice( + item=self.item, + company=self.company, + customer=self.customer, + debit_to=self.debit_to, + posting_date=today(), + parent_cost_center=self.cost_center, + cost_center=self.cost_center, + rate=200, + price_list_rate=200, + ) + # make partial payment + pe = get_payment_entry(si.doctype, si.name) + pe.paid_amount = 150 + pe.references[0].allocated_amount = 150 + pe.save().submit() + + customer_group, customer_territory = frappe.db.get_all( + "Customer", + filters={"name": self.customer}, + fields=["customer_group", "territory"], + as_list=True, + )[0] + + report = execute(filters) + rpt_output = report[1] + expected_data = { + "party_type": "Customer", + "advance": 0, + "party": self.customer, + "party_name": self.customer, + "invoiced": 200.0, + "paid": 150.0, + "credit_note": 0.0, + "outstanding": 50.0, + "range1": 50.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 50.0, + "future_amount": 0.0, + "sales_person": [], + "currency": si.currency, + "territory": customer_territory, + "customer_group": customer_group, + } + + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # with gl balance filter + filters.update({"show_gl_balance": True}) + expected_data.update({"gl_balance": 50.0, "diff": 0.0}) + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # with gl balance and future payments filter + filters.update({"show_future_payments": True}) + expected_data.update({"remaining_balance": 50.0}) + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 1) + self.assertDictEqual(rpt_output[0], expected_data) + + # invoice fully paid + pe = get_payment_entry(si.doctype, si.name).save().submit() + report = execute(filters) + rpt_output = report[1] + self.assertEqual(len(rpt_output), 0) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index cdcf73f6620..fe4b6c71ebc 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -749,13 +749,18 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters, d): if from_date: additional_conditions.append(gle.posting_date >= from_date) - finance_book = filters.get("finance_book") - company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book") + finance_books = [] + finance_books.append("") + if filter_fb := filters.get("finance_book"): + finance_books.append(filter_fb) if filters.get("include_default_book_entries"): - additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None]))) + if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"): + finance_books.append(company_fb) + + additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull()) else: - additional_conditions.append((gle.finance_book.isin([finance_book, "", None]))) + additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull()) return additional_conditions diff --git a/erpnext/accounts/report/trial_balance/test_trial_balance.py b/erpnext/accounts/report/trial_balance/test_trial_balance.py new file mode 100644 index 00000000000..4682ac4500a --- /dev/null +++ b/erpnext/accounts/report/trial_balance/test_trial_balance.py @@ -0,0 +1,118 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.trial_balance.trial_balance import execute + + +class TestTrialBalance(FrappeTestCase): + def setUp(self): + from erpnext.accounts.doctype.account.test_account import create_account + from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + from erpnext.accounts.utils import get_fiscal_year + + self.company = create_company() + create_cost_center( + cost_center_name="Test Cost Center", + company="Trial Balance Company", + parent_cost_center="Trial Balance Company - TBC", + ) + create_account( + account_name="Offsetting", + company="Trial Balance Company", + parent_account="Temporary Accounts - TBC", + ) + self.fiscal_year = get_fiscal_year(today(), company="Trial Balance Company")[0] + create_accounting_dimension() + + def test_offsetting_entries_for_accounting_dimensions(self): + """ + Checks if Trial Balance Report is balanced when filtered using a particular Accounting Dimension + """ + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + frappe.db.sql("delete from `tabSales Invoice` where company='Trial Balance Company'") + frappe.db.sql("delete from `tabGL Entry` where company='Trial Balance Company'") + + branch1 = frappe.new_doc("Branch") + branch1.branch = "Location 1" + branch1.insert(ignore_if_duplicate=True) + branch2 = frappe.new_doc("Branch") + branch2.branch = "Location 2" + branch2.insert(ignore_if_duplicate=True) + + si = create_sales_invoice( + company=self.company, + debit_to="Debtors - TBC", + cost_center="Test Cost Center - TBC", + income_account="Sales - TBC", + do_not_submit=1, + ) + si.branch = "Location 1" + si.items[0].branch = "Location 2" + si.save() + si.submit() + + filters = frappe._dict( + {"company": self.company, "fiscal_year": self.fiscal_year, "branch": ["Location 1"]} + ) + total_row = execute(filters)[1][-1] + self.assertEqual(total_row["debit"], total_row["credit"]) + + def tearDown(self): + clear_dimension_defaults("Branch") + disable_dimension() + + +def create_company(**args): + args = frappe._dict(args) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": args.company_name or "Trial Balance Company", + "country": args.country or "India", + "default_currency": args.currency or "INR", + } + ) + company.insert(ignore_if_duplicate=True) + return company.name + + +def create_accounting_dimension(**args): + args = frappe._dict(args) + document_type = args.document_type or "Branch" + if frappe.db.exists("Accounting Dimension", document_type): + accounting_dimension = frappe.get_doc("Accounting Dimension", document_type) + accounting_dimension.disabled = 0 + else: + accounting_dimension = frappe.new_doc("Accounting Dimension") + accounting_dimension.document_type = document_type + accounting_dimension.insert() + + accounting_dimension.set("dimension_defaults", []) + accounting_dimension.append( + "dimension_defaults", + { + "company": args.company or "Trial Balance Company", + "automatically_post_balancing_accounting_entry": 1, + "offsetting_account": args.offsetting_account or "Offsetting - TBC", + }, + ) + accounting_dimension.save() + + +def disable_dimension(**args): + args = frappe._dict(args) + document_type = args.document_type or "Branch" + dimension = frappe.get_doc("Accounting Dimension", document_type) + dimension.disabled = 1 + dimension.save() + + +def clear_dimension_defaults(dimension_name): + accounting_dimension = frappe.get_doc("Accounting Dimension", dimension_name) + accounting_dimension.dimension_defaults = [] + accounting_dimension.save() diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py index c82164ef644..debfffdcbb3 100644 --- a/erpnext/accounts/test/accounts_mixin.py +++ b/erpnext/accounts/test/accounts_mixin.py @@ -1,10 +1,11 @@ import frappe +from frappe import qb from erpnext.stock.doctype.item.test_item import create_item class AccountsTestMixin: - def create_customer(self, customer_name, currency=None): + def create_customer(self, customer_name="_Test Customer", currency=None): if not frappe.db.exists("Customer", customer_name): customer = frappe.new_doc("Customer") customer.customer_name = customer_name @@ -17,7 +18,7 @@ class AccountsTestMixin: else: self.customer = customer_name - def create_supplier(self, supplier_name, currency=None): + def create_supplier(self, supplier_name="_Test Supplier", currency=None): if not frappe.db.exists("Supplier", supplier_name): supplier = frappe.new_doc("Supplier") supplier.supplier_name = supplier_name @@ -31,7 +32,7 @@ class AccountsTestMixin: else: self.supplier = supplier_name - def create_item(self, item_name, is_stock=0, warehouse=None, company=None): + def create_item(self, item_name="_Test Item", is_stock=0, warehouse=None, company=None): item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company) self.item = item.name @@ -62,19 +63,56 @@ class AccountsTestMixin: self.debit_usd = "Debtors USD - " + abbr self.cash = "Cash - " + abbr self.creditors = "Creditors - " + abbr + self.retained_earnings = "Retained Earnings - " + abbr - # create bank account - bank_account = "HDFC - " + abbr - if frappe.db.exists("Account", bank_account): - self.bank = bank_account - else: - bank_acc = frappe.get_doc( + # Deferred revenue, expense and bank accounts + other_accounts = [ + frappe._dict( { - "doctype": "Account", + "attribute_name": "deferred_revenue", + "account_name": "Deferred Revenue", + "parent_account": "Current Liabilities - " + abbr, + } + ), + frappe._dict( + { + "attribute_name": "deferred_expense", + "account_name": "Deferred Expense", + "parent_account": "Current Assets - " + abbr, + } + ), + frappe._dict( + { + "attribute_name": "bank", "account_name": "HDFC", "parent_account": "Bank Accounts - " + abbr, - "company": self.company, } - ) - bank_acc.save() - self.bank = bank_acc.name + ), + ] + for acc in other_accounts: + acc_name = acc.account_name + " - " + abbr + if frappe.db.exists("Account", acc_name): + setattr(self, acc.attribute_name, acc_name) + else: + new_acc = frappe.get_doc( + { + "doctype": "Account", + "account_name": acc.account_name, + "parent_account": acc.parent_account, + "company": self.company, + } + ) + new_acc.save() + setattr(self, acc.attribute_name, new_acc.name) + + def clear_old_entries(self): + doctype_list = [ + "GL Entry", + "Payment Ledger Entry", + "Sales Invoice", + "Purchase Invoice", + "Payment Entry", + "Journal Entry", + ] + for doctype in doctype_list: + qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 3e06a36e67e..2df3387b83e 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -884,6 +884,7 @@ def get_outstanding_invoices( min_outstanding=None, max_outstanding=None, accounting_dimensions=None, + vouchers=None, ): ple = qb.DocType("Payment Ledger Entry") @@ -909,6 +910,7 @@ def get_outstanding_invoices( ple_query = QueryPaymentLedger() invoice_list = ple_query.get_voucher_outstandings( + vouchers=vouchers, common_filter=common_filter, posting_date=posting_date, min_outstanding=min_outstanding, diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index f4a1e3cc190..34d5430210c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -81,18 +81,27 @@ class Asset(AccountsController): _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) ) - def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None): + def prepare_depreciation_data( + self, + date_of_disposal=None, + date_of_return=None, + value_after_depreciation=None, + ignore_booked_entry=False, + ): if self.calculate_depreciation: self.value_after_depreciation = 0 self.set_depreciation_rate() if self.should_prepare_depreciation_schedule(): - self.make_depreciation_schedule(date_of_disposal) - self.set_accumulated_depreciation(date_of_disposal, date_of_return) + self.make_depreciation_schedule(date_of_disposal, value_after_depreciation) + self.set_accumulated_depreciation(date_of_disposal, date_of_return, ignore_booked_entry) else: self.finance_books = [] - self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( - self.opening_accumulated_depreciation - ) + if value_after_depreciation: + self.value_after_depreciation = value_after_depreciation + else: + self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( + self.opening_accumulated_depreciation + ) def should_prepare_depreciation_schedule(self): if not self.get("schedules"): @@ -285,7 +294,7 @@ class Asset(AccountsController): self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") ) - def make_depreciation_schedule(self, date_of_disposal): + def make_depreciation_schedule(self, date_of_disposal, value_after_depreciation=None): if not self.get("schedules"): self.schedules = [] @@ -295,24 +304,30 @@ class Asset(AccountsController): start = self.clear_depreciation_schedule() for finance_book in self.get("finance_books"): - self._make_depreciation_schedule(finance_book, start, date_of_disposal) + self._make_depreciation_schedule( + finance_book, start, date_of_disposal, value_after_depreciation + ) if len(self.get("finance_books")) > 1 and any(start): self.sort_depreciation_schedule() - def _make_depreciation_schedule(self, finance_book, start, date_of_disposal): + def _make_depreciation_schedule( + self, finance_book, start, date_of_disposal, value_after_depreciation=None + ): self.validate_asset_finance_books(finance_book) - value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book) + if not value_after_depreciation: + value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(finance_book) + finance_book.value_after_depreciation = value_after_depreciation - number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint( + final_number_of_depreciations = cint(finance_book.total_number_of_depreciations) - cint( self.number_of_depreciations_booked ) has_pro_rata = self.check_is_pro_rata(finance_book) if has_pro_rata: - number_of_pending_depreciations += 1 + final_number_of_depreciations += 1 has_wdv_or_dd_non_yearly_pro_rata = False if ( @@ -328,7 +343,9 @@ class Asset(AccountsController): depreciation_amount = 0 - for n in range(start[finance_book.idx - 1], number_of_pending_depreciations): + number_of_pending_depreciations = final_number_of_depreciations - start[finance_book.idx - 1] + + for n in range(start[finance_book.idx - 1], final_number_of_depreciations): # If depreciation is already completed (for double declining balance) if skip_row: continue @@ -345,10 +362,11 @@ class Asset(AccountsController): n, prev_depreciation_amount, has_wdv_or_dd_non_yearly_pro_rata, + number_of_pending_depreciations, ) if not has_pro_rata or ( - n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2 + n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2 ): schedule_date = add_months( finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation) @@ -416,7 +434,7 @@ class Asset(AccountsController): ) # For last row - elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: + elif has_pro_rata and n == cint(final_number_of_depreciations) - 1: if not self.flags.increase_in_asset_life: # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission self.to_date = add_months( @@ -447,7 +465,7 @@ class Asset(AccountsController): # Adjust depreciation amount in the last period based on the expected value after useful life if finance_book.expected_value_after_useful_life and ( ( - n == cint(number_of_pending_depreciations) - 1 + n == cint(final_number_of_depreciations) - 1 and value_after_depreciation != finance_book.expected_value_after_useful_life ) or value_after_depreciation < finance_book.expected_value_after_useful_life @@ -690,7 +708,10 @@ class Asset(AccountsController): if s.finance_book_id == d.finance_book_id and (s.depreciation_method == "Straight Line" or s.depreciation_method == "Manual") ] - accumulated_depreciation = flt(self.opening_accumulated_depreciation) + if i > 0 and self.flags.decrease_in_asset_value_due_to_value_adjustment: + accumulated_depreciation = self.get("schedules")[i - 1].accumulated_depreciation_amount + else: + accumulated_depreciation = flt(self.opening_accumulated_depreciation) value_after_depreciation = flt( self.get("finance_books")[cint(d.finance_book_id) - 1].value_after_depreciation ) @@ -1296,11 +1317,14 @@ def get_depreciation_amount( schedule_idx=0, prev_depreciation_amount=0, has_wdv_or_dd_non_yearly_pro_rata=False, + number_of_pending_depreciations=0, ): frappe.flags.company = asset.company if fb_row.depreciation_method in ("Straight Line", "Manual"): - return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx) + return get_straight_line_or_manual_depr_amount( + asset, fb_row, schedule_idx, number_of_pending_depreciations + ) else: rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd( asset, depreciable_value, fb_row @@ -1320,7 +1344,9 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb return fb_row.rate_of_depreciation -def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): +def get_straight_line_or_manual_depr_amount( + asset, row, schedule_idx, number_of_pending_depreciations +): # if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value if asset.flags.increase_in_asset_life: return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / ( @@ -1331,6 +1357,36 @@ def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx): return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt( row.total_number_of_depreciations ) + # if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value + elif asset.flags.decrease_in_asset_value_due_to_value_adjustment: + if row.daily_depreciation: + daily_depr_amount = ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / date_diff( + add_months( + row.depreciation_start_date, + flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked) + * row.frequency_of_depreciation, + ), + add_months( + row.depreciation_start_date, + flt( + row.total_number_of_depreciations + - asset.number_of_depreciations_booked + - number_of_pending_depreciations + ) + * row.frequency_of_depreciation, + ), + ) + to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation) + from_date = add_months( + row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation + ) + return daily_depr_amount * date_diff(to_date, from_date) + else: + return ( + flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life) + ) / number_of_pending_depreciations # if the Depreciation Schedule is being prepared for the first time else: if row.daily_depreciation: diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 9928b2f5f38..29e7a9bdfd6 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,15 +5,12 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint, date_diff, flt, formatdate, getdate +from frappe.utils import flt, formatdate, getdate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_checks_for_pl_and_bs_accounts, ) -from erpnext.assets.doctype.asset.asset import ( - get_asset_value_after_depreciation, - get_depreciation_amount, -) +from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts @@ -25,10 +22,10 @@ class AssetValueAdjustment(Document): def on_submit(self): self.make_depreciation_entry() - self.reschedule_depreciations(self.new_asset_value) + self.update_asset(self.new_asset_value) def on_cancel(self): - self.reschedule_depreciations(self.current_asset_value) + self.update_asset(self.current_asset_value) def validate_date(self): asset_purchase_date = frappe.db.get_value("Asset", self.asset, "purchase_date") @@ -71,12 +68,16 @@ class AssetValueAdjustment(Document): "account": accumulated_depreciation_account, "credit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center, + "reference_type": "Asset", + "reference_name": asset.name, } debit_entry = { "account": depreciation_expense_account, "debit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center, + "reference_type": "Asset", + "reference_name": asset.name, } accounting_dimensions = get_checks_for_pl_and_bs_accounts() @@ -106,44 +107,11 @@ class AssetValueAdjustment(Document): self.db_set("journal_entry", je.name) - def reschedule_depreciations(self, asset_value): + def update_asset(self, asset_value): asset = frappe.get_doc("Asset", self.asset) - country = frappe.get_value("Company", self.company, "country") - for d in asset.finance_books: - d.value_after_depreciation = asset_value + asset.flags.decrease_in_asset_value_due_to_value_adjustment = True - if d.depreciation_method in ("Straight Line", "Manual"): - end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx) - total_days = date_diff(end_date, self.date) - rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt( - total_days - ) - from_date = self.date - else: - no_of_depreciations = len( - [ - s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry) - ] - ) - - value_after_depreciation = d.value_after_depreciation - for data in asset.schedules: - if cint(data.finance_book_id) == d.idx and not data.journal_entry: - if d.depreciation_method in ("Straight Line", "Manual"): - days = date_diff(data.schedule_date, from_date) - depreciation_amount = days * rate_per_day - from_date = data.schedule_date - else: - depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d) - - if depreciation_amount: - value_after_depreciation -= flt(depreciation_amount) - data.depreciation_amount = depreciation_amount - - d.db_update() - - asset.set_accumulated_depreciation(ignore_booked_entry=True) - for asset_data in asset.schedules: - if not asset_data.journal_entry: - asset_data.db_update() + asset.prepare_depreciation_data(value_after_depreciation=asset_value, ignore_booked_entry=True) + asset.flags.ignore_validate_update_after_submit = True + asset.save() diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index b2aa3958080..977a9b3714b 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -4,9 +4,10 @@ import unittest import frappe -from frappe.utils import add_days, get_last_day, nowdate +from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation +from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.test_asset import create_asset_data from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt @@ -46,40 +47,44 @@ class TestAssetValueAdjustment(unittest.TestCase): def test_asset_depreciation_value_adjustment(self): pr = make_purchase_receipt( - item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location" + item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location" ) asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name") asset_doc = frappe.get_doc("Asset", asset_name) asset_doc.calculate_depreciation = 1 - month_end_date = get_last_day(nowdate()) - purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - - asset_doc.available_for_use_date = purchase_date - asset_doc.purchase_date = purchase_date + asset_doc.available_for_use_date = "2023-01-15" + asset_doc.purchase_date = "2023-01-15" asset_doc.calculate_depreciation = 1 asset_doc.append( "finance_books", { "expected_value_after_useful_life": 200, "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date, + "total_number_of_depreciations": 12, + "frequency_of_depreciation": 1, + "depreciation_start_date": "2023-01-31", }, ) asset_doc.submit() + post_depreciation_entries(getdate("2023-08-21")) + current_value = get_asset_value_after_depreciation(asset_doc.name) adj_doc = make_asset_value_adjustment( - asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 + asset=asset_doc.name, + current_asset_value=current_value, + new_asset_value=50000.0, + date="2023-08-21", ) adj_doc.submit() + asset_doc.reload() + expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0), - ("_Test Depreciations - _TC", 50000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29), + ("_Test Depreciations - _TC", 4625.29, 0.0), ) gle = frappe.db.sql( @@ -91,6 +96,29 @@ class TestAssetValueAdjustment(unittest.TestCase): self.assertSequenceEqual(gle, expected_gle) + expected_schedules = [ + ["2023-01-31", 5474.73, 5474.73], + ["2023-02-28", 9983.33, 15458.06], + ["2023-03-31", 9983.33, 25441.39], + ["2023-04-30", 9983.33, 35424.72], + ["2023-05-31", 9983.33, 45408.05], + ["2023-06-30", 9983.33, 55391.38], + ["2023-07-31", 9983.33, 65374.71], + ["2023-08-31", 8300.0, 73674.71], + ["2023-09-30", 8300.0, 81974.71], + ["2023-10-31", 8300.0, 90274.71], + ["2023-11-30", 8300.0, 98574.71], + ["2023-12-31", 8300.0, 106874.71], + ["2024-01-15", 8300.0, 115174.71], + ] + + schedules = [ + [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset_doc.get("schedules") + ] + + self.assertEqual(schedules, expected_schedules) + def make_asset_value_adjustment(**args): args = frappe._dict(args) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 2f0b7862a82..30abad528bf 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -245,19 +245,21 @@ frappe.ui.form.on("Request for Quotation",{ ] }); - dialog.fields_dict['supplier'].df.onchange = () => { - var supplier = dialog.get_value('supplier'); - frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => { + dialog.fields_dict["supplier"].df.onchange = () => { + frm.call("get_supplier_email_preview", { + supplier: dialog.get_value("supplier"), + }).then(({ message }) => { dialog.fields_dict.email_preview.$wrapper.empty(); - dialog.fields_dict.email_preview.$wrapper.append(result.message); + dialog.fields_dict.email_preview.$wrapper.append( + message.message + ); + dialog.set_value("subject", message.subject); }); - - } + }; dialog.fields_dict.note.$wrapper.append(`

This is a preview of the email to be sent. A PDF of the document will automatically be attached with the email.

`); - dialog.set_value("subject", frm.doc.subject); dialog.show(); } }) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json index c16abb2f41b..fbfc1ac1693 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json @@ -20,11 +20,10 @@ "items_section", "items", "supplier_response_section", - "salutation", - "subject", - "col_break_email_1", "email_template", "preview", + "col_break_email_1", + "html_llwp", "send_attached_files", "sec_break_email_2", "message_for_supplier", @@ -237,23 +236,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fetch_from": "email_template.subject", - "fetch_if_empty": 1, - "fieldname": "subject", - "fieldtype": "Data", - "label": "Subject", - "print_hide": 1 - }, - { - "description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.", - "fieldname": "salutation", - "fieldtype": "Link", - "label": "Salutation", - "no_copy": 1, - "options": "Salutation", - "print_hide": 1 - }, { "fieldname": "col_break_email_1", "fieldtype": "Column Break" @@ -287,6 +269,14 @@ "fieldtype": "Data", "label": "Named Place" }, + { + "fieldname": "html_llwp", + "fieldtype": "HTML", + "options": "

In your Email Template, you can use the following special variables:\n

\n\n

\n

Apart from these, you can access all values in this RFQ, like {{ message_for_supplier }} or {{ terms }}.

", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, { "default": "1", "description": "If enabled, all files attached to this document will be attached to each email", @@ -299,7 +289,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2023-07-27 16:41:48.468873", + "modified": "2023-08-08 16:30:10.870429", "modified_by": "Administrator", "module": "Buying", "name": "Request for Quotation", diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index 63e393aecd6..56840c11a6e 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -182,35 +182,28 @@ class RequestforQuotation(BuyingController): if full_name == "Guest": full_name = "Administrator" - # send document dict and some important data from suppliers row - # to render message_for_supplier from any template doc_args = self.as_dict() - doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")}) - # Get Contact Full Name - supplier_name = None if data.get("contact"): - contact_name = frappe.db.get_value( - "Contact", data.get("contact"), ["first_name", "middle_name", "last_name"] - ) - supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values + contact = frappe.get_doc("Contact", data.get("contact")) + doc_args["contact"] = contact.as_dict() - args = { - "update_password_link": update_password_link, - "message": frappe.render_template(self.message_for_supplier, doc_args), - "rfq_link": rfq_link, - "user_fullname": full_name, - "supplier_name": supplier_name or data.get("supplier_name"), - "supplier_salutation": self.salutation or "Dear Mx.", - } - - subject = self.subject or _("Request for Quotation") - template = "templates/emails/request_for_quotation.html" + doc_args.update( + { + "supplier": data.get("supplier"), + "supplier_name": data.get("supplier_name"), + "update_password_link": f'{_("Set Password")}', + "portal_link": f' {_("Submit your Quotation")} ', + "user_fullname": full_name, + } + ) + email_template = frappe.get_doc("Email Template", self.email_template) + message = frappe.render_template(email_template.response_, doc_args) + subject = frappe.render_template(email_template.subject, doc_args) sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None - message = frappe.get_template(template).render(args) if preview: - return message + return {"message": message, "subject": subject} attachments = None if self.send_attached_files: diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 71019e80377..a7e03c08fac 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -154,31 +154,35 @@ def get_data(filters): procurement_record = [] if procurement_record_against_mr: procurement_record += procurement_record_against_mr + for po in purchase_order_entry: # fetch material records linked to the purchase order item - mr_record = mr_records.get(po.material_request_item, [{}])[0] - procurement_detail = { - "material_request_date": mr_record.get("transaction_date"), - "cost_center": po.cost_center, - "project": po.project, - "requesting_site": po.warehouse, - "requestor": po.owner, - "material_request_no": po.material_request, - "item_code": po.item_code, - "quantity": flt(po.qty), - "unit_of_measurement": po.stock_uom, - "status": po.status, - "purchase_order_date": po.transaction_date, - "purchase_order": po.parent, - "supplier": po.supplier, - "estimated_cost": flt(mr_record.get("amount")), - "actual_cost": flt(pi_records.get(po.name)), - "purchase_order_amt": flt(po.amount), - "purchase_order_amt_in_company_currency": flt(po.base_amount), - "expected_delivery_date": po.schedule_date, - "actual_delivery_date": pr_records.get(po.name), - } - procurement_record.append(procurement_detail) + material_requests = mr_records.get(po.material_request_item, [{}]) + + for mr_record in material_requests: + procurement_detail = { + "material_request_date": mr_record.get("transaction_date"), + "cost_center": po.cost_center, + "project": po.project, + "requesting_site": po.warehouse, + "requestor": po.owner, + "material_request_no": po.material_request, + "item_code": po.item_code, + "quantity": flt(po.qty), + "unit_of_measurement": po.stock_uom, + "status": po.status, + "purchase_order_date": po.transaction_date, + "purchase_order": po.parent, + "supplier": po.supplier, + "estimated_cost": flt(mr_record.get("amount")), + "actual_cost": flt(pi_records.get(po.name)), + "purchase_order_amt": flt(po.amount), + "purchase_order_amt_in_company_currency": flt(po.base_amount), + "expected_delivery_date": po.schedule_date, + "actual_delivery_date": pr_records.get(po.name), + } + procurement_record.append(procurement_detail) + return procurement_record @@ -301,7 +305,7 @@ def get_po_entries(filters): & (parent.name == child.parent) & (parent.status.notin(("Closed", "Completed", "Cancelled"))) ) - .groupby(parent.name, child.item_code) + .groupby(parent.name, child.material_request_item) ) query = apply_filters_on_query(filters, parent, child, query) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 7afd80b4bcf..76fe6a91182 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -715,7 +715,9 @@ class AccountsController(TransactionBase): def validate_enabled_taxes_and_charges(self): taxes_and_charges_doctype = self.meta.get_options("taxes_and_charges") - if frappe.get_cached_value(taxes_and_charges_doctype, self.taxes_and_charges, "disabled"): + if self.taxes_and_charges and frappe.get_cached_value( + taxes_and_charges_doctype, self.taxes_and_charges, "disabled" + ): frappe.throw( _("{0} '{1}' is disabled").format(taxes_and_charges_doctype, self.taxes_and_charges) ) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 8d75c3cb60d..db4003bc585 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -347,7 +347,7 @@ class ProductionPlan(Document): if not data.pending_qty: continue - item_details = get_item_details(data.item_code) + item_details = get_item_details(data.item_code, throw=False) if self.combine_items: if item_details.bom_no in refs: refs[item_details.bom_no]["so_details"].append( @@ -795,6 +795,9 @@ class ProductionPlan(Document): if not row.item_code: frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx)) + if not row.bom_no: + frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx)) + bom_data = [] warehouse = row.warehouse if self.skip_available_sub_assembly_item else None diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 92678e44c84..c4b6846376f 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -1075,7 +1075,7 @@ def get_bom_operations(doctype, txt, searchfield, start, page_len, filters): @frappe.whitelist() -def get_item_details(item, project=None, skip_bom_info=False): +def get_item_details(item, project=None, skip_bom_info=False, throw=True): res = frappe.db.sql( """ select stock_uom, description, item_name, allow_alternative_item, @@ -1111,12 +1111,15 @@ def get_item_details(item, project=None, skip_bom_info=False): if not res["bom_no"]: if project: - res = get_item_details(item) + res = get_item_details(item, throw=throw) frappe.msgprint( _("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1 ) else: - frappe.throw(_("Default BOM for {0} not found").format(item)) + msg = _("Default BOM for {0} not found").format(item) + frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw)) + + return res bom_data = frappe.db.get_value( "BOM", diff --git a/erpnext/projects/report/billing_summary.py b/erpnext/projects/report/billing_summary.py index bc8f2afb8c9..ac1524a49dd 100644 --- a/erpnext/projects/report/billing_summary.py +++ b/erpnext/projects/report/billing_summary.py @@ -98,9 +98,11 @@ def get_timesheets(filters): record_filters = [ ["start_date", "<=", filters.to_date], ["end_date", ">=", filters.from_date], - ["docstatus", "=", 1], ] - + if not filters.get("include_draft_timesheets"): + record_filters.append(["docstatus", "=", 1]) + else: + record_filters.append(["docstatus", "!=", 2]) if "employee" in filters: record_filters.append(["employee", "=", filters.employee]) diff --git a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js index 13f49ed6bed..9c904c57872 100644 --- a/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js +++ b/erpnext/projects/report/employee_billing_summary/employee_billing_summary.js @@ -25,5 +25,10 @@ frappe.query_reports["Employee Billing Summary"] = { default: frappe.datetime.add_days(frappe.datetime.month_start(), -1), reqd: 1 }, + { + fieldname:"include_draft_timesheets", + label: __("Include Timesheets in Draft Status"), + fieldtype: "Check", + }, ] } diff --git a/erpnext/projects/report/project_billing_summary/project_billing_summary.js b/erpnext/projects/report/project_billing_summary/project_billing_summary.js index caac1d86b45..6a6f3677e3f 100644 --- a/erpnext/projects/report/project_billing_summary/project_billing_summary.js +++ b/erpnext/projects/report/project_billing_summary/project_billing_summary.js @@ -25,5 +25,10 @@ frappe.query_reports["Project Billing Summary"] = { default: frappe.datetime.add_days(frappe.datetime.month_start(),-1), reqd: 1 }, + { + fieldname:"include_draft_timesheets", + label: __("Include Timesheets in Draft Status"), + fieldtype: "Check", + }, ] } diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js index 1271e38049a..ddc10530581 100644 --- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js +++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js @@ -117,6 +117,9 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager { name: __("Document Name"), editable: false, width: 1, + format: (value, row) => { + return frappe.form.formatters.Link(value, {options: row[2].content}); + }, }, { name: __("Reference Date"), diff --git a/erpnext/public/js/controllers/stock_controller.js b/erpnext/public/js/controllers/stock_controller.js index d346357a8f8..2674df9c4af 100644 --- a/erpnext/public/js/controllers/stock_controller.js +++ b/erpnext/public/js/controllers/stock_controller.js @@ -57,7 +57,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con from_date: me.frm.doc.posting_date, to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), company: me.frm.doc.company, - show_cancelled_entries: me.frm.doc.docstatus === 2 + show_cancelled_entries: me.frm.doc.docstatus === 2, + ignore_prepared_report: true }; frappe.set_route("query-report", "Stock Ledger"); }, __("View")); @@ -75,7 +76,8 @@ erpnext.stock.StockController = class StockController extends frappe.ui.form.Con to_date: moment(me.frm.doc.modified).format('YYYY-MM-DD'), company: me.frm.doc.company, group_by: "Group by Voucher (Consolidated)", - show_cancelled_entries: me.frm.doc.docstatus === 2 + show_cancelled_entries: me.frm.doc.docstatus === 2, + ignore_prepared_report: true }; frappe.set_route("query-report", "General Ledger"); }, __("View")); diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 159fd32c123..bfb0d4e1853 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -660,7 +660,10 @@ def make_stock_entry(source_name, target_doc=None): "job_card_item": "job_card_item", }, "postprocess": update_item, - "condition": lambda doc: doc.ordered_qty < doc.stock_qty, + "condition": lambda doc: ( + flt(doc.ordered_qty, doc.precision("ordered_qty")) + < flt(doc.stock_qty, doc.precision("ordered_qty")) + ), }, }, target_doc, diff --git a/erpnext/templates/emails/request_for_quotation.html b/erpnext/templates/emails/request_for_quotation.html deleted file mode 100644 index 5b073e604ff..00000000000 --- a/erpnext/templates/emails/request_for_quotation.html +++ /dev/null @@ -1,29 +0,0 @@ -

{{_("Request for Quotation")}}

-

{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},

-

{{ message }}

-

{{_("The Request for Quotation can be accessed by clicking on the following button")}}:

-
- - {{ _("Submit your Quotation") }} - -
-
-{% if update_password_link %} -
-

{{_("Please click on the following button to set your new password")}}:

- - {{_("Set Password") }} - -
-
-{% endif %} -

- {{_("Regards")}},
- {{ user_fullname }} -