341 lines
9.8 KiB
Python
341 lines
9.8 KiB
Python
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
|
# License: GNU General Public License v3. See license.txt
|
|
|
|
|
|
import frappe
|
|
from frappe.query_builder import DocType
|
|
from frappe.query_builder.functions import Sum
|
|
from frappe.query_builder.utils import Table
|
|
from frappe.utils import cstr, flt, now, nowdate, nowtime
|
|
from pypika.queries import QueryBuilder
|
|
|
|
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
|
|
|
|
|
|
def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, only_bin=False):
|
|
"""
|
|
Repost everything!
|
|
"""
|
|
frappe.db.auto_commit_on_many_writes = 1
|
|
|
|
if allow_negative_stock:
|
|
existing_allow_negative_stock = frappe.db.get_value(
|
|
"Stock Settings", None, "allow_negative_stock"
|
|
)
|
|
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
|
|
|
item_warehouses = frappe.db.sql(
|
|
"""
|
|
select distinct item_code, warehouse
|
|
from
|
|
(select item_code, warehouse from tabBin
|
|
union
|
|
select item_code, warehouse from `tabStock Ledger Entry`) a
|
|
"""
|
|
)
|
|
for d in item_warehouses:
|
|
try:
|
|
repost_stock(d[0], d[1], allow_zero_rate, only_actual, only_bin, allow_negative_stock)
|
|
frappe.db.commit()
|
|
except Exception:
|
|
frappe.db.rollback()
|
|
|
|
if allow_negative_stock:
|
|
frappe.db.set_value(
|
|
"Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock
|
|
)
|
|
frappe.db.auto_commit_on_many_writes = 0
|
|
|
|
|
|
def repost_stock(
|
|
item_code,
|
|
warehouse,
|
|
allow_zero_rate=False,
|
|
only_actual=False,
|
|
only_bin=False,
|
|
allow_negative_stock=False,
|
|
):
|
|
|
|
if not only_bin:
|
|
repost_actual_qty(item_code, warehouse, allow_zero_rate, allow_negative_stock)
|
|
|
|
if item_code and warehouse and not only_actual:
|
|
qty_dict = {
|
|
"reserved_qty": get_reserved_qty(item_code, warehouse),
|
|
"indented_qty": get_indented_qty(item_code, warehouse),
|
|
"ordered_qty": get_ordered_qty(item_code, warehouse),
|
|
"planned_qty": get_planned_qty(item_code, warehouse),
|
|
}
|
|
if only_bin:
|
|
qty_dict.update({"actual_qty": get_balance_qty_from_sle(item_code, warehouse)})
|
|
|
|
update_bin_qty(item_code, warehouse, qty_dict)
|
|
|
|
|
|
def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False):
|
|
create_repost_item_valuation_entry(
|
|
{
|
|
"item_code": item_code,
|
|
"warehouse": warehouse,
|
|
"posting_date": "1900-01-01",
|
|
"posting_time": "00:01",
|
|
"allow_negative_stock": allow_negative_stock,
|
|
"allow_zero_rate": allow_zero_rate,
|
|
}
|
|
)
|
|
|
|
|
|
def get_balance_qty_from_sle(item_code, warehouse):
|
|
balance_qty = frappe.db.sql(
|
|
"""select qty_after_transaction from `tabStock Ledger Entry`
|
|
where item_code=%s and warehouse=%s and is_cancelled=0
|
|
order by posting_date desc, posting_time desc, creation desc
|
|
limit 1""",
|
|
(item_code, warehouse),
|
|
)
|
|
|
|
return flt(balance_qty[0][0]) if balance_qty else 0.0
|
|
|
|
|
|
def get_reserved_qty(item_code, warehouse):
|
|
def append_open_so_query(q: QueryBuilder, child_table: Table) -> QueryBuilder:
|
|
return (
|
|
q.inner_join(SalesOrder)
|
|
.on(SalesOrder.name == child_table.parent)
|
|
.where(SalesOrder.docstatus == 1)
|
|
.where(SalesOrder.status != "Closed")
|
|
)
|
|
|
|
SalesOrder = DocType("Sales Order")
|
|
SalesOrderItem = DocType("Sales Order Item")
|
|
PackedItem = DocType("Packed Item")
|
|
|
|
dont_reserve_qty_on_sales_return = frappe.db.get_single_value(
|
|
"Selling Settings", "dont_reserve_sales_order_qty_on_sales_return"
|
|
)
|
|
|
|
tab = (
|
|
frappe.qb.from_(SalesOrderItem)
|
|
.where(SalesOrderItem.item_code == item_code)
|
|
.where(SalesOrderItem.warehouse == warehouse)
|
|
)
|
|
for field, cond in [
|
|
(SalesOrderItem.stock_qty.as_("dnpi_qty"), 1),
|
|
(SalesOrderItem.qty.as_("so_item_qty"), 1),
|
|
(SalesOrderItem.delivered_qty.as_("so_item_delivered_qty"), 1),
|
|
(SalesOrderItem.returned_qty.as_("so_item_returned_qty"), dont_reserve_qty_on_sales_return),
|
|
(SalesOrderItem.parent, 1),
|
|
(SalesOrderItem.name, 1),
|
|
]:
|
|
if cond:
|
|
tab = tab.select(field)
|
|
tab = append_open_so_query(tab, SalesOrderItem)
|
|
|
|
dnpi = (
|
|
frappe.qb.from_(PackedItem)
|
|
.select(PackedItem.qty, PackedItem.parent_detail_docname, PackedItem.parent, PackedItem.name)
|
|
.where(PackedItem.item_code == item_code)
|
|
.where(PackedItem.warehouse == warehouse)
|
|
)
|
|
dnpi = append_open_so_query(dnpi, PackedItem)
|
|
|
|
dnpi_parent = frappe.qb.from_(dnpi).select(dnpi.qty.as_("dnpi_qty"))
|
|
for key, so_item_field, cond in [
|
|
("so_item_qty", "qty", 1),
|
|
("so_item_delivered_qty", "delivered_qty", 1),
|
|
("so_item_returned_qty", "returned_qty", dont_reserve_qty_on_sales_return),
|
|
]:
|
|
if cond:
|
|
dnpi_parent = dnpi_parent.select(
|
|
(
|
|
frappe.qb.from_(SalesOrderItem)
|
|
.select(SalesOrderItem[so_item_field])
|
|
.where(SalesOrderItem.name == dnpi.parent_detail_docname)
|
|
.where(SalesOrderItem.delivered_by_supplier == 0)
|
|
).as_(key)
|
|
)
|
|
dnpi_parent = dnpi_parent.select(dnpi.parent, dnpi.name)
|
|
|
|
dnpi_parent = dnpi_parent + tab
|
|
|
|
q = (
|
|
frappe.qb.from_(dnpi_parent)
|
|
.select(
|
|
Sum(
|
|
dnpi_parent.dnpi_qty
|
|
* (
|
|
(
|
|
dnpi_parent.so_item_qty
|
|
- dnpi_parent.so_item_delivered_qty
|
|
- (dnpi_parent.so_item_returned_qty if dont_reserve_qty_on_sales_return else 0)
|
|
)
|
|
/ dnpi_parent.so_item_qty
|
|
)
|
|
)
|
|
)
|
|
.where(dnpi_parent.so_item_qty >= dnpi_parent.so_item_delivered_qty)
|
|
)
|
|
|
|
reserved_qty = q.run()
|
|
return flt(reserved_qty[0][0]) if reserved_qty else 0
|
|
|
|
|
|
def get_indented_qty(item_code, warehouse):
|
|
# Ordered Qty is always maintained in stock UOM
|
|
inward_qty = frappe.db.sql(
|
|
"""
|
|
select sum(mr_item.stock_qty - mr_item.ordered_qty)
|
|
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
|
|
where mr_item.item_code=%s and mr_item.warehouse=%s
|
|
and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer')
|
|
and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name
|
|
and mr.status!='Stopped' and mr.docstatus=1
|
|
""",
|
|
(item_code, warehouse),
|
|
)
|
|
inward_qty = flt(inward_qty[0][0]) if inward_qty else 0
|
|
|
|
outward_qty = frappe.db.sql(
|
|
"""
|
|
select sum(mr_item.stock_qty - mr_item.ordered_qty)
|
|
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
|
|
where mr_item.item_code=%s and mr_item.warehouse=%s
|
|
and mr.material_request_type = 'Material Issue'
|
|
and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name
|
|
and mr.status!='Stopped' and mr.docstatus=1
|
|
""",
|
|
(item_code, warehouse),
|
|
)
|
|
outward_qty = flt(outward_qty[0][0]) if outward_qty else 0
|
|
|
|
requested_qty = inward_qty - outward_qty
|
|
|
|
return requested_qty
|
|
|
|
|
|
def get_ordered_qty(item_code, warehouse):
|
|
ordered_qty = frappe.db.sql(
|
|
"""
|
|
select sum((po_item.qty - po_item.received_qty)*po_item.conversion_factor)
|
|
from `tabPurchase Order Item` po_item, `tabPurchase Order` po
|
|
where po_item.item_code=%s and po_item.warehouse=%s
|
|
and po_item.qty > po_item.received_qty and po_item.parent=po.name
|
|
and po.status not in ('Closed', 'Delivered') and po.docstatus=1
|
|
and po_item.delivered_by_supplier = 0""",
|
|
(item_code, warehouse),
|
|
)
|
|
|
|
return flt(ordered_qty[0][0]) if ordered_qty else 0
|
|
|
|
|
|
def get_planned_qty(item_code, warehouse):
|
|
planned_qty = frappe.db.sql(
|
|
"""
|
|
select sum(qty - produced_qty) from `tabWork Order`
|
|
where production_item = %s and fg_warehouse = %s and status not in ('Stopped', 'Completed', 'Closed')
|
|
and docstatus=1 and qty > produced_qty""",
|
|
(item_code, warehouse),
|
|
)
|
|
|
|
return flt(planned_qty[0][0]) if planned_qty else 0
|
|
|
|
|
|
def update_bin_qty(item_code, warehouse, qty_dict=None):
|
|
from erpnext.stock.utils import get_bin
|
|
|
|
bin = get_bin(item_code, warehouse)
|
|
mismatch = False
|
|
for field, value in qty_dict.items():
|
|
if flt(bin.get(field)) != flt(value):
|
|
bin.set(field, flt(value))
|
|
mismatch = True
|
|
|
|
bin.modified = now()
|
|
if mismatch:
|
|
bin.set_projected_qty()
|
|
bin.db_update()
|
|
bin.clear_cache()
|
|
|
|
|
|
def set_stock_balance_as_per_serial_no(
|
|
item_code=None, posting_date=None, posting_time=None, fiscal_year=None
|
|
):
|
|
if not posting_date:
|
|
posting_date = nowdate()
|
|
if not posting_time:
|
|
posting_time = nowtime()
|
|
|
|
condition = " and item.name='%s'" % item_code.replace("'", "'") if item_code else ""
|
|
|
|
bin = frappe.db.sql(
|
|
"""select bin.item_code, bin.warehouse, bin.actual_qty, item.stock_uom
|
|
from `tabBin` bin, tabItem item
|
|
where bin.item_code = item.name and item.has_serial_no = 1 %s"""
|
|
% condition
|
|
)
|
|
|
|
for d in bin:
|
|
serial_nos = frappe.db.sql(
|
|
"""select count(name) from `tabSerial No`
|
|
where item_code=%s and warehouse=%s and docstatus < 2""",
|
|
(d[0], d[1]),
|
|
)
|
|
|
|
sle = frappe.db.sql(
|
|
"""select valuation_rate, company from `tabStock Ledger Entry`
|
|
where item_code = %s and warehouse = %s and is_cancelled = 0
|
|
order by posting_date desc limit 1""",
|
|
(d[0], d[1]),
|
|
)
|
|
|
|
sle_dict = {
|
|
"doctype": "Stock Ledger Entry",
|
|
"item_code": d[0],
|
|
"warehouse": d[1],
|
|
"transaction_date": nowdate(),
|
|
"posting_date": posting_date,
|
|
"posting_time": posting_time,
|
|
"voucher_type": "Stock Reconciliation (Manual)",
|
|
"voucher_no": "",
|
|
"voucher_detail_no": "",
|
|
"actual_qty": flt(serial_nos[0][0]) - flt(d[2]),
|
|
"stock_uom": d[3],
|
|
"incoming_rate": sle and flt(serial_nos[0][0]) > flt(d[2]) and flt(sle[0][0]) or 0,
|
|
"company": sle and cstr(sle[0][1]) or 0,
|
|
"batch_no": "",
|
|
"serial_no": "",
|
|
}
|
|
|
|
sle_doc = frappe.get_doc(sle_dict)
|
|
sle_doc.flags.ignore_validate = True
|
|
sle_doc.flags.ignore_links = True
|
|
sle_doc.insert()
|
|
|
|
args = sle_dict.copy()
|
|
args.update({"sle_id": sle_doc.name})
|
|
|
|
create_repost_item_valuation_entry(
|
|
{
|
|
"item_code": d[0],
|
|
"warehouse": d[1],
|
|
"posting_date": posting_date,
|
|
"posting_time": posting_time,
|
|
}
|
|
)
|
|
|
|
|
|
def reset_serial_no_status_and_warehouse(serial_nos=None):
|
|
if not serial_nos:
|
|
serial_nos = frappe.db.sql_list("""select name from `tabSerial No` where docstatus = 0""")
|
|
for serial_no in serial_nos:
|
|
try:
|
|
sr = frappe.get_doc("Serial No", serial_no)
|
|
last_sle = sr.get_last_sle()
|
|
if flt(last_sle.actual_qty) > 0:
|
|
sr.warehouse = last_sle.warehouse
|
|
|
|
sr.via_stock_ledger = True
|
|
sr.save()
|
|
except Exception:
|
|
pass
|