refactor: migrate PI supplier/tax/payment GL builders onto the composer

Move make_supplier_gl_entry, add_supplier_gl_entry, make_tax_gl_entries,
make_internal_transfer_gl_entries, make_gl_entries_for_tax_withholding,
make_payment_gl_entries, make_write_off_gl_entry, and
make_gle_for_rounding_adjustment from PurchaseInvoice onto
PurchaseInvoiceGLComposer.

compose() now calls self.X for all moved builders; the make_item cluster
(make_item_gl_entries, make_provisional_gl_entry, get_provisional_accounts,
update_net_purchase_amount_for_linked_assets, make_stock_adjustment_entry)
still lives on doc pending batch-2 migration.

All 12 GL characterization snapshots pass.
This commit is contained in:
Nabin Hait
2026-05-27 08:29:32 +05:30
parent 6467f07459
commit 32c4b1d98a
2 changed files with 329 additions and 338 deletions

View File

@@ -871,53 +871,6 @@ class PurchaseInvoice(BuyingController):
return 1
return 0
def make_supplier_gl_entry(self, gl_entries):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introduction of posting GLE based on rounded total
grand_total = (
self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
)
base_grand_total = flt(
self.base_rounded_total
if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total,
self.precision("base_grand_total"),
)
if grand_total and not self.is_internal_transfer():
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
def add_supplier_gl_entry(
self, gl_entries, base_grand_total, grand_total, against_account=None, remarks=None, skip_merge=False
):
against_voucher = self.name
if self.is_return and self.return_against and not self.update_outstanding_for_self:
against_voucher = self.return_against
# Did not use base_grand_total to book rounding loss gle
gl = {
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
"against": against_account or self.against_expense_account,
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total
if self.party_account_currency == self.company_currency
else grand_total,
"credit_in_transaction_currency": grand_total,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center,
"_skip_merge": skip_merge,
}
if remarks:
gl["remarks"] = remarks
gl_entries.append(self.get_gl_dict(gl, self.party_account_currency, item=self))
def make_item_gl_entries(self, gl_entries):
# item gl entries
stock_items = self.get_stock_items()
@@ -1375,283 +1328,6 @@ class PurchaseInvoice(BuyingController):
return warehouse_debit_amount
def make_tax_gl_entries(self, gl_entries):
# tax table gl entries
valuation_tax = {}
for tax in self.get("taxes"):
amount, base_amount = self.get_tax_amounts(tax, None)
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
account_currency = get_account_currency(tax.account_head)
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
gl_entries.append(
self.get_gl_dict(
{
"account": tax.account_head,
"against": self.supplier,
dr_or_cr: base_amount,
dr_or_cr + "_in_account_currency": base_amount
if account_currency == self.company_currency
else amount,
dr_or_cr + "_in_transaction_currency": amount,
"cost_center": tax.cost_center,
},
account_currency,
item=tax,
)
)
# accumulate valuation tax
if (
self.is_opening == "No"
and tax.category in ("Valuation", "Valuation and Total")
and flt(base_amount)
and not self.is_internal_transfer()
):
if self.auto_accounting_for_stock and not tax.cost_center:
frappe.throw(
_("Cost Center is required in row {0} in Taxes table for type {1}").format(
tax.idx, _(tax.category)
)
)
valuation_tax.setdefault(tax.name, 0)
valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax:
# credit valuation tax amount in "Expenses Included In Valuation"
# this will balance out valuation amount included in cost of goods sold
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = self.negative_expense_to_be_booked
i = 1
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):
if i == len(valuation_tax):
applicable_amount = amount_including_divisional_loss
else:
applicable_amount = self.negative_expense_to_be_booked * (
valuation_tax[tax.name] / total_valuation_amount
)
amount_including_divisional_loss -= applicable_amount
gl_entries.append(
self.get_gl_dict(
{
"account": tax.account_head,
"cost_center": tax.cost_center,
"against": self.supplier,
"credit": applicable_amount,
"credit_in_transaction_currency": flt(
applicable_amount / self.conversion_rate,
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
),
"remarks": self.remarks or _("Accounting Entry for Stock"),
},
item=tax,
)
)
i += 1
if self.auto_accounting_for_stock and self.update_stock and valuation_tax:
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):
gl_entries.append(
self.get_gl_dict(
{
"account": tax.account_head,
"cost_center": tax.cost_center,
"against": self.supplier,
"credit": valuation_tax[tax.name],
"credit_in_transaction_currency": flt(
valuation_tax[tax.name] / self.conversion_rate,
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
),
"remarks": self.remarks or _("Accounting Entry for Stock"),
},
item=tax,
)
)
def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)
gl_entries.append(
self.get_gl_dict(
{
"account": self.unrealized_profit_loss_account,
"against": self.supplier,
"credit": flt(self.total_taxes_and_charges),
"credit_in_transaction_currency": flt(self.total_taxes_and_charges),
"credit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center,
},
account_currency,
item=self,
)
)
def make_gl_entries_for_tax_withholding(self, gl_entries):
"""
Tax withholding amount is not part of supplier invoice.
Separate supplier GL Entry for correct reporting.
"""
if not self.apply_tds:
return
for row in self.get("taxes"):
if not row.is_tax_withholding_account or not row.tax_amount:
continue
base_tds_amount = row.base_tax_amount_after_discount_amount
tds_amount = row.tax_amount_after_discount_amount
self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount)
self.add_supplier_gl_entry(
gl_entries,
-base_tds_amount,
-tds_amount,
against_account=row.account_head,
remarks=_("TDS Deducted"),
skip_merge=True,
)
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
bank_account_currency = get_account_currency(self.cash_bank_account)
# CASH, make payment entries
gl_entries.append(
self.get_gl_dict(
{
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"against": self.cash_bank_account,
"debit": self.base_paid_amount,
"debit_in_account_currency": self.base_paid_amount
if self.party_account_currency == self.company_currency
else self.paid_amount,
"debit_in_transaction_currency": self.paid_amount,
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,
},
self.party_account_currency,
item=self,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": self.cash_bank_account,
"against": self.supplier,
"credit": self.base_paid_amount,
"credit_in_account_currency": self.base_paid_amount
if bank_account_currency == self.company_currency
else self.paid_amount,
"credit_in_transaction_currency": self.paid_amount,
"cost_center": self.cost_center,
},
bank_account_currency,
item=self,
)
)
def make_write_off_gl_entry(self, gl_entries):
# writeoff account includes petty difference in the invoice amount
# and the amount that is paid
if self.write_off_account and flt(self.write_off_amount):
write_off_account_currency = get_account_currency(self.write_off_account)
gl_entries.append(
self.get_gl_dict(
{
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"against": self.write_off_account,
"debit": self.base_write_off_amount,
"debit_in_account_currency": self.base_write_off_amount
if self.party_account_currency == self.company_currency
else self.write_off_amount,
"debit_in_transaction_currency": self.write_off_amount,
"against_voucher": self.return_against
if cint(self.is_return) and self.return_against
else self.name,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
"project": self.project,
},
self.party_account_currency,
item=self,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": self.write_off_account,
"against": self.supplier,
"credit": flt(self.base_write_off_amount),
"credit_in_account_currency": self.base_write_off_amount
if write_off_account_currency == self.company_currency
else self.write_off_amount,
"credit_in_transaction_currency": self.write_off_amount,
"cost_center": self.cost_center or self.write_off_cost_center,
},
item=self,
)
)
def make_gle_for_rounding_adjustment(self, gl_entries):
# if rounding adjustment in small and conversion rate is also small then
# base_rounding_adjustment may become zero due to small precision
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry
if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
(
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
)
if self.is_opening == "Yes" and self.rounding_adjustment:
if not round_off_for_opening:
frappe.throw(
_(
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
).format(
frappe.bold(self.rounding_adjustment),
frappe.bold("Round Off for Opening"),
get_link_to_form("Company", self.company),
frappe.bold("Disable Rounded Total"),
)
)
else:
round_off_account = round_off_for_opening
gl_entries.append(
self.get_gl_dict(
{
"account": round_off_account,
"against": self.supplier,
"debit_in_account_currency": self.rounding_adjustment,
"debit": self.base_rounding_adjustment,
"cost_center": round_off_cost_center
if self.use_company_roundoff_cost_center
else (self.cost_center or round_off_cost_center),
},
item=self,
)
)
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)

View File

@@ -1,17 +1,18 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe import _
from frappe.utils import cint, flt, get_link_to_form
import erpnext
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
from erpnext.accounts.services.base_gl_composer import BaseGLComposer
from erpnext.accounts.utils import get_account_currency
class PurchaseInvoiceGLComposer(BaseGLComposer):
"""Assembles the GL entries for a Purchase Invoice.
Orchestration only for now: the voucher-specific row builders still live on
the Purchase Invoice document and are invoked via ``self.doc``. They migrate
onto this composer in a later increment.
"""
"""Assembles the GL entries for a Purchase Invoice."""
def compose(self, inventory_account_map=None):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_regional_gl_entries
@@ -28,21 +29,335 @@ class PurchaseInvoiceGLComposer(BaseGLComposer):
doc.negative_expense_to_be_booked = 0.0
gl_entries = []
doc.make_supplier_gl_entry(gl_entries)
self.make_supplier_gl_entry(gl_entries)
doc.make_item_gl_entries(gl_entries)
doc.make_precision_loss_gl_entry(gl_entries)
doc.make_tax_gl_entries(gl_entries)
doc.make_internal_transfer_gl_entries(gl_entries)
doc.make_gl_entries_for_tax_withholding(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.make_gl_entries_for_tax_withholding(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, doc)
gl_entries = merge_similar_entries(gl_entries)
doc.make_payment_gl_entries(gl_entries)
doc.make_write_off_gl_entry(gl_entries)
doc.make_gle_for_rounding_adjustment(gl_entries)
self.make_payment_gl_entries(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
doc.set_transaction_currency_and_rate_in_gl_map(gl_entries)
doc.set_gl_entry_for_purchase_expense(gl_entries)
return gl_entries
def make_supplier_gl_entry(self, gl_entries):
doc = self.doc
grand_total = (
doc.rounded_total if (doc.rounding_adjustment and doc.rounded_total) else doc.grand_total
)
base_grand_total = flt(
doc.base_rounded_total
if (doc.base_rounding_adjustment and doc.base_rounded_total)
else doc.base_grand_total,
doc.precision("base_grand_total"),
)
if grand_total and not doc.is_internal_transfer():
self.add_supplier_gl_entry(gl_entries, base_grand_total, grand_total)
def add_supplier_gl_entry(
self,
gl_entries,
base_grand_total,
grand_total,
against_account=None,
remarks=None,
skip_merge=False,
):
doc = self.doc
against_voucher = doc.name
if doc.is_return and doc.return_against and not doc.update_outstanding_for_self:
against_voucher = doc.return_against
gl = {
"account": doc.credit_to,
"party_type": "Supplier",
"party": doc.supplier,
"due_date": doc.due_date,
"against": against_account or doc.against_expense_account,
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total
if doc.party_account_currency == doc.company_currency
else grand_total,
"credit_in_transaction_currency": grand_total,
"against_voucher": against_voucher,
"against_voucher_type": doc.doctype,
"project": doc.project,
"cost_center": doc.cost_center,
"_skip_merge": skip_merge,
}
if remarks:
gl["remarks"] = remarks
gl_entries.append(doc.get_gl_dict(gl, doc.party_account_currency, item=doc))
def make_tax_gl_entries(self, gl_entries):
doc = self.doc
valuation_tax = {}
for tax in doc.get("taxes"):
amount, base_amount = doc.get_tax_amounts(tax, None)
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
account_currency = get_account_currency(tax.account_head)
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
gl_entries.append(
doc.get_gl_dict(
{
"account": tax.account_head,
"against": doc.supplier,
dr_or_cr: base_amount,
dr_or_cr + "_in_account_currency": base_amount
if account_currency == doc.company_currency
else amount,
dr_or_cr + "_in_transaction_currency": amount,
"cost_center": tax.cost_center,
},
account_currency,
item=tax,
)
)
if (
doc.is_opening == "No"
and tax.category in ("Valuation", "Valuation and Total")
and flt(base_amount)
and not doc.is_internal_transfer()
):
if doc.auto_accounting_for_stock and not tax.cost_center:
frappe.throw(
_("Cost Center is required in row {0} in Taxes table for type {1}").format(
tax.idx, _(tax.category)
)
)
valuation_tax.setdefault(tax.name, 0)
valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
if doc.is_opening == "No" and doc.negative_expense_to_be_booked and valuation_tax:
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = doc.negative_expense_to_be_booked
i = 1
for tax in doc.get("taxes"):
if valuation_tax.get(tax.name):
if i == len(valuation_tax):
applicable_amount = amount_including_divisional_loss
else:
applicable_amount = doc.negative_expense_to_be_booked * (
valuation_tax[tax.name] / total_valuation_amount
)
amount_including_divisional_loss -= applicable_amount
gl_entries.append(
doc.get_gl_dict(
{
"account": tax.account_head,
"cost_center": tax.cost_center,
"against": doc.supplier,
"credit": applicable_amount,
"credit_in_transaction_currency": flt(
applicable_amount / doc.conversion_rate,
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
),
"remarks": doc.remarks or _("Accounting Entry for Stock"),
},
item=tax,
)
)
i += 1
if doc.auto_accounting_for_stock and doc.update_stock and valuation_tax:
for tax in doc.get("taxes"):
if valuation_tax.get(tax.name):
gl_entries.append(
doc.get_gl_dict(
{
"account": tax.account_head,
"cost_center": tax.cost_center,
"against": doc.supplier,
"credit": valuation_tax[tax.name],
"credit_in_transaction_currency": flt(
valuation_tax[tax.name] / doc.conversion_rate,
frappe.get_precision("Purchase Invoice Item", "item_tax_amount"),
),
"remarks": doc.remarks or _("Accounting Entry for Stock"),
},
item=tax,
)
)
def make_internal_transfer_gl_entries(self, gl_entries):
doc = self.doc
if doc.is_internal_transfer() and flt(doc.base_total_taxes_and_charges):
account_currency = get_account_currency(doc.unrealized_profit_loss_account)
gl_entries.append(
doc.get_gl_dict(
{
"account": doc.unrealized_profit_loss_account,
"against": doc.supplier,
"credit": flt(doc.total_taxes_and_charges),
"credit_in_transaction_currency": flt(doc.total_taxes_and_charges),
"credit_in_account_currency": flt(doc.base_total_taxes_and_charges),
"cost_center": doc.cost_center,
},
account_currency,
item=doc,
)
)
def make_gl_entries_for_tax_withholding(self, gl_entries):
"""Separate supplier GL entry for tax withholding (TDS) — not part of the supplier invoice amount."""
doc = self.doc
if not doc.apply_tds:
return
for row in doc.get("taxes"):
if not row.is_tax_withholding_account or not row.tax_amount:
continue
base_tds_amount = row.base_tax_amount_after_discount_amount
tds_amount = row.tax_amount_after_discount_amount
self.add_supplier_gl_entry(gl_entries, base_tds_amount, tds_amount)
self.add_supplier_gl_entry(
gl_entries,
-base_tds_amount,
-tds_amount,
against_account=row.account_head,
remarks=_("TDS Deducted"),
skip_merge=True,
)
def make_payment_gl_entries(self, gl_entries):
doc = self.doc
if cint(doc.is_paid) and doc.cash_bank_account and doc.paid_amount:
bank_account_currency = get_account_currency(doc.cash_bank_account)
gl_entries.append(
doc.get_gl_dict(
{
"account": doc.credit_to,
"party_type": "Supplier",
"party": doc.supplier,
"against": doc.cash_bank_account,
"debit": doc.base_paid_amount,
"debit_in_account_currency": doc.base_paid_amount
if doc.party_account_currency == doc.company_currency
else doc.paid_amount,
"debit_in_transaction_currency": doc.paid_amount,
"against_voucher": doc.return_against
if cint(doc.is_return) and doc.return_against
else doc.name,
"against_voucher_type": doc.doctype,
"cost_center": doc.cost_center,
"project": doc.project,
},
doc.party_account_currency,
item=doc,
)
)
gl_entries.append(
doc.get_gl_dict(
{
"account": doc.cash_bank_account,
"against": doc.supplier,
"credit": doc.base_paid_amount,
"credit_in_account_currency": doc.base_paid_amount
if bank_account_currency == doc.company_currency
else doc.paid_amount,
"credit_in_transaction_currency": doc.paid_amount,
"cost_center": doc.cost_center,
},
bank_account_currency,
item=doc,
)
)
def make_write_off_gl_entry(self, gl_entries):
doc = self.doc
if doc.write_off_account and flt(doc.write_off_amount):
write_off_account_currency = get_account_currency(doc.write_off_account)
gl_entries.append(
doc.get_gl_dict(
{
"account": doc.credit_to,
"party_type": "Supplier",
"party": doc.supplier,
"against": doc.write_off_account,
"debit": doc.base_write_off_amount,
"debit_in_account_currency": doc.base_write_off_amount
if doc.party_account_currency == doc.company_currency
else doc.write_off_amount,
"debit_in_transaction_currency": doc.write_off_amount,
"against_voucher": doc.return_against
if cint(doc.is_return) and doc.return_against
else doc.name,
"against_voucher_type": doc.doctype,
"cost_center": doc.cost_center,
"project": doc.project,
},
doc.party_account_currency,
item=doc,
)
)
gl_entries.append(
doc.get_gl_dict(
{
"account": doc.write_off_account,
"against": doc.supplier,
"credit": flt(doc.base_write_off_amount),
"credit_in_account_currency": doc.base_write_off_amount
if write_off_account_currency == doc.company_currency
else doc.write_off_amount,
"credit_in_transaction_currency": doc.write_off_amount,
"cost_center": doc.cost_center or doc.write_off_cost_center,
},
item=doc,
)
)
def make_gle_for_rounding_adjustment(self, gl_entries):
doc = self.doc
if not doc.is_internal_transfer() and doc.rounding_adjustment and doc.base_rounding_adjustment:
(
round_off_account,
round_off_cost_center,
round_off_for_opening,
) = get_round_off_account_and_cost_center(
doc.company, "Purchase Invoice", doc.name, doc.use_company_roundoff_cost_center
)
if doc.is_opening == "Yes" and doc.rounding_adjustment:
if not round_off_for_opening:
frappe.throw(
_(
"Opening Invoice has rounding adjustment of {0}.<br><br> '{1}' account is required to post these values. Please set it in Company: {2}.<br><br> Or, '{3}' can be enabled to not post any rounding adjustment."
).format(
frappe.bold(doc.rounding_adjustment),
frappe.bold("Round Off for Opening"),
get_link_to_form("Company", doc.company),
frappe.bold("Disable Rounded Total"),
)
)
else:
round_off_account = round_off_for_opening
gl_entries.append(
doc.get_gl_dict(
{
"account": round_off_account,
"against": doc.supplier,
"debit_in_account_currency": doc.rounding_adjustment,
"debit": doc.base_rounding_adjustment,
"cost_center": round_off_cost_center
if doc.use_company_roundoff_cost_center
else (doc.cost_center or round_off_cost_center),
},
item=doc,
)
)