[merge] item-variants

This commit is contained in:
Rushabh Mehta
2014-10-21 13:24:46 +05:30
70 changed files with 3923 additions and 3893 deletions

View File

@@ -11,27 +11,27 @@ class Bin(Document):
def validate(self):
if self.get("__islocal") or not self.stock_uom:
self.stock_uom = frappe.db.get_value('Item', self.item_code, 'stock_uom')
self.validate_mandatory()
self.projected_qty = flt(self.actual_qty) + flt(self.ordered_qty) + \
flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
def validate_mandatory(self):
qf = ['actual_qty', 'reserved_qty', 'ordered_qty', 'indented_qty']
for f in qf:
if (not getattr(self, f, None)) or (not self.get(f)):
if (not getattr(self, f, None)) or (not self.get(f)):
self.set(f, 0.0)
def update_stock(self, args):
self.update_qty(args)
if args.get("actual_qty"):
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after
if not args.get("posting_date"):
args["posting_date"] = nowdate()
# update valuation and qty after transaction for post dated entry
update_entries_after({
"item_code": self.item_code,
@@ -39,21 +39,34 @@ class Bin(Document):
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time")
})
def update_qty(self, args):
# update the stock values (for current quantities)
self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty"))
if args.get("voucher_type")=="Stock Reconciliation":
if args.get('is_cancelled') == 'No':
self.actual_qty = args.get("qty_after_transaction")
else:
qty_after_transaction = frappe.db.get_value("""select qty_after_transaction
from `tabStock Ledger Entry`
where item_code=%s and warehouse=%s
and not (voucher_type='Stock Reconciliation' and voucher_no=%s)
order by posting_date desc limit 1""",
(self.item_code, self.warehouse, args.get('voucher_no')))
self.actual_qty = flt(qty_after_transaction[0][0]) if qty_after_transaction else 0.0
else:
self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty"))
self.ordered_qty = flt(self.ordered_qty) + flt(args.get("ordered_qty"))
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
self.projected_qty = flt(self.actual_qty) + flt(self.ordered_qty) + \
flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
self.save()
def get_first_sle(self):
sle = frappe.db.sql("""
select * from `tabStock Ledger Entry`
@@ -62,4 +75,4 @@ class Bin(Document):
order by timestamp(posting_date, posting_time) asc, name asc
limit 1
""", (self.item_code, self.warehouse), as_dict=1)
return sle and sle[0] or None
return sle and sle[0] or None

View File

@@ -261,7 +261,7 @@ class DeliveryNote(SellingController):
sl_entries = []
for d in self.get_item_list():
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == "Yes" \
and d.warehouse:
and d.warehouse and flt(d['qty']):
self.update_reserved_qty(d)
sl_entries.append(self.get_sl_entries(d, {

View File

@@ -16,5 +16,11 @@
"item_code": "_Test Item 2",
"price_list": "_Test Price List Rest of the World",
"price_list_rate": 20
},
{
"doctype": "Item Price",
"item_code": "_Test Item Home Desktop 100",
"price_list": "_Test Price List",
"price_list_rate": 1000
}
]

View File

@@ -97,10 +97,10 @@ class LandedCostVoucher(Document):
# update stock & gl entries for cancelled state of PR
pr.docstatus = 2
pr.update_stock()
pr.update_stock_ledger()
pr.make_gl_entries_on_cancel()
# update stock & gl entries for submit state of PR
pr.docstatus = 1
pr.update_stock()
pr.update_stock_ledger()
pr.make_gl_entries()

View File

@@ -162,8 +162,7 @@ def item_details(doctype, txt, searchfield, start, page_len, filters):
from erpnext.controllers.queries import get_match_cond
return frappe.db.sql("""select name, item_name, description from `tabItem`
where name in ( select item_code FROM `tabDelivery Note Item`
where parent= %s
and ifnull(qty, 0) > ifnull(packed_qty, 0))
where parent= %s)
and %s like "%s" %s
limit %s, %s """ % ("%s", searchfield, "%s",
get_match_cond(doctype), "%s", "%s"),

View File

@@ -130,7 +130,7 @@ class PurchaseReceipt(BuyingController):
if not d.prevdoc_docname:
frappe.throw(_("Purchase Order number required for Item {0}").format(d.item_code))
def update_stock(self):
def update_stock_ledger(self):
sl_entries = []
stock_items = self.get_stock_items()
@@ -234,7 +234,7 @@ class PurchaseReceipt(BuyingController):
self.update_ordered_qty()
self.update_stock()
self.update_stock_ledger()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "purchase_receipt_details")
@@ -267,7 +267,7 @@ class PurchaseReceipt(BuyingController):
self.update_ordered_qty()
self.update_stock()
self.update_stock_ledger()
self.update_prevdoc_status()
pc_obj.update_last_purchase_rate(self, 0)

View File

@@ -95,7 +95,7 @@ class TestPurchaseReceipt(unittest.TestCase):
pr.insert()
self.assertEquals(len(pr.get("pr_raw_material_details")), 2)
self.assertEquals(pr.get("purchase_receipt_details")[0].rm_supp_cost, 70000.0)
self.assertEquals(pr.get("purchase_receipt_details")[0].rm_supp_cost, 20750.0)
def test_serial_no_supplier(self):
@@ -151,6 +151,6 @@ def set_perpetual_inventory(enable=1):
accounts_settings.save()
test_dependencies = ["BOM"]
test_dependencies = ["BOM", "Item Price"]
test_records = frappe.get_test_records('Purchase Receipt')

View File

@@ -9,6 +9,25 @@ from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
def get_sle(**args):
condition, values = "", []
for key, value in args.iteritems():
condition += " and " if condition else " where "
condition += "`{0}`=%s".format(key)
values.append(value)
return frappe.db.sql("""select * from `tabStock Ledger Entry` %s
order by timestamp(posting_date, posting_time) desc, name desc limit 1"""% condition,
values, as_dict=1)
def make_zero(item_code, warehouse):
sle = get_sle(item_code = item_code, warehouse = warehouse)
qty = sle[0].qty_after_transaction if sle else 0
if qty < 0:
make_stock_entry(item_code, None, warehouse, abs(qty), incoming_rate=10)
elif qty > 0:
make_stock_entry(item_code, warehouse, None, qty, incoming_rate=10)
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
@@ -16,6 +35,38 @@ class TestStockEntry(unittest.TestCase):
if hasattr(self, "old_default_company"):
frappe.db.set_default("company", self.old_default_company)
def test_fifo(self):
frappe.db.set_default("allow_negative_stock", 1)
item_code = "_Test Item 2"
warehouse = "_Test Warehouse - _TC"
make_zero(item_code, warehouse)
make_stock_entry(item_code, None, warehouse, 1, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[1, 10]], eval(sle.stock_queue))
# negative qty
make_zero(item_code, warehouse)
make_stock_entry(item_code, warehouse, None, 1, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[-1, 10]], eval(sle.stock_queue))
# further negative
make_stock_entry(item_code, warehouse, None, 1)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[-2, 10]], eval(sle.stock_queue))
# move stock to positive
make_stock_entry(item_code, None, warehouse, 3, incoming_rate=10)
sle = get_sle(item_code = item_code, warehouse = warehouse)[0]
self.assertEqual([[1, 10]], eval(sle.stock_queue))
frappe.db.set_default("allow_negative_stock", 0)
def test_auto_material_request(self):
self._test_auto_material_request("_Test Item")

View File

@@ -45,11 +45,14 @@ class StockLedgerEntry(Document):
formatdate(self.posting_date), self.posting_time))
def validate_mandatory(self):
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','actual_qty','company']
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
for k in mandatory:
if not self.get(k):
frappe.throw(_("{0} is required").format(self.meta.get_label(k)))
if self.voucher_type != "Stock Reconciliation" and not self.actual_qty:
frappe.throw(_("Actual Qty is mandatory"))
def validate_item(self):
item_det = frappe.db.sql("""select name, has_batch_no, docstatus,
is_stock_item, has_variants

View File

@@ -1,144 +1,145 @@
{
"allow_copy": 1,
"autoname": "SR/.######",
"creation": "2013-03-28 10:35:31",
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
"docstatus": 0,
"doctype": "DocType",
"allow_copy": 1,
"autoname": "SR/.######",
"creation": "2013-03-28 10:35:31",
"description": "This tool helps you to update or fix the quantity and valuation of stock in the system. It is typically used to synchronise the system values and what actually exists in your warehouses.",
"docstatus": 0,
"doctype": "DocType",
"fields": [
{
"fieldname": "posting_date",
"fieldtype": "Date",
"in_filter": 0,
"in_list_view": 1,
"label": "Posting Date",
"oldfieldname": "reconciliation_date",
"oldfieldtype": "Date",
"permlevel": 0,
"read_only": 0,
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_filter": 0,
"in_list_view": 1,
"label": "Posting Date",
"oldfieldname": "reconciliation_date",
"oldfieldtype": "Date",
"permlevel": 0,
"read_only": 0,
"reqd": 1
},
},
{
"fieldname": "posting_time",
"fieldtype": "Time",
"in_filter": 0,
"in_list_view": 1,
"label": "Posting Time",
"oldfieldname": "reconciliation_time",
"oldfieldtype": "Time",
"permlevel": 0,
"read_only": 0,
"fieldname": "posting_time",
"fieldtype": "Time",
"in_filter": 0,
"in_list_view": 1,
"label": "Posting Time",
"oldfieldname": "reconciliation_time",
"oldfieldtype": "Time",
"permlevel": 0,
"read_only": 0,
"reqd": 1
},
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Amended From",
"no_copy": 1,
"options": "Stock Reconciliation",
"permlevel": 0,
"print_hide": 1,
"fieldname": "amended_from",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Amended From",
"no_copy": 1,
"options": "Stock Reconciliation",
"permlevel": 0,
"print_hide": 1,
"read_only": 1
},
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"permlevel": 0,
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"permlevel": 0,
"reqd": 1
},
},
{
"fieldname": "fiscal_year",
"fieldtype": "Link",
"label": "Fiscal Year",
"options": "Fiscal Year",
"permlevel": 0,
"print_hide": 1,
"fieldname": "fiscal_year",
"fieldtype": "Link",
"label": "Fiscal Year",
"options": "Fiscal Year",
"permlevel": 0,
"print_hide": 1,
"reqd": 1
},
},
{
"depends_on": "eval:cint(sys_defaults.auto_accounting_for_stock)",
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Difference Account",
"options": "Account",
"depends_on": "eval:cint(sys_defaults.auto_accounting_for_stock)",
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Difference Account",
"options": "Account",
"permlevel": 0
},
},
{
"depends_on": "eval:cint(sys_defaults.auto_accounting_for_stock)",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"depends_on": "eval:cint(sys_defaults.auto_accounting_for_stock)",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"permlevel": 0
},
},
{
"fieldname": "col1",
"fieldtype": "Column Break",
"fieldname": "col1",
"fieldtype": "Column Break",
"permlevel": 0
},
},
{
"fieldname": "upload_html",
"fieldtype": "HTML",
"label": "Upload HTML",
"permlevel": 0,
"print_hide": 1,
"fieldname": "upload_html",
"fieldtype": "HTML",
"label": "Upload HTML",
"permlevel": 0,
"print_hide": 1,
"read_only": 1
},
},
{
"depends_on": "reconciliation_json",
"fieldname": "sb2",
"fieldtype": "Section Break",
"label": "Reconciliation Data",
"depends_on": "reconciliation_json",
"fieldname": "sb2",
"fieldtype": "Section Break",
"label": "Reconciliation Data",
"permlevel": 0
},
},
{
"fieldname": "reconciliation_html",
"fieldtype": "HTML",
"hidden": 0,
"label": "Reconciliation HTML",
"permlevel": 0,
"print_hide": 0,
"fieldname": "reconciliation_html",
"fieldtype": "HTML",
"hidden": 0,
"label": "Reconciliation HTML",
"permlevel": 0,
"print_hide": 0,
"read_only": 1
},
},
{
"fieldname": "reconciliation_json",
"fieldtype": "Long Text",
"hidden": 1,
"label": "Reconciliation JSON",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"fieldname": "reconciliation_json",
"fieldtype": "Long Text",
"hidden": 1,
"label": "Reconciliation JSON",
"no_copy": 1,
"permlevel": 0,
"print_hide": 1,
"read_only": 1
}
],
"icon": "icon-upload-alt",
"idx": 1,
"is_submittable": 1,
"max_attachments": 1,
"modified": "2014-10-08 12:47:52.102135",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
"owner": "Administrator",
],
"icon": "icon-upload-alt",
"idx": 1,
"is_submittable": 1,
"max_attachments": 1,
"modified": "2014-10-08 12:47:52.102135",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"permlevel": 0,
"read": 1,
"report": 1,
"role": "Material Manager",
"submit": 1,
"amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"permlevel": 0,
"read": 1,
"report": 1,
"role": "Material Manager",
"submit": 1,
"write": 1
}
],
"read_only_onload": 0,
"search_fields": "posting_date",
"sort_field": "modified",
],
"read_only_onload": 0,
"search_fields": "posting_date",
"sort_field": "modified",
"sort_order": "DESC"
}
}

View File

@@ -16,13 +16,11 @@ class StockReconciliation(StockController):
self.head_row = ["Item Code", "Warehouse", "Quantity", "Valuation Rate"]
def validate(self):
self.entries = []
self.validate_data()
self.validate_expense_account()
def on_submit(self):
self.insert_stock_ledger_entries()
self.update_stock_ledger()
self.make_gl_entries()
def on_cancel(self):
@@ -126,10 +124,9 @@ class StockReconciliation(StockController):
except Exception, e:
self.validation_messages.append(_("Row # ") + ("%d: " % (row_num)) + cstr(e))
def insert_stock_ledger_entries(self):
def update_stock_ledger(self):
""" find difference between current and expected entries
and create stock ledger entries based on the difference"""
from erpnext.stock.utils import get_valuation_method
from erpnext.stock.stock_ledger import get_previous_sle
row_template = ["item_code", "warehouse", "qty", "valuation_rate"]
@@ -141,105 +138,27 @@ class StockReconciliation(StockController):
for row_num, row in enumerate(data[data.index(self.head_row)+1:]):
row = frappe._dict(zip(row_template, row))
row["row_num"] = row_num
previous_sle = get_previous_sle({
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time
})
# check valuation rate mandatory
if row.qty not in ["", None] and not row.valuation_rate and \
flt(previous_sle.get("qty_after_transaction")) <= 0:
frappe.throw(_("Valuation Rate required for Item {0}").format(row.item_code))
if row.qty in ("", None) or row.valuation_rate in ("", None):
previous_sle = get_previous_sle({
"item_code": row.item_code,
"warehouse": row.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time
})
change_in_qty = row.qty not in ["", None] and \
(flt(row.qty) - flt(previous_sle.get("qty_after_transaction")))
if row.qty in ("", None):
row.qty = previous_sle.get("qty_after_transaction")
change_in_rate = row.valuation_rate not in ["", None] and \
(flt(row.valuation_rate) - flt(previous_sle.get("valuation_rate")))
if row.valuation_rate in ("", None):
row.valuation_rate = previous_sle.get("valuation_rate")
if get_valuation_method(row.item_code) == "Moving Average":
self.sle_for_moving_avg(row, previous_sle, change_in_qty, change_in_rate)
# if row.qty and not row.valuation_rate:
# frappe.throw(_("Valuation Rate required for Item {0}").format(row.item_code))
else:
self.sle_for_fifo(row, previous_sle, change_in_qty, change_in_rate)
self.insert_entries(row)
def sle_for_moving_avg(self, row, previous_sle, change_in_qty, change_in_rate):
"""Insert Stock Ledger Entries for Moving Average valuation"""
def _get_incoming_rate(qty, valuation_rate, previous_qty, previous_valuation_rate):
if previous_valuation_rate == 0:
return flt(valuation_rate)
else:
if valuation_rate in ["", None]:
valuation_rate = previous_valuation_rate
return (qty * valuation_rate - previous_qty * previous_valuation_rate) \
/ flt(qty - previous_qty)
if change_in_qty:
# if change in qty, irrespective of change in rate
incoming_rate = _get_incoming_rate(flt(row.qty), flt(row.valuation_rate),
flt(previous_sle.get("qty_after_transaction")), flt(previous_sle.get("valuation_rate")))
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Actual Entry"
self.insert_entries({"actual_qty": change_in_qty, "incoming_rate": incoming_rate}, row)
elif change_in_rate and flt(previous_sle.get("qty_after_transaction")) > 0:
# if no change in qty, but change in rate
# and positive actual stock before this reconciliation
incoming_rate = _get_incoming_rate(
flt(previous_sle.get("qty_after_transaction"))+1, flt(row.valuation_rate),
flt(previous_sle.get("qty_after_transaction")),
flt(previous_sle.get("valuation_rate")))
# +1 entry
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Valuation Adjustment +1"
self.insert_entries({"actual_qty": 1, "incoming_rate": incoming_rate}, row)
# -1 entry
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Valuation Adjustment -1"
self.insert_entries({"actual_qty": -1}, row)
def sle_for_fifo(self, row, previous_sle, change_in_qty, change_in_rate):
"""Insert Stock Ledger Entries for FIFO valuation"""
previous_stock_queue = json.loads(previous_sle.get("stock_queue") or "[]")
previous_stock_qty = sum((batch[0] for batch in previous_stock_queue))
previous_stock_value = sum((batch[0] * batch[1] for batch in \
previous_stock_queue))
def _insert_entries():
if previous_stock_queue != [[row.qty, row.valuation_rate]]:
# make entry as per attachment
if flt(row.qty):
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Actual Entry"
self.insert_entries({"actual_qty": row.qty,
"incoming_rate": flt(row.valuation_rate)}, row)
# Make reverse entry
if previous_stock_qty:
row["voucher_detail_no"] = "Row: " + cstr(row.row_num) + "/Reverse Entry"
self.insert_entries({"actual_qty": -1 * previous_stock_qty,
"incoming_rate": previous_stock_qty < 0 and
flt(row.valuation_rate) or 0}, row)
if change_in_qty:
if row.valuation_rate in ["", None]:
# dont want change in valuation
if previous_stock_qty > 0:
# set valuation_rate as previous valuation_rate
row.valuation_rate = previous_stock_value / flt(previous_stock_qty)
_insert_entries()
elif change_in_rate and previous_stock_qty > 0:
# if no change in qty, but change in rate
# and positive actual stock before this reconciliation
row.qty = previous_stock_qty
_insert_entries()
def insert_entries(self, opts, row):
def insert_entries(self, row):
"""Insert Stock Ledger Entries"""
args = frappe._dict({
"doctype": "Stock Ledger Entry",
@@ -251,16 +170,13 @@ class StockReconciliation(StockController):
"voucher_no": self.name,
"company": self.company,
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
"voucher_detail_no": row.voucher_detail_no,
"fiscal_year": self.fiscal_year,
"is_cancelled": "No"
"is_cancelled": "No",
"qty_after_transaction": row.qty,
"valuation_rate": row.valuation_rate
})
args.update(opts)
self.make_sl_entries([args])
# append to entries
self.entries.append(args)
def delete_and_repost_sle(self):
""" Delete Stock Ledger Entries related to this voucher
and repost future Stock Ledger Entries"""
@@ -295,7 +211,7 @@ class StockReconciliation(StockController):
if not self.expense_account:
msgprint(_("Please enter Expense Account"), raise_exception=1)
elif not frappe.db.sql("""select * from `tabStock Ledger Entry`"""):
elif not frappe.db.sql("""select name from `tabStock Ledger Entry` limit 1"""):
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
frappe.throw(_("Difference Account must be a 'Liability' type account, since this Stock Reconciliation is an Opening Entry"))

View File

@@ -28,7 +28,7 @@ class TestStockReconciliation(unittest.TestCase):
[20, "", "2012-12-26", "12:05", 16000, 15, 18000],
[10, 2000, "2012-12-26", "12:10", 20000, 5, 6000],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 13200],
[0, "", "2012-12-26", "12:10", 0, -5, 0]
[0, "", "2012-12-26", "12:10", 0, -5, -6000]
]
for d in input_data:
@@ -63,16 +63,16 @@ class TestStockReconciliation(unittest.TestCase):
input_data = [
[50, 1000, "2012-12-26", "12:00", 50000, 45, 48000],
[5, 1000, "2012-12-26", "12:00", 5000, 0, 0],
[15, 1000, "2012-12-26", "12:00", 15000, 10, 12000],
[15, 1000, "2012-12-26", "12:00", 15000, 10, 11500],
[25, 900, "2012-12-26", "12:00", 22500, 20, 22500],
[20, 500, "2012-12-26", "12:00", 10000, 15, 18000],
[50, 1000, "2013-01-01", "12:00", 50000, 65, 68000],
[5, 1000, "2013-01-01", "12:00", 5000, 20, 23000],
["", 1000, "2012-12-26", "12:05", 15000, 10, 12000],
["", 1000, "2012-12-26", "12:05", 15000, 10, 11500],
[20, "", "2012-12-26", "12:05", 18000, 15, 18000],
[10, 2000, "2012-12-26", "12:10", 20000, 5, 6000],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 13200],
[0, "", "2012-12-26", "12:10", 0, -5, 0]
[10, 2000, "2012-12-26", "12:10", 20000, 5, 7600],
[1, 1000, "2012-12-01", "00:00", 1000, 11, 12512.73],
[0, "", "2012-12-26", "12:10", 0, -5, -5142.86]
]

View File

@@ -6,18 +6,17 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import cint
from frappe.model.document import Document
class StockSettings(Document):
def validate(self):
for key in ["item_naming_by", "item_group", "stock_uom",
"allow_negative_stock"]:
for key in ["item_naming_by", "item_group", "stock_uom", "allow_negative_stock"]:
frappe.db.set_default(key, self.get(key, ""))
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
set_by_naming_series("Item", "item_code",
set_by_naming_series("Item", "item_code",
self.get("item_naming_by")=="Naming Series", hide_name_field=True)
stock_frozen_limit = 356
@@ -25,3 +24,5 @@ class StockSettings(Document):
if submitted_stock_frozen > stock_frozen_limit:
self.stock_frozen_upto_days = stock_frozen_limit
frappe.msgprint (_("`Freeze Stocks Older Than` should be smaller than %d days.") %stock_frozen_limit)

View File

@@ -1 +0,0 @@
Stock balances on a particular day, per warehouse.

View File

@@ -1,181 +0,0 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.require("assets/erpnext/js/stock_analytics.js");
frappe.pages['stock-balance'].onload = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: __('Stock Balance'),
single_column: true
});
new erpnext.StockBalance(wrapper);
wrapper.appframe.add_module_icon("Stock");
}
erpnext.StockBalance = erpnext.StockAnalytics.extend({
init: function(wrapper) {
this._super(wrapper, {
title: __("Stock Balance"),
doctypes: ["Item", "Item Group", "Warehouse", "Stock Ledger Entry", "Brand",
"Stock Entry", "Project", "Serial No"],
});
},
setup_columns: function() {
this.columns = [
{id: "name", name: __("Item"), field: "name", width: 300,
formatter: this.tree_formatter},
{id: "item_name", name: __("Item Name"), field: "item_name", width: 100},
{id: "description", name: __("Description"), field: "description", width: 200,
formatter: this.text_formatter},
{id: "brand", name: __("Brand"), field: "brand", width: 100},
{id: "stock_uom", name: __("UOM"), field: "stock_uom", width: 100},
{id: "opening_qty", name: __("Opening Qty"), field: "opening_qty", width: 100,
formatter: this.currency_formatter},
{id: "inflow_qty", name: __("In Qty"), field: "inflow_qty", width: 100,
formatter: this.currency_formatter},
{id: "outflow_qty", name: __("Out Qty"), field: "outflow_qty", width: 100,
formatter: this.currency_formatter},
{id: "closing_qty", name: __("Closing Qty"), field: "closing_qty", width: 100,
formatter: this.currency_formatter},
{id: "opening_value", name: __("Opening Value"), field: "opening_value", width: 100,
formatter: this.currency_formatter},
{id: "inflow_value", name: __("In Value"), field: "inflow_value", width: 100,
formatter: this.currency_formatter},
{id: "outflow_value", name: __("Out Value"), field: "outflow_value", width: 100,
formatter: this.currency_formatter},
{id: "closing_value", name: __("Closing Value"), field: "closing_value", width: 100,
formatter: this.currency_formatter},
{id: "valuation_rate", name: __("Valuation Rate"), field: "valuation_rate", width: 100,
formatter: this.currency_formatter},
];
},
filters: [
{fieldtype:"Select", label: __("Brand"), link:"Brand", fieldname: "brand",
default_value: __("Select Brand..."), filter: function(val, item, opts) {
return val == opts.default_value || item.brand == val || item._show;
}, link_formatter: {filter_input: "brand"}},
{fieldtype:"Select", label: __("Warehouse"), link:"Warehouse", fieldname: "warehouse",
default_value: __("Select Warehouse..."), filter: function(val, item, opts, me) {
return me.apply_zero_filter(val, item, opts, me);
}},
{fieldtype:"Select", label: __("Project"), link:"Project", fieldname: "project",
default_value: __("Select Project..."), filter: function(val, item, opts, me) {
return me.apply_zero_filter(val, item, opts, me);
}, link_formatter: {filter_input: "project"}},
{fieldtype:"Date", label: __("From Date"), fieldname: "from_date"},
{fieldtype:"Label", label: __("To")},
{fieldtype:"Date", label: __("To Date"), fieldname: "to_date"},
{fieldtype:"Button", label: __("Refresh"), icon:"icon-refresh icon-white"},
{fieldtype:"Button", label: __("Reset Filters"), icon: "icon-filter"}
],
setup_plot_check: function() {
return;
},
prepare_data: function() {
this.stock_entry_map = this.make_name_map(frappe.report_dump.data["Stock Entry"], "name");
this._super();
},
prepare_balances: function() {
var me = this;
var from_date = dateutil.str_to_obj(this.from_date);
var to_date = dateutil.str_to_obj(this.to_date);
var data = frappe.report_dump.data["Stock Ledger Entry"];
this.item_warehouse = {};
this.serialized_buying_rates = this.get_serialized_buying_rates();
for(var i=0, j=data.length; i<j; i++) {
var sl = data[i];
var sl_posting_date = dateutil.str_to_obj(sl.posting_date);
if((me.is_default("warehouse") ? true : me.warehouse == sl.warehouse) &&
(me.is_default("project") ? true : me.project == sl.project)) {
var item = me.item_by_name[sl.item_code];
var wh = me.get_item_warehouse(sl.warehouse, sl.item_code);
var valuation_method = item.valuation_method ?
item.valuation_method : sys_defaults.valuation_method;
var is_fifo = valuation_method == "FIFO";
var qty_diff = sl.qty;
var value_diff = me.get_value_diff(wh, sl, is_fifo);
if(sl_posting_date < from_date) {
item.opening_qty += qty_diff;
item.opening_value += value_diff;
} else if(sl_posting_date <= to_date) {
var ignore_inflow_outflow = this.is_default("warehouse")
&& sl.voucher_type=="Stock Entry"
&& this.stock_entry_map[sl.voucher_no].purpose=="Material Transfer";
if(!ignore_inflow_outflow) {
if(qty_diff < 0) {
item.outflow_qty += Math.abs(qty_diff);
} else {
item.inflow_qty += qty_diff;
}
if(value_diff < 0) {
item.outflow_value += Math.abs(value_diff);
} else {
item.inflow_value += value_diff;
}
item.closing_qty += qty_diff;
item.closing_value += value_diff;
}
} else {
break;
}
}
}
// opening + diff = closing
// adding opening, since diff already added to closing
$.each(me.item_by_name, function(key, item) {
item.closing_qty += item.opening_qty;
item.closing_value += item.opening_value;
// valuation rate
if(!item.is_group && flt(item.closing_qty) > 0)
item.valuation_rate = flt(item.closing_value) / flt(item.closing_qty);
else item.valuation_rate = 0.0
});
},
update_groups: function() {
var me = this;
$.each(this.data, function(i, item) {
// update groups
if(!item.is_group && me.apply_filter(item, "brand")) {
var parent = me.parent_map[item.name];
while(parent) {
parent_group = me.item_by_name[parent];
$.each(me.columns, function(c, col) {
if (col.formatter == me.currency_formatter && col.field != "valuation_rate") {
parent_group[col.field] = flt(parent_group[col.field]) + flt(item[col.field]);
}
});
// show parent if filtered by brand
if(item.brand == me.brand)
parent_group._show = true;
parent = me.parent_map[parent];
}
}
});
},
get_plot_data: function() {
return;
}
});

View File

@@ -1,23 +0,0 @@
{
"creation": "2012-12-27 18:57:47.000000",
"docstatus": 0,
"doctype": "Page",
"icon": "icon-table",
"idx": 1,
"modified": "2013-07-11 14:44:15.000000",
"modified_by": "Administrator",
"module": "Stock",
"name": "stock-balance",
"owner": "Administrator",
"page_name": "stock-balance",
"roles": [
{
"role": "Material Manager"
},
{
"role": "Analytics"
}
],
"standard": "Yes",
"title": "Stock Balance"
}

View File

@@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
from frappe.utils import flt, cint
def execute(filters=None):
if not filters: filters = {}
@@ -57,6 +57,7 @@ def get_stock_ledger_entries(filters):
conditions, as_dict=1)
def get_item_warehouse_batch_map(filters):
float_precision = cint(frappe.db.get_default("float_precision")) or 3
sle = get_stock_ledger_entries(filters)
iwb_map = {}
@@ -67,14 +68,14 @@ def get_item_warehouse_batch_map(filters):
}))
qty_dict = iwb_map[d.item_code][d.warehouse][d.batch_no]
if d.posting_date < filters["from_date"]:
qty_dict.opening_qty += flt(d.actual_qty)
qty_dict.opening_qty += flt(d.actual_qty, float_precision)
elif d.posting_date >= filters["from_date"] and d.posting_date <= filters["to_date"]:
if flt(d.actual_qty) > 0:
qty_dict.in_qty += flt(d.actual_qty)
qty_dict.in_qty += flt(d.actual_qty, float_precision)
else:
qty_dict.out_qty += abs(flt(d.actual_qty))
qty_dict.out_qty += abs(flt(d.actual_qty, float_precision))
qty_dict.bal_qty += flt(d.actual_qty)
qty_dict.bal_qty += flt(d.actual_qty, float_precision)
return iwb_map

View File

@@ -4,10 +4,10 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import date_diff
from frappe.utils import date_diff, flt
def execute(filters=None):
columns = get_columns()
item_details = get_fifo_queue(filters)
to_date = filters["to_date"]
@@ -16,35 +16,40 @@ def execute(filters=None):
fifo_queue = item_dict["fifo_queue"]
details = item_dict["details"]
if not fifo_queue: continue
average_age = get_average_age(fifo_queue, to_date)
earliest_age = date_diff(to_date, fifo_queue[0][1])
latest_age = date_diff(to_date, fifo_queue[-1][1])
data.append([item, details.item_name, details.description, details.item_group,
data.append([item, details.item_name, details.description, details.item_group,
details.brand, average_age, earliest_age, latest_age, details.stock_uom])
return columns, data
def get_average_age(fifo_queue, to_date):
batch_age = age_qty = total_qty = 0.0
for batch in fifo_queue:
batch_age = date_diff(to_date, batch[1])
age_qty += batch_age * batch[0]
total_qty += batch[0]
return (age_qty / total_qty) if total_qty else 0.0
def get_columns():
return [_("Item Code") + ":Link/Item:100", _("Item Name") + "::100", _("Description") + "::200",
_("Item Group") + ":Link/Item Group:100", _("Brand") + ":Link/Brand:100", _("Average Age") + ":Float:100",
return [_("Item Code") + ":Link/Item:100", _("Item Name") + "::100", _("Description") + "::200",
_("Item Group") + ":Link/Item Group:100", _("Brand") + ":Link/Brand:100", _("Average Age") + ":Float:100",
_("Earliest") + ":Int:80", _("Latest") + ":Int:80", _("UOM") + ":Link/UOM:100"]
def get_fifo_queue(filters):
item_details = {}
prev_qty = 0.0
for d in get_stock_ledger_entries(filters):
item_details.setdefault(d.name, {"details": d, "fifo_queue": []})
fifo_queue = item_details[d.name]["fifo_queue"]
if d.voucher_type == "Stock Reconciliation":
d.actual_qty = flt(d.qty_after_transaction) - flt(prev_qty)
if d.actual_qty > 0:
fifo_queue.append([d.actual_qty, d.posting_date])
else:
@@ -52,7 +57,7 @@ def get_fifo_queue(filters):
while qty_to_pop:
batch = fifo_queue[0] if fifo_queue else [0, None]
if 0 < batch[0] <= qty_to_pop:
# if batch qty > 0
# if batch qty > 0
# not enough or exactly same qty in current batch, clear batch
qty_to_pop -= batch[0]
fifo_queue.pop(0)
@@ -61,12 +66,14 @@ def get_fifo_queue(filters):
batch[0] -= qty_to_pop
qty_to_pop = 0
prev_qty = d.qty_after_transaction
return item_details
def get_stock_ledger_entries(filters):
return frappe.db.sql("""select
item.name, item.item_name, item_group, brand, description, item.stock_uom,
actual_qty, posting_date
return frappe.db.sql("""select
item.name, item.item_name, item_group, brand, description, item.stock_uom,
actual_qty, posting_date, voucher_type, qty_after_transaction
from `tabStock Ledger Entry` sle,
(select name, item_name, description, stock_uom, brand, item_group
from `tabItem` {item_conditions}) item
@@ -77,19 +84,19 @@ def get_stock_ledger_entries(filters):
order by posting_date, posting_time, sle.name"""\
.format(item_conditions=get_item_conditions(filters),
sle_conditions=get_sle_conditions(filters)), filters, as_dict=True)
def get_item_conditions(filters):
conditions = []
if filters.get("item_code"):
conditions.append("item_code=%(item_code)s")
if filters.get("brand"):
conditions.append("brand=%(brand)s")
return "where {}".format(" and ".join(conditions)) if conditions else ""
def get_sle_conditions(filters):
conditions = []
if filters.get("warehouse"):
conditions.append("warehouse=%(warehouse)s")
return "and {}".format(" and ".join(conditions)) if conditions else ""
return "and {}".format(" and ".join(conditions)) if conditions else ""

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
// Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors and contributors
// For license information, please see license.txt
frappe.query_reports["Warehouse-Wise Stock Balance"] = {
frappe.query_reports["Stock Balance"] = {
"filters": [
{
"fieldname":"from_date",
@@ -18,4 +18,4 @@ frappe.query_reports["Warehouse-Wise Stock Balance"] = {
"default": frappe.datetime.get_today()
}
]
}
}

View File

@@ -1,16 +1,17 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-06-05 11:00:31",
"creation": "2014-10-10 17:58:11.577901",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 1,
"is_standard": "Yes",
"modified": "2014-06-03 07:18:17.384923",
"modified": "2014-10-10 17:58:11.577901",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse-Wise Stock Balance",
"name": "Stock Balance",
"owner": "Administrator",
"ref_doctype": "Stock Ledger Entry",
"report_name": "Warehouse-Wise Stock Balance",
"report_name": "Stock Balance",
"report_type": "Script Report"
}

View File

@@ -58,10 +58,10 @@ def get_conditions(filters):
#get all details
def get_stock_ledger_entries(filters):
conditions = get_conditions(filters)
return frappe.db.sql("""select item_code, warehouse, posting_date,
actual_qty, valuation_rate, stock_uom, company
return frappe.db.sql("""select item_code, warehouse, posting_date, actual_qty, valuation_rate,
stock_uom, company, voucher_type, qty_after_transaction, stock_value_difference
from `tabStock Ledger Entry`
where docstatus < 2 %s order by item_code, warehouse""" %
where docstatus < 2 %s order by posting_date, posting_time, name""" %
conditions, as_dict=1)
def get_item_warehouse_map(filters):
@@ -80,21 +80,27 @@ def get_item_warehouse_map(filters):
qty_dict = iwb_map[d.company][d.item_code][d.warehouse]
qty_dict.uom = d.stock_uom
if d.voucher_type == "Stock Reconciliation":
qty_diff = flt(d.qty_after_transaction) - qty_dict.bal_qty
else:
qty_diff = flt(d.actual_qty)
value_diff = flt(d.stock_value_difference)
if d.posting_date < filters["from_date"]:
qty_dict.opening_qty += flt(d.actual_qty)
qty_dict.opening_val += flt(d.actual_qty) * flt(d.valuation_rate)
qty_dict.opening_qty += qty_diff
qty_dict.opening_val += value_diff
elif d.posting_date >= filters["from_date"] and d.posting_date <= filters["to_date"]:
qty_dict.val_rate = d.valuation_rate
if flt(d.actual_qty) > 0:
qty_dict.in_qty += flt(d.actual_qty)
qty_dict.in_val += flt(d.actual_qty) * flt(d.valuation_rate)
if qty_diff > 0:
qty_dict.in_qty += qty_diff
qty_dict.in_val += value_diff
else:
qty_dict.out_qty += abs(flt(d.actual_qty))
qty_dict.out_val += flt(abs(flt(d.actual_qty) * flt(d.valuation_rate)))
qty_dict.out_qty += abs(qty_diff)
qty_dict.out_val += abs(value_diff)
qty_dict.bal_qty += flt(d.actual_qty)
qty_dict.bal_val += flt(d.actual_qty) * flt(d.valuation_rate)
qty_dict.bal_qty += qty_diff
qty_dict.bal_val += value_diff
return iwb_map

View File

@@ -27,7 +27,7 @@ def make_sl_entries(sl_entries, is_amended=None):
if sle.get('is_cancelled') == 'Yes':
sle['actual_qty'] = -flt(sle['actual_qty'])
if sle.get("actual_qty"):
if sle.get("actual_qty") or sle.voucher_type=="Stock Reconciliation":
sle_id = make_entry(sle)
args = sle.copy()
@@ -36,9 +36,9 @@ def make_sl_entries(sl_entries, is_amended=None):
"is_amended": is_amended
})
update_bin(args)
if cancel:
delete_cancelled_entry(sl_entries[0].get('voucher_type'),
sl_entries[0].get('voucher_no'))
delete_cancelled_entry(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
def set_as_cancel(voucher_type, voucher_no):
frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled='Yes',
@@ -58,7 +58,7 @@ def delete_cancelled_entry(voucher_type, voucher_no):
frappe.db.sql("""delete from `tabStock Ledger Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
def update_entries_after(args, verbose=1):
def update_entries_after(args, allow_zero_rate=False, verbose=1):
"""
update valution rate and qty after transaction
from the current time-bucket onwards
@@ -83,7 +83,6 @@ def update_entries_after(args, verbose=1):
entries_to_fix = get_sle_after_datetime(previous_sle or \
{"item_code": args["item_code"], "warehouse": args["warehouse"]}, for_update=True)
valuation_method = get_valuation_method(args["item_code"])
stock_value_difference = 0.0
@@ -95,21 +94,30 @@ def update_entries_after(args, verbose=1):
qty_after_transaction += flt(sle.actual_qty)
continue
if sle.serial_no:
valuation_rate = get_serialized_values(qty_after_transaction, sle, valuation_rate)
elif valuation_method == "Moving Average":
valuation_rate = get_moving_average_values(qty_after_transaction, sle, valuation_rate)
else:
valuation_rate = get_fifo_values(qty_after_transaction, sle, stock_queue)
qty_after_transaction += flt(sle.actual_qty)
qty_after_transaction += flt(sle.actual_qty)
else:
if sle.voucher_type=="Stock Reconciliation":
valuation_rate = sle.valuation_rate
qty_after_transaction = sle.qty_after_transaction
stock_queue = [[qty_after_transaction, valuation_rate]]
else:
if valuation_method == "Moving Average":
valuation_rate = get_moving_average_values(qty_after_transaction, sle, valuation_rate, allow_zero_rate)
else:
valuation_rate = get_fifo_values(qty_after_transaction, sle, stock_queue, allow_zero_rate)
qty_after_transaction += flt(sle.actual_qty)
# get stock value
if sle.serial_no:
stock_value = qty_after_transaction * valuation_rate
elif valuation_method == "Moving Average":
stock_value = (qty_after_transaction > 0) and \
(qty_after_transaction * valuation_rate) or 0
stock_value = qty_after_transaction * valuation_rate
else:
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
@@ -243,69 +251,72 @@ def get_serialized_values(qty_after_transaction, sle, valuation_rate):
return valuation_rate
def get_moving_average_values(qty_after_transaction, sle, valuation_rate):
def get_moving_average_values(qty_after_transaction, sle, valuation_rate, allow_zero_rate):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if not incoming_rate:
# In case of delivery/stock issue in_rate = 0 or wrong incoming rate
incoming_rate = valuation_rate
if flt(sle.actual_qty) > 0:
if qty_after_transaction < 0 and not valuation_rate:
# if negative stock, take current valuation rate as incoming rate
valuation_rate = incoming_rate
elif qty_after_transaction < 0:
# if negative stock, take current valuation rate as incoming rate
valuation_rate = incoming_rate
new_stock_qty = abs(qty_after_transaction) + actual_qty
new_stock_value = (abs(qty_after_transaction) * valuation_rate) + (actual_qty * incoming_rate)
new_stock_qty = qty_after_transaction + actual_qty
new_stock_value = qty_after_transaction * valuation_rate + actual_qty * incoming_rate
if new_stock_qty:
valuation_rate = new_stock_value / flt(new_stock_qty)
elif not valuation_rate and qty_after_transaction <= 0:
valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, allow_zero_rate)
if new_stock_qty > 0 and new_stock_value > 0:
valuation_rate = new_stock_value / flt(new_stock_qty)
elif new_stock_qty <= 0:
valuation_rate = 0.0
return abs(flt(valuation_rate))
# NOTE: val_rate is same as previous entry if new stock value is negative
return valuation_rate
def get_fifo_values(qty_after_transaction, sle, stock_queue):
def get_fifo_values(qty_after_transaction, sle, stock_queue, allow_zero_rate):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
if not stock_queue:
stock_queue.append([0, 0])
if actual_qty > 0:
if not stock_queue:
stock_queue.append([0, 0])
if stock_queue[-1][0] > 0:
stock_queue.append([actual_qty, incoming_rate])
else:
qty = stock_queue[-1][0] + actual_qty
stock_queue[-1] = [qty, qty > 0 and incoming_rate or 0]
if qty == 0:
stock_queue.pop(-1)
else:
stock_queue[-1] = [qty, incoming_rate]
else:
incoming_cost = 0
qty_to_pop = abs(actual_qty)
while qty_to_pop:
if not stock_queue:
stock_queue.append([0, 0])
stock_queue.append([0, get_valuation_rate(sle.item_code, sle.warehouse, allow_zero_rate)
if qty_after_transaction <= 0 else 0])
batch = stock_queue[0]
if 0 < batch[0] <= qty_to_pop:
# if batch qty > 0
# not enough or exactly same qty in current batch, clear batch
incoming_cost += flt(batch[0]) * flt(batch[1])
qty_to_pop -= batch[0]
if qty_to_pop >= batch[0]:
# consume current batch
qty_to_pop = qty_to_pop - batch[0]
stock_queue.pop(0)
if not stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch
stock_queue.append([-qty_to_pop, batch[1]])
break
else:
# all from current batch
incoming_cost += flt(qty_to_pop) * flt(batch[1])
batch[0] -= qty_to_pop
# qty found in current batch
# consume it and exit
batch[0] = batch[0] - qty_to_pop
qty_to_pop = 0
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in stock_queue))
stock_qty = sum((flt(batch[0]) for batch in stock_queue))
valuation_rate = stock_qty and (stock_value / flt(stock_qty)) or 0
valuation_rate = (stock_value / flt(stock_qty)) if stock_qty else 0
return valuation_rate
return abs(valuation_rate)
def _raise_exceptions(args, verbose=1):
deficiency = min(e["diff"] for e in _exceptions)
@@ -337,3 +348,26 @@ def get_previous_sle(args, for_update=False):
"timestamp(posting_date, posting_time) <= timestamp(%(posting_date)s, %(posting_time)s)"],
"desc", "limit 1", for_update=for_update)
return sle and sle[0] or {}
def get_valuation_rate(item_code, warehouse, allow_zero_rate=False):
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
where item_code = %s and warehouse = %s
and ifnull(valuation_rate, 0) > 0
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse))
if not last_valuation_rate:
last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry`
where item_code = %s and ifnull(valuation_rate, 0) > 0
order by posting_date desc, posting_time desc, name desc limit 1""", item_code)
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
if not valuation_rate:
valuation_rate = frappe.db.get_value("Item Price", {"item_code": item_code, "buying": 1}, "price_list_rate")
if not allow_zero_rate and not valuation_rate and cint(frappe.db.get_value("Accounts Settings", None, "auto_accounting_for_stock")):
frappe.throw(_("Purchase rate for item: {0} not found, which is required to book accounting entry (expense). Please mention item price against a buying price list.").format(item_code))
return valuation_rate

View File

@@ -5,7 +5,6 @@ import frappe
from frappe import _
import json
from frappe.utils import flt, cstr, nowdate, nowtime
from frappe.defaults import get_global_default
class InvalidWarehouseCompany(frappe.ValidationError): pass
@@ -113,7 +112,7 @@ def get_valuation_method(item_code):
"""get valuation method from item or default"""
val_method = frappe.db.get_value('Item', item_code, 'valuation_method')
if not val_method:
val_method = get_global_default('valuation_method') or "FIFO"
val_method = frappe.db.get_value("Stock Settings", None, "valuation_method") or "FIFO"
return val_method
def get_fifo_rate(previous_stock_queue, qty):