diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000000..ff5300ef481 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.languageServer": "None" +} \ No newline at end of file diff --git a/sales_barcodescan/__manifest__.py b/sales_barcodescan/__manifest__.py new file mode 100644 index 00000000000..7f841b5b8f4 --- /dev/null +++ b/sales_barcodescan/__manifest__.py @@ -0,0 +1,15 @@ +{ + "name": "SO/PO barcode scan", + "version": "1.0", + "summary": "Add products to PO/SO from catalog via barcode scanning.", + "author": "prbo", + "depends": ["sale_management", "product"], + "license": "LGPL-3", + "assets": { + "web.assets_backend": [ + "sales_barcodescan/static/src/**/*", + ], + }, + "installable": True, + "application": True, +} diff --git a/sales_barcodescan/static/src/barcode_handler.js b/sales_barcodescan/static/src/barcode_handler.js new file mode 100644 index 00000000000..b5710c403a6 --- /dev/null +++ b/sales_barcodescan/static/src/barcode_handler.js @@ -0,0 +1,126 @@ +import { patch } from "@web/core/utils/patch"; +import { ProductCatalogKanbanController } from "@product/product_catalog/kanban_controller"; +import { rpc } from "@web/core/network/rpc"; +import { useService } from "@web/core/utils/hooks"; +import { onMounted, onWillUnmount } from "@odoo/owl"; + +patch(ProductCatalogKanbanController.prototype, { + setup(){ + super.setup(); + this.orm = useService("orm"); + this.notification = useService("notification"); + this.orderId = this.props.context.order_id; + this.orderResModel = this.props.context.product_catalog_order_model; + this.lastInputTime = 0; + this.barcodeBuffer = ""; + this._onKeyDown = this._onKeyDown.bind(this); + onMounted(() => { + document.addEventListener("keydown", this._onKeyDown); + }); + onWillUnmount(() => { + document.removeEventListener("keydown", this._onKeyDown); + }); + }, + + async _onKeyDown(res) { + const targetfield = res.target.tagName; + if (targetfield === "INPUT" || targetfield === "TEXTAREA" || targetfield === "SELECT") { + return; + } + const currentTime = new Date().getTime(); + if (currentTime - this.lastInputTime > 800) { + this.barcodeBuffer = ""; + } + if (res.key === "Enter") { + if (this.barcodeBuffer.length > 1) { + this._processBarcode(this.barcodeBuffer); + this.barcodeBuffer = ""; + } + } else if (res.key.length === 1) { + this.barcodeBuffer += res.key; + } + this.lastInputTime = currentTime; + }, + + async _processBarcode(scannedBarcode) { + if (!this.orderId) { + this.notification.add("Please select an order first.", { + type: "warning", + }); + return; + } + try { + const products = await this.orm.searchRead( + "product.product", + [["barcode", "=", scannedBarcode]], + ["id", "name"] + ); + + if (!products.length) { + this.notification.add("No product found for this barcode.", { + type: "warning", + }); + return; + } + + const product = products[0]; + + let orderLineModel, quantityField; + if (this.orderResModel === "sale.order") { + orderLineModel = "sale.order.line"; + quantityField = "product_uom_qty"; + } else if (this.orderResModel === "purchase.order") { + orderLineModel = "purchase.order.line"; + quantityField = "product_qty"; + } else { + console.error( + "Unsupported order model for barcode scanning:", + this.orderResModel + ); + this.notification.add( + "Barcode scanning is not supported for this type of model.", + { type: "danger" } + ); + return; + } + + const existingOrderLines = await this.orm.searchRead( + orderLineModel, + [ + ["order_id", "=", this.orderId], + ["product_id", "=", product.id], + ], + ["id", quantityField] + ); + + const updatedQuantity = existingOrderLines.length ? existingOrderLines[0][quantityField] + 1 : 1; + + const response = await rpc("/product/catalog/update_order_line_info", { + res_model: this.orderResModel, + order_id: this.orderId, + product_id: product.id, + quantity: updatedQuantity, + }); + + if (response && response.success) { + this.notification.add( + `successfully ${existingOrderLines ? "Updated" : "Added"} ${product.name} (Qty: ${updatedQuantity})`, + { type: "success" } + ); + this.model.load(); + } else { + this.notification.add( + `failed to ${existingOrderLines ? "update" : "add"} ${product.name}.`, + { type: "danger" } + ); + this.model.load(); + } + this.model.load(); + } catch (error) { + console.error("Error processing barcode scan:", error); + this.notification.add("An error occurred while processing the barcode.", { + type: "danger", + }); + } + }, +}); diff --git a/sales_barcodescan/static/src/barcode_kanban_model.js b/sales_barcodescan/static/src/barcode_kanban_model.js new file mode 100644 index 00000000000..ba931d92ffe --- /dev/null +++ b/sales_barcodescan/static/src/barcode_kanban_model.js @@ -0,0 +1,56 @@ +import { rpc } from "@web/core/network/rpc"; +import { ProductCatalogKanbanModel } from "@product/product_catalog/kanban_model"; +import { getFieldsSpec } from "@web/model/relational_model/utils"; + +export class BarcodeProductCatalogKanbanModel extends ProductCatalogKanbanModel { + async _loadUngroupedList(config) { + const allProducts = await this.orm.search(config.resModel, config.domain); + + if (!allProducts.length) { + return { records: [], length: 0 }; + } + + let orderLines = {}; + const scanned = [], unscanned = []; + + if (config.context.order_id && config.context.product_catalog_order_model) { + orderLines = await rpc("/product/catalog/order_lines_info", { + order_id: config.context.order_id, + product_ids: allProducts, + res_model: config.context.product_catalog_order_model, + }); + + for (const id of allProducts) { + const qty = (orderLines[id]?.quantity) || 0; + if (qty > 0) scanned.push(id); + else unscanned.push(id); + } + + scanned.sort((a, b) => + (orderLines[b]?.quantity || 0) - (orderLines[a]?.quantity || 0) + ); + } else { + unscanned.push(...allProducts); + } + + const sortedIds = [...scanned, ...unscanned]; + const paginatedIds = sortedIds.slice(config.offset, config.offset + config.limit); + + const kwargs = { + specification: getFieldsSpec(config.activeFields, config.fields, config.context), //passed through webserach() in orm + }; + + const result = await this.orm.webSearchRead(config.resModel, [["id", "in", paginatedIds]], kwargs); + + result.records.sort((a, b) => { + const qtyA = orderLines[a.id]?.quantity || 0; + const qtyB = orderLines[b.id]?.quantity || 0; + return qtyB - qtyA || a.id - b.id; + }); + + return { + length: allProducts.length, + records: result.records, + }; + } +} diff --git a/sales_barcodescan/static/src/barcode_kanban_view.js b/sales_barcodescan/static/src/barcode_kanban_view.js new file mode 100644 index 00000000000..b191604d1fb --- /dev/null +++ b/sales_barcodescan/static/src/barcode_kanban_view.js @@ -0,0 +1,11 @@ +import { registry } from "@web/core/registry"; +import { productCatalogKanbanView } from "@product/product_catalog/kanban_view"; +import { BarcodeProductCatalogKanbanModel } from "./barcode_kanban_model"; + +export const BarcodeProductCatalogKanbanView = { + ...productCatalogKanbanView, + Model: BarcodeProductCatalogKanbanModel, +}; + +registry.category("views").remove("product_kanban_catalog"); +registry.category("views").add("product_kanban_catalog", BarcodeProductCatalogKanbanView);