mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-04 00:50:18 +00:00
refactor(accounts): extract Purchase Invoice BillingStatusService
Move PR billing sync and provisional-entry cancellation into accounts/doctype/purchase_invoice/services/billing_status.py: - update_billing_status_in_pr, get_pr_details_billed_amt and cancel_provisional_entries move into the service (internal-only; on_submit/on_cancel and make_gl_entries repointed) - the service imports the shared allocation helpers from purchase_receipt/services/billing_status.py (PR owns the shared buying billing logic) - also repoints the validate_expense_account call in validate_for_repost missed in the ExpenseAccountService commit No behaviour change.
This commit is contained in:
@@ -3,13 +3,13 @@
|
|||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, throw
|
from frappe import _, throw
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.query_builder.functions import Sum
|
|
||||||
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||||
|
from erpnext.accounts.doctype.purchase_invoice.services.billing_status import BillingStatusService
|
||||||
from erpnext.accounts.doctype.purchase_invoice.services.expense_account import ExpenseAccountService
|
from erpnext.accounts.doctype.purchase_invoice.services.expense_account import ExpenseAccountService
|
||||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||||
validate_docs_for_deferred_accounting,
|
validate_docs_for_deferred_accounting,
|
||||||
@@ -34,9 +34,6 @@ from erpnext.accounts.party import get_due_date, get_party_account
|
|||||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
from erpnext.accounts.utils import get_account_currency, get_fiscal_year, update_voucher_outstanding
|
||||||
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||||
from erpnext.controllers.buying_controller import BuyingController
|
from erpnext.controllers.buying_controller import BuyingController
|
||||||
from erpnext.stock.doctype.purchase_receipt.services.billing_status import (
|
|
||||||
update_billed_amount_based_on_po,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class WarehouseMissingError(frappe.ValidationError):
|
class WarehouseMissingError(frappe.ValidationError):
|
||||||
@@ -600,7 +597,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
def validate_for_repost(self):
|
def validate_for_repost(self):
|
||||||
self.validate_write_off_account()
|
self.validate_write_off_account()
|
||||||
self.validate_write_off_cost_center()
|
self.validate_write_off_cost_center()
|
||||||
self.validate_expense_account()
|
ExpenseAccountService(self).validate_expense_account()
|
||||||
validate_docs_for_voucher_types(["Purchase Invoice"])
|
validate_docs_for_voucher_types(["Purchase Invoice"])
|
||||||
validate_docs_for_deferred_accounting([], [self.name])
|
validate_docs_for_deferred_accounting([], [self.name])
|
||||||
|
|
||||||
@@ -629,7 +626,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.update_billing_status_for_zero_amount_refdoc("Purchase Receipt")
|
self.update_billing_status_for_zero_amount_refdoc("Purchase Receipt")
|
||||||
self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
|
self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
|
||||||
|
|
||||||
self.update_billing_status_in_pr()
|
BillingStatusService(self).update_billing_status_in_pr()
|
||||||
|
|
||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||||
@@ -680,32 +677,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.make_exchange_gain_loss_journal()
|
self.make_exchange_gain_loss_journal()
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
self.cancel_provisional_entries()
|
BillingStatusService(self).cancel_provisional_entries()
|
||||||
|
|
||||||
self.update_supplier_outstanding(update_outstanding)
|
self.update_supplier_outstanding(update_outstanding)
|
||||||
|
|
||||||
def cancel_provisional_entries(self):
|
|
||||||
rows = set()
|
|
||||||
purchase_receipts = set()
|
|
||||||
for d in self.items:
|
|
||||||
if d.purchase_receipt:
|
|
||||||
purchase_receipts.add(d.purchase_receipt)
|
|
||||||
rows.add(d.name)
|
|
||||||
|
|
||||||
if rows:
|
|
||||||
# cancel gl entries
|
|
||||||
gle = qb.DocType("GL Entry")
|
|
||||||
gle_update_query = (
|
|
||||||
qb.update(gle)
|
|
||||||
.set(gle.is_cancelled, 1)
|
|
||||||
.where(
|
|
||||||
(gle.voucher_type == "Purchase Receipt")
|
|
||||||
& (gle.voucher_no.isin(purchase_receipts))
|
|
||||||
& (gle.voucher_detail_no.isin(rows))
|
|
||||||
)
|
|
||||||
)
|
|
||||||
gle_update_query.run()
|
|
||||||
|
|
||||||
def update_supplier_outstanding(self, update_outstanding):
|
def update_supplier_outstanding(self, update_outstanding):
|
||||||
if update_outstanding == "No":
|
if update_outstanding == "No":
|
||||||
update_voucher_outstanding(
|
update_voucher_outstanding(
|
||||||
@@ -755,7 +730,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.update_billing_status_for_zero_amount_refdoc("Purchase Receipt")
|
self.update_billing_status_for_zero_amount_refdoc("Purchase Receipt")
|
||||||
self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
|
self.update_billing_status_for_zero_amount_refdoc("Purchase Order")
|
||||||
|
|
||||||
self.update_billing_status_in_pr()
|
BillingStatusService(self).update_billing_status_in_pr()
|
||||||
|
|
||||||
# Updating stock ledger should always be called after updating prevdoc status,
|
# Updating stock ledger should always be called after updating prevdoc status,
|
||||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||||
@@ -838,63 +813,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_billing_status_in_pr(self, update_modified=True):
|
|
||||||
if self.is_return and not self.update_billed_amount_in_purchase_receipt:
|
|
||||||
return
|
|
||||||
|
|
||||||
updated_pr = []
|
|
||||||
po_details = []
|
|
||||||
|
|
||||||
pr_details_billed_amt = self.get_pr_details_billed_amt()
|
|
||||||
|
|
||||||
for d in self.get("items"):
|
|
||||||
if d.pr_detail:
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Purchase Receipt Item",
|
|
||||||
d.pr_detail,
|
|
||||||
"billed_amt",
|
|
||||||
flt(pr_details_billed_amt.get(d.pr_detail)),
|
|
||||||
update_modified=update_modified,
|
|
||||||
)
|
|
||||||
updated_pr.append(d.purchase_receipt)
|
|
||||||
elif d.po_detail:
|
|
||||||
po_details.append(d.po_detail)
|
|
||||||
|
|
||||||
if po_details:
|
|
||||||
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
|
|
||||||
|
|
||||||
adjust_incoming_rate = frappe.db.get_single_value(
|
|
||||||
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
|
||||||
)
|
|
||||||
|
|
||||||
for pr in set(updated_pr):
|
|
||||||
from erpnext.stock.doctype.purchase_receipt.services.billing_status import (
|
|
||||||
update_billing_percentage,
|
|
||||||
)
|
|
||||||
|
|
||||||
pr_doc = frappe.get_lazy_doc("Purchase Receipt", pr)
|
|
||||||
update_billing_percentage(
|
|
||||||
pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_pr_details_billed_amt(self):
|
|
||||||
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
|
|
||||||
|
|
||||||
pr_details_billed_amt = {}
|
|
||||||
pr_details = [d.get("pr_detail") for d in self.get("items") if d.get("pr_detail")]
|
|
||||||
if pr_details:
|
|
||||||
doctype = frappe.qb.DocType("Purchase Invoice Item")
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(doctype)
|
|
||||||
.select(doctype.pr_detail, Sum(doctype.amount))
|
|
||||||
.where(doctype.pr_detail.isin(pr_details) & doctype.docstatus == 1)
|
|
||||||
.groupby(doctype.pr_detail)
|
|
||||||
)
|
|
||||||
|
|
||||||
pr_details_billed_amt = frappe._dict(query.run(as_list=1))
|
|
||||||
|
|
||||||
return pr_details_billed_amt
|
|
||||||
|
|
||||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||||
self.due_date = None
|
self.due_date = None
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
"""Purchase Receipt billing sync and provisional-entry cancellation for Purchase Invoice."""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import qb
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.purchase_receipt.services.billing_status import (
|
||||||
|
update_billed_amount_based_on_po,
|
||||||
|
update_billing_percentage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BillingStatusService:
|
||||||
|
def __init__(self, doc):
|
||||||
|
self.doc = doc
|
||||||
|
|
||||||
|
def update_billing_status_in_pr(self, update_modified: bool = True) -> None:
|
||||||
|
doc = self.doc
|
||||||
|
if doc.is_return and not doc.update_billed_amount_in_purchase_receipt:
|
||||||
|
return
|
||||||
|
|
||||||
|
updated_pr = []
|
||||||
|
po_details = []
|
||||||
|
|
||||||
|
pr_details_billed_amt = self.get_pr_details_billed_amt()
|
||||||
|
|
||||||
|
for d in doc.get("items"):
|
||||||
|
if d.pr_detail:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Purchase Receipt Item",
|
||||||
|
d.pr_detail,
|
||||||
|
"billed_amt",
|
||||||
|
flt(pr_details_billed_amt.get(d.pr_detail)),
|
||||||
|
update_modified=update_modified,
|
||||||
|
)
|
||||||
|
updated_pr.append(d.purchase_receipt)
|
||||||
|
elif d.po_detail:
|
||||||
|
po_details.append(d.po_detail)
|
||||||
|
|
||||||
|
if po_details:
|
||||||
|
updated_pr += update_billed_amount_based_on_po(po_details, update_modified)
|
||||||
|
|
||||||
|
adjust_incoming_rate = frappe.db.get_single_value(
|
||||||
|
"Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate"
|
||||||
|
)
|
||||||
|
|
||||||
|
for pr in set(updated_pr):
|
||||||
|
pr_doc = frappe.get_lazy_doc("Purchase Receipt", pr)
|
||||||
|
update_billing_percentage(
|
||||||
|
pr_doc, update_modified=update_modified, adjust_incoming_rate=adjust_incoming_rate
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_pr_details_billed_amt(self) -> dict:
|
||||||
|
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
|
||||||
|
|
||||||
|
pr_details_billed_amt = {}
|
||||||
|
pr_details = [d.get("pr_detail") for d in self.doc.get("items") if d.get("pr_detail")]
|
||||||
|
if pr_details:
|
||||||
|
doctype = frappe.qb.DocType("Purchase Invoice Item")
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.select(doctype.pr_detail, Sum(doctype.amount))
|
||||||
|
.where(doctype.pr_detail.isin(pr_details) & doctype.docstatus == 1)
|
||||||
|
.groupby(doctype.pr_detail)
|
||||||
|
)
|
||||||
|
|
||||||
|
pr_details_billed_amt = frappe._dict(query.run(as_list=1))
|
||||||
|
|
||||||
|
return pr_details_billed_amt
|
||||||
|
|
||||||
|
def cancel_provisional_entries(self) -> None:
|
||||||
|
rows = set()
|
||||||
|
purchase_receipts = set()
|
||||||
|
for d in self.doc.items:
|
||||||
|
if d.purchase_receipt:
|
||||||
|
purchase_receipts.add(d.purchase_receipt)
|
||||||
|
rows.add(d.name)
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
# cancel gl entries
|
||||||
|
gle = qb.DocType("GL Entry")
|
||||||
|
gle_update_query = (
|
||||||
|
qb.update(gle)
|
||||||
|
.set(gle.is_cancelled, 1)
|
||||||
|
.where(
|
||||||
|
(gle.voucher_type == "Purchase Receipt")
|
||||||
|
& (gle.voucher_no.isin(purchase_receipts))
|
||||||
|
& (gle.voucher_detail_no.isin(rows))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
gle_update_query.run()
|
||||||
Reference in New Issue
Block a user