[enhancement] added reserved_qty_for_production in bin and updated item dashboard

This commit is contained in:
Rushabh Mehta 2016-04-18 19:27:36 +05:30
parent c46c1f0288
commit 0525387328
25 changed files with 635 additions and 125 deletions

View File

@ -3,10 +3,13 @@
frappe.require("assets/erpnext/js/financial_statements.js");
frappe.query_reports["Profit and Loss Statement"] = erpnext.financial_statements;
frappe.query_reports["Profit and Loss Statement"] = $.extend({}, erpnext.financial_statements);
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
"fieldname": "accumulated_values",
"label": __("Accumulated Values"),
"fieldtype": "Check"
})
});
console.log(frappe.query_reports["Profit and Loss Statement"]);

View File

@ -72,15 +72,15 @@ class PurchaseCommon(BuyingController):
if items and len(items) != len(set(items)) and \
not cint(frappe.db.get_single_value("Buying Settings", "allow_multiple_items") or 0):
frappe.msgprint(_("Warning: Same item has been entered multiple times."))
frappe.msgprint(_("Warning: Same item has been entered multiple times."), small=True)
def check_for_closed_status(self, doctype, docname):
status = frappe.db.get_value(doctype, docname, "status")
if status == "Closed":
frappe.throw(_("{0} {1} status is {2}").format(doctype, docname, status), frappe.InvalidStatusError)
def check_docstatus(self, check, doctype, docname, detail_doctype = ''):
if check == 'Next':
submitted = frappe.db.sql("""select t1.name from `tab%s` t1,`tab%s` t2

View File

@ -7,7 +7,7 @@ frappe.ui.form.on("Supplier", {
frappe.setup_language_field(frm);
},
refresh: function(frm) {
frm.dashboard.show_links();
frm.dashboard.show_dashboard();
if(frappe.defaults.get_default("supp_master_name")!="Naming Series") {
frm.toggle_display("naming_series", false);

View File

@ -25,7 +25,7 @@ erpnext.hr.EmployeeController = frappe.ui.form.Controller.extend({
refresh: function() {
var me = this;
erpnext.toggle_naming_series();
this.frm.dashboard.show_links();
this.frm.dashboard.show_dashboard();
},
date_of_birth: function() {

View File

@ -0,0 +1,8 @@
import frappe
def set_required_items(production_order):
pass
def reserve_for_production(production_order):
'''Reserve pending raw materials for production'''
pass

View File

@ -78,7 +78,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "\nDraft\nSubmitted\nStopped\nIn Process\nCompleted\nCancelled",
"options": "\nDraft\nSubmitted\nNot Started\nStopped\nIn Process\nCompleted\nCancelled",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
@ -305,6 +305,33 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Warehouse for reserving items",
"fieldname": "source_warehouse",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Source Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -1024,6 +1051,32 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "required_items",
"fieldtype": "Table",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Required Items",
"length": 0,
"no_copy": 0,
"options": "Production Order Item",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
@ -1036,7 +1089,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-04-06 05:44:08.681263",
"modified": "2016-04-18 08:42:47.582203",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Order",
@ -1083,6 +1136,7 @@
"write": 0
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "ASC",

View File

@ -15,6 +15,8 @@ from erpnext.projects.doctype.time_log.time_log import OverlapError
from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
from erpnext.stock.utils import get_bin
class OverProductionError(frappe.ValidationError): pass
class StockOverProductionError(frappe.ValidationError): pass
@ -27,13 +29,6 @@ form_grid_templates = {
class ProductionOrder(Document):
def validate(self):
if self.docstatus == 0:
self.status = "Draft"
from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Draft", "Submitted", "Stopped",
"In Process", "Completed", "Cancelled"])
self.validate_production_item()
if self.bom_no:
validate_bom_no(self.production_item, self.bom_no)
@ -43,6 +38,7 @@ class ProductionOrder(Document):
self.calculate_operating_cost()
self.validate_qty()
self.validate_operation_time()
self.status = self.get_status()
from erpnext.utilities.transaction_base import validate_uom_is_integer
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
@ -65,7 +61,7 @@ class ProductionOrder(Document):
def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company
for w in [self.fg_warehouse, self.wip_warehouse]:
for w in [self.source_warehouse, self.fg_warehouse, self.wip_warehouse]:
validate_warehouse_company(w, self.company)
def calculate_operating_cost(self):
@ -113,23 +109,36 @@ class ProductionOrder(Document):
def update_status(self, status=None):
'''Update status of production order'''
status = self.get_status()
if status != self.status:
self.db_set("status", status)
self.update_required_items()
def get_status(self, status=None):
'''Return the status based on stock entries against this production order'''
if not status:
status = self.status
if status != 'Stopped':
stock_entries = frappe._dict(frappe.db.sql("""select purpose, sum(fg_completed_qty)
from `tabStock Entry` where production_order=%s and docstatus=1
group by purpose""", self.name))
if self.docstatus==0:
status = 'Draft'
elif self.docstatus==1:
if status != 'Stopped':
stock_entries = frappe._dict(frappe.db.sql("""select purpose, sum(fg_completed_qty)
from `tabStock Entry` where production_order=%s and docstatus=1
group by purpose""", self.name))
status = "Submitted"
if stock_entries:
status = "In Process"
produced_qty = stock_entries.get("Manufacture")
if flt(produced_qty) == flt(self.qty):
status = "Completed"
status = "Not Started"
if stock_entries:
status = "In Process"
produced_qty = stock_entries.get("Manufacture")
if flt(produced_qty) == flt(self.qty):
status = "Completed"
else:
status = 'Cancelled'
if status != self.status:
self.db_set("status", status)
return status
def update_production_order_qty(self):
"""Update **Manufactured Qty** and **Material Transferred for Qty** in Production Order
@ -147,13 +156,16 @@ class ProductionOrder(Document):
self.db_set(fieldname, qty)
def before_submit(self):
self.set_required_items()
def on_submit(self):
if not self.wip_warehouse:
frappe.throw(_("Work-in-Progress Warehouse is required before Submit"))
if not self.fg_warehouse:
frappe.throw(_("For Warehouse is required before Submit"))
frappe.db.set(self,'status', 'Submitted')
self.update_reserved_qty_for_production()
self.make_time_logs()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
@ -162,6 +174,7 @@ class ProductionOrder(Document):
self.validate_cancel()
frappe.db.set(self,'status', 'Cancelled')
self.clear_required_items()
self.delete_time_logs()
self.update_completed_qty_in_material_request()
self.update_planned_qty()
@ -342,6 +355,74 @@ class ProductionOrder(Document):
if not d.time_in_mins > 0:
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}".format(d.operation)))
def update_required_items(self):
'''
update bin reserved_qty_for_production
called from Stock Entry for production, after submit, cancel
'''
if self.docstatus==1 and self.source_warehouse:
if self.material_transferred_for_manufacturing == self.produced_qty:
# clear required items table and save document
self.clear_required_items()
else:
# calculate transferred qty based on submitted
# stock entries
self.update_transaferred_qty_for_required_items()
# update in bin
self.update_reserved_qty_for_production()
def clear_required_items(self):
'''Remove the required_items table and update the bins'''
items = [d.item_code for d in self.required_items]
self.required_items = []
self.update_child_table('required_items')
# completed, update reserved qty in bin
self.update_reserved_qty_for_production(items)
def update_reserved_qty_for_production(self, items=None):
'''update reserved_qty_for_production in bins'''
if not self.source_warehouse:
return
if not items:
items = [d.item_code for d in self.required_items]
for item in items:
stock_bin = get_bin(item, self.source_warehouse)
stock_bin.update_reserved_qty_for_production()
def set_required_items(self):
'''set required_items for production to keep track of reserved qty'''
if self.source_warehouse:
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=self.qty,
fetch_exploded = self.use_multi_level_bom)
for item in item_dict.values():
self.append('required_items', {'item_code': item.item_code,
'required_qty': item.qty})
#print frappe.as_json(self.required_items)
def update_transaferred_qty_for_required_items(self):
'''update transferred qty from submitted stock entries for that item against
the production order'''
for d in self.required_items:
transferred_qty = frappe.db.sql('''select count(qty)
from `tabStock Entry` entry, `tabStock Entry Detail` detail
where
entry.production_order = %s
entry.purpose = "Material Transfer for Manufacture"
and entry.docstatus = 1
and detail.parent = entry.name
and detail.item_code = %s''', (self.name, d.item_code))[0][0]
d.db_set('transferred_qty', transferred_qty, update_modified = False)
@frappe.whitelist()
def get_item_details(item):
res = frappe.db.sql("""select stock_uom, description
@ -372,6 +453,8 @@ def make_stock_entry(production_order_id, purpose, qty=None):
stock_entry.fg_completed_qty = qty or (flt(production_order.qty) - flt(production_order.produced_qty))
if purpose=="Material Transfer for Manufacture":
if production_order.source_warehouse:
stock_entry.from_warehouse = production_order.source_warehouse
stock_entry.to_warehouse = production_order.wip_warehouse
else:
stock_entry.from_warehouse = production_order.wip_warehouse

View File

@ -5,14 +5,19 @@
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_days
from frappe.utils import flt, time_diff_in_hours, now, add_days, cint
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.production_order.production_order \
import make_stock_entry, ItemHasVariantError
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.projects.doctype.time_log.time_log import OverProductionLoggedError
from erpnext.stock.utils import get_bin
class TestProductionOrder(unittest.TestCase):
def setUp(self):
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
def check_planned_qty(self):
set_perpetual_inventory(0)
@ -140,6 +145,73 @@ class TestProductionOrder(unittest.TestCase):
prod_order = make_prod_order_test_record(item="_Test Variant Item", qty=1, do_not_save=True)
self.assertRaises(ItemHasVariantError, prod_order.save)
def test_reserved_qty_for_production_submit(self):
self.bin1_at_start = get_bin(self.item, self.warehouse)
# reset to correct value
self.bin1_at_start.update_reserved_qty_for_production()
self.pro_order = make_prod_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=self.warehouse)
self.bin1_on_submit = get_bin(self.item, self.warehouse)
# reserved qty for production is updated
self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production) + 2,
cint(self.bin1_on_submit.reserved_qty_for_production))
self.assertEqual(cint(self.bin1_at_start.projected_qty),
cint(self.bin1_on_submit.projected_qty) + 2)
def test_reserved_qty_for_production_cancel(self):
self.test_reserved_qty_for_production_submit()
self.pro_order.cancel()
bin1_on_cancel = get_bin(self.item, self.warehouse)
# reserved_qty_for_producion updated
self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production),
cint(bin1_on_cancel.reserved_qty_for_production))
self.assertEqual(self.bin1_at_start.projected_qty,
cint(bin1_on_cancel.projected_qty))
def test_reserved_qty_for_production_on_stock_entry(self):
test_stock_entry.make_stock_entry(item_code="_Test Item",
target= self.warehouse, qty=100, basic_rate=100)
test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target= self.warehouse, qty=100, basic_rate=100)
self.test_reserved_qty_for_production_submit()
s = frappe.get_doc(make_stock_entry(self.pro_order.name,
"Material Transfer for Manufacture", 2))
s.submit()
bin1_on_start_production = get_bin(self.item, self.warehouse)
# reserved_qty_for_producion updated
self.assertEqual(cint(self.bin1_at_start.reserved_qty_for_production),
cint(bin1_on_start_production.reserved_qty_for_production))
# projected qty will now be 2 less (becuase of item movement)
self.assertEqual(cint(self.bin1_at_start.projected_qty),
cint(bin1_on_start_production.projected_qty) + 2)
s = frappe.get_doc(make_stock_entry(self.pro_order.name, "Manufacture", 2))
bin1_on_end_production = get_bin(self.item, self.warehouse)
# no change in reserved / projected
self.assertEqual(cint(bin1_on_end_production.reserved_qty_for_production),
cint(bin1_on_start_production.reserved_qty_for_production))
self.assertEqual(cint(bin1_on_end_production.projected_qty),
cint(bin1_on_end_production.projected_qty))
# required_items removed
self.pro_order.reload()
self.assertEqual(len(self.pro_order.required_items), 0)
def make_prod_order_test_record(**args):
args = frappe._dict(args)
@ -152,6 +224,10 @@ def make_prod_order_test_record(**args):
pro_order.fg_warehouse = args.fg_warehouse or "_Test Warehouse 1 - _TC"
pro_order.company = args.company or "_Test Company"
pro_order.stock_uom = "_Test UOM"
if args.source_warehouse:
pro_order.source_warehouse = args.source_warehouse
if args.planned_start_date:
pro_order.planned_start_date = args.planned_start_date

View File

@ -0,0 +1,110 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"creation": "2016-04-18 07:38:26.314642",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "item_code",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"options": "Item",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "required_qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Required Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "transferred_qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Transferred Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-04-18 07:38:26.314642",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Order Item",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class ProductionOrderItem(Document):
pass

View File

@ -1,9 +1,21 @@
import frappe
from erpnext.accounts.party_status import update_status
from erpnext.accounts.party_status import status_depends_on, default_status
from frappe.desk.notifications import get_filters_for
def execute():
for doctype in ('Customer', 'Supplier'):
frappe.reload_doctype(doctype)
for doc in frappe.get_all(doctype):
doc = frappe.get_doc(doctype, doc.name)
update_status(doc)
for party_type in ('Customer', 'Supplier'):
frappe.reload_doctype(party_type)
# set all as default status
frappe.db.sql('update `tab{0}` set status=%s'.format(party_type), default_status[party_type])
for doctype in status_depends_on[party_type]:
filters = get_filters_for(doctype)
parties = frappe.get_all(doctype, fields="{0} as party".format(party_type.lower()),
filters=filters, limit_page_length=1)
parties = filter(None, [p.party for p in parties])
if parties:
frappe.db.sql('update `tab{0}` set status="Open" where name in ({1})'.format(party_type,
', '.join(len(parties) * ['%s'])), parties)

View File

@ -10,6 +10,32 @@
"doctype": "DocType",
"document_type": "Setup",
"fields": [
{
"allow_on_submit": 1,
"bold": 1,
"collapsible": 0,
"depends_on": "__islocal",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Title",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 1,
@ -896,31 +922,6 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Title",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
@ -933,7 +934,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-03-29 15:55:12.780956",
"modified": "2016-04-15 07:51:03.097280",
"modified_by": "Administrator",
"module": "Projects",
"name": "Time Log",
@ -953,8 +954,6 @@
"print": 1,
"read": 1,
"report": 1,
"restrict": 0,
"restricted": 1,
"role": "Projects User",
"set_user_permissions": 0,
"share": 1,
@ -975,8 +974,6 @@
"print": 1,
"read": 1,
"report": 1,
"restrict": 0,
"restricted": 0,
"role": "Projects Manager",
"set_user_permissions": 0,
"share": 1,
@ -984,6 +981,7 @@
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "ASC",

View File

@ -68,5 +68,20 @@ erpnext.financial_statements = {
"tree": true,
"name_field": "account",
"parent_field": "parent_account",
"initial_depth": 3
"initial_depth": 3,
onload: function(report) {
// dropdown for links to other financial statements
report.page.add_inner_button(__("Balance Sheet"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
}, 'Financial Statements');
report.page.add_inner_button(__("Profit and Loss"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
}, 'Financial Statements');
report.page.add_inner_button(__("Cash Flow Statement"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
}, 'Financial Statements');
},
};

View File

@ -7,7 +7,7 @@ frappe.ui.form.on("Customer", {
frappe.setup_language_field(frm);
},
refresh: function(frm) {
frm.dashboard.show_links();
frm.dashboard.show_dashboard();
if(frappe.defaults.get_default("cust_master_name")!="Naming Series") {
frm.toggle_display("naming_series", false);

View File

@ -0,0 +1,8 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Bin', {
refresh: function(frm) {
}
});

View File

@ -16,6 +16,7 @@
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 1,
"label": "Warehouse",
@ -42,6 +43,7 @@
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 1,
"label": "Item Code",
@ -69,6 +71,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Reserved Quantity",
@ -95,6 +98,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 1,
"label": "Actual Quantity",
@ -121,6 +125,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Ordered Quantity",
@ -147,6 +152,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Requested Quantity",
@ -172,6 +178,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Planned Qty",
@ -197,6 +204,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Projected Qty",
@ -214,6 +222,31 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "reserved_qty_for_production",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Reserved Qty for Production",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -222,6 +255,7 @@
"fieldtype": "Float",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Moving Average Rate",
@ -247,6 +281,7 @@
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 0,
"label": "UOM",
@ -273,6 +308,7 @@
"fieldtype": "Float",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "FCFS Rate",
@ -298,6 +334,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Valuation Rate",
@ -323,6 +360,7 @@
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Stock Value",
@ -350,7 +388,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-02-10 02:39:45.738623",
"modified": "2016-04-18 08:12:57.341517",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
@ -417,8 +455,10 @@
"write": 0
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"search_fields": "item_code,warehouse",
"sort_order": "ASC"
"sort_order": "ASC",
"track_seen": 0
}

View File

@ -65,12 +65,15 @@ class Bin(Document):
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.set_projected_qty()
self.save()
update_item_projected_qty(self.item_code)
def set_projected_qty(self):
self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty)
+ flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
- flt(self.reserved_qty_for_production))
def get_first_sle(self):
sle = frappe.db.sql("""
select * from `tabStock Ledger Entry`
@ -81,8 +84,25 @@ class Bin(Document):
""", (self.item_code, self.warehouse), as_dict=1)
return sle and sle[0] or None
def update_reserved_qty_for_production(self):
'''Update qty reserved for production from Production Item tables
in open production orders'''
self.reserved_qty_for_production = frappe.db.sql('''select sum(required_qty - transferred_qty)
from `tabProduction Order` pro, `tabProduction Order Item` item
where
item.item_code = %s
and item.parent = pro.name
and pro.docstatus = 1
and pro.source_warehouse = %s''', (self.item_code, self.warehouse))[0][0]
self.set_projected_qty()
self.db_set('reserved_qty_for_production', self.reserved_qty_for_production)
self.db_set('projected_qty', self.projected_qty)
def update_item_projected_qty(item_code):
'''Set Item project qty'''
'''Set total_projected_qty in Item as sum of projected qty in all warehouses'''
frappe.db.sql('''update tabItem set
total_projected_qty = ifnull((select sum(projected_qty) from tabBin where item_code=%s), 0)
where name=%s''', (item_code, item_code))

View File

@ -16,12 +16,6 @@ frappe.ui.form.on("Item", {
},
dashboard_update: function(frm) {
if(frm.dashboard_data.stock_data && frm.dashboard_data.stock_data.length) {
frm.dashboard.add_stats(frappe.render_template('item_dashboard', {data: frm.dashboard_data.stock_data}))
}
},
refresh: function(frm) {
if(frm.doc.is_stock_item) {
@ -82,7 +76,25 @@ frappe.ui.form.on("Item", {
erpnext.item.toggle_attributes(frm);
frm.dashboard.show_links();
frm.dashboard.show_dashboard();
},
dashboard_update: function(frm) {
if(frm.dashboard_data.stock_data && frm.dashboard_data.stock_data.length) {
var max_count = 0;
frm.dashboard_data.stock_data.forEach(function(d) {
d.actual_or_pending = d.projected_qty - d.reserved_qty;
d.pending_qty = 0;
if(d.actual_or_pending > d.actual_qty) {
d.pending_qty = d.actual_or_pending - d.actual_qty;
}
max_count = Math.max(d.actual_or_pending, d.actual_qty,
d.reserved_qty, max_count);
})
frm.dashboard.add_stats(frappe.render_template('item_dashboard',
{data: frm.dashboard_data.stock_data, max_count: max_count}));
}
},
validate: function(frm){

View File

@ -633,8 +633,8 @@ def get_timeline_data(name):
group by posting_date''', name))
def get_stock_data(name):
return frappe.get_all('Bin', fields=['warehouse', 'actual_qty', 'projected_qty'],
filters={'item_code': name})
return frappe.get_all('Bin', fields=['warehouse', 'actual_qty', 'projected_qty', 'reserved_qty'],
filters={'item_code': name}, order_by = 'warehouse asc')
def validate_end_of_life(item_code, end_of_life=None, disabled=None, verbose=1):
if (not end_of_life) or (disabled is None):

View File

@ -1,12 +1,39 @@
<div style="padding-left: 15px;">
<div style="padding-left: 15px; padding-right: 15px;">
<h5>Stock Levels</h5>
<div class="row">
<div class="col-md-6 col-xs-12">
<ul class="list-unstyled">
{% data.every(function(d) { %}
<li class="small">{{ d.warehouse }}: {{ d.actual_qty }} ({{ d.projected_qty }})</li>
{% }) %}
</ul>
</div>
</div>
<ul class="list-group">
{% for(var i=0; i < data.length; i++) { var d = data[i]; %}
<li class="list-group-item" style="background-color: inherit;">
<div class="row">
<div class="col-sm-8 small" style="margin-top: 8px;">{{ d.warehouse }}</div>
<div class="col-sm-4 small">
<span class="inline-graph">
<span class="inline-graph-half" title="{{ __("Reserved Qty") }}">
<span class="inline-graph-count">{{ d.reserved_qty }}</span>
<span class="inline-graph-bar">
<span class="inline-graph-bar-inner"
style="width: {{ cint(Math.abs(d.reserved_qty)/max_count * 100) || 5 }}%">
</span>
</span>
</span>
<span class="inline-graph-half" title="{{ __("Acutal Qty {0} / Waiting Qty {1}", [d.actual_qty, d.pending_qty]) }}">
<span class="inline-graph-count">
{{ d.actual_qty }} {{ (d.pending_qty > 0) ? ("(" + d.pending_qty+ ")") : "" }}
</span>
<span class="inline-graph-bar">
<span class="inline-graph-bar-inner dark"
style="width: {{ cint(d.actual_qty/max_count * 100) }}%">
</span>
{% if(d.pending_qty > 0) { %}
<span class="inline-graph-bar-inner" title="{{ __("Projected Qty") }}"
style="width: {{ cint(d.pending_qty/max_count * 100) }}%">
</span>
{% } %}
</span>
</span>
</span>
</div>
</div>
</li>
{% } %}
</ul>
</div>

View File

@ -11,6 +11,12 @@
"doctype": "Warehouse",
"warehouse_name": "_Test Warehouse 1"
},
{
"company": "_Test Company",
"create_account_under": "Fixed Assets - _TC",
"doctype": "Warehouse",
"warehouse_name": "_Test Warehouse 2"
},
{
"company": "_Test Company",
"create_account_under": "Stock Assets - _TC",

View File

@ -87,6 +87,54 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_4",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -117,32 +165,8 @@
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "disabled",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Disabled",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "For Reference Only.",
"collapsible": 1,
"description": "",
"fieldname": "warehouse_contact_info",
"fieldtype": "Section Break",
"hidden": 0,
@ -408,7 +432,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-03-30 03:31:43.954827",
"modified": "2016-04-18 05:44:24.837579",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
@ -535,6 +559,7 @@
"write": 0
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_order": "DESC",

View File

@ -14,7 +14,8 @@ def get_columns():
return [_("Item Code") + ":Link/Item:140", _("Item Name") + "::100", _("Description") + "::200",
_("Item Group") + ":Link/Item Group:100", _("Brand") + ":Link/Brand:100", _("Warehouse") + ":Link/Warehouse:120",
_("UOM") + ":Link/UOM:100", _("Actual Qty") + ":Float:100", _("Planned Qty") + ":Float:100",
_("Requested Qty") + ":Float:110", _("Ordered Qty") + ":Float:100", _("Reserved Qty") + ":Float:100",
_("Requested Qty") + ":Float:110", _("Ordered Qty") + ":Float:100",
_("Reserved Qty") + ":Float:100", _("Reserved Qty for Production") + ":Float:100",
_("Projected Qty") + ":Float:100", _("Reorder Level") + ":Float:100", _("Reorder Qty") + ":Float:100",
_("Shortage Qty") + ":Float:100"]
@ -51,7 +52,7 @@ def get_data(filters):
data.append([item.name, item.item_name, item.description, item.item_group, item.brand, bin.warehouse,
item.stock_uom, bin.actual_qty, bin.planned_qty, bin.indented_qty, bin.ordered_qty,
bin.reserved_qty, bin.projected_qty, re_order_level, re_order_qty, shortage_qty])
bin.reserved_qty, bin.reserved_qty_for_production, bin.projected_qty, re_order_level, re_order_qty, shortage_qty])
return data
@ -63,7 +64,8 @@ def get_bin_list(filters):
bin_filters.warehouse = filters.warehouse
bin_list = frappe.get_all("Bin", fields=["item_code", "warehouse",
"actual_qty", "planned_qty", "indented_qty", "ordered_qty", "reserved_qty", "projected_qty"],
"actual_qty", "planned_qty", "indented_qty", "ordered_qty", "reserved_qty",
"reserved_qty_for_production", "projected_qty"],
filters=bin_filters, order_by="item_code, warehouse")
return bin_list

View File

@ -148,8 +148,9 @@ def update_bin_qty(item_code, warehouse, qty_dict=None):
mismatch = True
if mismatch:
bin.projected_qty = flt(bin.actual_qty) + flt(bin.ordered_qty) + \
bin.projected_qty = (flt(bin.actual_qty) + flt(bin.ordered_qty) +
flt(bin.indented_qty) + flt(bin.planned_qty) - flt(bin.reserved_qty)
- flt(bin.reserved_qty_for_production))
bin.save()