Skip to content

Commit a959fc5

Browse files
committed
[ADD] sale purchase: added barcode scanning in product catalog
Automatically hit adds quantity 1 when a product is found via barcode scan (no manual intervention) Show toaster message 'No product found with this barcode number' if no match is found Increment quantity on existing order line if the same product is scanned multiple times Ensure all scanned products are sorted and shown on the first page
1 parent fbf9ee9 commit a959fc5

File tree

10 files changed

+421
-0
lines changed

10 files changed

+421
-0
lines changed

sales_order_barcode/__manifest__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "Sale Order Barcode Scanning",
3+
"version": "1.0",
4+
"category": "Sales",
5+
"summary": "Adds products to SO from catalog via barcode scanning.",
6+
"author": "Kalpan Desai",
7+
"depends": ["sale_management", "web", "product"],
8+
"license": "LGPL-3",
9+
"assets": {
10+
"web.assets_backend": [
11+
"sales_order_barcode/static/src/**/*"
12+
]
13+
},
14+
"installable": True,
15+
"application": True,
16+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { patch } from "@web/core/utils/patch";
2+
import { ProductCatalogKanbanController } from "@product/product_catalog/kanban_controller";
3+
import { rpc } from "@web/core/network/rpc";
4+
import { useService } from "@web/core/utils/hooks";
5+
import { onMounted, onWillUnmount } from "@odoo/owl";
6+
7+
patch(ProductCatalogKanbanController.prototype, {
8+
/**
9+
* @override
10+
*/
11+
setup() {
12+
super.setup();
13+
this.orm = useService("orm");
14+
this.notification = useService("notification");
15+
this.orderId = this.props.context.order_id;
16+
// The resModel of the order
17+
this.orderResModel = this.props.context.product_catalog_order_model;
18+
19+
// Properties for keyboard-based barcode scanning
20+
this.lastInputTime = 0;
21+
this.barcodeBuffer = "";
22+
23+
// Bind the keydown handler to this component instance
24+
this._onKeyDown = this._onKeyDown.bind(this);
25+
26+
// Add and remove the event listener when the component is mounted and unmounted.
27+
onMounted(() => {
28+
document.addEventListener("keydown", this._onKeyDown);
29+
});
30+
31+
onWillUnmount(() => {
32+
document.removeEventListener("keydown", this._onKeyDown);
33+
});
34+
},
35+
36+
/**
37+
* Handles the 'keydown' event to capture barcode scans from a keyboard/scanner.
38+
*
39+
* @param {KeyboardEvent} ev
40+
*/
41+
async _onKeyDown(ev) {
42+
// Ignore key events if they originate from an input, textarea, or select element
43+
// to avoid interfering with user typing.
44+
const targetTagName = ev.target.tagName;
45+
if (targetTagName === 'INPUT' || targetTagName === 'TEXTAREA' || targetTagName === 'SELECT') {
46+
return;
47+
}
48+
49+
const currentTime = new Date().getTime();
50+
// If the last keypress was more than 500ms ago, reset the buffer.
51+
// This prevents accidental concatenation of different barcodes.
52+
if (currentTime - this.lastInputTime > 500) {
53+
this.barcodeBuffer = "";
54+
}
55+
56+
// If the 'Enter' key is pressed, process the buffered input as a barcode.
57+
if (ev.key === "Enter") {
58+
if (this.barcodeBuffer.length > 1) {
59+
this._processBarcode(this.barcodeBuffer);
60+
this.barcodeBuffer = ""; // Reset buffer after processing
61+
}
62+
} else if (ev.key.length === 1) {
63+
// Append the character to our buffer if it's a single character key.
64+
this.barcodeBuffer += ev.key;
65+
}
66+
67+
// Update the timestamp of the last keypress.
68+
this.lastInputTime = currentTime;
69+
},
70+
71+
/**
72+
* Processes the scanned barcode to find the corresponding product and update the order.
73+
*
74+
* @param {string} scannedBarcode The barcode string to process.
75+
*/
76+
async _processBarcode(scannedBarcode) {
77+
// An order must be selected to add products.
78+
if (!this.orderId) {
79+
this.notification.add("Please select an order first.", { type: "warning" });
80+
return;
81+
}
82+
83+
try {
84+
// Search for a product with the scanned barcode.
85+
const products = await this.orm.searchRead(
86+
"product.product",
87+
[["barcode", "=", scannedBarcode]],
88+
["id", "name"]
89+
);
90+
91+
if (!products.length) {
92+
this.notification.add("No product found for this barcode.", { type: "warning" });
93+
return;
94+
}
95+
96+
const product = products[0];
97+
98+
let orderLineModel, quantityField;
99+
// Determine the correct model and field names based on the order type.
100+
if (this.orderResModel === "sale.order") {
101+
orderLineModel = "sale.order.line";
102+
quantityField = "product_uom_qty";
103+
} else if (this.orderResModel === "purchase.order") {
104+
orderLineModel = "purchase.order.line";
105+
quantityField = "product_qty";
106+
} else {
107+
// Log an error if the order model is not supported.
108+
console.error("Unsupported order model for barcode scanning:", this.orderResModel);
109+
this.notification.add("Barcode scanning is not supported for this document type.", { type: "danger" });
110+
return;
111+
}
112+
113+
// Check if there is an existing order line for this product.
114+
const existingOrderLines = await this.orm.searchRead(
115+
orderLineModel,
116+
[["order_id", "=", this.orderId], ["product_id", "=", product.id]],
117+
["id", quantityField]
118+
);
119+
120+
// If a line exists, increment its quantity; otherwise, set quantity to 1.
121+
const updatedQuantity = existingOrderLines.length ? existingOrderLines[0][quantityField] + 1 : 1;
122+
123+
// Call the backend to create or update the order line.
124+
await rpc("/product/catalog/update_order_line_info", {
125+
res_model: this.orderResModel,
126+
order_id: this.orderId,
127+
product_id: product.id,
128+
quantity: updatedQuantity,
129+
});
130+
131+
// Notify the user of the successful addition.
132+
this.notification.add(
133+
`Added ${product.name} (Qty: ${updatedQuantity})`,
134+
{ type: "success" }
135+
);
136+
137+
// Reload the view to show the updated order line information.
138+
this.model.load();
139+
140+
} catch (error) {
141+
console.error("Error processing barcode scan:", error);
142+
this.notification.add("An error occurred while processing the barcode.", { type: "danger" });
143+
}
144+
},
145+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/** @odoo-module **/
2+
3+
import { rpc } from "@web/core/network/rpc";
4+
import { ProductCatalogKanbanModel } from "@product/product_catalog/kanban_model";
5+
import { getFieldsSpec } from "@web/model/relational_model/utils";
6+
7+
export class BarcodeProductCatalogKanbanModel extends ProductCatalogKanbanModel {
8+
9+
// async _loadData(params) {
10+
// const result = await super._loadData(...arguments);
11+
// // Only when not mono-record and no grouping
12+
// if (!params.isMonoRecord && !params.groupBy.length) {
13+
// const orderLines = await rpc("/product/catalog/order_lines_info", super._getorderLinesParams(params, result.records.map((rec) => rec.id)));
14+
// for (const record of result.records) {
15+
// record.productCatalogData = orderLines[record.id];
16+
// }
17+
// // === SORT: Move products with higher ordered quantity to top ===
18+
// result.records.sort((a, b) => {
19+
// const qtyA = (a.productCatalogData && a.productCatalogData.quantity) || 0;
20+
// const qtyB = (b.productCatalogData && b.productCatalogData.quantity) || 0;
21+
// // Descending by quantity, then by id for stability
22+
// if (qtyB === qtyA) return a.id - b.id;
23+
// return qtyB - qtyA;
24+
// });
25+
// }
26+
// return result;
27+
// }
28+
29+
30+
async _loadUngroupedList(config) {
31+
const allProducts = await this.orm.search(config.resModel, config.domain);
32+
33+
if (!allProducts.length) {
34+
return { records: [], length: 0 };
35+
}
36+
37+
let orderLines = {};
38+
const scanned = [], unscanned = [];
39+
40+
if (config.context.order_id && config.context.product_catalog_order_model) {
41+
orderLines = await rpc("/product/catalog/order_lines_info", {
42+
order_id: config.context.order_id,
43+
product_ids: allProducts,
44+
res_model: config.context.product_catalog_order_model,
45+
});
46+
47+
for (const id of allProducts) {
48+
const qty = (orderLines[id]?.quantity) || 0;
49+
if (qty > 0) scanned.push(id);
50+
else unscanned.push(id);
51+
}
52+
53+
54+
scanned.sort((a, b) =>
55+
(orderLines[b]?.quantity || 0) - (orderLines[a]?.quantity || 0)
56+
);
57+
} else {
58+
unscanned.push(...allProducts);
59+
}
60+
61+
const sortedProductIds = [...scanned, ...unscanned];
62+
const paginatedProductIds = sortedProductIds.slice(config.offset, config.offset + config.limit);
63+
64+
const kwargs = {
65+
specification: getFieldsSpec(config.activeFields, config.fields, config.context),
66+
};
67+
68+
const result = await this.orm.webSearchRead(config.resModel, [["id", "in", paginatedProductIds]], kwargs);
69+
70+
result.records.sort((a, b) => {
71+
const qtyA = orderLines[a.id]?.quantity || 0;
72+
const qtyB = orderLines[b.id]?.quantity || 0;
73+
return qtyB - qtyA || a.id - b.id;
74+
});
75+
76+
return {
77+
length: allProducts.length,
78+
records: result.records,
79+
};
80+
}
81+
82+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { registry } from "@web/core/registry";
2+
import { productCatalogKanbanView } from "@product/product_catalog/kanban_view";
3+
import { BarcodeProductCatalogKanbanModel } from "./kanban_model";
4+
5+
export const BarcodeProductCatalogKanbanView = {
6+
...productCatalogKanbanView,
7+
Model: BarcodeProductCatalogKanbanModel,
8+
};
9+
10+
registry.category("views").remove("product_kanban_catalog");
11+
registry.category("views").add("product_kanban_catalog", BarcodeProductCatalogKanbanView);

sales_order_barcode1/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import models

sales_order_barcode1/__manifest__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "Sale Order Barcode Scanning",
3+
"version": "1.0",
4+
"summary": "Adds products to SO from catalog via barcode scanning.",
5+
"author": "Kalpan Desai",
6+
"depends": ["sale_management", "web", "product", "base"],
7+
"license": "LGPL-3",
8+
"assets": {
9+
"web.assets_backend": [
10+
"sales_order_barcode1/static/src/barcode_handler.js",
11+
],
12+
},
13+
"data": [
14+
"views/sale_order_view.xml",
15+
],
16+
"installable": True,
17+
"application": True,
18+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import sales_order
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from odoo import api, models
2+
3+
4+
class SaleOrder(models.Model):
5+
_inherit = 'sale.order'
6+
7+
@api.model
8+
def add_product_by_barcode(self, barcode, order_id):
9+
# print("function called by %s %s",barcode,order_id)
10+
11+
"""Finds a product by barcode and adds/updates it in the given SO."""
12+
if not barcode or not order_id:
13+
return {'warning': 'Barcode or Order ID not provided.'}
14+
15+
order = self.browse(order_id)
16+
product = self.env['product.product'].search(
17+
[('barcode', '=', barcode)], limit=1
18+
)
19+
20+
if not product:
21+
# Request: Raise toaster "No product found..."
22+
return {'warning': ('No product found with this barcode: %s') % barcode}
23+
24+
existing_line = self.env['sale.order.line'].search([
25+
('order_id', '=', order.id),
26+
('product_id', '=', product.id)
27+
], limit=1)
28+
29+
# Request: If scanning the same product, increase the qty
30+
if existing_line:
31+
existing_line.product_uom_qty += 1
32+
# Request: Hit the 'add' button with qty 1
33+
else:
34+
self.env['sale.order.line'].create({
35+
'order_id': order.id,
36+
'product_id': product.id,
37+
'product_uom_qty': 1,
38+
})
39+
40+
# Return product info to highlight the card in the UI
41+
return {
42+
'product_id': product.id,
43+
'product_name': product.name,
44+
}

0 commit comments

Comments
 (0)