Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
<!-- Copyright (c) 2025, AgriTheory and contributors
For license information, please see license.txt-->

For license information, please see license.txt-->

# CHANGELOG

<div class="byline">
Rohan Bansal, Devarsh Bhatt, github-actions, IshwaryaM1030, Myuddin Khatri, Heather Kusmierz, Tyler Matteson, and Francisco Roldán 2025-10-09
Rohan Bansal, Devarsh Bhatt, coleandreoli, fproldan, github-actions, Myuddin Khatri, Heather Kusmierz, and Tyler Matteson 2024-12-02
</div>


Expand Down
2 changes: 1 addition & 1 deletion inventory_tools/inventory_tools/page/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# Copyright (c) 2025, AgriTheory and contributors
# Copyright (c) 2024, AgriTheory and contributors
# For license information, please see license.txt
96 changes: 96 additions & 0 deletions inventory_tools/inventory_tools/page/optimizer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Copyright (c) 2024, AgriTheory and contributors
# For license information, please see license.txt

import frappe


@frappe.whitelist()
def get_work_order_gantt_data(work_order=None, production_item=None):
work_orders = get_work_order_dependencies(work_order, production_item)
dependency_map = {}
for d in work_orders:
if d.dependent_on:
if d.work_order not in dependency_map:
dependency_map[d.work_order] = []
dependency_map[d.work_order].append(d.dependent_on)
return [
{
"id": wo.work_order,
"name": f"{wo.production_item}:{wo.item_name}"
if wo.item_name != wo.production_item
else wo.production_item,
"start": wo.planned_start_date,
"end": wo.planned_end_date,
"progress": 100 if wo.status == "Completed" else 10,
"dependencies": ",".join(dependency_map.get(wo.work_order, [])),
}
for wo in work_orders
]


def get_work_order_dependencies(work_order=None, production_item=None):
conditions = ["wo.status = 'Not Started'"]
if work_order:
conditions.append(
"""(
wo.name = %(work_order)s
OR wo2.name = %(work_order)s
OR EXISTS (
SELECT 1 FROM `tabWork Order Item` woi2
WHERE woi2.parent = %(work_order)s
AND woi2.item_code = wo.production_item
)
)"""
)
if production_item:
conditions.append("wo.production_item = %(production_item)s")

where_clause = "WHERE " + " AND ".join(conditions)

query = f"""
WITH RECURSIVE work_order_tree AS (
SELECT
wo.name as work_order,
wo.production_item,
wo.item_name,
wo2.name as dependent_on,
wo.planned_start_date,
wo.planned_end_date,
wo.status,
0 as level
FROM `tabWork Order` wo
LEFT JOIN `tabWork Order Item` woi ON woi.parent = wo.name
LEFT JOIN `tabWork Order` wo2 ON wo2.production_item = woi.item_code
{where_clause}

UNION ALL

SELECT
t.work_order,
wo.production_item,
wo.item_name,
wo2.name,
wo.planned_start_date,
wo.planned_end_date,
wo.status,
t.level + 1
FROM work_order_tree t
JOIN `tabWork Order Item` woi ON woi.parent = t.dependent_on
JOIN `tabWork Order` wo2 ON wo2.production_item = woi.item_code
JOIN `tabWork Order` wo ON wo.name = t.work_order
WHERE t.level < 10
)
SELECT * FROM work_order_tree
GROUP BY work_order
ORDER BY level
"""
return frappe.db.sql(
query, {"work_order": work_order, "production_item": production_item}, as_dict=True
)


def get_optimized_data(work_order_names=None, start_datetime=None):
from Job_Shop_Scheduling_Benchmark_Environments_and_Instances.frappe.frappe_parser import FrappeJobShop
op_schedule = FrappeJobShop(work_order_names)
op_schedule.solve_fjsp()
return op_schedule.get_optimizer_schedule(start_datetime)
178 changes: 178 additions & 0 deletions inventory_tools/inventory_tools/page/optimizer/optimizer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) 2024, AgriTheory and contributors
// For license information, please see license.txt

frappe.pages['optimizer'].on_page_load = wrapper => {
frappe.require(
[
'assets/frappe/node_modules/frappe-gantt/dist/frappe-gantt.css',
'assets/frappe/node_modules/frappe-gantt/dist/frappe-gantt.min.js',
],
() => {
const page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Optimizer',
single_column: true,
})

frappe.pages['optimizer'].gantt_view = new OptimizerView(page)
}
)
}

class OptimizerView {
constructor(page) {
this.page = page
this.setup_filters()
this.setup_page()
this.init_gantt()
}

setup_filters() {
this.page.add_field({
label: 'Work Order',
fieldtype: 'Link',
options: 'Work Order',
fieldname: 'work_order',
onchange: () => this.init_gantt(),
})
this.page.add_field({
label: 'Production Item',
fieldtype: 'Link',
options: 'Item',
fieldname: 'production_item',
onchange: () => this.init_gantt(),
})
}

setup_page() {
this.list_paging_area = $(`
<div class="level">
<div class="level-left">
<div class="btn-group">
<button class="btn btn-default btn-sm btn-paging" data-value="Quarter Day">Quarter Day</button>
<button class="btn btn-default btn-sm btn-paging" data-value="Half Day">Half Day</button>
<button class="btn btn-default btn-sm btn-paging" data-value="Day">Day</button>
<button class="btn btn-default btn-sm btn-paging" data-value="Week">Week</button>
<button class="btn btn-default btn-sm btn-paging" data-value="Month">Month</button>
</div>
</div>
</div>
`).appendTo($('.page-form')[0])

this.setup_paging_events()

this.container = $('<div class="gantt-container gantt-modern">').appendTo(this.page.main)

this.output = $('<div class="gantt-view">')
.css({
overflow: 'auto',
minHeight: '200px',
})
.appendTo(this.container)[0]

this.resizer = $('<div class="resizer">')
.css({
height: '10px',
background: '#e0e0e0',
cursor: 'row-resize',
margin: '5px 0',
'&:hover': {
background: '#bdbdbd',
},
})
.appendTo(this.container)

this.actual = $('<div class="gantt-view">')
.css({
overflow: 'auto',
minHeight: '200px',
})
.appendTo(this.container)[0]

$(this.container).css({
display: 'grid',
'grid-template-rows': '40vh 10px 40vh',
gap: '0',
height: '85vh',
})

this.setup_resizer()
}

init_gantt() {
const filters = {
work_order: this.page.fields_dict.work_order.get_value(),
production_item: this.page.fields_dict.production_item.get_value(),
}

frappe
.xcall('inventory_tools.inventory_tools.page.optimizer.get_work_order_gantt_data', {
...filters,
})
.then(r => {
this.all_tasks = r
this.actual_gantt = new Gantt(this.actual, r, {
view_mode: 'Quarter Day',
on_click: task => {
frappe.set_route('Form', 'Work Order', task.id)
},
})
this.output_gantt = new Gantt(this.output, r, {
view_mode: 'Quarter Day',
on_click: task => {
frappe.set_route('Form', 'Work Order', task.id)
},
on_date_change: (task, start, end) => {
this.update_work_order(task.id, start, end)
},
})
})
}

update_work_order(name, start, end) {
frappe.call({
method: 'frappe.client.set_value',
args: {
doctype: 'Work Order',
name: name,
fieldname: {
planned_start_date: start,
planned_end_date: end,
},
},
})
}
setup_resizer() {
return
let startY, startHeightTop, startHeightBottom

this.resizer.on('mousedown', e => {
startY = e.clientY
startHeightTop = $(this.output).height()
startHeightBottom = $(this.actual).height()

$(document).on('mousemove', mousemove)
$(document).on('mouseup', mouseup)
})

const mousemove = e => {
const diff = e.clientY - startY
$(this.output).height(startHeightTop + diff)
$(this.actual).height(startHeightBottom - diff)
}

const mouseup = () => {
$(document).off('mousemove', mousemove)
$(document).off('mouseup', mouseup)
}
}

setup_paging_events() {
this.list_paging_area.find('.btn-paging').click(e => {
const view_mode = $(e.target).data('value')
console.log(view_mode)
this.output_gantt?.change_view_mode(view_mode)
this.actual_gantt?.change_view_mode(view_mode)
})
}
}
29 changes: 29 additions & 0 deletions inventory_tools/inventory_tools/page/optimizer/optimizer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"content": null,
"creation": "2024-12-02 15:01:35.785409",
"docstatus": 0,
"doctype": "Page",
"icon": "gantt",
"idx": 0,
"modified": "2024-12-02 15:01:35.785409",
"modified_by": "Administrator",
"module": "Inventory Tools",
"name": "optimizer",
"owner": "Administrator",
"page_name": "optimizer",
"roles": [
{
"role": "Manufacturing Manager"
},
{
"role": "System Manager"
},
{
"role": "Stock Manager"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0
}
10 changes: 9 additions & 1 deletion inventory_tools/public/js/custom/work_order_list.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
// Copyright (c) 2025, AgriTheory and contributors
// Copyright (c) 2024, AgriTheory and contributors
// For license information, please see license.txt

frappe.listview_settings['Work Order'] = {
refresh: listview => {
listview.page.add_custom_menu_item(
$('[data-view]').parent(),
__('Optimizer'),
() => frappe.set_route('/optimizer'),
true,
null,
'gantt'
)
listview.page.add_custom_menu_item(
$('[data-view]').parent(),
__('Alternative Workstations'),
Expand Down
Loading
Loading