8 Commits

Author SHA1 Message Date
b20411fce1 Fixed caching issues 2023-09-08 07:44:22 +00:00
b14eeff0b3 Fixed calculation Error 2023-09-08 07:16:33 +00:00
a487804c7c Removed logger 2023-09-08 05:14:21 +00:00
ea83e3e1ac Redone status generation and caching improvements 2023-09-07 06:29:01 +00:00
3cdcb239c3 Rework 2023-09-06 13:45:04 +00:00
CAnetzberger
f98905f6e6 Small fixes:wq 2023-03-06 16:58:19 +01:00
Christian Anetzberger
e62195815f Update for version-14 2022-11-10 14:49:14 +01:00
Christian Anetzberger
0619cae23c Quick fixes for Version 14 2022-11-09 08:02:29 +01:00
12 changed files with 208 additions and 137 deletions

19
.gitignore vendored
View File

@@ -1,6 +1,19 @@
.DS_Store
*.pyc *.pyc
*.py~
.DS_Store
conf.py
locale
latest_updates.json
.wnf-lang-status
*.egg-info *.egg-info
*.swp dist/
tags manufacturing_overview/public/dist
manufacturing_overview/docs/current manufacturing_overview/docs/current
*.swp
*.swo
__pycache__
*~
.idea/
.vscode/
node_modules/
.backportrc.json

View File

@@ -1,3 +1,2 @@
__version__ = '0.0.1' __version__ = '14.38.0'

View File

@@ -1,20 +1,21 @@
from . import __version__ as app_version from frappe import _
app_name = "manufacturing_overview" app_name = "manufacturing_overview"
app_title = "Manufacturing Overview" app_title = "Manufacturing Overview"
app_publisher = "CAnetzberger Design" app_publisher = "CAnetzberger Design"
app_description = "Dashboard Overview for Sales Orders Items to be manufactured" app_description = "Dashboard Overview for Sales Orders Items to be manufactured"
app_icon = "octicon octicon-file-directory" app_icon = "fa fa-th"
app_color = "grey" app_color = "grey"
app_email = "admin@canetzberger.design" app_email = "admin@canetzberger.design"
app_license = "MIT" app_license = "MIT"
required_apps = ["erpnext"]
# Includes in <head> # Includes in <head>
# ------------------ # ------------------
# include js, css files in header of desk.html # include js, css files in header of desk.html
# app_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css" # app_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css"
app_include_js = "/assets/manufacturing_overview/js/manufacturing_overview.min.js" app_include_js = "manufacturing_overview.bundle.js"
# include js, css files in header of web template # include js, css files in header of web template
# web_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css" # web_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css"
@@ -148,30 +149,6 @@ app_include_js = "/assets/manufacturing_overview/js/manufacturing_overview.min.j
# auto_cancel_exempted_doctypes = ["Auto Repeat"] # auto_cancel_exempted_doctypes = ["Auto Repeat"]
# User Data Protection
# --------------------
user_data_fields = [
{
"doctype": "{doctype_1}",
"filter_by": "{filter_by}",
"redact_fields": ["{field_1}", "{field_2}"],
"partial": 1,
},
{
"doctype": "{doctype_2}",
"filter_by": "{filter_by}",
"partial": 1,
},
{
"doctype": "{doctype_3}",
"strict": False,
},
{
"doctype": "{doctype_4}"
}
]
# Authentication and authorization # Authentication and authorization
# -------------------------------- # --------------------------------

View File

@@ -2,19 +2,11 @@ import frappe
from functools import reduce from functools import reduce
from datetime import date from datetime import date
# frappe.utils.logger.set_log_level("DEBUG")
# logger = frappe.logger("manufacturing_overview",
# allow_site=True, file_count=100000)
def clearCache(doc, event): # logger.debug(f"String")
frappe.cache().delete_value("production_overview")
@frappe.whitelist()
def getUserPermissions():
return frappe.get_doc('Production Overview User Mapping', frappe.session.user)
def formatDate(d):
return frappe.utils.formatdate(
d, 'dd.MM.yyyy')
def getDueInDays(d): def getDueInDays(d):
@@ -43,20 +35,7 @@ def shortenCustomerName(customer):
return customer return customer
def calculateSalesOrderStatus(item, warehouseamount, workorders): def generateProductionOverviewCacheData():
if item.delivered_qty < item.qty and item.delivered_qty > 0:
return "Partially Delivered"
elif item.qty <= warehouseamount:
return "In Warehouse"
elif len(workorders) <= 0:
return "No Work Order"
elif len(workorders) > 0 and item.qty > warehouseamount:
return "To Produce"
else:
return "Unknown"
def generateProductionOverviewCache():
salesOrderItems = frappe.db.sql( salesOrderItems = frappe.db.sql(
""" """
SELECT SELECT
@@ -68,7 +47,9 @@ def generateProductionOverviewCache():
`tabSales Order Item`.delivered_qty, `tabSales Order Item`.delivered_qty,
`tabSales Order Item`.delivery_date, `tabSales Order Item`.delivery_date,
`tabSales Order Item`.item_code, `tabSales Order Item`.item_code,
`tabSales Order Item`.work_order_qty,
`tabSales Order Item`.parent, `tabSales Order Item`.parent,
`tabSales Order Item`.production_plan_qty,
`tabSales Order`.status as parentStatus `tabSales Order`.status as parentStatus
FROM FROM
`tabSales Order Item` `tabSales Order Item`
@@ -80,37 +61,70 @@ def generateProductionOverviewCache():
status = 'To Deliver and Bill' and status = 'To Deliver and Bill' and
item_group = "Produkte" item_group = "Produkte"
ORDER BY ORDER BY
delivery_date delivery_date, item_code, name
""", as_dict=1) """, as_dict=1)
currentWarehouseQtyList = []
for soItem in salesOrderItems: for soItem in salesOrderItems:
soItem.currentWarehouseQty = calculateCurrentWarehouseQty(
soItem.item_code, soItem.qty, currentWarehouseQtyList)
soItem.totalWarehouseQty = getAmountInWarehouses(soItem.item_code)
soItem.due_in = getDueInDays(soItem.delivery_date) soItem.due_in = getDueInDays(soItem.delivery_date)
soItem.customer = shortenCustomerName(frappe.get_value( soItem.customer = shortenCustomerName(frappe.get_value(
'Sales Order', soItem.parent, 'customer')) 'Sales Order', soItem.parent, 'customer'))
soItem.wos = frappe.get_all('Work Order', filters=[
['sales_order', '=', soItem.parent], ['production_item', '=', soItem.item_code], ['status', '!=', "Cancelled"]])
soItem.delivery_date = formatDate(soItem.delivery_date)
soItem.warehouseamount = getAmountInWarehouses(soItem.item_code)
if soItem.warehouseamount >= soItem.qty:
soItem.status = 'In Warehouse'
elif len(soItem.wos) != 0:
soItem.status = 'To Produce'
else:
soItem.status = 'No Work Order'
soItem = formatDate(soItem)
soItem = calculateStatus(soItem)
soItem.qty = soItem.qty - soItem.delivered_qty soItem.qty = soItem.qty - soItem.delivered_qty
soItem.link = '/app/sales-order/' + soItem.parent
soItem.link = '/app#Form/Sales%20Order/' + soItem.parent return salesOrderItems
frappe.cache().set_value("production_overview", salesOrderItems)
def calculateCurrentWarehouseQty(item_code, qty, currentWarehouseQtyList):
for warehouseQty in currentWarehouseQtyList:
if warehouseQty['item_code'] == item_code:
returnQty = warehouseQty['qty']
warehouseQty['qty'] = warehouseQty['qty'] - qty
return returnQty
fetchedWarehouseQty = getAmountInWarehouses(item_code)
currentWarehouseQtyList.append(
{'item_code': item_code, 'qty': fetchedWarehouseQty - qty})
return fetchedWarehouseQty
def formatDate(item):
item.delivery_date = frappe.utils.formatdate(
item.delivery_date, 'dd.MM.yyyy')
return item
def calculateStatus(item):
if item.delivered_qty > 0 and item.delivered_qty < item.qty:
item.status = 'Partially Delivered'
elif item.delivered_qty >= item.qty:
item.status = 'Fully Delivered'
elif item.currentWarehouseQty >= item.qty:
item.status = 'In Warehouse'
elif item.work_order_qty > 0:
item.status = 'To Produce'
else:
item.status = 'No Work Order'
return item
@frappe.whitelist() @frappe.whitelist()
def getSalesorderOverviewList(): def getSalesorderOverviewList():
salesOrderItems = frappe.cache().get_value( salesOrderItems = frappe.cache().get_value('production_overview', expires=True)
"production_overview", generateProductionOverviewCache())
if salesOrderItems is None:
salesOrderItems = generateProductionOverviewCacheData()
frappe.cache().set_value('production_overview', salesOrderItems, expires_in_sec=60)
return salesOrderItems return salesOrderItems

View File

@@ -0,0 +1,59 @@
import frappe
from erpnext.stock.get_item_details import get_default_bom
# frappe.utils.logger.set_log_level("DEBUG")
# logger = frappe.logger("manufacturing_overview",
# allow_site=True, file_count=50)
# logger.debug(f"{current_value} + {value} = {updated_value}")
@frappe.whitelist()
def makepp(sales_order):
"""
Check if Production Plan already exists and break when it does.
"""
so = frappe.get_doc("Sales Order", sales_order)
checkpp = frappe.get_all('Production Plan Item', filters={'sales_order': so.name}, fields=[
'name', 'parent'])
if checkpp:
return {'status': 400,
'docname': [checkpp[0].parent]}
"""
Create Production Plan
"""
sales_orders = [{
"sales_order": so.name,
"customer": so.customer,
"sales_order_date": so.transaction_date,
"grand_total": so.grand_total,
}]
po_items = []
for item in so.items:
i = {
"item_code": item.item_code,
"bom_no": get_default_bom(item.item_code),
"planned_qty": item.qty,
"planned_start_date": frappe.utils.nowdate(),
"stock_uom": "Stk",
"sales_order": so.name,
"sales_order_item": item.name
}
po_items.append(i)
pp = frappe.get_doc({
'doctype': 'Production Plan',
'get_items_from': 'Sales Order',
'sales_orders': sales_orders,
'po_items': po_items,
'for_warehouse': "Lagerräume - HP"
})
pp.insert()
return {'status': 201,
'docname': [pp.name]}

View File

@@ -1,5 +0,0 @@
{
"manufacturing_overview/js/manufacturing_overview.min.js": [
"public/js/manufacturing_overview_page.js"
]
}

View File

@@ -0,0 +1,4 @@
import './manufacturing_overview_desk.vue';
import './manufacturing_overview_page.js';
import './manufacturing_overview_row.vue';
import './production_plan.js';

File diff suppressed because one or more lines are too long

View File

@@ -1 +0,0 @@
{"version":3,"file":"manufacturing_overview.min.js","sources":["../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_row.vue?rollup-plugin-vue=script.js","../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_desk.vue?rollup-plugin-vue=script.js","../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_page.js"],"sourcesContent":["//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\nexport default {\n name: \"ManufacturingOverviewRow\",\n props: [\n \"qty\",\n \"item_name\",\n \"item_code\",\n \"customer\",\n \"delivery_date\",\n \"status\",\n \"link\",\n \"reference\",\n \"due_in\",\n ],\n methods: {\n pushRoute(link) {\n frappe.router.push_state(link);\n },\n },\n};\n","//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\nimport ManufacturingOverviewRow from \"./manufacturing_overview_row.vue\";\n\nexport default {\n components: {\n ManufacturingOverviewRow,\n },\n data() {\n return {\n salesorderData: [\n {\n customer: \"\",\n delivery_date: \"\",\n link: \"\",\n name: \"\",\n item_name: \"Loading...\",\n item_code: \"\",\n qty: \"\",\n sales_order: \"\",\n status: \"Unknown\",\n due_in: 0,\n },\n ],\n timer: \"\",\n origin: window.location.origin,\n userPermissions: {},\n };\n },\n created() {\n this.fetchEventsList();\n this.timer = setInterval(this.fetchEventsList, 30000);\n },\n methods: {\n fetchEventsList() {\n let self = this;\n frappe.call({\n method:\n \"manufacturing_overview.manufacturing_overview.api.getSalesorderOverviewList\",\n async: true,\n args: {},\n callback: function (r) {\n if (r.message) {\n self.salesorderData = r.message;\n }\n },\n });\n },\n cancelAutoUpdate() {\n clearInterval(this.timer);\n },\n beforeDestroy() {\n clearInterval(this.timer);\n },\n },\n};\n","import ManufacturingOverviewDesk from \"./manufacturing_overview_desk.vue\";\n\n$(document).ready(function () {\n $(\".layout-main-section-wrapper\").after('<div class=\"col-12 col-lg-4 layout-main-section-wrapper\" id=\"manufacturing-overview-body\"></div>');\n var pod = new Vue({\n el: \"#manufacturing-overview-body\",\n render(h) {\n return h(ManufacturingOverviewDesk, {});\n }\n });\n});"],"names":["name","props","methods","pushRoute","link","frappe","router","push_state","components","data","salesorderData","customer","delivery_date","item_name","item_code","qty","sales_order","status","due_in","timer","origin","window","location","userPermissions","created","this","fetchEventsList","setInterval","let","self","call","method","async","args","callback","r","message","cancelAutoUpdate","clearInterval","beforeDestroy","$","document","ready","after","Vue","el","render","h","ManufacturingOverviewDesk"],"mappings":"+BA4De,CACbA,KAAM,2BACNC,MAAO,CACL,MACA,YACA,YACA,WACA,gBACA,SACA,OACA,YACA,UAEFC,QAAS,CACPC,mBAAUC,GACRC,OAAOC,OAAOC,WAAWH,0mDCzChB,CACbI,WAAY,o8DAGZC,gBACE,MAAO,CACLC,eAAgB,CACd,CACEC,SAAU,GACVC,cAAe,GACfR,KAAM,GACNJ,KAAM,GACNa,UAAW,aACXC,UAAW,GACXC,IAAK,GACLC,YAAa,GACbC,OAAQ,UACRC,OAAQ,IAGZC,MAAO,GACPC,OAAQC,OAAOC,SAASF,OACxBG,gBAAiB,KAGrBC,mBACEC,KAAKC,kBACLD,KAAKN,MAAQQ,YAAYF,KAAKC,gBAAiB,MAEjDxB,QAAS,CACPwB,2BACEE,IAAIC,EAAOJ,KACXpB,OAAOyB,KAAK,CACVC,OACE,8EACFC,OAAO,EACPC,KAAM,GACNC,SAAU,SAAUC,GACdA,EAAEC,UACJP,EAAKnB,eAAiByB,EAAEC,aAKhCC,4BACEC,cAAcb,KAAKN,QAErBoB,yBACED,cAAcb,KAAKN,8vFChFzBqB,EAAEC,UAAUC,MAAM,WACdF,EAAE,gCAAgCG,MAAM,oGAC9B,IAAIC,IAAI,CACdC,GAAI,+BACJC,gBAAOC,GACH,OAAOA,EAAEC,EAA2B"}

View File

@@ -3,25 +3,24 @@
<div class="layout-main-section"> <div class="layout-main-section">
<div class="widget links-widget-box" style="height: auto"> <div class="widget links-widget-box" style="height: auto">
<div class="widget-head"> <div class="widget-head">
<div> <div class="widget-label">
<div class="widget-title"><svg class="icon icon-lg" style="">
<!-- <use class="" href="#icon-file"></use> -->
</svg> <span class="ellipsis" title="Manufacturing OVerview">Manufacturing Overview</span>
<!-- <span>
<button class="btn btn-secondary btn-default btn-sm" data-label="Mark sent">Mark
sent</button>
</span> -->
</div>
<div class="widget-subtitle"></div> <div class="widget-subtitle"></div>
</div> </div>
<div class="widget-control"></div> <div class="widget-control"></div>
</div> </div>
<div class="widget-body"> <div class="widget-body">
<manufacturing-overview-row <manufacturing-overview-row v-for="so in salesorderData" :key="so.name" v-bind:qty="so.qty"
v-for="so in salesorderData" v-bind:item_name="so.item_name" v-bind:item_code="so.item_code" v-bind:customer="so.customer"
:key="so.name" v-bind:delivery_date="so.delivery_date" v-bind:status="so.status" v-bind:link="so.link"
v-bind:qty="so.qty" v-bind:reference="so.parent" v-bind:due_in="so.due_in">
v-bind:item_name="so.item_name"
v-bind:item_code="so.item_code"
v-bind:customer="so.customer"
v-bind:delivery_date="so.delivery_date"
v-bind:status="so.status"
v-bind:link="so.link"
v-bind:reference="so.parent"
v-bind:due_in="so.due_in"
>
</manufacturing-overview-row> </manufacturing-overview-row>
</div> </div>
</div> </div>
@@ -59,7 +58,7 @@ export default {
}, },
created() { created() {
this.fetchEventsList(); this.fetchEventsList();
this.timer = setInterval(this.fetchEventsList, 30000); this.timer = setInterval(this.fetchEventsList, 10000);
}, },
methods: { methods: {
fetchEventsList() { fetchEventsList() {
@@ -86,5 +85,4 @@ export default {
}; };
</script> </script>
<style> <style></style>
</style>

View File

@@ -1,29 +1,21 @@
<template> <template>
<a <a @click="pushRoute(link)" class="row link-item text-wrap ellipsis onbpoard-spotlight" type="Link">
@click="pushRoute(link)"
class="row link-item text-wrap ellipsis onbpoard-spotlight"
type="Link"
>
<div class="col col-xs-8"> <div class="col col-xs-8">
<span <span class="indicator-pill no-margin" v-bind:class="{
class="indicator-pill no-margin"
v-bind:class="{
red: status === 'No Work Order', red: status === 'No Work Order',
blue: status === 'Partially Delivered', blue: status === 'Partially Delivered',
gray: status === 'Fully Delivered',
green: status === 'In Warehouse', green: status === 'In Warehouse',
yellow: status === 'To Produce', yellow: status === 'To Produce',
grey: status === 'Unknown', grey: status === 'Unknown',
}" }"></span>
></span>
<span class="widget-subtitle">{{ qty }}</span> - <span class="widget-subtitle">{{ qty }}</span> -
<span class="widget-title">{{ item_name }}</span> <span class="widget-title">{{ item_name }}</span>
<div> <div>
<small v-if="customer && item_code" class="color-secondary" <small v-if="customer && item_code" class="color-secondary">{{ customer }} -
>{{ customer }} - <a @click="pushRoute('/app/item/' + item_code)" type="Link">{{
<a @click="pushRoute('/app#Form/Item/' + item_code)" type="Link">{{
item_code item_code
}}</a></small }}</a></small>
>
<small v-else-if="customer" class="color-secondary">{{ <small v-else-if="customer" class="color-secondary">{{
customer customer
}}</small> }}</small>
@@ -36,22 +28,13 @@
</div> </div>
</div> </div>
<div <div v-if="due_in < 0" class="text-muted ellipsis color-secondary col col-xs-4 text-right">
v-if="due_in < 0"
class="text-muted ellipsis color-secondary col col-xs-4 text-right"
>
<b style="color: red">{{ delivery_date }}</b> <b style="color: red">{{ delivery_date }}</b>
</div> </div>
<div <div v-else-if="due_in === 0" class="text-muted ellipsis color-secondary col col-xs-4 text-right">
v-else-if="due_in === 0"
class="text-muted ellipsis color-secondary col col-xs-4 text-right"
>
<b style="color: black">{{ delivery_date }}</b> <b style="color: black">{{ delivery_date }}</b>
</div> </div>
<div <div v-else class="text-muted ellipsis color-secondary col col-xs-4 text-right">
v-else
class="text-muted ellipsis color-secondary col col-xs-4 text-right"
>
{{ delivery_date }} {{ delivery_date }}
</div> </div>
</a> </a>
@@ -80,4 +63,5 @@ export default {
</script> </script>
<style> <style>
</style> </style>

View File

@@ -0,0 +1,31 @@
frappe.ui.form.on("Sales Order", {
refresh: function (frm) {
frm.add_custom_button(__('Production Plan'), function () {
frappe.call({
method: 'manufacturing_overview.manufacturing_overview.productionplan.makepp',
args: {
sales_order: frm.docname
},
callback: function (r) {
if (r.message.status === 201) {
frappe.msgprint({
title: __('Created'),
indicator: 'green',
message: __('Production Plan {0} created.',
['<a href="/app/production-plan/' + r.message.docname + '">' + r.message.docname + '</a>'])
});
} else if (r.message.status === 400) {
frappe.msgprint({
title: __('Already exists'),
indicator: 'yellow',
message: __('Production Plan {0} already exists.',
['<a href="/app/production-plan/' + r.message.docname + '">' + r.message.docname + '</a>'])
});
} else {
console.log(r.message)
}
}
});
})
}
});