7 Commits

Author SHA1 Message Date
Christian Anetzberger
78e1831314 Removed logs 2025-05-21 09:56:03 +00:00
b1935a3520 Added functionality to lost Purchase Order Items 2024-09-27 10:22:14 +00:00
cd98dffbab Production Plan extension 2024-08-08 10:37:37 +00:00
43d9b0dc0f Updated Links 2024-01-30 14:07:07 +00:00
2670bb83c7 Fixed for version-15 2024-01-10 11:59:09 +00:00
cccf121910 Merge pull request 'Fixed caching issues' (#4) from develop into version-14
Reviewed-on: #4
2023-09-08 09:45:19 +02:00
b20411fce1 Fixed caching issues 2023-09-08 07:44:22 +00:00
9 changed files with 452 additions and 118 deletions

View File

@@ -104,6 +104,12 @@ app_include_js = "manufacturing_overview.bundle.js"
# } # }
# } # }
# doc_events = {
# "Subcontracting Receipt": {
# "on_submit": "manufacturing_overview.manufacturing_overview.productionplan.update_productionplan_status",
# }
# }
# Scheduled Tasks # Scheduled Tasks
# --------------- # ---------------

View File

@@ -3,10 +3,9 @@ from functools import reduce
from datetime import date from datetime import date
# frappe.utils.logger.set_log_level("DEBUG") # frappe.utils.logger.set_log_level("DEBUG")
# logger = frappe.logger("manufacturing_overview", # logger = frappe.logger("manufacturing_overview", allow_site=True)
# allow_site=True, file_count=100000)
# logger.debug(f"String") #logger.debug(f"String")
def getDueInDays(d): def getDueInDays(d):
@@ -14,9 +13,12 @@ def getDueInDays(d):
return delta.days return delta.days
def getWorkOrders(soname): def getWorkOrder(soItemName):
return frappe.get_all("Work Order", filters={ return frappe.get_all("Work Order", filters={"sales_order_item": soItemName})
'sales_order': soname}, fields=['name'])
def getPurchaseOrder(soItemName):
return frappe.get_all("Purchase Order Item", filters={"sales_order_item": soItemName}, fields=['parent'])
def getAmountInWarehouses(item_code): def getAmountInWarehouses(item_code):
@@ -45,7 +47,7 @@ def generateProductionOverviewCacheData():
`tabSales Order Item`.qty, `tabSales Order Item`.qty,
`tabSales Order Item`.actual_qty, `tabSales Order Item`.actual_qty,
`tabSales Order Item`.delivered_qty, `tabSales Order Item`.delivered_qty,
`tabSales Order Item`.delivery_date, `tabSales Order Item`.delivery_date AS date,
`tabSales Order Item`.item_code, `tabSales Order Item`.item_code,
`tabSales Order Item`.work_order_qty, `tabSales Order Item`.work_order_qty,
`tabSales Order Item`.parent, `tabSales Order Item`.parent,
@@ -61,21 +63,35 @@ def generateProductionOverviewCacheData():
status = 'To Deliver and Bill' and status = 'To Deliver and Bill' and
item_group = "Produkte" item_group = "Produkte"
ORDER BY ORDER BY
delivery_date, item_code, name date, item_code, name
""", as_dict=1) """, as_dict=1)
currentWarehouseQtyList = [] currentWarehouseQtyList = []
for soItem in salesOrderItems: for soItem in salesOrderItems:
soItem.currentWarehouseQty = calculateCurrentWarehouseQty( soItem.currentWarehouseQty = calculateCurrentWarehouseQty(
soItem.item_code, soItem.qty, currentWarehouseQtyList) soItem.item_code, soItem.qty, currentWarehouseQtyList)
soItem.totalWarehouseQty = getAmountInWarehouses(soItem.item_code) soItem.totalWarehouseQty = getAmountInWarehouses(soItem.item_code)
soItem.due_in = getDueInDays(soItem.delivery_date) soItem.due_in = getDueInDays(soItem.date)
soItem.customer = shortenCustomerName(frappe.get_value( soItem.customer = shortenCustomerName(frappe.get_value(
'Sales Order', soItem.parent, 'customer')) 'Sales Order', soItem.parent, 'customer'))
soItem.direct_po = getPurchaseOrder(soItem.name)
if len(soItem.direct_po) >= 1:
soItem.direct_po = soItem.direct_po[0]['parent']
else:
soItem.direct_po = None
soItem.direct_wo = getWorkOrder(soItem.name)
if len(soItem.direct_wo) >= 1:
soItem.direct_wo = soItem.direct_wo[0]['name']
else:
soItem.direct_wo = None
soItem = formatDate(soItem) soItem = formatDate(soItem)
soItem = calculateStatus(soItem) soItem = calculateStatus(soItem)
@@ -85,6 +101,44 @@ def generateProductionOverviewCacheData():
return salesOrderItems return salesOrderItems
def generatePurchaseOrderOverviewCacheData():
purchaseOrderItems = frappe.db.sql(
"""
SELECT
`tabPurchase Order Item`.name,
`tabPurchase Order Item`.item_name,
`tabPurchase Order Item`.item_group,
`tabPurchase Order Item`.qty,
`tabPurchase Order Item`.schedule_date AS date,
`tabPurchase Order Item`.item_code,
`tabPurchase Order Item`.parent,
`tabPurchase Order Item`.fg_item,
`tabPurchase Order`.status as parentStatus
FROM
`tabPurchase Order Item`
INNER JOIN `tabPurchase Order`
ON
`tabPurchase Order Item`.parent = `tabPurchase Order`.name
WHERE
status = 'To Receive and Bill'
ORDER BY
date, item_code, name
""", as_dict=1)
for poItem in purchaseOrderItems:
poItem = formatDate(poItem)
poItem.customer = frappe.get_value("Purchase Order", poItem.parent, 'supplier_name')
poItem.fg_item_name = frappe.get_value("Item", poItem.fg_item, 'item_name')
poItem.direct_po = poItem.parent
poItem.link = '/app/purchase-order/' + poItem.parent
poItem.status = 'Unknown'
return purchaseOrderItems
def calculateCurrentWarehouseQty(item_code, qty, currentWarehouseQtyList): def calculateCurrentWarehouseQty(item_code, qty, currentWarehouseQtyList):
for warehouseQty in currentWarehouseQtyList: for warehouseQty in currentWarehouseQtyList:
@@ -100,8 +154,8 @@ def calculateCurrentWarehouseQty(item_code, qty, currentWarehouseQtyList):
def formatDate(item): def formatDate(item):
item.delivery_date = frappe.utils.formatdate( item.date = frappe.utils.formatdate(
item.delivery_date, 'dd.MM.yyyy') item.date, 'dd.MM.yyyy')
return item return item
@@ -121,10 +175,20 @@ def calculateStatus(item):
@frappe.whitelist() @frappe.whitelist()
def getSalesorderOverviewList(): def getSalesorderOverviewList():
salesOrderItems = frappe.cache().get_value('production_overview') salesOrderItems = frappe.cache().get_value('production_overview', expires=True)
if salesOrderItems is None: if salesOrderItems is None:
salesOrderItems = generateProductionOverviewCacheData() salesOrderItems = generateProductionOverviewCacheData()
frappe.cache().set_value('production_overview', salesOrderItems, expires_in_sec=60) frappe.cache().set_value('production_overview', salesOrderItems,expires_in_sec=600)
return generateProductionOverviewCacheData() return salesOrderItems
@frappe.whitelist()
def getPurchaseOrderOverviewList():
purchaseOrderItems = frappe.cache().get_value('purchaseOrder_items', expires=True)
if purchaseOrderItems is None:
purchaseOrderItems = generatePurchaseOrderOverviewCacheData()
frappe.cache().set_value('purchaseOrder_items', purchaseOrderItems, expires_in_sec=600)
return purchaseOrderItems

View File

@@ -1,59 +1,109 @@
import frappe import frappe
from erpnext.stock.get_item_details import get_default_bom from erpnext.stock.get_item_details import get_default_bom
from frappe.utils import (
nowdate
)
# frappe.utils.logger.set_log_level("DEBUG") frappe.utils.logger.set_log_level("DEBUG")
# logger = frappe.logger("manufacturing_overview", logger = frappe.logger("manufacturing_overview",
# allow_site=True, file_count=50) allow_site=True, file_count=50)
# logger.debug(f"{wo_id}")
# logger.debug(f"{current_value} + {value} = {updated_value}")
@frappe.whitelist() @frappe.whitelist()
def makepp(sales_order): def makepp(sales_order):
""" """
Check if Production Plan already exists and break when it does. Check if Production Plan already exists and break when it does.
""" """
so = frappe.get_doc("Sales Order", sales_order) so = frappe.get_doc("Sales Order", sales_order)
checkpp = frappe.get_all('Production Plan Item', filters={'sales_order': so.name}, fields=[ checkpp = frappe.get_all('Production Plan Item', filters={'sales_order': so.name}, fields=[
'name', 'parent']) 'name', 'parent'])
if checkpp: if checkpp:
return {'status': 400, return {'status': 400,
'docname': [checkpp[0].parent]} 'docname': [checkpp[0].parent]}
""" """
Create Production Plan Create Production Plan
""" """
sales_orders = [{ sales_orders = [{
"sales_order": so.name, "sales_order": so.name,
"customer": so.customer, "customer": so.customer,
"sales_order_date": so.transaction_date, "sales_order_date": so.transaction_date,
"grand_total": so.grand_total, "grand_total": so.grand_total,
}] }]
po_items = [] po_items = []
for item in so.items: for item in so.items:
i = { i = {
"item_code": item.item_code, "item_code": item.item_code,
"bom_no": get_default_bom(item.item_code), "bom_no": get_default_bom(item.item_code),
"planned_qty": item.qty, "planned_qty": item.qty,
"planned_start_date": frappe.utils.nowdate(), "planned_start_date": frappe.utils.nowdate(),
"stock_uom": "Stk", "stock_uom": "Stk",
"sales_order": so.name, "sales_order": so.name,
"sales_order_item": item.name "sales_order_item": item.name
} }
po_items.append(i) po_items.append(i)
pp = frappe.get_doc({ pp = frappe.get_doc({
'doctype': 'Production Plan', 'doctype': 'Production Plan',
'get_items_from': 'Sales Order', 'get_items_from': 'Sales Order',
'sales_orders': sales_orders, 'sales_orders': sales_orders,
'po_items': po_items, 'po_items': po_items,
'for_warehouse': "Lagerräume - HP" 'for_warehouse': "Lagerräume - HP"
}) })
pp.insert() pp.insert()
return {'status': 201, return {'status': 201,
'docname': [pp.name]} 'docname': [pp.name]}
@frappe.whitelist()
def fixfinisheditemwopo(production_plan):
pp = frappe.get_doc("Production Plan", production_plan)
wo_id = frappe.get_all('Work Order', filters={'production_plan_item': pp.po_items[0].name})
wo_id = wo_id[0]["name"]
frappe.delete_doc("Work Order", wo_id)
po = frappe.new_doc("Purchase Order")
po.supplier = pp.po_items[0].custom_supplier
po.schedule_date = nowdate()
po.is_subcontracted = 1
for row in pp.po_items:
po_data = {
"fg_item": row.item_code,
"warehouse": row.warehouse,
"bom": row.bom_no,
"production_plan": pp.name,
"fg_item_qty": row.planned_qty,
"supplier_warehouse": "Externe Arbeiten - HP"
}
for field in [
"schedule_date",
"qty",
"description",
"production_plan_item",
]:
po_data[field] = row.get(field)
po.append("items", po_data)
po.set_service_items_for_finished_goods()
po.set_missing_values()
# po.flags.ignore_mandatory = True
# po.flags.ignore_validate = True
po.insert()
# pp.db_set("total_produced_qty", pp.total_planned_qty)
# pp.db_set("status", "Completed")
return {'status': 201,
'docname': po.name}

View File

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

View File

@@ -1,26 +1,94 @@
<template> <template>
<div class="col-12 col-lg-4 layout-main-section-wrapper"> <div class="form-tabs-list">
<ul class="nav form-tabs" id="form-tabs" role="tablist">
<li class="nav-item show">
<a
class="nav-link"
:class="{ active: currentTab === 0 }"
role="tab"
@click="changeTab(0)"
>
Production Overview
</a>
</li>
<li class="nav-item show">
<a
class="nav-link"
:class="{ active: currentTab === 1 }"
role="tab"
@click="changeTab(1)"
>
Purchase Overview
</a>
</li>
</ul>
<div class="layout-main-section"> <div class="layout-main-section">
<div class="widget links-widget-box" style="height: auto"> <div
class="widget quick-list-widget-box"
style="height: auto"
v-if="currentTab === 0"
>
<div class="widget-head"> <div class="widget-head">
<div class="widget-label"> <div class="widget-label">
<div class="widget-title"><svg class="icon icon-lg" style=""> <div class="widget-title">
<!-- <use class="" href="#icon-file"></use> --> <span class="ellipsis" title="Sales Order"
</svg> <span class="ellipsis" title="Manufacturing OVerview">Manufacturing Overview</span> >Delivery Overview</span
<!-- <span> >
<button class="btn btn-secondary btn-default btn-sm" data-label="Mark sent">Mark
sent</button>
</span> -->
</div> </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 v-for="so in salesorderData" :key="so.name" v-bind:qty="so.qty" <manufacturing-overview-row
v-bind:item_name="so.item_name" v-bind:item_code="so.item_code" v-bind:customer="so.customer" v-for="so in salesorderData"
v-bind:delivery_date="so.delivery_date" v-bind:status="so.status" v-bind:link="so.link" :key="so.name"
v-bind:reference="so.parent" v-bind:due_in="so.due_in"> v-bind:qty="so.qty"
v-bind:item_name="so.item_name"
v-bind:item_code="so.item_code"
v-bind:customer="so.customer"
v-bind:date="so.date"
v-bind:status="so.status"
v-bind:link="so.link"
v-bind:reference="so.parent"
v-bind:due_in="so.due_in"
v-bind:direct_wo="so.direct_wo"
v-bind:direct_po="so.direct_po"
v-bind:rowtype="so"
>
</manufacturing-overview-row>
</div>
</div>
<div
class="widget quick-list-widget-box"
style="height: auto"
v-if="currentTab === 1"
>
<div class="widget-head">
<div class="widget-label">
<div class="widget-title">
<span class="ellipsis" title="Sales Order"
>Purchase Overview</span
>
</div>
</div>
<div class="widget-control"></div>
</div>
<div class="widget-body">
<manufacturing-overview-row
v-for="sc in purchaseOrderData"
:key="sc.name"
v-bind:qty="sc.qty"
v-bind:item_name="sc.fg_item_name"
v-bind:item_code="sc.fg_item"
v-bind:fg_item="sc.fg_item"
v-bind:customer="sc.customer"
v-bind:date="sc.date"
v-bind:status="sc.status"
v-bind:direct_po="sc.direct_po"
v-bind:link="sc.link"
v-bind:rowtype="po"
>
</manufacturing-overview-row> </manufacturing-overview-row>
</div> </div>
</div> </div>
@@ -37,10 +105,11 @@ export default {
}, },
data() { data() {
return { return {
currentTab: 0,
salesorderData: [ salesorderData: [
{ {
customer: "", customer: "",
delivery_date: "", date: "",
link: "", link: "",
name: "", name: "",
item_name: "Loading...", item_name: "Loading...",
@@ -49,6 +118,22 @@ export default {
sales_order: "", sales_order: "",
status: "Unknown", status: "Unknown",
due_in: 0, due_in: 0,
direct_wo: "",
direct_po: "",
},
],
purchaseOrderData: [
{
name: "",
customer: "",
link: "",
item_name: "Loading...",
item_code: "",
qty: "",
date: "",
fg_item: "",
status: "Unknown",
direct_po: "",
}, },
], ],
timer: "", timer: "",
@@ -58,9 +143,13 @@ export default {
}, },
created() { created() {
this.fetchEventsList(); this.fetchEventsList();
this.timer = setInterval(this.fetchEventsList, 10000); this.timer = setInterval(this.fetchEventsList, 30000);
}, },
methods: { methods: {
changeTab(id) {
let self = this;
self.currentTab = id;
},
fetchEventsList() { fetchEventsList() {
let self = this; let self = this;
frappe.call({ frappe.call({
@@ -74,6 +163,18 @@ export default {
} }
}, },
}); });
frappe.call({
method:
"manufacturing_overview.manufacturing_overview.api.getPurchaseOrderOverviewList",
async: true,
args: {},
callback: function (r) {
if (r.message) {
self.purchaseOrderData = r.message;
}
},
});
}, },
cancelAutoUpdate() { cancelAutoUpdate() {
clearInterval(this.timer); clearInterval(this.timer);
@@ -85,4 +186,4 @@ export default {
}; };
</script> </script>
<style></style> <style></style>

View File

@@ -1,11 +1,12 @@
import { createApp } from "vue";
import ManufacturingOverviewDesk from "./manufacturing_overview_desk.vue"; import ManufacturingOverviewDesk from "./manufacturing_overview_desk.vue";
$(document).ready(function () { $(document).ready(function () {
$(".layout-main-section-wrapper").after('<div class="col-12 col-lg-4 layout-main-section-wrapper" id="manufacturing-overview-body"></div>'); $(".layout-main-section-wrapper").after('<div class="col-12 col-lg-4 layout-main-section-wrapper" id="manufacturing-overview-body"></div>');
var pod = new Vue({
el: "#manufacturing-overview-body", const moapp = createApp(ManufacturingOverviewDesk)
render(h) {
return h(ManufacturingOverviewDesk, {}); moapp.mount("#manufacturing-overview-body");
} });
});
});

View File

@@ -1,67 +1,152 @@
<template> <template>
<a @click="pushRoute(link)" class="row link-item text-wrap ellipsis onbpoard-spotlight" type="Link"> <div
<div class="col col-xs-8"> @click="pushRoute(link)"
<span class="indicator-pill no-margin" v-bind:class="{ @mouseenter="hover = true"
red: status === 'No Work Order', @mouseleave="(hover = false), (detailsOpen = false)"
blue: status === 'Partially Delivered', class="quick-list-item align-items-start"
gray: status === 'Fully Delivered', >
green: status === 'In Warehouse', <div class="col-1">
yellow: status === 'To Produce', <span
grey: status === 'Unknown', class="indicator-pill no-margin"
}"></span> v-bind:class="{
<span class="widget-subtitle">{{ qty }}</span> - red: status === 'No Work Order',
<span class="widget-title">{{ item_name }}</span> blue: status === 'Partially Delivered',
gray: status === 'Fully Delivered',
green: status === 'In Warehouse',
yellow: status === 'To Produce',
grey: status === 'Unknown',
}"
></span>
</div>
<div class="col col-xs-6 col-lg-8 overflow-hidden">
<span class="ellipsis title"
><b>{{ qty }}</b> - {{ item_name }}</span
>
<div> <div>
<small v-if="customer && item_code" class="color-secondary">{{ customer }} - <small v-if="fg_item" class="color-secondary">
<a @click="pushRoute('/app/item/' + item_code)" type="Link">{{ <a
item_code class="underline-hover"
}}</a></small> @click="pushRoute('/app/item/' + fg_item)"
v-on:click.stop
>{{ fg_item }}</a
>
</small>
<small v-else-if="customer && item_code" class="color-secondary"
>{{ customer }} -
<a
class="underline-hover"
@click="pushRoute('/app/item/' + item_code)"
v-on:click.stop
>{{ item_code }}</a
>
</small>
<small v-else-if="customer" class="color-secondary">{{ <small v-else-if="customer" class="color-secondary">{{
customer customer
}}</small> }}</small>
<small v-else-if="item_code" class="color-secondary">{{ <small v-else-if="item_code" class="color-secondary">{{
item_code item_code
}}</small> }}</small>
</div> </div>
<div> <div>
<small class="color-secondary">{{ reference }}</small> <small>
<span>
<a
class="underline-hover"
@click="pushRoute('/app/sales-order/' + reference)"
v-on:click.stop
>{{ reference }}</a
>
</span>
<span v-if="direct_wo">
-
<a
class="underline-hover"
@click="pushRoute('/app/work-order/' + direct_wo)"
v-on:click.stop
>{{ direct_wo }}</a
>
</span>
<span v-if="direct_po && direct_wo">-</span>
<span v-if="direct_po">
<a
class="underline-hover"
@click="pushRoute('/app/purchase-order/' + direct_po)"
v-on:click.stop
>{{ direct_po }}</a
>
</span>
</small>
</div> </div>
</div> </div>
<div v-if="due_in < 0" class="text-muted ellipsis color-secondary col col-xs-4 text-right"> <!-- v-if="hover" -->
<b style="color: red">{{ delivery_date }}</b>
<div
class="text-muted ellipsis color-secondary col col-xs-5 col-lg-3 text-right"
>
<div class="d-flex justify-content-end flex-column align-items-end">
<div class="mb-auto">
<b v-if="due_in < 0" style="color: red">{{ date }}</b>
<b v-else-if="due_in === 0" style="color: black">{{ date }}</b>
<span v-else>{{ date }}</span>
</div>
<!-- <div @click="getItemDetails(item_code)" v-if="!detailsOpen" v-on:click.stop>
Details
</div> -->
</div>
</div> </div>
<div v-else-if="due_in === 0" class="text-muted ellipsis color-secondary col col-xs-4 text-right"> <!-- <div v-if="detailsOpen" :style="{ height: '500px' }"></div> -->
<b style="color: black">{{ delivery_date }}</b> </div>
</div>
<div v-else class="text-muted ellipsis color-secondary col col-xs-4 text-right">
{{ delivery_date }}
</div>
</a>
</template> </template>
<script> <script>
export default { export default {
name: "ManufacturingOverviewRow", name: "ManufacturingOverviewRow",
data() {
return {
hover: false,
detailsOpen: false,
currentDetails: ["1", "2"],
};
},
props: [ props: [
"qty", "qty",
"item_name", "item_name",
"item_code", "item_code",
"customer", "customer",
"delivery_date", "customershort",
"date",
"status", "status",
"link", "link",
"reference", "reference",
"due_in", "due_in",
"direct_wo",
"direct_po",
"fg_item",
], ],
methods: { methods: {
pushRoute(link) { pushRoute(link) {
frappe.router.push_state(link); frappe.router.push_state(link);
}, },
// getItemDetails(item) {
// this.detailsOpen = !this.detailsOpen;
// frappe.call({
// method:
// "manufacturing_overview.manufacturing_overview.api.get_bom_tree",
// async: true,
// args: { item_code: item },
// callback: function (r) {
// console.log(r.message);
// },
// });
// },
}, },
}; };
</script> </script>
<style> <style>
.underline-hover:hover {
</style> text-decoration: underline;
}
</style>

View File

@@ -0,0 +1,28 @@
frappe.ui.form.on("Production Plan", {
refresh: function (frm) {
frm.add_custom_button(__('Fix WO/PO'), function () {
frappe.call({
method: 'manufacturing_overview.manufacturing_overview.productionplan.fixfinisheditemwopo',
args: {
production_plan: frm.docname
},
callback: function (r) {
if (r.message.status === 201) {
frappe.msgprint({
title: __('Created'),
indicator: 'green',
message: __('Purchase Order {0} created.',
['<a href="/app/purchase-order/' + r.message.docname + '">' + r.message.docname + '</a>'])
});
} else if (r.message.status === 400) {
frappe.msgprint({
title: __('Already exists'),
indicator: 'yellow',
message: "Oooopsie"
});
}
}
});
})
}
});

View File

@@ -21,8 +21,6 @@ frappe.ui.form.on("Sales Order", {
message: __('Production Plan {0} already exists.', message: __('Production Plan {0} already exists.',
['<a href="/app/production-plan/' + r.message.docname + '">' + r.message.docname + '</a>']) ['<a href="/app/production-plan/' + r.message.docname + '">' + r.message.docname + '</a>'])
}); });
} else {
console.log(r.message)
} }
} }
}); });