refactor(stock): extract Purchase Receipt ProvisionalAccountingService

Move provisional accounting for non-stock items into
stock/doctype/purchase_receipt/services/provisional_accounting.py:

- add_provisional_gl_entry stays as a controller delegator (called as a
  doc method by both the PR and PI GL composers)
- validate_provisional_expense_account moves into the service;
  validate() repointed

No behaviour change.
This commit is contained in:
Nabin Hait
2026-06-07 09:54:28 +05:30
parent d1765e85aa
commit 2a6d9be18a
2 changed files with 84 additions and 58 deletions

View File

@@ -9,10 +9,12 @@ from frappe.model.document import Document
from frappe.utils import cint, flt, get_datetime, getdate, nowdate
import erpnext
from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.purchase_receipt.services.billing_status import BillingStatusService
from erpnext.stock.doctype.purchase_receipt.services.provisional_accounting import (
ProvisionalAccountingService,
)
from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import StockReservation
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -251,7 +253,7 @@ class PurchaseReceipt(BuyingController):
self.validate_with_previous_doc()
self.validate_uom_is_integer()
self.validate_cwip_accounts()
self.validate_provisional_expense_account()
ProvisionalAccountingService(self).validate_provisional_expense_account()
self.check_for_on_hold_or_closed_status("Purchase Order", "purchase_order")
@@ -280,19 +282,6 @@ class PurchaseReceipt(BuyingController):
)
break
def validate_provisional_expense_account(self):
provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value("Company", self.company, "enable_provisional_accounting_for_non_stock_items")
)
if not provisional_accounting_for_non_stock_items:
return
default_provisional_account = self.get_company_default("default_provisional_account")
for item in self.get("items"):
if not item.get("provisional_expense_account"):
item.provisional_expense_account = default_provisional_account
def validate_with_previous_doc(self):
super().validate_with_previous_doc(
{
@@ -483,49 +472,8 @@ class PurchaseReceipt(BuyingController):
def add_provisional_gl_entry(
self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None
):
credit_currency = get_account_currency(provisional_account)
expense_account = item.expense_account
debit_currency = get_account_currency(item.expense_account)
remarks = self.get("remarks") or _("Accounting Entry for Service")
multiplication_factor = 1
amount = item.base_amount
if reverse:
multiplication_factor = -1
# Post reverse entry for previously posted amount
amount = item_amount
expense_account = frappe.db.get_value(
"Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"]
)
self.add_gl_entry(
gl_entries=gl_entries,
account=provisional_account,
cost_center=item.cost_center,
debit=0.0,
credit=multiplication_factor * amount,
remarks=remarks,
against_account=expense_account,
account_currency=credit_currency,
project=item.project,
voucher_detail_no=item.name,
item=item,
posting_date=posting_date,
)
self.add_gl_entry(
gl_entries=gl_entries,
account=expense_account,
cost_center=item.cost_center,
debit=multiplication_factor * amount,
credit=0.0,
remarks=remarks,
against_account=provisional_account,
account_currency=debit_currency,
project=item.project,
voucher_detail_no=item.name,
item=item,
posting_date=posting_date,
ProvisionalAccountingService(self).add_provisional_gl_entry(
item, gl_entries, posting_date, provisional_account, reverse, item_amount
)
def is_landed_cost_booked_for_any_item(self) -> bool:

View File

@@ -0,0 +1,78 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
"""Provisional accounting for non-stock items received via Purchase Receipt."""
import frappe
from frappe import _
from frappe.utils import cint
from erpnext.accounts.utils import get_account_currency
class ProvisionalAccountingService:
def __init__(self, doc):
self.doc = doc
def validate_provisional_expense_account(self) -> None:
doc = self.doc
provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value("Company", doc.company, "enable_provisional_accounting_for_non_stock_items")
)
if not provisional_accounting_for_non_stock_items:
return
default_provisional_account = doc.get_company_default("default_provisional_account")
for item in doc.get("items"):
if not item.get("provisional_expense_account"):
item.provisional_expense_account = default_provisional_account
def add_provisional_gl_entry(
self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None
) -> None:
doc = self.doc
credit_currency = get_account_currency(provisional_account)
expense_account = item.expense_account
debit_currency = get_account_currency(item.expense_account)
remarks = doc.get("remarks") or _("Accounting Entry for Service")
multiplication_factor = 1
amount = item.base_amount
if reverse:
multiplication_factor = -1
# Post reverse entry for previously posted amount
amount = item_amount
expense_account = frappe.db.get_value(
"Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"]
)
doc.add_gl_entry(
gl_entries=gl_entries,
account=provisional_account,
cost_center=item.cost_center,
debit=0.0,
credit=multiplication_factor * amount,
remarks=remarks,
against_account=expense_account,
account_currency=credit_currency,
project=item.project,
voucher_detail_no=item.name,
item=item,
posting_date=posting_date,
)
doc.add_gl_entry(
gl_entries=gl_entries,
account=expense_account,
cost_center=item.cost_center,
debit=multiplication_factor * amount,
credit=0.0,
remarks=remarks,
against_account=provisional_account,
account_currency=debit_currency,
project=item.project,
voucher_detail_no=item.name,
item=item,
posting_date=posting_date,
)