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 payment schedule outstanding #251

Merged
merged 5 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions check_run/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,14 @@
"Payment Entry": {
"validate": [
"check_run.overrides.payment_entry.validate_duplicate_check_number",
"check_run.overrides.payment_entry.validate_add_payment_term",
],
"on_submit": ["check_run.overrides.payment_entry.update_check_number"],
"on_submit": [
"check_run.overrides.payment_entry.update_outstanding_amount",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't we need to reverse this in on_cancel?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fproldan I added the hook and some tests for this since it was a bit of a refactor. Can you review again?

"check_run.overrides.payment_entry.update_check_number",
],
"on_cancel": [
"check_run.overrides.payment_entry.update_outstanding_amount",
],
},
"Purchase Invoice": {
"before_cancel": ["check_run.check_run.disallow_cancellation_if_in_check_run"]
Expand Down
74 changes: 49 additions & 25 deletions check_run/overrides/payment_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
# For license information, please see license.txt

import frappe
from frappe.utils import get_link_to_form, comma_and, flt
from frappe.utils import get_link_to_form, flt
from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map
from frappe.utils.data import getdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
PaymentEntry,
get_outstanding_reference_documents,
)
from frappe import _
import json


class CheckRunPaymentEntry(PaymentEntry):
Expand Down Expand Up @@ -278,28 +277,53 @@ def validate_duplicate_check_number(doc: PaymentEntry, method: str | None = None


@frappe.whitelist()
def validate_add_payment_term(doc: PaymentEntry, method: str | None = None):
doc = frappe._dict(json.loads(doc)) if isinstance(doc, str) else doc
if doc.check_run:
return
adjusted_refs = []
def update_outstanding_amount(doc: PaymentEntry, method: str | None = None):
paid_amount = doc.paid_amount if method == "on_submit" else 0.0
for r in doc.get("references"):
if r.reference_doctype == "Purchase Invoice" and not r.payment_term:
pmt_term = frappe.get_all(
"Payment Schedule",
{"parent": r.reference_name, "outstanding": [">", 0.0]},
["payment_term"],
order_by="due_date ASC",
limit=1,
)
if pmt_term:
r.payment_term = pmt_term[0].get("payment_term")
adjusted_refs.append(r.reference_name)
if adjusted_refs:
frappe.msgprint(
msg=frappe._(
f"An outstanding Payment Schedule term was detected and added for {comma_and(adjusted_refs)} in the references table.<br>Please review - "
"this field must be filled in for the Payment Schedule to synchronize and to prevent a paid invoice portion from showing up in a Check Run."
),
title=frappe._("Payment Schedule Term Added"),
if r.reference_doctype != "Purchase Invoice":
continue
payment_schedules = frappe.get_all(
"Payment Schedule",
{"parent": r.reference_name},
["name", "outstanding", "payment_term", "payment_amount"],
order_by="due_date ASC",
)
if not payment_schedules:
continue

payment_schedule = frappe.get_doc("Payment Schedule", payment_schedules[0]["name"])
precision = payment_schedule.precision("outstanding")
payment_schedules = payment_schedules if method == "on_submit" else reversed(payment_schedules)

for term in payment_schedules:
if r.payment_term and term.payment_term != r.payment_term:
continue

if method == "on_submit":
if term.outstanding > 0.0 and paid_amount > 0.0:
if term.outstanding > paid_amount:
frappe.db.set_value(
"Payment Schedule",
term.name,
"outstanding",
flt(term.outstanding - paid_amount, precision),
)
break
else:
paid_amount = flt(paid_amount - term.outstanding, precision)
frappe.db.set_value("Payment Schedule", term.name, "outstanding", 0)
if paid_amount <= 0.0:
break

if method == "on_cancel":
if term.outstanding != term.payment_amount:
# if this payment term had previously been allocated against
paid_amount += flt(paid_amount + (term.payment_amount - term.outstanding), precision)
reverse = (
flt(paid_amount + term.outstanding, precision)
if paid_amount < term.payment_amount
else term.payment_amount
)
frappe.db.set_value("Payment Schedule", term.name, "outstanding", reverse)
if paid_amount >= doc.paid_amount:
break
2 changes: 1 addition & 1 deletion check_run/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"Phone Services",
"ACH/EFT",
250.00,
"Net 30",
"",
{
"address_line1": "1198 Carpenter Road",
"city": "Rolla",
Expand Down
2 changes: 2 additions & 0 deletions check_run/tests/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,8 @@ def create_invoices(settings):
"qty": 1,
},
)
if supplier[0].startswith("Sphere"):
pi.payment_terms_template = None
pi.save()
pi.submit()
# two electric meters / test invoice aggregation
Expand Down
21 changes: 11 additions & 10 deletions check_run/tests/test_check_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@

import frappe

from check_run.check_run.doctype.check_run.check_run import get_check_run_settings, get_entries
from check_run.check_run.doctype.check_run.check_run import (
get_check_run_settings,
get_entries,
check_for_draft_check_run,
)

year = datetime.date.today().year


@pytest.fixture
def cr(): # return draft check run
if (
frappe.db.exists("Check Run", f"ACC-CR-{year}-00001")
and frappe.get_value("Check Run", f"ACC-CR-{year}-00001", "docstatus") == 0
):
return frappe.get_doc("Check Run", f"ACC-CR-{year}-00001")
cr = frappe.new_doc("Check Run")
cr_name = check_for_draft_check_run(
company="Chelsea Fruit Co",
bank_account="Primary Checking - Local Bank",
payable_account="2110 - Accounts Payable - CFC",
)
cr = frappe.get_doc("Check Run", cr_name)
cr.flags.in_test = True
cr.company = "Chelsea Fruit Co"
cr.bank_account = "Primary Checking - Local Bank"
cr.pay_to_account = "2110 - Accounts Payable - CFC"
cr.posting_date = cr.end_date = datetime.date(year, 12, 31)
cr.set_last_check_number()
cr.set_default_payable_account()
Expand Down
190 changes: 190 additions & 0 deletions check_run/tests/test_payment_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import datetime
import pytest
import frappe

from check_run.check_run.doctype.check_run.check_run import (
get_check_run_settings,
get_entries,
check_for_draft_check_run,
)
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from check_run.tests.test_check_run import cr


year = datetime.date.today().year


def test_partial_payment_payment_entry_with_terms():
pi_name = frappe.get_all(
"Purchase Invoice",
{"supplier": "Exceptional Grid"},
pluck="name",
order_by="posting_date ASC",
limit=1,
)[0]
pe0 = get_payment_entry("Purchase Invoice", pi_name)
pe0.mode_of_payment = "Check"
pe0.paid_amount = 30.00
pe0.bank_account = "Primary Checking - Local Bank"
pe0.reference_no = frappe.get_value("Bank Account", pe0.bank_account, "check_number")
pe0.references[0].allocated_amount = 30.00
pe0.save()
pe0.submit()

pi = frappe.get_doc("Purchase Invoice", pi_name)
assert pi.payment_schedule[0].outstanding == 120.00
assert pi.outstanding_amount == 120.00

pe1 = get_payment_entry("Purchase Invoice", pi_name)
pe1.mode_of_payment = "Check"
pe1.paid_amount = 120.00
pe1.bank_account = "Primary Checking - Local Bank"
pe1.reference_no = frappe.get_value("Bank Account", pe1.bank_account, "check_number")
pe1.references[0].allocated_amount = 120.00
pe1.save()
pe1.submit()

pi = frappe.get_doc("Purchase Invoice", pi_name)
assert pi.payment_schedule[0].outstanding == 0.00
assert pi.outstanding_amount == 0.0


def test_payment_payment_entry_of_multiple_terms():
pi_name = frappe.get_all(
"Purchase Invoice",
{"supplier": "Tireless Equipment Rental, Inc"},
pluck="name",
order_by="posting_date ASC",
limit=1,
)[0]
pe0 = get_payment_entry("Purchase Invoice", pi_name)
pe0.mode_of_payment = "Check"
pe0.paid_amount = 4500.00
pe0.bank_account = "Primary Checking - Local Bank"
pe0.reference_no = frappe.get_value("Bank Account", pe0.bank_account, "check_number")
pe0.references[0].allocated_amount = 4500
pe0.save()
pe0.submit()

pi = frappe.get_doc("Purchase Invoice", pi_name)
assert pi.payment_schedule[0].outstanding == 0.0
assert pi.payment_schedule[1].outstanding == 0.0
assert pi.payment_schedule[2].outstanding == 500.01

pe0.cancel()
pi.reload()
assert pi.payment_schedule[2].outstanding == 1666.67
assert pi.payment_schedule[1].outstanding == 1666.67
assert pi.payment_schedule[0].outstanding == 1666.67


def test_partial_payment_payment_entry_without_terms():
pi_name = frappe.get_all(
"Purchase Invoice",
{"supplier": "Sphere Cellular"},
pluck="name",
order_by="posting_date ASC",
limit=1,
)[0]
pi = frappe.get_doc("Purchase Invoice", pi_name)
assert pi.payment_schedule[0].outstanding == 250.00
assert pi.outstanding_amount == 250.00

pe0 = get_payment_entry("Purchase Invoice", pi_name)
pe0.mode_of_payment = "Check"
pe0.paid_amount = 100.00
pe0.bank_account = "Primary Checking - Local Bank"
pe0.reference_no = frappe.get_value("Bank Account", pe0.bank_account, "check_number")
pe0.references[0].allocated_amount = 100.00
pe0.save()
pe0.submit()

pi.reload()
assert pi.payment_schedule[0].outstanding == 150.00
assert pi.outstanding_amount == 150

pe1 = get_payment_entry("Purchase Invoice", pi_name)
pe1.mode_of_payment = "Check"
pe1.paid_amount = 100.00
pe1.bank_account = "Primary Checking - Local Bank"
pe1.reference_no = frappe.get_value("Bank Account", pe1.bank_account, "check_number")
pe1.references[0].allocated_amount = 100.00
pe1.save()
pe1.submit()

pi = frappe.get_doc("Purchase Invoice", pi_name)
assert pi.payment_schedule[0].outstanding == 50.00
assert pi.outstanding_amount == 50.00

pe2 = get_payment_entry("Purchase Invoice", pi_name)
pe2.mode_of_payment = "Check"
pe2.paid_amount = 100.00
pe2.bank_account = "Primary Checking - Local Bank"
pe2.reference_no = frappe.get_value("Bank Account", pe2.bank_account, "check_number")
pe2.references[0].allocated_amount = 100.00

pi = frappe.get_doc("Purchase Invoice", pi_name)
with pytest.raises(
frappe.exceptions.ValidationError,
# match='Allocated Amount of 100.0 cannot be greater than outstanding amount of 50.0',
):
pe2.save()

pe2.paid_amount = 50.00
pe2.references[0].allocated_amount = 50.00
pe2.save()
pe2.submit()

pi.reload()
assert pi.payment_schedule[0].outstanding == 00.00
assert pi.outstanding_amount == 0.00


def test_outstanding_amount_in_check_run(cr):
pi_name = frappe.get_all(
"Purchase Invoice",
{"supplier": "Mare Digitalis"},
pluck="name",
order_by="posting_date ASC",
limit=1,
)[0]
pi = frappe.get_doc("Purchase Invoice", pi_name)
assert pi.outstanding_amount == 200.00
assert pi.payment_schedule[0].outstanding == 200.00

pe0 = get_payment_entry("Purchase Invoice", pi_name)
pe0.mode_of_payment = "Check"
pe0.paid_amount = 110.00
pe0.bank_account = "Primary Checking - Local Bank"
pe0.reference_no = frappe.get_value("Bank Account", pe0.bank_account, "check_number")
pe0.references[0].allocated_amount = 110.00
pe0.save()
pe0.submit()
pi.reload()
assert pi.payment_schedule[0].outstanding == 90.00
assert pi.outstanding_amount == 90.00

cr.transactions = None
cr.save()
entries = get_entries(cr)
for row in entries.get("transactions"):
row["pay"] = False
transactions = frappe.utils.safe_json_loads(entries.get("transactions"))

t = list(filter(lambda x: x.get("name") == f"ACC-PINV-{year}-00004", transactions))
assert t[0].get("amount") == 90.00

pe0.cancel()
pi.reload()
assert pi.payment_schedule[0].outstanding == 200.00
assert pi.outstanding_amount == 200.00

cr.transactions = None
cr.save()
entries = get_entries(cr)
for row in entries.get("transactions"):
row["pay"] = False
transactions = frappe.utils.safe_json_loads(entries.get("transactions"))

t = list(filter(lambda x: x.get("name") == f"ACC-PINV-{year}-00004", transactions))
assert t[0].get("amount") == 200.00
Loading