From 2a6d9be18a2618fa4f04daa9ab048153c4eccde9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 7 Jun 2026 09:54:28 +0530 Subject: [PATCH] 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. --- .../purchase_receipt/purchase_receipt.py | 64 ++------------- .../services/provisional_accounting.py | 78 +++++++++++++++++++ 2 files changed, 84 insertions(+), 58 deletions(-) create mode 100644 erpnext/stock/doctype/purchase_receipt/services/provisional_accounting.py diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1905027c2e8..1078a750e92 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -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: diff --git a/erpnext/stock/doctype/purchase_receipt/services/provisional_accounting.py b/erpnext/stock/doctype/purchase_receipt/services/provisional_accounting.py new file mode 100644 index 00000000000..3939135d3fb --- /dev/null +++ b/erpnext/stock/doctype/purchase_receipt/services/provisional_accounting.py @@ -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, + )