brotherton-erpnext/erpnext/stock/stock_balance.py
Nabin Hait a77b8c9fcc
Repost item valuation (#24031)
* feat: Reposting logic for future finished/transferred item

* feat: added fields to identify needs to recalculate rate while reposting

* refactor: Set rate for outgoing and finished items

* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item

* refactor: Arranged fields in Stock Entry item table and added fields to identify finished and scrap item

* refactor: Get outgoing rate for purchase return

* refactor: Get incoming rate for sales return

* test: Added tests for reposting valuation of transferred/finished/returned items

* feat: added incoming rate field in DN, SI and Packed Item table

* feat: get incoming rate for returned item

* fix: no error while getting valuation rate in stock entry

* fix: update stock ledger for DN and SI

* feat: update item valuation rate in PR and PI based on supplied items cost

* feat: SLE reposting logic for sales return and subcontracted item with test cases

* feat: update qty in future sle

* feat: repost future sle and gle via Repost Item Valuation

* fix: Skip unwanted function calling while reposting

* fix: repost sle for specific item and warehouse

* test: Modified tests for backdated stock reco

* fix: ignore cancelled sle in few methods

* feat: role allowed to do backdated entry

* feat: Show reposting status on stock valuation related reports

* fix: minor fixes

* fix: fixed sider issues

* fix: serial no fix related to immutable ledger

* fix: Test cases fixes related to perpetual inventory

* fix: Test cases fixed

* fix: Fixed reposting on cancel and test cases

* feat: Restart reposting item valuation

* refactor: Code cleanup using small functions and test case fixes

* fix: minor fixes

* fix: Raise on error while reposting item valuation

* fix: minor fix

* fix: Tests fixed

* fix: skip some validation ig gle made from reposting

* fix: test fixes

* fix: debugging stock and account validation

* fix: debugging stock and account validation

* fix: debugging travis for stock and account sync validation

* fix: debugging travis

* fix: debugging travis

* fix: debugging travis
2020-12-21 14:45:50 +05:30

255 lines
8.9 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import print_function, unicode_literals
import frappe
from frappe.utils import flt, cstr, nowdate, nowtime
from erpnext.stock.utils import update_bin
from erpnext.stock.stock_ledger import update_entries_after
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:
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):
reserved_qty = frappe.db.sql("""
select
sum(dnpi_qty * ((so_item_qty - so_item_delivered_qty) / so_item_qty))
from
(
(select
qty as dnpi_qty,
(
select qty from `tabSales Order Item`
where name = dnpi.parent_detail_docname
and (delivered_by_supplier is null or delivered_by_supplier = 0)
) as so_item_qty,
(
select delivered_qty from `tabSales Order Item`
where name = dnpi.parent_detail_docname
and delivered_by_supplier = 0
) as so_item_delivered_qty,
parent, name
from
(
select qty, parent_detail_docname, parent, name
from `tabPacked Item` dnpi_in
where item_code = %s and warehouse = %s
and parenttype="Sales Order"
and item_code != parent_item
and exists (select * from `tabSales Order` so
where name = dnpi_in.parent and docstatus = 1 and status != 'Closed')
) dnpi)
union
(select stock_qty as dnpi_qty, qty as so_item_qty,
delivered_qty as so_item_delivered_qty, parent, name
from `tabSales Order Item` so_item
where item_code = %s and warehouse = %s
and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0)
and exists(select * from `tabSales Order` so
where so.name = so_item.parent and so.docstatus = 1
and so.status != 'Closed'))
) tab
where
so_item_qty >= so_item_delivered_qty
""", (item_code, warehouse, item_code, warehouse))
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")
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
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]))
if serial_nos and flt(serial_nos[0][0]) != flt(d[2]):
print(d[0], d[1], d[2], serial_nos[0][0])
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
})
update_bin(args)
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:
pass