Compare commits

..

2 Commits

Author SHA1 Message Date
Anuja Pawar
601543ba0a refactor: payment reconciliation tool (#27128)
(cherry picked from commit 3e404f15ff)

# 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
2025-02-24 00:15:59 +00:00
mergify[bot]
f5160dc83d fix: use Stock Qty while getting POS Reserved Qty (backport #38962) (#38983)
fix: use `Stock Qty` while getting `POS Reserved Qty`

(cherry picked from commit 7223106417)

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
2023-12-28 12:00:04 +05:30
10 changed files with 513 additions and 3 deletions

View File

@@ -4,7 +4,7 @@ import frappe
from erpnext.hooks import regional_overrides
__version__ = "13.55.2"
__version__ = "13.54.4"
def get_default_company(user=None):

View File

@@ -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))

View File

@@ -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
}

View File

@@ -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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
ELSE {bank_account_condition}
END)
order by t1.posting_date
<<<<<<< HEAD
""".format(
**{
"dr_or_cr": dr_or_cr,
@@ -111,6 +142,13 @@ class PaymentReconciliation(Document):
}
),
{
=======
""".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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
"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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
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 @@ class PaymentReconciliation(Document):
"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()

View File

@@ -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))

View File

@@ -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",

View File

@@ -4,6 +4,9 @@
# import frappe
from frappe.model.document import Document
<<<<<<< HEAD
=======
>>>>>>> 3e404f15ff (refactor: payment reconciliation tool (#27128))
class PaymentReconciliationAllocation(Document):
pass

View File

@@ -704,7 +704,7 @@ def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = (
frappe.qb.from_(p_inv)
.from_(p_item)
.select(Sum(p_item.qty).as_("qty"))
.select(Sum(p_item.stock_qty).as_("stock_qty"))
.where(
(p_inv.name == p_item.parent)
& (IfNull(p_inv.consolidated_invoice, "") == "")
@@ -715,7 +715,7 @@ def get_pos_reserved_qty(item_code, warehouse):
)
).run(as_dict=True)
return reserved_qty[0].qty or 0 if reserved_qty else 0
return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
@frappe.whitelist()

View File

@@ -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(

View File

@@ -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)