From c7490b972d68018d2cbd392e7d3796470320ce74 Mon Sep 17 00:00:00 2001 From: Benoit Date: Fri, 22 Sep 2023 15:55:07 +0200 Subject: [PATCH] fix inventory qty picker --- .../src/components/inventory_line_detail.js | 4 +- .../src/components/inventory_qty_picker.js | 312 ++++++++++++++++++ .../templates/assets.xml | 10 +- 3 files changed, 322 insertions(+), 4 deletions(-) create mode 100644 shopfloor_inventory_mobile/static/wms/src/components/inventory_qty_picker.js diff --git a/shopfloor_inventory_mobile/static/wms/src/components/inventory_line_detail.js b/shopfloor_inventory_mobile/static/wms/src/components/inventory_line_detail.js index 6682a45b4a..c1bec38116 100644 --- a/shopfloor_inventory_mobile/static/wms/src/components/inventory_line_detail.js +++ b/shopfloor_inventory_mobile/static/wms/src/components/inventory_line_detail.js @@ -32,8 +32,8 @@ export var inventory_line = Vue.component("inventory-line-detail", { /> - diff --git a/shopfloor_inventory_mobile/static/wms/src/components/inventory_qty_picker.js b/shopfloor_inventory_mobile/static/wms/src/components/inventory_qty_picker.js new file mode 100644 index 0000000000..a0cc2f4666 --- /dev/null +++ b/shopfloor_inventory_mobile/static/wms/src/components/inventory_qty_picker.js @@ -0,0 +1,312 @@ +/** + * Copyright 2020 Camptocamp SA (http://www.camptocamp.com) + * @author Simone Orsi + * Copyright 2021 Jacques-Etienne Baudoux (BCIM) + * @author Jacques-Etienne Baudoux + * License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + */ + +export var InventoryQtyPickerMixin = { + props: { + options: Object, + }, + data: function () { + return { + qty_done: 0, + qty_todo: 0, + qty_by_pkg: {}, + qty_by_pkg_manual: false, + }; + }, + watch: { + options: function (opts, old_opts) { + if (opts.counted_value != old_opts.counted_value) { + this.qty_done = parseInt(opts.counted_value, 10); + } + }, + qty_done: function () { + if (!this.qty_by_pkg_manual) + this.qty_by_pkg = this.product_qty_by_packaging(); + this.qty_by_pkg_manual = false; + }, + }, + methods: { + _handle_qty_error(event, input, new_qty) { + 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); + }); + }, + packaging_by_id: function (id) { + // Special case for UOM ids as they can clash w/ pkg ids + // we prefix it w/ "uom-" + id = id.startsWith("uom-") ? id : parseInt(id, 10); + return _.find(this.packaging, ["id", id]); + }, + /** + * + 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.qty_done); + }, + /** + * 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; + const 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); + 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 = this.unit_uom.rounding || 3; + 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 () { + this.qty_done = this._compute_qty(); + }, + }, + created: function () { + this.qty_todo = parseInt(this.opts.init_value, 10); + this.qty_done = parseInt(this.opts.counted_value, 10); + }, + computed: { + opts() { + const opts = _.defaults({}, this.$props.options, { + input_type: "text", + init_value: 0, + counted_value: 0, + mode: "", + available_packaging: [], + uom: {}, + pkg_name_key: "code", // This comes from packaging type + }); + return opts; + }, + unit_uom: function () { + let unit = {}; + if (!_.isEmpty(this.opts.uom)) { + // Create an object like the packaging + // to be used seamlessly in the widget. + unit = { + id: "uom-" + this.opts.uom.id, + name: this.opts.uom.name, + qty: this.opts.uom.factor, + rounding: this.opts.uom.rounding, + }; + } + return unit; + }, + packaging: function () { + let unit = []; + if (!_.isEmpty(this.unit_uom)) { + unit = [this.unit_uom]; + } + return _.concat(this.opts.available_packaging, unit); + }, + /** + * Sort packaging by qty and exclude the ones w/ qty = 0 + */ + sorted_packaging: function () { + return _.reverse( + _.sortBy(_.filter(this.packaging, _.property("qty")), _.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 = {}, + qty_per_pkg, + remaining, + elected_next_pkg; + const packaging = this.sorted_packaging; + _.forEach(packaging, function (pkg, i) { + const next_pkgs = packaging.slice(i + 1); + remaining = undefined; + _.every(next_pkgs, function (next_pkg) { + [qty_per_pkg, remaining] = self._qty_by_pkg(next_pkg.qty, pkg.qty); + elected_next_pkg = next_pkg; + return remaining; + }); + if (remaining === 0) { + res[pkg.id] = { + pkg: elected_next_pkg, + qty: qty_per_pkg, + }; + } + }); + return res; + }, + }, +}; + +export var InventoryQtyPicker = Vue.component("inventory-qty-picker", { + mixins: [InventoryQtyPickerMixin], + props: { + readonly: Boolean, + }, + data: function () { + return { + qty_todo: 0, + panel: 0, // expand panel by default + }; + }, + watch: { + qty_by_pkg: { + deep: true, + handler: function () { + // prevent watched qty_done to update again qty_by_pkg + this.qty_by_pkg_manual = true; + this.compute_qty(); + this.qty_by_pkg_manual = false; + }, + }, + }, + created: function () { + // Propagate the newly initialized quantity to the parent component + this.$root.trigger("qty_edit", this.qty_done); + }, + updated: function () { + this.$root.trigger("qty_edit", this.qty_done); + }, + computed: { + qty_color: function () { + if (this.qty_done == this.qty_todo) { + if (this.readonly) return ""; + return "background-color: rgb(143, 191, 68)"; + } + if (this.qty_done > this.qty_todo) { + return "background-color: orangered"; + } + return "background-color: pink"; + }, + }, + template: ` +
+ + + + + + + + + / {{ qty_todo }} + + + {{ unit_uom.name }} + + + + + + + + + +
{{ pkg.name }}
+
(x{{ contained_packaging[pkg.id].qty }} {{ contained_packaging[pkg.id].pkg.name }})
+
+
+
+
+
+
+`, +}); + +export var InventoryQtyPickerDisplay = Vue.component("inventory-qty-picker-display", { + mixins: [InventoryQtyPickerMixin], + methods: { + display_pkg: function (pkg) { + return this.opts.non_zero_only ? this.qty_by_pkg[pkg.id] > 0 : true; + }, + }, + computed: { + visible_packaging: function () { + return _.filter(this.sorted_packaging, this.display_pkg); + }, + }, + updated: function () { + this.qty_todo = parseInt(this.opts.init_value, 10); + this.qty_done = parseInt(this.opts.counted_value, 10); + }, + template: ` +
+ + + , + + ({{ qty_todo }} {{ unit_uom.name }}) +
+`, +}); diff --git a/shopfloor_inventory_mobile/templates/assets.xml b/shopfloor_inventory_mobile/templates/assets.xml index b6624e4f7e..7f4898556e 100644 --- a/shopfloor_inventory_mobile/templates/assets.xml +++ b/shopfloor_inventory_mobile/templates/assets.xml @@ -15,15 +15,21 @@ t-value="get_version('shopfloor_inventory_mobile')" />