mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-30 15:09:47 +00:00
refactor: extract billing, payment schedule, and exchange gain/loss into services
Move billing validation, payment schedule, and exchange gain/loss logic from AccountsController into dedicated service modules under accounts/services/. AccountsController retains thin shim methods that delegate to the services.
This commit is contained in:
147
erpnext/accounts/services/billing_validation.py
Normal file
147
erpnext/accounts/services/billing_validation.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
"""Billing amount validation helpers (overbilling checks)."""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
from frappe.utils import cint, flt, fmt_money
|
||||||
|
|
||||||
|
|
||||||
|
def validate_multiple_billing(doc, ref_dt: str, item_ref_dn: str, based_on: str) -> None:
|
||||||
|
from erpnext.controllers.status_updater import get_allowance_for
|
||||||
|
|
||||||
|
ref_wise_billed_amount = get_reference_wise_billed_amt(doc, ref_dt, item_ref_dn, based_on)
|
||||||
|
if not ref_wise_billed_amount:
|
||||||
|
return
|
||||||
|
|
||||||
|
total_overbilled_amt = 0.0
|
||||||
|
overbilled_items = []
|
||||||
|
precision = doc.precision(based_on, "items")
|
||||||
|
precision_allowance = 1 / (10**precision)
|
||||||
|
|
||||||
|
role_allowed_to_overbill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
|
||||||
|
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
|
||||||
|
|
||||||
|
for row in ref_wise_billed_amount.values():
|
||||||
|
total_billed_amt = row.billed_amt
|
||||||
|
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
|
||||||
|
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
|
||||||
|
|
||||||
|
if total_billed_amt < 0 and max_allowed_amt < 0:
|
||||||
|
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
|
||||||
|
|
||||||
|
overbill_amt = total_billed_amt - max_allowed_amt
|
||||||
|
row["max_allowed_amt"] = max_allowed_amt
|
||||||
|
total_overbilled_amt += overbill_amt
|
||||||
|
|
||||||
|
if overbill_amt > precision_allowance and not is_overbilling_allowed:
|
||||||
|
if doc.doctype != "Purchase Invoice" or not cint(
|
||||||
|
frappe.db.get_single_value(
|
||||||
|
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
||||||
|
)
|
||||||
|
):
|
||||||
|
overbilled_items.append(row)
|
||||||
|
|
||||||
|
if overbilled_items:
|
||||||
|
throw_overbill_exception(doc, overbilled_items, precision)
|
||||||
|
|
||||||
|
if is_overbilling_allowed and total_overbilled_amt > 0.1:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Overbilling of {} ignored because you have {} role.").format(
|
||||||
|
total_overbilled_amt, role_allowed_to_overbill
|
||||||
|
),
|
||||||
|
indicator="orange",
|
||||||
|
alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_reference_wise_billed_amt(doc, ref_dt: str, item_ref_dn: str, based_on: str) -> dict | None:
|
||||||
|
"""Return sum of billed amounts per reference row, including previously submitted invoices."""
|
||||||
|
reference_names = [d.get(item_ref_dn) for d in doc.items if d.get(item_ref_dn)]
|
||||||
|
if not reference_names:
|
||||||
|
return
|
||||||
|
|
||||||
|
precision = doc.precision(based_on, "items")
|
||||||
|
reference_details = get_billing_reference_details(doc, reference_names, ref_dt + " Item", based_on)
|
||||||
|
already_billed = get_already_billed_amount(doc, reference_names, item_ref_dn, based_on)
|
||||||
|
|
||||||
|
ref_wise_billed_amount = {}
|
||||||
|
for item in doc.items:
|
||||||
|
key = item.get(item_ref_dn)
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
ref_amt = flt(reference_details.get(key), precision)
|
||||||
|
current_amount = flt(item.get(based_on), precision)
|
||||||
|
|
||||||
|
if not ref_amt:
|
||||||
|
if current_amount:
|
||||||
|
frappe.msgprint(
|
||||||
|
_("System will not check over billing since amount for Item {0} in {1} is zero").format(
|
||||||
|
item.item_code, ref_dt
|
||||||
|
),
|
||||||
|
title=_("Warning"),
|
||||||
|
indicator="orange",
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
ref_wise_billed_amount.setdefault(
|
||||||
|
key,
|
||||||
|
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
|
||||||
|
)
|
||||||
|
ref_wise_billed_amount[key]["rows"].append(item.idx)
|
||||||
|
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
|
||||||
|
ref_wise_billed_amount[key]["billed_amt"] += current_amount
|
||||||
|
if key in already_billed:
|
||||||
|
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
|
||||||
|
|
||||||
|
return ref_wise_billed_amount
|
||||||
|
|
||||||
|
|
||||||
|
def get_billing_reference_details(
|
||||||
|
doc, reference_names: list, reference_doctype: str, based_on: str
|
||||||
|
) -> frappe._dict:
|
||||||
|
return frappe._dict(
|
||||||
|
frappe.get_all(
|
||||||
|
reference_doctype,
|
||||||
|
filters={"name": ("in", reference_names)},
|
||||||
|
fields=["name", based_on],
|
||||||
|
as_list=1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_already_billed_amount(doc, reference_names: list, item_ref_dn: str, based_on: str) -> frappe._dict:
|
||||||
|
item_doctype = frappe.qb.DocType(doc.items[0].doctype)
|
||||||
|
based_on_field = frappe.qb.Field(based_on)
|
||||||
|
join_field = frappe.qb.Field(item_ref_dn)
|
||||||
|
|
||||||
|
return frappe._dict(
|
||||||
|
(
|
||||||
|
frappe.qb.from_(item_doctype)
|
||||||
|
.select(join_field, Sum(based_on_field))
|
||||||
|
.where(join_field.isin(reference_names))
|
||||||
|
.where((item_doctype.docstatus == 1) & (item_doctype.parent != doc.name))
|
||||||
|
.groupby(join_field)
|
||||||
|
).run()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def throw_overbill_exception(doc, overbilled_items: list, precision: int) -> None:
|
||||||
|
message = (
|
||||||
|
_("<p>Cannot overbill for the following Items:</p>")
|
||||||
|
+ "<ul>"
|
||||||
|
+ "".join(
|
||||||
|
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
|
||||||
|
frappe.bold(item.item_code),
|
||||||
|
", ".join(str(x) for x in item.rows),
|
||||||
|
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=doc.currency)),
|
||||||
|
)
|
||||||
|
for item in overbilled_items
|
||||||
|
)
|
||||||
|
+ "</ul>"
|
||||||
|
)
|
||||||
|
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
|
||||||
|
frappe.throw(_(message))
|
||||||
237
erpnext/accounts/services/exchange_gain_loss.py
Normal file
237
erpnext/accounts/services/exchange_gain_loss.py
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
"""Exchange gain/loss journal helpers."""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _, qb
|
||||||
|
from frappe.utils import flt, get_link_to_form
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||||
|
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||||
|
from erpnext.accounts.utils import create_gain_loss_journal, get_currency_precision
|
||||||
|
|
||||||
|
|
||||||
|
def make_precision_loss_gl_entry(doc, gl_entries: list) -> None:
|
||||||
|
round_off_account, round_off_cost_center, _ = get_round_off_account_and_cost_center(
|
||||||
|
doc.company, "Purchase Invoice", doc.name, doc.use_company_roundoff_cost_center
|
||||||
|
)
|
||||||
|
|
||||||
|
precision_loss = doc.get("base_net_total") - flt(
|
||||||
|
doc.get("net_total") * doc.conversion_rate, doc.precision("net_total")
|
||||||
|
)
|
||||||
|
|
||||||
|
credit_or_debit = "credit" if doc.doctype == "Purchase Invoice" else "debit"
|
||||||
|
against = doc.supplier if doc.doctype == "Purchase Invoice" else doc.customer
|
||||||
|
|
||||||
|
if precision_loss:
|
||||||
|
gl_entries.append(
|
||||||
|
doc.get_gl_dict(
|
||||||
|
{
|
||||||
|
"account": round_off_account,
|
||||||
|
"against": against,
|
||||||
|
credit_or_debit: precision_loss,
|
||||||
|
"cost_center": round_off_cost_center
|
||||||
|
if doc.use_company_roundoff_cost_center
|
||||||
|
else doc.cost_center or round_off_cost_center,
|
||||||
|
"remarks": _("Net total calculation precision loss"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def gain_loss_journal_already_booked(
|
||||||
|
gain_loss_account: str,
|
||||||
|
exc_gain_loss: float,
|
||||||
|
ref2_dt: str,
|
||||||
|
ref2_dn: str,
|
||||||
|
ref2_detail_no: str,
|
||||||
|
) -> bool:
|
||||||
|
"""Check if a gain/loss journal has already been booked for the given parameters."""
|
||||||
|
if res := frappe.db.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={
|
||||||
|
"docstatus": 1,
|
||||||
|
"account": gain_loss_account,
|
||||||
|
"reference_type": ref2_dt,
|
||||||
|
"reference_name": ref2_dn,
|
||||||
|
"reference_detail_no": ref2_detail_no,
|
||||||
|
},
|
||||||
|
pluck="parent",
|
||||||
|
):
|
||||||
|
res = list({x for x in res})
|
||||||
|
if exc_vouchers := frappe.db.get_all(
|
||||||
|
"Journal Entry",
|
||||||
|
filters={"name": ["in", res], "voucher_type": "Exchange Gain Or Loss"},
|
||||||
|
fields=["voucher_type", "total_debit", "total_credit"],
|
||||||
|
):
|
||||||
|
booked_voucher = exc_vouchers[0]
|
||||||
|
if (
|
||||||
|
booked_voucher.total_debit == exc_gain_loss
|
||||||
|
and booked_voucher.total_credit == exc_gain_loss
|
||||||
|
and booked_voucher.voucher_type == "Exchange Gain Or Loss"
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def make_exchange_gain_loss_journal(
|
||||||
|
doc, args: dict | None = None, dimensions_dict: dict | None = None
|
||||||
|
) -> None:
|
||||||
|
"""Make Exchange Gain/Loss journal for Invoices and Payments."""
|
||||||
|
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
||||||
|
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
||||||
|
if doc.docstatus != 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
if dimensions_dict is None:
|
||||||
|
dimensions_dict = frappe._dict()
|
||||||
|
active_dimensions = get_dimensions()[0]
|
||||||
|
for dim in active_dimensions:
|
||||||
|
dimensions_dict[dim.fieldname] = doc.get(dim.fieldname)
|
||||||
|
|
||||||
|
if doc.get("doctype") == "Journal Entry":
|
||||||
|
if args:
|
||||||
|
precision = get_currency_precision()
|
||||||
|
for arg in args:
|
||||||
|
if (
|
||||||
|
flt(arg.get("difference_amount", 0), precision) != 0
|
||||||
|
or flt(arg.get("exchange_gain_loss", 0), precision) != 0
|
||||||
|
) and arg.get("difference_account"):
|
||||||
|
party_account = arg.get("account")
|
||||||
|
gain_loss_account = arg.get("difference_account")
|
||||||
|
difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss")
|
||||||
|
if difference_amount > 0:
|
||||||
|
dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit"
|
||||||
|
else:
|
||||||
|
dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit"
|
||||||
|
|
||||||
|
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
|
if not gain_loss_journal_already_booked(
|
||||||
|
gain_loss_account,
|
||||||
|
difference_amount,
|
||||||
|
doc.doctype,
|
||||||
|
doc.name,
|
||||||
|
arg.get("referenced_row"),
|
||||||
|
):
|
||||||
|
posting_date = arg.get("difference_posting_date") or frappe.db.get_value(
|
||||||
|
arg.voucher_type, arg.voucher_no, "posting_date"
|
||||||
|
)
|
||||||
|
je = create_gain_loss_journal(
|
||||||
|
doc.company,
|
||||||
|
posting_date,
|
||||||
|
arg.get("party_type"),
|
||||||
|
arg.get("party"),
|
||||||
|
party_account,
|
||||||
|
gain_loss_account,
|
||||||
|
difference_amount,
|
||||||
|
dr_or_cr,
|
||||||
|
reverse_dr_or_cr,
|
||||||
|
arg.get("against_voucher_type"),
|
||||||
|
arg.get("against_voucher"),
|
||||||
|
arg.get("idx"),
|
||||||
|
doc.doctype,
|
||||||
|
doc.name,
|
||||||
|
arg.get("referenced_row"),
|
||||||
|
arg.get("cost_center"),
|
||||||
|
dimensions_dict,
|
||||||
|
arg.get("project"),
|
||||||
|
)
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||||
|
get_link_to_form("Journal Entry", je)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if doc.get("doctype") == "Payment Entry":
|
||||||
|
gain_loss_to_book = [x for x in doc.references if x.exchange_gain_loss != 0]
|
||||||
|
booked = []
|
||||||
|
if gain_loss_to_book:
|
||||||
|
je = qb.DocType("Journal Entry")
|
||||||
|
jea = qb.DocType("Journal Entry Account")
|
||||||
|
parents = (
|
||||||
|
qb.from_(jea)
|
||||||
|
.select(jea.parent)
|
||||||
|
.where(
|
||||||
|
(jea.reference_type == "Payment Entry")
|
||||||
|
& (jea.reference_name == doc.name)
|
||||||
|
& (jea.docstatus == 1)
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
|
||||||
|
if parents:
|
||||||
|
booked = (
|
||||||
|
qb.from_(je)
|
||||||
|
.inner_join(jea)
|
||||||
|
.on(je.name == jea.parent)
|
||||||
|
.select(jea.reference_type, jea.reference_name, jea.reference_detail_no)
|
||||||
|
.where(
|
||||||
|
(je.docstatus == 1)
|
||||||
|
& (je.name.isin(parents))
|
||||||
|
& (je.voucher_type == "Exchange Gain or Loss")
|
||||||
|
)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in gain_loss_to_book:
|
||||||
|
if d.exchange_gain_loss and ((d.reference_doctype, d.reference_name, str(d.idx)) not in booked):
|
||||||
|
if doc.book_advance_payments_in_separate_party_account:
|
||||||
|
party_account = d.account
|
||||||
|
else:
|
||||||
|
if doc.payment_type == "Receive":
|
||||||
|
party_account = doc.paid_from
|
||||||
|
elif doc.payment_type == "Pay":
|
||||||
|
party_account = doc.paid_to
|
||||||
|
|
||||||
|
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
||||||
|
|
||||||
|
if is_payable_account(d.reference_doctype, party_account):
|
||||||
|
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
|
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||||
|
|
||||||
|
gain_loss_account = frappe.get_cached_value(
|
||||||
|
"Company", doc.company, "exchange_gain_loss_account"
|
||||||
|
)
|
||||||
|
je = create_gain_loss_journal(
|
||||||
|
doc.company,
|
||||||
|
args.get("difference_posting_date") if args else doc.posting_date,
|
||||||
|
doc.party_type,
|
||||||
|
doc.party,
|
||||||
|
party_account,
|
||||||
|
gain_loss_account,
|
||||||
|
d.exchange_gain_loss,
|
||||||
|
dr_or_cr,
|
||||||
|
reverse_dr_or_cr,
|
||||||
|
d.reference_doctype,
|
||||||
|
d.reference_name,
|
||||||
|
d.idx,
|
||||||
|
doc.doctype,
|
||||||
|
doc.name,
|
||||||
|
d.idx,
|
||||||
|
doc.cost_center,
|
||||||
|
dimensions_dict,
|
||||||
|
doc.project,
|
||||||
|
)
|
||||||
|
frappe.msgprint(
|
||||||
|
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||||
|
get_link_to_form("Journal Entry", je)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_payable_account(reference_doctype: str, account: str) -> bool:
|
||||||
|
if reference_doctype == "Purchase Invoice" or (
|
||||||
|
reference_doctype == "Journal Entry"
|
||||||
|
and frappe.get_cached_value("Account", account, "account_type") == "Payable"
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def set_transaction_currency_and_rate_in_gl_map(doc, gl_entries: list) -> None:
|
||||||
|
for entry in gl_entries:
|
||||||
|
entry["transaction_currency"] = doc.currency
|
||||||
|
entry["transaction_exchange_rate"] = doc.get("conversion_rate") or 1
|
||||||
373
erpnext/accounts/services/payment_schedule.py
Normal file
373
erpnext/accounts/services/payment_schedule.py
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
"""Payment schedule and payment terms helpers."""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import DateTimeLikeObject, add_days, add_months, cint, flt, get_last_day, getdate
|
||||||
|
|
||||||
|
from erpnext.accounts.party import get_party_account_currency
|
||||||
|
|
||||||
|
|
||||||
|
def set_payment_schedule(doc) -> None:
|
||||||
|
if (doc.doctype == "Sales Invoice" and doc.is_pos) or doc.get("is_opening") == "Yes":
|
||||||
|
doc.payment_terms_template = ""
|
||||||
|
return
|
||||||
|
|
||||||
|
party_account_currency = doc.get("party_account_currency")
|
||||||
|
if not party_account_currency:
|
||||||
|
party_type, party = doc.get_party()
|
||||||
|
if party_type and party:
|
||||||
|
party_account_currency = get_party_account_currency(party_type, party, doc.company)
|
||||||
|
|
||||||
|
posting_date = doc.get("bill_date") or doc.get("posting_date") or doc.get("transaction_date")
|
||||||
|
due_date = doc.get("due_date") or posting_date
|
||||||
|
|
||||||
|
base_grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
||||||
|
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
|
||||||
|
automatically_fetch_payment_terms = 0
|
||||||
|
|
||||||
|
if doc.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
|
||||||
|
po_or_so, doctype, fieldname = get_order_details(doc)
|
||||||
|
automatically_fetch_payment_terms = cint(
|
||||||
|
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
|
||||||
|
)
|
||||||
|
if doc.doctype != "Sales Order":
|
||||||
|
base_grand_total = base_grand_total - flt(doc.base_write_off_amount)
|
||||||
|
grand_total = grand_total - flt(doc.write_off_amount)
|
||||||
|
|
||||||
|
if doc.get("total_advance"):
|
||||||
|
if party_account_currency == doc.company_currency:
|
||||||
|
base_grand_total -= doc.get("total_advance")
|
||||||
|
grand_total = flt(base_grand_total / doc.get("conversion_rate"), doc.precision("grand_total"))
|
||||||
|
else:
|
||||||
|
grand_total -= doc.get("total_advance")
|
||||||
|
base_grand_total = flt(
|
||||||
|
grand_total * doc.get("conversion_rate"), doc.precision("base_grand_total")
|
||||||
|
)
|
||||||
|
|
||||||
|
if not doc.get("payment_schedule"):
|
||||||
|
if (
|
||||||
|
doc.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
|
||||||
|
and automatically_fetch_payment_terms
|
||||||
|
and linked_order_has_payment_terms(doc, po_or_so, fieldname, doctype)
|
||||||
|
):
|
||||||
|
fetch_payment_terms_from_order(
|
||||||
|
doc, po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||||
|
)
|
||||||
|
if doc.get("payment_terms_template"):
|
||||||
|
doc.ignore_default_payment_terms_template = 1
|
||||||
|
elif doc.get("payment_terms_template"):
|
||||||
|
data = get_payment_terms(doc.payment_terms_template, posting_date, grand_total, base_grand_total)
|
||||||
|
for item in data:
|
||||||
|
doc.append("payment_schedule", item)
|
||||||
|
elif doc.doctype not in ["Purchase Receipt"]:
|
||||||
|
doc.append(
|
||||||
|
"payment_schedule",
|
||||||
|
dict(
|
||||||
|
due_date=due_date,
|
||||||
|
invoice_portion=100,
|
||||||
|
payment_amount=grand_total,
|
||||||
|
base_payment_amount=base_grand_total,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||||
|
"Payment Terms Template",
|
||||||
|
doc.payment_terms_template,
|
||||||
|
"allocate_payment_based_on_payment_terms",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (
|
||||||
|
automatically_fetch_payment_terms
|
||||||
|
and allocate_payment_based_on_payment_terms
|
||||||
|
and linked_order_has_payment_terms(doc, po_or_so, fieldname, doctype)
|
||||||
|
):
|
||||||
|
for d in doc.get("payment_schedule"):
|
||||||
|
if d.invoice_portion:
|
||||||
|
d.payment_amount = flt(
|
||||||
|
grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
|
||||||
|
)
|
||||||
|
d.base_payment_amount = flt(
|
||||||
|
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
||||||
|
)
|
||||||
|
d.outstanding = d.payment_amount
|
||||||
|
d.base_outstanding = d.base_payment_amount
|
||||||
|
elif not d.invoice_portion:
|
||||||
|
d.base_payment_amount = flt(
|
||||||
|
d.payment_amount * doc.get("conversion_rate"), d.precision("base_payment_amount")
|
||||||
|
)
|
||||||
|
d.base_outstanding = d.base_payment_amount
|
||||||
|
else:
|
||||||
|
fetch_payment_terms_from_order(
|
||||||
|
doc, po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||||
|
)
|
||||||
|
doc.ignore_default_payment_terms_template = 1
|
||||||
|
|
||||||
|
|
||||||
|
def get_order_details(doc) -> tuple:
|
||||||
|
if not doc.get("items"):
|
||||||
|
return None, None, None
|
||||||
|
if doc.doctype == "Sales Invoice":
|
||||||
|
prev_doc = doc.get("items")[0].get("sales_order")
|
||||||
|
prev_doctype = "Sales Order"
|
||||||
|
prev_doctype_name = "sales_order"
|
||||||
|
elif doc.doctype == "Purchase Invoice":
|
||||||
|
prev_doc = doc.get("items")[0].get("purchase_order")
|
||||||
|
prev_doctype = "Purchase Order"
|
||||||
|
prev_doctype_name = "purchase_order"
|
||||||
|
else:
|
||||||
|
prev_doc = doc.get("items")[0].get("prevdoc_docname")
|
||||||
|
prev_doctype = "Quotation"
|
||||||
|
prev_doctype_name = "prevdoc_docname"
|
||||||
|
return prev_doc, prev_doctype, prev_doctype_name
|
||||||
|
|
||||||
|
|
||||||
|
def linked_order_has_payment_terms(doc, po_or_so, fieldname, doctype) -> bool:
|
||||||
|
if po_or_so and all_items_have_same_po_or_so(doc, po_or_so, fieldname):
|
||||||
|
if linked_order_has_payment_terms_template(po_or_so, doctype):
|
||||||
|
return True
|
||||||
|
elif linked_order_has_payment_schedule(po_or_so):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def all_items_have_same_po_or_so(doc, po_or_so, fieldname) -> bool:
|
||||||
|
for item in doc.get("items"):
|
||||||
|
if item.get(fieldname) != po_or_so:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def linked_order_has_payment_terms_template(po_or_so, doctype) -> str | None:
|
||||||
|
return frappe.get_value(doctype, po_or_so, "payment_terms_template")
|
||||||
|
|
||||||
|
|
||||||
|
def linked_order_has_payment_schedule(po_or_so) -> list:
|
||||||
|
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_payment_terms_from_order(
|
||||||
|
doc, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||||
|
) -> None:
|
||||||
|
"""Fetch Payment Terms from Purchase/Sales Order when creating a new invoice."""
|
||||||
|
po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so)
|
||||||
|
|
||||||
|
doc.payment_schedule = []
|
||||||
|
doc.payment_terms_template = po_or_so.payment_terms_template
|
||||||
|
posting_date = doc.get("bill_date") or doc.get("posting_date") or doc.get("transaction_date")
|
||||||
|
|
||||||
|
for schedule in po_or_so.payment_schedule:
|
||||||
|
payment_schedule = {
|
||||||
|
"payment_term": schedule.payment_term,
|
||||||
|
"due_date": schedule.due_date,
|
||||||
|
"invoice_portion": schedule.invoice_portion,
|
||||||
|
"mode_of_payment": schedule.mode_of_payment,
|
||||||
|
"description": schedule.description,
|
||||||
|
"paid_amount": schedule.paid_amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
if automatically_fetch_payment_terms:
|
||||||
|
if schedule.due_date_based_on:
|
||||||
|
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
|
||||||
|
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
|
||||||
|
payment_schedule["credit_days"] = cint(schedule.credit_days)
|
||||||
|
payment_schedule["credit_months"] = cint(schedule.credit_months)
|
||||||
|
|
||||||
|
if schedule.discount_validity_based_on:
|
||||||
|
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
|
||||||
|
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
|
||||||
|
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
|
||||||
|
|
||||||
|
payment_schedule["payment_amount"] = flt(
|
||||||
|
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||||
|
schedule.precision("payment_amount"),
|
||||||
|
)
|
||||||
|
payment_schedule["base_payment_amount"] = flt(
|
||||||
|
base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
||||||
|
schedule.precision("base_payment_amount"),
|
||||||
|
)
|
||||||
|
payment_schedule["outstanding"] = payment_schedule["payment_amount"]
|
||||||
|
else:
|
||||||
|
payment_schedule["base_payment_amount"] = flt(
|
||||||
|
schedule.base_payment_amount * doc.get("conversion_rate"),
|
||||||
|
schedule.precision("base_payment_amount"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if schedule.discount_type == "Percentage":
|
||||||
|
payment_schedule["discount_type"] = schedule.discount_type
|
||||||
|
payment_schedule["discount"] = schedule.discount
|
||||||
|
|
||||||
|
if not schedule.invoice_portion:
|
||||||
|
payment_schedule["payment_amount"] = schedule.payment_amount
|
||||||
|
|
||||||
|
doc.append("payment_schedule", payment_schedule)
|
||||||
|
|
||||||
|
|
||||||
|
def set_due_date(doc) -> None:
|
||||||
|
due_dates = [d.due_date for d in doc.get("payment_schedule") if d.due_date]
|
||||||
|
if due_dates:
|
||||||
|
doc.due_date = max(due_dates)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_payment_schedule_dates(doc) -> None:
|
||||||
|
dates = []
|
||||||
|
li = []
|
||||||
|
|
||||||
|
if doc.doctype == "Sales Invoice" and doc.is_pos:
|
||||||
|
return
|
||||||
|
|
||||||
|
for d in doc.get("payment_schedule"):
|
||||||
|
d.validate_from_to_dates("discount_date", "due_date")
|
||||||
|
if doc.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
|
||||||
|
doc.transaction_date
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(d.idx)
|
||||||
|
)
|
||||||
|
elif d.due_date in dates:
|
||||||
|
li.append(_("{0} in row {1}").format(d.due_date, d.idx))
|
||||||
|
dates.append(d.due_date)
|
||||||
|
|
||||||
|
if li:
|
||||||
|
frappe.throw(
|
||||||
|
_("Rows with duplicate due dates in other rows were found: {0}").format("<br>" + "<br>".join(li)),
|
||||||
|
title=_("Payment Schedule"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_payment_schedule_amount(doc) -> None:
|
||||||
|
if (doc.doctype == "Sales Invoice" and doc.is_pos) or doc.get("is_opening") == "Yes":
|
||||||
|
return
|
||||||
|
|
||||||
|
party_account_currency = doc.get("party_account_currency")
|
||||||
|
if not party_account_currency:
|
||||||
|
party_type, party = doc.get_party()
|
||||||
|
if party_type and party:
|
||||||
|
party_account_currency = get_party_account_currency(party_type, party, doc.company)
|
||||||
|
|
||||||
|
if doc.get("payment_schedule"):
|
||||||
|
total = 0
|
||||||
|
base_total = 0
|
||||||
|
for d in doc.get("payment_schedule"):
|
||||||
|
total += flt(d.payment_amount, d.precision("payment_amount"))
|
||||||
|
base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
|
||||||
|
|
||||||
|
base_grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
|
||||||
|
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
|
||||||
|
|
||||||
|
if doc.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||||
|
base_grand_total = base_grand_total - flt(doc.base_write_off_amount)
|
||||||
|
grand_total = grand_total - flt(doc.write_off_amount)
|
||||||
|
|
||||||
|
if doc.get("total_advance"):
|
||||||
|
if party_account_currency == doc.company_currency:
|
||||||
|
base_grand_total -= doc.get("total_advance")
|
||||||
|
grand_total = flt(base_grand_total / doc.get("conversion_rate"), doc.precision("grand_total"))
|
||||||
|
else:
|
||||||
|
grand_total -= doc.get("total_advance")
|
||||||
|
base_grand_total = flt(
|
||||||
|
grand_total * doc.get("conversion_rate"), doc.precision("base_grand_total")
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
abs(flt(total, doc.precision("grand_total")) - flt(grand_total, doc.precision("grand_total")))
|
||||||
|
> 0.1
|
||||||
|
or abs(
|
||||||
|
flt(base_total, doc.precision("base_grand_total"))
|
||||||
|
- flt(base_grand_total, doc.precision("base_grand_total"))
|
||||||
|
)
|
||||||
|
> 0.1
|
||||||
|
):
|
||||||
|
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_terms(
|
||||||
|
terms_template: str,
|
||||||
|
posting_date: DateTimeLikeObject | None = None,
|
||||||
|
grand_total: float | None = None,
|
||||||
|
base_grand_total: float | None = None,
|
||||||
|
bill_date: DateTimeLikeObject | None = None,
|
||||||
|
) -> list:
|
||||||
|
if not terms_template:
|
||||||
|
return
|
||||||
|
|
||||||
|
terms_doc = frappe.get_doc("Payment Terms Template", terms_template)
|
||||||
|
schedule = []
|
||||||
|
for d in terms_doc.get("terms"):
|
||||||
|
d = frappe._dict(d.as_dict())
|
||||||
|
term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date)
|
||||||
|
schedule.append(term_details)
|
||||||
|
|
||||||
|
return schedule
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_payment_term_details(
|
||||||
|
term: str | frappe._dict,
|
||||||
|
posting_date: DateTimeLikeObject | None = None,
|
||||||
|
grand_total: float | None = None,
|
||||||
|
base_grand_total: float | None = None,
|
||||||
|
bill_date: DateTimeLikeObject | None = None,
|
||||||
|
) -> frappe._dict:
|
||||||
|
term_details = frappe._dict()
|
||||||
|
if isinstance(term, str):
|
||||||
|
term = frappe.get_doc("Payment Term", term)
|
||||||
|
else:
|
||||||
|
term_details.payment_term = term.payment_term
|
||||||
|
|
||||||
|
for field in [
|
||||||
|
"description",
|
||||||
|
"invoice_portion",
|
||||||
|
"discount_type",
|
||||||
|
"discount",
|
||||||
|
"mode_of_payment",
|
||||||
|
"due_date_based_on",
|
||||||
|
"credit_days",
|
||||||
|
"credit_months",
|
||||||
|
"discount_validity_based_on",
|
||||||
|
"discount_validity",
|
||||||
|
]:
|
||||||
|
term_details[field] = term.get(field)
|
||||||
|
|
||||||
|
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
|
||||||
|
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
|
||||||
|
term_details.outstanding = term_details.payment_amount
|
||||||
|
term_details.base_outstanding = term_details.base_payment_amount
|
||||||
|
|
||||||
|
if bill_date:
|
||||||
|
term_details.due_date = get_due_date(term, bill_date)
|
||||||
|
term_details.discount_date = get_discount_date(term, bill_date)
|
||||||
|
elif posting_date:
|
||||||
|
term_details.due_date = get_due_date(term, posting_date)
|
||||||
|
term_details.discount_date = get_discount_date(term, posting_date)
|
||||||
|
|
||||||
|
if posting_date and getdate(term_details.due_date) < getdate(posting_date):
|
||||||
|
term_details.due_date = posting_date
|
||||||
|
|
||||||
|
return term_details
|
||||||
|
|
||||||
|
|
||||||
|
def get_due_date(term, posting_date=None, bill_date=None):
|
||||||
|
due_date = None
|
||||||
|
date = bill_date or posting_date
|
||||||
|
if term.due_date_based_on == "Day(s) after invoice date":
|
||||||
|
due_date = add_days(date, cint(term.credit_days))
|
||||||
|
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
|
||||||
|
due_date = add_days(get_last_day(date), cint(term.credit_days))
|
||||||
|
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
|
||||||
|
due_date = get_last_day(add_months(date, cint(term.credit_months)))
|
||||||
|
return due_date
|
||||||
|
|
||||||
|
|
||||||
|
def get_discount_date(term, posting_date=None, bill_date=None):
|
||||||
|
discount_validity = None
|
||||||
|
date = bill_date or posting_date
|
||||||
|
if term.discount_validity_based_on == "Day(s) after invoice date":
|
||||||
|
discount_validity = add_days(date, cint(term.discount_validity))
|
||||||
|
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
|
||||||
|
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
|
||||||
|
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
|
||||||
|
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
|
||||||
|
return discount_validity
|
||||||
@@ -12,15 +12,9 @@ from frappe.model.workflow import get_workflow_name, is_transition_condition_sat
|
|||||||
from frappe.query_builder import DocType
|
from frappe.query_builder import DocType
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
DateTimeLikeObject,
|
|
||||||
add_days,
|
|
||||||
add_months,
|
|
||||||
cint,
|
cint,
|
||||||
comma_and,
|
comma_and,
|
||||||
flt,
|
flt,
|
||||||
fmt_money,
|
|
||||||
formatdate,
|
|
||||||
get_last_day,
|
|
||||||
get_link_to_form,
|
get_link_to_form,
|
||||||
getdate,
|
getdate,
|
||||||
nowdate,
|
nowdate,
|
||||||
@@ -48,10 +42,7 @@ from erpnext.accounts.party import (
|
|||||||
validate_party_frozen_disabled,
|
validate_party_frozen_disabled,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
create_gain_loss_journal,
|
|
||||||
get_account_currency,
|
get_account_currency,
|
||||||
get_currency_precision,
|
|
||||||
get_fiscal_years,
|
|
||||||
validate_fiscal_year,
|
validate_fiscal_year,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import (
|
from erpnext.accounts.utils import (
|
||||||
@@ -1407,239 +1398,30 @@ class AccountsController(TransactionBase):
|
|||||||
set_advance_gain_or_loss(self)
|
set_advance_gain_or_loss(self)
|
||||||
|
|
||||||
def make_precision_loss_gl_entry(self, gl_entries):
|
def make_precision_loss_gl_entry(self, gl_entries):
|
||||||
(
|
from erpnext.accounts.services.exchange_gain_loss import make_precision_loss_gl_entry
|
||||||
round_off_account,
|
|
||||||
round_off_cost_center,
|
|
||||||
round_off_for_opening,
|
|
||||||
) = get_round_off_account_and_cost_center(
|
|
||||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
|
||||||
)
|
|
||||||
|
|
||||||
precision_loss = self.get("base_net_total") - flt(
|
make_precision_loss_gl_entry(self, gl_entries)
|
||||||
self.get("net_total") * self.conversion_rate, self.precision("net_total")
|
|
||||||
)
|
|
||||||
|
|
||||||
credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
|
|
||||||
against = self.supplier if self.doctype == "Purchase Invoice" else self.customer
|
|
||||||
|
|
||||||
if precision_loss:
|
|
||||||
gl_entries.append(
|
|
||||||
self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"account": round_off_account,
|
|
||||||
"against": against,
|
|
||||||
credit_or_debit: precision_loss,
|
|
||||||
"cost_center": round_off_cost_center
|
|
||||||
if self.use_company_roundoff_cost_center
|
|
||||||
else self.cost_center or round_off_cost_center,
|
|
||||||
"remarks": _("Net total calculation precision loss"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def gain_loss_journal_already_booked(
|
def gain_loss_journal_already_booked(
|
||||||
self,
|
self, gain_loss_account, exc_gain_loss, ref2_dt, ref2_dn, ref2_detail_no
|
||||||
gain_loss_account,
|
|
||||||
exc_gain_loss,
|
|
||||||
ref2_dt,
|
|
||||||
ref2_dn,
|
|
||||||
ref2_detail_no,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
from erpnext.accounts.services.exchange_gain_loss import gain_loss_journal_already_booked
|
||||||
Check if gain/loss is booked
|
|
||||||
"""
|
return gain_loss_journal_already_booked(
|
||||||
if res := frappe.db.get_all(
|
gain_loss_account, exc_gain_loss, ref2_dt, ref2_dn, ref2_detail_no
|
||||||
"Journal Entry Account",
|
)
|
||||||
filters={
|
|
||||||
"docstatus": 1,
|
|
||||||
"account": gain_loss_account,
|
|
||||||
"reference_type": ref2_dt, # this will be Journal Entry
|
|
||||||
"reference_name": ref2_dn,
|
|
||||||
"reference_detail_no": ref2_detail_no,
|
|
||||||
},
|
|
||||||
pluck="parent",
|
|
||||||
):
|
|
||||||
# deduplicate
|
|
||||||
res = list({x for x in res})
|
|
||||||
if exc_vouchers := frappe.db.get_all(
|
|
||||||
"Journal Entry",
|
|
||||||
filters={"name": ["in", res], "voucher_type": "Exchange Gain Or Loss"},
|
|
||||||
fields=["voucher_type", "total_debit", "total_credit"],
|
|
||||||
):
|
|
||||||
booked_voucher = exc_vouchers[0]
|
|
||||||
if (
|
|
||||||
booked_voucher.total_debit == exc_gain_loss
|
|
||||||
and booked_voucher.total_credit == exc_gain_loss
|
|
||||||
and booked_voucher.voucher_type == "Exchange Gain Or Loss"
|
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def make_exchange_gain_loss_journal(
|
def make_exchange_gain_loss_journal(
|
||||||
self, args: dict | None = None, dimensions_dict: dict | None = None
|
self, args: dict | None = None, dimensions_dict: dict | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
from erpnext.accounts.services.exchange_gain_loss import make_exchange_gain_loss_journal
|
||||||
Make Exchange Gain/Loss journal for Invoices and Payments
|
|
||||||
"""
|
|
||||||
# Cancelling existing exchange gain/loss journals is handled during the `on_cancel` event.
|
|
||||||
# see accounts/utils.py:cancel_exchange_gain_loss_journal()
|
|
||||||
if self.docstatus == 1:
|
|
||||||
if dimensions_dict is None:
|
|
||||||
dimensions_dict = frappe._dict()
|
|
||||||
active_dimensions = get_dimensions()[0]
|
|
||||||
for dim in active_dimensions:
|
|
||||||
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
|
|
||||||
|
|
||||||
if self.get("doctype") == "Journal Entry":
|
make_exchange_gain_loss_journal(self, args, dimensions_dict)
|
||||||
# 'args' is populated with exchange gain/loss account and the amount to be booked.
|
|
||||||
# These are generated by Sales/Purchase Invoice during reconciliation and advance allocation.
|
|
||||||
# and below logic is only for such scenarios
|
|
||||||
if args:
|
|
||||||
precision = get_currency_precision()
|
|
||||||
for arg in args:
|
|
||||||
# Advance section uses `exchange_gain_loss` and reconciliation uses `difference_amount`
|
|
||||||
if (
|
|
||||||
flt(arg.get("difference_amount", 0), precision) != 0
|
|
||||||
or flt(arg.get("exchange_gain_loss", 0), precision) != 0
|
|
||||||
) and arg.get("difference_account"):
|
|
||||||
party_account = arg.get("account")
|
|
||||||
gain_loss_account = arg.get("difference_account")
|
|
||||||
difference_amount = arg.get("difference_amount") or arg.get("exchange_gain_loss")
|
|
||||||
if difference_amount > 0:
|
|
||||||
dr_or_cr = "debit" if arg.get("party_type") == "Customer" else "credit"
|
|
||||||
else:
|
|
||||||
dr_or_cr = "credit" if arg.get("party_type") == "Customer" else "debit"
|
|
||||||
|
|
||||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
|
||||||
|
|
||||||
if not self.gain_loss_journal_already_booked(
|
|
||||||
gain_loss_account,
|
|
||||||
difference_amount,
|
|
||||||
self.doctype,
|
|
||||||
self.name,
|
|
||||||
arg.get("referenced_row"),
|
|
||||||
):
|
|
||||||
posting_date = arg.get("difference_posting_date") or frappe.db.get_value(
|
|
||||||
arg.voucher_type, arg.voucher_no, "posting_date"
|
|
||||||
)
|
|
||||||
je = create_gain_loss_journal(
|
|
||||||
self.company,
|
|
||||||
posting_date,
|
|
||||||
arg.get("party_type"),
|
|
||||||
arg.get("party"),
|
|
||||||
party_account,
|
|
||||||
gain_loss_account,
|
|
||||||
difference_amount,
|
|
||||||
dr_or_cr,
|
|
||||||
reverse_dr_or_cr,
|
|
||||||
arg.get("against_voucher_type"),
|
|
||||||
arg.get("against_voucher"),
|
|
||||||
arg.get("idx"),
|
|
||||||
self.doctype,
|
|
||||||
self.name,
|
|
||||||
arg.get("referenced_row"),
|
|
||||||
arg.get("cost_center"),
|
|
||||||
dimensions_dict,
|
|
||||||
arg.get("project"),
|
|
||||||
)
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
|
||||||
get_link_to_form("Journal Entry", je)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.get("doctype") == "Payment Entry":
|
|
||||||
# For Payment Entry, exchange_gain_loss field in the `references` table is the trigger for journal creation
|
|
||||||
gain_loss_to_book = [x for x in self.references if x.exchange_gain_loss != 0]
|
|
||||||
booked = []
|
|
||||||
if gain_loss_to_book:
|
|
||||||
[x.reference_doctype for x in gain_loss_to_book]
|
|
||||||
[x.reference_name for x in gain_loss_to_book]
|
|
||||||
je = qb.DocType("Journal Entry")
|
|
||||||
jea = qb.DocType("Journal Entry Account")
|
|
||||||
parents = (
|
|
||||||
qb.from_(jea)
|
|
||||||
.select(jea.parent)
|
|
||||||
.where(
|
|
||||||
(jea.reference_type == "Payment Entry")
|
|
||||||
& (jea.reference_name == self.name)
|
|
||||||
& (jea.docstatus == 1)
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
)
|
|
||||||
|
|
||||||
booked = []
|
|
||||||
if parents:
|
|
||||||
booked = (
|
|
||||||
qb.from_(je)
|
|
||||||
.inner_join(jea)
|
|
||||||
.on(je.name == jea.parent)
|
|
||||||
.select(jea.reference_type, jea.reference_name, jea.reference_detail_no)
|
|
||||||
.where(
|
|
||||||
(je.docstatus == 1)
|
|
||||||
& (je.name.isin(parents))
|
|
||||||
& (je.voucher_type == "Exchange Gain or Loss")
|
|
||||||
)
|
|
||||||
.run()
|
|
||||||
)
|
|
||||||
|
|
||||||
for d in gain_loss_to_book:
|
|
||||||
# Filter out References for which Gain/Loss is already booked
|
|
||||||
if d.exchange_gain_loss and (
|
|
||||||
(d.reference_doctype, d.reference_name, str(d.idx)) not in booked
|
|
||||||
):
|
|
||||||
if self.book_advance_payments_in_separate_party_account:
|
|
||||||
party_account = d.account
|
|
||||||
else:
|
|
||||||
if self.payment_type == "Receive":
|
|
||||||
party_account = self.paid_from
|
|
||||||
elif self.payment_type == "Pay":
|
|
||||||
party_account = self.paid_to
|
|
||||||
|
|
||||||
dr_or_cr = "debit" if d.exchange_gain_loss > 0 else "credit"
|
|
||||||
|
|
||||||
# Inverse debit/credit for payable accounts
|
|
||||||
if self.is_payable_account(d.reference_doctype, party_account):
|
|
||||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
|
||||||
|
|
||||||
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
|
||||||
|
|
||||||
gain_loss_account = frappe.get_cached_value(
|
|
||||||
"Company", self.company, "exchange_gain_loss_account"
|
|
||||||
)
|
|
||||||
je = create_gain_loss_journal(
|
|
||||||
self.company,
|
|
||||||
args.get("difference_posting_date") if args else self.posting_date,
|
|
||||||
self.party_type,
|
|
||||||
self.party,
|
|
||||||
party_account,
|
|
||||||
gain_loss_account,
|
|
||||||
d.exchange_gain_loss,
|
|
||||||
dr_or_cr,
|
|
||||||
reverse_dr_or_cr,
|
|
||||||
d.reference_doctype,
|
|
||||||
d.reference_name,
|
|
||||||
d.idx,
|
|
||||||
self.doctype,
|
|
||||||
self.name,
|
|
||||||
d.idx,
|
|
||||||
self.cost_center,
|
|
||||||
dimensions_dict,
|
|
||||||
self.project,
|
|
||||||
)
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
|
||||||
get_link_to_form("Journal Entry", je)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_payable_account(self, reference_doctype, account):
|
def is_payable_account(self, reference_doctype, account):
|
||||||
if reference_doctype == "Purchase Invoice" or (
|
from erpnext.accounts.services.exchange_gain_loss import is_payable_account
|
||||||
reference_doctype == "Journal Entry"
|
|
||||||
and frappe.get_cached_value("Account", account, "account_type") == "Payable"
|
return is_payable_account(reference_doctype, account)
|
||||||
):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def update_against_document_in_jv(self):
|
def update_against_document_in_jv(self):
|
||||||
"""
|
"""
|
||||||
@@ -1907,147 +1689,34 @@ class AccountsController(TransactionBase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on):
|
def validate_multiple_billing(self, ref_dt: str, item_ref_dn: str, based_on: str) -> None:
|
||||||
from erpnext.controllers.status_updater import get_allowance_for
|
from erpnext.accounts.services.billing_validation import validate_multiple_billing
|
||||||
|
|
||||||
ref_wise_billed_amount = self.get_reference_wise_billed_amt(ref_dt, item_ref_dn, based_on)
|
validate_multiple_billing(self, ref_dt, item_ref_dn, based_on)
|
||||||
|
|
||||||
if not ref_wise_billed_amount:
|
def get_billing_reference_details(
|
||||||
return
|
self, reference_names: list, reference_doctype: str, based_on: str
|
||||||
|
) -> frappe._dict:
|
||||||
|
from erpnext.accounts.services.billing_validation import get_billing_reference_details
|
||||||
|
|
||||||
total_overbilled_amt = 0.0
|
return get_billing_reference_details(self, reference_names, reference_doctype, based_on)
|
||||||
overbilled_items = []
|
|
||||||
precision = self.precision(based_on, "items")
|
|
||||||
precision_allowance = 1 / (10**precision)
|
|
||||||
|
|
||||||
role_allowed_to_overbill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
|
def get_reference_wise_billed_amt(self, ref_dt: str, item_ref_dn: str, based_on: str) -> dict | None:
|
||||||
is_overbilling_allowed = role_allowed_to_overbill in frappe.get_roles()
|
from erpnext.accounts.services.billing_validation import get_reference_wise_billed_amt
|
||||||
|
|
||||||
for row in ref_wise_billed_amount.values():
|
return get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on)
|
||||||
total_billed_amt = row.billed_amt
|
|
||||||
allowance = get_allowance_for(row.item_code, {}, None, None, "amount")[0]
|
|
||||||
|
|
||||||
max_allowed_amt = flt(row.ref_amt * (100 + allowance) / 100)
|
def get_already_billed_amount(
|
||||||
|
self, reference_names: list, item_ref_dn: str, based_on: str
|
||||||
|
) -> frappe._dict:
|
||||||
|
from erpnext.accounts.services.billing_validation import get_already_billed_amount
|
||||||
|
|
||||||
if total_billed_amt < 0 and max_allowed_amt < 0:
|
return get_already_billed_amount(self, reference_names, item_ref_dn, based_on)
|
||||||
# while making debit note against purchase return entry(purchase receipt) getting overbill error
|
|
||||||
total_billed_amt, max_allowed_amt = abs(total_billed_amt), abs(max_allowed_amt)
|
|
||||||
|
|
||||||
overbill_amt = total_billed_amt - max_allowed_amt
|
def throw_overbill_exception(self, overbilled_items: list, precision: int) -> None:
|
||||||
row["max_allowed_amt"] = max_allowed_amt
|
from erpnext.accounts.services.billing_validation import throw_overbill_exception
|
||||||
total_overbilled_amt += overbill_amt
|
|
||||||
|
|
||||||
if overbill_amt > precision_allowance and not is_overbilling_allowed:
|
throw_overbill_exception(self, overbilled_items, precision)
|
||||||
if self.doctype != "Purchase Invoice" or not cint(
|
|
||||||
frappe.db.get_single_value(
|
|
||||||
"Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"
|
|
||||||
)
|
|
||||||
):
|
|
||||||
overbilled_items.append(row)
|
|
||||||
|
|
||||||
if overbilled_items:
|
|
||||||
self.throw_overbill_exception(overbilled_items, precision)
|
|
||||||
|
|
||||||
if is_overbilling_allowed and total_overbilled_amt > 0.1:
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Overbilling of {} ignored because you have {} role.").format(
|
|
||||||
total_overbilled_amt, role_allowed_to_overbill
|
|
||||||
),
|
|
||||||
indicator="orange",
|
|
||||||
alert=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_billing_reference_details(self, reference_names, reference_doctype, based_on):
|
|
||||||
return frappe._dict(
|
|
||||||
frappe.get_all(
|
|
||||||
reference_doctype,
|
|
||||||
filters={"name": ("in", reference_names)},
|
|
||||||
fields=["name", based_on],
|
|
||||||
as_list=1,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_reference_wise_billed_amt(self, ref_dt, item_ref_dn, based_on):
|
|
||||||
"""
|
|
||||||
Returns Sum of Amount of
|
|
||||||
Sales/Purchase Invoice Items
|
|
||||||
that are linked to `item_ref_dn` (`dn_detail` / `pr_detail`)
|
|
||||||
that are submitted OR not submitted but are under current invoice
|
|
||||||
"""
|
|
||||||
reference_names = [d.get(item_ref_dn) for d in self.items if d.get(item_ref_dn)]
|
|
||||||
|
|
||||||
if not reference_names:
|
|
||||||
return
|
|
||||||
|
|
||||||
ref_wise_billed_amount = {}
|
|
||||||
precision = self.precision(based_on, "items")
|
|
||||||
reference_details = self.get_billing_reference_details(reference_names, ref_dt + " Item", based_on)
|
|
||||||
already_billed = self.get_already_billed_amount(reference_names, item_ref_dn, based_on)
|
|
||||||
|
|
||||||
for item in self.items:
|
|
||||||
key = item.get(item_ref_dn)
|
|
||||||
if not key:
|
|
||||||
continue
|
|
||||||
|
|
||||||
ref_amt = flt(reference_details.get(key), precision)
|
|
||||||
current_amount = flt(item.get(based_on), precision)
|
|
||||||
|
|
||||||
if not ref_amt:
|
|
||||||
if current_amount: # Skip warning for free items
|
|
||||||
frappe.msgprint(
|
|
||||||
_(
|
|
||||||
"System will not check over billing since amount for Item {0} in {1} is zero"
|
|
||||||
).format(item.item_code, ref_dt),
|
|
||||||
title=_("Warning"),
|
|
||||||
indicator="orange",
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
|
|
||||||
ref_wise_billed_amount.setdefault(
|
|
||||||
key,
|
|
||||||
frappe._dict(item_code=item.item_code, billed_amt=0.0, ref_amt=ref_amt, rows=[]),
|
|
||||||
)
|
|
||||||
|
|
||||||
ref_wise_billed_amount[key]["rows"].append(item.idx)
|
|
||||||
ref_wise_billed_amount[key]["ref_amt"] = ref_amt
|
|
||||||
ref_wise_billed_amount[key]["billed_amt"] += current_amount
|
|
||||||
if key in already_billed:
|
|
||||||
ref_wise_billed_amount[key]["billed_amt"] += flt(already_billed.pop(key, 0), precision)
|
|
||||||
|
|
||||||
return ref_wise_billed_amount
|
|
||||||
|
|
||||||
def get_already_billed_amount(self, reference_names, item_ref_dn, based_on):
|
|
||||||
item_doctype = frappe.qb.DocType(self.items[0].doctype)
|
|
||||||
based_on_field = frappe.qb.Field(based_on)
|
|
||||||
join_field = frappe.qb.Field(item_ref_dn)
|
|
||||||
|
|
||||||
return frappe._dict(
|
|
||||||
(
|
|
||||||
frappe.qb.from_(item_doctype)
|
|
||||||
.select(join_field, Sum(based_on_field))
|
|
||||||
.where(join_field.isin(reference_names))
|
|
||||||
.where((item_doctype.docstatus == 1) & (item_doctype.parent != self.name))
|
|
||||||
.groupby(join_field)
|
|
||||||
).run()
|
|
||||||
)
|
|
||||||
|
|
||||||
def throw_overbill_exception(self, overbilled_items, precision):
|
|
||||||
message = (
|
|
||||||
_("<p>Cannot overbill for the following Items:</p>")
|
|
||||||
+ "<ul>"
|
|
||||||
+ "".join(
|
|
||||||
_("<li>Item {0} in row(s) {1} billed more than {2}</li>").format(
|
|
||||||
frappe.bold(item.item_code),
|
|
||||||
", ".join(str(x) for x in item.rows),
|
|
||||||
frappe.bold(fmt_money(item.max_allowed_amt, precision=precision, currency=self.currency)),
|
|
||||||
)
|
|
||||||
for item in overbilled_items
|
|
||||||
)
|
|
||||||
+ "</ul>"
|
|
||||||
)
|
|
||||||
message += _("<p>To allow over-billing, please set allowance in Accounts Settings.</p>")
|
|
||||||
|
|
||||||
frappe.throw(_(message))
|
|
||||||
|
|
||||||
def get_company_default(self, fieldname, ignore_validation=False):
|
def get_company_default(self, fieldname, ignore_validation=False):
|
||||||
from erpnext.accounts.utils import get_company_default
|
from erpnext.accounts.utils import get_company_default
|
||||||
@@ -2235,285 +1904,64 @@ class AccountsController(TransactionBase):
|
|||||||
for item in duplicate_list:
|
for item in duplicate_list:
|
||||||
self.remove(item)
|
self.remove(item)
|
||||||
|
|
||||||
def set_payment_schedule(self):
|
def set_payment_schedule(self) -> None:
|
||||||
if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
|
from erpnext.accounts.services.payment_schedule import set_payment_schedule
|
||||||
self.payment_terms_template = ""
|
|
||||||
return
|
|
||||||
|
|
||||||
party_account_currency = self.get("party_account_currency")
|
set_payment_schedule(self)
|
||||||
if not party_account_currency:
|
|
||||||
party_type, party = self.get_party()
|
|
||||||
|
|
||||||
if party_type and party:
|
def get_order_details(self) -> tuple:
|
||||||
party_account_currency = get_party_account_currency(party_type, party, self.company)
|
from erpnext.accounts.services.payment_schedule import get_order_details
|
||||||
|
|
||||||
posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
|
return get_order_details(self)
|
||||||
date = self.get("due_date")
|
|
||||||
due_date = date or posting_date
|
|
||||||
|
|
||||||
base_grand_total = flt(self.get("base_rounded_total") or self.base_grand_total)
|
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype) -> bool:
|
||||||
grand_total = flt(self.get("rounded_total") or self.grand_total)
|
from erpnext.accounts.services.payment_schedule import linked_order_has_payment_terms
|
||||||
automatically_fetch_payment_terms = 0
|
|
||||||
|
|
||||||
if self.doctype in ("Sales Invoice", "Purchase Invoice", "Sales Order"):
|
return linked_order_has_payment_terms(self, po_or_so, fieldname, doctype)
|
||||||
po_or_so, doctype, fieldname = self.get_order_details()
|
|
||||||
automatically_fetch_payment_terms = cint(
|
|
||||||
frappe.get_single_value("Accounts Settings", "automatically_fetch_payment_terms")
|
|
||||||
)
|
|
||||||
if self.doctype != "Sales Order":
|
|
||||||
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
|
|
||||||
grand_total = grand_total - flt(self.write_off_amount)
|
|
||||||
|
|
||||||
if self.get("total_advance"):
|
def all_items_have_same_po_or_so(self, po_or_so, fieldname) -> bool:
|
||||||
if party_account_currency == self.company_currency:
|
from erpnext.accounts.services.payment_schedule import all_items_have_same_po_or_so
|
||||||
base_grand_total -= self.get("total_advance")
|
|
||||||
grand_total = flt(
|
|
||||||
base_grand_total / self.get("conversion_rate"), self.precision("grand_total")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
grand_total -= self.get("total_advance")
|
|
||||||
base_grand_total = flt(
|
|
||||||
grand_total * self.get("conversion_rate"), self.precision("base_grand_total")
|
|
||||||
)
|
|
||||||
|
|
||||||
if not self.get("payment_schedule"):
|
return all_items_have_same_po_or_so(self, po_or_so, fieldname)
|
||||||
if (
|
|
||||||
self.doctype in ["Sales Invoice", "Purchase Invoice", "Sales Order"]
|
|
||||||
and automatically_fetch_payment_terms
|
|
||||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
|
||||||
):
|
|
||||||
self.fetch_payment_terms_from_order(
|
|
||||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
|
||||||
)
|
|
||||||
if self.get("payment_terms_template"):
|
|
||||||
self.ignore_default_payment_terms_template = 1
|
|
||||||
elif self.get("payment_terms_template"):
|
|
||||||
data = get_payment_terms(
|
|
||||||
self.payment_terms_template, posting_date, grand_total, base_grand_total
|
|
||||||
)
|
|
||||||
for item in data:
|
|
||||||
self.append("payment_schedule", item)
|
|
||||||
elif self.doctype not in ["Purchase Receipt"]:
|
|
||||||
data = dict(
|
|
||||||
due_date=due_date,
|
|
||||||
invoice_portion=100,
|
|
||||||
payment_amount=grand_total,
|
|
||||||
base_payment_amount=base_grand_total,
|
|
||||||
)
|
|
||||||
self.append("payment_schedule", data)
|
|
||||||
|
|
||||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
def linked_order_has_payment_terms_template(self, po_or_so, doctype) -> str | None:
|
||||||
"Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
|
from erpnext.accounts.services.payment_schedule import linked_order_has_payment_terms_template
|
||||||
)
|
|
||||||
|
|
||||||
if not (
|
return linked_order_has_payment_terms_template(po_or_so, doctype)
|
||||||
automatically_fetch_payment_terms
|
|
||||||
and allocate_payment_based_on_payment_terms
|
|
||||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
|
||||||
):
|
|
||||||
for d in self.get("payment_schedule"):
|
|
||||||
if d.invoice_portion:
|
|
||||||
d.payment_amount = flt(
|
|
||||||
grand_total * flt(d.invoice_portion) / 100, d.precision("payment_amount")
|
|
||||||
)
|
|
||||||
d.base_payment_amount = flt(
|
|
||||||
base_grand_total * flt(d.invoice_portion) / 100, d.precision("base_payment_amount")
|
|
||||||
)
|
|
||||||
d.outstanding = d.payment_amount
|
|
||||||
d.base_outstanding = d.base_payment_amount
|
|
||||||
elif not d.invoice_portion:
|
|
||||||
d.base_payment_amount = flt(
|
|
||||||
d.payment_amount * self.get("conversion_rate"), d.precision("base_payment_amount")
|
|
||||||
)
|
|
||||||
d.base_outstanding = d.base_payment_amount
|
|
||||||
else:
|
|
||||||
self.fetch_payment_terms_from_order(
|
|
||||||
po_or_so, doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
|
||||||
)
|
|
||||||
self.ignore_default_payment_terms_template = 1
|
|
||||||
|
|
||||||
def get_order_details(self):
|
def linked_order_has_payment_schedule(self, po_or_so) -> list:
|
||||||
if not self.get("items"):
|
from erpnext.accounts.services.payment_schedule import linked_order_has_payment_schedule
|
||||||
return None, None, None
|
|
||||||
if self.doctype == "Sales Invoice":
|
|
||||||
prev_doc = self.get("items")[0].get("sales_order")
|
|
||||||
prev_doctype = "Sales Order"
|
|
||||||
prev_doctype_name = "sales_order"
|
|
||||||
elif self.doctype == "Purchase Invoice":
|
|
||||||
prev_doc = self.get("items")[0].get("purchase_order")
|
|
||||||
prev_doctype = "Purchase Order"
|
|
||||||
prev_doctype_name = "purchase_order"
|
|
||||||
else:
|
|
||||||
prev_doc = self.get("items")[0].get("prevdoc_docname")
|
|
||||||
prev_doctype = "Quotation"
|
|
||||||
prev_doctype_name = "prevdoc_docname"
|
|
||||||
return prev_doc, prev_doctype, prev_doctype_name
|
|
||||||
|
|
||||||
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
|
return linked_order_has_payment_schedule(po_or_so)
|
||||||
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
|
|
||||||
if self.linked_order_has_payment_terms_template(po_or_so, doctype):
|
|
||||||
return True
|
|
||||||
elif self.linked_order_has_payment_schedule(po_or_so):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
def all_items_have_same_po_or_so(self, po_or_so, fieldname):
|
|
||||||
for item in self.get("items"):
|
|
||||||
if item.get(fieldname) != po_or_so:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def linked_order_has_payment_terms_template(self, po_or_so, doctype):
|
|
||||||
return frappe.get_value(doctype, po_or_so, "payment_terms_template")
|
|
||||||
|
|
||||||
def linked_order_has_payment_schedule(self, po_or_so):
|
|
||||||
return frappe.get_all("Payment Schedule", filters={"parent": po_or_so})
|
|
||||||
|
|
||||||
def fetch_payment_terms_from_order(
|
def fetch_payment_terms_from_order(
|
||||||
self, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
self,
|
||||||
):
|
po_or_so,
|
||||||
"""
|
po_or_so_doctype,
|
||||||
Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
|
grand_total,
|
||||||
"""
|
base_grand_total,
|
||||||
po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so)
|
automatically_fetch_payment_terms,
|
||||||
|
) -> None:
|
||||||
|
from erpnext.accounts.services.payment_schedule import fetch_payment_terms_from_order
|
||||||
|
|
||||||
self.payment_schedule = []
|
fetch_payment_terms_from_order(
|
||||||
self.payment_terms_template = po_or_so.payment_terms_template
|
self, po_or_so, po_or_so_doctype, grand_total, base_grand_total, automatically_fetch_payment_terms
|
||||||
posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date")
|
)
|
||||||
|
|
||||||
for schedule in po_or_so.payment_schedule:
|
def set_due_date(self) -> None:
|
||||||
payment_schedule = {
|
from erpnext.accounts.services.payment_schedule import set_due_date
|
||||||
"payment_term": schedule.payment_term,
|
|
||||||
"due_date": schedule.due_date,
|
|
||||||
"invoice_portion": schedule.invoice_portion,
|
|
||||||
"mode_of_payment": schedule.mode_of_payment,
|
|
||||||
"description": schedule.description,
|
|
||||||
"paid_amount": schedule.paid_amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
if automatically_fetch_payment_terms:
|
set_due_date(self)
|
||||||
if schedule.due_date_based_on:
|
|
||||||
payment_schedule["due_date"] = get_due_date(schedule, posting_date)
|
|
||||||
payment_schedule["due_date_based_on"] = schedule.due_date_based_on
|
|
||||||
payment_schedule["credit_days"] = cint(schedule.credit_days)
|
|
||||||
payment_schedule["credit_months"] = cint(schedule.credit_months)
|
|
||||||
|
|
||||||
if schedule.discount_validity_based_on:
|
def validate_payment_schedule_dates(self) -> None:
|
||||||
payment_schedule["discount_date"] = get_discount_date(schedule, posting_date)
|
from erpnext.accounts.services.payment_schedule import validate_payment_schedule_dates
|
||||||
payment_schedule["discount_validity_based_on"] = schedule.discount_validity_based_on
|
|
||||||
payment_schedule["discount_validity"] = cint(schedule.discount_validity)
|
|
||||||
|
|
||||||
payment_schedule["payment_amount"] = flt(
|
validate_payment_schedule_dates(self)
|
||||||
grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
|
||||||
schedule.precision("payment_amount"),
|
|
||||||
)
|
|
||||||
payment_schedule["base_payment_amount"] = flt(
|
|
||||||
base_grand_total * flt(payment_schedule["invoice_portion"]) / 100,
|
|
||||||
schedule.precision("base_payment_amount"),
|
|
||||||
)
|
|
||||||
payment_schedule["outstanding"] = payment_schedule["payment_amount"]
|
|
||||||
else:
|
|
||||||
payment_schedule["base_payment_amount"] = flt(
|
|
||||||
schedule.base_payment_amount * self.get("conversion_rate"),
|
|
||||||
schedule.precision("base_payment_amount"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if schedule.discount_type == "Percentage":
|
def validate_payment_schedule_amount(self) -> None:
|
||||||
payment_schedule["discount_type"] = schedule.discount_type
|
from erpnext.accounts.services.payment_schedule import validate_payment_schedule_amount
|
||||||
payment_schedule["discount"] = schedule.discount
|
|
||||||
|
|
||||||
if not schedule.invoice_portion:
|
validate_payment_schedule_amount(self)
|
||||||
payment_schedule["payment_amount"] = schedule.payment_amount
|
|
||||||
|
|
||||||
self.append("payment_schedule", payment_schedule)
|
|
||||||
|
|
||||||
def set_due_date(self):
|
|
||||||
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
|
|
||||||
if due_dates:
|
|
||||||
self.due_date = max(due_dates)
|
|
||||||
|
|
||||||
def validate_payment_schedule_dates(self):
|
|
||||||
dates = []
|
|
||||||
li = []
|
|
||||||
|
|
||||||
if self.doctype == "Sales Invoice" and self.is_pos:
|
|
||||||
return
|
|
||||||
|
|
||||||
for d in self.get("payment_schedule"):
|
|
||||||
d.validate_from_to_dates("discount_date", "due_date")
|
|
||||||
if self.doctype in ["Sales Order", "Quotation"] and getdate(d.due_date) < getdate(
|
|
||||||
self.transaction_date
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_("Row {0}: Due Date in the Payment Terms table cannot be before Posting Date").format(
|
|
||||||
d.idx
|
|
||||||
)
|
|
||||||
)
|
|
||||||
elif d.due_date in dates:
|
|
||||||
li.append(_("{0} in row {1}").format(d.due_date, d.idx))
|
|
||||||
dates.append(d.due_date)
|
|
||||||
|
|
||||||
if li:
|
|
||||||
duplicates = "<br>" + "<br>".join(li)
|
|
||||||
frappe.throw(
|
|
||||||
_("Rows with duplicate due dates in other rows were found: {0}").format(duplicates),
|
|
||||||
title=_("Payment Schedule"),
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_payment_schedule_amount(self):
|
|
||||||
if (self.doctype == "Sales Invoice" and self.is_pos) or self.get("is_opening") == "Yes":
|
|
||||||
return
|
|
||||||
|
|
||||||
party_account_currency = self.get("party_account_currency")
|
|
||||||
if not party_account_currency:
|
|
||||||
party_type, party = self.get_party()
|
|
||||||
|
|
||||||
if party_type and party:
|
|
||||||
party_account_currency = get_party_account_currency(party_type, party, self.company)
|
|
||||||
|
|
||||||
if self.get("payment_schedule"):
|
|
||||||
total = 0
|
|
||||||
base_total = 0
|
|
||||||
for d in self.get("payment_schedule"):
|
|
||||||
total += flt(d.payment_amount, d.precision("payment_amount"))
|
|
||||||
base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
|
|
||||||
|
|
||||||
base_grand_total = flt(self.get("base_rounded_total") or self.base_grand_total)
|
|
||||||
grand_total = flt(self.get("rounded_total") or self.grand_total)
|
|
||||||
|
|
||||||
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
|
|
||||||
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
|
|
||||||
grand_total = grand_total - flt(self.write_off_amount)
|
|
||||||
|
|
||||||
if self.get("total_advance"):
|
|
||||||
if party_account_currency == self.company_currency:
|
|
||||||
base_grand_total -= self.get("total_advance")
|
|
||||||
grand_total = flt(
|
|
||||||
base_grand_total / self.get("conversion_rate"), self.precision("grand_total")
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
grand_total -= self.get("total_advance")
|
|
||||||
base_grand_total = flt(
|
|
||||||
grand_total * self.get("conversion_rate"), self.precision("base_grand_total")
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
|
||||||
abs(
|
|
||||||
flt(total, self.precision("grand_total"))
|
|
||||||
- flt(grand_total, self.precision("grand_total"))
|
|
||||||
)
|
|
||||||
> 0.1
|
|
||||||
or abs(
|
|
||||||
flt(base_total, self.precision("base_grand_total"))
|
|
||||||
- flt(base_grand_total, self.precision("base_grand_total"))
|
|
||||||
)
|
|
||||||
> 0.1
|
|
||||||
):
|
|
||||||
frappe.throw(
|
|
||||||
_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total")
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_rounded_total_disabled(self):
|
def is_rounded_total_disabled(self):
|
||||||
if self.meta.get_field("disable_rounded_total"):
|
if self.meta.get_field("disable_rounded_total"):
|
||||||
@@ -2641,10 +2089,10 @@ class AccountsController(TransactionBase):
|
|||||||
def get_advance_payment_doctypes(self, payment_type=None) -> list:
|
def get_advance_payment_doctypes(self, payment_type=None) -> list:
|
||||||
return _get_advance_payment_doctypes(payment_type=payment_type)
|
return _get_advance_payment_doctypes(payment_type=payment_type)
|
||||||
|
|
||||||
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries):
|
def set_transaction_currency_and_rate_in_gl_map(self, gl_entries: list) -> None:
|
||||||
for x in gl_entries:
|
from erpnext.accounts.services.exchange_gain_loss import set_transaction_currency_and_rate_in_gl_map
|
||||||
x["transaction_currency"] = self.currency
|
|
||||||
x["transaction_exchange_rate"] = self.get("conversion_rate") or 1
|
set_transaction_currency_and_rate_in_gl_map(self, gl_entries)
|
||||||
|
|
||||||
def after_mapping(self, source_doc):
|
def after_mapping(self, source_doc):
|
||||||
self.set_discount_amount_after_mapping(source_doc)
|
self.set_discount_amount_after_mapping(source_doc)
|
||||||
@@ -2823,98 +2271,12 @@ def update_invoice_status():
|
|||||||
frappe.qb.update(invoice).set("status", status).where(conditions).run()
|
frappe.qb.update(invoice).set("status", status).where(conditions).run()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
from erpnext.accounts.services.payment_schedule import (
|
||||||
def get_payment_terms(
|
get_discount_date,
|
||||||
terms_template: str,
|
get_due_date,
|
||||||
posting_date: DateTimeLikeObject | None = None,
|
get_payment_term_details,
|
||||||
grand_total: float | None = None,
|
get_payment_terms,
|
||||||
base_grand_total: float | None = None,
|
)
|
||||||
bill_date: DateTimeLikeObject | None = None,
|
|
||||||
):
|
|
||||||
if not terms_template:
|
|
||||||
return
|
|
||||||
|
|
||||||
terms_doc = frappe.get_doc("Payment Terms Template", terms_template)
|
|
||||||
|
|
||||||
schedule = []
|
|
||||||
for d in terms_doc.get("terms"):
|
|
||||||
d = frappe._dict(d.as_dict())
|
|
||||||
term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date)
|
|
||||||
schedule.append(term_details)
|
|
||||||
|
|
||||||
return schedule
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_payment_term_details(
|
|
||||||
term: str | frappe._dict,
|
|
||||||
posting_date: DateTimeLikeObject | None = None,
|
|
||||||
grand_total: float | None = None,
|
|
||||||
base_grand_total: float | None = None,
|
|
||||||
bill_date: DateTimeLikeObject | None = None,
|
|
||||||
):
|
|
||||||
term_details = frappe._dict()
|
|
||||||
if isinstance(term, str):
|
|
||||||
term = frappe.get_doc("Payment Term", term)
|
|
||||||
else:
|
|
||||||
term_details.payment_term = term.payment_term
|
|
||||||
|
|
||||||
fields_to_copy = [
|
|
||||||
"description",
|
|
||||||
"invoice_portion",
|
|
||||||
"discount_type",
|
|
||||||
"discount",
|
|
||||||
"mode_of_payment",
|
|
||||||
"due_date_based_on",
|
|
||||||
"credit_days",
|
|
||||||
"credit_months",
|
|
||||||
"discount_validity_based_on",
|
|
||||||
"discount_validity",
|
|
||||||
]
|
|
||||||
|
|
||||||
for field in fields_to_copy:
|
|
||||||
term_details[field] = term.get(field)
|
|
||||||
|
|
||||||
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
|
|
||||||
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
|
|
||||||
term_details.outstanding = term_details.payment_amount
|
|
||||||
term_details.base_outstanding = term_details.base_payment_amount
|
|
||||||
|
|
||||||
if bill_date:
|
|
||||||
term_details.due_date = get_due_date(term, bill_date)
|
|
||||||
term_details.discount_date = get_discount_date(term, bill_date)
|
|
||||||
elif posting_date:
|
|
||||||
term_details.due_date = get_due_date(term, posting_date)
|
|
||||||
term_details.discount_date = get_discount_date(term, posting_date)
|
|
||||||
|
|
||||||
if posting_date and getdate(term_details.due_date) < getdate(posting_date):
|
|
||||||
term_details.due_date = posting_date
|
|
||||||
|
|
||||||
return term_details
|
|
||||||
|
|
||||||
|
|
||||||
def get_due_date(term, posting_date=None, bill_date=None):
|
|
||||||
due_date = None
|
|
||||||
date = bill_date or posting_date
|
|
||||||
if term.due_date_based_on == "Day(s) after invoice date":
|
|
||||||
due_date = add_days(date, cint(term.credit_days))
|
|
||||||
elif term.due_date_based_on == "Day(s) after the end of the invoice month":
|
|
||||||
due_date = add_days(get_last_day(date), cint(term.credit_days))
|
|
||||||
elif term.due_date_based_on == "Month(s) after the end of the invoice month":
|
|
||||||
due_date = get_last_day(add_months(date, cint(term.credit_months)))
|
|
||||||
return due_date
|
|
||||||
|
|
||||||
|
|
||||||
def get_discount_date(term, posting_date=None, bill_date=None):
|
|
||||||
discount_validity = None
|
|
||||||
date = bill_date or posting_date
|
|
||||||
if term.discount_validity_based_on == "Day(s) after invoice date":
|
|
||||||
discount_validity = add_days(date, cint(term.discount_validity))
|
|
||||||
elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
|
|
||||||
discount_validity = add_days(get_last_day(date), cint(term.discount_validity))
|
|
||||||
elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
|
|
||||||
discount_validity = get_last_day(add_months(date, cint(term.discount_validity)))
|
|
||||||
return discount_validity
|
|
||||||
|
|
||||||
|
|
||||||
def get_supplier_block_status(party_name):
|
def get_supplier_block_status(party_name):
|
||||||
|
|||||||
Reference in New Issue
Block a user