diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index c71a62dfb3c..b1f3e6fd014 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -2,46 +2,10 @@ // For license information, please see license.txt frappe.provide("erpnext.accounts"); - -frappe.ui.form.on("Payment Reconciliation Payment", { - invoice_number: function(frm, cdt, cdn) { - var row = locals[cdt][cdn]; - if(row.invoice_number) { - var parts = row.invoice_number.split(' | '); - var invoice_type = parts[0]; - var invoice_number = parts[1]; - - var invoice_amount = frm.doc.invoices.filter(function(d) { - return d.invoice_type === invoice_type && d.invoice_number === invoice_number; - })[0].outstanding_amount; - - frappe.model.set_value(cdt, cdn, "allocated_amount", Math.min(invoice_amount, row.amount)); - - frm.call({ - doc: frm.doc, - method: 'get_difference_amount', - args: { - child_row: row - }, - callback: function(r, rt) { - if(r.message) { - frappe.model.set_value(cdt, cdn, - "difference_amount", r.message); - } - } - }); - } - } -}); - erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller { onload() { var me = this; - this.frm.set_query("party", function() { - check_mandatory(me.frm); - }); - this.frm.set_query("party_type", function() { return { "filters": { @@ -88,15 +52,36 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo refresh() { this.frm.disable_save(); - this.toggle_primary_action(); + + if (this.frm.doc.receivable_payable_account) { + this.frm.add_custom_button(__('Get Unreconciled Entries'), () => + this.frm.trigger("get_unreconciled_entries") + ); + } + if (this.frm.doc.invoices.length && this.frm.doc.payments.length) { + this.frm.add_custom_button(__('Allocate'), () => + this.frm.trigger("allocate") + ); + } + if (this.frm.doc.allocation.length) { + this.frm.add_custom_button(__('Reconcile'), () => + this.frm.trigger("reconcile") + ); + } } - onload_post_render() { - this.toggle_primary_action(); + company() { + var me = this; + this.frm.set_value('receivable_payable_account', ''); + me.frm.clear_table("allocation"); + me.frm.clear_table("invoices"); + me.frm.clear_table("payments"); + me.frm.refresh_fields(); + me.frm.trigger('party'); } party() { - var me = this + var me = this; if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { return frappe.call({ method: "erpnext.accounts.party.get_party_account", @@ -109,6 +94,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo if (!r.exc && r.message) { me.frm.set_value("receivable_payable_account", r.message); } + me.frm.refresh(); } }); } @@ -120,16 +106,41 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo doc: me.frm.doc, method: 'get_unreconciled_entries', callback: function(r, rt) { - me.set_invoice_options(); - me.toggle_primary_action(); + if (!(me.frm.doc.payments.length || me.frm.doc.invoices.length)) { + frappe.throw({message: __("No invoice and payment records found for this party")}); + } + me.frm.refresh(); } }); } + allocate() { + var me = this; + let payments = me.frm.fields_dict.payments.grid.get_selected_children(); + if (!(payments.length)) { + payments = me.frm.doc.payments; + } + let invoices = me.frm.fields_dict.invoices.grid.get_selected_children(); + if (!(invoices.length)) { + invoices = me.frm.doc.invoices; + } + return me.frm.call({ + doc: me.frm.doc, + method: 'allocate_entries', + args: { + payments: payments, + invoices: invoices + }, + callback: function() { + me.frm.refresh(); + } + }); + } + reconcile() { var me = this; - var show_dialog = me.frm.doc.payments.filter(d => d.difference_amount && !d.difference_account); + var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account); if (show_dialog && show_dialog.length) { @@ -138,7 +149,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo title: __("Select Difference Account"), fields: [ { - fieldname: "payments", fieldtype: "Table", label: __("Payments"), + fieldname: "allocation", fieldtype: "Table", label: __("Allocation"), data: this.data, in_place_edit: true, get_data: () => { return this.data; @@ -179,10 +190,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo }, ], primary_action: function() { - const args = dialog.get_values()["payments"]; + const args = dialog.get_values()["allocation"]; args.forEach(d => { - frappe.model.set_value("Payment Reconciliation Payment", d.docname, + frappe.model.set_value("Payment Reconciliation Allocation", d.docname, "difference_account", d.difference_account); }); @@ -192,9 +203,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo primary_action_label: __('Reconcile Entries') }); - this.frm.doc.payments.forEach(d => { + this.frm.doc.allocation.forEach(d => { if (d.difference_amount && !d.difference_account) { - dialog.fields_dict.payments.df.data.push({ + dialog.fields_dict.allocation.df.data.push({ 'docname': d.name, 'reference_name': d.reference_name, 'difference_amount': d.difference_amount, @@ -203,8 +214,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo } }); - this.data = dialog.fields_dict.payments.df.data; - dialog.fields_dict.payments.grid.refresh(); + this.data = dialog.fields_dict.allocation.df.data; + dialog.fields_dict.allocation.grid.refresh(); dialog.show(); } else { this.reconcile_payment_entries(); @@ -218,48 +229,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo doc: me.frm.doc, method: 'reconcile', callback: function(r, rt) { - me.set_invoice_options(); - me.toggle_primary_action(); + me.frm.clear_table("allocation"); + me.frm.refresh_fields(); + me.frm.refresh(); } }); } - - set_invoice_options() { - var me = this; - var invoices = []; - - $.each(me.frm.doc.invoices || [], function(i, row) { - if (row.invoice_number && !in_list(invoices, row.invoice_number)) - invoices.push(row.invoice_type + " | " + row.invoice_number); - }); - - if (invoices) { - this.frm.fields_dict.payments.grid.update_docfield_property( - 'invoice_number', 'options', "\n" + invoices.join("\n") - ); - - $.each(me.frm.doc.payments || [], function(i, p) { - if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null; - }); - } - - refresh_field("payments"); - } - - toggle_primary_action() { - if ((this.frm.doc.payments || []).length) { - this.frm.fields_dict.reconcile.$input - && this.frm.fields_dict.reconcile.$input.addClass("btn-primary"); - this.frm.fields_dict.get_unreconciled_entries.$input - && this.frm.fields_dict.get_unreconciled_entries.$input.removeClass("btn-primary"); - } else { - this.frm.fields_dict.reconcile.$input - && this.frm.fields_dict.reconcile.$input.removeClass("btn-primary"); - this.frm.fields_dict.get_unreconciled_entries.$input - && this.frm.fields_dict.get_unreconciled_entries.$input.addClass("btn-primary"); - } - } - }; extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm})); diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index cfb24c3954c..9023b3646f2 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -1,622 +1,206 @@ { - "allow_copy": 1, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-07-09 12:04:51.681583", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "allow_copy": 1, + "creation": "2014-07-09 12:04:51.681583", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "company", + "party_type", + "column_break_4", + "party", + "receivable_payable_account", + "col_break1", + "from_invoice_date", + "to_invoice_date", + "minimum_invoice_amount", + "maximum_invoice_amount", + "invoice_limit", + "column_break_13", + "from_payment_date", + "to_payment_date", + "minimum_payment_amount", + "maximum_payment_amount", + "payment_limit", + "bank_cash_account", + "sec_break1", + "invoices", + "column_break_15", + "payments", + "sec_break2", + "allocation" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "company", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "party_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Party Type", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "party_type", + "fieldtype": "Link", + "label": "Party Type", + "options": "DocType", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "party", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Party", - "length": 0, - "no_copy": 0, - "options": "party_type", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.party_type", + "fieldname": "party", + "fieldtype": "Dynamic Link", + "label": "Party", + "options": "party_type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "receivable_payable_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Receivable / Payable Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:doc.company && doc.party", + "fieldname": "receivable_payable_account", + "fieldtype": "Link", + "label": "Receivable / Payable Account", + "options": "Account", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bank_cash_account", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Bank / Cash Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "description": "This filter will be applied to Journal Entry.", + "fieldname": "bank_cash_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Bank / Cash Account", + "options": "Account" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "collapsible": 1, + "collapsible_depends_on": "eval: doc.invoices.length == 0", + "depends_on": "eval:doc.receivable_payable_account", + "fieldname": "col_break1", + "fieldtype": "Section Break", + "label": "Filters" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "From Invoice Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:(doc.payments).length || (doc.invoices).length", + "fieldname": "sec_break1", + "fieldtype": "Section Break", + "label": "Unreconciled Entries" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "To Invoice Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "payments", + "fieldtype": "Table", + "label": "Payments", + "options": "Payment Reconciliation Payment" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "minimum_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Minimum Invoice Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "allocation", + "fieldname": "sec_break2", + "fieldtype": "Section Break", + "label": "Allocated Entries" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "maximum_amount", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Maximum Invoice Amount", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "invoices", + "fieldtype": "Table", + "label": "Invoices", + "options": "Payment Reconciliation Invoice" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "System will fetch all the entries if limit value is zero.", - "fieldname": "limit", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Limit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_15", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "get_unreconciled_entries", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Get Unreconciled Entries", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "allocation", + "fieldtype": "Table", + "label": "Allocation", + "options": "Payment Reconciliation Allocation" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sec_break1", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Unreconciled Payment Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payments", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Payments", - "length": 0, - "no_copy": 0, - "options": "Payment Reconciliation Payment", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "from_invoice_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "From Invoice Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reconcile", - "fieldtype": "Button", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Reconcile", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "to_invoice_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "To Invoice Date" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sec_break2", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Invoice/Journal Entry Details", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "minimum_invoice_amount", + "fieldtype": "Currency", + "label": "Minimum Invoice Amount" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "invoices", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Invoices", - "length": 0, - "no_copy": 0, - "options": "Payment Reconciliation Invoice", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "description": "System will fetch all the entries if limit value is zero.", + "fieldname": "invoice_limit", + "fieldtype": "Int", + "label": "Invoice Limit" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "fieldname": "from_payment_date", + "fieldtype": "Date", + "label": "From Payment Date" + }, + { + "fieldname": "to_payment_date", + "fieldtype": "Date", + "label": "To Payment Date" + }, + { + "fieldname": "minimum_payment_amount", + "fieldtype": "Currency", + "label": "Minimum Payment Amount" + }, + { + "fieldname": "maximum_payment_amount", + "fieldtype": "Currency", + "label": "Maximum Payment Amount" + }, + { + "fieldname": "payment_limit", + "fieldtype": "Int", + "label": "Payment Limit" + }, + { + "fieldname": "maximum_invoice_amount", + "fieldtype": "Currency", + "label": "Maximum Invoice Amount" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 1, - "icon": "icon-resize-horizontal", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2019-01-15 17:42:21.135214", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment Reconciliation", - "name_case": "", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "icon": "icon-resize-horizontal", + "issingle": 1, + "links": [], + "modified": "2021-08-30 13:05:51.977861", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Reconciliation", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "read": 1, + "role": "Accounts User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index acfe1fef2ee..1286bf0f0bb 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe, erpnext -from frappe.utils import flt, today +from frappe.utils import flt, today, getdate, nowdate from frappe import msgprint, _ from frappe.model.document import Document from erpnext.accounts.utils import (get_outstanding_invoices, @@ -27,24 +27,32 @@ class PaymentReconciliation(Document): else: dr_or_cr_notes = [] - self.add_payment_entries(payment_entries + journal_entries + dr_or_cr_notes) + non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes + + if self.payment_limit: + non_reconciled_payments = non_reconciled_payments[:self.payment_limit] + + non_reconciled_payments = sorted(non_reconciled_payments, key=lambda k: k['posting_date'] or getdate(nowdate())) + + self.add_payment_entries(non_reconciled_payments) def get_payment_entries(self): order_doctype = "Sales Order" if self.party_type=="Customer" else "Purchase Order" + condition = self.get_conditions(get_payments=True) payment_entries = get_advance_payment_entries(self.party_type, self.party, - self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.limit) + self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.payment_limit, + condition=condition) return payment_entries def get_jv_entries(self): + condition = self.get_conditions() dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") bank_account_condition = "t2.against_account like %(bank_cash_account)s" \ if self.bank_cash_account else "1=1" - limit_cond = "limit %s" % self.limit if self.limit else "" - journal_entries = frappe.db.sql(""" select "Journal Entry" as reference_type, t1.name as reference_name, @@ -56,7 +64,7 @@ class PaymentReconciliation(Document): where t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1 and t2.party_type = %(party_type)s and t2.party = %(party)s - and t2.account = %(account)s and {dr_or_cr} > 0 + and t2.account = %(account)s and {dr_or_cr} > 0 {condition} and (t2.reference_type is null or t2.reference_type = '' or (t2.reference_type in ('Sales Order', 'Purchase Order') and t2.reference_name is not null and t2.reference_name != '')) @@ -65,11 +73,11 @@ class PaymentReconciliation(Document): THEN 1=1 ELSE {bank_account_condition} END) - order by t1.posting_date {limit_cond} + order by t1.posting_date """.format(**{ "dr_or_cr": dr_or_cr, "bank_account_condition": bank_account_condition, - "limit_cond": limit_cond + "condition": condition }), { "party_type": self.party_type, "party": self.party, @@ -80,6 +88,7 @@ class PaymentReconciliation(Document): return list(journal_entries) def get_dr_or_cr_notes(self): + condition = self.get_conditions(get_return_invoices=True) dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") @@ -90,7 +99,7 @@ class PaymentReconciliation(Document): if self.party_type == 'Customer' else "Purchase Invoice") return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type, - (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, + (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date, account_currency as currency FROM `tab{doc}` doc, `tabGL Entry` gl WHERE @@ -100,15 +109,17 @@ class PaymentReconciliation(Document): and gl.against_voucher_type = %(voucher_type)s and doc.docstatus = 1 and gl.party = %(party)s and gl.party_type = %(party_type)s and gl.account = %(account)s - and gl.is_cancelled = 0 + and gl.is_cancelled = 0 {condition} GROUP BY doc.name Having amount > 0 + ORDER BY doc.posting_date """.format( doc=voucher_type, dr_or_cr=dr_or_cr, reconciled_dr_or_cr=reconciled_dr_or_cr, - party_type_field=frappe.scrub(self.party_type)), + party_type_field=frappe.scrub(self.party_type), + condition=condition or ""), { 'party': self.party, 'party_type': self.party_type, @@ -116,22 +127,23 @@ class PaymentReconciliation(Document): 'account': self.receivable_payable_account }, as_dict=1) - def add_payment_entries(self, entries): + def add_payment_entries(self, non_reconciled_payments): self.set('payments', []) - for e in entries: + + for payment in non_reconciled_payments: row = self.append('payments', {}) - row.update(e) + row.update(payment) def get_invoice_entries(self): #Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against - condition = self.check_condition() + condition = self.get_conditions(get_invoices=True) non_reconciled_invoices = get_outstanding_invoices(self.party_type, self.party, self.receivable_payable_account, condition=condition) - if self.limit: - non_reconciled_invoices = non_reconciled_invoices[:self.limit] + if self.invoice_limit: + non_reconciled_invoices = non_reconciled_invoices[:self.invoice_limit] self.add_invoice_entries(non_reconciled_invoices) @@ -139,41 +151,78 @@ class PaymentReconciliation(Document): #Populate 'invoices' with JVs and Invoices to reconcile against self.set('invoices', []) - for e in non_reconciled_invoices: - ent = self.append('invoices', {}) - ent.invoice_type = e.get('voucher_type') - ent.invoice_number = e.get('voucher_no') - ent.invoice_date = e.get('posting_date') - ent.amount = flt(e.get('invoice_amount')) - ent.currency = e.get('currency') - ent.outstanding_amount = e.get('outstanding_amount') + for entry in non_reconciled_invoices: + inv = self.append('invoices', {}) + inv.invoice_type = entry.get('voucher_type') + inv.invoice_number = entry.get('voucher_no') + inv.invoice_date = entry.get('posting_date') + inv.amount = flt(entry.get('invoice_amount')) + inv.currency = entry.get('currency') + inv.outstanding_amount = flt(entry.get('outstanding_amount')) @frappe.whitelist() - def reconcile(self, args): - for e in self.get('payments'): - e.invoice_type = None - if e.invoice_number and " | " in e.invoice_number: - e.invoice_type, e.invoice_number = e.invoice_number.split(" | ") + def allocate_entries(self, args): + self.validate_entries() + entries = [] + for pay in args.get('payments'): + pay.update({'unreconciled_amount': pay.get('amount')}) + for inv in args.get('invoices'): + if pay.get('amount') >= inv.get('outstanding_amount'): + res = self.get_allocated_entry(pay, inv, inv['outstanding_amount']) + pay['amount'] = flt(pay.get('amount')) - flt(inv.get('outstanding_amount')) + inv['outstanding_amount'] = 0 + else: + res = self.get_allocated_entry(pay, inv, pay['amount']) + inv['outstanding_amount'] = flt(inv.get('outstanding_amount')) - flt(pay.get('amount')) + pay['amount'] = 0 + if pay.get('amount') == 0: + entries.append(res) + break + elif inv.get('outstanding_amount') == 0: + entries.append(res) + continue + else: + break - self.get_invoice_entries() - self.validate_invoice() + self.set('allocation', []) + for entry in entries: + if entry['allocated_amount'] != 0: + row = self.append('allocation', {}) + row.update(entry) + + def get_allocated_entry(self, pay, inv, allocated_amount): + return frappe._dict({ + 'reference_type': pay.get('reference_type'), + 'reference_name': pay.get('reference_name'), + 'reference_row': pay.get('reference_row'), + 'invoice_type': inv.get('invoice_type'), + 'invoice_number': inv.get('invoice_number'), + 'unreconciled_amount': pay.get('unreconciled_amount'), + 'amount': pay.get('amount'), + 'allocated_amount': allocated_amount, + 'difference_amount': pay.get('difference_amount') + }) + + @frappe.whitelist() + def reconcile(self): + self.validate_allocation() dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") - lst = [] + entry_list = [] dr_or_cr_notes = [] - for e in self.get('payments'): + for row in self.get('allocation'): reconciled_entry = [] - if e.invoice_number and e.allocated_amount: - if e.reference_type in ['Sales Invoice', 'Purchase Invoice']: + if row.invoice_number and row.allocated_amount: + if row.reference_type in ['Sales Invoice', 'Purchase Invoice']: reconciled_entry = dr_or_cr_notes else: - reconciled_entry = lst + reconciled_entry = entry_list - reconciled_entry.append(self.get_payment_details(e, dr_or_cr)) + reconciled_entry.append(self.get_payment_details(row, dr_or_cr)) - if lst: - reconcile_against_document(lst) + if entry_list: + reconcile_against_document(entry_list) if dr_or_cr_notes: reconcile_dr_cr_note(dr_or_cr_notes, self.company) @@ -183,98 +232,104 @@ class PaymentReconciliation(Document): def get_payment_details(self, row, dr_or_cr): return frappe._dict({ - 'voucher_type': row.reference_type, - 'voucher_no' : row.reference_name, - 'voucher_detail_no' : row.reference_row, - 'against_voucher_type' : row.invoice_type, - 'against_voucher' : row.invoice_number, + 'voucher_type': row.get('reference_type'), + 'voucher_no' : row.get('reference_name'), + 'voucher_detail_no' : row.get('reference_row'), + 'against_voucher_type' : row.get('invoice_type'), + 'against_voucher' : row.get('invoice_number'), 'account' : self.receivable_payable_account, 'party_type': self.party_type, 'party': self.party, - 'is_advance' : row.is_advance, + 'is_advance' : row.get('is_advance'), 'dr_or_cr' : dr_or_cr, - 'unadjusted_amount' : flt(row.amount), - 'allocated_amount' : flt(row.allocated_amount), - 'difference_amount': row.difference_amount, - 'difference_account': row.difference_account + 'unreconciled_amount': flt(row.get('unreconciled_amount')), + 'unadjusted_amount' : flt(row.get('amount')), + 'allocated_amount' : flt(row.get('allocated_amount')), + 'difference_amount': flt(row.get('difference_amount')), + 'difference_account': row.get('difference_account') }) - @frappe.whitelist() - def get_difference_amount(self, child_row): - if child_row.get("reference_type") != 'Payment Entry': return - - child_row = frappe._dict(child_row) - - if child_row.invoice_number and " | " in child_row.invoice_number: - child_row.invoice_type, child_row.invoice_number = child_row.invoice_number.split(" | ") - - dr_or_cr = ("credit_in_account_currency" - if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency") - - row = self.get_payment_details(child_row, dr_or_cr) - - doc = frappe.get_doc(row.voucher_type, row.voucher_no) - update_reference_in_payment_entry(row, doc, do_not_save=True) - - return doc.difference_amount - def check_mandatory_to_fetch(self): for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: if not self.get(fieldname): frappe.throw(_("Please select {0} first").format(self.meta.get_label(fieldname))) - def validate_invoice(self): + def validate_entries(self): if not self.get("invoices"): - frappe.throw(_("No records found in the Invoice table")) + frappe.throw(_("No records found in the Invoices table")) if not self.get("payments"): - frappe.throw(_("No records found in the Payment table")) + frappe.throw(_("No records found in the Payments table")) + def validate_allocation(self): unreconciled_invoices = frappe._dict() - for d in self.get("invoices"): - unreconciled_invoices.setdefault(d.invoice_type, {}).setdefault(d.invoice_number, d.outstanding_amount) + + for inv in self.get("invoices"): + unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(inv.invoice_number, inv.outstanding_amount) invoices_to_reconcile = [] - for p in self.get("payments"): - if p.invoice_type and p.invoice_number and p.allocated_amount: - invoices_to_reconcile.append(p.invoice_number) + for row in self.get("allocation"): + if row.invoice_type and row.invoice_number and row.allocated_amount: + invoices_to_reconcile.append(row.invoice_number) - if p.invoice_number not in unreconciled_invoices.get(p.invoice_type, {}): - frappe.throw(_("{0}: {1} not found in Invoice Details table") - .format(p.invoice_type, p.invoice_number)) + if flt(row.amount) - flt(row.allocated_amount) < 0: + frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}") + .format(row.idx, row.allocated_amount, row.amount)) - if flt(p.allocated_amount) > flt(p.amount): - frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equals to Payment Entry amount {2}") - .format(p.idx, p.allocated_amount, p.amount)) - - invoice_outstanding = unreconciled_invoices.get(p.invoice_type, {}).get(p.invoice_number) - if flt(p.allocated_amount) - invoice_outstanding > 0.009: - frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equals to invoice outstanding amount {2}") - .format(p.idx, p.allocated_amount, invoice_outstanding)) + invoice_outstanding = unreconciled_invoices.get(row.invoice_type, {}).get(row.invoice_number) + if flt(row.allocated_amount) - invoice_outstanding > 0.009: + frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}") + .format(row.idx, row.allocated_amount, invoice_outstanding)) if not invoices_to_reconcile: - frappe.throw(_("Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row")) + frappe.throw(_("No records found in Allocation table")) - def check_condition(self): - cond = " and posting_date >= {0}".format(frappe.db.escape(self.from_date)) if self.from_date else "" - cond += " and posting_date <= {0}".format(frappe.db.escape(self.to_date)) if self.to_date else "" - dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' - else "credit_in_account_currency") + def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False): + condition = " and company = '{0}' ".format(self.company) - if self.minimum_amount: - cond += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_amount)) - if self.maximum_amount: - cond += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_amount)) + if get_invoices: + condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) if self.from_invoice_date else "" + condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date)) if self.to_invoice_date else "" + dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' + else "credit_in_account_currency") - return cond + if self.minimum_invoice_amount: + condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount)) + if self.maximum_invoice_amount: + condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount)) + + elif get_return_invoices: + condition = " and doc.company = '{0}' ".format(self.company) + condition += " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else "" + condition += " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else "" + dr_or_cr = ("gl.debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' + else "gl.credit_in_account_currency") + + if self.minimum_invoice_amount: + condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount)) + if self.maximum_invoice_amount: + condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount)) + + else: + condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else "" + condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else "" + + if self.minimum_payment_amount: + condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) if get_payments \ + else " and total_debit >= {0}".format(flt(self.minimum_payment_amount)) + if self.maximum_payment_amount: + condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) if get_payments \ + else " and total_debit <= {0}".format(flt(self.maximum_payment_amount)) + + return condition def reconcile_dr_cr_note(dr_cr_notes, company): - for d in dr_cr_notes: + for inv in dr_cr_notes: voucher_type = ('Credit Note' - if d.voucher_type == 'Sales Invoice' else 'Debit Note') + if inv.voucher_type == 'Sales Invoice' else 'Debit Note') reconcile_dr_or_cr = ('debit_in_account_currency' - if d.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') + if inv.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') company_currency = erpnext.get_company_currency(company) @@ -283,25 +338,25 @@ def reconcile_dr_cr_note(dr_cr_notes, company): "voucher_type": voucher_type, "posting_date": today(), "company": company, - "multi_currency": 1 if d.currency != company_currency else 0, + "multi_currency": 1 if inv.currency != company_currency else 0, "accounts": [ { - 'account': d.account, - 'party': d.party, - 'party_type': d.party_type, - d.dr_or_cr: abs(d.allocated_amount), - 'reference_type': d.against_voucher_type, - 'reference_name': d.against_voucher, + 'account': inv.account, + 'party': inv.party, + 'party_type': inv.party_type, + inv.dr_or_cr: abs(inv.allocated_amount), + 'reference_type': inv.against_voucher_type, + 'reference_name': inv.against_voucher, 'cost_center': erpnext.get_default_cost_center(company) }, { - 'account': d.account, - 'party': d.party, - 'party_type': d.party_type, - reconcile_dr_or_cr: (abs(d.allocated_amount) - if abs(d.unadjusted_amount) > abs(d.allocated_amount) else abs(d.unadjusted_amount)), - 'reference_type': d.voucher_type, - 'reference_name': d.voucher_no, + 'account': inv.account, + 'party': inv.party, + 'party_type': inv.party_type, + reconcile_dr_or_cr: (abs(inv.allocated_amount) + if abs(inv.unadjusted_amount) > abs(inv.allocated_amount) else abs(inv.unadjusted_amount)), + 'reference_type': inv.voucher_type, + 'reference_name': inv.voucher_no, 'cost_center': erpnext.get_default_cost_center(company) } ] diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py new file mode 100644 index 00000000000..87eaaee8564 --- /dev/null +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt + +# import frappe +import unittest + +class TestPaymentReconciliation(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/__init__.py b/erpnext/accounts/doctype/payment_reconciliation_allocation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json new file mode 100644 index 00000000000..36535014320 --- /dev/null +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -0,0 +1,137 @@ +{ + "actions": [], + "creation": "2021-08-16 17:04:40.185167", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "reference_type", + "reference_name", + "column_break_3", + "invoice_type", + "invoice_number", + "section_break_6", + "allocated_amount", + "unreconciled_amount", + "amount", + "column_break_8", + "is_advance", + "section_break_5", + "difference_amount", + "column_break_7", + "difference_account" + ], + "fields": [ + { + "fieldname": "invoice_number", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Invoice Number", + "options": "invoice_type", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Allocated Amount", + "options": "Currency", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "difference_account", + "fieldtype": "Link", + "label": "Difference Account", + "options": "Account", + "read_only": 1 + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "difference_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Difference Amount", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Reference Name", + "options": "reference_type", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "is_advance", + "fieldtype": "Data", + "hidden": 1, + "label": "Is Advance", + "read_only": 1 + }, + { + "fieldname": "reference_type", + "fieldtype": "Link", + "label": "Reference Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "invoice_type", + "fieldtype": "Link", + "label": "Invoice Type", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "unreconciled_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Unreconciled Amount", + "options": "Currency", + "read_only": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Amount", + "options": "Currency", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2021-08-30 10:58:42.665107", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Reconciliation Allocation", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py new file mode 100644 index 00000000000..0fb63b1cd17 --- /dev/null +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class PaymentReconciliationAllocation(Document): + pass diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json index 6a79a85c348..00c9e1240c5 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json +++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json @@ -44,7 +44,6 @@ { "fieldname": "amount", "fieldtype": "Currency", - "in_list_view": 1, "label": "Amount", "options": "currency", "read_only": 1 @@ -67,7 +66,7 @@ ], "istable": 1, "links": [], - "modified": "2020-07-19 18:12:27.964073", + "modified": "2021-08-24 22:42:40.923179", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Invoice", diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json index 925a6f10a5e..add07e870d8 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json +++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json @@ -11,11 +11,7 @@ "is_advance", "reference_row", "col_break1", - "invoice_number", "amount", - "allocated_amount", - "section_break_10", - "difference_account", "difference_amount", "sec_break1", "remark", @@ -41,6 +37,7 @@ { "fieldname": "posting_date", "fieldtype": "Date", + "in_list_view": 1, "label": "Posting Date", "read_only": 1 }, @@ -62,14 +59,6 @@ "fieldname": "col_break1", "fieldtype": "Column Break" }, - { - "columns": 2, - "fieldname": "invoice_number", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Invoice Number", - "reqd": 1 - }, { "columns": 2, "fieldname": "amount", @@ -79,15 +68,6 @@ "options": "currency", "read_only": 1 }, - { - "columns": 2, - "fieldname": "allocated_amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Allocated amount", - "options": "currency", - "reqd": 1 - }, { "fieldname": "sec_break1", "fieldtype": "Section Break" @@ -95,41 +75,27 @@ { "fieldname": "remark", "fieldtype": "Small Text", - "in_list_view": 1, "label": "Remark", "read_only": 1 }, - { - "columns": 2, - "fieldname": "difference_account", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Difference Account", - "options": "Account" - }, - { - "fieldname": "difference_amount", - "fieldtype": "Currency", - "label": "Difference Amount", - "options": "currency", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "section_break_10", - "fieldtype": "Section Break" - }, { "fieldname": "currency", "fieldtype": "Link", "hidden": 1, "label": "Currency", "options": "Currency" + }, + { + "fieldname": "difference_amount", + "fieldtype": "Currency", + "label": "Difference Amount", + "options": "currency", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2020-07-19 18:12:41.682347", + "modified": "2021-08-30 10:51:48.140062", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Payment", diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5ed4a1947c2..c46eb7e6310 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -341,31 +341,42 @@ def add_cc(args=None): def reconcile_against_document(args): """ - Cancel JV, Update aginst document, split if required and resubmit jv + Cancel PE or JV, Update against document, split if required and resubmit """ - for d in args: + # To optimize making GL Entry for PE or JV with multiple references + reconciled_entries = {} + for row in args: + if not reconciled_entries.get((row.voucher_type, row.voucher_no)): + reconciled_entries[(row.voucher_type, row.voucher_no)] = [] - check_if_advance_entry_modified(d) - validate_allocated_amount(d) + reconciled_entries[(row.voucher_type, row.voucher_no)].append(row) + + for key, entries in reconciled_entries.items(): + voucher_type = key[0] + voucher_no = key[1] # cancel advance entry - doc = frappe.get_doc(d.voucher_type, d.voucher_no) - + doc = frappe.get_doc(voucher_type, voucher_no) frappe.flags.ignore_party_validation = True doc.make_gl_entries(cancel=1, adv_adj=1) - # update ref in advance entry - if d.voucher_type == "Journal Entry": - update_reference_in_journal_entry(d, doc) - else: - update_reference_in_payment_entry(d, doc) + for entry in entries: + check_if_advance_entry_modified(entry) + validate_allocated_amount(entry) + # update ref in advance entry + if voucher_type == "Journal Entry": + update_reference_in_journal_entry(entry, doc, do_not_save=True) + else: + update_reference_in_payment_entry(entry, doc, do_not_save=True) + + doc.save(ignore_permissions=True) # re-submit advance entry - doc = frappe.get_doc(d.voucher_type, d.voucher_no) + doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) doc.make_gl_entries(cancel = 0, adv_adj =1) frappe.flags.ignore_party_validation = False - if d.voucher_type in ('Payment Entry', 'Journal Entry'): + if entry.voucher_type in ('Payment Entry', 'Journal Entry'): doc.update_expense_claim() def check_if_advance_entry_modified(args): @@ -374,6 +385,9 @@ def check_if_advance_entry_modified(args): check if amount is same check if jv is submitted """ + if not args.get('unreconciled_amount'): + args.update({'unreconciled_amount': args.get('unadjusted_amount')}) + ret = None if args.voucher_type == "Journal Entry": ret = frappe.db.sql(""" @@ -395,14 +409,14 @@ def check_if_advance_entry_modified(args): and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s and t2.reference_doctype in ("", "Sales Order", "Purchase Order") - and t2.allocated_amount = %(unadjusted_amount)s + and t2.allocated_amount = %(unreconciled_amount)s """.format(party_account_field), args) else: ret = frappe.db.sql("""select name from `tabPayment Entry` where name = %(voucher_no)s and docstatus = 1 and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s - and unallocated_amount = %(unadjusted_amount)s + and unallocated_amount = %(unreconciled_amount)s """.format(party_account_field), args) if not ret: @@ -415,58 +429,44 @@ def validate_allocated_amount(args): elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision): throw(_("Allocated amount cannot be greater than unadjusted amount")) -def update_reference_in_journal_entry(d, jv_obj): +def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): """ Updates against document, if partial amount splits into rows """ - jv_detail = jv_obj.get("accounts", {"name": d["voucher_detail_no"]})[0] - jv_detail.set(d["dr_or_cr"], d["allocated_amount"]) - jv_detail.set('debit' if d['dr_or_cr']=='debit_in_account_currency' else 'credit', - d["allocated_amount"]*flt(jv_detail.exchange_rate)) - - original_reference_type = jv_detail.reference_type - original_reference_name = jv_detail.reference_name - - jv_detail.set("reference_type", d["against_voucher_type"]) - jv_detail.set("reference_name", d["against_voucher"]) - - if d['allocated_amount'] < d['unadjusted_amount']: - jvd = frappe.db.sql(""" - select cost_center, balance, against_account, is_advance, - account_type, exchange_rate, account_currency - from `tabJournal Entry Account` where name = %s - """, d['voucher_detail_no'], as_dict=True) + jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] + if flt(d['unadjusted_amount']) - flt(d['allocated_amount']) != 0: + # adjust the unreconciled balance amount_in_account_currency = flt(d['unadjusted_amount']) - flt(d['allocated_amount']) - amount_in_company_currency = amount_in_account_currency * flt(jvd[0]['exchange_rate']) + amount_in_company_currency = amount_in_account_currency * flt(jv_detail.exchange_rate) + jv_detail.set(d['dr_or_cr'], amount_in_account_currency) + jv_detail.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit', amount_in_company_currency) + else: + journal_entry.remove(jv_detail) - # new entry with balance amount - ch = jv_obj.append("accounts") - ch.account = d['account'] - ch.account_type = jvd[0]['account_type'] - ch.account_currency = jvd[0]['account_currency'] - ch.exchange_rate = jvd[0]['exchange_rate'] - ch.party_type = d["party_type"] - ch.party = d["party"] - ch.cost_center = cstr(jvd[0]["cost_center"]) - ch.balance = flt(jvd[0]["balance"]) + # new row with references + new_row = journal_entry.append("accounts") + new_row.update(jv_detail.as_dict().copy()) - ch.set(d['dr_or_cr'], amount_in_account_currency) - ch.set('debit' if d['dr_or_cr']=='debit_in_account_currency' else 'credit', amount_in_company_currency) + new_row.set(d["dr_or_cr"], d["allocated_amount"]) + new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit', + d["allocated_amount"] * flt(jv_detail.exchange_rate)) - ch.set('credit_in_account_currency' if d['dr_or_cr']== 'debit_in_account_currency' - else 'debit_in_account_currency', 0) - ch.set('credit' if d['dr_or_cr']== 'debit_in_account_currency' else 'debit', 0) + new_row.set('credit_in_account_currency' if d['dr_or_cr'] == 'debit_in_account_currency' + else 'debit_in_account_currency', 0) + new_row.set('credit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'debit', 0) - ch.against_account = cstr(jvd[0]["against_account"]) - ch.reference_type = original_reference_type - ch.reference_name = original_reference_name - ch.is_advance = cstr(jvd[0]["is_advance"]) - ch.docstatus = 1 + new_row.set("reference_type", d["against_voucher_type"]) + new_row.set("reference_name", d["against_voucher"]) + + new_row.against_account = cstr(jv_detail.against_account) + new_row.is_advance = cstr(jv_detail.is_advance) + new_row.docstatus = 1 # will work as update after submit - jv_obj.flags.ignore_validate_update_after_submit = True - jv_obj.save(ignore_permissions=True) + journal_entry.flags.ignore_validate_update_after_submit = True + if not do_not_save: + journal_entry.save(ignore_permissions=True) def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): reference_details = { @@ -576,7 +576,7 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): @frappe.whitelist() def get_company_default(company, fieldname, ignore_validation=False): - value = frappe.get_cached_value('Company', company, fieldname) + value = frappe.get_cached_value('Company', company, fieldname) if not ignore_validation and not value: throw(_("Please set default {0} in Company {1}") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index f4af8932b6d..c41f0836955 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1587,7 +1587,7 @@ def get_advance_journal_entries(party_type, party, party_account, amount_field, def get_advance_payment_entries(party_type, party, party_account, order_doctype, - order_list=None, include_unallocated=True, against_all_orders=False, limit=None): + order_list=None, include_unallocated=True, against_all_orders=False, limit=None, condition=None): party_account_field = "paid_from" if party_type == "Customer" else "paid_to" currency_field = "paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency" payment_type = "Receive" if party_type == "Customer" else "Pay" @@ -1622,14 +1622,14 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, if include_unallocated: unallocated_payment_entries = frappe.db.sql(""" - select "Payment Entry" as reference_type, name as reference_name, - remarks, unallocated_amount as amount, {2} as exchange_rate + select "Payment Entry" as reference_type, name as reference_name, posting_date, + remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency from `tabPayment Entry` where {0} = %s and party_type = %s and party = %s and payment_type = %s - and docstatus = 1 and unallocated_amount > 0 + and docstatus = 1 and unallocated_amount > 0 {condition} order by posting_date {1} - """.format(party_account_field, limit_cond, exchange_rate_field), + """.format(party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or ""), (party_account, party_type, party, payment_type), as_dict=1) return list(payment_entries_against_order) + list(unallocated_payment_entries)