diff --git a/shopfloor/actions/data.py b/shopfloor/actions/data.py
index 0d987e0da0..9d53aa23bf 100644
--- a/shopfloor/actions/data.py
+++ b/shopfloor/actions/data.py
@@ -18,6 +18,9 @@ def _jsonify(self, recordset, parser, multi=False, **kw):
return res[0] if res else None
return res
+ def _simple_record_parser(self):
+ return ["id", "name"]
+
def partner(self, record, **kw):
return self._jsonify(record, self._partner_parser, **kw)
@@ -26,7 +29,7 @@ def partners(self, record, **kw):
@property
def _partner_parser(self):
- return ["id", "name"]
+ return self._simple_record_parser()
def location(self, record, **kw):
return self._jsonify(
@@ -95,12 +98,12 @@ def _package_packaging_parser(self):
def packaging(self, record, **kw):
return self._jsonify(record, self._packaging_parser, **kw)
- def packagings(self, record, **kw):
+ def packaging_list(self, record, **kw):
return self.packaging(record, multi=True)
@property
def _packaging_parser(self):
- return ["id", "name"]
+ return self._simple_record_parser() + ["qty"]
def lot(self, record, **kw):
return self._jsonify(record, self._lot_parser, **kw)
@@ -110,7 +113,7 @@ def lots(self, record, **kw):
@property
def _lot_parser(self):
- return ["id", "name", "ref"]
+ return self._simple_record_parser() + ["ref"]
def move_line(self, record, **kw):
record = record.with_context(location=record.location_id.id)
@@ -153,7 +156,14 @@ def products(self, record, **kw):
@property
def _product_parser(self):
- return ["id", "name", "display_name", "default_code", "barcode"]
+ return [
+ "id",
+ "name",
+ "display_name",
+ "default_code",
+ "barcode",
+ ("packaging_ids:packaging", self._packaging_parser),
+ ]
def picking_batch(self, record, with_pickings=False, **kw):
parser = self._picking_batch_parser
diff --git a/shopfloor/services/checkout.py b/shopfloor/services/checkout.py
index f427115d00..2c63580da0 100644
--- a/shopfloor/services/checkout.py
+++ b/shopfloor/services/checkout.py
@@ -122,7 +122,7 @@ def _response_for_change_packaging(self, picking, package, packaging_list):
"package": self.data_struct.package(
package, picking=picking, with_packaging=True
),
- "packagings": self.data_struct.packagings(packaging_list.sorted()),
+ "packaging": self.data_struct.packaging_list(packaging_list.sorted()),
},
)
@@ -1229,7 +1229,7 @@ def _schema_select_packaging(self):
"type": "dict",
"schema": self.schemas.package(with_packaging=True),
},
- "packagings": {
+ "packaging": {
"type": "list",
"schema": {"type": "dict", "schema": self.schemas.packaging()},
},
diff --git a/shopfloor/services/schema.py b/shopfloor/services/schema.py
index 6e97fa3525..ebe1523a29 100644
--- a/shopfloor/services/schema.py
+++ b/shopfloor/services/schema.py
@@ -112,6 +112,7 @@ def product(self):
"display_name": {"type": "string", "nullable": False, "required": True},
"default_code": {"type": "string", "nullable": False, "required": True},
"barcode": {"type": "string", "nullable": True, "required": False},
+ "packaging": self._schema_list_of(self.packaging()),
}
def package(self, with_packaging=False):
@@ -122,12 +123,7 @@ def package(self, with_packaging=False):
"move_line_count": {"required": False, "nullable": True, "type": "integer"},
}
if with_packaging:
- schema["packaging"] = {
- "type": "dict",
- "required": True,
- "nullable": True,
- "schema": self.packaging(),
- }
+ schema["packaging"] = self._schema_dict_of(self.packaging())
return schema
def lot(self):
@@ -148,6 +144,7 @@ def packaging(self):
return {
"id": {"required": True, "type": "integer"},
"name": {"type": "string", "nullable": False, "required": True},
+ "qty": {"type": "float", "required": True},
}
def picking_batch(self, with_pickings=False):
diff --git a/shopfloor/tests/test_actions_data.py b/shopfloor/tests/test_actions_data.py
index d0612fe1ee..395de60b80 100644
--- a/shopfloor/tests/test_actions_data.py
+++ b/shopfloor/tests/test_actions_data.py
@@ -64,6 +64,14 @@ def _expected_product(self, record, **kw):
"display_name": record.display_name,
"default_code": record.default_code,
"barcode": record.barcode,
+ "packaging": [self._expected_packaging(x) for x in record.packaging_ids],
+ }
+
+ def _expected_packaging(self, record, **kw):
+ return {
+ "id": record.id,
+ "name": record.name,
+ "qty": record.qty,
}
@@ -71,8 +79,7 @@ class ActionsDataCase(ActionsDataCaseBase):
def test_data_packaging(self):
data = self.data.packaging(self.packaging)
self.assert_schema(self.schema.packaging(), data)
- expected = {"id": self.packaging.id, "name": self.packaging.name}
- self.assertDictEqual(data, expected)
+ self.assertDictEqual(data, self._expected_packaging(self.packaging))
def test_data_location(self):
location = self.stock_location
@@ -107,8 +114,8 @@ def test_data_package(self):
"id": package.id,
"name": package.name,
"move_line_count": 1,
- "packaging": self.data.packaging(package.product_packaging_id),
- "weight": 0,
+ "packaging": self._expected_packaging(package.product_packaging_id),
+ "weight": 0.0,
}
self.assertDictEqual(data, expected)
diff --git a/shopfloor/tests/test_actions_data_detail.py b/shopfloor/tests/test_actions_data_detail.py
index 6f3c85d62d..d715d4aefa 100644
--- a/shopfloor/tests/test_actions_data_detail.py
+++ b/shopfloor/tests/test_actions_data_detail.py
@@ -97,8 +97,7 @@ def test_data_location(self):
def test_data_packaging(self):
data = self.data_detail.packaging(self.packaging)
self.assert_schema(self.schema_detail.packaging(), data)
- expected = {"id": self.packaging.id, "name": self.packaging.name}
- self.assertDictEqual(data, expected)
+ self.assertDictEqual(data, self._expected_packaging(self.packaging))
def test_data_lot(self):
lot = self.env["stock.production.lot"].create(
diff --git a/shopfloor_mobile/static/wms/index.html b/shopfloor_mobile/static/wms/index.html
index f3782f4217..3b69e035ee 100644
--- a/shopfloor_mobile/static/wms/index.html
+++ b/shopfloor_mobile/static/wms/index.html
@@ -46,6 +46,7 @@
+
diff --git a/shopfloor_mobile/static/wms/src/components/batch_picking_detail.js b/shopfloor_mobile/static/wms/src/components/batch_picking_detail.js
index 86cd6c8269..e988cd5491 100644
--- a/shopfloor_mobile/static/wms/src/components/batch_picking_detail.js
+++ b/shopfloor_mobile/static/wms/src/components/batch_picking_detail.js
@@ -1,6 +1,6 @@
/* eslint-disable strict */
Vue.component("batch-picking-detail", {
- props: ["info"],
+ props: ["record"],
methods: {
detail_fields() {
return [
@@ -17,36 +17,38 @@ Vue.component("batch-picking-detail", {
},
},
template: `
-
-
+
-
+
-
+
-
Pickings
+
+
+
+ Start
+
+
+
+
+ Cancel
+
+
+
+
+
+
+ Pickings list
-
-
-
-
-
- Start
-
-
-
-
- Cancel
-
-
+
`,
});
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 1ba7f40c54..a4bb43ee82 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,12 +1,11 @@
export var batch_picking_line = Vue.component("batch-picking-line-detail", {
props: {
line: Object,
- // TODO: not sure this is still needed
- showFullInfo: {
+ articleScanned: {
type: Boolean,
- default: true,
+ default: false,
},
- articleScanned: {
+ showQtyPicker: {
type: Boolean,
default: false,
},
@@ -22,18 +21,6 @@ export var batch_picking_line = Vue.component("batch-picking-line-detail", {
},
},
methods: {
- detail_fields(key) {
- const mapping = {
- location_src: [],
- product: [
- {path: "package_src.name", label: "Pack"},
- {path: "quantity", label: "Qty"},
- {path: "product.qty_available", label: "Qty on hand"},
- ],
- location_dest: [],
- };
- return mapping[key];
- },
full_detail_fields() {
return [
{path: "batch.name", label: "Batch"},
@@ -50,37 +37,41 @@ export var batch_picking_line = Vue.component("batch-picking-line-detail", {
+
+
+
+
Destination pack not selected.
-
+
`,
@@ -120,7 +110,7 @@ export var batch_picking_line_actions = Vue.component("batch-picking-line-action
- Action
+ Action
@@ -129,28 +119,28 @@ export var batch_picking_line_actions = Vue.component("batch-picking-line-action
- Go to destination - full bin(s)
+ Go to destination - full bin(s)
- Skip line
+ Skip line
- Declare stock out
+ Declare stock out
- Change lot or pack
+ Change lot or pack
- Back
+ Back
@@ -171,11 +161,11 @@ export var batch_picking_line_stock_out = Vue.component(
},
template: `
-
+
- // TODO packagings
-
diff --git a/shopfloor_mobile/static/wms/src/components/detail/detail_mixin.js b/shopfloor_mobile/static/wms/src/components/detail/detail_mixin.js
index fdea99e5a6..906e4948a1 100644
--- a/shopfloor_mobile/static/wms/src/components/detail/detail_mixin.js
+++ b/shopfloor_mobile/static/wms/src/components/detail/detail_mixin.js
@@ -1,5 +1,3 @@
-import {utils} from "../../utils.js";
-
export var ItemDetailMixin = {
props: {
record: Object,
@@ -41,7 +39,7 @@ export var ItemDetailMixin = {
return [];
},
_render_date(record, field) {
- return this.utils.format_date_display(_.result(record, field.path));
+ return this.utils.misc.format_date_display(_.result(record, field.path));
},
has_detail_action(record, field) {
return _.result(record, field.action_val_path);
@@ -62,13 +60,12 @@ export var ItemDetailMixin = {
params: {identifier: identifier},
query: {displayOnly: 1},
});
+ } else {
+ console.error("Action handler found not value for", field);
}
},
},
computed: {
- utils: function() {
- return utils;
- },
wrapper_klass: function() {
return [
"detail",
diff --git a/shopfloor_mobile/static/wms/src/components/detail/detail_operation.js b/shopfloor_mobile/static/wms/src/components/detail/detail_operation.js
index 2269d7f8d0..6020139dea 100644
--- a/shopfloor_mobile/static/wms/src/components/detail/detail_operation.js
+++ b/shopfloor_mobile/static/wms/src/components/detail/detail_operation.js
@@ -33,7 +33,7 @@ Vue.component("detail-operation", {
diff --git a/shopfloor_mobile/static/wms/src/components/detail/detail_package.js b/shopfloor_mobile/static/wms/src/components/detail/detail_package.js
index 2088ede555..e176fb28b6 100644
--- a/shopfloor_mobile/static/wms/src/components/detail/detail_package.js
+++ b/shopfloor_mobile/static/wms/src/components/detail/detail_package.js
@@ -39,7 +39,7 @@ Vue.component("detail-package", {
diff --git a/shopfloor_mobile/static/wms/src/components/detail/detail_picking.js b/shopfloor_mobile/static/wms/src/components/detail/detail_picking.js
index d1bd52fe64..d6c3b5fe0e 100644
--- a/shopfloor_mobile/static/wms/src/components/detail/detail_picking.js
+++ b/shopfloor_mobile/static/wms/src/components/detail/detail_picking.js
@@ -1,41 +1,31 @@
export var PickingDetailMixin = {
props: {
// TODO: rename to `record`
- picking: Object,
+ record: Object,
options: Object,
- clickable: {
- type: Boolean,
- // TODO: this must be false when showing picking screen (eg: scan anything)
- default: true,
- },
- },
- methods: {
- on_title_action() {
- // TODO: we should probably delegate this to a global event
- this.$router.push({
- name: "scananything",
- params: {identifier: this.picking.name},
- query: {displayOnly: 1},
- });
- },
+ // clickable: {
+ // type: Boolean,
+ // // TODO: this must be false when showing record screen (eg: scan anything)
+ // default: true,
+ // },
},
computed: {
opts() {
const opts = _.defaults({}, this.$props.options, {
- on_title_action: this.$props.clickable ? this.on_title_action : null,
+ title_action_field: {path: "name", action_val_path: "name"},
});
return opts;
},
},
template: `
-
+
-
- {{ picking.origin }}
+
+ {{ record.origin }}
- -
-
- {{ picking.partner.name }}
+ -
+
+ {{ record.partner.name }}
diff --git a/shopfloor_mobile/static/wms/src/components/detail/detail_product.js b/shopfloor_mobile/static/wms/src/components/detail/detail_product.js
index 40921f9611..17c39ae649 100644
--- a/shopfloor_mobile/static/wms/src/components/detail/detail_product.js
+++ b/shopfloor_mobile/static/wms/src/components/detail/detail_product.js
@@ -32,9 +32,9 @@ Vue.component("detail-product", {
-
diff --git a/shopfloor_mobile/static/wms/src/components/detail/detail_transfer.js b/shopfloor_mobile/static/wms/src/components/detail/detail_transfer.js
index 4e67f68a61..30b576e5ba 100644
--- a/shopfloor_mobile/static/wms/src/components/detail/detail_transfer.js
+++ b/shopfloor_mobile/static/wms/src/components/detail/detail_transfer.js
@@ -1,5 +1,4 @@
import {ItemDetailMixin} from "./detail_mixin.js";
-import {utils} from "../../utils.js";
Vue.component("detail-transfer", {
mixins: [ItemDetailMixin],
@@ -44,14 +43,14 @@ Vue.component("detail-transfer", {
];
},
grouped_lines() {
- return this.utils.group_lines_by_locations(this.record.move_lines);
+ return this.utils.misc.group_lines_by_locations(this.record.move_lines);
},
},
template: `
@@ -63,7 +62,7 @@ Vue.component("detail-transfer", {
diff --git a/shopfloor_mobile/static/wms/src/components/input-number-spinner.js b/shopfloor_mobile/static/wms/src/components/input-number-spinner.js
index 3eb8e81e70..50ae6475ce 100644
--- a/shopfloor_mobile/static/wms/src/components/input-number-spinner.js
+++ b/shopfloor_mobile/static/wms/src/components/input-number-spinner.js
@@ -1,8 +1,8 @@
export var NumberSpinner = Vue.component("input-number-spinner", {
template: `
-
-
+
+
+
@@ -11,7 +11,7 @@ export var NumberSpinner = Vue.component("input-number-spinner", {
{{ original_value }}
-
@@ -41,14 +41,18 @@ export var NumberSpinner = Vue.component("input-number-spinner", {
type: Boolean,
default: true,
},
- spinner_position: {
+ mode: {
type: String,
- default: "right",
+ default: "text-only",
},
show_init_value: {
type: Boolean,
default: true,
},
+ select_value_on_load: {
+ type: Boolean,
+ default: true,
+ },
},
data: function() {
return {
@@ -81,4 +85,11 @@ export var NumberSpinner = Vue.component("input-number-spinner", {
this.original_value = parseInt(this.init_value);
this.value = parseInt(this.init_value);
},
+ mounted: function() {
+ if (this.$props.select_value_on_load) {
+ const input = $("input", this.$el).get(0);
+ input.focus();
+ input.select();
+ }
+ },
});
diff --git a/shopfloor_mobile/static/wms/src/components/list.js b/shopfloor_mobile/static/wms/src/components/list.js
index c2808d8ae5..a74121c7ba 100644
--- a/shopfloor_mobile/static/wms/src/components/list.js
+++ b/shopfloor_mobile/static/wms/src/components/list.js
@@ -135,7 +135,7 @@ Vue.component("list-item", {
- mdi-information
+
diff --git a/shopfloor_mobile/static/wms/src/components/manual_select.js b/shopfloor_mobile/static/wms/src/components/manual_select.js
index 077dc68648..61d9bbb9f8 100644
--- a/shopfloor_mobile/static/wms/src/components/manual_select.js
+++ b/shopfloor_mobile/static/wms/src/components/manual_select.js
@@ -114,7 +114,7 @@ Vue.component("manual-select", {
this._updateValue(val, elem.checked);
$(elem)
.closest(".list-item-wrapper")
- .toggleClass("active", elem.checked);
+ .toggleClass(this.selected_color_klass(), elem.checked);
if (!this.opts.showActions) {
this._emitSelected(this._getSelected());
}
@@ -135,6 +135,13 @@ Vue.component("manual-select", {
? this.selected.includes(rec.id)
: this.selected === rec.id;
},
+ selected_color_klass(modifier) {
+ return (
+ "active " +
+ this.utils.colors.color_for("item_selected") +
+ (modifier ? " " + modifier : "")
+ );
+ },
},
computed: {
has_records() {
@@ -156,6 +163,7 @@ Vue.component("manual-select", {
list_item_actions: [],
list_item_extra_component: "",
selected_event: "select",
+ group_color: "",
});
return opts;
},
@@ -201,10 +209,10 @@ Vue.component("manual-select", {
},
template: `
-
+
{{ group.title }}
-
+
-
+
Get work
-
+
Manual selection
@@ -90,7 +90,7 @@ Vue.component("state-display-info", {
},
template: `
@@ -178,7 +178,7 @@ Vue.component("picking-list-item-progress-bar", {
mixins: [ItemDetailMixin],
computed: {
value() {
- return this.utils.picking_completeness(this.record);
+ return this.utils.misc.picking_completeness(this.record);
},
},
template: `
@@ -198,3 +198,30 @@ Vue.component("todo", {
`,
});
+
+// TODO: use color registry for the icon color
+Vue.component("btn-info-icon", {
+ props: {
+ color: {
+ type: String,
+ },
+ },
+ template: `
+ mdi-information
+`,
+});
+
+// TODO: move to separated file
+Vue.component("btn-action", {
+ props: {
+ action: {
+ type: String,
+ default: "",
+ },
+ },
+ template: `
+
+
+
+`,
+});
diff --git a/shopfloor_mobile/static/wms/src/components/packaging-qty-picker.js b/shopfloor_mobile/static/wms/src/components/packaging-qty-picker.js
new file mode 100644
index 0000000000..6195cb92e3
--- /dev/null
+++ b/shopfloor_mobile/static/wms/src/components/packaging-qty-picker.js
@@ -0,0 +1,222 @@
+export var PackagingQtyPickerMixin = {
+ props: {
+ options: Object,
+ },
+ data: function() {
+ return {
+ value: 0,
+ original_value: 0,
+ orig_qty_by_pkg: {},
+ qty_by_pkg: {},
+ };
+ },
+ methods: {
+ on_change_pkg_qty: function(event) {
+ const input = event.target;
+ let new_qty = parseInt(input.value, 10);
+ const data = $(input).data();
+ const origvalue = data.origvalue || 0;
+ // Check max qty reached
+ const future_qty = this.value + data.pkg.qty * (new_qty - origvalue);
+ if (new_qty && future_qty > this.original_value) {
+ // restore qty just in case we can get here
+ new_qty = origvalue;
+ event.preventDefault();
+ // Make it red and shake it
+ $(input)
+ .closest(".inner-wrapper")
+ .addClass("error shake-it")
+ .delay(800)
+ .queue(function() {
+ // End animation
+ $(this)
+ .removeClass("error shake-it", 2000, "easeInOutQuad")
+ .dequeue();
+ // Restore value
+ $(input).val(new_qty);
+ });
+ }
+ // Trigger update
+ this.$set(this.qty_by_pkg, data.pkg.id, new_qty);
+ // Set new orig value
+ $(input).data("origvalue", new_qty);
+ },
+ packaging_by_id: function(id) {
+ return _.find(this.opts.available_packaging, ["id", parseInt(id, 10)]);
+ },
+ /**
+ *
+ Calculate quantity by packaging.
+
+ Limitation: fractional quantities are lost.
+
+ :prod_qty:
+ :min_unit: minimal unit of measure as a tuple (qty, name).
+ Default: to UoM unit.
+ :returns: list of tuple in the form [(qty_per_package, package_name)]
+
+ * @param {*} prod_qty total qty to satisfy.
+ * @param {*} min_unit minimal unit of measure as a tuple (qty, name).
+ Default: to UoM unit.
+ */
+ product_qty_by_packaging: function() {
+ return this._product_qty_by_packaging(this.sorted_packaging, this.value);
+ },
+ /**
+ * Produce a list of tuple of packaging qty and packaging name.
+ * TODO: refactor to handle fractional quantities (eg: 0.5 Kg)
+ *
+ * @param {*} pkg_by_qty packaging records sorted by major qty
+ * @param {*} qty total qty to satisfy
+ */
+ _product_qty_by_packaging: function(pkg_by_qty, qty) {
+ const self = this;
+ let res = {};
+ // const min_unit = _.last(pkg_by_qty);
+ pkg_by_qty.forEach(function(pkg) {
+ let qty_per_pkg = 0;
+ [qty_per_pkg, qty] = self._qty_by_pkg(pkg.qty, qty);
+ if (qty_per_pkg) res[pkg.id] = qty_per_pkg;
+ if (!qty) return;
+ });
+ return res;
+ },
+ /**
+ * Calculate qty needed for given package qty.
+ *
+ * @param {*} pkg_by_qty
+ * @param {*} qty
+ */
+ _qty_by_pkg: function(pkg_qty, qty) {
+ const precision = 3; // TODO: get it from product UoM
+ let qty_per_pkg = 0;
+ // TODO: anything better to do like `float_compare`?
+ while (_.round(qty - pkg_qty, precision) >= 0.0) {
+ qty -= pkg_qty;
+ qty_per_pkg += 1;
+ }
+ return [qty_per_pkg, qty];
+ },
+ _compute_qty: function() {
+ const self = this;
+ let value = 0;
+ _.forEach(this.qty_by_pkg, function(qty, id) {
+ value += self.packaging_by_id(id).qty * qty;
+ });
+ return value;
+ },
+ compute_qty: function(newVal, oldVal) {
+ this.value = this._compute_qty();
+ },
+ },
+ watch: {
+ value: {
+ handler: function(newVal, oldVal) {
+ this.$root.trigger("qty_edit", this.value);
+ },
+ },
+ },
+ created: function() {
+ this.original_value = parseInt(this.opts.init_value, 10);
+ this.value = parseInt(this.opts.init_value, 10);
+ },
+ mounted: function() {
+ const self = this;
+ this.$watch(
+ "qty_by_pkg",
+ function() {
+ self.compute_qty();
+ },
+ {deep: true}
+ );
+ this.qty_by_pkg = this.product_qty_by_packaging();
+ this.orig_qty_by_pkg = this.qty_by_pkg;
+ // hooking via `v-on:change` we don't get the full event but only the qty :/
+ // And forget about using v-text-field because it loses the full event object
+ $(".pkg-value", this.$el).change(this.on_change_pkg_qty);
+ $(".pkg-value", this.$el).on("focus click", function() {
+ $(this).select();
+ });
+ },
+ computed: {
+ opts() {
+ const opts = _.defaults({}, this.$props.options, {
+ input_type: "text",
+ init_value: 0,
+ mode: "",
+ available_packaging: [],
+ });
+ return opts;
+ },
+ /**
+ *
+ */
+ sorted_packaging: function() {
+ return _.reverse(
+ _.sortBy(this.opts.available_packaging, _.property("qty"))
+ );
+ },
+ /**
+ * Collect qty of contained packaging inside bigger packaging.
+ * Eg: "1 Pallet" contains "4 Big boxes".
+ */
+ contained_packaging: function() {
+ const self = this;
+ let res = {};
+ const packaging = this.sorted_packaging;
+ _.forEach(packaging, function(pkg, i) {
+ if (packaging[i + 1]) {
+ const next_pkg = packaging[i + 1];
+ res[pkg.id] = {
+ pkg: next_pkg,
+ qty: self._qty_by_pkg(next_pkg.qty, pkg.qty)[0],
+ };
+ }
+ });
+ return res;
+ },
+ },
+};
+
+export var PackagingQtyPicker = Vue.component("packaging-qty-picker", {
+ mixins: [PackagingQtyPickerMixin],
+ template: `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ pkg.name }}
+
({{ contained_packaging[pkg.id].qty }} {{ contained_packaging[pkg.id].pkg.name }})
+
+
+
+
+`,
+});
+
+export var PackagingQtyPickerDisplay = Vue.component("packaging-qty-picker-display", {
+ mixins: [PackagingQtyPickerMixin],
+ template: `
+
+
+
+ ,
+
+
+`,
+});
diff --git a/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/mixins.js b/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/mixins.js
index 7e83478417..5f82241c55 100644
--- a/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/mixins.js
+++ b/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/mixins.js
@@ -50,6 +50,10 @@ export var ListActionsConsumerMixin = {
export var PickingDetailSelectMixin = {
mixins: [PickingDetailMixin, ListActionsConsumerMixin],
props: {
+ show_picking_info: {
+ type: Boolean,
+ default: false,
+ },
select_records: Array,
select_records_grouped: Array,
select_options: Object,
@@ -64,12 +68,12 @@ export var PickingDetailSelectMixin = {
},
},
template: `
-
+
-
+
@@ -80,6 +84,10 @@ export var PickingDetailSelectMixin = {
export var PickingDetailListMixin = {
mixins: [PickingDetailMixin, ListActionsConsumerMixin],
props: {
+ show_picking_info: {
+ type: Boolean,
+ default: false,
+ },
records: Array,
records_grouped: Array,
list_options: Object,
@@ -93,12 +101,12 @@ export var PickingDetailListMixin = {
},
},
template: `
-
+
-
+
diff --git a/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_select.js b/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_select.js
index d5566e684b..d9e0bced6a 100644
--- a/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_select.js
+++ b/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_select.js
@@ -1,6 +1,7 @@
/* eslint-disable strict */
/* eslint-disable no-implicit-globals */
import {PickingDetailSelectMixin} from "./mixins.js";
+import {ItemDetailMixin} from "../detail/detail_mixin.js";
Vue.component("detail-picking-select", {
mixins: [PickingDetailSelectMixin],
@@ -26,9 +27,8 @@ Vue.component("detail-picking-select", {
});
Vue.component("picking-select-line-content", {
+ mixins: [ItemDetailMixin],
props: {
- record: Object,
- options: Object,
index: Number,
count: Number,
},
@@ -38,13 +38,19 @@ Vue.component("picking-select-line-content", {
{{ index + 1 }} / {{ count }}
-
{{ record.package_dest.name }}
+
+
+ {{ record.package_dest.name }}
+
{{ index + 1 }} / {{ count }}
-
{{ record.product.display_name }}
+
+
+ {{ record.product.display_name }}
+
Lot: {{ record.lot.name }}
diff --git a/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_summary.js b/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_summary.js
index 49a3aa15e2..414683de83 100644
--- a/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_summary.js
+++ b/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_summary.js
@@ -88,7 +88,7 @@ Vue.component("picking-summary-content", {
{{ record.title }}
- {{ pkg_type.title }}
+ {{ pkg_type.title }}
- {{ info.title }}
+
+
+ {{ info.title }}
+
+
+
+
+
+
diff --git a/shopfloor_mobile/static/wms/src/css/main.css b/shopfloor_mobile/static/wms/src/css/main.css
index 55b4c4ddc6..dac741d9b5 100644
--- a/shopfloor_mobile/static/wms/src/css/main.css
+++ b/shopfloor_mobile/static/wms/src/css/main.css
@@ -135,20 +135,20 @@ ul.packaging span:first-child {
text-align: center;
margin: 0.5rem 0;
}
-.number-spinner.spinner-position-right .spinner-btn {
+.number-spinner.spinner-right .spinner-btn {
float: right;
margin: 0.5rem 0;
}
-.number-spinner.spinner-position-right .input-wrapper {
+.number-spinner.spinner-right .input-wrapper {
float: left;
width: 50%;
margin: 0.5rem 0;
}
-.number-spinner.spinner-position-left .spinner-btn {
+.number-spinner.spinner-left .spinner-btn {
float: left;
margin: 0.5rem 0;
}
-.number-spinner.spinner-position-left .input-wrapper {
+.number-spinner.spinner-left .input-wrapper {
float: right;
width: 50%;
margin: 0.5rem 0;
@@ -164,6 +164,13 @@ ul.packaging span:first-child {
font-size: 1.2rem;
color: #999;
}
+.number-spinner.spinner-text-only input {
+ max-height: auto;
+ font-size: 180%;
+}
+.number-spinner.spinner-text-only .init-value {
+ font-size: 150%;
+}
.manual-select.with-bottom-actions {
height: 100%;
@@ -186,9 +193,11 @@ ul.packaging span:first-child {
height: 3.5rem;
width: 100%;
}
-
+.manual-select[class*="with-group_color"] .v-card {
+ border: 5px solid;
+}
.manual-select.with-groups .v-card__title {
- padding: 0.5rem 1rem 1rem 1rem;
+ padding: 0.5rem 1rem 0.5rem 1rem;
}
.manual-select.with-groups .v-card.select-group:not(:last-child) {
margin-bottom: 1.2rem;
@@ -233,9 +242,6 @@ ul.packaging span:first-child {
padding: 0;
}
-.list-item-wrapper.active {
- background-color: lightgreen;
-}
.list-item-wrapper .action-edit {
display: none;
}
@@ -244,7 +250,7 @@ ul.packaging span:first-child {
display: block;
}
-.my-checkbox {
+.sf-checkbox {
-webkit-appearance: none;
width: 30px;
height: 30px;
@@ -253,10 +259,6 @@ ul.packaging span:first-child {
border: 2px solid #555;
}
-input.my-checkbox[type="checkbox"]:checked {
- background: green;
-}
-
.summary-content {
max-width: 100%;
}
@@ -382,3 +384,110 @@ input.my-checkbox[type="checkbox"]:checked {
.todo .message {
background-color: white;
}
+
+.clickable {
+ cursor: pointer;
+}
+
+.packaging-qty-picker * {
+ text-align: center;
+}
+.packaging-qty-picker .unit-value input {
+ font-size: 130%;
+}
+.packaging-qty-picker .unit-value .col {
+ padding-bottom: 0;
+}
+.packaging-qty-picker .packaging-value .col {
+ padding-top: 0;
+ padding-bottom: 5px;
+}
+.packaging-qty-picker .packaging-value .col:not(:last-child) {
+ padding-right: 0;
+}
+.packaging-qty-picker .pkg-qty {
+ font-size: 80%;
+}
+.packaging-qty-picker input.pkg-value {
+ max-width: 60px;
+ padding: 0.5rem 0.5rem 0;
+ font-size: 130%;
+ border-bottom: 1px solid;
+}
+.packaging-qty-picker .inner-wrapper {
+ padding-bottom: 8px;
+}
+.packaging-qty-picker .input-wrapper {
+ margin-bottom: 8px;
+}
+
+.shake-it {
+ -webkit-animation: kf_shake 0.4s 1 linear;
+ -moz-animation: kf_shake 0.4s 1 linear;
+ -o-animation: kf_shake 0.4s 1 linear;
+}
+@-webkit-keyframes kf_shake {
+ 0% {
+ -webkit-transform: translate(10px);
+ }
+ 20% {
+ -webkit-transform: translate(-10px);
+ }
+ 40% {
+ -webkit-transform: translate(15px);
+ }
+ 60% {
+ -webkit-transform: translate(-15px);
+ }
+ 80% {
+ -webkit-transform: translate(8px);
+ }
+ 100% {
+ -webkit-transform: translate(0px);
+ }
+}
+@-moz-keyframes kf_shake {
+ 0% {
+ -moz-transform: translate(10px);
+ }
+ 20% {
+ -moz-transform: translate(-10px);
+ }
+ 40% {
+ -moz-transform: translate(15px);
+ }
+ 60% {
+ -moz-transform: translate(-15px);
+ }
+ 80% {
+ -moz-transform: translate(8px);
+ }
+ 100% {
+ -moz-transform: translate(0px);
+ }
+}
+@-o-keyframes kf_shake {
+ 0% {
+ -o-transform: translate(10px);
+ }
+ 20% {
+ -o-transform: translate(-10px);
+ }
+ 40% {
+ -o-transform: translate(15px);
+ }
+ 60% {
+ -o-transform: translate(-15px);
+ }
+ 80% {
+ -o-transform: translate(8px);
+ }
+ 100% {
+ -o-origin-transform: translate(0px);
+ }
+}
+
+.v-card .v-card__title {
+ font-size: 1.1rem;
+ line-height: 1.1rem;
+}
diff --git a/shopfloor_mobile/static/wms/src/demo/demo.checkout.js b/shopfloor_mobile/static/wms/src/demo/demo.checkout.js
index 4d745503c1..ea388e366b 100644
--- a/shopfloor_mobile/static/wms/src/demo/demo.checkout.js
+++ b/shopfloor_mobile/static/wms/src/demo/demo.checkout.js
@@ -187,7 +187,7 @@ const DEMO_CHECKOUT = {
change_packaging: {
picking: select_pack_picking,
package: demotools.makePack(),
- packagings: _.sampleSize(
+ packaging: _.sampleSize(
[
demotools.makePackaging(),
demotools.makePackaging(),
diff --git a/shopfloor_mobile/static/wms/src/demo/demo.core.js b/shopfloor_mobile/static/wms/src/demo/demo.core.js
index ae3c1fe06b..c91a45cba9 100644
--- a/shopfloor_mobile/static/wms/src/demo/demo.core.js
+++ b/shopfloor_mobile/static/wms/src/demo/demo.core.js
@@ -20,7 +20,7 @@ export class DemoTools {
this.makeLocation({}, {name_prefix: "LOC-DST"}),
this.makeLocation({}, {name_prefix: "LOC-DST"}),
];
- this.packagings = [
+ this.packaging = [
this.makePackaging({
name: "Little Box",
qty: 10,
@@ -226,7 +226,6 @@ export class DemoTools {
0
);
}
-
makeProduct(defaults = {}, options = {}) {
_.defaults(options, {
name_prefix: "Prod " + this.getRandomInt(),
@@ -237,6 +236,7 @@ export class DemoTools {
default_code: default_code,
barcode: default_code,
qty_available: this.getRandomInt(200),
+ packaging: this.randomSetFromArray(this.packaging, 4),
});
const rec = this.makeSimpleRecord(defaults, options);
_.extend(rec, {
@@ -252,7 +252,6 @@ export class DemoTools {
qty_available: this.getRandomInt(),
qty_reserved: this.getRandomInt(),
expiry_date: "2020-12-01",
- packagings: this.randomSetFromArray(this.packagings, 4),
// TODO: load some random images
image: null,
manufacturer: this.makeSimpleRecord({
diff --git a/shopfloor_mobile/static/wms/src/main.js b/shopfloor_mobile/static/wms/src/main.js
index cf1c55f869..38516cf2b7 100644
--- a/shopfloor_mobile/static/wms/src/main.js
+++ b/shopfloor_mobile/static/wms/src/main.js
@@ -20,19 +20,6 @@ Vue.use(Vuetify);
var EventHub = new Vue();
-// TODO: move to color registry
-const vuetify_themes = {
- light: {
- primary: "#491966",
- secondary: "#424242",
- accent: "#82B1FF",
- error: "#FF5252",
- info: "#2196F3",
- success: "#4CAF50",
- warning: "#FFC107",
- },
-};
-
Vue.mixin(GlobalMixin);
const app = new Vue({
@@ -40,7 +27,7 @@ const app = new Vue({
router: router,
vuetify: new Vuetify({
theme: {
- themes: vuetify_themes,
+ themes: color_registry.get_themes(),
},
}),
data: function() {
@@ -56,7 +43,6 @@ const app = new Vue({
appconfig: null,
authenticated: false,
registry: process_registry,
- colors: color_registry,
};
},
created: function() {
diff --git a/shopfloor_mobile/static/wms/src/mixin.js b/shopfloor_mobile/static/wms/src/mixin.js
index 02cffa9227..30a74cb316 100644
--- a/shopfloor_mobile/static/wms/src/mixin.js
+++ b/shopfloor_mobile/static/wms/src/mixin.js
@@ -1,3 +1,5 @@
+import {utils} from "./utils.js";
+import {color_registry} from "./services/color_registry.js";
export var GlobalMixin = {
methods: {
/*
@@ -12,4 +14,15 @@ export var GlobalMixin = {
return bits.join("-");
},
},
+ computed: {
+ /*
+ Provide utils to all components
+ */
+ utils: function() {
+ return {
+ misc: utils,
+ colors: color_registry,
+ };
+ },
+ },
};
diff --git a/shopfloor_mobile/static/wms/src/scenario/checkout.js b/shopfloor_mobile/static/wms/src/scenario/checkout.js
index 1d7266ff9b..b3e3710e14 100644
--- a/shopfloor_mobile/static/wms/src/scenario/checkout.js
+++ b/shopfloor_mobile/static/wms/src/scenario/checkout.js
@@ -1,6 +1,5 @@
import {ScenarioBaseMixin} from "./mixins.js";
import {process_registry} from "../services/process_registry.js";
-import {utils} from "../utils.js";
import {demotools} from "../demo/demo.core.js"; // FIXME: dev only
import {} from "../demo/demo.checkout.js"; // FIXME: dev only
@@ -30,7 +29,7 @@ export var Checkout = Vue.component("checkout", {
- Manual selection
+ Manual selection
@@ -40,7 +39,7 @@ export var Checkout = Vue.component("checkout", {
:records="state.data.pickings"
:list_item_fields="manual_select_picking_fields"
:options="{list_item_options: {bold_title: true}}"
- :key="current_state_key + '-manual-select'"
+ :key="make_state_component_key(['manual-select'])"
/>
@@ -52,15 +51,16 @@ export var Checkout = Vue.component("checkout", {
- Summary
+ Summary
@@ -68,66 +68,44 @@ export var Checkout = Vue.component("checkout", {
- Existing pack
+ >Existing pack
- New pack
+ >New pack
- Process w/o pack
-
-
-
-
-
-
-
-
-
-
- Continue checkout
-
-
-
-
- Mark as done
+ >Process w/o pack
@@ -138,18 +116,24 @@ export var Checkout = Vue.component("checkout", {
-
-
-
-
+
+
+
+
+
+
- Confirm
+ Confirm
@@ -161,10 +145,10 @@ export var Checkout = Vue.component("checkout", {
@@ -174,17 +158,39 @@ export var Checkout = Vue.component("checkout", {
+
+
+
+
+
+ Continue checkout
+
+
+
+
+ Mark as done
+
+
+
+
-
+
- Confirm
+ Confirm
- Back
+ Back
@@ -192,9 +198,6 @@ export var Checkout = Vue.component("checkout", {
`,
computed: {
- utils: function() {
- return utils;
- },
// TODO: move these to methods
manual_select_picking_fields: function() {
return [
@@ -221,10 +224,34 @@ export var Checkout = Vue.component("checkout", {
// this._state_load(state);
// },
methods: {
- record_by_id: function(records, _id) {
- // TODO: double check when the process is done if this is still needed or not.
- // `manual-select` can now buble up events w/ full record.
- return _.first(_.filter(records, {id: _id}));
+ screen_title: function() {
+ if (_.isEmpty(this.current_doc()) || this.state_is("confirm_start"))
+ return this.menu_item.name;
+ let title = this.current_doc().record.name;
+ return title;
+ },
+ current_doc: function() {
+ const data = this.state_get_data("select_line");
+ if (_.isEmpty(data)) {
+ return null;
+ }
+ return {
+ record: data.picking,
+ identifier: data.picking.name,
+ };
+ },
+ select_line_manual_select_opts: function() {
+ return {
+ group_color: this.utils.colors.color_for("screen_step_todo"),
+ };
+ },
+ select_package_manual_select_opts: function() {
+ return {
+ multiple: true,
+ initSelectAll: true,
+ list_item_component: "picking-select-package-content",
+ list_item_options: {actions: ["action_qty_edit"]},
+ };
},
},
data: function() {
@@ -368,6 +395,11 @@ export var Checkout = Vue.component("checkout", {
console.log("unselected", unselected);
this.wait_call(
this.odoo.call("reset_line_qty", {
+ picking_id: this.state.data.picking.id,
+ selected_line_ids: _.map(
+ this.state.data.selected,
+ _.property("id")
+ ),
move_line_id: unselected.id,
})
);
@@ -377,7 +409,7 @@ export var Checkout = Vue.component("checkout", {
this.state_set_data(
{
picking: this.state.data.picking,
- record: record,
+ line: record,
selected_line_ids: _.map(
this.state.data.selected,
_.property("id")
@@ -431,6 +463,7 @@ export var Checkout = Vue.component("checkout", {
},
events: {
qty_change_confirm: "on_confirm",
+ qty_edit: "on_qty_update",
},
on_back: () => {
this.state_to("select_package");
@@ -445,7 +478,7 @@ export var Checkout = Vue.component("checkout", {
this.odoo.call("set_custom_qty", {
picking_id: this.state.data.picking.id,
selected_line_ids: this.state.data.selected_line_ids,
- move_line_id: this.state.data.record.id,
+ move_line_id: this.state.data.line.id,
qty_done: this.state.data.qty,
})
);
diff --git a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js
index 6511b94411..80702afb4f 100644
--- a/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js
+++ b/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js
@@ -20,14 +20,15 @@ export var ClusterPicking = Vue.component("cluster-picking", {
/>
-
-
-
@@ -114,14 +112,32 @@ export var ClusterPicking = Vue.component("cluster-picking", {
if (_.isEmpty(this.current_batch()) || this.state_is("confirm_start"))
return this.menu_item.name;
let title = this.current_batch().name;
- // if (this.current_picking) {
- // title += " - " + this.current_picking.name;
- // }
+ const picking = this.current_picking();
+ if (picking) {
+ title += " > " + picking.name;
+ }
return title;
},
current_batch: function() {
return this.state_get_data("confirm_start");
},
+ current_picking: function() {
+ const data = this.state_get_data("start_line") || {};
+ if (!data.picking) {
+ return null;
+ }
+ return data.picking;
+ },
+ current_doc: function() {
+ const picking = this.current_picking();
+ if (!picking) {
+ return {};
+ }
+ return {
+ record: picking,
+ identifier: picking.name,
+ };
+ },
action_full_bin: function() {
this.wait_call(
this.odoo.call("prepare_unload", {
@@ -228,13 +244,16 @@ export var ClusterPicking = Vue.component("cluster-picking", {
title: "Check qty and scan a destination bin",
scan_placeholder: "Scan destination bin",
},
+ events: {
+ qty_edit: "on_qty_edit",
+ },
enter: () => {
// TODO: shalle we hook v-model for qty input straight to the state data?
this.scan_destination_qty = this.state_get_data(
"start_line"
).quantity;
},
- on_qty_update: qty => {
+ on_qty_edit: qty => {
this.scan_destination_qty = parseInt(qty);
},
on_scan: scanned => {
diff --git a/shopfloor_mobile/static/wms/src/scenario/delivery.js b/shopfloor_mobile/static/wms/src/scenario/delivery.js
index cbb75b0487..b3fcd1ce68 100644
--- a/shopfloor_mobile/static/wms/src/scenario/delivery.js
+++ b/shopfloor_mobile/static/wms/src/scenario/delivery.js
@@ -1,6 +1,5 @@
import {ScenarioBaseMixin} from "./mixins.js";
import {process_registry} from "../services/process_registry.js";
-import {utils} from "../utils.js";
import {demotools} from "../demo/demo.core.js"; // FIXME: dev only
import {} from "../demo/demo.delivery.js"; // FIXME: dev only
@@ -28,8 +27,8 @@ export var Delivery = Vue.component("checkout", {
@@ -59,11 +58,6 @@ export var Delivery = Vue.component("checkout", {
`,
- computed: {
- utils: function() {
- return utils;
- },
- },
// FIXME: just for dev
// mounted: function() {
// // TEST force state and data
@@ -74,17 +68,12 @@ export var Delivery = Vue.component("checkout", {
// this._state_load(state);
// },
methods: {
- record_by_id: function(records, _id) {
- // TODO: double check when the process is done if this is still needed or not.
- // `manual-select` can now buble up events w/ full record.
- return _.first(_.filter(records, {id: _id}));
- },
deliver_move_line_list_options: function() {
return {
list_item_options: {
actions: ["action_cancel_line"],
fields: this.move_line_detail_fields(),
- list_item_klass_maker: this.utils.move_line_color_klass,
+ list_item_klass_maker: this.utils.misc.move_line_color_klass,
},
};
},
@@ -102,7 +91,7 @@ export var Delivery = Vue.component("checkout", {
label: "Lines",
renderer: function(rec, field) {
return (
- self.utils.picking_completed_lines(rec) +
+ self.utils.misc.picking_completed_lines(rec) +
" / " +
rec.move_lines.length
);
@@ -194,7 +183,7 @@ export var Delivery = Vue.component("checkout", {
},
visible_records: records => {
const self = this;
- let visible_records = utils.order_picking_by_completeness(
+ let visible_records = this.utils.misc.order_picking_by_completeness(
records
);
if (this.state.data.filtered) {
diff --git a/shopfloor_mobile/static/wms/src/scenario/mixins.js b/shopfloor_mobile/static/wms/src/scenario/mixins.js
index 224c84f400..ecd4689e25 100644
--- a/shopfloor_mobile/static/wms/src/scenario/mixins.js
+++ b/shopfloor_mobile/static/wms/src/scenario/mixins.js
@@ -84,6 +84,7 @@ export var ScenarioBaseMixin = {
return {
// you can provide a different screen title
title: this.screen_title ? this.screen_title() : this.menu_item.name,
+ current_doc: this.current_doc ? this.current_doc() : null,
klass: this.usage + " " + "state-" + this.state.key,
user_message: this.user_message,
user_popup: this.user_popup,
@@ -104,6 +105,11 @@ export var ScenarioBaseMixin = {
},
},
methods: {
+ make_state_component_key: function(bits) {
+ bits.unshift(this.current_state_key);
+ bits.unshift(this.usage);
+ return this.make_component_key(bits);
+ },
storage_key: function(state_key) {
state_key = _.isUndefined(state_key) ? this.current_state_key : state_key;
return this.usage + "." + state_key;
@@ -321,10 +327,10 @@ export var ScenarioBaseMixin = {
_.each(self.state.events, function(handler, name) {
if (typeof handler == "string") handler = self.state[handler];
const event_name = self.state.key + ":" + name;
- // Wipe old handlers
- // TODO: any way to register them just once?
- self.$root.event_hub.$off(event_name, handler);
- self.$root.event_hub.$on(event_name, handler);
+ const existing = self.$root.event_hub._events[event_name];
+ if (handler && _.isEmpty(existing)) {
+ self.$root.event_hub.$on(event_name, handler);
+ }
});
}
},
diff --git a/shopfloor_mobile/static/wms/src/services/color_registry.js b/shopfloor_mobile/static/wms/src/services/color_registry.js
index 17e3b97aed..d7c2e1d184 100644
--- a/shopfloor_mobile/static/wms/src/services/color_registry.js
+++ b/shopfloor_mobile/static/wms/src/services/color_registry.js
@@ -2,7 +2,38 @@ import {ColorRegistry} from "./registry.js";
export var color_registry = new ColorRegistry();
-color_registry.add_theme({
- screen_step_done: "green accent-4",
- screen_step_missing: "amber",
-});
+color_registry.add_theme(
+ {
+ /**
+ * standard keys
+ */
+ primary: "#491966",
+ secondary: "#CFD2FF",
+ accent: "#82B1FF",
+ error: "#c22a4a",
+ info: "#5e60ab",
+ success: "#8fbf44",
+ warning: "amber",
+ /**
+ * app specific
+ */
+ screen_step_done: "success",
+ screen_step_todo: "#FFE3AC",
+ /**
+ * icons
+ */
+ info_icon: "info darken-2",
+ /**
+ * buttons / actions
+ */
+ btn_action: "primary lighten-2",
+ btn_action_cancel: "error",
+ btn_action_warn: "amber",
+ btn_action_complete: "success",
+ /**
+ * selection
+ */
+ item_selected: "success",
+ },
+ "light"
+); // TODO: we should bave a theme named "coosa" and select it
diff --git a/shopfloor_mobile/static/wms/src/services/registry.js b/shopfloor_mobile/static/wms/src/services/registry.js
index 08938eb670..8da03a0efb 100644
--- a/shopfloor_mobile/static/wms/src/services/registry.js
+++ b/shopfloor_mobile/static/wms/src/services/registry.js
@@ -21,26 +21,26 @@ export class Registry {
}
export class ColorRegistry {
- constructor(theme, _default = "default") {
- this.themes = [];
- this.colors = {};
+ constructor(theme, _default = "light") {
+ this.themes = {};
this.default_theme = _default;
}
add_theme(colors, theme) {
if (_.isUndefined(theme)) theme = this.default_theme;
- if (!this.themes.includes(theme)) {
- this.themes.push(theme);
- }
- this.colors[theme] = colors;
+ this.themes[theme] = colors;
}
color_for(key, theme) {
if (_.isUndefined(theme)) theme = this.default_theme;
- if (!this.themes.includes(theme)) {
- console.log("Theme", theme, "not registeed.");
+ if (!this.themes[theme]) {
+ console.log("Theme", theme, "not registered.");
return null;
}
- return this.colors[theme][key];
+ return this.themes[theme][key];
+ }
+
+ get_themes() {
+ return this.themes;
}
}
diff --git a/shopfloor_mobile/static/wms/src/settings/profile.js b/shopfloor_mobile/static/wms/src/settings/profile.js
index 8e150047ce..720cd65b23 100644
--- a/shopfloor_mobile/static/wms/src/settings/profile.js
+++ b/shopfloor_mobile/static/wms/src/settings/profile.js
@@ -33,12 +33,12 @@ export var Profile = Vue.component("profile", {
- Reload config and menu
+ Reload config and menu
- Logout
+ Logout
diff --git a/shopfloor_mobile/static/wms/src/utils.js b/shopfloor_mobile/static/wms/src/utils.js
index ee7e78d0ac..617ef0ef80 100644
--- a/shopfloor_mobile/static/wms/src/utils.js
+++ b/shopfloor_mobile/static/wms/src/utils.js
@@ -146,6 +146,8 @@ export class Utils {
return _.reverse(ordered);
}
+ // DIsplay utils: TODO: split them to their own place
+
format_date_display(date_string, options = {}) {
_.defaults(options, {
locale: navigator ? navigator.language : "en-US",
@@ -174,6 +176,48 @@ export class Utils {
}
return "move-line-" + klass;
}
+
+ /**
+ * Provide display options for rendering move line product's info.
+ *
+ * TODO: @simahawk this is a first attempt to stop creating a specific widget of each case
+ * whereas you have to handle different options.
+ * The aim is to be able to re-use detail-card and alike by passing options only.
+ *
+ * @param {*} line The move line
+ */
+ move_line_product_detail_options(line, override = {}) {
+ const fields = [
+ {path: "package_src.name", label: "Pack"},
+ {path: "lot.name", label: "Lot"},
+ {
+ path: "quantity",
+ label: "Qty",
+ render_component: "packaging-qty-picker-display",
+ render_options: function(record) {
+ return {
+ init_value: record.quantity,
+ available_packaging: record.product.packaging,
+ };
+ },
+ },
+ {path: "product.qty_available", label: "Qty on hand"},
+ ];
+ let opts = {
+ main: true,
+ key_title: "product.display_name",
+ fields: fields,
+ title_action_field: {action_val_path: "product.barcode"},
+ };
+ return _.extend(opts, override || {});
+ }
+ move_line_qty_picker_options(line, override = {}) {
+ let opts = {
+ init_value: line.quantity,
+ available_packaging: line.product.packaging,
+ };
+ return _.extend(opts, override || {});
+ }
}
export const utils = new Utils();