质量模块和库存扫码

This commit is contained in:
qihao.gong@jikimo.com
2023-07-24 11:42:15 +08:00
parent 8d024ad625
commit 3c89404543
228 changed files with 142596 additions and 0 deletions

View 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';

View 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;
}
}
}
}

View 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>

View 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';

View 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; }
}
}

View 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' &amp;&amp; qtyDone ? 'o_flash' : ''}}
{{isSelected &amp;&amp; qtyDemand &amp;&amp; qtyDone &amp;&amp; qtyDone &lt; 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 &amp;&amp; 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 &amp;&amp; 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 &amp;&amp; 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 &lt;= 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>

View 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;

View 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;
}
}
}
}
}

View 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">&#215;</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 &amp;&amp; (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 &amp;&amp; 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>

View 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';

View 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>