From 601543ba0a204ea744d13b18abe5ed5421a5f293 Mon Sep 17 00:00:00 2001 From: Anuja Pawar <60467153+Anuja-pawar@users.noreply.github.com> Date: Tue, 31 Aug 2021 18:59:29 +0530 Subject: [PATCH] refactor: payment reconciliation tool (#27128) (cherry picked from commit 3e404f15ff03a5e0825bbba2d504bfb679455453) # Conflicts: # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json # erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py # erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py # erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json # erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py # erpnext/accounts/utils.py # erpnext/controllers/accounts_controller.py --- .../payment_reconciliation.js | 100 +++++++ .../payment_reconciliation.json | 30 ++ .../payment_reconciliation.py | 264 ++++++++++++++++++ .../test_payment_reconciliation.py | 8 + .../payment_reconciliation_allocation.json | 29 ++ .../payment_reconciliation_allocation.py | 3 + erpnext/accounts/utils.py | 62 ++++ erpnext/controllers/accounts_controller.py | 14 + 8 files changed, 510 insertions(+) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js index 867fcc7f13ee..b3a84848b579 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js @@ -2,6 +2,7 @@ // For license information, please see license.txt frappe.provide("erpnext.accounts"); +<<<<<<< HEAD erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({ onload: function() { const default_company = frappe.defaults.get_default('company'); @@ -12,6 +13,13 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext this.frm.set_value('receivable_payable_account', ''); this.frm.set_query("party_type", () => { +======= +erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller { + onload() { + var me = this; + + this.frm.set_query("party_type", function() { +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) return { "filters": { "name": ["in", Object.keys(frappe.boot.party_account_types)], @@ -93,11 +101,47 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext this.frm.set_value('party', ''); }, +<<<<<<< HEAD party: function() { this.frm.set_value('receivable_payable_account', ''); this.frm.trigger("clear_child_tables"); if (!this.frm.doc.receivable_payable_account && this.frm.doc.party_type && this.frm.doc.party) { +======= + refresh() { + this.frm.disable_save(); + + 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") + ); + } + } + + 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; + if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) return frappe.call({ method: "erpnext.accounts.party.get_party_account", args: { @@ -109,8 +153,12 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext if (!r.exc && r.message) { this.frm.set_value("receivable_payable_account", r.message); } +<<<<<<< HEAD this.frm.refresh(); +======= + me.frm.refresh(); +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) } }); } @@ -133,6 +181,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext return this.frm.call({ doc: this.frm.doc, method: 'get_unreconciled_entries', +<<<<<<< HEAD callback: () => { if (!(this.frm.doc.payments.length || this.frm.doc.invoices.length)) { frappe.throw({message: __("No Unreconciled Invoices and Payments found for this party and account")}); @@ -142,11 +191,19 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext frappe.throw({message: __("No Unreconciled Payments found for this party")}); } this.frm.refresh(); +======= + callback: function(r, rt) { + 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(); +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) } }); }, +<<<<<<< HEAD allocate: function() { let payments = this.frm.fields_dict.payments.grid.get_selected_children(); if (!(payments.length)) { @@ -158,11 +215,26 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext } return this.frm.call({ doc: this.frm.doc, +======= + 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, +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) method: 'allocate_entries', args: { payments: payments, invoices: invoices }, +<<<<<<< HEAD callback: () => { this.frm.refresh(); } @@ -171,6 +243,17 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext reconcile: function() { var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account); +======= + callback: function() { + me.frm.refresh(); + } + }); + } + + reconcile() { + var me = this; + var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account); +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) if (show_dialog && show_dialog.length) { @@ -219,7 +302,11 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext }] }, ], +<<<<<<< HEAD primary_action: () => { +======= + primary_action: function() { +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) const args = dialog.get_values()["allocation"]; args.forEach(d => { @@ -256,6 +343,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext return this.frm.call({ doc: this.frm.doc, method: 'reconcile', +<<<<<<< HEAD callback: () => { this.frm.clear_table("allocation"); this.frm.refresh(); @@ -265,3 +353,15 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext }); $.extend(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm})); +======= + callback: function(r, rt) { + me.frm.clear_table("allocation"); + me.frm.refresh_fields(); + me.frm.refresh(); + } + }); + } +}; + +extend_cscript(cur_frm.cscript, new erpnext.accounts.PaymentReconciliationController({frm: cur_frm})); +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json index 18d34850850f..8d2b8fa727f6 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json @@ -12,6 +12,7 @@ "receivable_payable_account", "col_break1", "from_invoice_date", +<<<<<<< HEAD "from_payment_date", "minimum_invoice_amount", "minimum_payment_amount", @@ -25,6 +26,19 @@ "payment_limit", "bank_cash_account", "cost_center", +======= + "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", +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "sec_break1", "invoices", "column_break_15", @@ -81,7 +95,10 @@ }, { "depends_on": "eval:(doc.payments).length || (doc.invoices).length", +<<<<<<< HEAD "description": "If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.", +======= +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "fieldname": "sec_break1", "fieldtype": "Section Break", "label": "Unreconciled Entries" @@ -166,7 +183,10 @@ "label": "Maximum Payment Amount" }, { +<<<<<<< HEAD "description": "System will fetch all the entries if limit value is zero.", +======= +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "fieldname": "payment_limit", "fieldtype": "Int", "label": "Payment Limit" @@ -175,6 +195,7 @@ "fieldname": "maximum_invoice_amount", "fieldtype": "Currency", "label": "Maximum Invoice Amount" +<<<<<<< HEAD }, { "fieldname": "column_break_11", @@ -185,13 +206,19 @@ "fieldtype": "Link", "label": "Cost Center", "options": "Cost Center" +======= +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) } ], "hide_toolbar": 1, "icon": "icon-resize-horizontal", "issingle": 1, "links": [], +<<<<<<< HEAD "modified": "2022-04-29 15:37:10.246831", +======= + "modified": "2021-08-30 13:05:51.977861", +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation", @@ -216,6 +243,9 @@ ], "sort_field": "modified", "sort_order": "DESC", +<<<<<<< HEAD "states": [], +======= +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "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 d7274d7d3179..5358ca4e5735 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -1,9 +1,16 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # For license information, please see license.txt +<<<<<<< HEAD import frappe from frappe import _, msgprint, qb +======= +from __future__ import unicode_literals +import frappe, erpnext +from frappe.utils import flt, today, getdate, nowdate +from frappe import msgprint, _ +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) from frappe.model.document import Document from frappe.query_builder import Criterion from frappe.query_builder.functions import Sum @@ -38,15 +45,22 @@ def get_nonreconciled_payment_entries(self): non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes if self.payment_limit: +<<<<<<< HEAD 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()) ) +======= + 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())) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) self.add_payment_entries(non_reconciled_payments) def get_payment_entries(self): +<<<<<<< HEAD order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order" condition = self.get_conditions(get_payments=True) @@ -62,15 +76,28 @@ def get_payment_entries(self): limit=self.payment_limit, condition=condition, ) +======= + 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.payment_limit, + condition=condition) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) return payment_entries def get_jv_entries(self): condition = self.get_conditions() +<<<<<<< HEAD +======= + dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable' + else "debit_in_account_currency") +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) if self.get("cost_center"): condition += " and t2.cost_center = '{0}' ".format(self.cost_center) +<<<<<<< HEAD dr_or_cr = ( "credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == "Receivable" @@ -83,6 +110,9 @@ def get_jv_entries(self): journal_entries = frappe.db.sql( """ +======= + journal_entries = frappe.db.sql(""" +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) select "Journal Entry" as reference_type, t1.name as reference_name, t1.posting_date, t1.remark as remarks, t2.name as reference_row, @@ -103,6 +133,7 @@ def get_jv_entries(self): ELSE {bank_account_condition} END) order by t1.posting_date +<<<<<<< HEAD """.format( **{ "dr_or_cr": dr_or_cr, @@ -111,6 +142,13 @@ def get_jv_entries(self): } ), { +======= + """.format(**{ + "dr_or_cr": dr_or_cr, + "bank_account_condition": bank_account_condition, + "condition": condition + }), { +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "party_type": self.party_type, "party": self.party, "account": self.receivable_payable_account, @@ -122,7 +160,13 @@ def get_jv_entries(self): return list(journal_entries) def get_dr_or_cr_notes(self): +<<<<<<< HEAD gl = qb.DocType("GL Entry") +======= + 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") +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" doc = qb.DocType(voucher_type) @@ -132,6 +176,7 @@ def get_dr_or_cr_notes(self): conditions = [] sub_query_conditions.append(doc.company == self.company) +<<<<<<< HEAD if self.get("from_payment_date"): sub_query_conditions.append(doc.posting_date.gte(self.from_payment_date)) @@ -200,6 +245,42 @@ def add_payment_entries(self, non_reconciled_payments): for payment in non_reconciled_payments: row = self.append("payments", {}) +======= + 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, doc.posting_date, + account_currency as currency + FROM `tab{doc}` doc, `tabGL Entry` gl + WHERE + (doc.name = gl.against_voucher or doc.name = gl.voucher_no) + and doc.{party_type_field} = %(party)s + and doc.is_return = 1 and ifnull(doc.return_against, "") = "" + 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 {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), + condition=condition or ""), + { + 'party': self.party, + 'party_type': self.party_type, + 'voucher_type': voucher_type, + 'account': self.receivable_payable_account + }, as_dict=1) + + def add_payment_entries(self, non_reconciled_payments): + self.set('payments', []) + + for payment in non_reconciled_payments: + row = self.append('payments', {}) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) row.update(payment) def get_invoice_entries(self): @@ -210,12 +291,17 @@ def get_invoice_entries(self): if self.get("cost_center"): condition += " and cost_center = '{0}' ".format(self.cost_center) +<<<<<<< HEAD non_reconciled_invoices = get_outstanding_invoices( self.party_type, self.party, self.receivable_payable_account, self.company, condition=condition ) if self.invoice_limit: non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit] +======= + if self.invoice_limit: + non_reconciled_invoices = non_reconciled_invoices[:self.invoice_limit] +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) self.add_invoice_entries(non_reconciled_invoices) @@ -224,6 +310,7 @@ def add_invoice_entries(self, non_reconciled_invoices): self.set("invoices", []) for entry in non_reconciled_invoices: +<<<<<<< HEAD inv = self.append("invoices", {}) inv.invoice_type = entry.get("voucher_type") inv.invoice_number = entry.get("voucher_no") @@ -248,11 +335,21 @@ def get_difference_amount(self, allocated_entry): update_reference_in_payment_entry(row, doc, do_not_save=True) return doc.difference_amount +======= + 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')) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) @frappe.whitelist() def allocate_entries(self, args): self.validate_entries() entries = [] +<<<<<<< HEAD for pay in args.get("payments"): pay.update({"unreconciled_amount": pay.get("amount")}) for inv in args.get("invoices"): @@ -314,6 +411,59 @@ def reconcile(self): reconciled_entry = [] if row.invoice_number and row.allocated_amount: if row.reference_type in ["Sales Invoice", "Purchase Invoice"]: +======= + 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.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") + + entry_list = [] + dr_or_cr_notes = [] + for row in self.get('allocation'): + reconciled_entry = [] + if row.invoice_number and row.allocated_amount: + if row.reference_type in ['Sales Invoice', 'Purchase Invoice']: +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) reconciled_entry = dr_or_cr_notes else: reconciled_entry = entry_list @@ -330,6 +480,7 @@ def reconcile(self): self.get_unreconciled_entries() def get_payment_details(self, row, dr_or_cr): +<<<<<<< HEAD return frappe._dict( { "voucher_type": row.get("reference_type"), @@ -349,6 +500,25 @@ def get_payment_details(self, row, dr_or_cr): "difference_account": row.get("difference_account"), } ) +======= + return frappe._dict({ + '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.get('is_advance'), + 'dr_or_cr' : dr_or_cr, + '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') + }) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) def check_mandatory_to_fetch(self): for fieldname in ["company", "party_type", "party", "receivable_payable_account"]: @@ -366,9 +536,13 @@ def validate_allocation(self): unreconciled_invoices = frappe._dict() for inv in self.get("invoices"): +<<<<<<< HEAD unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault( inv.invoice_number, inv.outstanding_amount ) +======= + unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(inv.invoice_number, inv.outstanding_amount) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) invoices_to_reconcile = [] for row in self.get("allocation"): @@ -376,6 +550,7 @@ def validate_allocation(self): invoices_to_reconcile.append(row.invoice_number) if flt(row.amount) - flt(row.allocated_amount) < 0: +<<<<<<< HEAD frappe.throw( _( "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}" @@ -389,10 +564,20 @@ def validate_allocation(self): "Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}" ).format(row.idx, row.allocated_amount, invoice_outstanding) ) +======= + 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)) + + 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)) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) if not invoices_to_reconcile: frappe.throw(_("No records found in Allocation table")) +<<<<<<< HEAD def get_conditions(self, get_invoices=False, get_payments=False): condition = " and company = '{0}' ".format(self.company) @@ -494,5 +679,84 @@ def reconcile_dr_cr_note(dr_cr_notes, company): ], } ) +======= + def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False): + condition = " and company = '{0}' ".format(self.company) + + 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") + + 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 inv in dr_cr_notes: + voucher_type = ('Credit Note' + if inv.voucher_type == 'Sales Invoice' else 'Debit Note') + + reconcile_dr_or_cr = ('debit_in_account_currency' + if inv.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') + + company_currency = erpnext.get_company_currency(company) + + jv = frappe.get_doc({ + "doctype": "Journal Entry", + "voucher_type": voucher_type, + "posting_date": today(), + "company": company, + "multi_currency": 1 if inv.currency != company_currency else 0, + "accounts": [ + { + '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': 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) + } + ] + }) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) jv.flags.ignore_mandatory = True jv.submit() diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index d2374b77a631..6afb1779f81d 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -1,6 +1,7 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt +<<<<<<< HEAD import unittest import frappe @@ -94,3 +95,10 @@ def make_invoice_and_payment(): ) pe.insert() pe.submit() +======= +# import frappe +import unittest + +class TestPaymentReconciliation(unittest.TestCase): + pass +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json index 7b94eb0ea4a2..136cc0428d19 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json @@ -7,21 +7,33 @@ "field_order": [ "reference_type", "reference_name", +<<<<<<< HEAD "reference_row", +======= +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "column_break_3", "invoice_type", "invoice_number", "section_break_6", "allocated_amount", "unreconciled_amount", +<<<<<<< HEAD "column_break_8", "amount", +======= + "amount", + "column_break_8", +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "is_advance", "section_break_5", "difference_amount", "column_break_7", +<<<<<<< HEAD "difference_account", "currency" +======= + "difference_account" +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) ], "fields": [ { @@ -38,7 +50,11 @@ "fieldtype": "Currency", "in_list_view": 1, "label": "Allocated Amount", +<<<<<<< HEAD "options": "currency", +======= + "options": "Currency", +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "reqd": 1 }, { @@ -113,7 +129,11 @@ "fieldtype": "Currency", "hidden": 1, "label": "Unreconciled Amount", +<<<<<<< HEAD "options": "currency", +======= + "options": "Currency", +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "read_only": 1 }, { @@ -121,6 +141,7 @@ "fieldtype": "Currency", "hidden": 1, "label": "Amount", +<<<<<<< HEAD "options": "currency", "read_only": 1 }, @@ -137,11 +158,19 @@ "hidden": 1, "label": "Currency", "options": "Currency" +======= + "options": "Currency", + "read_only": 1 +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) } ], "istable": 1, "links": [], +<<<<<<< HEAD "modified": "2023-11-28 16:30:43.344612", +======= + "modified": "2021-08-30 10:58:42.665107", +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) "modified_by": "Administrator", "module": "Accounts", "name": "Payment Reconciliation Allocation", diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py index 9db8e62af08b..17dbe40598f3 100644 --- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py +++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.py @@ -4,6 +4,9 @@ # import frappe from frappe.model.document import Document +<<<<<<< HEAD +======= +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) class PaymentReconciliationAllocation(Document): pass diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index f611fa329143..382430b25843 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -422,7 +422,11 @@ def add_cc(args=None): def reconcile_against_document(args): """ +<<<<<<< HEAD Cancel PE or JV, Update against document, split if required and resubmit +======= + Cancel PE or JV, Update against document, split if required and resubmit +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) """ # To optimize making GL Entry for PE or JV with multiple references reconciled_entries = {} @@ -454,10 +458,17 @@ def reconcile_against_document(args): doc.save(ignore_permissions=True) # re-submit advance entry doc = frappe.get_doc(entry.voucher_type, entry.voucher_no) +<<<<<<< HEAD doc.make_gl_entries(cancel=0, adv_adj=1) frappe.flags.ignore_party_validation = False if entry.voucher_type in ("Payment Entry", "Journal Entry"): +======= + doc.make_gl_entries(cancel = 0, adv_adj =1) + frappe.flags.ignore_party_validation = False + + if entry.voucher_type in ('Payment Entry', 'Journal Entry'): +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) doc.update_expense_claim() @@ -467,8 +478,13 @@ def check_if_advance_entry_modified(args): check if amount is same check if jv is submitted """ +<<<<<<< HEAD if not args.get("unreconciled_amount"): args.update({"unreconciled_amount": args.get("unadjusted_amount")}) +======= + if not args.get('unreconciled_amount'): + args.update({'unreconciled_amount': args.get('unadjusted_amount')}) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) ret = None if args.voucher_type == "Journal Entry": @@ -499,11 +515,15 @@ def check_if_advance_entry_modified(args): 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 = %(unreconciled_amount)s +<<<<<<< HEAD """.format( party_account_field ), args, ) +======= + """.format(party_account_field), args) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) else: ret = frappe.db.sql( """select name from `tabPayment Entry` @@ -511,11 +531,15 @@ def check_if_advance_entry_modified(args): name = %(voucher_no)s and docstatus = 1 and party_type = %(party_type)s and party = %(party)s and {0} = %(account)s and unallocated_amount = %(unreconciled_amount)s +<<<<<<< HEAD """.format( party_account_field ), args, ) +======= + """.format(party_account_field), args) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) if not ret: throw(_("""Payment Entry has been modified after you pulled it. Please pull it again.""")) @@ -530,12 +554,16 @@ 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")) +<<<<<<< HEAD +======= +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): """ Updates against document, if partial amount splits into rows """ jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0] +<<<<<<< HEAD # Update Advance Paid in SO/PO since they might be getting unlinked if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"): @@ -575,6 +603,33 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): new_row.set("reference_type", d["against_voucher_type"]) new_row.set("reference_name", d["against_voucher"]) +======= + + 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(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 row with references + new_row = journal_entry.append("accounts") + new_row.update(jv_detail.as_dict().copy()) + + 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)) + + 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) + + new_row.set("reference_type", d["against_voucher_type"]) + new_row.set("reference_name", d["against_voucher"]) + +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) new_row.against_account = cstr(jv_detail.against_account) new_row.is_advance = cstr(jv_detail.is_advance) new_row.docstatus = 1 @@ -583,7 +638,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False): journal_entry.flags.ignore_validate_update_after_submit = True if not do_not_save: journal_entry.save(ignore_permissions=True) +<<<<<<< HEAD +======= +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) def update_reference_in_payment_entry(d, payment_entry, do_not_save=False): reference_details = { @@ -730,7 +788,11 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no): @frappe.whitelist() def get_company_default(company, fieldname, ignore_validation=False): +<<<<<<< HEAD value = frappe.get_cached_value("Company", company, fieldname) +======= + value = frappe.get_cached_value('Company', company, fieldname) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) if not ignore_validation and not value: throw( diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 98aa28abbea2..f2bbbfc8f872 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -2135,6 +2135,7 @@ def get_advance_journal_entries( return list(journal_entries) +<<<<<<< HEAD def get_advance_payment_entries( party_type, party, @@ -2146,6 +2147,10 @@ def get_advance_payment_entries( limit=None, condition=None, ): +======= +def get_advance_payment_entries(party_type, party, party_account, order_doctype, + order_list=None, include_unallocated=True, against_all_orders=False, limit=None, condition=None): +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) 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" @@ -2188,8 +2193,12 @@ def get_advance_payment_entries( ) if include_unallocated: +<<<<<<< HEAD unallocated_payment_entries = frappe.db.sql( """ +======= + unallocated_payment_entries = frappe.db.sql(""" +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) 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` @@ -2197,12 +2206,17 @@ def get_advance_payment_entries( {0} = %s and party_type = %s and party = %s and payment_type = %s and docstatus = 1 and unallocated_amount > 0 {condition} order by posting_date {1} +<<<<<<< HEAD """.format( party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or "" ), (party_account, party_type, party, payment_type), as_dict=1, ) +======= + """.format(party_account_field, limit_cond, exchange_rate_field, currency_field, condition=condition or ""), + (party_account, party_type, party, payment_type), as_dict=1) +>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128)) return list(payment_entries_against_order) + list(unallocated_payment_entries)