Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: payments irrespective of party types (backport #37828) #38630

Open
wants to merge 11 commits into
base: version-14-hotfix
Choose a base branch
from
72 changes: 24 additions & 48 deletions erpnext/accounts/doctype/payment_entry/payment_entry.py
Expand Up @@ -31,6 +31,7 @@
get_account_currency,
get_balance_on,
get_outstanding_invoices,
get_party_types_from_account_type,
)
from erpnext.controllers.accounts_controller import (
AccountsController,
Expand Down Expand Up @@ -80,7 +81,6 @@ def validate(self):
self.apply_taxes()
self.set_amounts_after_tax()
self.clear_unallocated_reference_document_rows()
self.validate_payment_against_negative_invoice()
self.validate_transaction_reference()
self.set_title()
self.set_remarks()
Expand Down Expand Up @@ -909,38 +909,6 @@ def clear_unallocated_reference_document_rows(self):
self.name,
)

def validate_payment_against_negative_invoice(self):
if (self.payment_type != "Pay" or self.party_type != "Customer") and (
self.payment_type != "Receive" or self.party_type != "Supplier"
):
return

total_negative_outstanding = flt(
sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
),
self.references[0].precision("outstanding_amount") if self.references else None,
)

paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum(flt(d.amount) for d in self.deductions)

if not total_negative_outstanding:
if self.party_type == "Customer":
msg = _("Cannot pay to Customer without any negative outstanding invoice")
else:
msg = _("Cannot receive from Supplier without any negative outstanding invoice")

frappe.throw(msg, InvalidPaymentEntry)

elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
fmt_money(total_negative_outstanding)
),
InvalidPaymentEntry,
)

def set_title(self):
if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import.
Expand Down Expand Up @@ -1041,40 +1009,48 @@ def add_party_gl_entries(self, gl_entries):
item=self,
)

dr_or_cr = (
"credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit"
)

for d in self.get("references"):
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")

gle = party_gl_dict.copy()
gle.update(
{
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
"cost_center": cost_center,
}
)

allocated_amount_in_company_currency = self.calculate_base_allocated_amount_for_reference(d)
reverse_dr_or_cr = 0

if d.reference_doctype in ["Sales Invoice", "Purchase Invoice"]:
is_return = frappe.db.get_value(d.reference_doctype, d.reference_name, "is_return")
payable_party_types = get_party_types_from_account_type("Payable")
receivable_party_types = get_party_types_from_account_type("Receivable")
if is_return and self.party_type in receivable_party_types and (self.payment_type == "Pay"):
reverse_dr_or_cr = 1
elif (
is_return and self.party_type in payable_party_types and (self.payment_type == "Receive")
):
reverse_dr_or_cr = 1

if is_return and not reverse_dr_or_cr:
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"

gle.update(
{
dr_or_cr + "_in_account_currency": d.allocated_amount,
dr_or_cr: allocated_amount_in_company_currency,
dr_or_cr: abs(allocated_amount_in_company_currency),
dr_or_cr + "_in_account_currency": abs(d.allocated_amount),
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name,
"cost_center": cost_center,
}
)

gl_entries.append(gle)

if self.unallocated_amount:
dr_or_cr = "credit" if self.payment_type == "Receive" else "debit"
exchange_rate = self.get_exchange_rate()
base_unallocated_amount = self.unallocated_amount * exchange_rate

gle = party_gl_dict.copy()

gle.update(
{
dr_or_cr + "_in_account_currency": self.unallocated_amount,
Expand Down
46 changes: 33 additions & 13 deletions erpnext/accounts/doctype/payment_entry/test_payment_entry.py
Expand Up @@ -686,17 +686,6 @@ def test_internal_transfer_usd_to_inr(self):
self.validate_gl_entries(pe.name, expected_gle)

def test_payment_against_negative_sales_invoice(self):
pe1 = frappe.new_doc("Payment Entry")
pe1.payment_type = "Pay"
pe1.company = "_Test Company"
pe1.party_type = "Customer"
pe1.party = "_Test Customer"
pe1.paid_from = "_Test Cash - _TC"
pe1.paid_amount = 100
pe1.received_amount = 100

self.assertRaises(InvalidPaymentEntry, pe1.validate)

si1 = create_sales_invoice()

# create full payment entry against si1
Expand All @@ -711,8 +700,6 @@ def test_payment_against_negative_sales_invoice(self):

# pay more than outstanding against si1
pe3 = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Cash - _TC")
pe3.paid_amount = pe3.received_amount = 300
self.assertRaises(InvalidPaymentEntry, pe3.validate)

# pay negative outstanding against si1
pe3.paid_to = "Debtors - _TC"
Expand Down Expand Up @@ -1331,6 +1318,39 @@ def assertPLEntries(self, payment_doc, expected_pl_entries):
expected_out_str = json.dumps(sorted(expected_pl_entries, key=json.dumps))
self.assertEqual(out_str, expected_out_str)

def test_receive_payment_from_payable_party_type(self):
pe = create_payment_entry(
party_type="Supplier",
party="_Test Supplier",
payment_type="Receive",
paid_from="Creditors - _TC",
paid_to="_Test Cash - _TC",
save=True,
submit=True,
)
self.voucher_no = pe.name
self.expected_gle = [
{"account": "_Test Cash - _TC", "debit": 1000.0, "credit": 0.0},
{"account": "Creditors - _TC", "debit": 0.0, "credit": 1000.0},
]
self.check_gl_entries()

def check_gl_entries(self):
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(
gle.account,
gle.debit,
gle.credit,
)
.where((gle.voucher_no == self.voucher_no) & (gle.is_cancelled == 0))
.orderby(gle.account)
).run(as_dict=True)
for row in range(len(self.expected_gle)):
for field in ["account", "debit", "credit"]:
self.assertEqual(self.expected_gle[row][field], gl_entries[row][field])


def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
Expand Down
Expand Up @@ -14,7 +14,7 @@
get_accounting_dimensions,
get_dimension_with_children,
)
from erpnext.accounts.utils import get_currency_precision
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type

# This report gives a summary of all Outstanding Invoices considering the following

Expand Down Expand Up @@ -72,9 +72,7 @@ def set_defaults(self):
self.currency_precision = get_currency_precision() or 2
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
self.account_type = self.filters.account_type
self.party_type = frappe.db.get_all(
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_type = get_party_types_from_account_type(self.account_type)
self.party_details = {}
self.invoices = set()
self.skip_total_row = 0
Expand Down
Expand Up @@ -8,7 +8,7 @@

from erpnext.accounts.party import get_partywise_advanced_payment_amount
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
from erpnext.accounts.utils import get_currency_precision
from erpnext.accounts.utils import get_currency_precision, get_party_types_from_account_type


def execute(filters=None):
Expand All @@ -23,9 +23,7 @@ def execute(filters=None):
class AccountsReceivableSummary(ReceivablePayableReport):
def run(self, args):
self.account_type = args.get("account_type")
self.party_type = frappe.db.get_all(
"Party Type", {"account_type": self.account_type}, pluck="name"
)
self.party_type = get_party_types_from_account_type(self.account_type)
self.party_naming_by = frappe.db.get_value(
args.get("naming_by")[0], None, args.get("naming_by")[1]
)
Expand Down
4 changes: 4 additions & 0 deletions erpnext/accounts/utils.py
Expand Up @@ -2039,3 +2039,7 @@ def create_gain_loss_journal(
journal_entry.save()
journal_entry.submit()
return journal_entry.name


def get_party_types_from_account_type(account_type):
return frappe.db.get_all("Party Type", {"account_type": account_type}, pluck="name")