diff --git a/shopfloor/actions/data.py b/shopfloor/actions/data.py
index 09ead326eb..b5f3e30622 100644
--- a/shopfloor/actions/data.py
+++ b/shopfloor/actions/data.py
@@ -159,6 +159,7 @@ def _package_level_parser(self):
return [
"id",
"is_done",
+ ("picking_id:picking", self._simple_record_parser()),
("package_id:package_src", self._package_parser),
("location_dest_id:location_dest", self._location_parser),
(
diff --git a/shopfloor/services/schema.py b/shopfloor/services/schema.py
index c7d78c39e3..ac610977ad 100644
--- a/shopfloor/services/schema.py
+++ b/shopfloor/services/schema.py
@@ -157,6 +157,7 @@ def package_level(self):
return {
"id": {"required": True, "type": "integer"},
"is_done": {"type": "boolean", "nullable": False, "required": True},
+ "picking": self._schema_dict_of(self._simple_record()),
"package_src": self._schema_dict_of(self.package()),
"location_src": self._schema_dict_of(self.location()),
"location_dest": self._schema_dict_of(self.location()),
diff --git a/shopfloor/tests/test_actions_data.py b/shopfloor/tests/test_actions_data.py
index 4be811ac45..fbd1942025 100644
--- a/shopfloor/tests/test_actions_data.py
+++ b/shopfloor/tests/test_actions_data.py
@@ -149,6 +149,7 @@ def test_data_package_level(self):
expected = {
"id": package_level.id,
"is_done": False,
+ "picking": self.picking.jsonify(["id", "name"])[0],
"package_src": self._expected_package(package_level.package_id),
"location_dest": self._expected_location(package_level.location_dest_id),
"location_src": self._expected_location(
diff --git a/shopfloor_mobile/static/wms/index.html b/shopfloor_mobile/static/wms/index.html
index 3b69e035ee..c480de64d4 100644
--- a/shopfloor_mobile/static/wms/index.html
+++ b/shopfloor_mobile/static/wms/index.html
@@ -36,7 +36,6 @@
-
@@ -72,6 +71,9 @@
+
+
+
diff --git a/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.js b/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.js
index a4bb43ee82..11e32ded6e 100644
--- a/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.js
+++ b/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.js
@@ -1,3 +1,4 @@
+// TODO: rename as we used this not only for batch picking
export var batch_picking_line = Vue.component("batch-picking-line-detail", {
props: {
line: Object,
@@ -9,6 +10,10 @@ export var batch_picking_line = Vue.component("batch-picking-line-detail", {
type: Boolean,
default: false,
},
+ defaultDestinationKey: {
+ type: String,
+ default: "package_dest",
+ },
},
data() {
return {
@@ -16,19 +21,8 @@ export var batch_picking_line = Vue.component("batch-picking-line-detail", {
};
},
computed: {
- has_destination_pack() {
- return _.result(this.line, "package_dest.id");
- },
- },
- methods: {
- full_detail_fields() {
- return [
- {path: "batch.name", label: "Batch"},
- {path: "picking.name", label: "Picking"},
- {path: "picking.origin", label: "Origin"},
- {path: "picking.partner.name", label: "Customer"},
- {path: "location_dest.name", label: "Destination"},
- ];
+ destination() {
+ return _.result(this.line, this.$props.defaultDestinationKey);
},
},
template: `
@@ -48,11 +42,11 @@ export var batch_picking_line = Vue.component("batch-picking-line-detail", {
/>
@@ -60,36 +54,22 @@ export var batch_picking_line = Vue.component("batch-picking-line-detail", {
- Destination pack not selected.
+ Destination not selected.
-
-
`,
});
+// TODO: use `misc.line-actions-popup` instead
export var batch_picking_line_actions = Vue.component("batch-picking-line-actions", {
props: ["line"],
data() {
@@ -149,32 +129,3 @@ export var batch_picking_line_actions = Vue.component("batch-picking-line-action
`,
});
-
-export var batch_picking_line_stock_out = Vue.component(
- "batch-picking-line-stock-out",
- {
- props: ["line"],
- methods: {
- handle_action(action) {
- this.$emit("action", action);
- },
- },
- template: `
-
+
- {{ info.title }}
+ {{ title }}
`,
@@ -266,3 +271,100 @@ Vue.component("btn-back", {
`,
});
+
+Vue.component("btn-reset-config", {
+ props: {
+ redirect: {
+ type: Object,
+ default: function() {
+ return {name: "home"};
+ },
+ },
+ },
+ methods: {
+ reset_data: function() {
+ this.$root._clearConfig();
+ this.$root._loadMenu();
+ this.$root.$router.push(this.$props.redirect);
+ },
+ },
+ template: `
Reload config and menu`,
+});
+
+Vue.component("line-actions-popup", {
+ props: {
+ line: {
+ type: Object,
+ },
+ actions: {
+ type: Array,
+ default: function() {
+ return [];
+ },
+ },
+ },
+ data() {
+ return {
+ dialog: false,
+ };
+ },
+ methods: {
+ handle_action(action) {
+ this.dialog = false;
+ this.$emit("action", action);
+ },
+ },
+ template: `
+
+
+
+
+
+
+ Action
+
+
+
+
+
+
+
+
+ {{ action.name }}
+
+
+
+
+ Back
+
+
+
+
+
+
+`,
+});
+
+Vue.component("line-stock-out", {
+ methods: {
+ handle_action(action) {
+ this.$emit(action);
+ },
+ },
+ template: `
+
+
+
+
+ Confirm stock = 0
+
+
+
+
+
+
+
+
+
+`,
+});
diff --git a/shopfloor_mobile/static/wms/src/components/screen.js b/shopfloor_mobile/static/wms/src/components/screen.js
index f6e54eb0e3..092b7e678f 100644
--- a/shopfloor_mobile/static/wms/src/components/screen.js
+++ b/shopfloor_mobile/static/wms/src/components/screen.js
@@ -49,7 +49,6 @@ Vue.component("Screen", {
},
show_profile_not_ready() {
return (
- !this.$root.demo_mode &&
!this.$root.has_profile &&
this.$route.name != "profile" &&
this.$route.name != "settings"
diff --git a/shopfloor_mobile/static/wms/src/components/userInformation/userInformation.js b/shopfloor_mobile/static/wms/src/components/userInformation/userInformation.js
index 4630626390..b37e943f9d 100644
--- a/shopfloor_mobile/static/wms/src/components/userInformation/userInformation.js
+++ b/shopfloor_mobile/static/wms/src/components/userInformation/userInformation.js
@@ -3,12 +3,12 @@ Vue.component("user-information", {
props: ["message"],
template: `
- {{ message.body }}
+ {{ _.result(message, "body") }}
`,
computed: {
alert_type: function() {
- return this.message.message_type || "info";
+ return _.result(this.message, "message_type", "info");
},
},
});
diff --git a/shopfloor_mobile/static/wms/src/demo/demo.core.js b/shopfloor_mobile/static/wms/src/demo/demo.core.js
index 1eb385a60b..97566e41f3 100644
--- a/shopfloor_mobile/static/wms/src/demo/demo.core.js
+++ b/shopfloor_mobile/static/wms/src/demo/demo.core.js
@@ -3,7 +3,10 @@ import {process_registry} from "../services/process_registry.js";
export class DemoTools {
constructor(demo_cases) {
this.demo_cases = demo_cases.length ? demo_cases : {};
- // used to search w/ scan anything
+ // Used to collect fake menu entries
+ this.app_menus_by_id = {};
+ this.app_menus = [];
+ // Used to search w/ scan anything
this.indexed = {};
this.locations = [
this.makeLocation(),
@@ -398,6 +401,19 @@ export class DemoTools {
return _.head(this.makePickingLines({}, {picking_auto: true, lines_count: 1}));
}
+ makePackageLevel(defaults = {}, options = {}) {
+ _.defaults(defaults, {
+ is_done: false,
+ picking: this.makePicking(),
+ package: this.makePack(),
+ product: this.makeProduct(),
+ location: this.makeLocation(),
+ // TODO: shall we add move lines to be able to show product details?
+ });
+ const rec = this.makeSimpleRecord(defaults, options);
+ return rec;
+ }
+
partnerNames() {
return [
"Edith Sanchez",
@@ -438,19 +454,55 @@ export class DemoTools {
];
}
- makeAppMenu() {
- let menu;
- _.forEach(process_registry.all(), function(component, key) {
- menu.push({
- id: key,
- process: {
- id: key,
- code: key,
- },
- });
+ makeAppConfig() {
+ return {
+ profiles: this.makeProfiles(),
+ };
+ }
+ makeProfiles() {
+ const profiles = [
+ {id: 1, name: "SCH Transport", warehouse: {id: 1, name: "Schlieren"}},
+ {id: 2, name: "SCH Pick", warehouse: {id: 1, name: "Schlieren"}},
+ ];
+ return profiles;
+ }
+ getAppMenus() {
+ return this.app_menus;
+ }
+ /**
+ *
+ * @param {*} new_item
+ *
+ * id: 123,
+ * name: "Menu 1",
+ * scenario: "scenario_usage_key",
+ * picking_types: [{"id": 27, "name": "Random type"}]
+ */
+ addAppMenu(new_item) {
+ // Generate a unique ID to be used in demo data
+ let menu_id = this.getRandomInt();
+ while (menu_id in this.app_menus_by_id) {
+ menu_id = this.getRandomInt();
+ }
+ new_item.id = menu_id;
+ this.app_menus_by_id[menu_id] = new_item;
+
+ // Find insert index to keep items w/ same scenario grouped
+ let index = 0;
+ this.app_menus.every(function(item, i) {
+ if (item.scenario == new_item.scenario) {
+ index = i;
+ return false;
+ }
});
- return menu;
+ if (_.isUndefined(new_item.picking_types)) {
+ new_item.picking_types = [{id: 99999999, name: "Fake type"}];
+ }
+ this.app_menus.splice(index, 0, new_item);
+ // Return the unique id
+ return menu_id;
}
+
/*
Detect cyclic references between objects.
diff --git a/shopfloor_mobile/static/wms/src/demo/demo.delivery.js b/shopfloor_mobile/static/wms/src/demo/demo.delivery.js
index 12d0ae9a6d..77f1360479 100644
--- a/shopfloor_mobile/static/wms/src/demo/demo.delivery.js
+++ b/shopfloor_mobile/static/wms/src/demo/demo.delivery.js
@@ -1,9 +1,5 @@
import {demotools} from "./demo.core.js";
-const deliver_data = {
- picking: demotools.makePicking({}, {lines_count: 5, line_random_pack: true}),
-};
-
let pickings = [];
const count = 8;
diff --git a/shopfloor_mobile/static/wms/src/demo/demo.location_content_transfer.js b/shopfloor_mobile/static/wms/src/demo/demo.location_content_transfer.js
new file mode 100644
index 0000000000..606d7a91bd
--- /dev/null
+++ b/shopfloor_mobile/static/wms/src/demo/demo.location_content_transfer.js
@@ -0,0 +1,170 @@
+import {demotools} from "./demo.core.js";
+
+const DEMO_CASE = {
+ by_menu_id: {},
+};
+
+// Case for recover existing work on single move line
+const recover_single_move_line_menu_id = demotools.addAppMenu({
+ name: "Loc.Cont.Transfer: recover single line",
+ scenario: "location_content_transfer",
+ picking_types: [{id: 27, name: "Random type"}],
+});
+const single_line_case_move_line = demotools.makeSingleLineOperation();
+const RECOVER_SINGLE_MOVE_LINE_CASE = {
+ start_or_recover: {
+ next_state: "start_single",
+ message: {
+ message_type: "info",
+ body: "Recovered line from previous session.",
+ },
+ data: {
+ start_single: {
+ recovered: true,
+ move_line: _.cloneDeep(single_line_case_move_line),
+ },
+ },
+ },
+ scan_line: {
+ next_state: "scan_destination",
+ message: {
+ message_type: "info",
+ body: "Recovered line from previous session.",
+ },
+ data: {
+ scan_destination: {
+ move_line: _.cloneDeep(single_line_case_move_line),
+ },
+ },
+ },
+ set_destination_line: {
+ next_state: "start_single",
+ message: {
+ message_type: "info",
+ body: "Destination set on the line",
+ },
+ data: {
+ start_single: {
+ move_line: _.cloneDeep(single_line_case_move_line),
+ },
+ },
+ },
+};
+DEMO_CASE.by_menu_id[recover_single_move_line_menu_id] = RECOVER_SINGLE_MOVE_LINE_CASE;
+
+// Case for recover existing work on single package level
+const recover_single_package_level_menu_id = demotools.addAppMenu({
+ name: "Loc.Cont.Transfer: recover single package level",
+ scenario: "location_content_transfer",
+ picking_types: [{id: 27, name: "Random type"}],
+});
+
+const single_line_package_level = demotools.makePackageLevel();
+const RECOVER_SINGLE_PACKAGE_LEVEL_CASE = {
+ start_or_recover: {
+ next_state: "start_single",
+ message: {
+ message_type: "info",
+ body: "Recovered package from previous session.",
+ },
+ data: {
+ start_single: {
+ recovered: true,
+ package_level: _.cloneDeep(single_line_package_level),
+ },
+ },
+ },
+ scan_line: {
+ next_state: "scan_destination",
+ message: {
+ message_type: "info",
+ body: "Recovered package from previous session.",
+ },
+ data: {
+ scan_destination: {
+ package_level: _.cloneDeep(single_line_package_level),
+ },
+ },
+ },
+ set_destination_line: {
+ next_state: "start_single",
+ message: {
+ message_type: "info",
+ body: "Destination set on the line",
+ },
+ data: {
+ start_single: {
+ package_level: _.cloneDeep(single_line_package_level),
+ },
+ },
+ },
+};
+DEMO_CASE.by_menu_id[
+ recover_single_package_level_menu_id
+] = RECOVER_SINGLE_PACKAGE_LEVEL_CASE;
+
+// Case for recover existing work on several lines from the same location
+const recover_move_lines_same_location_menu_id = demotools.addAppMenu({
+ name: "Loc.Cont.Transfer: recover lines same loc.",
+ scenario: "location_content_transfer",
+ picking_types: [{id: 27, name: "Random type"}],
+});
+const same_location = demotools.makeLocation();
+const scan_destination_all_move_lines = demotools.makePickingLines(
+ {},
+ {lines_count: demotools.getRandomInt(5), line_random_pack: true}
+);
+for (let i = 0; i < scan_destination_all_move_lines.length; i++) {
+ // set same location (clone to avoid circular dep on storage)
+ scan_destination_all_move_lines[i].location_src = _.cloneDeep(same_location);
+}
+const RECOVER_MOVE_LINES_SAME_LOCATION_CASE = {
+ start_or_recover: {
+ next_state: "scan_destination_all",
+ message: {
+ message_type: "info",
+ body: "Recovered lines from previous session.",
+ },
+ data: {
+ scan_destination_all: {
+ recovered: true,
+ move_lines: _.cloneDeep(scan_destination_all_move_lines),
+ },
+ },
+ },
+ scan_line: {
+ next_state: "scan_destination",
+ message: {
+ message_type: "info",
+ body: "Recovered line from previous session.",
+ },
+ data: {
+ scan_destination: {
+ move_lines: _.cloneDeep(scan_destination_all_move_lines),
+ },
+ },
+ },
+ set_destination_all: {
+ next_state: "start",
+ message: {
+ message_type: "info",
+ body: "Destination set on all lines",
+ },
+ data: {
+ start: {},
+ },
+ },
+ go_to_single: {
+ next_state: "start_single",
+ data: {
+ start_single: {
+ move_line: _.cloneDeep(scan_destination_all_move_lines[0]),
+ },
+ },
+ },
+};
+DEMO_CASE.by_menu_id[
+ recover_move_lines_same_location_menu_id
+] = RECOVER_MOVE_LINES_SAME_LOCATION_CASE;
+
+demotools.add_case("location_content_transfer", DEMO_CASE);
diff --git a/shopfloor_mobile/static/wms/src/homepage.js b/shopfloor_mobile/static/wms/src/homepage.js
index f23dcc623e..d8e598ac7f 100644
--- a/shopfloor_mobile/static/wms/src/homepage.js
+++ b/shopfloor_mobile/static/wms/src/homepage.js
@@ -4,18 +4,32 @@ export var HomePage = Vue.component("home-page", {
return this.$root.appmenu.menus;
},
},
+ methods: {
+ nuke_data_and_reload: function() {
+ // Force reload config (loads profiles)
+ this.$root.loadConfig(true);
+ // Enforce 1st profile
+ const selected_profile = _.head(this.$root.appconfig.profiles);
+ this.$root.trigger("profile:selected", selected_profile, true);
+ // Reload page
+ location.reload();
+ },
+ },
props: ["routes"],
template: `
-
+
DEMO MODE ON
+
+
+
+ Force reload data and refresh
+
+
+
`,
});
diff --git a/shopfloor_mobile/static/wms/src/i18n.translation.js b/shopfloor_mobile/static/wms/src/i18n.translation.js
index 087e89e932..2d5a688844 100644
--- a/shopfloor_mobile/static/wms/src/i18n.translation.js
+++ b/shopfloor_mobile/static/wms/src/i18n.translation.js
@@ -2,6 +2,9 @@ export const messages = {
// {{ $t('message.hello') }}
"en-US": {
screen: {
+ home: {
+ title: "Barcode scanner",
+ },
scan_anything: {
title: "Scan {what}",
},
@@ -41,6 +44,9 @@ export const messages = {
},
"fr-FR": {
screen: {
+ home: {
+ title: "Barcode scanner",
+ },
scan_anything: {
name: "Scanner",
title: "Scanner {what}",
@@ -81,6 +87,9 @@ export const messages = {
},
"de-DE": {
screen: {
+ home: {
+ title: "Barcode scanner",
+ },
scan_anything: {
name: "Gescannt",
title: "Gescannt {what}",
diff --git a/shopfloor_mobile/static/wms/src/main.js b/shopfloor_mobile/static/wms/src/main.js
index 38516cf2b7..8f44cc640d 100644
--- a/shopfloor_mobile/static/wms/src/main.js
+++ b/shopfloor_mobile/static/wms/src/main.js
@@ -1,7 +1,6 @@
import {router} from "./router.js";
import {i18n} from "./i18n.js";
import {GlobalMixin} from "./mixin.js";
-import {Config} from "./services/config.js";
import {process_registry} from "./services/process_registry.js";
import {color_registry} from "./services/color_registry.js";
import {Odoo, OdooMocked} from "./services/odoo.js";
@@ -81,9 +80,6 @@ const app = new Vue({
this.$storage.set("profile", v);
},
},
- // TODO: demo mode is half working now because
- // if we don't load real menu/profiles 1st
- // we don't have any menu nor profile to play with.
profiles: function() {
return this.appconfig ? this.appconfig.profiles || [] : [];
},
@@ -126,8 +122,8 @@ const app = new Vue({
}
return new OdooClass(params);
},
- loadConfig: function() {
- if (this.appconfig) {
+ loadConfig: function(force) {
+ if (this.appconfig && !force) {
return this.appconfig;
}
// TODO: we can do this via watcher
@@ -145,13 +141,15 @@ const app = new Vue({
const self = this;
self.loading = true;
const odoo = self.getOdoo({usage: "app"});
- const config = new Config(odoo);
- return config.load().then(function() {
- if (config.authenticated) {
- self.appconfig = config.data;
- self.authenticated = config.authenticated;
+ return odoo.call("user_config").then(function(result) {
+ if (!_.isUndefined(result.data)) {
+ self.appconfig = result.data;
+ self.authenticated = true;
self.$storage.set("appconfig", self.appconfig);
self.loading = false;
+ } else {
+ // TODO: any better thing to do here?
+ console.log(result);
}
});
},
@@ -178,6 +176,10 @@ const app = new Vue({
self.loading = false;
});
},
+ _clearConfig: function() {
+ this.$storage.remove("appconfig");
+ return this._loadConfig();
+ },
logout: function() {
this.apikey = "";
this.authenticated = false;
diff --git a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js
index 870cf3e79f..1ad0cfebf0 100644
--- a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js
+++ b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js
@@ -25,7 +25,7 @@ export var ClusterPicking = Vue.component("cluster-picking", {
v-on:cancel="state.on_cancel"
/>
-
diff --git a/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js b/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js
new file mode 100644
index 0000000000..905df4954a
--- /dev/null
+++ b/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js
@@ -0,0 +1,323 @@
+import {ScenarioBaseMixin} from "./mixins.js";
+import {process_registry} from "../services/process_registry.js";
+import {demotools} from "../demo/demo.core.js"; // FIXME: dev only
+
+export var LocationContentTransfer = Vue.component("location-content-transfer", {
+ mixins: [ScenarioBaseMixin],
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Split by line
+
+
+
+
+ `,
+ // FIXME: just for dev
+ // mounted: function() {
+ // // TEST force state and data
+ // const state = "select_package";
+ // const dcase = demotools.get_case(this.usage);
+ // const data = dcase["select_package"].data[state];
+ // this.state_set_data(data, state);
+ // this._state_load(state);
+ // },
+ methods: {
+ screen_title: function() {
+ if (!this.has_picking()) return this.menu_item().name;
+ return this.current_picking().name;
+ },
+ // TODO: if we have this working we can remove the picking detail?
+ current_doc: function() {
+ const picking = this.current_picking();
+ return {
+ record: picking,
+ identifier: picking.name,
+ };
+ },
+ // TODO: ask Joél if this is needed
+ current_picking: function() {
+ const data = this.state_get_data("start");
+ if (_.isEmpty(data) || _.isEmpty(data.move_line.picking)) {
+ return {};
+ }
+ return data.move_line.picking;
+ },
+ has_picking: function() {
+ return !_.isEmpty(this.current_picking());
+ },
+ /**
+ * As we can get `move_line` or `package_level` for single items
+ * and `move_lines` or `package_levels` for multiple items,
+ * this function tries to make such data homogenous.
+ * We'll work alway with multiple records and loop on them.
+ */
+ wrapped_context: function() {
+ const data = this.state.data;
+ let res = {
+ has_records: true,
+ records: data.move_lines,
+ _multi: true,
+ _type: "move_line",
+ };
+ if (_.isEmpty(res.records) && data.move_line) {
+ res.records = [data.move_line];
+ res._multi = false;
+ }
+ if (_.isEmpty(res.records) && data.package_levels) {
+ res.records = data.package_levels;
+ res._type = "pkg_level";
+ }
+ if (_.isEmpty(res.records) && data.package_level) {
+ res.records = [data.package_level];
+ res._type = "pkg_level";
+ }
+ res.has_records = res.records.length > 0;
+ return res;
+ },
+ move_line_detail_list_options: function(move_line) {
+ return this.utils.misc.move_line_product_detail_options(move_line, {
+ loud_labels: true,
+ fields_blacklist: ["product.qty_available"],
+ fields: [{path: "location_src.name", label: "From"}],
+ });
+ },
+ // Common actions
+ on_line_action: function(action) {
+ this["on_" + action.event_name].call(this);
+ },
+ on_action_postpone: function() {
+ let endpoint, endpoint_data;
+ const data = this.state.data;
+ if (data.package_level) {
+ endpoint = "postpone_package";
+ endpoint_data = {
+ package_level_id: data.package_level.id,
+ location_id: data.package_level.location_src.id,
+ };
+ } else {
+ endpoint = "postpone_line";
+ endpoint_data = {
+ move_line_id: data.move_line.id,
+ location_id: data.move_line.location_src.id,
+ };
+ }
+ this.wait_call(this.odoo.call(endpoint, endpoint_data));
+ },
+ on_action_stock_out: function() {
+ this.state_set_data(this.state.data, "stock_issue");
+ this.state_to("stock_issue");
+ },
+ },
+ data: function() {
+ const self = this;
+ return {
+ usage: "location_content_transfer",
+ initial_state_key: "scan_location",
+ states: {
+ init: {
+ enter: () => {
+ this.state_reset_data();
+ this.wait_call(this.odoo.call("start_or_recover"));
+ },
+ },
+ scan_location: {
+ display_info: {
+ title: "Start by scanning a location",
+ scan_placeholder: "Scan location",
+ },
+ on_scan: scanned => {
+ this.wait_call(
+ this.odoo.call("scan_location", {barcode: scanned.text})
+ );
+ },
+ },
+ scan_destination_all: {
+ display_info: {
+ title: "Scan destination location for all items",
+ scan_placeholder: "Scan location",
+ },
+ on_scan: scanned => {
+ const data = this.state.data;
+ this.wait_call(
+ this.odoo.call("set_destination_all", {
+ location_id: data.location.id,
+ barcode: scanned.text,
+ confirmation: data.confirmation_required,
+ })
+ );
+ },
+ on_split_by_line: () => {
+ const location = this.state.data.location;
+ this.wait_call(
+ this.odoo.call("go_to_single", {location_id: location.id})
+ );
+ },
+ },
+ start_single: {
+ display_info: {
+ scan_placeholder: "Scan pack / product / lot",
+ },
+ on_scan: scanned => {
+ let endpoint, endpoint_data;
+ const data = this.state.data;
+ if (data.package_level) {
+ endpoint = "scan_package";
+ endpoint_data = {
+ package_level_id: data.package_level.id,
+ location_id: data.package_level.location_src.id,
+ barcode: scanned.text,
+ };
+ } else {
+ endpoint = "scan_line";
+ endpoint_data = {
+ move_line_id: data.move_line.id,
+ location_id: data.move_line.location_src.id,
+ barcode: scanned.text,
+ };
+ }
+ this.wait_call(this.odoo.call(endpoint, endpoint_data));
+ },
+ },
+ scan_destination: {
+ enter: () => {
+ this.reset_notification();
+ },
+ display_info: {
+ title: "Set a qty and scan destination location",
+ scan_placeholder: "Scan location",
+ },
+ events: {
+ qty_edit: "on_qty_update",
+ },
+ on_qty_update: qty => {
+ this.state.data.destination_qty = qty;
+ },
+ on_scan: scanned => {
+ let endpoint, endpoint_data;
+ const data = this.state.data;
+ if (data.package_level) {
+ endpoint = "set_destination_package";
+ endpoint_data = {
+ package_level_id: data.package_level.id,
+ location_id: data.package_level.location_src.id,
+ barcode: scanned.text,
+ confirmation: data.confirmation_required,
+ };
+ } else {
+ endpoint = "set_destination_line";
+ endpoint_data = {
+ move_line_id: data.move_line.id,
+ location_id: data.move_line.location_src.id,
+ barcode: scanned.text,
+ confirmation: data.confirmation_required,
+ quantity:
+ this.state.data.destination_qty ||
+ data.move_line.quantity,
+ };
+ }
+ this.wait_call(this.odoo.call(endpoint, endpoint_data));
+ },
+ on_split_by_line: () => {
+ const location = this.state.data.move_lines[0].location_src;
+ this.wait_call(
+ this.odoo.call("go_to_single", {location_id: location.id})
+ );
+ },
+ },
+ stock_issue: {
+ enter: () => {
+ this.reset_notification();
+ },
+ on_confirm_stock_issue: () => {
+ let endpoint, endpoint_data;
+ const data = this.state.data;
+ if (data.package_level) {
+ endpoint = "stock_out_package";
+ endpoint_data = {
+ package_level_id: data.package_level.id,
+ location_id: data.package_level.location_src.id,
+ };
+ } else {
+ endpoint = "stock_out_line";
+ endpoint_data = {
+ move_line_id: data.move_line.id,
+ location_id: data.move_line.location_src.id,
+ };
+ }
+ this.wait_call(this.odoo.call(endpoint, endpoint_data));
+ },
+ },
+ },
+ };
+ },
+});
+
+process_registry.add("location_content_transfer", LocationContentTransfer);
diff --git a/shopfloor_mobile/static/wms/src/scenario/mixins.js b/shopfloor_mobile/static/wms/src/scenario/mixins.js
index 7f3c29a468..90ab0a1f68 100644
--- a/shopfloor_mobile/static/wms/src/scenario/mixins.js
+++ b/shopfloor_mobile/static/wms/src/scenario/mixins.js
@@ -66,11 +66,8 @@ export var ScenarioBaseMixin = {
return state;
},
search_input_placeholder: function() {
- if (this.state.scan_placeholder) {
- // TMP backward compat
- return this.state.scan_placeholder;
- }
- return this.state.display_info.scan_placeholder;
+ const placeholder = this.state.display_info.scan_placeholder;
+ return _.isFunction(placeholder) ? placeholder.call(this) : placeholder;
},
show_cancel_button: function() {
return this.state.display_info.show_cancel_button;
@@ -182,10 +179,21 @@ export var ScenarioBaseMixin = {
/*
Alias "init" to the initial state
and erase all existing data if any.
- Used when we enter from a menu or to enforce data cleanup.
+ Used when we enter from a menu or to enforce data cleanup
+ or any other case where you want to erase all data on demand.
*/
this.state_reset_data_all();
- state_key = this.initial_state_key;
+ /**
+ * Special case: if `init` is defined as state
+ * you can use it do particular initialization.
+ * Eg: call the server to preload some data.
+ * In this case the state is responsible
+ * for handline the transition to another state
+ * or delegate it to server result via `next_state` key.
+ */
+ if (!"init" in this.states) {
+ state_key = this.initial_state_key;
+ }
}
state_key = state_key || "start";
if (state_key == "start") {
@@ -235,10 +243,6 @@ export var ScenarioBaseMixin = {
? this.initial_state_key
: result.next_state;
if (!_.isUndefined(result.data)) {
- // TODO: we should find a way to reset the whole scenario data
- // as soon as we start w/ the scenario by clicking the menu item
- // and not just when you go to /start because you could get back
- // to it from another state.
this.state_reset_data(state_key);
this.state_set_data(result.data[state_key], state_key);
}
diff --git a/shopfloor_mobile/static/wms/src/services/config.js b/shopfloor_mobile/static/wms/src/services/config.js
deleted file mode 100644
index 4d7e88e8ff..0000000000
--- a/shopfloor_mobile/static/wms/src/services/config.js
+++ /dev/null
@@ -1,18 +0,0 @@
-export class Config {
- constructor(odoo) {
- this.odoo = odoo;
- this.data = {};
- this.authenticated = false;
- }
- load() {
- return this.odoo._call("app/user_config", "POST", {}).then(result => {
- if (!_.isUndefined(result.data)) {
- this.data = result.data;
- this.authenticated = true;
- } else {
- // TODO: any better thing to do here?
- console.log(result);
- }
- });
- }
-}
diff --git a/shopfloor_mobile/static/wms/src/services/odoo.js b/shopfloor_mobile/static/wms/src/services/odoo.js
index 463aa7ee2c..ba6ca5b961 100644
--- a/shopfloor_mobile/static/wms/src/services/odoo.js
+++ b/shopfloor_mobile/static/wms/src/services/odoo.js
@@ -99,13 +99,18 @@ export class OdooMocked extends OdooMixin {
const barcode = data
? data.barcode || data.identifier || data.location_barcode
: null;
- if (_.has(this.demo_data, barcode)) {
+ let demo_data = this.demo_data;
+ if (demo_data.by_menu_id && _.has(demo_data.by_menu_id, this.process_menu_id)) {
+ // Load subset of responses by menu id
+ demo_data = demo_data.by_menu_id[this.process_menu_id];
+ }
+ if (_.has(demo_data, barcode)) {
// Pick a specific case for this barcode
- result = this.demo_data[barcode];
+ result = demo_data[barcode];
}
- if (_.has(this.demo_data, path)) {
+ if (_.has(demo_data, path)) {
// Pick general case for this path
- result = this.demo_data[path];
+ result = demo_data[path];
}
if (_.has(result, barcode)) {
// Pick specific barcode case inside path case
@@ -125,6 +130,12 @@ export class OdooMocked extends OdooMixin {
console.dir("CALL RETURN data:", result);
return Promise.resolve(result);
}
+ user_config(params) {
+ return Promise.resolve({data: demotools.makeAppConfig()});
+ }
+ menu(params) {
+ return Promise.resolve({data: {menus: demotools.getAppMenus()}});
+ }
scan(params) {
let result = {};
const data = demotools.get_indexed(params.identifier);
diff --git a/shopfloor_mobile/static/wms/src/settings/language.js b/shopfloor_mobile/static/wms/src/settings/language.js
index 27fd45ec33..917108b578 100644
--- a/shopfloor_mobile/static/wms/src/settings/language.js
+++ b/shopfloor_mobile/static/wms/src/settings/language.js
@@ -1,9 +1,9 @@
export var Language = Vue.component("language", {
template: `
-
+
diff --git a/shopfloor_mobile/static/wms/src/settings/profile.js b/shopfloor_mobile/static/wms/src/settings/profile.js
index c1d3b15108..850771b0aa 100644
--- a/shopfloor_mobile/static/wms/src/settings/profile.js
+++ b/shopfloor_mobile/static/wms/src/settings/profile.js
@@ -10,7 +10,7 @@ export var Profile = Vue.component("profile", {
};
},
template: `
-
+
- Reload config and menu
+
@@ -53,10 +53,5 @@ export var Profile = Vue.component("profile", {
logout: function() {
this.$root.logout();
},
- reset_data: function() {
- this.$root._clearConfig();
- this.$root._loadMenu();
- this.$root.$router.push({name: "home"});
- },
},
});
diff --git a/shopfloor_mobile/static/wms/src/settings/settings.js b/shopfloor_mobile/static/wms/src/settings/settings.js
index 3c82c3a910..4205fb530d 100644
--- a/shopfloor_mobile/static/wms/src/settings/settings.js
+++ b/shopfloor_mobile/static/wms/src/settings/settings.js
@@ -3,7 +3,7 @@ export var SettingsControlPanel = Vue.component("settings-control-panel", {
return {};
},
template: `
-
+
diff --git a/shopfloor_mobile/static/wms/src/utils.js b/shopfloor_mobile/static/wms/src/utils.js
index d9d44fe411..1b92eaa7e7 100644
--- a/shopfloor_mobile/static/wms/src/utils.js
+++ b/shopfloor_mobile/static/wms/src/utils.js
@@ -216,9 +216,9 @@ export class Utils {
*
* @param {*} line The move line
*/
- move_line_product_detail_options(line, override = {}) {
+ move_line_product_detail_options(line, options = {}) {
const self = this;
- const fields = [
+ const default_fields = [
{path: "package_src.name", label: "Pack"},
{path: "lot.name", label: "Lot"},
{
@@ -231,13 +231,20 @@ export class Utils {
},
{path: "product.qty_available", label: "Qty on hand"},
];
- let opts = {
+ options = _.defaults({}, options, {
main: true,
key_title: "product.display_name",
- fields: fields,
title_action_field: {action_val_path: "product.barcode"},
- };
- return _.extend(opts, override || {});
+ fields_blacklist: [],
+ fields_extend_default: true,
+ });
+ options.fields = options.fields_extend_default
+ ? default_fields.concat(options.fields || [])
+ : options.fields || [];
+ options.fields = _.filter(options.fields, function(field) {
+ return !options.fields_blacklist.includes(field.path);
+ });
+ return options;
}
move_line_qty_picker_options(line, override = {}) {
let opts = {