From 046f37c865b4ead9f44fc209434313a6962e2a22 Mon Sep 17 00:00:00 2001 From: Johan De Taeye Date: Thu, 22 Oct 2020 17:02:24 +0200 Subject: [PATCH] adding wizard on community branch + syncup with enterprise branch --- contrib/installer/pgsql/README.txt | 2 +- contrib/linux/debian-10/debian/httpd.conf | 9 - djangosettings.py | 3 +- doc/installation-guide/linux-binaries.rst | 1 + doc/release-notes.rst | 11 + .../common/static/common/src/webfactory.js | 104 +- freppledb/common/static/css/frepple.less | 69 + freppledb/common/static/js/frepple.js | 3 +- .../templates/admin/base_site_grid.html | 6 +- freppledb/common/templates/index.html | 2 + freppledb/execute/commands.py | 6 +- .../execute/management/commands/loaddata.py | 48 + .../execute/management/commands/runplan.py | 3 + .../management/commands/runwebserver.py | 4 +- freppledb/input/commands/export.py | 2 + freppledb/input/commands/load.py | 2 +- freppledb/input/static/input/src/buffer.js | 12 - freppledb/input/static/input/src/demand.js | 12 - freppledb/input/static/input/src/operation.js | 8 - freppledb/input/static/input/src/resource.js | 10 +- freppledb/input/templates/input/demand.html | 2 +- freppledb/input/views.py | 64 +- freppledb/output/commands.py | 2 +- freppledb/wizard/__init__.py | 0 freppledb/wizard/static/wizard/img/buffer.png | Bin 0 -> 85922 bytes .../wizard/static/wizard/img/customer.png | Bin 0 -> 52775 bytes .../static/wizard/img/distributionorder.png | Bin 0 -> 172318 bytes .../wizard/img/distributionordersummary.png | Bin 0 -> 65310 bytes .../wizard/img/generate_constrained.png | Bin 0 -> 118304 bytes .../wizard/img/generate_unconstrained.png | Bin 0 -> 117649 bytes .../static/wizard/img/inventoryreport.png | Bin 0 -> 94911 bytes .../wizard/img/inventoryreport_annotated.png | Bin 0 -> 111393 bytes freppledb/wizard/static/wizard/img/item.png | Bin 0 -> 103589 bytes .../static/wizard/img/item_drilldown.png | Bin 0 -> 101791 bytes .../static/wizard/img/itemdistribution.png | Bin 0 -> 59337 bytes .../wizard/static/wizard/img/itemsupplier.png | Bin 0 -> 64903 bytes .../wizard/static/wizard/img/location.png | Bin 0 -> 67774 bytes .../static/wizard/img/manufacturingorder.png | Bin 0 -> 144619 bytes .../wizard/static/wizard/img/operation.png | Bin 0 -> 73786 bytes .../static/wizard/img/operation_routing.png | Bin 0 -> 14905 bytes .../static/wizard/img/operation_single.png | Bin 0 -> 10782 bytes .../static/wizard/img/operationmaterial.png | Bin 0 -> 85980 bytes .../static/wizard/img/operationresource.png | Bin 0 -> 70360 bytes .../static/wizard/img/purchaseorder.png | Bin 0 -> 153162 bytes .../wizard/img/purchaseordersummary.png | Bin 0 -> 77596 bytes .../static/wizard/img/resource-default.png | Bin 0 -> 6664 bytes .../wizard/img/resource-time-buckets.png | Bin 0 -> 10675 bytes .../wizard/static/wizard/img/resource.png | Bin 0 -> 56949 bytes .../static/wizard/img/resourcereport.png | Bin 0 -> 72537 bytes .../wizard/static/wizard/img/salesorder.png | Bin 0 -> 161536 bytes .../static/wizard/img/salesorder_analysis.png | Bin 0 -> 193547 bytes .../wizard/img/salesorder_drilldown.png | Bin 0 -> 147735 bytes .../img/salesorder_why_short_or_late.png | Bin 0 -> 56972 bytes .../wizard/img/segment_businessrule.png | Bin 0 -> 70705 bytes .../wizard/static/wizard/img/supplier.png | Bin 0 -> 54940 bytes .../static/wizard/img/supplypath_fcst.png | Bin 0 -> 84041 bytes .../static/wizard/img/supplypath_mfg.png | Bin 0 -> 125661 bytes .../static/wizard/sample_data/buffer.mfg.xlsx | Bin 0 -> 8551 bytes .../static/wizard/sample_data/customer.xlsx | Bin 0 -> 8089 bytes .../wizard/sample_data/distributionorder.xlsx | Bin 0 -> 8448 bytes .../static/wizard/sample_data/item.mfg.xlsx | Bin 0 -> 8295 bytes .../wizard/sample_data/itemdistribution.xlsx | Bin 0 -> 8294 bytes .../wizard/sample_data/itemsupplier.mfg.xlsx | Bin 0 -> 8269 bytes .../static/wizard/sample_data/location.xlsx | Bin 0 -> 8199 bytes .../sample_data/manufacturingorder.xlsx | Bin 0 -> 8433 bytes .../static/wizard/sample_data/operation.xlsx | Bin 0 -> 8515 bytes .../wizard/sample_data/operationmaterial.xlsx | Bin 0 -> 8482 bytes .../wizard/sample_data/operationresource.xlsx | Bin 0 -> 8278 bytes .../wizard/sample_data/purchaseorder.mfg.xlsx | Bin 0 -> 8470 bytes .../static/wizard/sample_data/resource.xlsx | Bin 0 -> 8249 bytes .../wizard/sample_data/salesorder.mfg.xlsx | Bin 0 -> 8621 bytes .../wizard/sample_data/supplier.mfg.xlsx | Bin 0 -> 8121 bytes freppledb/wizard/templates/wizard/index.html | 43 + freppledb/wizard/templates/wizard/odoo.html | 26 + .../wizard/progress_productionplanning.svg | 191 +++ .../wizard/quickstart_production.html | 450 +++++ .../templates/wizard/send_us_dataset.html | 21 + freppledb/wizard/urls.py | 33 + freppledb/wizard/views.py | 1497 +++++++++++++++++ include/Makefile.am | 2 - include/frepple/timeline.h | 104 +- src/main.cpp | 2 + 82 files changed, 2617 insertions(+), 137 deletions(-) create mode 100644 freppledb/wizard/__init__.py create mode 100644 freppledb/wizard/static/wizard/img/buffer.png create mode 100644 freppledb/wizard/static/wizard/img/customer.png create mode 100644 freppledb/wizard/static/wizard/img/distributionorder.png create mode 100644 freppledb/wizard/static/wizard/img/distributionordersummary.png create mode 100644 freppledb/wizard/static/wizard/img/generate_constrained.png create mode 100644 freppledb/wizard/static/wizard/img/generate_unconstrained.png create mode 100644 freppledb/wizard/static/wizard/img/inventoryreport.png create mode 100644 freppledb/wizard/static/wizard/img/inventoryreport_annotated.png create mode 100644 freppledb/wizard/static/wizard/img/item.png create mode 100644 freppledb/wizard/static/wizard/img/item_drilldown.png create mode 100644 freppledb/wizard/static/wizard/img/itemdistribution.png create mode 100644 freppledb/wizard/static/wizard/img/itemsupplier.png create mode 100644 freppledb/wizard/static/wizard/img/location.png create mode 100644 freppledb/wizard/static/wizard/img/manufacturingorder.png create mode 100644 freppledb/wizard/static/wizard/img/operation.png create mode 100644 freppledb/wizard/static/wizard/img/operation_routing.png create mode 100644 freppledb/wizard/static/wizard/img/operation_single.png create mode 100644 freppledb/wizard/static/wizard/img/operationmaterial.png create mode 100644 freppledb/wizard/static/wizard/img/operationresource.png create mode 100644 freppledb/wizard/static/wizard/img/purchaseorder.png create mode 100644 freppledb/wizard/static/wizard/img/purchaseordersummary.png create mode 100644 freppledb/wizard/static/wizard/img/resource-default.png create mode 100644 freppledb/wizard/static/wizard/img/resource-time-buckets.png create mode 100644 freppledb/wizard/static/wizard/img/resource.png create mode 100644 freppledb/wizard/static/wizard/img/resourcereport.png create mode 100644 freppledb/wizard/static/wizard/img/salesorder.png create mode 100644 freppledb/wizard/static/wizard/img/salesorder_analysis.png create mode 100644 freppledb/wizard/static/wizard/img/salesorder_drilldown.png create mode 100644 freppledb/wizard/static/wizard/img/salesorder_why_short_or_late.png create mode 100644 freppledb/wizard/static/wizard/img/segment_businessrule.png create mode 100644 freppledb/wizard/static/wizard/img/supplier.png create mode 100644 freppledb/wizard/static/wizard/img/supplypath_fcst.png create mode 100644 freppledb/wizard/static/wizard/img/supplypath_mfg.png create mode 100644 freppledb/wizard/static/wizard/sample_data/buffer.mfg.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/customer.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/distributionorder.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/item.mfg.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/itemdistribution.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/itemsupplier.mfg.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/location.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/manufacturingorder.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/operation.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/operationmaterial.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/operationresource.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/purchaseorder.mfg.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/resource.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/salesorder.mfg.xlsx create mode 100644 freppledb/wizard/static/wizard/sample_data/supplier.mfg.xlsx create mode 100644 freppledb/wizard/templates/wizard/index.html create mode 100644 freppledb/wizard/templates/wizard/odoo.html create mode 100644 freppledb/wizard/templates/wizard/progress_productionplanning.svg create mode 100644 freppledb/wizard/templates/wizard/quickstart_production.html create mode 100644 freppledb/wizard/templates/wizard/send_us_dataset.html create mode 100644 freppledb/wizard/urls.py create mode 100644 freppledb/wizard/views.py diff --git a/contrib/installer/pgsql/README.txt b/contrib/installer/pgsql/README.txt index 3c2557feb4..bb85ee8411 100644 --- a/contrib/installer/pgsql/README.txt +++ b/contrib/installer/pgsql/README.txt @@ -5,7 +5,7 @@ obtained from: We redistribute this product unmodified. Initialising a database with the provided binaries is easy. -The frePPLe installer runs the following steps for you, but you're +The frePPLe installer runs the following steps for you, but you're always free to redo the initialisation to meet your needs: initdb --pgdata YOUR_DATA_FOLDER --encoding UTF8 pg_ctl --pgdata YOUR_DATA_FOLDER --log YOUR_LOG_FILE -w start diff --git a/contrib/linux/debian-10/debian/httpd.conf b/contrib/linux/debian-10/debian/httpd.conf index 1fe6d47c23..3270230163 100644 --- a/contrib/linux/debian-10/debian/httpd.conf +++ b/contrib/linux/debian-10/debian/httpd.conf @@ -43,15 +43,6 @@ #LoadModule authz_host_module modules/mod_authz_host.so #LoadModule env_module modules/mod_env.so -# Extra settings for websocket proxying support -#LoadModule proxy -#LoadModule proxy_wstunnel -# Modify the next lines to match the name of your scenarios name and FREPPLE_PORT settings -Proxypass "/ws/default" "ws://localhost:8002" retry=0 -Proxypass "/ws/scenario1/" "ws://localhost:8003" retry=0 -Proxypass "/ws/scenario2/" "ws://localhost:8004" retry=0 -Proxypass "/ws/scenario3/" "ws://localhost:8005" retry=0 - WSGIRestrictStdout Off ## HINT: All of the frePPLe-specific configurations can be put in a virtual host. This is diff --git a/djangosettings.py b/djangosettings.py index dce5395f53..c3d27ce035 100644 --- a/djangosettings.py +++ b/djangosettings.py @@ -239,6 +239,7 @@ # Add any project specific apps here # "freppledb.odoo", #'freppledb.erpconnection', + "freppledb.wizard", "freppledb.input", "freppledb.output", "freppledb.metrics", @@ -252,7 +253,7 @@ # The next two apps allow users to run their own SQL statements on # the database, using the SQL_ROLE configured above. "freppledb.reportmanager", - # "freppledb.executesql", + "freppledb.executesql", ) # Custom attribute fields in the database diff --git a/doc/installation-guide/linux-binaries.rst b/doc/installation-guide/linux-binaries.rst index 308c39748e..3cff3f9c4f 100644 --- a/doc/installation-guide/linux-binaries.rst +++ b/doc/installation-guide/linux-binaries.rst @@ -203,6 +203,7 @@ Here are the steps to get a fully working environment. 'bootstrap3', 'freppledb.boot', # << ADD YOUR CUSTOM EXTENSION APPS HERE + 'freppledb.wizard', << COMMENT IF MODEL BUILDING WIZARD ISN'T NEEDED 'freppledb.input', #'freppledb.odoo', # << UNCOMMENT TO ACTIVATE THE ODOO INTEGRATION #'freppledb.erpconnection', # << UNCOMMENT TO ACTIVATE THE GENERIC ERP INTEGRATION diff --git a/doc/release-notes.rst b/doc/release-notes.rst index 1cdad0e8b6..640924efe2 100644 --- a/doc/release-notes.rst +++ b/doc/release-notes.rst @@ -6,6 +6,17 @@ Release notes .. rubric:: User interface +- A new get-started wizard is added to generate forecast for a single item. + Fill in a simple form with the item, location, customer and recent sales + history, and we'll populate the data tables and generate the statistical forecast. + +- A new get-started wizard is added to generate a production plan for a single + sales order. Fill in the details of the sales order, define the supply path + and we'll populate the data tables and generate the production plan. + +- A data loading wizard which is already available on the Enterprise and Cloud + Editions for a long time. It is now also made available on the Community Edition. + - The cockpit is renamed to `home `_. 6.8.0 (2020/10/03) diff --git a/freppledb/common/static/common/src/webfactory.js b/freppledb/common/static/common/src/webfactory.js index d8d325882c..8007108dd7 100644 --- a/freppledb/common/static/common/src/webfactory.js +++ b/freppledb/common/static/common/src/webfactory.js @@ -18,15 +18,49 @@ angular.module('frepple.common').factory('WebSvc', webfactory); -webfactory.$inject = ['$rootScope', '$websocket', '$interval']; +webfactory.$inject = ['$rootScope', '$websocket', '$interval', '$http', '$window']; /* * This service connects to the frePPLe web service using a web socket. * The URL of the web socket is retrieved from the global variable "service_url". + * The authentication is retrieved from the global variable "service_token". */ -function webfactory($rootScope, $websocket, $interval) { +function webfactory($rootScope, $websocket, $interval, $http, $window) { 'use strict'; - var debug = true; + var debug = false; + var authenticated = false; + var message=''; + var themodal = angular.element(document); + + function showerrormodal(msg) { + angular.element("#controller").scope().databaseerrormodal = false; + + if (typeof msg === 'string') { + message = '

'+msg+'

'; + } else if (typeof msg === 'object') { + message = '
'+ msg.description + '
'; + } else { + message = '

Websocket connection is not working.

Please check that plan.webservice parameter is set to true,
and execute the plan.

'; + } + + angular.element("#controller").scope().$applyAsync(function(){ + if (typeof msg === 'object') { + if (msg.hasOwnProperty('category') && msg.hasOwnProperty('description')) { + themodal.find('.modal-title span').html('Webservice error'); + themodal.find('.modal-body').css({'width':500,'height':350,'overflow':'auto'}); + themodal.find('#savechangesparagraph').hide(); + themodal.find('#saveAbutton').hide(); + themodal.find('.modal-body').append(message); + } + } else { + themodal.find('.modal-title span').html('Websocket connection problem'); + themodal.find('.modal-body').html('
'+ message + '
'); + } + }); + + angular.element(document).find('#popup2').modal('show'); + } + var webservice = $websocket(service_url, { reconnectIfNotNormalClose: true }); @@ -37,21 +71,53 @@ function webfactory($rootScope, $websocket, $interval) { webservice.onMessage(function(message) { var jsondoc = angular.fromJson(message.data); - if (debug) - console.log(jsondoc); - $rootScope.$broadcast("websocket-" + jsondoc.category, jsondoc); + if (debug) console.log(jsondoc); + if (jsondoc.hasOwnProperty('category') && jsondoc.category === 'error') { + showerrormodal(jsondoc); + } else { + $rootScope.$broadcast("websocket-" + jsondoc.category, jsondoc); + } }); + var alreadyprocessing = false; webservice.onError(function(message) { - message = '

Websocket connection is not working.

Please check that plan.webservice parameter is set to true,
and execute the plan.

'; - angular.element("#controller").scope().databaseerrormodal = true; - angular.element("#controller").scope().$apply(); - angular.element(document) - .find('#popup2 .modal-title span').html('Websocket connection problem'); - angular.element(document) - .find('.modal-body').html('
'+ message + '
'); - angular.element(document).find('#popup2').modal('show'); - angular.element("#controller").scope().$apply(); + + if (!alreadyprocessing) { + alreadyprocessing = true; + $http({ + method: 'POST', + url: url_prefix + '/execute/api/frepple_start_web_service/' + }).then(function successCallback(response) { + if(response.data.message !== "Successfully launched task") { + showerrormodal(); + } else { + const taskid = response.data.taskid; + var answer = {}; + const idUrl = url_prefix + '/execute/api/status/?id=' + taskid; + var testsuccess = $interval(function(counter) { + $http({ + method: 'POST', + url: idUrl + }).then(function(response) { + showerrormodal('Starting Web Service, please wait a moment.'); + angular.element(document).find('#saveAbutton').hide(); + answer = response.data[Object.keys(response.data)]; + if (answer.message === "Web service active") { + $interval.cancel(testsuccess); + $window.location.reload(); + } else if (answer.finished !== "None" && answer.status !== "Web service active") { + $interval.cancel(testsuccess); + showerrormodal(); + alreadyprocessing = false; + } + }); + }, 1000, 0); //every second + } + }, function errorCallback(response) { + showerrormodal(response.data); + }); + } + }); function subscribe(msg, callback) { @@ -64,9 +130,14 @@ function webfactory($rootScope, $websocket, $interval) { function send(data) { if (webservice.readyState > 1) { webservice = $websocket(service_url); + authenticated = false; + } + if (!authenticated) { + webservice.send("/authenticate/" + service_token); + authenticated = true; } if (debug) - console.log("send:" + data); + console.log("send:" + data); webservice.send(data); } @@ -75,4 +146,5 @@ function webfactory($rootScope, $websocket, $interval) { subscribe: subscribe }; return methods; + } diff --git a/freppledb/common/static/css/frepple.less b/freppledb/common/static/css/frepple.less index 35595b32b4..bd2826c84b 100644 --- a/freppledb/common/static/css/frepple.less +++ b/freppledb/common/static/css/frepple.less @@ -817,3 +817,72 @@ h1 small { color: #777777; } } + +// Wizard + +.fill-primary { + fill: @panel-default-heading-bg; +} + +.fill-primary-light { + fill: lighten(@panel-default-heading-bg, 20%); +} + +@light-panel-header: lighten(@panel-default-heading-bg, 20%); + +.getBackground2Steps(@color1, @color2, @color3) { + @r1: ceil(red(@color1)); + @g1: ceil(green(@color1)); + @b1: ceil(blue(@color1)); + @r2: ceil(red(@color2)); + @g2: ceil(green(@color2)); + @b2: ceil(blue(@color2)); + @r3: ceil(red(@color3)); + @g3: ceil(green(@color3)); + @b3: ceil(blue(@color3)); + background-image: url("data:image/svg+xml;utf8,"); + background-size: 100% 100%; + background-color: transparent; +} + +.getBackground3Steps(@color1, @color2, @color3, @color4) { + @r1: ceil(red(@color1)); + @g1: ceil(green(@color1)); + @b1: ceil(blue(@color1)); + @r2: ceil(red(@color2)); + @g2: ceil(green(@color2)); + @b2: ceil(blue(@color2)); + @r3: ceil(red(@color3)); + @g3: ceil(green(@color3)); + @b3: ceil(blue(@color3)); + @r4: ceil(red(@color4)); + @g4: ceil(green(@color4)); + @b4: ceil(blue(@color4)); + background-image: url("data:image/svg+xml;utf8,"); + background-size: 100% 100%; + background-color: transparent; +} + +.wizard-1-of-2 table, .wizard-2-of-2 table, .wizard-1-of-3 table, .wizard-2-of-3 table, .wizard-3-of-3 table { + background-color: transparent; +} + +.wizard-1-of-2 { + .getBackground2Steps(@panel-default-heading-bg, @light-panel-header, @panel-default-heading-bg); +} + +.wizard-2-of-2 { + .getBackground2Steps(@light-panel-header, @panel-default-heading-bg, @panel-default-heading-bg); +} + +.wizard-1-of-3 { + .getBackground3Steps(@panel-default-heading-bg, @light-panel-header, @light-panel-header, @panel-default-heading-bg); +} + +.wizard-2-of-3 { + .getBackground3Steps(@light-panel-header, @panel-default-heading-bg, @light-panel-header, @panel-default-heading-bg); +} + +.wizard-3-of-3 { + .getBackground3Steps(@light-panel-header, @light-panel-header, @panel-default-heading-bg, @panel-default-heading-bg); +} diff --git a/freppledb/common/static/js/frepple.js b/freppledb/common/static/js/frepple.js index 9fb060f9d0..c770566014 100644 --- a/freppledb/common/static/js/frepple.js +++ b/freppledb/common/static/js/frepple.js @@ -2319,7 +2319,7 @@ var dashboard = { }); }); - $("#workarea").each( function() { + $("#dashboard").each( function() { Sortable.create($(this)[ 0 ], { group: "cockpit", handle: "h1", @@ -2328,7 +2328,6 @@ var dashboard = { }); }); - //stop: dashboard.save $(".panel-toggle").click(function() { var icon = $(this); icon.toggleClass("fa-minus fa-plus"); diff --git a/freppledb/common/templates/admin/base_site_grid.html b/freppledb/common/templates/admin/base_site_grid.html index 2820e34727..4179b24503 100644 --- a/freppledb/common/templates/admin/base_site_grid.html +++ b/freppledb/common/templates/admin/base_site_grid.html @@ -154,10 +154,10 @@ return true; },{% endif %} loadComplete: function(data) { - {% if reportclass.message_when_empty and not filters %} + {% if reportclass.message_when_empty %} $("#grid_empty_message").remove(); - if (data.records == 0) - $("#grid").parent().append( + if (data.records == 0 && $(this).getGridParam("postData").filters === undefined) + $(this).parent().append( `
{% include reportclass.message_when_empty %}
` diff --git a/freppledb/common/templates/index.html b/freppledb/common/templates/index.html index 05a2771652..5af919effb 100644 --- a/freppledb/common/templates/index.html +++ b/freppledb/common/templates/index.html @@ -27,6 +27,7 @@ {% block content %} {% getDashboard as dashboard hiddenwidgets %} +
{% for row in dashboard %}
@@ -101,6 +102,7 @@

{% if forloop.last %}{% endif %} {% endfor %} {% endfor %} +

{% include "admin/subtemplate_timebuckets.html" %} +{% endif %} +{% endblock %} + +{% block content %} +{% if not post %} +
+
+
+ +
+
+

+ Create a sales order +

+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+
+
+
+
+ +{% else %} +
+
+
+ +
+
+
+ Here is the production plan for your sales order {{post.salesorder}}: +
+ +
+
Check out what we did with your input data: +
    + {% for msg in post.messages %} +
  • {{ msg | safe }}
  • + {% endfor %} +
+
+
+
+ Learn how to load Excel spreadsheets to repeat this calculation for many sales orders.
+
+
+
+ +
+
+
+{% endif %} +{% endblock %} diff --git a/freppledb/wizard/templates/wizard/send_us_dataset.html b/freppledb/wizard/templates/wizard/send_us_dataset.html new file mode 100644 index 0000000000..0a43d4f645 --- /dev/null +++ b/freppledb/wizard/templates/wizard/send_us_dataset.html @@ -0,0 +1,21 @@ +{% extends "admin/base_site_nav.html" %} +{% load i18n %} + +{% block content %} +
+
+
+ +
+

Learning an APS tool takes a considerable learning... and time - which you may not have. +By engaging with our expert team, we guarantuee a fast and efficient way +to configure the application to fit your business requirements.

+

Contact us to discuss your requirements and expectations for a proof of concept.

+

Contact us

+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/freppledb/wizard/urls.py b/freppledb/wizard/urls.py new file mode 100644 index 0000000000..2b7ebf7185 --- /dev/null +++ b/freppledb/wizard/urls.py @@ -0,0 +1,33 @@ +# +# Copyright (C) 2019 by frePPLe bv +# +# All information contained herein is, and remains the property of frePPLe. +# You are allowed to use and modify the source code, as long as the software is used +# within your company. +# You are not allowed to distribute the software, either in the form of source code +# or in the form of compiled binaries. +# + +from django.conf.urls import url +from django.urls import path + +from .views import ( + Home, + Odoo, + SendUsDataset, + WizardLoad, + SendSurveyMail, + QuickStartProduction, +) + +# Automatically add these URLs when the application is installed +autodiscover = True + +urlpatterns = [ + url(r"^$", Home), + url(r"^wizard/quickstart/production/$", QuickStartProduction.as_view()), + url(r"^wizard/send-us-dataset/$", SendUsDataset.as_view()), + url(r"^wizard/odoo/$", Odoo.as_view()), + path(r"wizard/load//", WizardLoad), + url(r"^wizard/sendsurveymail/$", SendSurveyMail.action), +] diff --git a/freppledb/wizard/views.py b/freppledb/wizard/views.py new file mode 100644 index 0000000000..75c328c254 --- /dev/null +++ b/freppledb/wizard/views.py @@ -0,0 +1,1497 @@ +# +# Copyright (C) 2019 by frePPLe bv +# +# All information contained herein is, and remains the property of frePPLe. +# You are allowed to use and modify the source code, as long as the software is used +# within your company. +# You are not allowed to distribute the software, either in the form of source code +# or in the form of compiled binaries. +# +from datetime import datetime, timedelta +from dateutil.parser import parse +import json + +from django.conf import settings +from django.contrib.admin.views.decorators import staff_member_required +from django.core import management +from django.core.mail import EmailMessage +from django.http import HttpResponse, HttpResponseServerError +from django.http import HttpResponseNotAllowed, HttpResponseRedirect +from django.shortcuts import render +from django.utils.decorators import method_decorator +from django.utils.translation import gettext_lazy as _ +from django.views import View +from django.views.generic.base import TemplateView + +from freppledb import VERSION +from freppledb.common.report import getCurrency +from freppledb.common.models import Bucket, BucketDetail, Parameter +from freppledb.input.models import ( + Location, + Item, + Customer, + Demand, + ItemSupplier, + ItemDistribution, + Operation, + PurchaseOrder, + Buffer, + Supplier, + ManufacturingOrder, + DistributionOrder, + OperationMaterial, + OperationResource, + Resource, +) + +import logging + +logger = logging.getLogger(__name__) + + +def parseDuration(v): + d = v.strip().split(": ") + args = len(d) + try: + if args == 0: + return timedelta(0) + elif args == 1: + return timedelta(seconds=int(d[0])) + elif args == 2: + return timedelta(minutes=int(d[0]), seconds=int(d[1])) + elif args == 3: + return timedelta(hours=int(d[0]), minutes=int(d[1]), seconds=int(d[2])) + elif args > 3: + return timedelta( + days=int(d[0]), hours=int(d[1]), minutes=int(d[2]), seconds=int(d[3]) + ) + except Exception: + return timedelta(0) + + +@staff_member_required +def Home(request): + return render( + request, + "wizard/index.html", + context={ + "title": _("home"), + "bucketnames": Bucket.objects.order_by("-level").values_list( + "name", flat=True + ), + "currency": getCurrency(), + }, + ) + + +def getWizardSteps(request, mode): + versionnumber = VERSION.split(".", 2) + context = { + "docroot": "%s/docs/%s.%s" + % (settings.DOCUMENTATION_URL, versionnumber[0], versionnumber[1]), + "prefix": request.prefix, + "label_data": 'Data entry', + "label_config": 'Configuration', + "label_action": 'Action', + "label_check": 'Check', + "label_analysis": 'Analyis', + } + + # Possible icons to display on the right hand side + ICON_DONE = "fa-check-square-o" + ICON_AVAILABLE = "fa-square-o" + ICON_LOCK = "fa-lock" + + index = 0 + steps = [] + script = "" + locked = False + done = False + + # Welcome step + welcome = """This wizard will guide you through the steps to load your data and configure up a + first basic planning model.
+
+ Before you start, we need to set some expectations right: +
    +
  1. +

    It will take you considerable time before you get valid planning results + with frePPLe. You will need to read through quite a bit of documentation, + collect quite some data file and more forward by trial and error.

    +
  2. +
  3. +

    This wizard guides you towards a basic model only. The goal is simply to get + you started as quick and easy as possible.
    You will find links to more + advanced features in the bonus section of the wizard.
    +

    +
  4. +
+

Ready to get going? Work towards the goal!

+

New steps unlock only if you complete the previous one.

+ """ + steps.append( + { + "index": index, + "title": "Welcome", + "icon": None, + "locked": False, + "content": welcome.format(**context), + } + ) + index += 1 + + if mode == "production": + # Production master data + done = ( + Item.objects.using(request.database).exists() + and Location.objects.using(request.database).exists() + and Customer.objects.using(request.database).exists() + ) + locked = not done + steps.append( + { + "index": index, + "title": "Step %s: Load master data: items, locations and customers" + % index, + "icon": ICON_DONE if done else ICON_AVAILABLE, + "locked": False, + "content": """ +

It won't be a surprise that we start by loading some basic master data: items, locations and customers.

+

You can either enter some sample records one by one, or (even better) load an Excel + or CSV file you extract from some existing database.

+ + + + + + + + + + + + + + + + + + + + + +
StepScreenshot
+ 1
{label_data} +
+

Load item data +    + + + +    + + + +

+

Load all items: end items sold to customers, intermediate items in the production process and + raw materials purchased from suppliers.

+

Data can be loaded by:
+ A Click on the plus sign to add data records one by one in form.
+ B Edit data directly in the grid.
+ C Click the up arrow icon to import a data file in Excel or CSV format. Have a look + at the sample data to see how your data file should look like. You can even drag and drop your data + file directly on the grid area B.
+ D You can click the down arrow icon to export the existing data as a spreadsheet, + make changes to the spreadsheet and then upload it again with the up arrow icon C.

+
+ +
+ 2
{label_data} +

Load location data +    + + + +    + + + +

+

Load all locations from where items are sold to customers or where inventory is stored.

+
+ +
+ 3
{label_data} +

Load customer data +    + + + +    + + + +

+

Load all customers to which products are sold.

+
+ +
+ """.format( + **context + ), + } + ) + index += 1 + + # Production - sales orders + if not locked: + done = Demand.objects.using(request.database).exists() + steps.append( + { + "index": index, + "title": "Step %s: Load sales order data" % index, + "icon": ICON_LOCK if locked else ICON_DONE if done else ICON_AVAILABLE, + "locked": locked, + "content": """ +

With the master data in place we can now proceed and load the sales order book.

+ + + + + + + + + + + + + + +
StepScreenshot
1
{label_data}
+

Load sales order data +    + + + +    + + + +

+ For planning we only need the open sales orders, the remaining quantity to ship + and the delivery date expected by customers.

+
+ +
+ """.format( + **context + ), + } + ) + index += 1 + if not locked: + locked = not done + + # Production - define production operations + if not locked: + done = ( + Operation.objects.using(request.database).exists() + and OperationMaterial.objects.using(request.database).exists() + ) + steps.append( + { + "index": index, + "title": "Step %s: Define operations and bill of material" % index, + "icon": ICON_LOCK if locked else ICON_DONE if done else ICON_AVAILABLE, + "locked": locked, + "content": """ +

Before moving on please read + this page. + You'll learn how the supply chain network is built up with operations + that are connecting buffers.

+ +

There are 2 common model structures: +

    +
  • +

    + Models with single operation production
    + These models use a single operation to produce an item. +

  • +
  • + + Models with multiple steps per operation
    + The operation of an item is modeled as a sequence of step operations that are grouped + together in a routing. +
  • + + + + + + + + + + + + + + + + + + + +
    StepScreenshot
    1
    {label_data}
    +

    Load operation data +    + + + +    + + + +

    +

    Defines the operations and their duration.

    +

    An operation of type "routing" defines the producion routings. Extra records + defines the step operations and their duration.

    +
    + +
    2
    {label_data}
    +

    Load operation material data +    + + + +    + + + +

    +

    Defines the materials produced and consumed by the operations.

    +
    + +
    + """.format( + **context + ), + } + ) + index += 1 + if not locked: + locked = not done + + # Production - item supplier + if not locked: + done = ItemSupplier.objects.all().using(request.database).exists() + steps.append( + { + "index": index, + "title": "Step %s: Define suppliers and lead time for procured items" + % index, + "icon": ICON_LOCK if locked else ICON_DONE if done else ICON_AVAILABLE, + "locked": locked, + "content": """ +

    In this step you define all suppliers and the lead times for purchasing items from them.

    + + + + + + + + + + + + + + + + + + +
    StepScreenshot
    1
    {label_data}
    +

    Load supplier data +    + + + +    + + + +

    +

    Load all the suppliers from which you can purchase items.

    +
    + +
    2
    {label_data}
    +

    Load item supplier data +    + + + +    + + + +

    +

    In this table you define which item can be purchased from which supplier.

    +
    + +
    + """.format( + **context + ), + } + ) + index += 1 + if not locked: + locked = not done + + # Production - review the network + if not locked: + done = ( + ManufacturingOrder.objects.all() + .using(request.database) + .filter(status="proposed") + .exists() + or PurchaseOrder.objects.all() + .using(request.database) + .filter(status="proposed") + .exists() + ) + steps.append( + { + "index": index, + "title": "Step %s: Review the supply network" % index, + "icon": ICON_LOCK if locked else ICON_DONE if done else ICON_AVAILABLE, + "locked": locked, + "content": """ +

    All right, it's time to for a first checkpoint. We'll verify the supply chain structure + you have modeled in the previous steps.

    + + + + + + + + + + + + + + +
    StepScreenshot
    1
    {label_check}

    Review the supply path of some sales orders

    +

    Go to the sales order list + and click the triangle icon A to investigate some example sales orders.

    +

    Select the "supply path" tab B, and study the graph.

    +

    On the far right you find the end items, and moving towards the left we move to operations + deeper in the bill of material. On the far left we find the raw materials + and their purchasing operations.

    +

    If the path is complete and correct, congratulations! You have successfully + understood and implemented the previous steps.

    +

    If your paths are broken or contain cycles, you will need to review and correct the operations + to get the supply path correct.

    +
    + +

    + +
    + """.format( + **context + ), + } + ) + index += 1 + + # Production - generate unconstrained plan + parameter_populateForecastTable = Parameter.getValue( + "forecast.populateForecastTable", request.database, "true" + ) + steps.append( + { + "index": index, + "title": "Step %s: Generate and review the unconstrained MRP-plan" + % index, + "icon": ICON_LOCK if locked else ICON_DONE if done else ICON_AVAILABLE, + "locked": locked, + "content": """ +

    We'll generate a first unconstrained plan and review the list of proposed manufacturing orders and + purchase orders.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    StepScreenshot
    1
    {label_config}

    Enable or disable the use of forecast:

    +
    + +
    +
    + +
    +

    You can always update your choice later with the parameter "forecast.populateForecastTable" + in the parameter table (available in the "admin" menu).

    +
    2
    {label_action}

    Generate an unconstrained plan

    +

    You can now compute the first plan.

    +

    Open the execution screen (available in the "admin" menu) and select + the "generate plan" task. Make sure the "generate supply plan" option A + is checked, and make sure to generate an unconstrained plan B.

    +

    C Launch the task and wait for it to complete. D

    +

    Whenever you change any of the input data, you will need to come back to this screen to recompute the plan.

    +
    + +
    3
    {label_analysis}
    +

    Load Manufacturing order data

    +

    The manufacturing order screen + (available in the "Manufacturing" menu) gives an overview of all manufacturing orders. + The plan generated in the previous step created a set of proposed manufacturing + orders to deliver your sales orders.

    +

    If the list is empty, it is very likely you made a mistake in any of the + previous steps. You should review the supply path of the sales orders again.

    +

    If the list isn't empty, you can review that the timing, duration and quantity + of the proposed manufacturing orders is matching your expectations. The result will + match a textbook MRP explosion.

    +
    +
    +
    4
    {label_analysis}
    +

    Load purchase order data

    +

    The purchase order report (available in the "purchasing" menu) gives an overview of all + purchase orders. The plan generated in the previous step created a set of + proposed purchase orders to meet your sales orders.

    +

    If the list is empty, it is very likely you made a mistake in any of the + previous steps. You should review the supply path of the sales orders again.

    +

    If the list isn't empty, you can review that the timing, duration and quantity + of the proposed purchase orders is matching your expectations. The result will + match a classic textbook MRP explosion.

    +
    +
    +
    + """.format( + **context + ), + } + ) + index += 1 + if not locked: + locked = not done + + # Production - load inventories, open PO and open MO + if not locked: + done = ( + Buffer.objects.all().using(request.database).exists() + or PurchaseOrder.objects.all() + .using(request.database) + .filter(status="confirmed") + .exists() + or ManufacturingOrder.objects.all() + .using(request.database) + .filter(status="confirmed") + .exists() + ) + steps.append( + { + "index": index, + "title": "Step %s: Load inventories, open purchase orders and work-in-progress manufacturing orders" + % index, + "icon": ICON_AVAILABLE, + "icon": ICON_LOCK if locked else ICON_DONE if done else ICON_AVAILABLE, + "locked": locked, + "content": """ +

    The plan in of the previous steps started with an empty factory and empty inventories. + A correct plan obviously needs to consider the current stock and all purchase orders and + manufacturing orders that are already ongoing or confirmed to start.

    + + + + + + + + + + + + + + + + + + + + + +
    StepScreenshot
    1
    {label_data}
    +

    Load inventory data +    + + + +    + + + +

    +

    Load the current stock of all items. If the stock is 0, no record is required.

    +
    + +
    2
    {label_data}
    +

    Load purchase order data +    + + + +    + + + +

    +

    Load all purchase orders that you have already opened with suppliers.
    + The status field of the records should be "confirmed" to seperate them from the + proposed purchase orders that were generated in the previous step.

    +

    This table is thus used both as input and output.

    +
    + +
    3
    {label_data}
    +

    Load manufacturing order data +    + + + +    + + + +

    +

    Load all manufacturing orders already released to the shop floor as work-in-progress.
    + The status field of the records should be "confirmed" to seperate them from the + proposed manufacturing orders that were generated in the previous step.

    +

    This table is thus used both as input and output.

    +
    + +
    + """.format( + **context + ), + } + ) + index += 1 + + # Production - resources + if not locked: + done = OperationResource.objects.all().using(request.database).exists() + steps.append( + { + "index": index, + "title": "Step %s: Define resources and capacity consumption" % index, + "icon": ICON_LOCK if locked else ICON_DONE if done else ICON_AVAILABLE, + "locked": locked, + "content": """ +

    Let's add some capacity constraints.

    +

    FrePPLe has different resource types (see + here + for more detail). The most common types are:

    +
      +
    • +

      + "default": for a resource with a continuous representation of capacity.
      + This resource model is typically used for short-term detailed planning and scheduling.

      +
    • +
    • +

      + "bucket_week" / "bucket_month": for a resource with capacity is expressed + as available resource-hours per time bucket.
      + This resource model is typically used for mid-term master planning and rough cut + capacity planning.

      +
    • +
    + + + + + + + + + + + + + + + + + +
    StepScreenshot
    1
    {label_data}
    +

    Load resource data +    + + + +    + + + +

    +

    Load all resources.
    + A resource models a machine, a group of machines, an operator, a group of operators, + or other capacity constraints.

    +
    + +
    2
    {label_data}
    +

    Load operation resource data + + + + + + +

    +

    This table associates each operation with the resources it utilizes.

    +
    + +
    + """.format( + **context + ), + } + ) + index += 1 + if not locked: + locked = not done + + # Production - generate plan + if not locked: + done = ( + PurchaseOrder.objects.all() + .using(request.database) + .filter(status="proposed") + .exists() + or ManufacturingOrder.objects.all() + .using(request.database) + .filter(status="proposed") + .exists() + ) + steps.append( + { + "index": index, + "title": "Step %s: Generate a constrained plan" % index, + "icon": ICON_LOCK if locked else ICON_DONE if done else ICON_AVAILABLE, + "locked": locked, + "content": """ +

    You can now generate a more realistic plan.

    +

    The unconstrained plan you generated earlier doesn't respect constraints: it will + plan in the past and overload resources. It does plan all the demands on time.

    +

    The constrained plan generated in this step will respect all the capacity, material + availability, and procurement lead times. In case of lead time or capacity shortages, + demands will be planned late.

    + + + + + + + + + + + + + +
    StepScreenshot
    1
    {label_action}
    +

    Generate constrained plan

    +

    Navigate to the execution screen (available in the "admin" + menu) and select the "generate plan" task A. Make sure the options "generate supply + plan" and "constrained plan" B are both checked.

    +

    C Launch the task and wait for it to complete. D

    +

    Whenever you change any of the input data, you will need to come back here to regenerate the plan.

    +
    + +
    + """.format( + **context + ), + } + ) + index += 1 + + # Production - review results + steps.append( + { + "index": index, + "title": "Step %s: Review results" % index, + "icon": ICON_LOCK if locked else None, + "locked": locked, + "content": """ +

    A number of new screens are ready to be explored now!

    + + + + + + + + + + + + + + + + + + + + + +
    StepScreenshot
    1
    {label_analysis}
    +

    Capacity report

    +

    This report visualizes the utilization of all resources per time bucket.

    +

    A The results can be displayed as a graph or as a table. You can click on + cells in the table or buckets in the graph to get more detailed information.

    +

    B You can adjust the time buckets and horizon of the report.

    +

    C The down arrow icon allows to export the results in an + Excel spreadsheet.

    +

    D In all reports you can customize which fields to display and + their order.

    +
    + +
    2
    {label_analysis}

    Sales order

    +

    At the start of the planning run, you loaded the sales orders in frePPLe. The constrained + planning run you have just completed has 1) computed the planned delivery date for all sales + orders and 2) collected the reasons why a certain demand was planned short or late.

    +

    A Review the list of + sales orders and + sort on the delay field to find some sales orders that can't be delivered on time.

    +

    B Click on the triangle icon next to a demand to drill into its details.

    +

    C The "plan" tab shows all operations planned to deliver the order.

    +

    D The "why short or late" tab shows all constraints causing lateness + in the delivery of the order.

    +
    + +

    + +
    3
    {label_analysis}
    +

    Inventory report

    +

    This report visualizes the planned inventory for all item-locations per time bucket.

    +
    + +
    +

    Congratulations! You are now able to use the production planning capabilities of frePPLe.

    + """.format( + **context + ), + } + ) + index += 1 + + # Production - advanced features + steps.append( + { + "index": index, + "title": "Bonus: Advanced production planning functionality", + "icon": ICON_LOCK if locked else None, + "locked": locked, + "content": """ +

    With the basics under your belt, you are ready to dig into some more advanced + modeling and configuration topics.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    TopicDescription
    Working hoursModeling working hours, shifts and holidays is required to get a realistic plan.
    Operation typesThis example model demonstrates the different operation types.
    Resource typesThis example model demonstrates the different resource types.
    Resource skillsResources can be assigned skills, which represent certain qualifications.
    + You can specify a required skill for an operation.
    Setup matricesResources can require a setup time to change the configuration between different setups/configurations. + This models the time required for cleaning, installation of new tooling, re-calibration, feeding new + raw materials, etc.
    Demand prioritiesDemand priorities give you control over the allocation of constrained supply. + Top priority orders will be the first to get the required material and capacity. + Less prioritized orders are planned with the remaining availability and have + a higher chance of being planned late or short.
    Demand policiesThis model describes how to model demand policies like "ship all in full", "allow + partial deliveries", "don't plan late shipments", etc.
    Release fenceA release fence can be set to specify a frozen zone in the planning horizon in which + the planning algorithm cannot propose any new manufacturing orders, purchase orders or distribution + orders. The fence represents a period during which the plan is already being executed and can no + longer be changed.
    Transfer batchingTransfer batching refers to operations that are planned with some overlap. The subsequent + operation can already start when the previous one hasn't completely finished yet.
    Alternate materialsIn many industries the bill of materials can contain alternate materials: the same product + can be produced using different components.
    + """.format( + **context + ), + } + ) + index += 1 + + # Feedback step + if index > 2: + steps.append( + { + "index": index, + "title": "Give us feedback", + "icon": None, + "content": """ + + + + + + + + + + + + + + + +
    + + + +
    + +
    + +
    +
    + + +
    + """.format( + **context + ), + } + ) + script += ( + ''' + var $textarea = $('#textarea'); + var $submit = $('#submit'); + var $happy = $('#happy'); + var $average = $('#average'); + var $nothappy = $('#nothappy'); + + // Set the onkeyup events + $textarea.on('keyup', function() { + $submit.prop('disabled', $.trim($textarea.val()) === ''); + }); + + // Set default value of smiley to happy (let's be optimistic) + $happy.click( function() { + $happy.css("color", "green"); + $average.css("color", "#DCDCDC"); + $nothappy.css("color", "#DCDCDC"); + }); + $average.click( function() { + $happy.css("color", "#DCDCDC"); + $average.css("color", "orange"); + $nothappy.css("color", "#DCDCDC"); + }); + $nothappy.click( function() { + $happy.css("color", "#DCDCDC"); + $average.css("color", "#DCDCDC"); + $nothappy.css("color", "red"); + }); + + $('#submit').click(function(e) { + $.ajax({ + type: "POST", + url: '/wizard/sendsurveymail/', + data: { + 'feeling': $happy.css("color") == "rgb(0, 128, 0)" ? "Happy" : ($nothappy.css("color") == "rgb(255, 0, 0)" ? "Not happy" : "Average"), + 'comments': $textarea.val() + }, + success: function() { + $textarea.val("Thank you for your comments!"); + $submit.prop('disabled', true); + }, + error: function() { + $textarea.val( + $textarea.val() + + "\\n\\nOuch, our server couldn't send an email. Please email your feedback to info@frepple.com" + ); + } + }); + }); + + $("input[data-parameter]").on('change', function(event) { + var val = $(this).attr("data-parameter-value"); + if (typeof val === typeof undefined || val === false) + val = $(this).val(); + $.ajax({ + type: 'POST', + url: "''' + + request.prefix + + """/api/common/parameter/", + data: { + name: $(this).attr("data-parameter"), + value: val + } + }); + }); + """ + ) + index += 1 + + if steps: + return {"steps": steps, "script": script, "mode": mode} + else: + return None + + +@staff_member_required +def WizardLoad(request, mode=None): + db = request.database + steps = getWizardSteps(request, mode) + with_fcst_module = "freppledb.forecast" in settings.INSTALLED_APPS + with_ip_module = "freppledb.inventoryplanning" in settings.INSTALLED_APPS + if mode == "forecast" and with_fcst_module: + title = _("Data loading wizard for forecasting") + elif mode == "inventory" and with_ip_module: + title = _("Data loading wizard for inventory planning") + elif mode == "production": + title = _("Data loading wizard for production planning") + elif not mode: + title = _("Get started - Data loading wizard") + else: + return HttpResponseServerError("Invalid wizard mode: %s" % mode) + context = { + "prefix": "/" + request.prefix, + "mode": mode, + "wizard": steps, + "with_fcst_module": with_fcst_module, + "with_inventory_module": with_ip_module, + "currentstep": int(request.GET.get("currentstep", 0)), + "title": title, + "bucketnames": Bucket.objects.order_by("-level").values_list("name", flat=True), + "currency": getCurrency(), + } + if steps: + context.update( + { + "noItem": not Item.objects.using(db).exists(), + "noLocation": not Location.objects.using(db).exists(), + "noCustomer": not Customer.objects.using(db).exists(), + "noDemand": not Demand.objects.using(db).exists(), + "noSupplier": not Supplier.objects.using(db).exists(), + "noItemSupplier": not ItemSupplier.objects.using(db).exists(), + "noItemDistribution": not ItemDistribution.objects.using(db).exists(), + "noOperation": not Operation.objects.using(db).exists(), + "noBuffer": not Buffer.objects.using(db).exists(), + "noMO": not ManufacturingOrder.objects.using(db).exists(), + "noMOproposed": not ManufacturingOrder.objects.filter(status="proposed") + .using(db) + .exists(), + "noDO": not DistributionOrder.objects.using(db).exists(), + "noPO": not PurchaseOrder.objects.using(db).exists(), + "noResource": not Resource.objects.using(db).exists(), + "noOperationMaterial": not OperationMaterial.objects.using(db).exists(), + "noOperationResource": not OperationResource.objects.using(db).exists(), + } + ) + if with_ip_module: + context.update( + { + "noIPParameter": not InventoryPlanning.objects.using(db).exists(), + "noSegment": not Segment.objects.using(db).exists(), + "noBusinessRule": not BusinessRule.objects.using(db).exists(), + "noIPOut": not InventoryPlanningOutput.objects.using(db).exists(), + } + ) + if with_fcst_module: + context.update( + {"noForecastPlan": not ForecastPlan.objects.using(db).exists()} + ) + return render(request, "wizard/load.html", context=context) + + +class SendSurveyMail: + @staticmethod + @staff_member_required + def action(request): + # Dispatch to the correct method + try: + if request.method == "POST": + subject = "Survey received from %s : %s" % ( + request.build_absolute_uri()[:-23], + request.POST.get("feeling"), + ) + from_email = settings.DEFAULT_FROM_EMAIL + message_txt = request.POST.get("comments") + email_message = EmailMessage( + subject, message_txt, from_email, ("devops@frepple.com",) + ) + email_message.send() + return HttpResponse("OK") + else: + return HttpResponseNotAllowed(["post"]) + except Exception: + return HttpResponseServerError( + "An error occurred when sending your comments" + ) + + +class QuickStartProduction(View): + @method_decorator(staff_member_required()) + def get(self, request, *args, **kwargs): + post = request.session.get("post", False) + if post: + del request.session["post"] + return render( + request, + "wizard/quickstart_production.html", + context={"title": _("Quickstart production planning"), "post": post}, + ) + + @method_decorator(staff_member_required()) + def post(self, request, *args, **kwargs): + try: + db = request.database + data = json.loads( + request.body.decode(request.encoding or settings.DEFAULT_CHARSET) + ) + post = {"salesorder": data["name"], "messages": []} + + items = 0 + locations = 0 + customers = 0 + suppliers = 0 + resources = 0 + itemsuppliers = 0 + itemdistributions = 0 + demands = 0 + operations = 0 + operationmaterials = 0 + operationresources = 0 + + # Create item + item, created = Item.objects.using(db).get_or_create(name=data["item"]) + if created: + items += 1 + + # Create location + location, created = Location.objects.using(db).get_or_create( + name=data["location"] + ) + if created: + locations += 1 + + # Create customer + customer, created = Customer.objects.using(db).get_or_create( + name=data["customer"] + ) + if created: + customers += 1 + + # Create demand + created = Demand.objects.using(db).get_or_create( + name=data["name"], + defaults={ + "item": item, + "location": location, + "customer": customer, + "due": parse(data["due"]), + "quantity": float(data["quantity"]), + "status": "open", + }, + ) + if created: + demands += 1 + for supply in data["supply"]: + item, created = Item.objects.using(db).get_or_create( + name=supply["item"] + ) + if created: + items += 1 + location, created = Location.objects.using(db).get_or_create( + name=supply["location"] + ) + if created: + locations += 1 + if supply["type"] == "PO": + supplier, created = Supplier.objects.using(db).get_or_create( + name=supply["supplier"] + ) + if created: + suppliers += 1 + created = ItemSupplier.objects.using(db).get_or_create( + supplier=supplier, + item=item, + location=location, + defaults={ + "leadtime": timedelta(days=float(supply["leadtime"])) + }, + )[1] + if created: + itemsuppliers += 1 + elif supply["type"] == "DO": + origin, created = Location.objects.using(db).get_or_create( + name=supply["origin"] + ) + if created: + locations += 1 + created = ItemDistribution.objects.using(db).get_or_create( + item=item, + location=location, + origin=origin, + defaults={ + "leadtime": timedelta(days=float(supply["leadtime"])) + }, + )[1] + if created: + itemdistributions += 1 + elif supply["type"] == "MO": + operation, created = Operation.objects.using(db).get_or_create( + name=supply["operation"], + defaults={ + "item": item, + "location": location, + "duration": parseDuration(supply["duration"]), + "duration_per": parseDuration(supply["durationper"]), + "type": "time_per", + }, + ) + if created: + operations += 1 + if supply.get("resource", None): + resource, created = Resource.objects.using(db).get_or_create( + name=supply["resource"] + ) + if created: + resources += 1 + created = OperationResource.objects.using(db).get_or_create( + resource=resource, + operation=operation, + defaults={"quantity": 1}, + ) + if created: + operationresources += 1 + consumedindex = 0 + while True: + key = "consumeditem-%s" % consumedindex + if key not in supply: + break + if supply[key]: + item, created = Item.objects.using(db).get_or_create( + name=supply[key] + ) + if created: + items += 1 + try: + qty = -float( + supply["consumedquantity-%s" % consumedindex] + ) + except Exception: + qty = -1 + created = OperationMaterial.objects.using(db).get_or_create( + item=item, + operation=operation, + defaults={"quantity": qty, "type": "start"}, + )[1] + if created: + operationmaterials += 1 + consumedindex += 1 + + # Compile the messages + if items > 0: + post["messages"].append( + "Created %s new item" + % (items, request.prefix) + ) + if locations > 0: + post["messages"].append( + "Created %s new location" + % (locations, request.prefix) + ) + if customers > 0: + post["messages"].append( + "Created %d new customer" + % (customers, request.prefix) + ) + if demands > 0: + post["messages"].append( + "Created %d new sales order" + % (demands, request.prefix) + ) + if suppliers > 0: + post["messages"].append( + "Created %d new supplier" + % (suppliers, request.prefix) + ) + if itemsuppliers > 0: + post["messages"].append( + "Created %d new item supplier" + % (itemsuppliers, request.prefix) + ) + if itemdistributions > 0: + post["messages"].append( + "Created %d new item distribution" + % (itemdistributions, request.prefix) + ) + if resources > 0: + post["messages"].append( + "Created %d new resource" + % (resources, request.prefix) + ) + if operations > 0: + post["messages"].append( + "Created %d new operation" + % (operations, request.prefix) + ) + if operationresources > 0: + post["messages"].append( + "Created %d new operation resource" + % (operationresources, request.prefix) + ) + if operationmaterials > 0: + post["messages"].append( + "Created %d new operation material" + % (operationmaterials, request.prefix) + ) + + # Generate the plan + management.call_command( + "runplan", + database=request.database, + env="fcst,supply", + constraint=13, + background=True, + ) + post["messages"].append( + "Generated the plan" + % request.prefix + ) + + # Leave feedback messages on the session + request.session["post"] = post + return HttpResponse(content="OK") + + except Exception as e: + logger.error("Error creating supply path: %s" % e) + post["messages"] = ["Error creating the supply path: %s" % e] + request.session["post"] = post + return HttpResponseServerError("Error creating supply path") + + +class SendUsDataset(TemplateView): + template_name = "wizard/send_us_dataset.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = "Get started - Send us a dataset" + return context + + +class Odoo(TemplateView): + template_name = "wizard/odoo.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["title"] = "Get started - Connect to your Odoo" + return context diff --git a/include/Makefile.am b/include/Makefile.am index 2b454d8fd8..ad51607a80 100644 --- a/include/Makefile.am +++ b/include/Makefile.am @@ -5,5 +5,3 @@ SUBDIRS = frepple include_HEADERS = frepple.h freppleinterface.h - - diff --git a/include/frepple/timeline.h b/include/frepple/timeline.h index c53fb020c5..39a5177136 100644 --- a/include/frepple/timeline.h +++ b/include/frepple/timeline.h @@ -28,12 +28,12 @@ DECLARE_EXPORT extern PythonType* EventPythonType; -/** @brief This class implements a "sorted list" data structure, sorting +/* This class implements a "sorted list" data structure, sorting * "events" based on a date. * - * The data structure has slow insert scalability: O(n)
    - * Moving data around in the structure is efficient though: O(1)
    - * The class leverages the STL library and also follows its api.
    + * The data structure has slow insert scalability: O(n) + * Moving data around in the structure is efficient though: O(1) + * The class leverages the STL library and also follows its api. * The class used to instantiate a timeline must support the * "bool operator < (TYPE)". * @@ -49,7 +49,7 @@ class TimeLine { public: class iterator; class const_iterator; - /** @brief Base class for nodes in the timeline. */ + /* Base class for nodes in the timeline. */ class Event : public NonCopyable, public Object { friend class TimeLine; friend class const_iterator; @@ -68,10 +68,10 @@ class TimeLine { public: virtual ~Event(){}; - /** Default constructor. */ + /* Default constructor. */ Event() : tp(0), qty(0) {} - /** Return the event type: + /* Return the event type: * - 0: null event, don't use * - 1: change on hand * - 2: set on hand @@ -81,28 +81,28 @@ class TimeLine { */ inline unsigned short getEventType() const { return tp; } - /** Return the owning operationplan. */ + /* Return the owning operationplan. */ virtual OperationPlan* getOperationPlan() const = 0; - /** Return the quantity. */ + /* Return the quantity. */ inline double getQuantity() const { return qty; } - /** Return the current onhand value. */ + /* Return the current onhand value. */ inline double getOnhand() const { return oh; } - /** Verify whether the next event is on the same date or not. */ + /* Verify whether the next event is on the same date or not. */ inline bool isLastOnDate() const { return next ? (next->dt != dt) : true; } - /** Verify whether the previous event is on the same date or not. */ + /* Verify whether the previous event is on the same date or not. */ inline bool isFirstOnDate() const { return prev ? (prev->dt != dt) : true; } - /** Return true if there is no other event at the same date. */ + /* Return true if there is no other event at the same date. */ inline bool isOnlyEventOnDate() const { return (!next || next->getDate() != getDate()) && (!prev || prev->getDate() != getDate()); } - /** Return the onhand before this date. */ + /* Return the onhand before this date. */ inline double getOnhandBeforeDate() const { const Event* tmp = this; while (tmp && tmp->dt == dt) { @@ -114,26 +114,26 @@ class TimeLine { return tmp->oh; } - /** Return the onhand after this date. */ + /* Return the onhand after this date. */ inline double getOnhandAfterDate() const { const Event* tmp = this; while (tmp->next && tmp->next->dt == dt) tmp = tmp->next; return tmp ? tmp->oh : oh; } - /** Return the total produced quantity till the current date. */ + /* Return the total produced quantity till the current date. */ inline double getCumulativeProduced() const { return cum_prod; } - /** Return the total consumed quantity till the current date. */ + /* Return the total consumed quantity till the current date. */ inline double getCumulativeConsumed() const { return cum_prod - oh; } - /** Return the date of the event. */ + /* Return the date of the event. */ inline Date getDate() const { return dt; } - /** Return a pointer to the owning timeline. */ + /* Return a pointer to the owning timeline. */ virtual TimeLine* getTimeLine() const { return nullptr; } - /** These functions return the minimum boundary valid at the time of + /* These functions return the minimum boundary valid at the time of * this event. */ double getMin() const { return getMin(true); } @@ -146,7 +146,7 @@ class TimeLine { return m ? m->newMin : 0.0; } - /** This functions return the maximum boundary valid at the time of + /* This functions return the maximum boundary valid at the time of * this event. */ double getMax() const { return getMax(true); } @@ -159,10 +159,10 @@ class TimeLine { return m ? m->newMax : 0.0; } - /** First criterion is date: earlier dates come first.
    - * Second criterion is the size: big events come first.
    - * As a third tie-breaking criterion, we use a pointer comparison.
    - * This garantuees us a fixed and unambiguous ordering.
    + /* First criterion is date: earlier dates come first. + * Second criterion is the size: big events come first. + * As a third tie-breaking criterion, we use a pointer comparison. + * This garantuees us a fixed and unambiguous ordering. * As a side effect, this makes sure that producers come before * consumers. This feature is required to avoid zero-time * material shortages. @@ -170,7 +170,7 @@ class TimeLine { bool operator<(const Event& fl2) const; }; - /** @brief A timeline event representing a change of the current value. */ + /* A timeline event representing a change of the current value. */ class EventChangeOnhand : public Event { friend class TimeLine; @@ -178,7 +178,7 @@ class TimeLine { EventChangeOnhand(double qty = 0.0) : Event(1, qty) {} }; - /** @brief A timeline event representing a change of the current value. */ + /* A timeline event representing a change of the current value. */ class EventSetOnhand : public Event { friend class TimeLine; @@ -197,7 +197,7 @@ class TimeLine { virtual OperationPlan* getOperationPlan() const { return nullptr; } }; - /** @brief A timeline event representing a change of the minimum target. */ + /* A timeline event representing a change of the minimum target. */ class EventMinQuantity : public Event { friend class TimeLine; friend class Event; @@ -230,7 +230,7 @@ class TimeLine { virtual OperationPlan* getOperationPlan() const { return nullptr; } }; - /** @brief A timeline event representing a change of the maximum target. */ + /* A timeline event representing a change of the maximum target. */ class EventMaxQuantity : public Event { friend class Event; friend class TimeLine; @@ -263,7 +263,7 @@ class TimeLine { virtual OperationPlan* getOperationPlan() const { return nullptr; } }; - /** @brief This is bi-directional iterator through the timeline. */ + /* This is bi-directional iterator through the timeline. */ class const_iterator { protected: const Event* cur; @@ -314,7 +314,7 @@ class TimeLine { bool operator!=(const const_iterator& x) const { return cur != x.cur; } }; - /** @brief This is bi-directional iterator through the timeline. */ + /* This is bi-directional iterator through the timeline. */ class iterator : public const_iterator { public: iterator() {} @@ -380,25 +380,25 @@ class TimeLine { void insert(Event*); - /** Insert an onhandchange event in the timeline. */ + /* Insert an onhandchange event in the timeline. */ void insert(EventChangeOnhand* e, double qty, const Date& d) { e->qty = qty; e->dt = d; insert(static_cast(e)); }; - /** Remove an event from the timeline. */ + /* Remove an event from the timeline. */ void erase(Event*); - /** Update the timeline to move an event to a new date and quantity. */ + /* Update the timeline to move an event to a new date and quantity. */ void update(EventChangeOnhand*, double, const Date&); - /** Update the timeline to move an event to a new date and quantity. + /* Update the timeline to move an event to a new date and quantity. * This method can only be used for events with quantity 0. */ void update(Event*, const Date&); - /** This functions returns the mimimum valid at a certain date. */ + /* This functions returns the mimimum valid at a certain date. */ virtual double getMin(Date d, bool inclusive = true) const { EventMinQuantity* m = this->lastMin; if (inclusive) @@ -408,7 +408,7 @@ class TimeLine { return m ? m->getMin() : 0.0; } - /** This functions returns the minimum valid at a certain event. */ + /* This functions returns the minimum valid at a certain event. */ virtual double getMin(const Event* e, bool inclusive = true) const { if (!e) return 0.0; EventMinQuantity* m = this->lastMin; @@ -419,7 +419,7 @@ class TimeLine { return m ? m->getMin() : 0.0; } - /** This functions returns the maximum valid at a certain date. */ + /* This functions returns the maximum valid at a certain date. */ virtual double getMax(Date d, bool inclusive = true) const { EventMaxQuantity* m = this->lastMax; if (inclusive) @@ -429,7 +429,7 @@ class TimeLine { return m ? m->getMax() : 0.0; } - /** This functions returns the minimum valid at a certain event. */ + /* This functions returns the minimum valid at a certain event. */ virtual double getMax(const Event* e, bool inclusive = true) const { if (!e) return 0.0; EventMaxQuantity* m = this->lastMax; @@ -440,7 +440,7 @@ class TimeLine { return m ? m->getMax() : 0.0; } - /** This functions returns the minimum event valid at a certain date. */ + /* This functions returns the minimum event valid at a certain date. */ virtual EventMinQuantity* getMinEvent(Date d, bool inclusive = true) const { EventMinQuantity* m = this->lastMin; if (inclusive) @@ -450,7 +450,7 @@ class TimeLine { return m ? m : nullptr; } - /** This functions returns the maximum event valid at a certain date. */ + /* This functions returns the maximum event valid at a certain date. */ virtual EventMaxQuantity* getMaxEvent(Date d, bool inclusive = true) const { EventMaxQuantity* m = this->lastMax; if (inclusive) @@ -460,10 +460,10 @@ class TimeLine { return m ? m : nullptr; } - /** Return the lowest excess inventory level between this event - * and the end of the horizon.
    + /* Return the lowest excess inventory level between this event + * and the end of the horizon. * If the boolean argument is true, excess is defined as the difference - * between the onhand level and the minimum stock level.
    + * between the onhand level and the minimum stock level. * If the boolean argument is false, excess is defined as the onhand level. */ double getExcess(const Event* curevent, @@ -483,7 +483,7 @@ class TimeLine { return excess; } - /** Return the total production or consumption between 2 events. */ + /* Return the total production or consumption between 2 events. */ double getFlow(const Event* strt, const Event* nd, bool consumed) const { double total = 0.0; for (const_iterator cur(strt); cur != end() && &*cur != nd; ++cur) { @@ -495,7 +495,7 @@ class TimeLine { return total; } - /** Return the total production or consumption between an event. */ + /* Return the total production or consumption between an event. */ double getFlow(const Event* strt, Duration prd, bool consumed) const { Date nd = strt->getDate() + prd; double total = 0.0; @@ -509,23 +509,23 @@ class TimeLine { return total; } - /** This function is used to trace the consistency of the data structure. */ + /* This function is used to trace the consistency of the data structure. */ bool check() const; private: - /** A pointer to the first event in the timeline. */ + /* A pointer to the first event in the timeline. */ Event* first = nullptr; - /** A pointer to the last event in the timeline. */ + /* A pointer to the last event in the timeline. */ Event* last = nullptr; - /** A pointer to the last maximum change. */ + /* A pointer to the last maximum change. */ EventMaxQuantity* lastMax = nullptr; - /** A pointer to the last minimum change. */ + /* A pointer to the last minimum change. */ EventMinQuantity* lastMin = nullptr; - /** A pointer to the last fixed onhand. */ + /* A pointer to the last fixed onhand. */ EventSetOnhand* lastSet = nullptr; }; diff --git a/src/main.cpp b/src/main.cpp index e29a21077b..cad0f5bd4e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,10 +19,12 @@ ***************************************************************************/ #include + #include #include #include #include + #include "freppleinterface.h" using namespace std;