质量模块和库存扫码
This commit is contained in:
21
stock_barcode/static/src/components/grouped_line.js
Normal file
21
stock_barcode/static/src/components/grouped_line.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import LineComponent from "@stock_barcode/components/line";
|
||||
|
||||
export default class GroupedLineComponent extends LineComponent {
|
||||
|
||||
get isSelected() {
|
||||
return this.line.virtual_ids.indexOf(this.env.model.selectedLineVirtualId) !== -1;
|
||||
}
|
||||
|
||||
get opened() {
|
||||
return this.env.model.groupKey(this.line) === this.env.model.unfoldLineKey;
|
||||
}
|
||||
|
||||
toggleSublines(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.toggleSublines(this.line);
|
||||
}
|
||||
}
|
||||
GroupedLineComponent.components = { LineComponent };
|
||||
GroupedLineComponent.template = 'stock_barcode.GroupedLineComponent';
|
||||
28
stock_barcode/static/src/components/grouped_line.scss
Normal file
28
stock_barcode/static/src/components/grouped_line.scss
Normal file
@@ -0,0 +1,28 @@
|
||||
.o_barcode_client_action .o_barcode_lines {
|
||||
.o_barcode_line_summary {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
flex-basis: 100%;
|
||||
|
||||
&.o_unfolded {
|
||||
border-bottom-width: 2px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.o_sublines .o_barcode_line {
|
||||
border-left-width: 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
box-shadow: none;
|
||||
border-bottom: 0;
|
||||
|
||||
&.o_selected {
|
||||
box-shadow: inset 0px 0px 0px 3px $o-enterprise-primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
stock_barcode/static/src/components/grouped_line.xml
Normal file
27
stock_barcode/static/src/components/grouped_line.xml
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="stock_barcode.GroupedLineComponent" owl="1">
|
||||
<div t-on-click="select"
|
||||
class="o_barcode_line list-group-item d-flex flex-row flex-wrap pt-0"
|
||||
t-att-data-barcode="line.product_id.barcode" t-attf-class="{{componentClasses}}">
|
||||
<div class="o_barcode_line_summary d-flex flex-grow-1 py-2 mt-2" t-att-class="opened ? 'o_unfolded': ''">
|
||||
<div class="o_barcode_line_details flex-grow-1">
|
||||
<t t-call="stock_barcode.LineSourceLocation"/>
|
||||
<t t-call="stock_barcode.LineTitle"/>
|
||||
<t t-call="stock_barcode.LineQuantity"/>
|
||||
<t t-call="stock_barcode.LineDestinationLocation"/>
|
||||
</div>
|
||||
<button t-on-click="toggleSublines" class="o_line_button o_toggle_sublines btn btn-primary ms-2 ms-sm-4">
|
||||
<i t-att-class="'fa fa-2x fa-caret-' + (opened ? 'up' : 'down')"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="o_sublines mb-2 flex-grow-1" t-if="opened">
|
||||
<t t-foreach="line.lines" t-as="subline" t-key="subline.virtual_id">
|
||||
<LineComponent line="subline" displayUOM="props.displayUOM" subline="true"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
125
stock_barcode/static/src/components/line.js
Normal file
125
stock_barcode/static/src/components/line.js
Normal file
@@ -0,0 +1,125 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { bus } from 'web.core';
|
||||
const { Component } = owl;
|
||||
|
||||
export default class LineComponent extends Component {
|
||||
get destinationLocationPath () {
|
||||
return this._getLocationPath(this.env.model._defaultDestLocation(), this.line.location_dest_id);
|
||||
}
|
||||
|
||||
get displayDestinationLocation() {
|
||||
return !this.props.subline && this.env.model.displayDestinationLocation;
|
||||
}
|
||||
|
||||
get displayResultPackage() {
|
||||
return this.env.model.displayResultPackage;
|
||||
}
|
||||
|
||||
get displaySourceLocation() {
|
||||
return !this.props.subline && this.env.model.displaySourceLocation;
|
||||
}
|
||||
|
||||
get highlightLocation() {
|
||||
return this.env.model.lastScanned.sourceLocation &&
|
||||
this.env.model.lastScanned.sourceLocation.id == this.line.location_id.id;
|
||||
}
|
||||
|
||||
get isComplete() {
|
||||
if (!this.qtyDemand || this.qtyDemand != this.qtyDone) {
|
||||
return false;
|
||||
} else if (this.isTracked && !this.lotName) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
get isSelected() {
|
||||
return this.line.virtual_id === this.env.model.selectedLineVirtualId ||
|
||||
(this.line.package_id && this.line.package_id.id === this.env.model.lastScanned.packageId);
|
||||
}
|
||||
|
||||
get isTracked() {
|
||||
return this.line.product_id.tracking !== 'none';
|
||||
}
|
||||
|
||||
get lotName() {
|
||||
return (this.line.lot_id && this.line.lot_id.name) || this.line.lot_name || '';
|
||||
}
|
||||
|
||||
get nextExpected() {
|
||||
if (!this.isSelected) {
|
||||
return false;
|
||||
} else if (this.isTracked && !this.lotName) {
|
||||
return 'lot';
|
||||
} else if (this.qtyDemand && this.qtyDone < this.qtyDemand) {
|
||||
return 'quantity';
|
||||
}
|
||||
}
|
||||
|
||||
get qtyDemand() {
|
||||
return this.env.model.getQtyDemand(this.line);
|
||||
}
|
||||
|
||||
get qtyDone() {
|
||||
return this.env.model.getQtyDone(this.line);
|
||||
}
|
||||
|
||||
get quantityIsSet() {
|
||||
return this.line.inventory_quantity_set;
|
||||
}
|
||||
|
||||
get incrementQty() {
|
||||
return this.env.model.getIncrementQuantity(this.line);
|
||||
}
|
||||
|
||||
get line() {
|
||||
return this.props.line;
|
||||
}
|
||||
|
||||
get requireLotNumber() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get sourceLocationPath() {
|
||||
return this._getLocationPath(this.env.model._defaultLocation(), this.line.location_id);
|
||||
}
|
||||
|
||||
get componentClasses() {
|
||||
return [
|
||||
this.isComplete ? 'o_line_completed' : 'o_line_not_completed',
|
||||
this.env.model.lineIsFaulty(this) ? 'o_faulty' : '',
|
||||
this.isSelected ? 'o_selected o_highlight' : ''
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
_getLocationPath(rootLocation, currentLocation) {
|
||||
let locationName = currentLocation.display_name;
|
||||
if (this.env.model.shouldShortenLocationName) {
|
||||
if (rootLocation && rootLocation.id != currentLocation.id) {
|
||||
const name = rootLocation.display_name;
|
||||
locationName = locationName.replace(name, '...');
|
||||
}
|
||||
}
|
||||
return locationName.replace(new RegExp(currentLocation.name + '$'), '');
|
||||
}
|
||||
|
||||
edit() {
|
||||
bus.trigger('edit-line', { line: this.line });
|
||||
}
|
||||
|
||||
addQuantity(quantity, ev) {
|
||||
this.env.model.updateLineQty(this.line.virtual_id, quantity);
|
||||
}
|
||||
|
||||
select(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.selectLine(this.line);
|
||||
this.env.model.trigger('update');
|
||||
}
|
||||
|
||||
setOnHandQuantity(ev) {
|
||||
this.env.model.setOnHandQuantity(this.line);
|
||||
}
|
||||
}
|
||||
LineComponent.template = 'stock_barcode.LineComponent';
|
||||
128
stock_barcode/static/src/components/line.scss
Normal file
128
stock_barcode/static/src/components/line.scss
Normal file
@@ -0,0 +1,128 @@
|
||||
$o-barcode-completed-color: #befdcc;
|
||||
|
||||
.o_barcode_client_action .o_barcode_lines .o_barcode_line {
|
||||
flex: 0 0 auto;
|
||||
border-width: 1px 0;
|
||||
|
||||
&:first-child {
|
||||
border-top-width: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
box-shadow: 0 3px 10px $gray-300;
|
||||
margin-bottom: 60vh;
|
||||
}
|
||||
|
||||
&_details {
|
||||
.fa:first-child {
|
||||
opacity: 0.5;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&.o_barcode_line_package {
|
||||
.o_barcode_line_details > * {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.o_barcode_line_details > .o_barcode_package_name {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
|
||||
> span {
|
||||
max-width: 100%;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.o_faulty {
|
||||
background-color: rgba(map-get($theme-colors, 'danger'), 0.25);
|
||||
|
||||
&.o_selected {
|
||||
box-shadow: inset 0px 0px 0px 3px map-get($theme-colors, 'danger');
|
||||
}
|
||||
}
|
||||
|
||||
&.o_line_completed {
|
||||
background: var(--barcode__line--completed, #befdcc);
|
||||
}
|
||||
|
||||
&.o_line_not_completed {
|
||||
background: var(--barcode__line--notCompleted, #fcf9f2);
|
||||
}
|
||||
|
||||
&.o_selected {
|
||||
box-shadow: inset 0px 0px 0px 3px $o-enterprise-primary-color;
|
||||
}
|
||||
|
||||
.o_barcode_scanner_qty {
|
||||
font-size: 1em;
|
||||
border-color: transparent; // Overwrite default badge color
|
||||
margin-left: -$badge-padding-x; // Compensate badge padding
|
||||
|
||||
&[class*="badge-"] {
|
||||
margin-left: 0; // If a style class is applied, reset compensation margin
|
||||
}
|
||||
|
||||
.qty-done, .inventory_quantity {
|
||||
min-width: 20px;
|
||||
&.o_flash {
|
||||
animation-name: highlighting-flash-primary;
|
||||
animation-duration: 0.5s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_line_buttons {
|
||||
min-width: 132px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.o_line_button {
|
||||
min-width: 60px;
|
||||
height: 60px;
|
||||
padding: 0 8px;
|
||||
border-radius: 8px;
|
||||
line-height: 16px;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-transform: none;
|
||||
|
||||
&.o_shortcut_displayed {
|
||||
padding-top: 14px;
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
@include o-hover-opacity();
|
||||
}
|
||||
|
||||
&.o_set {
|
||||
border: 4px solid $o-brand-primary;
|
||||
color: $o-brand-primary;
|
||||
|
||||
&.o_difference {
|
||||
color: orange;
|
||||
border-color: orange;
|
||||
}
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
background-color: gray;
|
||||
border-color: gray;
|
||||
}
|
||||
}
|
||||
|
||||
.o_next_expected {
|
||||
color: #00A09D;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
[name=source_location].o_highlight {
|
||||
background-color: $o-barcode-completed-color;
|
||||
& .fa { opacity: 1; }
|
||||
}
|
||||
}
|
||||
136
stock_barcode/static/src/components/line.xml
Normal file
136
stock_barcode/static/src/components/line.xml
Normal file
@@ -0,0 +1,136 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
<!-- Line's sub-elements -->
|
||||
<t t-name="stock_barcode.LineTitle" owl="1">
|
||||
<t t-if="props.line.product_id.default_code or props.line.product_id.code">
|
||||
<div class="o_barcode_line_title">
|
||||
<i class="fa fa-fw fa-tags"/>
|
||||
<span t-if="props.line.product_id.default_code"
|
||||
class="o_barcode_product_ref h5 fw-bold"
|
||||
t-esc="props.line.product_id.default_code"/>
|
||||
<span t-if="props.line.product_id.code != props.line.product_id.default_code"
|
||||
class="o_barcode_partner_code ms-1 h5 text-muted"
|
||||
t-esc="props.line.product_id.code"/>
|
||||
</div>
|
||||
<div>
|
||||
<i class="fa fa-fw"/>
|
||||
<span class="product-label" t-esc="props.line.product_id.display_name"/>
|
||||
</div>
|
||||
</t>
|
||||
<div t-else="" class="o_barcode_line_title pb-1">
|
||||
<i class="fa fa-fw fa-tags"/>
|
||||
<span class="product-label" t-esc="props.line.product_id.display_name"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="stock_barcode.LineQuantity" owl="1">
|
||||
<div name="quantity">
|
||||
<i class="fa fa-fw fa-cube" t-attf-class="{{nextExpected === 'quantity' ? 'o_next_expected' : ''}}"/>
|
||||
<span t-attf-class="o_barcode_scanner_qty font-monospace badge #{' '}">
|
||||
<span class="qty-done d-inline-block text-start"
|
||||
t-attf-class="
|
||||
{{nextExpected === 'quantity' && qtyDone ? 'o_flash' : ''}}
|
||||
{{isSelected && qtyDemand && qtyDone && qtyDone < qtyDemand ? 'fw-bolder' : ''}}"
|
||||
t-esc="env.model.IsNotSet(line) ? '?' : qtyDone"/>
|
||||
<span t-if="qtyDemand" t-esc="'/ ' + qtyDemand"/>
|
||||
</span>
|
||||
<span t-if="props.displayUOM" t-esc="line.product_uom_id.name"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="stock_barcode.LineOwner" owl="1">
|
||||
<div t-if="line.owner_id">
|
||||
<i class="fa fa-fw fa-user-o"/>
|
||||
<span class="o_line_owner" t-esc="line.owner_id.display_name"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="stock_barcode.LineSourceLocation" owl="1">
|
||||
<div name="source_location" t-if="displaySourceLocation" title="Source Location"
|
||||
t-attf-class="{{line.location_id.usage != 'internal' ? 'text-danger' : ''}} {{highlightLocation ? 'o_highlight' : ''}}">
|
||||
<i class="fa fa-fw fa-sign-out"/>
|
||||
<span class="o_line_source_location fst-italic text-muted">
|
||||
<t t-esc="sourceLocationPath"/>
|
||||
<span t-esc="line.location_id.name"
|
||||
t-attf-class="
|
||||
{{highlightLocation ? 'fw-bold' : ''}}
|
||||
{{line.location_id.usage != 'internal' ? 'text-danger' : 'text-black'}}"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<t t-name="stock_barcode.LineDestinationLocation" owl="1">
|
||||
<div name="destination_location" t-if="displayDestinationLocation" title="Destination Location"
|
||||
t-att-class="line.location_dest_id.usage != 'internal' ? 'text-danger' : ''">
|
||||
<i class="fa fa-fw fa-sign-in"/>
|
||||
<span class="o_line_destination_location fst-italic text-muted">
|
||||
<t t-esc="destinationLocationPath"/>
|
||||
<span t-esc="line.location_dest_id.name"
|
||||
t-attf-class="
|
||||
{{env.model.lastScanned.destLocation && env.model.lastScanned.destLocation.id == line.location_dest_id.id ? 'fw-bold' : ''}}
|
||||
{{line.location_dest_id.usage != 'internal' ? 'text-danger' : 'text-black'}}"/>
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
<!-- Line's template -->
|
||||
<t t-name="stock_barcode.LineComponent" owl="1">
|
||||
<div t-on-click="select"
|
||||
class="o_barcode_line list-group-item d-flex flex-row flex-nowrap"
|
||||
t-att-data-virtual-id="line.virtual_id" t-attf-class="{{componentClasses}}"
|
||||
t-att-data-barcode="line.product_id.barcode">
|
||||
<div class="o_barcode_line_details flex-grow-1 flex-column flex-nowrap">
|
||||
<t t-call="stock_barcode.LineSourceLocation"/>
|
||||
<!-- Hides product's name if it's a subline, as it is already on the parent line. -->
|
||||
<t t-if="!props.subline" t-call="stock_barcode.LineTitle"/>
|
||||
<div t-if="isTracked and requireLotNumber" name="lot">
|
||||
<i class="fa fa-fw fa-barcode" t-attf-class="{{nextExpected === 'lot' ? 'o_next_expected' : ''}}"/>
|
||||
<span class="o_line_lot_name" t-esc="lotName"/>
|
||||
</div>
|
||||
<t t-call="stock_barcode.LineQuantity"/>
|
||||
<div t-if="line.package_id || line.result_package_id" name="package">
|
||||
<i class="fa fa-fw fa-archive"/>
|
||||
<span t-if="line.package_id" class="package" t-esc="line.package_id.name"/>
|
||||
<i t-if="displayResultPackage" class="fa fa-long-arrow-right mx-1"/>
|
||||
<span t-if="line.result_package_id" class="result-package" t-esc="line.result_package_id.name"/>
|
||||
<span t-if="line.result_package_id && line.result_package_id.package_type_id"
|
||||
class="fst-italic text-muted">
|
||||
(<t t-esc="line.result_package_id.package_type_id.name"/>)
|
||||
</span>
|
||||
</div>
|
||||
<t t-call="stock_barcode.LineOwner"/>
|
||||
<t t-call="stock_barcode.LineDestinationLocation"/>
|
||||
</div>
|
||||
<div class="o_line_buttons">
|
||||
<button t-on-click="edit" class="o_line_button o_edit btn"
|
||||
t-att-class="this.env.model.lineCanBeEdited(line) ? 'btn-secondary' : ''"
|
||||
t-att-disabled="!this.env.model.lineCanBeEdited(line)">
|
||||
<i class="fa fa-2x fa-pencil"/>
|
||||
</button>
|
||||
<button t-if="env.model.displaySetButton" t-on-click="setOnHandQuantity"
|
||||
class="o_line_button o_set btn ms-2 ms-sm-4"
|
||||
t-attf-class="{{quantityIsSet && qtyDone != qtyDemand ? 'o_difference' : ''}}">
|
||||
<i t-if="quantityIsSet" class="fa fa-2x"
|
||||
t-attf-class="{{qtyDone == qtyDemand ? 'fa-check' : 'fa-times'}}"/>
|
||||
</button>
|
||||
<span t-attf-class="{{env.model.incrementButtonsDisplayStyle}}">
|
||||
<button t-if="env.model.getDisplayDecrementBtn(line)" name="decrementButton" t-on-click="(ev) => this.addQuantity(-1, ev)"
|
||||
class="o_line_button o_remove_unit btn btn-primary ms-2 ms-sm-4"
|
||||
t-attf-disabled="{{qtyDone <= 0 || qtyDone == '?'}}">-1</button>
|
||||
<button t-if="env.model.getDisplayIncrementBtn(line)" name="incrementButton"
|
||||
t-on-click="(ev) => this.addQuantity(incrementQty, ev)" t-esc="'+' + incrementQty"
|
||||
t-att-disabled="!this.env.model.lineCanBeEdited(line)"
|
||||
class="o_line_button o_add_quantity btn btn-primary ms-2 ms-sm-4"/>
|
||||
</span>
|
||||
<button t-if="isSelected and env.model.getDisplayIncrementPackagingBtn(line)" name="incrementPackagingButton"
|
||||
t-on-click="(ev) => this.addQuantity(line.product_packaging_uom_qty, ev)"
|
||||
class="o_line_button w-100 btn btn-primary my-3 d-block">
|
||||
<div class="text-capitalize">
|
||||
+ <t t-esc="line.product_packaging_id.name"/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
391
stock_barcode/static/src/components/main.js
Normal file
391
stock_barcode/static/src/components/main.js
Normal file
@@ -0,0 +1,391 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { ChatterContainer } from '@mail/components/chatter_container/chatter_container';
|
||||
|
||||
import BarcodePickingModel from '@stock_barcode/models/barcode_picking_model';
|
||||
import BarcodeQuantModel from '@stock_barcode/models/barcode_quant_model';
|
||||
import { bus } from 'web.core';
|
||||
import config from 'web.config';
|
||||
import GroupedLineComponent from '@stock_barcode/components/grouped_line';
|
||||
import LineComponent from '@stock_barcode/components/line';
|
||||
import PackageLineComponent from '@stock_barcode/components/package_line';
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import * as BarcodeScanner from '@web/webclient/barcode/barcode_scanner';
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { View } from "@web/views/view";
|
||||
|
||||
const { Component, onMounted, onPatched, onWillStart, onWillUnmount, useSubEnv } = owl;
|
||||
|
||||
/**
|
||||
* Main Component
|
||||
* Gather the line information.
|
||||
* Manage the scan and save process.
|
||||
*/
|
||||
|
||||
class MainComponent extends Component {
|
||||
//--------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
setup() {
|
||||
this.rpc = useService('rpc');
|
||||
this.orm = useService('orm');
|
||||
this.notification = useService('notification');
|
||||
this.props.model = this.props.action.res_model;
|
||||
this.props.id = this.props.action.context.active_id;
|
||||
const model = this._getModel(this.props);
|
||||
useSubEnv({model});
|
||||
this._scrollBehavior = 'smooth';
|
||||
this.isMobile = config.device.isMobile;
|
||||
|
||||
onWillStart(async () => {
|
||||
const barcodeData = await this.rpc(
|
||||
'/stock_barcode/get_barcode_data',
|
||||
{
|
||||
model: this.props.model,
|
||||
res_id: this.props.id || false,
|
||||
}
|
||||
);
|
||||
this.groups = barcodeData.groups;
|
||||
this.env.model.setData(barcodeData);
|
||||
this.env.model.on('process-action', this, this._onDoAction);
|
||||
this.env.model.on('notification', this, this._onNotification);
|
||||
this.env.model.on('refresh', this, this._onRefreshState);
|
||||
this.env.model.on('update', this, () => this.render(true));
|
||||
this.env.model.on('do-action', this, args => bus.trigger('do-action', args));
|
||||
this.env.model.on('history-back', this, () => this.env.config.historyBack());
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
bus.on('barcode_scanned', this, this._onBarcodeScanned);
|
||||
bus.on('edit-line', this, this._onEditLine);
|
||||
bus.on('exit', this, this.exit);
|
||||
bus.on('open-package', this, this._onOpenPackage);
|
||||
bus.on('refresh', this, this._onRefreshState);
|
||||
bus.on('warning', this, this._onWarning);
|
||||
});
|
||||
|
||||
onWillUnmount(() => {
|
||||
bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
bus.off('edit-line', this, this._onEditLine);
|
||||
bus.off('exit', this, this.exit);
|
||||
bus.off('open-package', this, this._onOpenPackage);
|
||||
bus.off('refresh', this, this._onRefreshState);
|
||||
bus.off('warning', this, this._onWarning);
|
||||
});
|
||||
|
||||
onPatched(() => {
|
||||
this._scrollToSelectedLine();
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Public
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
get displayHeaderInfoAsColumn() {
|
||||
return this.env.model.isDone || this.env.model.isCancelled;
|
||||
}
|
||||
|
||||
get displayBarcodeApplication() {
|
||||
return this.env.model.view === 'barcodeLines';
|
||||
}
|
||||
|
||||
get displayBarcodeActions() {
|
||||
return this.env.model.view === 'actionsView';
|
||||
}
|
||||
|
||||
get displayBarcodeLines() {
|
||||
return this.displayBarcodeApplication && this.env.model.canBeProcessed;
|
||||
}
|
||||
|
||||
get displayInformation() {
|
||||
return this.env.model.view === 'infoFormView';
|
||||
}
|
||||
|
||||
get displayNote() {
|
||||
return !this._hideNote && this.env.model.record.note;
|
||||
}
|
||||
|
||||
get displayPackageContent() {
|
||||
return this.env.model.view === 'packagePage';
|
||||
}
|
||||
|
||||
get displayProductPage() {
|
||||
return this.env.model.view === 'productPage';
|
||||
}
|
||||
|
||||
get lineFormViewData() {
|
||||
const data = this.env.model.viewsWidgetData;
|
||||
data.context = data.additionalContext;
|
||||
data.resId = this._editedLineParams && this._editedLineParams.currentId;
|
||||
return data;
|
||||
}
|
||||
|
||||
get highlightValidateButton() {
|
||||
return this.env.model.highlightValidateButton;
|
||||
}
|
||||
|
||||
get info() {
|
||||
return this.env.model.barcodeInfo;
|
||||
}
|
||||
|
||||
get isTransfer() {
|
||||
return this.currentSourceLocation && this.currentDestinationLocation;
|
||||
}
|
||||
|
||||
get lines() {
|
||||
return this.env.model.groupedLines;
|
||||
}
|
||||
|
||||
get mobileScanner() {
|
||||
return BarcodeScanner.isBarcodeScannerSupported();
|
||||
}
|
||||
|
||||
get packageLines() {
|
||||
return this.env.model.packageLines;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_getModel(params) {
|
||||
const { rpc, orm, notification } = this;
|
||||
if (params.model === 'stock.picking') {
|
||||
return new BarcodePickingModel(params, { rpc, orm, notification });
|
||||
} else if (params.model === 'stock.quant') {
|
||||
return new BarcodeQuantModel(params, { rpc, orm, notification });
|
||||
} else {
|
||||
throw new Error('No JS model define');
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
async cancel() {
|
||||
await this.env.model.save();
|
||||
const action = await this.orm.call(
|
||||
this.props.model,
|
||||
'action_cancel_from_barcode',
|
||||
[[this.props.id]]
|
||||
);
|
||||
const onClose = res => {
|
||||
if (res && res.cancelled) {
|
||||
this.env.model._cancelNotification();
|
||||
this.env.config.historyBack();
|
||||
}
|
||||
};
|
||||
bus.trigger('do-action', {
|
||||
action,
|
||||
options: {
|
||||
on_close: onClose.bind(this),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async openMobileScanner() {
|
||||
const barcode = await BarcodeScanner.scanBarcode();
|
||||
if (barcode) {
|
||||
this.env.model.processBarcode(barcode);
|
||||
if ('vibrate' in window.navigator) {
|
||||
window.navigator.vibrate(100);
|
||||
}
|
||||
} else {
|
||||
this.env.services.notification.notify({
|
||||
type: 'warning',
|
||||
message: this.env._t("Please, Scan again !"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async exit(ev) {
|
||||
if (this.displayBarcodeApplication) {
|
||||
await this.env.model.save();
|
||||
this.env.config.historyBack();
|
||||
} else {
|
||||
this.toggleBarcodeLines();
|
||||
}
|
||||
}
|
||||
|
||||
hideNote(ev) {
|
||||
this._hideNote = true;
|
||||
this.render();
|
||||
}
|
||||
|
||||
async openProductPage() {
|
||||
if (!this._editedLineParams) {
|
||||
await this.env.model.save();
|
||||
}
|
||||
this.env.model.displayProductPage();
|
||||
}
|
||||
|
||||
async print(action, method) {
|
||||
await this.env.model.save();
|
||||
const options = this.env.model._getPrintOptions();
|
||||
if (options.warning) {
|
||||
return this.env.model.notification.add(options.warning, { type: 'warning' });
|
||||
}
|
||||
if (!action && method) {
|
||||
action = await this.orm.call(
|
||||
this.props.model,
|
||||
method,
|
||||
[[this.props.id]]
|
||||
);
|
||||
}
|
||||
bus.trigger('do-action', { action, options });
|
||||
}
|
||||
|
||||
putInPack(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model._putInPack();
|
||||
}
|
||||
|
||||
saveFormView(lineRecord) {
|
||||
const lineId = (lineRecord && lineRecord.data.id) || (this._editedLineParams && this._editedLineParams.currentId);
|
||||
const recordId = (lineRecord.resModel === this.props.model) ? lineId : undefined
|
||||
this._onRefreshState({ recordId, lineId });
|
||||
}
|
||||
|
||||
toggleBarcodeActions(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.displayBarcodeActions();
|
||||
}
|
||||
|
||||
async toggleBarcodeLines(lineId) {
|
||||
this._editedLineParams = undefined;
|
||||
await this.env.model.displayBarcodeLines(lineId);
|
||||
}
|
||||
|
||||
async toggleInformation() {
|
||||
await this.env.model.save();
|
||||
this.env.model.displayInformation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `validate` on the model and then triggers up the action because OWL
|
||||
* components don't seem able to manage wizard without doing custom things.
|
||||
*
|
||||
* @param {OdooEvent} ev
|
||||
*/
|
||||
async validate(ev) {
|
||||
ev.stopPropagation();
|
||||
await this.env.model.validate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler called when a barcode is scanned.
|
||||
*
|
||||
* @private
|
||||
* @param {string} barcode
|
||||
*/
|
||||
_onBarcodeScanned(barcode) {
|
||||
if (this.displayBarcodeApplication) {
|
||||
this.env.model.processBarcode(barcode);
|
||||
}
|
||||
}
|
||||
|
||||
_scrollToSelectedLine() {
|
||||
if (!this.displayBarcodeLines) {
|
||||
this._scrollBehavior = 'auto';
|
||||
return;
|
||||
}
|
||||
let selectedLine = document.querySelector('.o_sublines .o_barcode_line.o_highlight');
|
||||
const isSubline = Boolean(selectedLine);
|
||||
if (!selectedLine) {
|
||||
selectedLine = document.querySelector('.o_barcode_line.o_highlight');
|
||||
}
|
||||
if (!selectedLine) {
|
||||
const matchingLine = this.env.model.findLineForCurrentLocation();
|
||||
if (matchingLine) {
|
||||
selectedLine = document.querySelector(`.o_barcode_line[data-virtual-id="${matchingLine.virtual_id}"]`);
|
||||
}
|
||||
}
|
||||
if (selectedLine) {
|
||||
// If a line is selected, checks if this line is on the top of the
|
||||
// page, and if it's not, scrolls until the line is on top.
|
||||
const header = document.querySelector('.o_barcode_header');
|
||||
const lineRect = selectedLine.getBoundingClientRect();
|
||||
const navbar = document.querySelector('.o_main_navbar');
|
||||
const page = document.querySelector('.o_barcode_lines');
|
||||
// Computes the real header's height (the navbar is present if the page was refreshed).
|
||||
const headerHeight = navbar ? navbar.offsetHeight + header.offsetHeight : header.offsetHeight;
|
||||
if (lineRect.top < headerHeight || lineRect.bottom > (headerHeight + lineRect.height)) {
|
||||
let top = lineRect.top - headerHeight + page.scrollTop;
|
||||
if (isSubline) {
|
||||
const parentLine = selectedLine.closest('.o_barcode_lines > .o_barcode_line');
|
||||
const parentSummary = parentLine.querySelector('.o_barcode_line_summary');
|
||||
top -= parentSummary.getBoundingClientRect().height;
|
||||
}
|
||||
page.scroll({ left: 0, top, behavior: this._scrollBehavior });
|
||||
this._scrollBehavior = 'smooth';
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
async _onDoAction(ev) {
|
||||
bus.trigger('do-action', {
|
||||
action: ev,
|
||||
options: {
|
||||
on_close: this._onRefreshState.bind(this),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async _onEditLine(ev) {
|
||||
let { line } = ev;
|
||||
const virtualId = line.virtual_id;
|
||||
await this.env.model.save();
|
||||
// Updates the line id if it's missing, in order to open the line form view.
|
||||
if (!line.id && virtualId) {
|
||||
line = this.env.model.pageLines.find(l => Number(l.dummy_id) === virtualId);
|
||||
}
|
||||
this._editedLineParams = this.env.model.getEditedLineParams(line);
|
||||
await this.openProductPage();
|
||||
}
|
||||
|
||||
_onNotification(notifParams) {
|
||||
const { message } = notifParams;
|
||||
delete notifParams.message;
|
||||
this.env.services.notification.add(message, notifParams);
|
||||
}
|
||||
|
||||
_onOpenPackage(packageId) {
|
||||
this._inspectedPackageId = packageId;
|
||||
this.env.model.displayPackagePage();
|
||||
}
|
||||
|
||||
async _onRefreshState(paramsRefresh) {
|
||||
const { recordId, lineId } = paramsRefresh || {}
|
||||
const { route, params } = this.env.model.getActionRefresh(recordId);
|
||||
const result = await this.rpc(route, params);
|
||||
await this.env.model.refreshCache(result.data.records);
|
||||
await this.toggleBarcodeLines(lineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles triggered warnings. It can happen from an onchange for example.
|
||||
*
|
||||
* @param {CustomEvent} ev
|
||||
*/
|
||||
_onWarning(ev) {
|
||||
const { title, message } = ev.detail;
|
||||
this.env.services.dialog.add(ConfirmationDialog, { title, body: message });
|
||||
}
|
||||
}
|
||||
MainComponent.template = 'stock_barcode.MainComponent';
|
||||
MainComponent.components = {
|
||||
View,
|
||||
GroupedLineComponent,
|
||||
LineComponent,
|
||||
PackageLineComponent,
|
||||
ChatterContainer,
|
||||
};
|
||||
|
||||
registry.category("actions").add("stock_barcode_client_action", MainComponent);
|
||||
|
||||
export default MainComponent;
|
||||
270
stock_barcode/static/src/components/main.scss
Normal file
270
stock_barcode/static/src/components/main.scss
Normal file
@@ -0,0 +1,270 @@
|
||||
.o_barcode_client_action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: $o-view-background-color;
|
||||
overflow: auto;
|
||||
|
||||
.o_strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
// Top navbar
|
||||
// =====================================
|
||||
.o_barcode_header {
|
||||
flex: 0 0 46px;
|
||||
color: white;
|
||||
background-color: var(--barcode__header-bg, #{$o-brand-odoo});
|
||||
|
||||
.nav-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
.nav-link, .navbar-text {
|
||||
font-size: 16px;
|
||||
color: #FFFFFF;
|
||||
|
||||
&:hover {
|
||||
color: rgba($color: #FFFFFF, $alpha: 0.75)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Top Block
|
||||
// =====================================
|
||||
.o_barcode_message {
|
||||
box-shadow: inset 0 0 20px $gray-900;
|
||||
|
||||
.o_barcode_pic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1 1 60%;
|
||||
max-width: 200px;
|
||||
.fa-exclamation-triangle {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Summary
|
||||
// =====================================
|
||||
.o_barcode_lines_header {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
background-color: var(--barcode__linesHeader-bg, #{$o-gray-800});
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Lines Block
|
||||
// =====================================
|
||||
.o_barcode_lines {
|
||||
clear: both;
|
||||
flex: auto;
|
||||
overflow: auto;
|
||||
color: $gray-800;
|
||||
margin-bottom: 60px;
|
||||
@media (orientation: portrait) {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
&.o_js_has_highlight .o_barcode_line.o_highlight {
|
||||
&.o_highlight_green {
|
||||
box-shadow: inset 0px 0px 0px 3px $o-brand-secondary;
|
||||
}
|
||||
|
||||
.product-label, .o_barcode_scanner_qty {
|
||||
color: $headings-color;
|
||||
}
|
||||
|
||||
.qty-done, .inventory_quantity {
|
||||
font-weight: bold;
|
||||
|
||||
&.o_js_qty_animate {
|
||||
animation: o_barcode_scanner_qty_update .2s alternate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Embedded views
|
||||
// =====================================
|
||||
.o_barcode_generic_view {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.o_view_controller, .o_view_controller .o_form_view.o_form_nosheet {
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.o_field_one2many.o_field_widget .o_kanban_record {
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.o_form_view {
|
||||
&.o_xxs_form_view {
|
||||
.o_td_label > .o_form_label {
|
||||
color: $gray-900;
|
||||
font-weight: bold;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.o_field_widget {
|
||||
font-size: 1em;
|
||||
.btn.fa {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
.o_list_view {
|
||||
th, .o_field_widget {
|
||||
font-size: $font-size-base;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.o_form_nosheet {
|
||||
padding-bottom: 80px;
|
||||
}
|
||||
.o_kanban_record {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Settings menu
|
||||
// =====================================
|
||||
.o_barcode_settings {
|
||||
display: flex;
|
||||
flex: auto;
|
||||
|
||||
> button {
|
||||
flex: 1 0 auto;
|
||||
border-bottom: 1px solid $border-color;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Control buttons (validate, previous,
|
||||
// next, put in pack, ...)
|
||||
// =====================================
|
||||
.o_barcode_control {
|
||||
flex: 0 0 60px;
|
||||
margin: 0 -1px;
|
||||
width: 100%;
|
||||
> .btn {
|
||||
flex: 1;
|
||||
width: 50%;
|
||||
@media (orientation: portrait) {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
@media (orientation: landscape) {
|
||||
height: 60px;
|
||||
}
|
||||
border-width: 1px 0 0 0;
|
||||
border-style: solid;
|
||||
&.btn-secondary {
|
||||
color: $gray-800;
|
||||
border-color: $gray-400;
|
||||
}
|
||||
&.btn-primary {
|
||||
border-color: $primary;
|
||||
}
|
||||
&.btn-success {
|
||||
border-color: $success;
|
||||
}
|
||||
&[disabled] {
|
||||
opacity: 1;
|
||||
background-color: $gray-200;
|
||||
color: $btn-link-disabled-color;
|
||||
}
|
||||
+ .btn {
|
||||
border-left-width: 1px;
|
||||
border-left-color: $gray-400;
|
||||
}
|
||||
}
|
||||
.fa-angle-left, .fa-angle-right {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
&:first-of-type {
|
||||
box-shadow: 0 -3px 10px $gray-300;
|
||||
}
|
||||
}
|
||||
|
||||
// Line form
|
||||
// =====================================
|
||||
.o_barcode_line_form {
|
||||
margin-left: 24px;
|
||||
margin-bottom: 36px;
|
||||
font-size: 1.4em;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.row {
|
||||
width: 700px;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
&.row-long {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
a.o_field_widget {
|
||||
display: inline-block;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
// Avoids to make the UoM field as long as the quantity done field.
|
||||
.o_field_widget[name="product_uom_id"] input {
|
||||
@include media-breakpoint-up(sm) {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.o_qty_done_field_completed input {
|
||||
background: var(--barcode__input--completed, #f6fdf6);
|
||||
}
|
||||
|
||||
.o_qty_done_field_not_completed input {
|
||||
background: var(--barcode__input--notCompleted, #fcf9f2);
|
||||
}
|
||||
|
||||
& > div {
|
||||
.o_field_float {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.o_input {
|
||||
padding: 8px;
|
||||
border: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.o_required_modifier .o_input {
|
||||
border-bottom: 2px solid $border-color
|
||||
}
|
||||
|
||||
.o_dropdown_button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
i {
|
||||
min-width: 24px;
|
||||
max-width: 24px;
|
||||
color: $o-main-color-muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
127
stock_barcode/static/src/components/main.xml
Normal file
127
stock_barcode/static/src/components/main.xml
Normal file
@@ -0,0 +1,127 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="stock_barcode.MainComponent" class="o_content o_barcode_client_action" owl="1">
|
||||
<div class="o_barcode_header">
|
||||
<div class="navbar navbar-expand navbar-dark">
|
||||
<nav class="navbar-nav me-auto">
|
||||
<button t-on-click="exit" class="o_exit btn nav-link me-4">
|
||||
<i class="fa fa-chevron-left"/>
|
||||
</button>
|
||||
<span class="o_title navbar-text" t-esc="env.model.name"/>
|
||||
</nav>
|
||||
<nav class="navbar-nav">
|
||||
<t t-if="displayBarcodeApplication">
|
||||
<button t-if="env.model.formViewId" t-on-click="toggleInformation" class="o_show_information btn nav-link">
|
||||
<i class="fa fa-info-circle"/>
|
||||
</button>
|
||||
<button t-if="mobileScanner" class="o_stock_mobile_barcode btn nav-link" t-on-click="openMobileScanner">
|
||||
<i class="fa fa-barcode"/>
|
||||
</button>
|
||||
<button t-on-click="toggleBarcodeActions" class="o_barcode_actions btn nav-link">
|
||||
<i class="fa fa-cog"/>
|
||||
</button>
|
||||
</t>
|
||||
<button t-else="" t-on-click="() => this.toggleBarcodeLines()" class="o_close btn nav-link">
|
||||
<i class="fa fa-times"/>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<t t-if="displayBarcodeApplication">
|
||||
<div t-if="displayNote" class="alert alert-warning text-start mb-0">
|
||||
<button type="button" class="close" title="Close" aria-label="Close" t-on-click="hideNote">×</button>
|
||||
<t t-esc="env.model.record.note"/>
|
||||
</div>
|
||||
<div class="o_barcode_lines_header row alert m-0 px-1 "
|
||||
t-attf-class="{{displayHeaderInfoAsColumn ? 'flex-column justify-content-center align-items-center' : 'justify-content-between'}}">
|
||||
<div t-if="info.warning" class="o_barcode_pic position-relative text-center mt-2 mb-1">
|
||||
<i class="fa fa-5x fa-exclamation-triangle"/>
|
||||
</div>
|
||||
<div name="barcode_messages" class="d-flex align-items-center justify-content-center w-100">
|
||||
<span t-if="info.icon" class="fa fa-3x me-3" t-attf-class="fa-{{info.icon}}"/>
|
||||
<span class="o_scan_message" t-attf-class="o_{{info.class}}">
|
||||
<span t-if="info.warning" name="warning" class="fa fa-exclamation-triangle me-1"/>
|
||||
<span t-out="info.message"/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="displayBarcodeLines && (lines.length || packageLines.length)" class="o_barcode_lines"> <!-- Lines -->
|
||||
<t t-foreach="lines" t-as="line" t-key="line.virtual_id">
|
||||
<GroupedLineComponent t-if="line.lines" line="line" displayUOM="groups.group_uom"/>
|
||||
<LineComponent t-else="" line="line" displayUOM="groups.group_uom"/>
|
||||
</t>
|
||||
<t t-foreach="packageLines" t-as="line" t-key="line.virtual_id">
|
||||
<PackageLineComponent line="line" displayUOM="false"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="displayProductPage"> <!-- Barcode Line Edit Form View -->
|
||||
<View type="'form'" mode="'edit'"
|
||||
viewId="env.model.lineFormViewId"
|
||||
resModel="lineFormViewData.resModel"
|
||||
resId="lineFormViewData.resId"
|
||||
display="{ controlPanel: false }"
|
||||
context="lineFormViewData.context"
|
||||
onSave="(record) => this.saveFormView(record)"
|
||||
onDiscard="() => this.toggleBarcodeLines()"/>
|
||||
</div>
|
||||
<div t-if="displayPackageContent"> <!-- Quants (in package) Kanban View -->
|
||||
<View type="'kanban'"
|
||||
viewId="packageKanbanViewId"
|
||||
display="{ controlPanel: false }"
|
||||
resModel="'stock.quant'"
|
||||
domain="[['package_id', '=', _inspectedPackageId]]"/>
|
||||
</div>
|
||||
<div t-if="displayInformation"> <!-- Res Model Form View -->
|
||||
<View type="'form'" mode="'edit'"
|
||||
viewId="env.model.formViewId"
|
||||
display="{ controlPanel: false }"
|
||||
resModel="props.model"
|
||||
resId="props.id"
|
||||
onSave="() => this._onRefreshState({ lineId: this._editedLineParams && this._editedLineParams.currentId })"
|
||||
onDiscard="() => this.toggleBarcodeLines()"/>
|
||||
<ChatterContainer threadModel="props.model" threadId="props.id"/>
|
||||
</div>
|
||||
<div t-if="displayBarcodeActions" class="o_barcode_settings flex-column h100">
|
||||
<t t-foreach="env.model.printButtons" t-as="button" t-key="button.class">
|
||||
<button class="btn-lg btn btn-light text-uppercase"
|
||||
t-attf-class="{{button.class}}" t-esc="button.name"
|
||||
t-on-click="() => this.print(button.action, button.method)"/>
|
||||
</t>
|
||||
<button t-if="env.model.displayCancelButton"
|
||||
t-on-click="cancel"
|
||||
class="o_cancel_operation btn-lg btn btn-light text-uppercase">
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
<div t-if="displayBarcodeLines" class="fixed-bottom"> <!-- Footer -->
|
||||
<div class="o_barcode_control o_action_buttons d-flex">
|
||||
<button class="o_add_line btn btn-secondary text-uppercase" t-on-click="openProductPage">
|
||||
<i class="fa fa-plus me-1"/> Add Product
|
||||
</button>
|
||||
<button t-if="env.model.displayPutInPackButton" t-on-click="putInPack"
|
||||
t-att-disabled="!env.model.canPutInPack"
|
||||
class="o_put_in_pack btn btn-secondary text-uppercase">
|
||||
<i class="fa fa-cube me-1"/> Put In Pack
|
||||
</button>
|
||||
<button t-if="env.model.displayValidateButton" t-on-click="validate"
|
||||
class="btn text-uppercase o_validate_page"
|
||||
t-att-disabled="!env.model.canBeValidate"
|
||||
t-attf-class="{{highlightValidateButton ? 'btn-success' : 'btn-secondary'}}">
|
||||
<i class="fa fa-check me-1"/> Validate
|
||||
</button>
|
||||
<button t-if="env.model.displayApplyButton" t-on-click="() => this.env.model.apply()"
|
||||
class="btn text-uppercase o_apply_page"
|
||||
t-att-disabled="env.model.applyOn === 0"
|
||||
t-attf-class="{{highlightValidateButton ? 'btn-success' : 'btn-secondary'}}">
|
||||
<i class="fa fa-check me-1"/> Apply
|
||||
<span t-attf-class="{{highlightValidateButton ? '' : 'text-muted'}}">
|
||||
(<t t-esc="env.model.applyOn"/>)
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
41
stock_barcode/static/src/components/package_line.js
Normal file
41
stock_barcode/static/src/components/package_line.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { bus } from 'web.core';
|
||||
import LineComponent from './line';
|
||||
|
||||
export default class PackageLineComponent extends LineComponent {
|
||||
get componentClasses() {
|
||||
return [
|
||||
this.qtyDone == 1 ? 'o_line_completed' : 'o_line_not_completed',
|
||||
this.isSelected ? 'o_selected o_highlight' : ''
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
get isSelected() {
|
||||
return this.line.package_id.id === this.env.model.lastScanned.packageId;
|
||||
}
|
||||
|
||||
get qtyDemand() {
|
||||
return this.props.line.reservedPackage ? 1 : false;
|
||||
}
|
||||
|
||||
get qtyDone() {
|
||||
const reservedQuantity = this.line.lines.reduce((r, l) => r + l.reserved_uom_qty, 0);
|
||||
const doneQuantity = this.line.lines.reduce((r, l) => r + l.qty_done, 0);
|
||||
if (reservedQuantity > 0) {
|
||||
return doneQuantity / reservedQuantity;
|
||||
}
|
||||
return doneQuantity >= 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
openPackage() {
|
||||
bus.trigger('open-package', this.line.package_id.id);
|
||||
}
|
||||
|
||||
select(ev) {
|
||||
ev.stopPropagation();
|
||||
this.env.model.selectPackageLine(this.line);
|
||||
this.env.model.trigger('update');
|
||||
}
|
||||
}
|
||||
PackageLineComponent.template = 'stock_barcode.PackageLineComponent';
|
||||
24
stock_barcode/static/src/components/package_line.xml
Normal file
24
stock_barcode/static/src/components/package_line.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="stock_barcode.PackageLineComponent" owl="1" t-on-click="select"
|
||||
class="o_barcode_line list-group-item d-flex flex-row flex-nowrap py-3"
|
||||
t-attf-class="{{componentClasses}}" t-att-data-package="line.package_id.name">
|
||||
<div class="o_barcode_line_details flex-grow-1 flex-column flex-nowrap">
|
||||
<t t-call="stock_barcode.LineSourceLocation"/>
|
||||
<div>
|
||||
<i class="fa fa-fw fa-archive"/>
|
||||
<t t-esc="line.package_id.name"/>
|
||||
<i class="fa fa-long-arrow-right mx-1"/>
|
||||
<t t-esc="line.result_package_id.name"/>
|
||||
</div>
|
||||
<t t-call="stock_barcode.LineQuantity"/>
|
||||
<t t-call="stock_barcode.LineOwner"/>
|
||||
<t t-call="stock_barcode.LineDestinationLocation"/>
|
||||
</div>
|
||||
<button t-on-click="openPackage" class="o_line_button o_package_content btn btn-secondary ms-2 ms-sm-4">
|
||||
<i class="fa fa-2x fa-dropbox"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,60 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { KanbanController } from '@web/views/kanban/kanban_controller';
|
||||
import { bus } from 'web.core';
|
||||
|
||||
const { onMounted, onWillUnmount } = owl;
|
||||
|
||||
export class StockBarcodeKanbanController extends KanbanController {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
onMounted(() => {
|
||||
bus.on('barcode_scanned', this, this._onBarcodeScannedHandler);
|
||||
document.activeElement.blur();
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
bus.off('barcode_scanned', this, this._onBarcodeScannedHandler);
|
||||
});
|
||||
}
|
||||
|
||||
openRecord(record) {
|
||||
this.actionService.doAction('stock_barcode.stock_barcode_picking_client_action', {
|
||||
additionalContext: { active_id: record.resId },
|
||||
});
|
||||
}
|
||||
|
||||
async createRecord() {
|
||||
const action = await this.model.orm.call(
|
||||
'stock.picking',
|
||||
'action_open_new_picking',
|
||||
[], { context: this.props.context }
|
||||
);
|
||||
if (action) {
|
||||
return this.actionService.doAction(action);
|
||||
}
|
||||
return super.createRecord(...arguments);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Called when the user scans a barcode.
|
||||
*
|
||||
* @param {String} barcode
|
||||
*/
|
||||
async _onBarcodeScannedHandler(barcode) {
|
||||
if (this.props.resModel != 'stock.picking') {
|
||||
return;
|
||||
}
|
||||
const kwargs = { barcode, context: this.props.context };
|
||||
const res = await this.model.orm.call(this.props.resModel, 'filter_on_barcode', [], kwargs);
|
||||
if (res.action) {
|
||||
this.actionService.doAction(res.action);
|
||||
} else if (res.warning) {
|
||||
const params = { title: res.warning.title, type: 'danger' };
|
||||
this.model.notificationService.add(res.warning.message, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { KanbanRenderer } from '@web/views/kanban/kanban_renderer';
|
||||
import { useService } from '@web/core/utils/hooks';
|
||||
|
||||
const { onWillStart } = owl;
|
||||
|
||||
export class StockBarcodeKanbanRenderer extends KanbanRenderer {
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
const user = useService('user');
|
||||
this.display_protip = this.props.list.resModel === 'stock.picking';
|
||||
onWillStart(async () => {
|
||||
this.packageEnabled = await user.hasGroup('stock.group_tracking_lot');
|
||||
});
|
||||
}
|
||||
}
|
||||
StockBarcodeKanbanRenderer.template = 'stock_barcode.KanbanRenderer';
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="stock_barcode.KanbanRenderer" t-inherit="web.KanbanRenderer" owl="1">
|
||||
<xpath expr="//div[hasclass('o_kanban_renderer')]" position="before">
|
||||
<div t-if="display_protip" class="o_kanban_tip_filter text-center h5 w-100 mt-4 mb-2">
|
||||
<p t-if="packageEnabled">Scan a transfer, a product or a package to filter your records</p>
|
||||
<p t-else="">Scan a transfer or a product to filter your records</p>
|
||||
</div>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
12
stock_barcode/static/src/kanban/stock_barcode_kanban_view.js
Normal file
12
stock_barcode/static/src/kanban/stock_barcode_kanban_view.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @odoo-module */
|
||||
|
||||
import { kanbanView } from '@web/views/kanban/kanban_view';
|
||||
import { registry } from "@web/core/registry";
|
||||
import { StockBarcodeKanbanController } from './stock_barcode_kanban_controller';
|
||||
import { StockBarcodeKanbanRenderer } from './stock_barcode_kanban_renderer';
|
||||
|
||||
export const stockBarcodeKanbanView = Object.assign({}, kanbanView, {
|
||||
Controller: StockBarcodeKanbanController,
|
||||
Renderer: StockBarcodeKanbanRenderer,
|
||||
});
|
||||
registry.category("views").add("stock_barcode_list_kanban", stockBarcodeKanbanView);
|
||||
214
stock_barcode/static/src/lazy_barcode_cache.js
Normal file
214
stock_barcode/static/src/lazy_barcode_cache.js
Normal file
@@ -0,0 +1,214 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
export default class LazyBarcodeCache {
|
||||
constructor(cacheData, params) {
|
||||
this.rpc = params.rpc;
|
||||
this.dbIdCache = {}; // Cache by model + id
|
||||
this.dbBarcodeCache = {}; // Cache by model + barcode
|
||||
this.missingBarcode = new Set(); // Used as a cache by `_getMissingRecord`
|
||||
this.barcodeFieldByModel = {
|
||||
'stock.location': 'barcode',
|
||||
'product.product': 'barcode',
|
||||
'product.packaging': 'barcode',
|
||||
'stock.package.type': 'barcode',
|
||||
'stock.picking': 'name',
|
||||
'stock.quant.package': 'name',
|
||||
'stock.lot': 'name', // Also ref, should take in account multiple fields ?
|
||||
};
|
||||
this.gs1LengthsByModel = {
|
||||
'product.product': 14,
|
||||
'product.packaging': 14,
|
||||
'stock.location': 13,
|
||||
'stock.quant.package': 18,
|
||||
};
|
||||
// If there is only one active barcode nomenclature, set the cache to be compliant with it.
|
||||
if (cacheData['barcode.nomenclature'].length === 1) {
|
||||
this.nomenclature = cacheData['barcode.nomenclature'][0];
|
||||
}
|
||||
this.setCache(cacheData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds records to the barcode application's cache.
|
||||
*
|
||||
* @param {Object} cacheData each key is a model's name and contains an array of records.
|
||||
*/
|
||||
setCache(cacheData) {
|
||||
for (const model in cacheData) {
|
||||
const records = cacheData[model];
|
||||
// Adds the model's key in the cache's DB.
|
||||
if (!this.dbIdCache.hasOwnProperty(model)) {
|
||||
this.dbIdCache[model] = {};
|
||||
}
|
||||
if (!this.dbBarcodeCache.hasOwnProperty(model)) {
|
||||
this.dbBarcodeCache[model] = {};
|
||||
}
|
||||
// Adds the record in the cache.
|
||||
const barcodeField = this._getBarcodeField(model);
|
||||
for (const record of records) {
|
||||
this.dbIdCache[model][record.id] = record;
|
||||
if (barcodeField) {
|
||||
const barcode = record[barcodeField];
|
||||
if (!this.dbBarcodeCache[model][barcode]) {
|
||||
this.dbBarcodeCache[model][barcode] = [];
|
||||
}
|
||||
if (!this.dbBarcodeCache[model][barcode].includes(record.id)) {
|
||||
this.dbBarcodeCache[model][barcode].push(record.id);
|
||||
if (this.nomenclature && this.nomenclature.is_gs1_nomenclature && this.gs1LengthsByModel[model]) {
|
||||
this._setBarcodeInCacheForGS1(barcode, model, record);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get record from the cache, throw a error if we don't find in the cache
|
||||
* (the server should have return this information).
|
||||
*
|
||||
* @param {int} id id of the record
|
||||
* @param {string} model model_name of the record
|
||||
* @param {boolean} [copy=true] if true, returns a deep copy (to avoid to write the cache)
|
||||
* @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode)
|
||||
*/
|
||||
getRecord(model, id) {
|
||||
if (!this.dbIdCache.hasOwnProperty(model)) {
|
||||
throw new Error(`Model ${model} doesn't exist in the cache`);
|
||||
}
|
||||
if (!this.dbIdCache[model].hasOwnProperty(id)) {
|
||||
throw new Error(`Record ${model} with id=${id} doesn't exist in the cache, it should return by the server`);
|
||||
}
|
||||
const record = this.dbIdCache[model][id];
|
||||
return JSON.parse(JSON.stringify(record));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} barcode barcode to match with a record
|
||||
* @param {string} [model] model name of the record to match (if empty search on all models)
|
||||
* @param {boolean} [onlyInCache] search only in the cache
|
||||
* @param {Object} [filters]
|
||||
* @returns copy of the record send by the server (fields limited to _get_fields_stock_barcode)
|
||||
*/
|
||||
async getRecordByBarcode(barcode, model = false, onlyInCache = false, filters = {}) {
|
||||
if (model) {
|
||||
if (!this.dbBarcodeCache.hasOwnProperty(model)) {
|
||||
throw new Error(`Model ${model} doesn't exist in the cache`);
|
||||
}
|
||||
if (!this.dbBarcodeCache[model].hasOwnProperty(barcode)) {
|
||||
if (onlyInCache) {
|
||||
return null;
|
||||
}
|
||||
await this._getMissingRecord(barcode, model);
|
||||
return await this.getRecordByBarcode(barcode, model, true);
|
||||
}
|
||||
const id = this.dbBarcodeCache[model][barcode][0];
|
||||
return this.getRecord(model, id);
|
||||
} else {
|
||||
const result = new Map();
|
||||
// Returns object {model: record} of possible record.
|
||||
const models = Object.keys(this.dbBarcodeCache);
|
||||
for (const model of models) {
|
||||
if (this.dbBarcodeCache[model].hasOwnProperty(barcode)) {
|
||||
const ids = this.dbBarcodeCache[model][barcode];
|
||||
for (const id of ids) {
|
||||
const record = this.dbIdCache[model][id];
|
||||
let pass = true;
|
||||
if (filters[model]) {
|
||||
const fields = Object.keys(filters[model]);
|
||||
for (const field of fields) {
|
||||
if (record[field] != filters[model][field]) {
|
||||
pass = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pass) {
|
||||
result.set(model, JSON.parse(JSON.stringify(record)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.size < 1) {
|
||||
if (onlyInCache) {
|
||||
return result;
|
||||
}
|
||||
await this._getMissingRecord(barcode, model, filters);
|
||||
return await this.getRecordByBarcode(barcode, model, true, filters);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
_getBarcodeField(model) {
|
||||
if (!this.barcodeFieldByModel.hasOwnProperty(model)) {
|
||||
return null;
|
||||
}
|
||||
return this.barcodeFieldByModel[model];
|
||||
}
|
||||
|
||||
async _getMissingRecord(barcode, model, filters) {
|
||||
const missCache = this.missingBarcode;
|
||||
const params = { barcode, model_name: model };
|
||||
// Check if we already try to fetch this missing record.
|
||||
if (missCache.has(barcode) || missCache.has(`${barcode}_${model}`)) {
|
||||
return false;
|
||||
}
|
||||
// Creates and passes a domain if some filters are provided.
|
||||
if (filters) {
|
||||
const domainsByModel = {};
|
||||
for (const filter of Object.entries(filters)) {
|
||||
const modelName = filter[0];
|
||||
const filtersByField = filter[1];
|
||||
domainsByModel[modelName] = [];
|
||||
for (const filterByField of Object.entries(filtersByField)) {
|
||||
domainsByModel[modelName].push([filterByField[0], '=', filterByField[1]]);
|
||||
}
|
||||
}
|
||||
params.domains_by_model = domainsByModel;
|
||||
}
|
||||
const result = await this.rpc('/stock_barcode/get_specific_barcode_data', params);
|
||||
this.setCache(result);
|
||||
// Set the missing cache if no filters (the barcode's result can vary if there is filter)
|
||||
if (!filters) {
|
||||
const keyCache = (model && `${barcode}_${model}`) || barcode;
|
||||
missCache.add(keyCache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets in the cache an entry for the given record with its formatted barcode as key.
|
||||
* The barcode will be formatted (if needed) at the length corresponding to its data part in a
|
||||
* GS1 barcode (e.g.: 14 digits for a product's barcode) by padding with 0 the original barcode.
|
||||
* That makes it easier to find when a GS1 barcode is scanned.
|
||||
* If the formatted barcode is similar to an another barcode for the same model, it will show a
|
||||
* warning in the console (as a clue to find where issue could come from, not to alert the user)
|
||||
*
|
||||
* @param {string} barcode
|
||||
* @param {string} model
|
||||
* @param {Object} record
|
||||
*/
|
||||
_setBarcodeInCacheForGS1(barcode, model, record) {
|
||||
const length = this.gs1LengthsByModel[model];
|
||||
if (!barcode || barcode.length >= length || isNaN(Number(barcode))) {
|
||||
// Barcode already has the good length, or is too long or isn't
|
||||
// fully numerical (and so, it doesn't make sense to adapt it).
|
||||
return;
|
||||
}
|
||||
const paddedBarcode = barcode.padStart(length, '0');
|
||||
// Avoids to override or mix records if there is already a key for this
|
||||
// barcode (which means there is a conflict somewhere).
|
||||
if (!this.dbBarcodeCache[model][paddedBarcode]) {
|
||||
this.dbBarcodeCache[model][paddedBarcode] = [record.id];
|
||||
} else if (!this.dbBarcodeCache[model][paddedBarcode].includes(record.id)) {
|
||||
const previousRecordId = this.dbBarcodeCache[model][paddedBarcode][0];
|
||||
const previousRecord = this.getRecord(model, previousRecordId);
|
||||
console.log(
|
||||
`Conflict for barcode %c${paddedBarcode}%c:`, 'font-weight: bold', '',
|
||||
`it could refer for both ${record.display_name} and ${previousRecord.display_name}.`,
|
||||
`\nThe last one will be used but consider to edit those products barcode to avoid error due to ambiguities.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
75
stock_barcode/static/src/main_menu.js
Normal file
75
stock_barcode/static/src/main_menu.js
Normal file
@@ -0,0 +1,75 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import * as BarcodeScanner from '@web/webclient/barcode/barcode_scanner';
|
||||
import { bus } from 'web.core';
|
||||
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, onMounted, onWillUnmount, onWillStart, useState } = owl;
|
||||
|
||||
export class MainMenu extends Component {
|
||||
setup() {
|
||||
const displayDemoMessage = this.props.action.params.message_demo_barcodes;
|
||||
const user = useService('user');
|
||||
this.actionService = useService('action');
|
||||
this.dialogService = useService('dialog');
|
||||
this.home = useService("home_menu");
|
||||
this.notificationService = useService("notification");
|
||||
this.rpc = useService('rpc');
|
||||
this.state = useState({ displayDemoMessage });
|
||||
|
||||
this.mobileScanner = BarcodeScanner.isBarcodeScannerSupported();
|
||||
|
||||
onWillStart(async () => {
|
||||
this.locationsEnabled = await user.hasGroup('stock.group_stock_multi_locations');
|
||||
this.packagesEnabled = await user.hasGroup('stock.group_tracking_lot');
|
||||
});
|
||||
onMounted(() => {
|
||||
bus.on('barcode_scanned', this, this._onBarcodeScanned);
|
||||
});
|
||||
onWillUnmount(() => {
|
||||
bus.off('barcode_scanned', this, this._onBarcodeScanned);
|
||||
});
|
||||
}
|
||||
|
||||
async openMobileScanner() {
|
||||
const barcode = await BarcodeScanner.scanBarcode();
|
||||
if (barcode){
|
||||
this._onBarcodeScanned(barcode);
|
||||
if ('vibrate' in window.navigator) {
|
||||
window.navigator.vibrate(100);
|
||||
}
|
||||
} else {
|
||||
this.notificationService.add(this.env._t("Please, Scan again !"), { type: 'warning' });
|
||||
}
|
||||
}
|
||||
|
||||
removeDemoMessage() {
|
||||
this.state.displayDemoMessage = false;
|
||||
const params = {
|
||||
title: this.env._t("Don't show this message again"),
|
||||
body: this.env._t("Do you want to permanently remove this message ?\
|
||||
It won't appear anymore, so make sure you don't need the barcodes sheet or you have a copy."),
|
||||
confirm: () => {
|
||||
this.rpc('/stock_barcode/rid_of_message_demo_barcodes');
|
||||
location.reload();
|
||||
},
|
||||
cancel: () => {},
|
||||
confirmLabel: this.env._t("Remove it"),
|
||||
cancelLabel: this.env._t("Leave it"),
|
||||
};
|
||||
this.dialogService.add(ConfirmationDialog, params);
|
||||
}
|
||||
|
||||
async _onBarcodeScanned(barcode) {
|
||||
const res = await this.rpc('/stock_barcode/scan_from_main_menu', { barcode });
|
||||
if (res.action) {
|
||||
return this.actionService.doAction(res.action);
|
||||
}
|
||||
this.notificationService.add(res.warning, { type: 'danger' });
|
||||
}
|
||||
}
|
||||
MainMenu.template = 'stock_barcode.MainMenu';
|
||||
|
||||
registry.category('actions').add('stock_barcode_main_menu', MainMenu);
|
||||
57
stock_barcode/static/src/main_menu.xml
Normal file
57
stock_barcode/static/src/main_menu.xml
Normal file
@@ -0,0 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="stock_barcode.MainMenu" class="o_stock_barcode_main_menu_container o_home_menu_background" owl="1">
|
||||
<div class="o_stock_barcode_main_menu position-relative bg-view">
|
||||
<a href="#" class="o_stock_barcode_menu d-block float-start" t-on-click="() => this.home.toggle(true)">
|
||||
<i class="fa fa-chevron-left"/>
|
||||
</a>
|
||||
<h1 class="mb-4">Barcode Scanning</h1>
|
||||
|
||||
<div t-if="state.displayDemoMessage" class="message_demo_barcodes alert alert-info alert-dismissible text-start" role="status">
|
||||
<button t-on-click="removeDemoMessage" type="button" class="btn-close" title="Close"/>
|
||||
We have created a few demo data with barcodes for you to explore the features. Print the
|
||||
<a href="/stock_barcode/static/img/barcodes_demo.pdf" target="_blank">stock barcodes sheet</a>
|
||||
to check out what this module can do! You can also print the barcode
|
||||
<a class="o_stock_inventory_commands_download" href="/stock_barcode/print_inventory_commands" target="_blank" aria-label="Download" title="Download">commands for Inventory</a>.
|
||||
</div>
|
||||
|
||||
<div class="o_stock_barcode_container position-relative d-inline-block mt-4 mb-5">
|
||||
<div t-if='mobileScanner' class="o_stock_mobile_barcode_container">
|
||||
<button class="btn btn-primary o_stock_mobile_barcode" t-on-click="openMobileScanner">
|
||||
<i class="fa fa-camera fa-2x o_mobile_barcode_camera"/> Tap to scan
|
||||
</button>
|
||||
<img src="/barcodes/static/img/barcode.png" alt="Barcode" class="img-fluid mb-1 mt-1"/>
|
||||
</div>
|
||||
<img t-else="" src="/barcodes/static/img/barcode.png" alt="Barcode" class="img-fluid mb-1 mt-1"/>
|
||||
<span class="o_stock_barcode_laser"/>
|
||||
</div>
|
||||
|
||||
<ul class="text-start mb-sm-5 ps-4">
|
||||
<li>Scan an <b>operation type</b> to create a new transfer.</li>
|
||||
<li t-if="locationsEnabled">Scan a <b>location</b> to create a new transfer from this location.</li>
|
||||
<li>Scan a <b>document</b> to open it.</li>
|
||||
<li>Scan a <b>product</b> to show its location and quantity.</li>
|
||||
<li t-if="packagesEnabled">Scan a <b>package</b> to know its content.</li>
|
||||
</ul>
|
||||
|
||||
<hr class="mb-4 d-none d-sm-block"/>
|
||||
|
||||
<div class="o_main_menu_buttons row">
|
||||
<div class="col">
|
||||
<button class="button_operations btn btn-block btn-primary mb-4 w-100"
|
||||
t-on-click="() => this.actionService.doAction('stock_barcode.stock_picking_type_action_kanban')">
|
||||
Operations
|
||||
</button>
|
||||
</div>
|
||||
<div class="col">
|
||||
<button class="button_inventory btn btn-block btn-primary mb-4 w-100"
|
||||
t-on-click="() => this.actionService.doAction('stock_barcode.stock_barcode_inventory_client_action')">
|
||||
Inventory Adjustments
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
1429
stock_barcode/static/src/models/barcode_model.js
Normal file
1429
stock_barcode/static/src/models/barcode_model.js
Normal file
File diff suppressed because it is too large
Load Diff
1179
stock_barcode/static/src/models/barcode_picking_model.js
Normal file
1179
stock_barcode/static/src/models/barcode_picking_model.js
Normal file
File diff suppressed because it is too large
Load Diff
597
stock_barcode/static/src/models/barcode_quant_model.js
Normal file
597
stock_barcode/static/src/models/barcode_quant_model.js
Normal file
@@ -0,0 +1,597 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import BarcodeModel from '@stock_barcode/models/barcode_model';
|
||||
import {_t} from "web.core";
|
||||
import { sprintf } from '@web/core/utils/strings';
|
||||
|
||||
export default class BarcodeQuantModel extends BarcodeModel {
|
||||
constructor(params) {
|
||||
super(...arguments);
|
||||
this.lineModel = params.model;
|
||||
this.validateMessage = _t("The inventory adjustment has been validated");
|
||||
this.validateMethod = 'action_validate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates only the quants of the current inventory page and don't close it.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async apply() {
|
||||
await this.save();
|
||||
const linesToApply = this.pageLines.filter(line => line.inventory_quantity_set);
|
||||
if (linesToApply.length === 0) {
|
||||
const message = _t("There is nothing to apply in this page.");
|
||||
return this.notification.add(message, { type: 'warning' });
|
||||
}
|
||||
const action = await this.orm.call('stock.quant', 'action_validate',
|
||||
[linesToApply.map(quant => quant.id)]
|
||||
);
|
||||
const notifyAndGoAhead = res => {
|
||||
if (res && res.special) { // Do nothing if come from a discarded wizard.
|
||||
return this.trigger('refresh');
|
||||
}
|
||||
this.notification.add(_t("The inventory adjustment has been validated"), { type: 'success' });
|
||||
this.trigger('history-back');
|
||||
};
|
||||
if (action && action.res_model) {
|
||||
const options = { on_close: notifyAndGoAhead };
|
||||
return this.trigger('do-action', { action, options });
|
||||
}
|
||||
notifyAndGoAhead();
|
||||
}
|
||||
|
||||
get applyOn() {
|
||||
return this.pageLines.filter(line => line.inventory_quantity_set).length;
|
||||
}
|
||||
|
||||
get barcodeInfo() {
|
||||
// Takes the parent line if the current line is part of a group.
|
||||
let line = this._getParentLine(this.selectedLine) || this.selectedLine;
|
||||
if (!line && this.lastScanned.packageId) {
|
||||
const lines = this._moveEntirePackage() ? this.packageLines : this.pageLines;
|
||||
line = lines.find(l => l.package_id && l.package_id.id === this.lastScanned.packageId);
|
||||
}
|
||||
|
||||
if (line) { // Message depends of the selected line's state.
|
||||
const { tracking } = line.product_id;
|
||||
const trackingNumber = (line.lot_id && line.lot_id.name) || line.lot_name;
|
||||
if (this._lineIsNotComplete(line)) {
|
||||
if (tracking === 'none') {
|
||||
this.messageType = 'scan_product';
|
||||
} else {
|
||||
this.messageType = tracking === 'lot' ? 'scan_lot' : 'scan_serial';
|
||||
}
|
||||
} else if (tracking !== 'none' && !trackingNumber) {
|
||||
// Line's quantity is fulfilled but still waiting a tracking number.
|
||||
this.messageType = tracking === 'lot' ? 'scan_lot' : 'scan_serial';
|
||||
} else { // Line's quantity is fulfilled.
|
||||
this.messageType = this.groups.group_stock_multi_locations && line.location_id.id === this.location.id ?
|
||||
"scan_product_or_src" :
|
||||
"scan_product";
|
||||
}
|
||||
} else { // Message depends if multilocation is enabled.
|
||||
this.messageType = this.groups.group_stock_multi_locations && !this.lastScanned.sourceLocation ?
|
||||
'scan_src' :
|
||||
'scan_product';
|
||||
}
|
||||
|
||||
const barcodeInformations = { class: this.messageType, warning: false, icon: 'barcode' };
|
||||
switch (this.messageType) {
|
||||
case 'scan_product':
|
||||
barcodeInformations.message = this.groups.group_stock_multi_locations ?
|
||||
sprintf(_t("Scan a product in %s or scan another location"), this.location.display_name) :
|
||||
_t("Scan a product");
|
||||
break;
|
||||
case 'scan_src':
|
||||
barcodeInformations.message = _t("Scan a location");
|
||||
barcodeInformations.icon = 'sign-out';
|
||||
break;
|
||||
case 'scan_product_or_src':
|
||||
barcodeInformations.message = sprintf(
|
||||
_t("Scan more products in %s or scan another location"),
|
||||
this.location.display_name);
|
||||
break;
|
||||
case 'scan_product_or_dest':
|
||||
barcodeInformations.message = _t("Scan more products, or scan the destination location");
|
||||
barcodeInformations.icon = 'sign-in';
|
||||
break;
|
||||
case 'scan_lot':
|
||||
barcodeInformations.message = sprintf(
|
||||
_t("Scan lot numbers for product %s to change their quantity"),
|
||||
line.product_id.display_name
|
||||
);
|
||||
break;
|
||||
case 'scan_serial':
|
||||
barcodeInformations.message = sprintf(
|
||||
_t("Scan serial numbers for product %s to change their quantity"),
|
||||
line.product_id.display_name
|
||||
);
|
||||
break;
|
||||
}
|
||||
return barcodeInformations;
|
||||
}
|
||||
|
||||
get displayByUnitButton () {
|
||||
return true;
|
||||
}
|
||||
|
||||
get displaySetButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
setData(data) {
|
||||
this.userId = data.data.user_id;
|
||||
super.setData(...arguments);
|
||||
const companies = data.data.records['res.company'];
|
||||
this.companyIds = companies.map(company => company.id);
|
||||
this.lineFormViewId = data.data.line_view_id;
|
||||
}
|
||||
|
||||
get displayApplyButton() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getDisplayIncrementBtn(line) {
|
||||
return line.product_id.tracking !== 'serial' && this.selectedLine &&
|
||||
line.virtual_id === this.selectedLine.virtual_id;
|
||||
}
|
||||
|
||||
getDisplayDecrementBtn(line) {
|
||||
return this.getDisplayIncrementBtn(line);
|
||||
}
|
||||
|
||||
getQtyDone(line) {
|
||||
return line.inventory_quantity;
|
||||
}
|
||||
|
||||
getQtyDemand(line) {
|
||||
return line.quantity;
|
||||
}
|
||||
|
||||
getActionRefresh(newId) {
|
||||
const action = super.getActionRefresh(newId);
|
||||
action.params.res_id = this.currentState.lines.map(l => l.id);
|
||||
if (newId) {
|
||||
action.params.res_id.push(newId);
|
||||
}
|
||||
return action;
|
||||
}
|
||||
|
||||
get highlightValidateButton() {
|
||||
return this.applyOn > 0 && this.applyOn === this.pageLines.length;
|
||||
}
|
||||
|
||||
get incrementButtonsDisplayStyle() {
|
||||
return "d-block my-3";
|
||||
}
|
||||
|
||||
IsNotSet(line) {
|
||||
return !line.inventory_quantity_set;
|
||||
}
|
||||
|
||||
lineIsFaulty(line) {
|
||||
return line.inventory_quantity_set && line.inventory_quantity !== line.quantity;
|
||||
}
|
||||
|
||||
get printButtons() {
|
||||
return [{
|
||||
name: _t("Print Inventory"),
|
||||
class: 'o_print_inventory',
|
||||
action: 'stock.action_report_inventory',
|
||||
}];
|
||||
}
|
||||
|
||||
get recordIds() {
|
||||
return this.currentState.lines.map(l => l.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the line as set and set its inventory quantity if it was unset, or
|
||||
* unset it if the line was already set.
|
||||
*
|
||||
* @param {Object} line
|
||||
*/
|
||||
setOnHandQuantity(line) {
|
||||
if (line.product_id.tracking === 'serial') { // Special case for product tracked by SN.
|
||||
const quantity = !(line.lot_name || line.lot_id) && line.quantity || 1;
|
||||
if (line.inventory_quantity_set) {
|
||||
line.inventory_quantity = line.inventory_quantity ? 0 : quantity;
|
||||
line.inventory_quantity_set = line.inventory_quantity != quantity;
|
||||
} else {
|
||||
line.inventory_quantity = quantity;
|
||||
line.inventory_quantity_set = true;
|
||||
}
|
||||
this._markLineAsDirty(line);
|
||||
} else {
|
||||
if (line.inventory_quantity_set) {
|
||||
line.inventory_quantity = 0;
|
||||
line.inventory_quantity_set = false;
|
||||
this._markLineAsDirty(line);
|
||||
} else {
|
||||
const inventory_quantity = line.quantity - line.inventory_quantity;
|
||||
this.updateLine(line, { inventory_quantity });
|
||||
line.inventory_quantity_set = true;
|
||||
}
|
||||
}
|
||||
this.trigger('update');
|
||||
}
|
||||
|
||||
updateLineQty(virtualId, qty = 1) {
|
||||
this.actionMutex.exec(() => {
|
||||
const line = this.pageLines.find(l => l.virtual_id === virtualId);
|
||||
this.updateLine(line, {inventory_quantity: qty});
|
||||
this.trigger('update');
|
||||
});
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Private
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
_getCommands() {
|
||||
return Object.assign(super._getCommands(), {
|
||||
'O-BTN.apply': this.apply.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
_getNewLineDefaultContext() {
|
||||
return {
|
||||
default_company_id: this.companyIds[0],
|
||||
default_location_id: this._defaultLocation().id,
|
||||
default_inventory_quantity: 1,
|
||||
default_user_id: this.userId,
|
||||
inventory_mode: true,
|
||||
};
|
||||
}
|
||||
|
||||
_createCommandVals(line) {
|
||||
const values = {
|
||||
dummy_id: line.virtual_id,
|
||||
inventory_date: line.inventory_date,
|
||||
inventory_quantity: line.inventory_quantity,
|
||||
inventory_quantity_set: line.inventory_quantity_set,
|
||||
location_id: line.location_id,
|
||||
lot_id: line.lot_id,
|
||||
lot_name: line.lot_name,
|
||||
package_id: line.package_id,
|
||||
product_id: line.product_id,
|
||||
owner_id: line.owner_id,
|
||||
user_id: this.userId,
|
||||
};
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
values[key] = this._fieldToValue(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
async _createNewLine(params) {
|
||||
// When creating a new line, we need to know if a quant already exists
|
||||
// for this line, and in this case, update the new line fields.
|
||||
const product = params.fieldsParams.product_id;
|
||||
if (product.detailed_type != 'product') {
|
||||
const productName = (product.default_code ? `[${product.default_code}] ` : '') + product.display_name;
|
||||
const message = sprintf(
|
||||
_t("%s can't be inventoried. Only storable products can be inventoried."), productName);
|
||||
this.notification.add(message, { type: 'warning' });
|
||||
return false;
|
||||
}
|
||||
const domain = [
|
||||
['location_id', '=', this.location.id],
|
||||
['product_id', '=', product.id],
|
||||
];
|
||||
if (product.tracking !== 'none') {
|
||||
if (params.fieldsParams.lot_name) { // Search for a quant with the exact same lot.
|
||||
domain.push(['lot_id.name', '=', params.fieldsParams.lot_name]);
|
||||
} else { // Search for a quant with no lot.
|
||||
domain.push(['lot_id', '=', false]);
|
||||
}
|
||||
}
|
||||
if (params.fieldsParams.package_id) {
|
||||
domain.push(['package_id', '=', params.fieldsParams.package_id]);
|
||||
}
|
||||
const quant = await this.orm.searchRead(
|
||||
'stock.quant',
|
||||
domain,
|
||||
['id', 'inventory_date', 'inventory_quantity', 'inventory_quantity_set', 'quantity', 'user_id'],
|
||||
{ limit: 1 }
|
||||
);
|
||||
if (quant.length) {
|
||||
Object.assign(params.fieldsParams, quant[0], { inventory_quantity: 1 });
|
||||
}
|
||||
const newLine = await super._createNewLine(params);
|
||||
if (quant.length) {
|
||||
// If the quant already exits, we add it into the `initialState` to
|
||||
// avoid comparison issue with the `currentState` when the save occurs.
|
||||
this.initialState.lines.push(Object.assign({}, newLine, quant[0]));
|
||||
}
|
||||
return newLine;
|
||||
}
|
||||
|
||||
_convertDataToFieldsParams(args) {
|
||||
const params = {
|
||||
inventory_quantity: args.quantity,
|
||||
lot_id: args.lot,
|
||||
lot_name: args.lotName,
|
||||
owner_id: args.owner,
|
||||
package_id: args.package || args.resultPackage,
|
||||
product_id: args.product,
|
||||
product_uom_id: args.product && args.product.uom_id,
|
||||
};
|
||||
return params;
|
||||
}
|
||||
|
||||
_getNewLineDefaultValues(fieldsParams) {
|
||||
const defaultValues = super._getNewLineDefaultValues(...arguments);
|
||||
return Object.assign(defaultValues, {
|
||||
inventory_date: new Date().toISOString().slice(0, 10),
|
||||
inventory_quantity: 0,
|
||||
inventory_quantity_set: true,
|
||||
quantity: (fieldsParams && fieldsParams.quantity) || 0,
|
||||
user_id: this.userId,
|
||||
});
|
||||
}
|
||||
|
||||
_getFieldToWrite() {
|
||||
return [
|
||||
'inventory_date',
|
||||
'inventory_quantity',
|
||||
'inventory_quantity_set',
|
||||
'user_id',
|
||||
'location_id',
|
||||
'lot_name',
|
||||
'lot_id',
|
||||
'package_id',
|
||||
'owner_id',
|
||||
];
|
||||
}
|
||||
|
||||
_getSaveCommand() {
|
||||
const commands = this._getSaveLineCommand();
|
||||
if (commands.length) {
|
||||
return {
|
||||
route: '/stock_barcode/save_barcode_data',
|
||||
params: {
|
||||
model: this.params.model,
|
||||
res_id: false,
|
||||
write_field: false,
|
||||
write_vals: commands,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
_groupSublines(sublines, ids, virtual_ids, qtyDemand, qtyDone) {
|
||||
return Object.assign(super._groupSublines(...arguments), {
|
||||
inventory_quantity: qtyDone,
|
||||
quantity: qtyDemand,
|
||||
});
|
||||
}
|
||||
|
||||
_lineIsNotComplete(line) {
|
||||
return line.inventory_quantity === 0;
|
||||
}
|
||||
|
||||
async _processPackage(barcodeData) {
|
||||
const { packageType, packageName } = barcodeData;
|
||||
let recPackage = barcodeData.package;
|
||||
this.lastScanned.packageId = false;
|
||||
if (!recPackage && !packageType && !packageName) {
|
||||
return; // No Package data to process.
|
||||
}
|
||||
// Scan a new package and/or a package type -> Create a new package with those parameters.
|
||||
const currentLine = this.selectedLine || this.lastScannedLine;
|
||||
if (currentLine.package_id && packageType &&
|
||||
!recPackage && ! packageName &&
|
||||
currentLine.package_id.id !== packageType) {
|
||||
// Changes the package type for the scanned one.
|
||||
await this.orm.write('stock.quant.package', [currentLine.package_id.id], {
|
||||
package_type_id: packageType.id,
|
||||
});
|
||||
const message = sprintf(
|
||||
_t("Package type %s was correctly applied to the package %s"),
|
||||
packageType.name, currentLine.package_id.name
|
||||
);
|
||||
barcodeData.stopped = true;
|
||||
return this.notification.add(message, { type: 'success' });
|
||||
}
|
||||
if (!recPackage) {
|
||||
if (currentLine && !currentLine.package_id) {
|
||||
const valueList = {};
|
||||
if (packageName) {
|
||||
valueList.name = packageName;
|
||||
}
|
||||
if (packageType) {
|
||||
valueList.package_type_id = packageType.id;
|
||||
}
|
||||
const newPackageData = await this.orm.call(
|
||||
'stock.quant.package',
|
||||
'action_create_from_barcode',
|
||||
[valueList]
|
||||
);
|
||||
this.cache.setCache(newPackageData);
|
||||
recPackage = newPackageData['stock.quant.package'][0];
|
||||
}
|
||||
}
|
||||
if (!recPackage && packageName) {
|
||||
const currentLine = this.selectedLine || this.lastScannedLine;
|
||||
if (currentLine && !currentLine.package_id) {
|
||||
const newPackageData = await this.orm.call(
|
||||
'stock.quant.package',
|
||||
'action_create_from_barcode',
|
||||
[{ name: packageName }]
|
||||
);
|
||||
this.cache.setCache(newPackageData);
|
||||
recPackage = newPackageData['stock.quant.package'][0];
|
||||
}
|
||||
}
|
||||
if (!recPackage || (
|
||||
recPackage.location_id && recPackage.location_id != this.location.id
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
// TODO: can check if quants already in cache to avoid to make a RPC if
|
||||
// there is all in it (or make the RPC only on missing quants).
|
||||
const res = await this.orm.call(
|
||||
'stock.quant',
|
||||
'get_stock_barcode_data_records',
|
||||
[recPackage.quant_ids]
|
||||
);
|
||||
const quants = res.records['stock.quant'];
|
||||
if (!quants.length) { // Empty package => Assigns it to the last scanned line.
|
||||
const currentLine = this.selectedLine || this.lastScannedLine;
|
||||
if (currentLine && !currentLine.package_id && !currentLine.result_package_id) {
|
||||
const fieldsParams = this._convertDataToFieldsParams({
|
||||
resultPackage: recPackage,
|
||||
});
|
||||
await this.updateLine(currentLine, fieldsParams);
|
||||
barcodeData.stopped = true;
|
||||
this.selectedLineVirtualId = false;
|
||||
this.lastScanned.packageId = recPackage.id;
|
||||
this.trigger('update');
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.cache.setCache(res.records);
|
||||
|
||||
// Checks if the package is already scanned.
|
||||
let alreadyExisting = 0;
|
||||
for (const line of this.pageLines) {
|
||||
if (line.package_id && line.package_id.id === recPackage.id &&
|
||||
this.getQtyDone(line) > 0) {
|
||||
alreadyExisting++;
|
||||
}
|
||||
}
|
||||
if (alreadyExisting === quants.length) {
|
||||
barcodeData.error = _t("This package is already scanned.");
|
||||
return;
|
||||
}
|
||||
// For each quants, creates or increments a barcode line.
|
||||
for (const quant of quants) {
|
||||
const product = this.cache.getRecord('product.product', quant.product_id);
|
||||
const searchLineParams = Object.assign({}, barcodeData, { product });
|
||||
const currentLine = this._findLine(searchLineParams);
|
||||
if (currentLine) { // Updates an existing line.
|
||||
const fieldsParams = this._convertDataToFieldsParams({
|
||||
quantity: quant.quantity,
|
||||
lotName: barcodeData.lotName,
|
||||
lot: barcodeData.lot,
|
||||
package: recPackage,
|
||||
owner: barcodeData.owner,
|
||||
});
|
||||
await this.updateLine(currentLine, fieldsParams);
|
||||
} else { // Creates a new line.
|
||||
const fieldsParams = this._convertDataToFieldsParams({
|
||||
product,
|
||||
quantity: quant.quantity,
|
||||
lot: quant.lot_id,
|
||||
package: quant.package_id,
|
||||
resultPackage: quant.package_id,
|
||||
owner: quant.owner_id,
|
||||
});
|
||||
const newLine = await this._createNewLine({ fieldsParams });
|
||||
newLine.inventory_quantity = quant.quantity;
|
||||
}
|
||||
}
|
||||
barcodeData.stopped = true;
|
||||
this.selectedLineVirtualId = false;
|
||||
this.lastScanned.packageId = recPackage.id;
|
||||
this.trigger('update');
|
||||
}
|
||||
|
||||
_updateLineQty(line, args) {
|
||||
if (args.quantity) { // Set stock quantity.
|
||||
line.quantity = args.quantity;
|
||||
}
|
||||
if (args.inventory_quantity) { // Increments inventory quantity.
|
||||
if (args.uom) {
|
||||
// An UoM was passed alongside the quantity, needs to check it's
|
||||
// compatible with the product's UoM.
|
||||
const productUOM = this.cache.getRecord('uom.uom', line.product_id.uom_id);
|
||||
if (args.uom.category_id !== productUOM.category_id) {
|
||||
// Not the same UoM's category -> Can't be converted.
|
||||
const message = sprintf(
|
||||
_t("Scanned quantity uses %s as Unit of Measure, but this UoM is not compatible with the product's one (%s)."),
|
||||
args.uom.name, productUOM.name
|
||||
);
|
||||
return this.notification.add(message, { title: _t("Wrong Unit of Measure"), type: 'warning' });
|
||||
} else if (args.uom.id !== productUOM.id) {
|
||||
// Compatible but not the same UoM => Need a conversion.
|
||||
args.inventory_quantity = (args.inventory_quantity / args.uom.factor) * productUOM.factor;
|
||||
}
|
||||
}
|
||||
line.inventory_quantity += args.inventory_quantity;
|
||||
line.inventory_quantity_set = true;
|
||||
if (line.product_id.tracking === 'serial' && (line.lot_name || line.lot_id)) {
|
||||
line.inventory_quantity = Math.max(0, Math.min(1, line.inventory_quantity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _updateLotName(line, lotName) {
|
||||
if (line.lot_name === lotName) {
|
||||
// No need to update the line's tracking number if it's already set.
|
||||
return Promise.resolve();
|
||||
}
|
||||
line.lot_name = lotName;
|
||||
// Checks if a quant exists for this line and updates the line in this case.
|
||||
const domain = [
|
||||
['location_id', '=', line.location_id.id],
|
||||
['product_id', '=', line.product_id.id],
|
||||
['lot_id.name', '=', lotName],
|
||||
['owner_id', '=', line.owner_id && line.owner_id.id],
|
||||
['package_id', '=', line.package_id && line.package_id.id],
|
||||
];
|
||||
const existingQuant = await this.orm.searchRead(
|
||||
'stock.quant',
|
||||
domain,
|
||||
['id', 'quantity'],
|
||||
{ limit: 1, load: false }
|
||||
);
|
||||
if (existingQuant.length) {
|
||||
Object.assign(line, existingQuant[0]);
|
||||
if (line.lot_id) {
|
||||
line.lot_id = await this.cache.getRecordByBarcode(lotName, 'stock.lot');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_createLinesState() {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const lines = [];
|
||||
for (const id of Object.keys(this.cache.dbIdCache['stock.quant']).map(id => Number(id))) {
|
||||
const quant = this.cache.getRecord('stock.quant', id);
|
||||
if (quant.user_id !== this.userId || quant.inventory_date > today) {
|
||||
// Doesn't take quants who must be counted by another user or in the future.
|
||||
continue;
|
||||
}
|
||||
// Checks if this line is already in the quant state to get back
|
||||
// its `virtual_id` (and so, avoid to set a new `virtual_id`).
|
||||
const prevLine = this.currentState && this.currentState.lines.find(l => l.id === id);
|
||||
const previousVirtualId = prevLine && prevLine.virtual_id;
|
||||
quant.virtual_id = quant.dummy_id || previousVirtualId || this._uniqueVirtualId;
|
||||
quant.product_id = this.cache.getRecord('product.product', quant.product_id);
|
||||
quant.location_id = this.cache.getRecord('stock.location', quant.location_id);
|
||||
quant.lot_id = quant.lot_id && this.cache.getRecord('stock.lot', quant.lot_id);
|
||||
quant.package_id = quant.package_id && this.cache.getRecord('stock.quant.package', quant.package_id);
|
||||
quant.owner_id = quant.owner_id && this.cache.getRecord('res.partner', quant.owner_id);
|
||||
lines.push(Object.assign({}, quant));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
_getName() {
|
||||
return _t("Inventory Adjustment");
|
||||
}
|
||||
|
||||
_getPrintOptions() {
|
||||
const options = super._getPrintOptions();
|
||||
const quantsToPrint = this.pageLines.filter(quant => quant.inventory_quantity_set);
|
||||
if (quantsToPrint.length === 0) {
|
||||
return { warning: _t("There is nothing to print in this page.") };
|
||||
}
|
||||
options.additional_context = { active_ids: quantsToPrint.map(quant => quant.id) };
|
||||
return options;
|
||||
}
|
||||
}
|
||||
15
stock_barcode/static/src/scss/stock_barcode.dark.scss
Normal file
15
stock_barcode/static/src/scss/stock_barcode.dark.scss
Normal file
@@ -0,0 +1,15 @@
|
||||
// = Stock Barcode
|
||||
// ============================================================================
|
||||
// No CSS hacks, variables overrides only
|
||||
|
||||
.o_barcode_client_action {
|
||||
--barcode__header-bg: #{$o-gray-100};
|
||||
|
||||
--barcode__linesHeader-bg: #{$o-gray-300};
|
||||
|
||||
--barcode__line--completed: #1f5a2d;
|
||||
--barcode__line--notCompleted: #303030;
|
||||
|
||||
--barcode__input--completed: #262c26;
|
||||
--barcode__input--notCompleted: #303030;
|
||||
}
|
||||
138
stock_barcode/static/src/scss/stock_barcode.scss
Normal file
138
stock_barcode/static/src/scss/stock_barcode.scss
Normal file
@@ -0,0 +1,138 @@
|
||||
.o_stock_barcode_main_menu_container {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100%;
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.o_stock_barcode_main_menu {
|
||||
text-align: center;
|
||||
padding: 24px 16px;
|
||||
font-size: 1.2em;
|
||||
overflow: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.o_stock_mobile_barcode_container{
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
.o_stock_mobile_barcode{
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
position: absolute;
|
||||
opacity: 0.75;
|
||||
font-size: 12px;
|
||||
.o_mobile_barcode_camera{
|
||||
margin: 5px;
|
||||
font-size: 2.2em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_stock_barcode_menu {
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.message_demo_barcodes {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
>ul {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.o_stock_barcode_container {
|
||||
span.o_stock_barcode_laser {
|
||||
@include o-position-absolute(50%, -15px, auto, -15px);
|
||||
height: 5px;
|
||||
background: rgba(red, 0.6);
|
||||
box-shadow: 0 1px 10px 1px rgba(red, 0.8);
|
||||
animation: o_barcode_scanner_intro 1s cubic-bezier(0.6, -0.28, 0.735, 0.045) 0.4s;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 200px;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: 140px;
|
||||
height: 94px;
|
||||
}
|
||||
}
|
||||
.o_stock_mobile_barcode {
|
||||
height: 94px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex: 0 0 auto;
|
||||
width: 550px;
|
||||
border-radius: 4px;
|
||||
margin-top: -24px;
|
||||
|
||||
.o_stock_barcode_menu {
|
||||
line-height: 2.6;
|
||||
}
|
||||
|
||||
button.btn-primary {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
.row .col .btn {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.row .col {
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Defines animation for highlighting flash.
|
||||
$highlighting-colors: (
|
||||
"primary": theme-color("primary"),
|
||||
"white": white
|
||||
);
|
||||
|
||||
@each $c-name, $c-value in $highlighting-colors {
|
||||
@keyframes highlighting-flash-#{$c-name} {
|
||||
0% {
|
||||
background-color: #{$c-value};
|
||||
}
|
||||
20% {
|
||||
background-color: transparent;
|
||||
}
|
||||
21% {
|
||||
background-color: #{$c-value};
|
||||
}
|
||||
100% {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes o_barcode_scanner_intro {
|
||||
25% {
|
||||
top: 75%;
|
||||
}
|
||||
50% {
|
||||
top: 0;
|
||||
}
|
||||
75% {
|
||||
top: 100%;
|
||||
}
|
||||
100% {
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
111
stock_barcode/static/src/widgets/digipad.js
Normal file
111
stock_barcode/static/src/widgets/digipad.js
Normal file
@@ -0,0 +1,111 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, onWillStart } = owl;
|
||||
|
||||
export class Digipad extends Component {
|
||||
setup() {
|
||||
this.orm = useService('orm');
|
||||
const user = useService('user');
|
||||
this.buttons = [7, 8, 9, 4, 5, 6, 1, 2, 3, '.', '0', 'erase'].map((value, index) => {
|
||||
return { index, value };
|
||||
});
|
||||
this.value = String(this.props.record.data[this.props.quantityField]);
|
||||
onWillStart(async () => {
|
||||
this.displayUOM = await user.hasGroup('uom.group_uom');
|
||||
await this._fetchPackagingButtons();
|
||||
});
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Copies the input value if digipad value is not set yet, or overrides it if there is a
|
||||
* difference between the two values (in case user has manualy edited the input value).
|
||||
* @private
|
||||
*/
|
||||
_checkInputValue() {
|
||||
const input = document.querySelector(`div[name="${this.props.quantityField}"] input`);
|
||||
const inputValue = input.value;
|
||||
if (Number(this.value) != Number(inputValue)) {
|
||||
console.warn(`-- Change widget value: ${this.value} -> ${inputValue}`);
|
||||
this.value = inputValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments the field value by the interval amount (1 by default).
|
||||
* @private
|
||||
* @param {integer} [interval=1]
|
||||
*/
|
||||
async _increment(interval=1) {
|
||||
this._checkInputValue();
|
||||
const numberValue = Number(this.value || 0);
|
||||
this.value = String(numberValue + interval);
|
||||
await this._notifyChanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies changes on the field to mark the record as dirty.
|
||||
* @private
|
||||
*/
|
||||
async _notifyChanges() {
|
||||
const changes = { [this.props.quantityField]: Number(this.value) };
|
||||
await this.props.record.update(changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for the product's packaging buttons.
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async _fetchPackagingButtons() {
|
||||
const record = this.props.record.data;
|
||||
const demandQty = record.reserved_uom_qty;
|
||||
const domain = [['product_id', '=', record.product_id[0]]];
|
||||
if (demandQty) { // Doesn't fetch packaging with a too high quantity.
|
||||
domain.push(['qty', '<=', demandQty]);
|
||||
}
|
||||
this.packageButtons = await this.orm.searchRead(
|
||||
'product.packaging',
|
||||
domain,
|
||||
['name', 'product_uom_id', 'qty'],
|
||||
{ limit: 3 },
|
||||
);
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Handlers
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Handles the click on one of the digipad's button and updates the value..
|
||||
* @private
|
||||
* @param {String} button
|
||||
*/
|
||||
_onCLickButton(button) {
|
||||
this._checkInputValue();
|
||||
if (button === 'erase') {
|
||||
this.value = this.value.substr(0, this.value.length - 1);
|
||||
} else {
|
||||
if (button === '.' && this.value.indexOf('.') != -1) {
|
||||
// Avoids to add a decimal separator multiple time.
|
||||
return;
|
||||
}
|
||||
this.value += button;
|
||||
}
|
||||
this._notifyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
Digipad.template = 'stock_barcode.DigipadTemplate';
|
||||
Digipad.extractProps = ({ attrs }) => {
|
||||
return {
|
||||
quantityField: attrs.quantity_field,
|
||||
};
|
||||
};
|
||||
registry.category('view_widgets').add('digipad', Digipad);
|
||||
46
stock_barcode/static/src/widgets/digipad.scss
Normal file
46
stock_barcode/static/src/widgets/digipad.scss
Normal file
@@ -0,0 +1,46 @@
|
||||
.o_digipad_widget {
|
||||
display: flex;
|
||||
|
||||
.btn {
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
font-size: 2em;
|
||||
|
||||
.o_web_client.o_touch_device & {
|
||||
border-radius: 4px;
|
||||
font-size: 1em;
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_digipad_digit_buttons, .o_digipad_special_buttons {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(4, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.o_digipad_digit_buttons {
|
||||
width: 75%;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.o_digipad_special_buttons {
|
||||
width: 25%;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
.o_packaging_button {
|
||||
font-size: 1em;
|
||||
overflow: hidden;
|
||||
|
||||
div[name=packaging_name] {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 0.66em;
|
||||
}
|
||||
}
|
||||
36
stock_barcode/static/src/widgets/digipad.xml
Normal file
36
stock_barcode/static/src/widgets/digipad.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<div t-name="stock_barcode.DigipadTemplate" class="o_digipad_widget" owl="1">
|
||||
<t t-set="commonCssClasses">o_digipad_button btn d-flex justify-content-center align-items-center border w-100 py-2</t>
|
||||
<!-- Digits, erase and dot buttons -->
|
||||
<div class="o_digipad_digit_buttons me-2">
|
||||
<div t-foreach="buttons" t-as="button" t-key="button.index" t-att-data-button="button.value"
|
||||
class="btn-primary" t-att-class="commonCssClasses"
|
||||
t-on-click="() => this._onCLickButton(button.value)">
|
||||
<div t-if="button.value == 'erase'" class="fa fa-lg fa-long-arrow-left"/>
|
||||
<div t-else="" t-out="button.value"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_digipad_special_buttons">
|
||||
<!-- +1 / -1 buttons -->
|
||||
<div class="btn-secondary o_increase" t-att-class="commonCssClasses"
|
||||
t-on-click="() => this._increment()"
|
||||
t-att-data-button="increase">+1</div>
|
||||
<div class="btn-secondary o_decrease" t-att-class="commonCssClasses"
|
||||
t-on-click="() => this._increment(-1)"
|
||||
t-att-data-button="decrease">-1</div>
|
||||
<!-- Product packagings buttons -->
|
||||
<div t-foreach="packageButtons" t-as="button" t-key="button.id" t-att-data-qty="button.qty"
|
||||
t-on-click="() => this._increment(button.qty)"
|
||||
class="o_packaging_button btn btn-secondary border w-100 py-2">
|
||||
<div class="text-capitalize">
|
||||
<span t-out="'+' + button.qty"/>
|
||||
<span t-if="displayUOM" class="small-text ms-1" t-out="button.product_uom_id[1]"/>
|
||||
</div>
|
||||
<div name="packaging_name" class="small-text" t-out="button.name"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</templates>
|
||||
34
stock_barcode/static/src/widgets/set_reserved_qty_button.js
Normal file
34
stock_barcode/static/src/widgets/set_reserved_qty_button.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
const { Component, onWillStart } = owl;
|
||||
|
||||
export class SetReservedQuantityButton extends Component {
|
||||
setup() {
|
||||
const user = useService('user');
|
||||
onWillStart(async () => {
|
||||
this.displayUOM = await user.hasGroup('uom.group_uom');
|
||||
});
|
||||
}
|
||||
|
||||
get uom() {
|
||||
const [id, name] = this.props.record.data.product_uom_id || [];
|
||||
return { id, name };
|
||||
}
|
||||
|
||||
_setQuantity (ev) {
|
||||
ev.stopPropagation();
|
||||
this.props.record.update({ [this.props.fieldToSet]: this.props.value });
|
||||
}
|
||||
}
|
||||
|
||||
SetReservedQuantityButton.extractProps = ({ attrs }) => {
|
||||
if (attrs.field_to_set) {
|
||||
return { fieldToSet: attrs.field_to_set };
|
||||
}
|
||||
};
|
||||
|
||||
SetReservedQuantityButton.template = 'stock_barcode.SetReservedQuantityButtonTemplate';
|
||||
registry.category('fields').add('set_reserved_qty_button', SetReservedQuantityButton);
|
||||
@@ -0,0 +1,5 @@
|
||||
.o_web_client.o_touch_device .o_button_qty_done, .o_button_qty_done {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 1em;
|
||||
}
|
||||
14
stock_barcode/static/src/widgets/set_reserved_qty_button.xml
Normal file
14
stock_barcode/static/src/widgets/set_reserved_qty_button.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<button t-name="stock_barcode.SetReservedQuantityButtonTemplate" owl="1"
|
||||
class="o_button_qty_done d-flex btn"
|
||||
t-attf-class="{{props.value ? 'btn-primary' : 'btn-secondary'}}"
|
||||
t-on-click="_setQuantity" t-att-disabled="!props.value">
|
||||
<t t-if="props.value">
|
||||
/ <span name="product_uom_qty" class="ms-1" t-out="props.value"/>
|
||||
</t>
|
||||
<span t-if="displayUOM" name="product_uom_id" class="text-capitalize ms-1" t-out="uom.name"/>
|
||||
</button>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user