refactor(stock): extract Purchase Receipt BillingStatusService

Move PR↔PI billed-amount allocation into
stock/doctype/purchase_receipt/services/billing_status.py. Purchase
Receipt owns the shared buying billing logic; Purchase Invoice imports
from the service module:

- update_billing_status stays as a controller delegator (called by
  Purchase Invoice flows and v13 patches)
- the module-function family moves verbatim:
  update_billed_amount_based_on_po, update_billing_percentage,
  get_billed_amount_against_pr/_po,
  get_purchase_receipts_against_po_details,
  get_billed_qty_amount_against_purchase_receipt/_order,
  adjust_incoming_rate_for_pr, get_item_wise_returned_qty
- imports repointed in purchase_invoice.py (top-level + lazy) and
  patches/v15_0/recalculate_amount_difference_field.py

No behaviour change.
This commit is contained in:
Nabin Hait
2026-06-07 09:53:37 +05:30
parent fcff212eec
commit d1765e85aa
4 changed files with 398 additions and 372 deletions

View File

@@ -35,7 +35,7 @@ from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.controllers.accounts_controller import validate_account_head
from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
from erpnext.stock.doctype.purchase_receipt.services.billing_status import (
update_billed_amount_based_on_po,
)
@@ -1024,7 +1024,9 @@ class PurchaseInvoice(BuyingController):
)
for pr in set(updated_pr):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
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(

View File

@@ -3,7 +3,7 @@ from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import adjust_incoming_rate_for_pr
from erpnext.stock.doctype.purchase_receipt.services.billing_status import adjust_incoming_rate_for_pr
def execute():

View File

@@ -6,14 +6,13 @@ import frappe
from frappe import _, throw
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.document import Document
from frappe.query_builder.functions import CombineDatetime
from frappe.utils import cint, flt, get_datetime, getdate, nowdate
from pypika import functions as fn
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.stock_reservation_entry.stock_reservation_entry import StockReservation
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
@@ -564,20 +563,7 @@ class PurchaseReceipt(BuyingController):
clear_doctype_notifications(self)
def update_billing_status(self, update_modified=True):
updated_pr = [self.name]
po_details = []
for d in self.get("items"):
if d.get("purchase_invoice") and d.get("purchase_invoice_item"):
d.db_set("billed_amt", d.amount, update_modified=update_modified)
elif d.purchase_order_item:
po_details.append(d.purchase_order_item)
if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified, self)
for pr in set(updated_pr):
pr_doc = self if (pr == self.name) else frappe.get_lazy_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
BillingStatusService(self).update_billing_status(update_modified)
def reserve_stock(self):
self.reserve_stock_for_sales_order()
@@ -712,359 +698,6 @@ def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse):
)
def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=None):
po_billed_amt_details = get_billed_amount_against_po(po_details)
# Get all Purchase Receipt Item rows against the Purchase Order Items
pr_details = get_purchase_receipts_against_po_details(po_details)
pr_items = [pr_detail.name for pr_detail in pr_details]
pr_items_billed_amount = get_billed_amount_against_pr(pr_items)
updated_pr = []
for pr_item in pr_details:
billed_amt_against_po, billed_qty_against_po = 0, 0
if billed_details := po_billed_amt_details.get(pr_item.purchase_order_item):
billed_amt_against_po = flt(billed_details["billed_amt"])
billed_qty_against_po = flt(billed_details["billed_qty"])
# Get billed amount directly against Purchase Receipt
billed_amt_against_pr = flt(pr_items_billed_amount.get(pr_item.name, 0))
# Distribute billed amount directly against PO between PRs based on FIFO
if billed_amt_against_po and billed_amt_against_pr < pr_item.amount:
if not billed_amt_against_pr and billed_qty_against_po and billed_qty_against_po > pr_item.qty:
billed_amt_against_pr = flt(flt(billed_amt_against_po) * flt(pr_item.qty)) / flt(
billed_qty_against_po
)
else:
pending_to_bill = flt(pr_item.amount) - billed_amt_against_pr
if pending_to_bill <= billed_amt_against_po:
billed_amt_against_pr += pending_to_bill
billed_amt_against_po -= pending_to_bill
else:
billed_amt_against_pr += billed_amt_against_po
billed_amt_against_po = 0
po_billed_amt_details[pr_item.purchase_order_item]["billed_amt"] = billed_amt_against_po
if pr_item.billed_amt != billed_amt_against_pr:
# update existing doc if possible
if pr_doc and pr_item.parent == pr_doc.name:
pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None)
pr_item.db_set("billed_amt", billed_amt_against_pr, update_modified=update_modified)
else:
frappe.db.set_value(
"Purchase Receipt Item",
pr_item.name,
"billed_amt",
billed_amt_against_pr,
update_modified=update_modified,
)
updated_pr.append(pr_item.parent)
return updated_pr
def get_purchase_receipts_against_po_details(po_details):
# Get Purchase Receipts against Purchase Order Items
purchase_receipt = frappe.qb.DocType("Purchase Receipt")
purchase_receipt_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(purchase_receipt)
.inner_join(purchase_receipt_item)
.on(purchase_receipt.name == purchase_receipt_item.parent)
.select(
purchase_receipt_item.name,
purchase_receipt_item.qty,
purchase_receipt_item.parent,
purchase_receipt_item.amount,
purchase_receipt_item.billed_amt,
purchase_receipt_item.purchase_order_item,
)
.where(
(purchase_receipt_item.purchase_order_item.isin(po_details))
& (purchase_receipt.docstatus == 1)
& (purchase_receipt.is_return == 0)
)
.orderby(CombineDatetime(purchase_receipt.posting_date, purchase_receipt.posting_time))
.orderby(purchase_receipt.name)
)
return query.run(as_dict=True)
def get_billed_amount_against_pr(pr_items):
# Get billed amount directly against Purchase Receipt
if not pr_items:
return {}
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.pr_detail)
.where((purchase_invoice_item.pr_detail.isin(pr_items)) & (purchase_invoice_item.docstatus == 1))
.groupby(purchase_invoice_item.pr_detail)
).run(as_dict=1)
return {d.pr_detail: flt(d.billed_amt) for d in query}
def get_billed_amount_against_po(po_items):
# Get billed amount directly against Purchase Order
if not po_items:
return {}
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
.inner_join(purchase_invoice)
.on(purchase_invoice_item.parent == purchase_invoice.name)
.select(
fn.Sum(purchase_invoice_item.amount).as_("billed_amt"),
fn.Sum(purchase_invoice_item.qty).as_("qty"),
purchase_invoice_item.po_detail,
)
.where(
(purchase_invoice_item.po_detail.isin(po_items))
& (purchase_invoice.docstatus == 1)
& (purchase_invoice_item.pr_detail.isnull())
& (purchase_invoice.update_stock == 0)
)
.groupby(purchase_invoice_item.po_detail)
).run(as_dict=1)
return {d.po_detail: {"billed_amt": flt(d.billed_amt), "billed_qty": flt(d.qty)} for d in query}
def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate=False):
# Update Billing % based on pending accepted qty
buying_settings = frappe.get_single("Buying Settings")
over_billing_allowance, role_allowed_to_over_bill = frappe.get_single_value(
"Accounts Settings", ["over_billing_allowance", "role_allowed_to_over_bill"]
)
total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
billed_qty_amt = frappe._dict()
if adjust_incoming_rate:
billed_qty_amt = get_billed_qty_amount_against_purchase_receipt(pr_doc)
billed_qty_amt_based_on_po = get_billed_qty_amount_against_purchase_order(pr_doc)
for item in pr_doc.items:
returned_qty = flt(item_wise_returned_qty.get(item.name))
returned_amount = flt(returned_qty) * flt(item.rate)
pending_amount = flt(item.amount) - returned_amount
if buying_settings.bill_for_rejected_quantity_in_purchase_invoice:
pending_amount = flt(item.amount)
total_billable_amount = abs(flt(item.amount))
if pending_amount > 0:
total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
total_amount += total_billable_amount
total_billed_amount += abs(flt(item.billed_amt))
if pr_doc.get("is_return") and not total_amount and total_billed_amount:
total_amount = total_billed_amount
amount = item.amount
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
amount += flt(item.rejected_qty * item.rate, item.precision("amount"))
if adjust_incoming_rate:
adjusted_amt = 0.0
if (
item.billed_amt is not None
and item.amount is not None
and (
billed_qty_amt.get(item.name) or billed_qty_amt_based_on_po.get(item.purchase_order_item)
)
):
qty = None
if billed_qty_amt.get(item.name):
qty = billed_qty_amt.get(item.name).get("qty")
if not qty and billed_qty_amt_based_on_po.get(item.purchase_order_item):
if item.qty < billed_qty_amt_based_on_po.get(item.purchase_order_item)["qty"]:
qty = item.qty
else:
qty = billed_qty_amt_based_on_po.get(item.purchase_order_item)["qty"]
billed_qty_amt_based_on_po[item.purchase_order_item]["qty"] -= qty
billed_amt = item.billed_amt
if billed_qty_amt.get(item.name):
billed_amt = flt(billed_qty_amt.get(item.name).get("amount"))
elif billed_qty_amt_based_on_po.get(item.purchase_order_item):
total_billed_qty = (
billed_qty_amt_based_on_po.get(item.purchase_order_item).get("qty") + qty
)
if total_billed_qty:
billed_amt = flt(
flt(billed_qty_amt_based_on_po.get(item.purchase_order_item).get("amount"))
* (qty / total_billed_qty)
)
else:
billed_amt = 0.0
# Reduce billed amount based on PO for next iterations
billed_qty_amt_based_on_po[item.purchase_order_item]["amount"] -= billed_amt
if qty:
adjusted_amt = (
flt(billed_amt / qty) - (flt(item.rate) * flt(pr_doc.conversion_rate))
) * item.qty
adjusted_amt = flt(adjusted_amt, item.precision("amount"))
pi_landed_cost_amount += adjusted_amt
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
elif amount and item.billed_amt > amount:
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
if (
per_over_billed > over_billing_allowance
and role_allowed_to_over_bill not in frappe.get_roles()
):
frappe.throw(
_("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format(
item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance
)
)
if pi_landed_cost_amount < 0:
total_billed_amount += abs(pi_landed_cost_amount)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)
if update_modified:
pr_doc.set_status(update=True)
pr_doc.notify_update()
if adjust_incoming_rate:
adjust_incoming_rate_for_pr(pr_doc)
def get_billed_qty_amount_against_purchase_receipt(pr_doc):
pr_names = [d.name for d in pr_doc.items]
parent_table = frappe.qb.DocType("Purchase Invoice")
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(parent_table)
.inner_join(table)
.on(parent_table.name == table.parent)
.select(
table.pr_detail,
fn.Sum(table.base_net_amount).as_("amount"),
fn.Sum(table.qty).as_("qty"),
)
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
.groupby(table.pr_detail)
)
invoice_data = query.run(as_dict=1)
if not invoice_data:
return frappe._dict()
billed_qty_amt = frappe._dict()
for row in invoice_data:
if row.pr_detail not in billed_qty_amt:
billed_qty_amt[row.pr_detail] = {"amount": 0, "qty": 0}
billed_qty_amt[row.pr_detail]["amount"] += flt(row.amount)
billed_qty_amt[row.pr_detail]["qty"] += flt(row.qty)
return billed_qty_amt
def get_billed_qty_amount_against_purchase_order(pr_doc):
po_names = list(
set(
[
d.purchase_order_item
for d in pr_doc.items
if d.purchase_order_item and not d.purchase_invoice_item
]
)
)
invoice_data_po_based = frappe._dict()
if po_names:
parent_table = frappe.qb.DocType("Purchase Invoice")
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(parent_table)
.inner_join(table)
.on(parent_table.name == table.parent)
.select(
table.po_detail,
fn.Sum(table.qty).as_("qty"),
fn.Sum(table.base_net_amount).as_("amount"),
)
.where((table.po_detail.isin(po_names)) & (table.docstatus == 1) & (table.pr_detail.isnull()))
.groupby(table.po_detail)
)
invoice_data = query.run(as_dict=1)
if not invoice_data:
return frappe._dict()
for row in invoice_data:
if row.po_detail not in invoice_data_po_based:
invoice_data_po_based[row.po_detail] = {"amount": 0, "qty": 0}
invoice_data_po_based[row.po_detail]["amount"] += flt(row.amount)
invoice_data_po_based[row.po_detail]["qty"] += flt(row.qty)
return invoice_data_po_based
def adjust_incoming_rate_for_pr(doc):
doc.update_valuation_rate(reset_outgoing_rate=False)
for item in doc.get("items"):
item.db_update()
if doc.doctype == "Purchase Receipt":
doc.enable_recalculate_rate_in_sles()
doc.repost_future_sle_and_gle(force=True)
def get_item_wise_returned_qty(pr_doc):
items = [d.name for d in pr_doc.items]
return frappe._dict(
frappe.get_all(
"Purchase Receipt",
fields=[
"`tabPurchase Receipt Item`.purchase_receipt_item",
{"SUM": [{"ABS": "`tabPurchase Receipt Item`.qty"}], "as": "qty"},
],
filters=[
["Purchase Receipt", "docstatus", "=", 1],
["Purchase Receipt", "is_return", "=", 1],
["Purchase Receipt Item", "purchase_receipt_item", "in", items],
],
group_by="`tabPurchase Receipt Item`.purchase_receipt_item",
as_list=1,
)
)
@frappe.whitelist()
def update_purchase_receipt_status(docname: str, status: str):
pr = frappe.get_lazy_doc("Purchase Receipt", docname, check_permission="submit")

View File

@@ -0,0 +1,391 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
"""Billing status tracking and PR↔PI billed-amount allocation for Purchase Receipt.
Purchase Invoice imports the module-level allocation helpers from here —
Purchase Receipt owns the shared buying billing logic.
"""
import frappe
from frappe import _
from frappe.query_builder.functions import CombineDatetime
from frappe.utils import flt
from pypika import functions as fn
class BillingStatusService:
def __init__(self, doc):
self.doc = doc
def update_billing_status(self, update_modified: bool = True) -> None:
doc = self.doc
updated_pr = [doc.name]
po_details = []
for d in doc.get("items"):
if d.get("purchase_invoice") and d.get("purchase_invoice_item"):
d.db_set("billed_amt", d.amount, update_modified=update_modified)
elif d.purchase_order_item:
po_details.append(d.purchase_order_item)
if po_details:
updated_pr += update_billed_amount_based_on_po(po_details, update_modified, doc)
for pr in set(updated_pr):
pr_doc = doc if (pr == doc.name) else frappe.get_lazy_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
def update_billed_amount_based_on_po(po_details: list, update_modified: bool = True, pr_doc=None) -> list:
po_billed_amt_details = get_billed_amount_against_po(po_details)
# Get all Purchase Receipt Item rows against the Purchase Order Items
pr_details = get_purchase_receipts_against_po_details(po_details)
pr_items = [pr_detail.name for pr_detail in pr_details]
pr_items_billed_amount = get_billed_amount_against_pr(pr_items)
updated_pr = []
for pr_item in pr_details:
billed_amt_against_po, billed_qty_against_po = 0, 0
if billed_details := po_billed_amt_details.get(pr_item.purchase_order_item):
billed_amt_against_po = flt(billed_details["billed_amt"])
billed_qty_against_po = flt(billed_details["billed_qty"])
# Get billed amount directly against Purchase Receipt
billed_amt_against_pr = flt(pr_items_billed_amount.get(pr_item.name, 0))
# Distribute billed amount directly against PO between PRs based on FIFO
if billed_amt_against_po and billed_amt_against_pr < pr_item.amount:
if not billed_amt_against_pr and billed_qty_against_po and billed_qty_against_po > pr_item.qty:
billed_amt_against_pr = flt(flt(billed_amt_against_po) * flt(pr_item.qty)) / flt(
billed_qty_against_po
)
else:
pending_to_bill = flt(pr_item.amount) - billed_amt_against_pr
if pending_to_bill <= billed_amt_against_po:
billed_amt_against_pr += pending_to_bill
billed_amt_against_po -= pending_to_bill
else:
billed_amt_against_pr += billed_amt_against_po
billed_amt_against_po = 0
po_billed_amt_details[pr_item.purchase_order_item]["billed_amt"] = billed_amt_against_po
if pr_item.billed_amt != billed_amt_against_pr:
# update existing doc if possible
if pr_doc and pr_item.parent == pr_doc.name:
pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None)
pr_item.db_set("billed_amt", billed_amt_against_pr, update_modified=update_modified)
else:
frappe.db.set_value(
"Purchase Receipt Item",
pr_item.name,
"billed_amt",
billed_amt_against_pr,
update_modified=update_modified,
)
updated_pr.append(pr_item.parent)
return updated_pr
def get_purchase_receipts_against_po_details(po_details: list) -> list[dict]:
# Get Purchase Receipts against Purchase Order Items
purchase_receipt = frappe.qb.DocType("Purchase Receipt")
purchase_receipt_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(purchase_receipt)
.inner_join(purchase_receipt_item)
.on(purchase_receipt.name == purchase_receipt_item.parent)
.select(
purchase_receipt_item.name,
purchase_receipt_item.qty,
purchase_receipt_item.parent,
purchase_receipt_item.amount,
purchase_receipt_item.billed_amt,
purchase_receipt_item.purchase_order_item,
)
.where(
(purchase_receipt_item.purchase_order_item.isin(po_details))
& (purchase_receipt.docstatus == 1)
& (purchase_receipt.is_return == 0)
)
.orderby(CombineDatetime(purchase_receipt.posting_date, purchase_receipt.posting_time))
.orderby(purchase_receipt.name)
)
return query.run(as_dict=True)
def get_billed_amount_against_pr(pr_items: list) -> dict:
# Get billed amount directly against Purchase Receipt
if not pr_items:
return {}
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
.select(fn.Sum(purchase_invoice_item.amount).as_("billed_amt"), purchase_invoice_item.pr_detail)
.where((purchase_invoice_item.pr_detail.isin(pr_items)) & (purchase_invoice_item.docstatus == 1))
.groupby(purchase_invoice_item.pr_detail)
).run(as_dict=1)
return {d.pr_detail: flt(d.billed_amt) for d in query}
def get_billed_amount_against_po(po_items: list) -> dict:
# Get billed amount directly against Purchase Order
if not po_items:
return {}
purchase_invoice = frappe.qb.DocType("Purchase Invoice")
purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(purchase_invoice_item)
.inner_join(purchase_invoice)
.on(purchase_invoice_item.parent == purchase_invoice.name)
.select(
fn.Sum(purchase_invoice_item.amount).as_("billed_amt"),
fn.Sum(purchase_invoice_item.qty).as_("qty"),
purchase_invoice_item.po_detail,
)
.where(
(purchase_invoice_item.po_detail.isin(po_items))
& (purchase_invoice.docstatus == 1)
& (purchase_invoice_item.pr_detail.isnull())
& (purchase_invoice.update_stock == 0)
)
.groupby(purchase_invoice_item.po_detail)
).run(as_dict=1)
return {d.po_detail: {"billed_amt": flt(d.billed_amt), "billed_qty": flt(d.qty)} for d in query}
def update_billing_percentage(
pr_doc, update_modified: bool = True, adjust_incoming_rate: bool = False
) -> None:
# Update Billing % based on pending accepted qty
buying_settings = frappe.get_single("Buying Settings")
over_billing_allowance, role_allowed_to_over_bill = frappe.get_single_value(
"Accounts Settings", ["over_billing_allowance", "role_allowed_to_over_bill"]
)
total_amount, total_billed_amount, pi_landed_cost_amount = 0, 0, 0
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
billed_qty_amt = frappe._dict()
if adjust_incoming_rate:
billed_qty_amt = get_billed_qty_amount_against_purchase_receipt(pr_doc)
billed_qty_amt_based_on_po = get_billed_qty_amount_against_purchase_order(pr_doc)
for item in pr_doc.items:
returned_qty = flt(item_wise_returned_qty.get(item.name))
returned_amount = flt(returned_qty) * flt(item.rate)
pending_amount = flt(item.amount) - returned_amount
if buying_settings.bill_for_rejected_quantity_in_purchase_invoice:
pending_amount = flt(item.amount)
total_billable_amount = abs(flt(item.amount))
if pending_amount > 0:
total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
total_amount += total_billable_amount
total_billed_amount += abs(flt(item.billed_amt))
if pr_doc.get("is_return") and not total_amount and total_billed_amount:
total_amount = total_billed_amount
amount = item.amount
if frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice"):
amount += flt(item.rejected_qty * item.rate, item.precision("amount"))
if adjust_incoming_rate:
adjusted_amt = 0.0
if (
item.billed_amt is not None
and item.amount is not None
and (
billed_qty_amt.get(item.name) or billed_qty_amt_based_on_po.get(item.purchase_order_item)
)
):
qty = None
if billed_qty_amt.get(item.name):
qty = billed_qty_amt.get(item.name).get("qty")
if not qty and billed_qty_amt_based_on_po.get(item.purchase_order_item):
if item.qty < billed_qty_amt_based_on_po.get(item.purchase_order_item)["qty"]:
qty = item.qty
else:
qty = billed_qty_amt_based_on_po.get(item.purchase_order_item)["qty"]
billed_qty_amt_based_on_po[item.purchase_order_item]["qty"] -= qty
billed_amt = item.billed_amt
if billed_qty_amt.get(item.name):
billed_amt = flt(billed_qty_amt.get(item.name).get("amount"))
elif billed_qty_amt_based_on_po.get(item.purchase_order_item):
total_billed_qty = (
billed_qty_amt_based_on_po.get(item.purchase_order_item).get("qty") + qty
)
if total_billed_qty:
billed_amt = flt(
flt(billed_qty_amt_based_on_po.get(item.purchase_order_item).get("amount"))
* (qty / total_billed_qty)
)
else:
billed_amt = 0.0
# Reduce billed amount based on PO for next iterations
billed_qty_amt_based_on_po[item.purchase_order_item]["amount"] -= billed_amt
if qty:
adjusted_amt = (
flt(billed_amt / qty) - (flt(item.rate) * flt(pr_doc.conversion_rate))
) * item.qty
adjusted_amt = flt(adjusted_amt, item.precision("amount"))
pi_landed_cost_amount += adjusted_amt
item.db_set("amount_difference_with_purchase_invoice", adjusted_amt, update_modified=False)
elif amount and item.billed_amt > amount:
per_over_billed = (flt(item.billed_amt / amount, 2) * 100) - 100
if (
per_over_billed > over_billing_allowance
and role_allowed_to_over_bill not in frappe.get_roles()
):
frappe.throw(
_("Over Billing Allowance exceeded for Purchase Receipt Item {0} ({1}) by {2}%").format(
item.name, frappe.bold(item.item_code), per_over_billed - over_billing_allowance
)
)
if pi_landed_cost_amount < 0:
total_billed_amount += abs(pi_landed_cost_amount)
percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
pr_doc.db_set("per_billed", percent_billed)
if update_modified:
pr_doc.set_status(update=True)
pr_doc.notify_update()
if adjust_incoming_rate:
adjust_incoming_rate_for_pr(pr_doc)
def get_billed_qty_amount_against_purchase_receipt(pr_doc) -> dict:
pr_names = [d.name for d in pr_doc.items]
parent_table = frappe.qb.DocType("Purchase Invoice")
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(parent_table)
.inner_join(table)
.on(parent_table.name == table.parent)
.select(
table.pr_detail,
fn.Sum(table.base_net_amount).as_("amount"),
fn.Sum(table.qty).as_("qty"),
)
.where((table.pr_detail.isin(pr_names)) & (table.docstatus == 1))
.groupby(table.pr_detail)
)
invoice_data = query.run(as_dict=1)
if not invoice_data:
return frappe._dict()
billed_qty_amt = frappe._dict()
for row in invoice_data:
if row.pr_detail not in billed_qty_amt:
billed_qty_amt[row.pr_detail] = {"amount": 0, "qty": 0}
billed_qty_amt[row.pr_detail]["amount"] += flt(row.amount)
billed_qty_amt[row.pr_detail]["qty"] += flt(row.qty)
return billed_qty_amt
def get_billed_qty_amount_against_purchase_order(pr_doc) -> dict:
po_names = list(
set(
[
d.purchase_order_item
for d in pr_doc.items
if d.purchase_order_item and not d.purchase_invoice_item
]
)
)
invoice_data_po_based = frappe._dict()
if po_names:
parent_table = frappe.qb.DocType("Purchase Invoice")
table = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(parent_table)
.inner_join(table)
.on(parent_table.name == table.parent)
.select(
table.po_detail,
fn.Sum(table.qty).as_("qty"),
fn.Sum(table.base_net_amount).as_("amount"),
)
.where((table.po_detail.isin(po_names)) & (table.docstatus == 1) & (table.pr_detail.isnull()))
.groupby(table.po_detail)
)
invoice_data = query.run(as_dict=1)
if not invoice_data:
return frappe._dict()
for row in invoice_data:
if row.po_detail not in invoice_data_po_based:
invoice_data_po_based[row.po_detail] = {"amount": 0, "qty": 0}
invoice_data_po_based[row.po_detail]["amount"] += flt(row.amount)
invoice_data_po_based[row.po_detail]["qty"] += flt(row.qty)
return invoice_data_po_based
def adjust_incoming_rate_for_pr(doc) -> None:
doc.update_valuation_rate(reset_outgoing_rate=False)
for item in doc.get("items"):
item.db_update()
if doc.doctype == "Purchase Receipt":
doc.enable_recalculate_rate_in_sles()
doc.repost_future_sle_and_gle(force=True)
def get_item_wise_returned_qty(pr_doc) -> dict:
items = [d.name for d in pr_doc.items]
return frappe._dict(
frappe.get_all(
"Purchase Receipt",
fields=[
"`tabPurchase Receipt Item`.purchase_receipt_item",
{"SUM": [{"ABS": "`tabPurchase Receipt Item`.qty"}], "as": "qty"},
],
filters=[
["Purchase Receipt", "docstatus", "=", 1],
["Purchase Receipt", "is_return", "=", 1],
["Purchase Receipt Item", "purchase_receipt_item", "in", items],
],
group_by="`tabPurchase Receipt Item`.purchase_receipt_item",
as_list=1,
)
)