Merge pull request #34287 from frappe/early-payment-loss
fix: Allocate tax loss to tax account head on early payment discount
This commit is contained in:
commit
1e3eb4deed
@ -31,6 +31,7 @@
|
|||||||
"determine_address_tax_category_from",
|
"determine_address_tax_category_from",
|
||||||
"column_break_19",
|
"column_break_19",
|
||||||
"add_taxes_from_item_tax_template",
|
"add_taxes_from_item_tax_template",
|
||||||
|
"book_tax_discount_loss",
|
||||||
"print_settings",
|
"print_settings",
|
||||||
"show_inclusive_tax_in_print",
|
"show_inclusive_tax_in_print",
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
@ -360,6 +361,13 @@
|
|||||||
"fieldname": "show_balance_in_coa",
|
"fieldname": "show_balance_in_coa",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Show Balances in Chart Of Accounts"
|
"label": "Show Balances in Chart Of Accounts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
|
||||||
|
"fieldname": "book_tax_discount_loss",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Book Tax Loss on Early Payment Discount"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@ -367,7 +375,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-01-02 12:07:42.434214",
|
"modified": "2023-03-28 09:50:20.375233",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
@ -244,8 +244,6 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
|
frm.set_currency_labels(["total_amount", "outstanding_amount", "allocated_amount"],
|
||||||
party_account_currency, "references");
|
party_account_currency, "references");
|
||||||
|
|
||||||
frm.set_currency_labels(["amount"], company_currency, "deductions");
|
|
||||||
|
|
||||||
cur_frm.set_df_property("source_exchange_rate", "description",
|
cur_frm.set_df_property("source_exchange_rate", "description",
|
||||||
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
|
("1 " + frm.doc.paid_from_account_currency + " = [?] " + company_currency));
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
for ref in self.get("references"):
|
for ref in self.get("references"):
|
||||||
if ref.payment_term and ref.reference_name:
|
if ref.payment_term and ref.reference_name:
|
||||||
key = (ref.payment_term, ref.reference_name)
|
key = (ref.payment_term, ref.reference_name, ref.reference_doctype)
|
||||||
invoice_payment_amount_map.setdefault(key, 0.0)
|
invoice_payment_amount_map.setdefault(key, 0.0)
|
||||||
invoice_payment_amount_map[key] += ref.allocated_amount
|
invoice_payment_amount_map[key] += ref.allocated_amount
|
||||||
|
|
||||||
@ -424,20 +424,37 @@ class PaymentEntry(AccountsController):
|
|||||||
payment_schedule = frappe.get_all(
|
payment_schedule = frappe.get_all(
|
||||||
"Payment Schedule",
|
"Payment Schedule",
|
||||||
filters={"parent": ref.reference_name},
|
filters={"parent": ref.reference_name},
|
||||||
fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
|
fields=[
|
||||||
|
"paid_amount",
|
||||||
|
"payment_amount",
|
||||||
|
"payment_term",
|
||||||
|
"discount",
|
||||||
|
"outstanding",
|
||||||
|
"discount_type",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
for term in payment_schedule:
|
for term in payment_schedule:
|
||||||
invoice_key = (term.payment_term, ref.reference_name)
|
invoice_key = (term.payment_term, ref.reference_name, ref.reference_doctype)
|
||||||
invoice_paid_amount_map.setdefault(invoice_key, {})
|
invoice_paid_amount_map.setdefault(invoice_key, {})
|
||||||
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
|
||||||
|
if not (term.discount_type and term.discount):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if term.discount_type == "Percentage":
|
||||||
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
|
||||||
term.discount / 100
|
term.discount / 100
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
invoice_paid_amount_map[invoice_key]["discounted_amt"] = term.discount
|
||||||
|
|
||||||
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
|
for idx, (key, allocated_amount) in enumerate(invoice_payment_amount_map.items(), 1):
|
||||||
if not invoice_paid_amount_map.get(key):
|
if not invoice_paid_amount_map.get(key):
|
||||||
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
|
frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
|
||||||
|
|
||||||
|
allocated_amount = self.get_allocated_amount_in_transaction_currency(
|
||||||
|
allocated_amount, key[2], key[1]
|
||||||
|
)
|
||||||
|
|
||||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
|
||||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
|
||||||
|
|
||||||
@ -472,6 +489,33 @@ class PaymentEntry(AccountsController):
|
|||||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_allocated_amount_in_transaction_currency(
|
||||||
|
self, allocated_amount, reference_doctype, reference_docname
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Payment Entry could be in base currency while reference's payment schedule
|
||||||
|
is always in transaction currency.
|
||||||
|
E.g.
|
||||||
|
* SI with base=INR and currency=USD
|
||||||
|
* SI with payment schedule in USD
|
||||||
|
* PE in INR (accounting done in base currency)
|
||||||
|
"""
|
||||||
|
ref_currency, ref_exchange_rate = frappe.db.get_value(
|
||||||
|
reference_doctype, reference_docname, ["currency", "conversion_rate"]
|
||||||
|
)
|
||||||
|
is_single_currency = self.paid_from_account_currency == self.paid_to_account_currency
|
||||||
|
# PE in different currency
|
||||||
|
reference_is_multi_currency = self.paid_from_account_currency != ref_currency
|
||||||
|
|
||||||
|
if not (is_single_currency and reference_is_multi_currency):
|
||||||
|
return allocated_amount
|
||||||
|
|
||||||
|
allocated_amount = flt(
|
||||||
|
allocated_amount / ref_exchange_rate, self.precision("total_allocated_amount")
|
||||||
|
)
|
||||||
|
|
||||||
|
return allocated_amount
|
||||||
|
|
||||||
def set_status(self):
|
def set_status(self):
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
self.status = "Cancelled"
|
self.status = "Cancelled"
|
||||||
@ -1642,7 +1686,14 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_payment_entry(
|
def get_payment_entry(
|
||||||
dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
|
dt,
|
||||||
|
dn,
|
||||||
|
party_amount=None,
|
||||||
|
bank_account=None,
|
||||||
|
bank_amount=None,
|
||||||
|
party_type=None,
|
||||||
|
payment_type=None,
|
||||||
|
reference_date=None,
|
||||||
):
|
):
|
||||||
reference_doc = None
|
reference_doc = None
|
||||||
doc = frappe.get_doc(dt, dn)
|
doc = frappe.get_doc(dt, dn)
|
||||||
@ -1669,8 +1720,9 @@ def get_payment_entry(
|
|||||||
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
|
||||||
)
|
)
|
||||||
|
|
||||||
paid_amount, received_amount, discount_amount = apply_early_payment_discount(
|
reference_date = getdate(reference_date)
|
||||||
paid_amount, received_amount, doc
|
paid_amount, received_amount, discount_amount, valid_discounts = apply_early_payment_discount(
|
||||||
|
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||||
)
|
)
|
||||||
|
|
||||||
pe = frappe.new_doc("Payment Entry")
|
pe = frappe.new_doc("Payment Entry")
|
||||||
@ -1678,6 +1730,7 @@ def get_payment_entry(
|
|||||||
pe.company = doc.company
|
pe.company = doc.company
|
||||||
pe.cost_center = doc.get("cost_center")
|
pe.cost_center = doc.get("cost_center")
|
||||||
pe.posting_date = nowdate()
|
pe.posting_date = nowdate()
|
||||||
|
pe.reference_date = reference_date
|
||||||
pe.mode_of_payment = doc.get("mode_of_payment")
|
pe.mode_of_payment = doc.get("mode_of_payment")
|
||||||
pe.party_type = party_type
|
pe.party_type = party_type
|
||||||
pe.party = doc.get(scrub(party_type))
|
pe.party = doc.get(scrub(party_type))
|
||||||
@ -1718,7 +1771,7 @@ def get_payment_entry(
|
|||||||
):
|
):
|
||||||
|
|
||||||
for reference in get_reference_as_per_payment_terms(
|
for reference in get_reference_as_per_payment_terms(
|
||||||
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||||
):
|
):
|
||||||
pe.append("references", reference)
|
pe.append("references", reference)
|
||||||
else:
|
else:
|
||||||
@ -1769,15 +1822,16 @@ def get_payment_entry(
|
|||||||
if party_account and bank:
|
if party_account and bank:
|
||||||
pe.set_exchange_rate(ref_doc=reference_doc)
|
pe.set_exchange_rate(ref_doc=reference_doc)
|
||||||
pe.set_amounts()
|
pe.set_amounts()
|
||||||
|
|
||||||
if discount_amount:
|
if discount_amount:
|
||||||
pe.set_gain_or_loss(
|
base_total_discount_loss = 0
|
||||||
account_details={
|
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||||
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
base_total_discount_loss = split_early_payment_discount_loss(pe, doc, valid_discounts)
|
||||||
"cost_center": pe.cost_center
|
|
||||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
set_pending_discount_loss(
|
||||||
"amount": discount_amount * (-1 if payment_type == "Pay" else 1),
|
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
pe.set_difference_amount()
|
pe.set_difference_amount()
|
||||||
|
|
||||||
return pe
|
return pe
|
||||||
@ -1889,20 +1943,28 @@ def set_paid_amount_and_received_amount(
|
|||||||
return paid_amount, received_amount
|
return paid_amount, received_amount
|
||||||
|
|
||||||
|
|
||||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
def apply_early_payment_discount(
|
||||||
|
paid_amount, received_amount, doc, party_account_currency, reference_date
|
||||||
|
):
|
||||||
total_discount = 0
|
total_discount = 0
|
||||||
|
valid_discounts = []
|
||||||
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
|
||||||
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
|
has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
|
||||||
|
is_multi_currency = party_account_currency != doc.company_currency
|
||||||
|
|
||||||
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||||
for term in doc.payment_schedule:
|
for term in doc.payment_schedule:
|
||||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
if not term.discounted_amount and term.discount and reference_date <= term.discount_date:
|
||||||
|
|
||||||
if term.discount_type == "Percentage":
|
if term.discount_type == "Percentage":
|
||||||
discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
|
grand_total = doc.get("grand_total") if is_multi_currency else doc.get("base_grand_total")
|
||||||
|
discount_amount = flt(grand_total) * (term.discount / 100)
|
||||||
else:
|
else:
|
||||||
discount_amount = term.discount
|
discount_amount = term.discount
|
||||||
|
|
||||||
discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
|
# if accounting is done in the same currency, paid_amount = received_amount
|
||||||
|
conversion_rate = doc.get("conversion_rate", 1) if is_multi_currency else 1
|
||||||
|
discount_amount_in_foreign_currency = discount_amount * conversion_rate
|
||||||
|
|
||||||
if doc.doctype == "Sales Invoice":
|
if doc.doctype == "Sales Invoice":
|
||||||
paid_amount -= discount_amount
|
paid_amount -= discount_amount
|
||||||
@ -1911,23 +1973,151 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
|
|||||||
received_amount -= discount_amount
|
received_amount -= discount_amount
|
||||||
paid_amount -= discount_amount_in_foreign_currency
|
paid_amount -= discount_amount_in_foreign_currency
|
||||||
|
|
||||||
|
valid_discounts.append({"type": term.discount_type, "discount": term.discount})
|
||||||
total_discount += discount_amount
|
total_discount += discount_amount
|
||||||
|
|
||||||
if total_discount:
|
if total_discount:
|
||||||
money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
|
currency = doc.get("currency") if is_multi_currency else doc.company_currency
|
||||||
|
money = frappe.utils.fmt_money(total_discount, currency=currency)
|
||||||
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
|
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
|
||||||
|
|
||||||
return paid_amount, received_amount, total_discount
|
return paid_amount, received_amount, total_discount, valid_discounts
|
||||||
|
|
||||||
|
|
||||||
|
def set_pending_discount_loss(
|
||||||
|
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||||
|
):
|
||||||
|
# If multi-currency, get base discount amount to adjust with base currency deductions/losses
|
||||||
|
if party_account_currency != doc.company_currency:
|
||||||
|
discount_amount = discount_amount * doc.get("conversion_rate", 1)
|
||||||
|
|
||||||
|
# Avoid considering miniscule losses
|
||||||
|
discount_amount = flt(discount_amount - base_total_discount_loss, doc.precision("grand_total"))
|
||||||
|
|
||||||
|
# Set base discount amount (discount loss/pending rounding loss) in deductions
|
||||||
|
if discount_amount > 0.0:
|
||||||
|
positive_negative = -1 if pe.payment_type == "Pay" else 1
|
||||||
|
|
||||||
|
# If tax loss booking is enabled, pending loss will be rounding loss.
|
||||||
|
# Otherwise it will be the total discount loss.
|
||||||
|
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||||
|
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||||
|
|
||||||
|
pe.set_gain_or_loss(
|
||||||
|
account_details={
|
||||||
|
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||||
|
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
|
"amount": discount_amount * positive_negative,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def split_early_payment_discount_loss(pe, doc, valid_discounts) -> float:
|
||||||
|
"""Split early payment discount into Income Loss & Tax Loss."""
|
||||||
|
total_discount_percent = get_total_discount_percent(doc, valid_discounts)
|
||||||
|
|
||||||
|
if not total_discount_percent:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
base_loss_on_income = add_income_discount_loss(pe, doc, total_discount_percent)
|
||||||
|
base_loss_on_taxes = add_tax_discount_loss(pe, doc, total_discount_percent)
|
||||||
|
|
||||||
|
# Round off total loss rather than individual losses to reduce rounding error
|
||||||
|
return flt(base_loss_on_income + base_loss_on_taxes, doc.precision("grand_total"))
|
||||||
|
|
||||||
|
|
||||||
|
def get_total_discount_percent(doc, valid_discounts) -> float:
|
||||||
|
"""Get total percentage and amount discount applied as a percentage."""
|
||||||
|
total_discount_percent = (
|
||||||
|
sum(
|
||||||
|
discount.get("discount") for discount in valid_discounts if discount.get("type") == "Percentage"
|
||||||
|
)
|
||||||
|
or 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
# Operate in percentages only as it makes the income & tax split easier
|
||||||
|
total_discount_amount = (
|
||||||
|
sum(discount.get("discount") for discount in valid_discounts if discount.get("type") == "Amount")
|
||||||
|
or 0.0
|
||||||
|
)
|
||||||
|
|
||||||
|
if total_discount_amount:
|
||||||
|
discount_percentage = (total_discount_amount / doc.get("grand_total")) * 100
|
||||||
|
total_discount_percent += discount_percentage
|
||||||
|
return total_discount_percent
|
||||||
|
|
||||||
|
return total_discount_percent
|
||||||
|
|
||||||
|
|
||||||
|
def add_income_discount_loss(pe, doc, total_discount_percent) -> float:
|
||||||
|
"""Add loss on income discount in base currency."""
|
||||||
|
precision = doc.precision("total")
|
||||||
|
base_loss_on_income = doc.get("base_total") * (total_discount_percent / 100)
|
||||||
|
|
||||||
|
pe.append(
|
||||||
|
"deductions",
|
||||||
|
{
|
||||||
|
"account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
|
||||||
|
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
|
"amount": flt(base_loss_on_income, precision),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_loss_on_income # Return loss without rounding
|
||||||
|
|
||||||
|
|
||||||
|
def add_tax_discount_loss(pe, doc, total_discount_percentage) -> float:
|
||||||
|
"""Add loss on tax discount in base currency."""
|
||||||
|
tax_discount_loss = {}
|
||||||
|
base_total_tax_loss = 0
|
||||||
|
precision = doc.precision("tax_amount_after_discount_amount", "taxes")
|
||||||
|
|
||||||
|
# The same account head could be used more than once
|
||||||
|
for tax in doc.get("taxes", []):
|
||||||
|
base_tax_loss = tax.get("base_tax_amount_after_discount_amount") * (
|
||||||
|
total_discount_percentage / 100
|
||||||
|
)
|
||||||
|
|
||||||
|
account = tax.get("account_head")
|
||||||
|
if not tax_discount_loss.get(account):
|
||||||
|
tax_discount_loss[account] = base_tax_loss
|
||||||
|
else:
|
||||||
|
tax_discount_loss[account] += base_tax_loss
|
||||||
|
|
||||||
|
for account, loss in tax_discount_loss.items():
|
||||||
|
base_total_tax_loss += loss
|
||||||
|
if loss == 0.0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
pe.append(
|
||||||
|
"deductions",
|
||||||
|
{
|
||||||
|
"account": account,
|
||||||
|
"cost_center": pe.cost_center or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||||
|
"amount": flt(loss, precision),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return base_total_tax_loss # Return loss without rounding
|
||||||
|
|
||||||
|
|
||||||
def get_reference_as_per_payment_terms(
|
def get_reference_as_per_payment_terms(
|
||||||
payment_schedule, dt, dn, doc, grand_total, outstanding_amount
|
payment_schedule, dt, dn, doc, grand_total, outstanding_amount, party_account_currency
|
||||||
):
|
):
|
||||||
references = []
|
references = []
|
||||||
|
is_multi_currency_acc = (doc.currency != doc.company_currency) and (
|
||||||
|
party_account_currency != doc.company_currency
|
||||||
|
)
|
||||||
|
|
||||||
for payment_term in payment_schedule:
|
for payment_term in payment_schedule:
|
||||||
payment_term_outstanding = flt(
|
payment_term_outstanding = flt(
|
||||||
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
|
payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
|
||||||
)
|
)
|
||||||
|
if not is_multi_currency_acc:
|
||||||
|
# If accounting is done in company currency for multi-currency transaction
|
||||||
|
payment_term_outstanding = flt(
|
||||||
|
payment_term_outstanding * doc.get("conversion_rate"), payment_term.precision("payment_amount")
|
||||||
|
)
|
||||||
|
|
||||||
if payment_term_outstanding:
|
if payment_term_outstanding:
|
||||||
references.append(
|
references.append(
|
||||||
|
@ -5,7 +5,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||||
@ -256,10 +256,25 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
si.save()
|
si.save()
|
||||||
|
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||||
|
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||||
|
|
||||||
|
self.assertEqual(pe_with_tax_loss.references[0].payment_term, "30 Credit Days with 10% Discount")
|
||||||
|
self.assertEqual(pe_with_tax_loss.references[0].allocated_amount, 236.0)
|
||||||
|
self.assertEqual(pe_with_tax_loss.paid_amount, 212.4)
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 20.0) # Loss on Income
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 3.6) # Loss on Tax
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||||
|
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 236.0)
|
||||||
|
self.assertEqual(pe.paid_amount, 212.4)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, 23.6)
|
||||||
|
|
||||||
pe.submit()
|
pe.submit()
|
||||||
si.load_from_db()
|
si.load_from_db()
|
||||||
|
|
||||||
@ -269,6 +284,190 @@ class TestPaymentEntry(FrappeTestCase):
|
|||||||
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
||||||
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
|
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
|
||||||
|
|
||||||
|
def test_payment_entry_against_payment_terms_with_discount_amount(self):
|
||||||
|
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||||
|
|
||||||
|
si.payment_terms_template = "Test Discount Amount Template"
|
||||||
|
create_payment_terms_template_with_discount(
|
||||||
|
name="30 Credit Days with Rs.50 Discount",
|
||||||
|
discount_type="Amount",
|
||||||
|
discount=50,
|
||||||
|
template_name="Test Discount Amount Template",
|
||||||
|
)
|
||||||
|
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||||
|
|
||||||
|
si.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "_Test Account Service Tax - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Service Tax",
|
||||||
|
"rate": 18,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# Set reference date past discount cut off date
|
||||||
|
pe_1 = get_payment_entry(
|
||||||
|
"Sales Invoice",
|
||||||
|
si.name,
|
||||||
|
bank_account="_Test Cash - _TC",
|
||||||
|
reference_date=frappe.utils.add_days(si.posting_date, 2),
|
||||||
|
)
|
||||||
|
self.assertEqual(pe_1.paid_amount, 236.0) # discount not applied
|
||||||
|
|
||||||
|
# Test if tax loss is booked on enabling configuration
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 1)
|
||||||
|
pe_with_tax_loss = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[0].amount, 42.37) # Loss on Income
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].amount, 7.63) # Loss on Tax
|
||||||
|
self.assertEqual(pe_with_tax_loss.deductions[1].account, "_Test Account Service Tax - _TC")
|
||||||
|
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "book_tax_discount_loss", 0)
|
||||||
|
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
|
||||||
|
self.assertEqual(pe.references[0].allocated_amount, 236.0)
|
||||||
|
self.assertEqual(pe.paid_amount, 186)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, 50.0)
|
||||||
|
|
||||||
|
pe.submit()
|
||||||
|
si.load_from_db()
|
||||||
|
|
||||||
|
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
|
||||||
|
self.assertEqual(si.payment_schedule[0].paid_amount, 186)
|
||||||
|
self.assertEqual(si.payment_schedule[0].outstanding, 0)
|
||||||
|
self.assertEqual(si.payment_schedule[0].discounted_amount, 50)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{
|
||||||
|
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||||
|
"book_tax_discount_loss": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
def test_payment_entry_multicurrency_si_with_base_currency_accounting_early_payment_discount(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
1. Multi-currency SI with single currency accounting (company currency)
|
||||||
|
2. PE with early payment discount
|
||||||
|
3. Test if Paid Amount is calculated in company currency
|
||||||
|
4. Test if deductions are calculated in company currency
|
||||||
|
|
||||||
|
SI is in USD to document agreed amounts that are in USD, but the accounting is in base currency.
|
||||||
|
"""
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer="_Test Customer",
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=50,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
create_payment_terms_template_with_discount()
|
||||||
|
si.payment_terms_template = "Test Discount Template"
|
||||||
|
|
||||||
|
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
pe = get_payment_entry(
|
||||||
|
"Sales Invoice",
|
||||||
|
si.name,
|
||||||
|
bank_account="_Test Bank - _TC",
|
||||||
|
)
|
||||||
|
pe.reference_no = si.name
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
|
||||||
|
# Early payment discount loss on income
|
||||||
|
self.assertEqual(pe.paid_amount, 4500.0) # Amount in company currency
|
||||||
|
self.assertEqual(pe.received_amount, 4500.0)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, 500.0)
|
||||||
|
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||||
|
self.assertEqual(pe.difference_amount, 0.0)
|
||||||
|
|
||||||
|
pe.insert()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
expected_gle = dict(
|
||||||
|
(d[0], d)
|
||||||
|
for d in [
|
||||||
|
["Debtors - _TC", 0, 5000, si.name],
|
||||||
|
["_Test Bank - _TC", 4500, 0, None],
|
||||||
|
["Write Off - _TC", 500.0, 0, None],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.validate_gl_entries(pe.name, expected_gle)
|
||||||
|
|
||||||
|
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||||
|
self.assertEqual(outstanding_amount, 0)
|
||||||
|
|
||||||
|
def test_payment_entry_multicurrency_accounting_si_with_early_payment_discount(self):
|
||||||
|
"""
|
||||||
|
1. Multi-currency SI with multi-currency accounting
|
||||||
|
2. PE with early payment discount and also exchange loss
|
||||||
|
3. Test if Paid Amount is calculated in transaction currency
|
||||||
|
4. Test if deductions are calculated in base/company currency
|
||||||
|
5. Test if exchange loss is reflected in difference
|
||||||
|
"""
|
||||||
|
si = create_sales_invoice(
|
||||||
|
customer="_Test Customer USD",
|
||||||
|
debit_to="_Test Receivable USD - _TC",
|
||||||
|
currency="USD",
|
||||||
|
conversion_rate=50,
|
||||||
|
do_not_save=1,
|
||||||
|
)
|
||||||
|
create_payment_terms_template_with_discount()
|
||||||
|
si.payment_terms_template = "Test Discount Template"
|
||||||
|
|
||||||
|
frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
pe = get_payment_entry(
|
||||||
|
"Sales Invoice", si.name, bank_account="_Test Bank - _TC", bank_amount=4700
|
||||||
|
)
|
||||||
|
pe.reference_no = si.name
|
||||||
|
pe.reference_date = nowdate()
|
||||||
|
|
||||||
|
# Early payment discount loss on income
|
||||||
|
self.assertEqual(pe.paid_amount, 90.0)
|
||||||
|
self.assertEqual(pe.received_amount, 4200.0) # 5000 - 500 (discount) - 300 (exchange loss)
|
||||||
|
self.assertEqual(pe.deductions[0].amount, 500.0)
|
||||||
|
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||||
|
|
||||||
|
# Exchange loss
|
||||||
|
self.assertEqual(pe.difference_amount, 300.0)
|
||||||
|
|
||||||
|
pe.append(
|
||||||
|
"deductions",
|
||||||
|
{
|
||||||
|
"account": "_Test Exchange Gain/Loss - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"amount": 300.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
pe.insert()
|
||||||
|
pe.submit()
|
||||||
|
|
||||||
|
self.assertEqual(pe.difference_amount, 0.0)
|
||||||
|
|
||||||
|
expected_gle = dict(
|
||||||
|
(d[0], d)
|
||||||
|
for d in [
|
||||||
|
["_Test Receivable USD - _TC", 0, 5000, si.name],
|
||||||
|
["_Test Bank - _TC", 4200, 0, None],
|
||||||
|
["Write Off - _TC", 500.0, 0, None],
|
||||||
|
["_Test Exchange Gain/Loss - _TC", 300.0, 0, None],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.validate_gl_entries(pe.name, expected_gle)
|
||||||
|
|
||||||
|
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
|
||||||
|
self.assertEqual(outstanding_amount, 0)
|
||||||
|
|
||||||
def test_payment_against_purchase_invoice_to_check_status(self):
|
def test_payment_against_purchase_invoice_to_check_status(self):
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
supplier="_Test Supplier USD",
|
supplier="_Test Supplier USD",
|
||||||
@ -839,24 +1038,27 @@ def create_payment_terms_template():
|
|||||||
).insert()
|
).insert()
|
||||||
|
|
||||||
|
|
||||||
def create_payment_terms_template_with_discount():
|
def create_payment_terms_template_with_discount(
|
||||||
|
name=None, discount_type=None, discount=None, template_name=None
|
||||||
|
):
|
||||||
|
create_payment_term(name or "30 Credit Days with 10% Discount")
|
||||||
|
template_name = template_name or "Test Discount Template"
|
||||||
|
|
||||||
create_payment_term("30 Credit Days with 10% Discount")
|
if not frappe.db.exists("Payment Terms Template", template_name):
|
||||||
|
frappe.get_doc(
|
||||||
if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
|
|
||||||
payment_term_template = frappe.get_doc(
|
|
||||||
{
|
{
|
||||||
"doctype": "Payment Terms Template",
|
"doctype": "Payment Terms Template",
|
||||||
"template_name": "Test Discount Template",
|
"template_name": template_name,
|
||||||
"allocate_payment_based_on_payment_terms": 1,
|
"allocate_payment_based_on_payment_terms": 1,
|
||||||
"terms": [
|
"terms": [
|
||||||
{
|
{
|
||||||
"doctype": "Payment Terms Template Detail",
|
"doctype": "Payment Terms Template Detail",
|
||||||
"payment_term": "30 Credit Days with 10% Discount",
|
"payment_term": name or "30 Credit Days with 10% Discount",
|
||||||
"invoice_portion": 100,
|
"invoice_portion": 100,
|
||||||
"credit_days_based_on": "Day(s) after invoice date",
|
"credit_days_based_on": "Day(s) after invoice date",
|
||||||
"credit_days": 2,
|
"credit_days": 2,
|
||||||
"discount": 10,
|
"discount_type": discount_type or "Percentage",
|
||||||
|
"discount": discount or 10,
|
||||||
"discount_validity_based_on": "Day(s) after invoice date",
|
"discount_validity_based_on": "Day(s) after invoice date",
|
||||||
"discount_validity": 1,
|
"discount_validity": 1,
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
"creation": "2016-06-15 15:56:30.815503",
|
"creation": "2016-06-15 15:56:30.815503",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"account",
|
"account",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
@ -17,9 +18,7 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Account",
|
"label": "Account",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"reqd": 1,
|
"reqd": 1
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "cost_center",
|
"fieldname": "cost_center",
|
||||||
@ -28,37 +27,30 @@
|
|||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center",
|
"options": "Cost Center",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"reqd": 1,
|
"reqd": 1
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amount",
|
"fieldname": "amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Amount",
|
"label": "Amount (Company Currency)",
|
||||||
"reqd": 1,
|
"options": "Company:company:default_currency",
|
||||||
"show_days": 1,
|
"reqd": 1
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_2",
|
"fieldname": "column_break_2",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "description",
|
"fieldname": "description",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Description",
|
"label": "Description"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-12 20:38:08.110674",
|
"modified": "2023-03-06 07:11:57.739619",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry Deduction",
|
"name": "Payment Entry Deduction",
|
||||||
@ -66,5 +58,6 @@
|
|||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
@ -82,7 +82,11 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
|
|
||||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
||||||
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
||||||
this.frm.add_custom_button(__('Payment'), this.make_payment_entry, __('Create'));
|
this.frm.add_custom_button(
|
||||||
|
__('Payment'),
|
||||||
|
() => this.make_payment_entry(),
|
||||||
|
__('Create')
|
||||||
|
);
|
||||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,9 +93,12 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
|
|
||||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
||||||
&& !(cint(doc.is_return) && doc.return_against)) {
|
&& !(cint(doc.is_return) && doc.return_against)) {
|
||||||
cur_frm.add_custom_button(__('Payment'),
|
this.frm.add_custom_button(
|
||||||
this.make_payment_entry, __('Create'));
|
__('Payment'),
|
||||||
cur_frm.page.set_inner_btn_group_as_primary(__('Create'));
|
() => this.make_payment_entry(),
|
||||||
|
__('Create')
|
||||||
|
);
|
||||||
|
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(doc.docstatus==1 && !doc.is_return) {
|
if(doc.docstatus==1 && !doc.is_return) {
|
||||||
|
@ -236,7 +236,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
|||||||
this.make_purchase_invoice, __('Create'));
|
this.make_purchase_invoice, __('Create'));
|
||||||
|
|
||||||
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
||||||
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
|
this.frm.add_custom_button(
|
||||||
|
__('Payment'),
|
||||||
|
() => this.make_payment_entry(),
|
||||||
|
__('Create')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(flt(doc.per_billed) < 100) {
|
if(flt(doc.per_billed) < 100) {
|
||||||
|
@ -1897,20 +1897,60 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
|
|
||||||
make_payment_entry() {
|
make_payment_entry() {
|
||||||
|
let via_journal_entry = this.frm.doc.__onload && this.frm.doc.__onload.make_payment_via_journal_entry;
|
||||||
|
if(this.has_discount_in_schedule() && !via_journal_entry) {
|
||||||
|
// If early payment discount is applied, ask user for reference date
|
||||||
|
this.prompt_user_for_reference_date();
|
||||||
|
} else {
|
||||||
|
this.make_mapped_payment_entry();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
make_mapped_payment_entry(args) {
|
||||||
|
var me = this;
|
||||||
|
args = args || { "dt": this.frm.doc.doctype, "dn": this.frm.doc.name };
|
||||||
return frappe.call({
|
return frappe.call({
|
||||||
method: cur_frm.cscript.get_method_for_payment(),
|
method: me.get_method_for_payment(),
|
||||||
args: {
|
args: args,
|
||||||
"dt": cur_frm.doc.doctype,
|
|
||||||
"dn": cur_frm.doc.name
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
var doclist = frappe.model.sync(r.message);
|
var doclist = frappe.model.sync(r.message);
|
||||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||||
// cur_frm.refresh_fields()
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompt_user_for_reference_date(){
|
||||||
|
var me = this;
|
||||||
|
frappe.prompt({
|
||||||
|
label: __("Cheque/Reference Date"),
|
||||||
|
fieldname: "reference_date",
|
||||||
|
fieldtype: "Date",
|
||||||
|
reqd: 1,
|
||||||
|
}, (values) => {
|
||||||
|
let args = {
|
||||||
|
"dt": me.frm.doc.doctype,
|
||||||
|
"dn": me.frm.doc.name,
|
||||||
|
"reference_date": values.reference_date
|
||||||
|
}
|
||||||
|
me.make_mapped_payment_entry(args);
|
||||||
|
},
|
||||||
|
__("Reference Date for Early Payment Discount"),
|
||||||
|
__("Continue")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
has_discount_in_schedule() {
|
||||||
|
let is_eligible = in_list(
|
||||||
|
["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"],
|
||||||
|
this.frm.doctype
|
||||||
|
);
|
||||||
|
let has_payment_schedule = this.frm.doc.payment_schedule && this.frm.doc.payment_schedule.length;
|
||||||
|
if(!is_eligible || !has_payment_schedule) return false;
|
||||||
|
|
||||||
|
let has_discount = this.frm.doc.payment_schedule.some(row => row.discount_date);
|
||||||
|
return has_discount;
|
||||||
|
}
|
||||||
|
|
||||||
make_quality_inspection() {
|
make_quality_inspection() {
|
||||||
let data = [];
|
let data = [];
|
||||||
const fields = [
|
const fields = [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user