diff --git a/CHANGELOG.md b/CHANGELOG.md index 3516285e..d83f7e6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ +For license information, please see license.txt--> + # CHANGELOG
- 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
diff --git a/inventory_tools/inventory_tools/page/__init__.py b/inventory_tools/inventory_tools/page/__init__.py index b1279b72..6b9109e0 100644 --- a/inventory_tools/inventory_tools/page/__init__.py +++ b/inventory_tools/inventory_tools/page/__init__.py @@ -1,2 +1,2 @@ -# Copyright (c) 2025, AgriTheory and contributors +# Copyright (c) 2024, AgriTheory and contributors # For license information, please see license.txt diff --git a/inventory_tools/inventory_tools/page/optimizer/__init__.py b/inventory_tools/inventory_tools/page/optimizer/__init__.py new file mode 100644 index 00000000..0d402827 --- /dev/null +++ b/inventory_tools/inventory_tools/page/optimizer/__init__.py @@ -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) diff --git a/inventory_tools/inventory_tools/page/optimizer/optimizer.js b/inventory_tools/inventory_tools/page/optimizer/optimizer.js new file mode 100644 index 00000000..1bc35888 --- /dev/null +++ b/inventory_tools/inventory_tools/page/optimizer/optimizer.js @@ -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 = $(` +
+
+
+ + + + + +
+
+
+ `).appendTo($('.page-form')[0]) + + this.setup_paging_events() + + this.container = $('
').appendTo(this.page.main) + + this.output = $('
') + .css({ + overflow: 'auto', + minHeight: '200px', + }) + .appendTo(this.container)[0] + + this.resizer = $('
') + .css({ + height: '10px', + background: '#e0e0e0', + cursor: 'row-resize', + margin: '5px 0', + '&:hover': { + background: '#bdbdbd', + }, + }) + .appendTo(this.container) + + this.actual = $('
') + .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) + }) + } +} diff --git a/inventory_tools/inventory_tools/page/optimizer/optimizer.json b/inventory_tools/inventory_tools/page/optimizer/optimizer.json new file mode 100644 index 00000000..2170611e --- /dev/null +++ b/inventory_tools/inventory_tools/page/optimizer/optimizer.json @@ -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 +} diff --git a/inventory_tools/public/js/custom/work_order_list.js b/inventory_tools/public/js/custom/work_order_list.js index 4a739212..b795fb7e 100644 --- a/inventory_tools/public/js/custom/work_order_list.js +++ b/inventory_tools/public/js/custom/work_order_list.js @@ -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'), diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index 6f9eaf42..bec953c3 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -75,25 +75,145 @@ ), ] + workstations = [ - ("Mix Pie Crust Station", "20", "Table", "mixer.png", "mixer.png"), - ("Roll Pie Crust Station", "20", "Table", "rolling.png", "rolling.png"), - ("Make Pie Filling Station", "20", "Table", "table.png", "table.png"), - ("Cooling Station", "100", "Table", "rack.png", "rack.png"), - ("Box Pie Station", "100", "Table", "box.png", "box.png"), - ("Baking Station", "20", "Table", "oven.png", "oven.png"), - ("Assemble Pie Station", "20", "Table", "table.png", "table.png"), - ("Mix Pie Filling Station", "20", "Table", "mixer.png", "mixer.png"), - ("Packaging Station", "2", "Table", "box.png", "box.png"), - ("Food Prep Table 2", "10", "Table", "table.png", "table.png"), - ("Food Prep Table 1", "5", "Table", "table.png", "table.png"), - ("Range Station", "20", "Range", "range.png", "range.png"), - ("Cooling Racks Station", "80", "Cooling", "rack.png", "rack.png"), - ("Refrigerator Station", "200", "Refrigerator", "fridge.png", "fridge.png"), - ("Oven Station", "20", "Oven", "oven.png", "oven.png"), - ("Mixer Station", "10", "Mixer", "mixer.png", "mixer.png"), + { + "workstation_name": "Mix Pie Crust Station", + "production_capacity": "20", + "type": "Table", + "off_status_image": "mixer.png", + "on_status_image": "mixer.png", + "shift_types": ["Day Shift", "Evening Shift"], + }, + { + "workstation_name": "Roll Pie Crust Station", + "production_capacity": "20", + "type": "Table", + "off_status_image": "rolling.png", + "on_status_image": "rolling.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Make Pie Filling Station", + "production_capacity": "20", + "type": "Table", + "off_status_image": "table.png", + "on_status_image": "table.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Cooling Station", + "production_capacity": "100", + "type": "Table", + "off_status_image": "rack.png", + "on_status_image": "rack.png", + "shift_types": ["Day Shift", "Evening Shift"], + }, + { + "workstation_name": "Box Pie Station", + "production_capacity": "100", + "type": "Table", + "off_status_image": "box.png", + "on_status_image": "box.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Baking Station", + "production_capacity": "20", + "type": "Table", + "off_status_image": "oven.png", + "on_status_image": "oven.png", + "shift_types": ["Day Shift", "Evening Shift"], + }, + { + "workstation_name": "Assemble Pie Station", + "production_capacity": "20", + "type": "Table", + "off_status_image": "table.png", + "on_status_image": "table.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Mix Pie Filling Station", + "production_capacity": "20", + "type": "Table", + "off_status_image": "mixer.png", + "on_status_image": "mixer.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Packaging Station", + "production_capacity": "2", + "type": "Table", + "off_status_image": "box.png", + "on_status_image": "box.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Food Prep Table 2", + "production_capacity": "10", + "type": "Table", + "off_status_image": "table.png", + "on_status_image": "table.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Food Prep Table 1", + "production_capacity": "5", + "type": "Table", + "off_status_image": "table.png", + "on_status_image": "table.png", + "shift_types": ["Day Shift", "Evening Shift"], + }, + { + "workstation_name": "Range Station", + "production_capacity": "20", + "type": "Range", + "off_status_image": "range.png", + "on_status_image": "range.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Cooling Racks Station", + "production_capacity": "80", + "type": "Cooling", + "off_status_image": "rack.png", + "on_status_image": "rack.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Refrigerator Station", + "production_capacity": "200", + "type": "Refrigerator", + "off_status_image": "fridge.png", + "on_status_image": "fridge.png", + "shift_types": ["Day Shift"], + }, + { + "workstation_name": "Oven Station", + "production_capacity": "20", + "type": "Oven", + "off_status_image": "oven.png", + "on_status_image": "oven.png", + "shift_types": ["Day Shift", "Evening Shift"], + }, + { + "workstation_name": "Mixer Station", + "production_capacity": "10", + "type": "Mixer", + "off_status_image": "mixer.png", + "on_status_image": "mixer.png", + "shift_types": ["Day Shift"], + }, ] +shifts = [ + {"name": "Day Shift", "start_time": "06:00:00", "end_time": "14:00:00", "color": "Yellow"}, + {"name": "Evening Shift", "start_time": "14:00:00", "end_time": "22:00:00", "color": "Blue"}, + {"name": "Night Shift", "start_time": "22:00:00", "end_time": "06:00:00", "color": "Violet"}, +] + + operations = [ ( "Gather Pie Crust Ingredients", @@ -196,9 +316,10 @@ "Food Prep Table 1", "5", """- Tower: package one pie and one pocket, and one popper - - Pocketful of Bay: package one pocket with two poppers""", + - Pocketful of Bay: package one pocket with two poppers""", ), ] + attributes = { "Ambrosia Pie": { "Fruits": ["Hairless Rambutan", "Cloudberry", "Tayberry"], diff --git a/inventory_tools/tests/fixtures/items_stockentry.json b/inventory_tools/tests/fixtures/items_stock_entry.json similarity index 100% rename from inventory_tools/tests/fixtures/items_stockentry.json rename to inventory_tools/tests/fixtures/items_stock_entry.json diff --git a/inventory_tools/tests/setup.py b/inventory_tools/tests/setup.py index 69f4fc48..9da51622 100644 --- a/inventory_tools/tests/setup.py +++ b/inventory_tools/tests/setup.py @@ -11,13 +11,14 @@ ) from erpnext.setup.utils import set_defaults_for_tests from frappe.desk.page.setup_wizard.setup_wizard import setup_complete -from frappe.utils.data import add_months, flt, getdate, nowdate, get_datetime +from frappe.utils.data import add_months, flt, getdate, nowdate from webshop.webshop.doctype.website_item.website_item import make_website_item from inventory_tools.tests.fixtures import ( operations, suppliers, workstations, + shifts, ) @@ -30,7 +31,7 @@ def read_json(name): CUSTOMERS = read_json("customers") ITEM_DIMENSIONS = read_json("item_dimensions") ITEMS = read_json("items") -ITEMS_STOCKENTRY = read_json("items_stockentry") +ITEMS_STOCK_ENTRY = read_json("items_stock_entry") SPECIFICATIONS = read_json("specifications") WAREHOUSE_DIMENSIONS = read_json("warehouse_dimensions") WAREHOUSE_LOCATIONS = read_json("warehouse_locations") @@ -107,6 +108,7 @@ def create_test_data(): create_warehouses(settings) create_warehouse_locations() setup_manufacturing_settings(settings) + create_shift_types() create_workstations(settings) create_operations() create_item_groups(settings) @@ -230,12 +232,27 @@ def setup_manufacturing_settings(settings): ) frappe.set_value("Inventory Tools Settings", settings.company, "create_purchase_orders", 0) frappe.set_value( - "Inventory Tools Settings", settings.company, "overproduction_percentage_for_work_order", 50 + "Inventory Tools Settings", + settings.company, + "overproduction_percentage_for_work_order", + 50, ) frappe.set_value("Inventory Tools Settings", settings.company, "show_on_website", 1) frappe.set_value("Inventory Tools Settings", settings.company, "show_in_listview", 1) +def create_shift_types(): + for shift in shifts: + if frappe.db.exists("Shift Type", shift["name"]): + continue + sh = frappe.new_doc("Shift Type") + sh.name = shift["name"] + sh.start_time = shift["start_time"] + sh.end_time = shift["end_time"] + sh.color = shift["color"] + sh.save() + + def create_workstations(settings): if not frappe.db.exists("Plant Floor", "Kitchen"): pf = frappe.new_doc("Plant Floor") @@ -244,21 +261,42 @@ def create_workstations(settings): pf.warehouse = "Kitchen - APC" pf.plant_floor_layout = "/files/floor_plan.png" pf.save() + for ws in workstations: - if not frappe.db.exists("Workstation Type", ws[2]): + # Create workstation type if it doesn't exist + if not frappe.db.exists("Workstation Type", ws["type"]): wst = frappe.new_doc("Workstation Type") - wst.workstation_type = ws[2] + wst.workstation_type = ws["type"] wst.save() - if frappe.db.exists("Workstation", ws[0]): - work = frappe.get_doc("Workstation", ws[0]) + + # Create or update workstation + if frappe.db.exists("Workstation", ws["workstation_name"]): + work = frappe.get_doc("Workstation", ws["workstation_name"]) else: work = frappe.new_doc("Workstation") - work.workstation_name = ws[0] - work.production_capacity = ws[1] - work.workstation_type = ws[2] + + work.workstation_name = ws["workstation_name"] + work.production_capacity = ws["production_capacity"] + work.workstation_type = ws["type"] work.plant_floor = "Kitchen" - work.off_status_image = f"/files/{ws[3]}" - work.on_status_image = f"/files/{ws[4]}" + work.off_status_image = f"/files/{ws['off_status_image']}" + work.on_status_image = f"/files/{ws['on_status_image']}" + + work.working_hours = [] + for shift_type in ws["shift_types"]: + for sh in shifts: + if sh["name"] == shift_type: + result = sh + break + work.append( + "working_hours", + { + "shift_type": shift_type, + "start_time": result["start_time"], + "end_time": result["end_time"], + }, + ) + work.save() @@ -709,22 +747,39 @@ def create_production_plan(settings, prod_plan_from_doc): }, ) pp.get_mr_items() - for item in pp.po_items: - item.planned_start_date = settings.day + + pp.po_items = sorted(pp.po_items, key=lambda x: x.get("item_code")) + + for idx, item in enumerate(pp.po_items): + item.planned_start_date = settings.day + datetime.timedelta(days=idx) + pp.get_sub_assembly_items() - for item in pp.sub_assembly_items: - item.schedule_date = settings.day + start_time = datetime.datetime(settings.day.year, settings.day.month, settings.day.day, 6, 0) + pp.append( + "sub_assembly_items", + { + "schedule_date": start_time, + "production_item": "Pie Crust", + "name": None, + "type_of_manufacturing": "In House", + "idx": 1, + "qty": 50, + "bom_no": "BOM-Pie Crust-001", + "bom_level": 1, + "supplier": None, + "for_warehouse": "Storeroom - APC", + }, + ) + for idx, item in enumerate( + sorted(pp.sub_assembly_items, key=lambda x: (-abs(x.bom_level), x.idx)), start=1 + ): + item.idx = idx + item.schedule_date = start_time if item.production_item == "Pie Crust": - idx = item.idx - item.type_of_manufacturing = "Subcontract" - item.supplier = "Credible Contract Baking" item.qty = 50 - pp.append("sub_assembly_items", pp.sub_assembly_items[idx - 1].as_dict()) - pp.sub_assembly_items[-1].name = None - pp.sub_assembly_items[-1].type_of_manufacturing = "In House" - pp.sub_assembly_items[-1].bom_no = "BOM-Pie Crust-001" - pp.sub_assembly_items[-1].supplier = None - pp.for_warehouse = "Storeroom - APC" + time = frappe.get_value("BOM Operation", {"parent": item.bom_no}, "SUM(time_in_mins) AS time") + if time: + start_time += datetime.timedelta(minutes=time + 2) raw_materials = get_items_for_material_requests( pp.as_dict(), warehouses=None, get_parent_warehouse_data=None ) @@ -753,10 +808,21 @@ def create_production_plan(settings, prod_plan_from_doc): for wo in wos: wo = frappe.get_doc("Work Order", wo) wo.wip_warehouse = "Kitchen - APC" + current_year = str(settings.day.year) wo.save() wo.submit() job_cards = frappe.get_all("Job Card", {"work_order": wo.name}) - start_time = get_datetime() + wo.planned_start_date = start_time + time = frappe.get_value("BOM Operation", {"parent": item.bom_no}, "SUM(time_in_mins) AS time") + if time: + wo.planned_end_date = wo.planned_start_date + datetime.timedelta(minutes=time + 2) + wo.required_items = sorted(wo.required_items, key=lambda x: x.get("item_code")) + for idx, w in enumerate(wo.required_items, start=1): + w.idx = idx + w.required_qty = flt(w.required_qty, 3) + wo.save() + wo.submit() + frappe.db.set_value("Work Order", wo.name, "creation", start_time) for job_card in job_cards: job_card = frappe.get_doc("Job Card", job_card) batch_size, total_operation_time = frappe.get_value( @@ -846,7 +912,10 @@ def create_quotations(settings): "conversion_rate": 1, "transaction_date": nowdate(), "valid_till": add_months(nowdate(), 1), - "items": [{"item_code": "Ambrosia Pie", "qty": 1}, {"item_code": "Gooseberry Pie", "qty": 5}], + "items": [ + {"item_code": "Ambrosia Pie", "qty": 1}, + {"item_code": "Gooseberry Pie", "qty": 5}, + ], "company": settings.company, } quotation.update(values) @@ -863,7 +932,10 @@ def create_quotations(settings): "conversion_rate": 1, "transaction_date": nowdate(), "valid_till": add_months(nowdate(), 1), - "items": [{"item_code": "Ambrosia Pie", "qty": 1}, {"item_code": "Gooseberry Pie", "qty": 5}], + "items": [ + {"item_code": "Ambrosia Pie", "qty": 1}, + {"item_code": "Gooseberry Pie", "qty": 5}, + ], "company": settings.company, } quotation.update(values) @@ -880,7 +952,10 @@ def create_quotations(settings): "conversion_rate": 1, "transaction_date": nowdate(), "valid_till": add_months(nowdate(), 1), - "items": [{"item_code": "Ambrosia Pie", "qty": 2}, {"item_code": "Double Plum Pie", "qty": 1}], + "items": [ + {"item_code": "Ambrosia Pie", "qty": 2}, + {"item_code": "Double Plum Pie", "qty": 1}, + ], "company": settings.company, } quotation.update(values) @@ -897,7 +972,10 @@ def create_quotations(settings): "conversion_rate": 1, "transaction_date": nowdate(), "valid_till": add_months(nowdate(), 1), - "items": [{"item_code": "Ambrosia Pie", "qty": 5}, {"item_code": "Double Plum Pie", "qty": 10}], + "items": [ + {"item_code": "Ambrosia Pie", "qty": 5}, + {"item_code": "Double Plum Pie", "qty": 10}, + ], "company": "Chelsea Fruit Co", } quotation.update(values) @@ -968,7 +1046,7 @@ def create_warehouse_dimensions(): def create_stock_entries(): - j = len(ITEMS_STOCKENTRY) // 2 + j = len(ITEMS_STOCK_ENTRY) // 2 # Add items to warehouse se = frappe.new_doc("Stock Entry") se.company = "Chelsea Fruit Co" @@ -976,7 +1054,7 @@ def create_stock_entries(): se.set_posting_time = 1 se.stock_entry_type = "Material Receipt" - for item in ITEMS_STOCKENTRY[0:j]: + for item in ITEMS_STOCK_ENTRY[0:j]: se.append( "items", { @@ -997,7 +1075,7 @@ def create_stock_entries(): se.set_posting_time = 1 se.stock_entry_type = "Material Receipt" - for item in ITEMS_STOCKENTRY[j:]: + for item in ITEMS_STOCK_ENTRY[j:]: se.append( "items", {