odoo-addon-shopfloor-mobile 16.0.1.0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- odoo/addons/shopfloor_mobile/README.rst +215 -0
- odoo/addons/shopfloor_mobile/__init__.py +0 -0
- odoo/addons/shopfloor_mobile/__manifest__.py +17 -0
- odoo/addons/shopfloor_mobile/i18n/es_AR.po +39 -0
- odoo/addons/shopfloor_mobile/i18n/pt_BR.po +14 -0
- odoo/addons/shopfloor_mobile/i18n/shopfloor_mobile.pot +13 -0
- odoo/addons/shopfloor_mobile/readme/CONTRIBUTORS.rst +12 -0
- odoo/addons/shopfloor_mobile/readme/CREDITS.rst +5 -0
- odoo/addons/shopfloor_mobile/readme/DESCRIPTION.rst +31 -0
- odoo/addons/shopfloor_mobile/readme/HISTORY.rst +4 -0
- odoo/addons/shopfloor_mobile/readme/ROADMAP.rst +29 -0
- odoo/addons/shopfloor_mobile/readme/USAGE.rst +34 -0
- odoo/addons/shopfloor_mobile/static/description/icon.png +0 -0
- odoo/addons/shopfloor_mobile/static/description/index.html +555 -0
- odoo/addons/shopfloor_mobile/static/wms/.gitignore +21 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/batch_picking_detail.js +69 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/batch_picking_line_detail.js +141 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/detail/detail_location.js +66 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/detail/detail_lot.js +91 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/detail/detail_operation.js +50 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/detail/detail_package.js +73 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/detail/detail_picking.js +40 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/detail/detail_product.js +70 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/detail/detail_transfer.js +128 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/forms/form_edit_stock_picking.js +39 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/manual_select_color.js +24 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/misc.js +201 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/packaging-qty-picker.js +329 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/mixins.js +130 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_select.js +135 -0
- odoo/addons/shopfloor_mobile/static/wms/src/components/scenario_picking_detail/picking_summary.js +212 -0
- odoo/addons/shopfloor_mobile/static/wms/src/css/main.css +73 -0
- odoo/addons/shopfloor_mobile/static/wms/src/css/normalize.css +351 -0
- odoo/addons/shopfloor_mobile/static/wms/src/demo/demo.checkout.js +257 -0
- odoo/addons/shopfloor_mobile/static/wms/src/demo/demo.cluster_picking.js +188 -0
- odoo/addons/shopfloor_mobile/static/wms/src/demo/demo.delivery.js +79 -0
- odoo/addons/shopfloor_mobile/static/wms/src/demo/demo.location_content_transfer.js +179 -0
- odoo/addons/shopfloor_mobile/static/wms/src/demo/demo.scan_anything.js +124 -0
- odoo/addons/shopfloor_mobile/static/wms/src/demo/demo.single_pack_transfer.js +83 -0
- odoo/addons/shopfloor_mobile/static/wms/src/demo/demo.zone_picking.js +277 -0
- odoo/addons/shopfloor_mobile/static/wms/src/i18n/add_translations_to_registry.js +4 -0
- odoo/addons/shopfloor_mobile/static/wms/src/i18n/en.json +31 -0
- odoo/addons/shopfloor_mobile/static/wms/src/i18n/fr.json +27 -0
- odoo/addons/shopfloor_mobile/static/wms/src/scenario/checkout.js +390 -0
- odoo/addons/shopfloor_mobile/static/wms/src/scenario/checkout_states.js +380 -0
- odoo/addons/shopfloor_mobile/static/wms/src/scenario/cluster_picking.js +481 -0
- odoo/addons/shopfloor_mobile/static/wms/src/scenario/delivery.js +353 -0
- odoo/addons/shopfloor_mobile/static/wms/src/scenario/location_content_transfer.js +388 -0
- odoo/addons/shopfloor_mobile/static/wms/src/scenario/single_pack_transfer.js +132 -0
- odoo/addons/shopfloor_mobile/static/wms/src/scenario/zone_picking.js +838 -0
- odoo/addons/shopfloor_mobile/static/wms/src/screen.js +36 -0
- odoo/addons/shopfloor_mobile/static/wms/src/wms_utils.js +318 -0
- odoo/addons/shopfloor_mobile/templates/assets.xml +180 -0
- odoo_addon_shopfloor_mobile-16.0.1.0.0.6.dist-info/METADATA +235 -0
- odoo_addon_shopfloor_mobile-16.0.1.0.0.6.dist-info/RECORD +57 -0
- odoo_addon_shopfloor_mobile-16.0.1.0.0.6.dist-info/WHEEL +5 -0
- odoo_addon_shopfloor_mobile-16.0.1.0.0.6.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
|
|
3
|
+
* @author Simone Orsi <simahawk@gmail.com>
|
|
4
|
+
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* eslint-disable strict */
|
|
8
|
+
Vue.component("form-edit-stock-picking", {
|
|
9
|
+
props: ["record", "form"],
|
|
10
|
+
data: function () {
|
|
11
|
+
return {
|
|
12
|
+
form_values: {},
|
|
13
|
+
changed: false,
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
methods: {
|
|
17
|
+
on_select: function (selected, fname) {
|
|
18
|
+
this.$set(this.form_values, fname, selected.id);
|
|
19
|
+
this.changed = true;
|
|
20
|
+
this.$emit("change", {changed: this.changed, values: this.form_values});
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
template: `
|
|
24
|
+
<div :class="['form', $options._componentTag]">
|
|
25
|
+
|
|
26
|
+
<v-form ref="form">
|
|
27
|
+
<div class="fields-wrapper">
|
|
28
|
+
<separator-title>Change carrier</separator-title>
|
|
29
|
+
<manual-select
|
|
30
|
+
:records="form.carrier_id.select_options"
|
|
31
|
+
:options="{showActions: false, initValue: record.carrier.id}"
|
|
32
|
+
v-on:select="($event) => { on_select($event, 'carrier_id') }"
|
|
33
|
+
/>
|
|
34
|
+
</div>
|
|
35
|
+
</v-form>
|
|
36
|
+
|
|
37
|
+
</div>
|
|
38
|
+
`,
|
|
39
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2022 Jacques-Etienne Baudoux (BCIM) <je@bcim.be>
|
|
3
|
+
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Overriding manual-select from shopfloor_mobile_base
|
|
7
|
+
const Base = Vue.options.components["manual-select"];
|
|
8
|
+
const Custom = Base.extend({
|
|
9
|
+
methods: {
|
|
10
|
+
selected_color_klass(rec, modifier) {
|
|
11
|
+
let color;
|
|
12
|
+
if (rec && rec.qty_done && rec.quantity) {
|
|
13
|
+
if (rec.qty_done < rec.quantity)
|
|
14
|
+
color = this.utils.colors.color_for("item_selected_partial");
|
|
15
|
+
if (rec.qty_done > rec.quantity)
|
|
16
|
+
color = this.utils.colors.color_for("item_selected_excess");
|
|
17
|
+
if (color) return "active " + color + (modifier ? " " + modifier : "");
|
|
18
|
+
}
|
|
19
|
+
return Base.options.methods.selected_color_klass.call(this, rec, modifier);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
Vue.component("manual-select", Custom);
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
|
|
3
|
+
* @author Simone Orsi <simahawk@gmail.com>
|
|
4
|
+
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/* eslint-disable strict */
|
|
8
|
+
|
|
9
|
+
import {ItemDetailMixin} from "/shopfloor_mobile_base/static/wms/src/components/detail/detail_mixin.js";
|
|
10
|
+
|
|
11
|
+
// TODO: could be merged w/ userConfirmation
|
|
12
|
+
Vue.component("last-operation", {
|
|
13
|
+
data: function () {
|
|
14
|
+
return {info: {}};
|
|
15
|
+
},
|
|
16
|
+
template: `
|
|
17
|
+
<div class="last-operation">
|
|
18
|
+
<v-dialog persistent fullscreen tile value=true>
|
|
19
|
+
<v-alert tile type="info" prominent transition="scale-transition">
|
|
20
|
+
<v-card outlined color="blue lighten-1" class="message mt-10">
|
|
21
|
+
<v-card-title>This was the last operation of the document.</v-card-title>
|
|
22
|
+
<v-card-text>The next operation is ready to be processed.</v-card-text>
|
|
23
|
+
</v-card>
|
|
24
|
+
<v-form class="mt-10">
|
|
25
|
+
<v-btn x-large color="success" @click="$emit('confirm')">{{ $t('btn.ok.title') }}</v-btn>
|
|
26
|
+
</v-form>
|
|
27
|
+
</v-alert>
|
|
28
|
+
</v-dialog>
|
|
29
|
+
</div>
|
|
30
|
+
`,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
Vue.component("get-work", {
|
|
34
|
+
template: `
|
|
35
|
+
<div class="get-work fullscreen-buttons fullscreen-buttons-50">
|
|
36
|
+
<btn-action id="btn-get-work" @click="$emit('get_work')">
|
|
37
|
+
{{ $t('misc.btn_get_work') }}
|
|
38
|
+
</btn-action>
|
|
39
|
+
<btn-action id="btn-manual" color="default" @click="$emit('manual_selection')">
|
|
40
|
+
{{ $t('misc.btn_manual_selection') }}
|
|
41
|
+
</btn-action>
|
|
42
|
+
</div>
|
|
43
|
+
`,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Vue.component("stock-zero-check", {
|
|
47
|
+
template: `
|
|
48
|
+
<div class="stock-zero-check">
|
|
49
|
+
<v-dialog fullscreen tile value=true class="actions fullscreen">
|
|
50
|
+
<v-card>
|
|
51
|
+
<div class="button-list button-vertical-list">
|
|
52
|
+
<v-row align="center">
|
|
53
|
+
<v-col class="text-center" cols="12">
|
|
54
|
+
<v-btn x-large color="primary" @click="$emit('action', 'action_confirm_zero')">
|
|
55
|
+
{{ $t('misc.stock_zero_check.confirm_stock_zero') }}
|
|
56
|
+
</v-btn>
|
|
57
|
+
</v-col>
|
|
58
|
+
</v-row>
|
|
59
|
+
<v-row align="center">
|
|
60
|
+
<v-col class="text-center" cols="12">
|
|
61
|
+
<v-btn x-large color="warning" @click="$emit('action', 'action_confirm_not_zero')">
|
|
62
|
+
{{ $t('misc.stock_zero_check.confirm_stock_not_zero') }}
|
|
63
|
+
</v-btn>
|
|
64
|
+
</v-col>
|
|
65
|
+
</v-row>
|
|
66
|
+
</div>
|
|
67
|
+
</v-card>
|
|
68
|
+
</v-dialog>
|
|
69
|
+
</div>
|
|
70
|
+
`,
|
|
71
|
+
});
|
|
72
|
+
Vue.component("empty-location-icon", {
|
|
73
|
+
mixins: [ItemDetailMixin],
|
|
74
|
+
template: `
|
|
75
|
+
<v-icon color="orange" :class="$options._componentTag" v-if="record.location_will_be_empty">mdi-alert-rhombus-outline</v-icon>
|
|
76
|
+
`,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
Vue.component("select-zone-item", {
|
|
80
|
+
mixins: [ItemDetailMixin],
|
|
81
|
+
template: `
|
|
82
|
+
<div :class="$options._componentTag">
|
|
83
|
+
<div class="detail-field mt-2 title">
|
|
84
|
+
<span class="counters">({{ $t("misc.lines_count", record) }})</span>
|
|
85
|
+
<span class="name font-weight-bold">{{ record.name }}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<div v-for="op_type in record.operation_types" :key="make_component_key([op_type.id])">
|
|
88
|
+
<div class="detail-field mt-2">
|
|
89
|
+
<span class="counters">({{ $t("misc.lines_count", op_type) }})</span>
|
|
90
|
+
<span class="name">{{ op_type.name }}</span>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
`,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
Vue.component("cancel-move-line-action", {
|
|
98
|
+
props: {
|
|
99
|
+
record: {
|
|
100
|
+
type: Object,
|
|
101
|
+
},
|
|
102
|
+
options: {
|
|
103
|
+
type: Object,
|
|
104
|
+
default: function () {
|
|
105
|
+
// Take control of which package key (source or destination) is used
|
|
106
|
+
// to cancel the line when cancel line action is available.
|
|
107
|
+
return {
|
|
108
|
+
package_cancel_key: "package_dest",
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
data() {
|
|
114
|
+
return {
|
|
115
|
+
dialog: false,
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
methods: {
|
|
119
|
+
on_user_confirm: function (answer) {
|
|
120
|
+
this.dialog = false;
|
|
121
|
+
if (answer === "yes") {
|
|
122
|
+
let data = {};
|
|
123
|
+
if (this.the_package) {
|
|
124
|
+
data = {package_id: this.the_package.id};
|
|
125
|
+
} else {
|
|
126
|
+
data = {line_id: this.record.id};
|
|
127
|
+
}
|
|
128
|
+
this.$root.trigger("cancel_picking_line", data);
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
computed: {
|
|
133
|
+
// `package` is a reserved identifier!
|
|
134
|
+
the_package: function () {
|
|
135
|
+
return _.result(this.record, this.$props.options.package_cancel_key);
|
|
136
|
+
},
|
|
137
|
+
message: function () {
|
|
138
|
+
const item = this.the_package
|
|
139
|
+
? this.the_package.name
|
|
140
|
+
: this.record.product.name;
|
|
141
|
+
return "Please confirm cancellation for " + item;
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
template: `
|
|
145
|
+
<div class="action action-destroy">
|
|
146
|
+
<v-dialog v-model="dialog" fullscreen tile class="actions fullscreen text-center">
|
|
147
|
+
<template v-slot:activator="{ on }">
|
|
148
|
+
<v-btn icon class="destroy" x-large rounded color="error" v-on="on"><v-icon>mdi-close-circle</v-icon></v-btn>
|
|
149
|
+
</template>
|
|
150
|
+
<v-card>
|
|
151
|
+
<user-confirmation
|
|
152
|
+
v-on:user-confirmation="on_user_confirm"
|
|
153
|
+
v-bind:question="message"></user-confirmation>
|
|
154
|
+
</v-card>
|
|
155
|
+
</v-dialog>
|
|
156
|
+
</div>
|
|
157
|
+
`,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
Vue.component("picking-list-item-progress-bar", {
|
|
161
|
+
mixins: [ItemDetailMixin],
|
|
162
|
+
computed: {
|
|
163
|
+
value() {
|
|
164
|
+
if (!_.isUndefined(this.record.progress)) {
|
|
165
|
+
return this.record.progress;
|
|
166
|
+
}
|
|
167
|
+
return this.utils.wms.picking_completeness(this.record);
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
template: `
|
|
171
|
+
<div :class="$options._componentTag">
|
|
172
|
+
<v-progress-linear :value="value" color="success" height="8"></v-progress-linear>
|
|
173
|
+
</div>
|
|
174
|
+
`,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
Vue.component("line-stock-out", {
|
|
178
|
+
methods: {
|
|
179
|
+
handle_action(action) {
|
|
180
|
+
this.$emit(action);
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
template: `
|
|
184
|
+
<div :class="$options._componentTag">
|
|
185
|
+
<div class="button-list button-vertical-list full">
|
|
186
|
+
<v-row align="center">
|
|
187
|
+
<v-col class="text-center" cols="12">
|
|
188
|
+
<btn-action @click="handle_action('confirm_stock_issue')">
|
|
189
|
+
{{ $t('misc.stock_zero_check.confirm_stock_zero') }}
|
|
190
|
+
</btn-action>
|
|
191
|
+
</v-col>
|
|
192
|
+
</v-row>
|
|
193
|
+
<v-row align="center">
|
|
194
|
+
<v-col class="text-center" cols="12">
|
|
195
|
+
<btn-back />
|
|
196
|
+
</v-col>
|
|
197
|
+
</v-row>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
`,
|
|
201
|
+
});
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2020 Camptocamp SA (http://www.camptocamp.com)
|
|
3
|
+
* @author Simone Orsi <simahawk@gmail.com>
|
|
4
|
+
* Copyright 2021 Jacques-Etienne Baudoux (BCIM)
|
|
5
|
+
* @author Jacques-Etienne Baudoux <je@bcim.be>
|
|
6
|
+
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export var PackagingQtyPickerMixin = {
|
|
10
|
+
props: {
|
|
11
|
+
options: Object, // options are replaced by props
|
|
12
|
+
mode: String,
|
|
13
|
+
qtyInit: Number,
|
|
14
|
+
uom: {type: Object, required: true},
|
|
15
|
+
availablePackaging: Array,
|
|
16
|
+
pkgNameKey: String, // "code" or "name"
|
|
17
|
+
},
|
|
18
|
+
data: function () {
|
|
19
|
+
return {
|
|
20
|
+
qty: parseInt(this.qtyInit, 10),
|
|
21
|
+
qty_by_pkg: {},
|
|
22
|
+
qty_by_pkg_manual: false,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
watch: {
|
|
26
|
+
qtyInit: function () {
|
|
27
|
+
this.qty = parseInt(this.qtyInit, 10);
|
|
28
|
+
},
|
|
29
|
+
qty: {
|
|
30
|
+
handler() {
|
|
31
|
+
if (!this.qty_by_pkg_manual) {
|
|
32
|
+
this.qty_by_pkg = this.product_qty_by_packaging();
|
|
33
|
+
}
|
|
34
|
+
this.qty_by_pkg_manual = false;
|
|
35
|
+
},
|
|
36
|
+
immediate: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
methods: {
|
|
40
|
+
_handle_qty_error(event, input, new_qty) {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
// Make it red and shake it
|
|
43
|
+
$(input)
|
|
44
|
+
.closest(".inner-wrapper")
|
|
45
|
+
.addClass("error shake-it")
|
|
46
|
+
.delay(800)
|
|
47
|
+
.queue(function () {
|
|
48
|
+
// End animation
|
|
49
|
+
$(this)
|
|
50
|
+
.removeClass("error shake-it", 2000, "easeInOutQuad")
|
|
51
|
+
.dequeue();
|
|
52
|
+
// Restore value
|
|
53
|
+
$(input).val(new_qty);
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
packaging_by_id: function (id) {
|
|
57
|
+
// Special case for UOM ids as they can clash w/ pkg ids
|
|
58
|
+
// we prefix it w/ "uom-"
|
|
59
|
+
id = id.startsWith("uom-") ? id : parseInt(id, 10);
|
|
60
|
+
return _.find(this.sorted_packaging, ["id", id]);
|
|
61
|
+
},
|
|
62
|
+
/**
|
|
63
|
+
*
|
|
64
|
+
Calculate quantity by packaging.
|
|
65
|
+
|
|
66
|
+
Limitation: fractional quantities are lost.
|
|
67
|
+
|
|
68
|
+
:prod_qty:
|
|
69
|
+
:min_unit: minimal unit of measure as a tuple (qty, name).
|
|
70
|
+
Default: to UoM unit.
|
|
71
|
+
:returns: list of tuple in the form [(qty_per_package, package_name)]
|
|
72
|
+
|
|
73
|
+
* @param {*} prod_qty total qty to satisfy.
|
|
74
|
+
* @param {*} min_unit minimal unit of measure as a tuple (qty, name).
|
|
75
|
+
Default: to UoM unit.
|
|
76
|
+
*/
|
|
77
|
+
product_qty_by_packaging: function () {
|
|
78
|
+
return this._product_qty_by_packaging(this.sorted_packaging, this.qty);
|
|
79
|
+
},
|
|
80
|
+
/**
|
|
81
|
+
* Produce a list of tuple of packaging qty and packaging name.
|
|
82
|
+
* TODO: refactor to handle fractional quantities (eg: 0.5 Kg)
|
|
83
|
+
*
|
|
84
|
+
* @param {*} pkg_by_qty packaging records sorted by major qty
|
|
85
|
+
* @param {*} qty total qty to satisfy
|
|
86
|
+
*/
|
|
87
|
+
_product_qty_by_packaging: function (pkg_by_qty, qty) {
|
|
88
|
+
const self = this;
|
|
89
|
+
const res = {};
|
|
90
|
+
// Const min_unit = _.last(pkg_by_qty);
|
|
91
|
+
pkg_by_qty.forEach(function (pkg) {
|
|
92
|
+
let qty_per_pkg = 0;
|
|
93
|
+
[qty_per_pkg, qty] = self._qty_by_pkg(pkg.qty, qty);
|
|
94
|
+
res[pkg.id] = qty_per_pkg;
|
|
95
|
+
if (!qty) return;
|
|
96
|
+
});
|
|
97
|
+
return res;
|
|
98
|
+
},
|
|
99
|
+
/**
|
|
100
|
+
* Calculate qty needed for given package qty.
|
|
101
|
+
*
|
|
102
|
+
* @param {*} pkg_by_qty
|
|
103
|
+
* @param {*} qty
|
|
104
|
+
*/
|
|
105
|
+
_qty_by_pkg: function (pkg_qty, qty) {
|
|
106
|
+
const precision = this.unit_uom.rounding || 3;
|
|
107
|
+
const remainder = _.round(qty % pkg_qty, precision);
|
|
108
|
+
const qty_for_pkg = (qty - remainder) / pkg_qty;
|
|
109
|
+
return [qty_for_pkg, remainder];
|
|
110
|
+
},
|
|
111
|
+
_compute_qty: function () {
|
|
112
|
+
const self = this;
|
|
113
|
+
let value = 0;
|
|
114
|
+
_.forEach(this.qty_by_pkg, function (qty, id) {
|
|
115
|
+
value += self.packaging_by_id(id).qty * qty;
|
|
116
|
+
});
|
|
117
|
+
return value;
|
|
118
|
+
},
|
|
119
|
+
compute_qty: function () {
|
|
120
|
+
this.qty = this._compute_qty();
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
computed: {
|
|
124
|
+
unit_uom: function () {
|
|
125
|
+
let unit = {};
|
|
126
|
+
if (!_.isEmpty(this.uom)) {
|
|
127
|
+
// Create an object like the packaging
|
|
128
|
+
// to be used seamlessly in the widget.
|
|
129
|
+
unit = {
|
|
130
|
+
id: "uom-" + this.uom.id,
|
|
131
|
+
name: this.uom.name,
|
|
132
|
+
qty: this.uom.factor,
|
|
133
|
+
rounding: this.uom.rounding,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return unit;
|
|
137
|
+
},
|
|
138
|
+
/**
|
|
139
|
+
* Sort packaging by qty and exclude the ones w/ qty = 0
|
|
140
|
+
* Include the uom
|
|
141
|
+
*/
|
|
142
|
+
sorted_packaging: function () {
|
|
143
|
+
let packagings = _.reverse(
|
|
144
|
+
_.sortBy(
|
|
145
|
+
_.filter(this.availablePackaging, _.property("qty")),
|
|
146
|
+
_.property("qty")
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
let unit = [];
|
|
150
|
+
if (!_.isEmpty(this.unit_uom)) {
|
|
151
|
+
unit = [this.unit_uom];
|
|
152
|
+
}
|
|
153
|
+
return _.concat(packagings, unit);
|
|
154
|
+
},
|
|
155
|
+
/**
|
|
156
|
+
* Collect qty of contained packaging inside bigger packaging.
|
|
157
|
+
* Eg: "1 Pallet" contains "4 Big boxes".
|
|
158
|
+
*/
|
|
159
|
+
contained_packaging: function () {
|
|
160
|
+
const self = this;
|
|
161
|
+
let res = {},
|
|
162
|
+
qty_per_pkg,
|
|
163
|
+
remaining,
|
|
164
|
+
elected_next_pkg;
|
|
165
|
+
const packaging = this.sorted_packaging;
|
|
166
|
+
_.forEach(packaging, function (pkg, i) {
|
|
167
|
+
const next_pkgs = packaging.slice(i + 1);
|
|
168
|
+
remaining = undefined;
|
|
169
|
+
_.every(next_pkgs, function (next_pkg) {
|
|
170
|
+
[qty_per_pkg, remaining] = self._qty_by_pkg(next_pkg.qty, pkg.qty);
|
|
171
|
+
elected_next_pkg = next_pkg;
|
|
172
|
+
return remaining;
|
|
173
|
+
});
|
|
174
|
+
if (remaining === 0) {
|
|
175
|
+
res[pkg.id] = {
|
|
176
|
+
pkg: elected_next_pkg,
|
|
177
|
+
qty: qty_per_pkg,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return res;
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export var PackagingQtyPicker = Vue.component("packaging-qty-picker", {
|
|
187
|
+
mixins: [PackagingQtyPickerMixin],
|
|
188
|
+
props: {
|
|
189
|
+
readonly: Boolean,
|
|
190
|
+
qtyTodo: {type: Number, required: true},
|
|
191
|
+
pkgNameKey: {default: "name"},
|
|
192
|
+
},
|
|
193
|
+
data: function () {
|
|
194
|
+
return {
|
|
195
|
+
panel: 0, // expand panel by default
|
|
196
|
+
};
|
|
197
|
+
},
|
|
198
|
+
watch: {
|
|
199
|
+
qty_by_pkg: {
|
|
200
|
+
deep: true,
|
|
201
|
+
handler: function () {
|
|
202
|
+
// prevent watched qty to update again qty_by_pkg
|
|
203
|
+
this.qty_by_pkg_manual = true;
|
|
204
|
+
this.compute_qty();
|
|
205
|
+
this.qty_by_pkg_manual = false;
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
created: function () {
|
|
210
|
+
// Propagate the newly initialized quantity to the parent component
|
|
211
|
+
this.$root.trigger("qty_edit", this.qty);
|
|
212
|
+
},
|
|
213
|
+
updated: function () {
|
|
214
|
+
this.$root.trigger("qty_edit", this.qty);
|
|
215
|
+
},
|
|
216
|
+
computed: {
|
|
217
|
+
qty_color: function () {
|
|
218
|
+
if (this.qty == this.qtyTodo) {
|
|
219
|
+
if (this.readonly) return "";
|
|
220
|
+
return "background-color: rgb(143, 191, 68)";
|
|
221
|
+
}
|
|
222
|
+
if (this.qty > this.qtyTodo) {
|
|
223
|
+
return "background-color: orangered";
|
|
224
|
+
}
|
|
225
|
+
return "background-color: pink";
|
|
226
|
+
},
|
|
227
|
+
qty_todo_by_pkg: function () {
|
|
228
|
+
// Used to calculate the qty needed of each package type
|
|
229
|
+
// based on the qty todo.
|
|
230
|
+
let total_qty_todo = this.qtyTodo;
|
|
231
|
+
const res = {};
|
|
232
|
+
this.sorted_packaging.forEach((pkg) => {
|
|
233
|
+
let pkg_units = 0;
|
|
234
|
+
while (pkg.qty <= total_qty_todo) {
|
|
235
|
+
pkg_units++;
|
|
236
|
+
total_qty_todo -= pkg.qty;
|
|
237
|
+
}
|
|
238
|
+
res[pkg.id] = pkg_units;
|
|
239
|
+
});
|
|
240
|
+
return res;
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
template: `
|
|
244
|
+
<div :class="[$options._componentTag, mode ? 'mode-' + mode : '']">
|
|
245
|
+
<v-expansion-panels flat v-model="panel">
|
|
246
|
+
<v-expansion-panel>
|
|
247
|
+
<v-expansion-panel-header expand-icon="mdi-menu-down">
|
|
248
|
+
<v-row dense align="center">
|
|
249
|
+
<v-col cols="5" md="3">
|
|
250
|
+
<input type="number" v-model="qty" class="qty-done" :style="qty_color"
|
|
251
|
+
v-on:click.stop
|
|
252
|
+
:readonly="readonly"
|
|
253
|
+
/>
|
|
254
|
+
</v-col>
|
|
255
|
+
<v-col cols="3" md="2" v-if="!readonly">
|
|
256
|
+
<span class="qty-todo">/ {{ qtyTodo }}</span>
|
|
257
|
+
</v-col>
|
|
258
|
+
<v-col>
|
|
259
|
+
{{ unit_uom.name }}
|
|
260
|
+
</v-col>
|
|
261
|
+
</v-row>
|
|
262
|
+
</v-expansion-panel-header>
|
|
263
|
+
<v-expansion-panel-content v-if="sorted_packaging.length > 1">
|
|
264
|
+
<v-row dense
|
|
265
|
+
align="center"
|
|
266
|
+
v-for="(pkg, index) in sorted_packaging"
|
|
267
|
+
:key="make_component_key([pkg.id])"
|
|
268
|
+
:class="(readonly && !qty_by_pkg[pkg.id]) ? 'd-none' : ''"
|
|
269
|
+
>
|
|
270
|
+
<v-col cols="4" md="2">
|
|
271
|
+
<input type="text" inputmode="decimal" class="qty-done"
|
|
272
|
+
v-model.lazy="qty_by_pkg[pkg.id]"
|
|
273
|
+
:data-origvalue="qty_by_pkg[pkg.id]"
|
|
274
|
+
:data-pkg="JSON.stringify(pkg)"
|
|
275
|
+
:readonly="readonly"
|
|
276
|
+
@focus="!readonly && ($event.target.value='')"
|
|
277
|
+
@blur="$event.target.value=qty_by_pkg[pkg.id]"
|
|
278
|
+
/>
|
|
279
|
+
</v-col>
|
|
280
|
+
<v-col cols="2" md="2" v-if="!readonly">
|
|
281
|
+
<span class="qty-todo">/ {{ qty_todo_by_pkg[pkg.id] }}</span>
|
|
282
|
+
</v-col>
|
|
283
|
+
<v-col>
|
|
284
|
+
<div class="pkg-name"> {{ pkg[pkgNameKey] }}</div>
|
|
285
|
+
<div v-if="contained_packaging[pkg.id]" class="pkg-qty">(x{{ contained_packaging[pkg.id].qty }} {{ contained_packaging[pkg.id].pkg.name }})</div>
|
|
286
|
+
</v-col>
|
|
287
|
+
</v-row>
|
|
288
|
+
</v-expansion-panel-content>
|
|
289
|
+
</v-expansion-panel>
|
|
290
|
+
</v-expansion-panels>
|
|
291
|
+
</div>
|
|
292
|
+
`,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
export var PackagingQtyPickerDisplay = Vue.component("packaging-qty-picker-display", {
|
|
296
|
+
mixins: [PackagingQtyPickerMixin],
|
|
297
|
+
props: {
|
|
298
|
+
nonZeroOnly: Boolean,
|
|
299
|
+
pkgNameKey: {default: "code"},
|
|
300
|
+
},
|
|
301
|
+
methods: {
|
|
302
|
+
display_pkg: function (pkg) {
|
|
303
|
+
return this.nonZeroOnly ? this.qty_by_pkg[pkg.id] > 0 : true;
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
computed: {
|
|
307
|
+
visible_packaging: function () {
|
|
308
|
+
let packagings = _.filter(this.sorted_packaging, this.display_pkg);
|
|
309
|
+
// Do not display if only uom packaging
|
|
310
|
+
if (
|
|
311
|
+
packagings.length == 1 &&
|
|
312
|
+
packagings[0].id.toString().startsWith("uom-")
|
|
313
|
+
)
|
|
314
|
+
return [];
|
|
315
|
+
return packagings;
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
template: `
|
|
319
|
+
<div :class="[$options._componentTag, mode ? 'mode-' + mode: '', 'd-inline']">
|
|
320
|
+
<span class="min-unit">{{ qty }} {{ unit_uom.name }}</span>
|
|
321
|
+
<span class="packaging" v-for="(pkg, index) in visible_packaging" :key="make_component_key([pkg.id])">
|
|
322
|
+
<span v-if="index == 0">(</span>
|
|
323
|
+
<span class="pkg-qty" v-text="qty_by_pkg[pkg.id]" />
|
|
324
|
+
<span class="pkg-name" v-text="pkg[pkgNameKey] || unit_uom.name" /><span class="sep" v-if="index != Object.keys(visible_packaging).length - 1"> + </span>
|
|
325
|
+
<span v-if="index == visible_packaging.length - 1">)</span>
|
|
326
|
+
</span>
|
|
327
|
+
</div>
|
|
328
|
+
`,
|
|
329
|
+
});
|