Skip to content

Commit 6dc64d0

Browse files
committed
[ADD] sales_order_barcode: implement barcode scanning in product catalog
Before this commit: Users had to manually enter the barcode into the search bar to find products in the catalog view. After this commit: Scanning a barcode automatically adds a quantity of 1 to the order without manual intervention. Displays a toaster message saying “No product found with this barcode number” if no matching product is found. If the same product is scanned multiple times, its quantity is incremented on the existing order line. All scanned products are sorted and displayed on the first page for easy access.
1 parent fbf9ee9 commit 6dc64d0

File tree

10 files changed

+367
-0
lines changed

10 files changed

+367
-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: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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, useBus } from "@web/core/utils/hooks";
5+
6+
patch(ProductCatalogKanbanController.prototype, {
7+
/**
8+
* @override
9+
*/
10+
setup() {
11+
super.setup();
12+
this.orm = useService("orm");
13+
this.notification = useService("notification");
14+
this.barcodeService = useService('barcode');
15+
useBus(this.barcodeService.bus, 'barcode_scanned', (ev) => this._processBarcode(ev.detail.barcode));
16+
},
17+
18+
/**
19+
* Processes the scanned barcode to find the corresponding product and update the order.
20+
*
21+
* @param {string} scannedBarcode The barcode string to process.
22+
*/
23+
async _processBarcode(scannedBarcode) {
24+
// An order must be selected to add products.
25+
if (!this.orderId) {
26+
this.notification.add("Please select an order first.", { type: "warning" });
27+
return;
28+
}
29+
30+
try {
31+
// Search for a product with the scanned barcode.
32+
const products = await this.orm.searchRead(
33+
"product.product",
34+
[["barcode", "=", scannedBarcode]],
35+
["id", "name"]
36+
);
37+
38+
if (!products.length) {
39+
this.notification.add("No product found for this barcode.", { type: "warning" });
40+
return;
41+
}
42+
43+
const product = products[0];
44+
45+
let orderLineModel, quantityField;
46+
// Determine the correct model and field names based on the order type.
47+
if (this.orderResModel === "sale.order") {
48+
orderLineModel = "sale.order.line";
49+
quantityField = "product_uom_qty";
50+
} else if (this.orderResModel === "purchase.order") {
51+
orderLineModel = "purchase.order.line";
52+
quantityField = "product_qty";
53+
} else {
54+
// Log an error if the order model is not supported.
55+
console.error("Unsupported order model for barcode scanning:", this.orderResModel);
56+
this.notification.add("Barcode scanning is not supported for this document type.", { type: "danger" });
57+
return;
58+
}
59+
60+
// Check if there is an existing order line for this product.
61+
const existingOrderLines = await this.orm.searchRead(
62+
orderLineModel,
63+
[["order_id", "=", this.orderId], ["product_id", "=", product.id]],
64+
["id", quantityField]
65+
);
66+
67+
// If a line exists, increment its quantity; otherwise, set quantity to 1.
68+
const updatedQuantity = existingOrderLines.length ? existingOrderLines[0][quantityField] + 1 : 1;
69+
70+
// Call the backend to create or update the order line.
71+
await rpc("/product/catalog/update_order_line_info", {
72+
res_model: this.orderResModel,
73+
order_id: this.orderId,
74+
product_id: product.id,
75+
quantity: updatedQuantity,
76+
});
77+
78+
// Notify the user of the successful addition.
79+
this.notification.add(
80+
`Added ${product.name} (Qty: ${updatedQuantity})`,
81+
{ type: "success" }
82+
);
83+
84+
// Reload the view to show the updated order line information.
85+
this.model.load();
86+
87+
} catch (error) {
88+
console.error("Error processing barcode scan:", error);
89+
this.notification.add("An error occurred while processing the barcode.", { type: "danger" });
90+
}
91+
},
92+
});
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: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
10+
"""Finds a product by barcode and adds/updates it in the given SO."""
11+
if not barcode or not order_id:
12+
return {'warning': 'Barcode or Order ID not provided.'}
13+
14+
order = self.browse(order_id)
15+
product = self.env['product.product'].search(
16+
[('barcode', '=', barcode)], limit=1
17+
)
18+
19+
if not product:
20+
# Request: Raise toaster "No product found..."
21+
return {'warning': ('No product found with this barcode: %s') % barcode}
22+
23+
existing_line = self.env['sale.order.line'].search([
24+
('order_id', '=', order.id),
25+
('product_id', '=', product.id)
26+
], limit=1)
27+
28+
# Request: If scanning the same product, increase the qty
29+
if existing_line:
30+
existing_line.product_uom_qty += 1
31+
# Request: Hit the 'add' button with qty 1
32+
else:
33+
self.env['sale.order.line'].create({
34+
'order_id': order.id,
35+
'product_id': product.id,
36+
'product_uom_qty': 1,
37+
})
38+
39+
# Return product info to highlight the card in the UI
40+
return {
41+
'product_id': product.id,
42+
'product_name': product.name,
43+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/** @odoo-module **/
2+
3+
import { registry } from "@web/core/registry";
4+
import { useService } from "@web/core/utils/hooks";
5+
import { session } from "@web/session";
6+
import { Component, xml, onMounted, onWillUnmount } from "@odoo/owl";
7+
8+
// This component will be attached to the form view
9+
export class BarcodeHandler extends Component {
10+
11+
static template = xml``;
12+
static props = {
13+
"*": { optional: true },
14+
};
15+
16+
setup() {
17+
this.orm = useService("orm");
18+
this.notification = useService("notification");
19+
this.lastInputTime = 0;
20+
this.barcodeBuffer = "";
21+
22+
this._onKeyDown = this._onKeyDown.bind(this);
23+
24+
onMounted(() => {
25+
console.log("BarcodeHandler: Component Mounted. Listening for keys.");
26+
document.addEventListener("keydown", this._onKeyDown);
27+
});
28+
29+
onWillUnmount(() => {
30+
document.removeEventListener("keydown", this._onKeyDown);
31+
});
32+
}
33+
34+
async _onKeyDown(ev) {
35+
// Specifically identify the search input
36+
const isSearchInput = ev.target.classList.contains('o_searchview_input');
37+
const isRegularInput = ev.target.tagName === 'INPUT' || ev.target.tagName === 'TEXTAREA';
38+
39+
// Allow the function to run if the target is the search input,
40+
// but block it for other regular input fields.
41+
if (this.env.model.root.resModel !== 'sale.order' || (isRegularInput && !isSearchInput)) {
42+
return;
43+
}
44+
45+
const currentTime = new Date().getTime();
46+
// Using a longer timeout for easier manual testing
47+
if (currentTime - this.lastInputTime > 500) {
48+
this.barcodeBuffer = "";
49+
}
50+
51+
if (ev.key === "Enter") {
52+
// Using a short length for easier manual testing
53+
if (this.barcodeBuffer.length > 1) {
54+
this._processBarcode(this.barcodeBuffer);
55+
}
56+
this.barcodeBuffer = "";
57+
} else if (ev.key.length === 1) {
58+
this.barcodeBuffer += ev.key;
59+
}
60+
this.lastInputTime = currentTime;
61+
}
62+
63+
async _processBarcode(barcode) {
64+
const orderId = this.env.model.root.resId;
65+
console.log(`BarcodeHandler: Processing barcode '${barcode}' for Order ID ${orderId}`); // <-- ADD THIS
66+
67+
const result = await this.orm.call(
68+
"sale.order",
69+
"add_product_by_barcode",
70+
[barcode, orderId],
71+
{ context: session.user_context }
72+
);
73+
74+
console.log("BarcodeHandler: Received response from backend:", result); // <-- ADD THIS
75+
76+
if (result.warning) {
77+
this.notification.add(result.warning, { type: "danger" });
78+
} else if (result.product_id) {
79+
80+
await this.env.model.load();
81+
}
82+
}
83+
}
84+
// We don't have a template as this is a logic-only component
85+
// BarcodeHandler.template = xml``;
86+
87+
// Add it to the form view's widgets registry
88+
registry.category("view_widgets").add("barcode_handler_widget", {
89+
component: BarcodeHandler,
90+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<odoo>
3+
<record id="view_order_form_with_barcode" model="ir.ui.view">
4+
<field name="name">sale.order.form.barcode</field>
5+
<field name="model">sale.order</field>
6+
<field name="inherit_id" ref="sale.view_order_form"/>
7+
<field name="arch" type="xml">
8+
<xpath expr="//header" position="inside">
9+
<widget name="barcode_handler_widget"/>
10+
</xpath>
11+
</field>
12+
</record>
13+
</odoo>

0 commit comments

Comments
 (0)