@ -1,15 +1,46 @@ |
|||
POS Serial Number Validator v15 |
|||
=============================== |
|||
.. image:: https://img.shields.io/badge/licence-LGPL--3-green.svg |
|||
:target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html |
|||
:alt: License: LGPL-3 |
|||
|
|||
Validate Serial number of a product by checking availability in stock |
|||
POS Serial Number Validator |
|||
=========================== |
|||
Validate lot/ Serial number of a product by checking the availability in the stock and duplication of serial number |
|||
|
|||
Company |
|||
------- |
|||
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__ |
|||
|
|||
License |
|||
------- |
|||
General Public License, Version 3 (LGPL v3). |
|||
(https://www.gnu.org/licenses/lgpl-3.0-standalone.html) |
|||
|
|||
Credits |
|||
======= |
|||
Cybrosys Techno Solutions |
|||
|
|||
Author |
|||
------ |
|||
* Akhilesh N S <odoo@cybrosys.com> |
|||
* V13 Sreenath |
|||
* V14 Jibin James |
|||
* V15 Mily |
|||
------- |
|||
Developers: (V12) Akhilesh N S, |
|||
(V13) Sreenath |
|||
(V14) Jibin James |
|||
(V15) Mily Shajan, Abhishek E T |
|||
Contact: odoo@cybrosys.com |
|||
|
|||
Contacts |
|||
-------- |
|||
* Mail Contact : odoo@cybrosys.com |
|||
* Website : https://cybrosys.com |
|||
|
|||
Bug Tracker |
|||
----------- |
|||
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. |
|||
|
|||
Maintainer |
|||
========== |
|||
.. image:: https://cybrosys.com/images/logo.png |
|||
:target: https://cybrosys.com |
|||
|
|||
This module is maintained by Cybrosys Technologies. |
|||
|
|||
For support and more information, please visit `Our Website <https://cybrosys.com/>`__ |
|||
|
|||
Further information |
|||
=================== |
|||
HTML Description: `<static/description/index.html>`__ |
|||
|
@ -1,7 +1,11 @@ |
|||
## Module <pos_traceability_validation> |
|||
|
|||
#### 08.04.2021 |
|||
#### 08.04.2022 |
|||
#### Version 15.0.1.0.0 |
|||
#### ADD |
|||
Initial Commit for POS Serial Number Validator |
|||
|
|||
Initial Commit |
|||
#### 16.10.2023 |
|||
#### Version 15.0.1.1.0 |
|||
#### REF |
|||
Refactor the module and update the index |
|||
|
@ -0,0 +1,73 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################### |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2023-TODAY Cybrosys Technologies(<https://www.cybrosys.com>) |
|||
# Author: Abhishek E T (odoo@cybrosys.com) |
|||
# |
|||
# You can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################### |
|||
from odoo import api, models |
|||
|
|||
|
|||
class StockProductionLot(models.Model): |
|||
""" |
|||
This class is inherited for adding a new function to validate the lots and |
|||
serial numbers. |
|||
Methods: |
|||
validate_lots(lots): |
|||
check and validate the lots and serial numbers for the product |
|||
based on the stock location. |
|||
""" |
|||
_inherit = 'stock.production.lot' |
|||
|
|||
@api.model |
|||
def validate_lots(self, lots, product_id, picking_type_id): |
|||
""" To check |
|||
- the invalid lots/ serial numbers |
|||
- duplicate serial numbers |
|||
- insufficient stock for the lots or serial numbers. |
|||
All these cases are checked based on the product and the stock location |
|||
set for the active PoS. |
|||
Args: |
|||
lots (list[str,..., str]): the lots for validation. |
|||
product_id (int): id of the selected product. |
|||
picking_type_id (int): id of the operation type added for the PoS. |
|||
Returns: |
|||
list[str, str] or Bool: True if the lot is valid, else the list of |
|||
the string that indicates the exception: 'invalid', 'duplicate' or |
|||
'no_stock' with the lot/ serial number. |
|||
""" |
|||
processed = [] |
|||
if not product_id: |
|||
return ['invalid', 'product'] |
|||
for lot in lots: |
|||
stock_lots = self.sudo().search([ |
|||
('name', '=', lot), ('product_id', '=', product_id)]) |
|||
if not stock_lots: |
|||
return ['invalid', lot] |
|||
picking_type = self.env['stock.picking.type'].sudo().browse( |
|||
picking_type_id) |
|||
stock_quant = self.env['stock.quant'].sudo().search( |
|||
[('location_id', '=', picking_type.default_location_src_id.id), |
|||
('lot_id', 'in', stock_lots.ids)]) |
|||
if (stock_quant and stock_quant.available_quantity > 0 |
|||
and lot not in processed): |
|||
processed.append(lot) |
|||
else: |
|||
if lot in processed: |
|||
return ['duplicate', lot] |
|||
return ['no_stock', lot] |
|||
return True |
@ -1,45 +0,0 @@ |
|||
# -*- coding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# Cybrosys Technologies Pvt. Ltd. |
|||
# |
|||
# Copyright (C) 2019-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |
|||
# Author: Akhilesh N S(<http://www.cybrosys.com>) |
|||
# you can modify it under the terms of the GNU LESSER |
|||
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3. |
|||
# |
|||
# This program is distributed in the hope that it will be useful, |
|||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details. |
|||
# |
|||
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE |
|||
# GENERAL PUBLIC LICENSE (LGPL v3) along with this program. |
|||
# If not, see <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from odoo import models, api |
|||
|
|||
|
|||
class ValidateLotNumber(models.Model): |
|||
_name = 'serial_no.validation' |
|||
|
|||
@api.model |
|||
def validate_lots(self, lots): |
|||
processed = [] |
|||
LotObj = self.env['stock.production.lot'] |
|||
for lot in lots: |
|||
lot_id = LotObj.search([('name', '=', lot)], limit=1) |
|||
try: |
|||
if lot_id.product_qty > 0 and lot not in processed: |
|||
processed.append(lot) |
|||
continue |
|||
else: |
|||
if lot in processed: |
|||
return ['duplicate', lot] |
|||
else: |
|||
return ['no_stock', lot] |
|||
except Exception: |
|||
return ['except', lot] |
|||
return True |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 59 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 60 KiB |
After Width: | Height: | Size: 58 KiB |
After Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 59 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 58 KiB |
Before Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 184 KiB |
After Width: | Height: | Size: 217 KiB |
After Width: | Height: | Size: 94 KiB |
After Width: | Height: | Size: 86 KiB |
After Width: | Height: | Size: 71 KiB |
After Width: | Height: | Size: 69 KiB |
After Width: | Height: | Size: 175 KiB |
After Width: | Height: | Size: 209 KiB |
After Width: | Height: | Size: 217 KiB |
After Width: | Height: | Size: 273 KiB |
@ -0,0 +1,74 @@ |
|||
odoo.define('pos_traceability_validation.PoSEditListPopup', function (require) { |
|||
'use strict'; |
|||
const EditListPopup = require('point_of_sale.EditListPopup'); |
|||
const Registries = require('point_of_sale.Registries'); |
|||
const { _lt } = require('@web/core/l10n/translation'); |
|||
var rpc = require('web.rpc'); |
|||
/** |
|||
* Extends EditListPopup for adding functionality for lot/ serial number |
|||
* validation |
|||
*/ |
|||
const PoSEditListPopup = (EditListPopup) => |
|||
class extends EditListPopup { |
|||
constructor() { |
|||
super(...arguments); |
|||
this.product = this.props.product; |
|||
} |
|||
/** |
|||
* On confirming from the popup after adding lots/ serial numbers, |
|||
* the values are passed to the function validate_lots() for the |
|||
* validation. The corresponding error messages will be displayed |
|||
* on the popup if the lot is invalid or duplicated, or there is |
|||
* no insufficient stock. |
|||
*/ |
|||
async confirm() { |
|||
if (this.props.title == 'Lot/Serial Number(s) Required') { |
|||
var lot_string = this.state.array; |
|||
var lot_names = []; |
|||
for (var i = 0; i < lot_string.length; i++) { |
|||
if (lot_string[i].text != '') { |
|||
lot_names.push(lot_string[i].text); |
|||
} |
|||
} |
|||
const picking_type_id = this.env.pos.config && this.env.pos.config.picking_type_id && this.env.pos.config.picking_type_id[0] |
|||
const result = await rpc.query({ |
|||
model: 'stock.production.lot', |
|||
method: 'validate_lots', |
|||
args: [lot_names, this.props.product, picking_type_id] |
|||
}) |
|||
if (result != true) { |
|||
if(result[0] == 'no_stock') { |
|||
this.showPopup('ErrorPopup', { |
|||
'title': _lt('Out of stock'), |
|||
'body': _lt('The product is out of stock for ' + result[1] + '.') |
|||
}); |
|||
} else if(result[0] == 'duplicate') { |
|||
this.showPopup('ErrorPopup', { |
|||
'title': _lt('Duplicate Serial Number'), |
|||
'body': _lt('Duplicate entry for ' + result[1] + '.') |
|||
}); |
|||
} else if(result[0] == 'invalid') { |
|||
this.showPopup('ErrorPopup', { |
|||
'title': _lt('Invalid Lot/ Serial Number'), |
|||
'body': _lt('The Lot/ Serial Number ' + result[1] + ' is not available for this product.') |
|||
}); |
|||
} |
|||
} else { |
|||
this.props.resolve({ |
|||
confirmed: true, |
|||
payload: await this.getPayload() |
|||
}); |
|||
this.trigger('close-popup'); |
|||
} |
|||
} else { |
|||
this.props.resolve({ |
|||
confirmed: true, |
|||
payload: await this.getPayload() |
|||
}); |
|||
this.trigger('close-popup'); |
|||
} |
|||
} |
|||
}; |
|||
Registries.Component.extend(EditListPopup, PoSEditListPopup); |
|||
return EditListPopup; |
|||
}); |
@ -0,0 +1,37 @@ |
|||
odoo.define('pos_traceability_validation.PoSOrderWidget', function (require) { |
|||
'use strict'; |
|||
const OrderWidget = require('point_of_sale.OrderWidget'); |
|||
const Registries = require('point_of_sale.Registries'); |
|||
/** |
|||
* Extends OrderWidget for passing the product IDs to the EditListPopup |
|||
* validation |
|||
*/ |
|||
const PoSOrderWidget = (OrderWidget) => |
|||
class extends OrderWidget { |
|||
async _editPackLotLines(event) { |
|||
const orderline = event.detail.orderline; |
|||
const isAllowOnlyOneLot = orderline.product.isAllowOnlyOneLot(); |
|||
const packLotLinesToEdit = orderline.getPackLotLinesToEdit(isAllowOnlyOneLot); |
|||
const { confirmed, payload } = await this.showPopup('EditListPopup', { |
|||
title: this.env._t('Lot/Serial Number(s) Required'), |
|||
isSingleItem: isAllowOnlyOneLot, |
|||
array: packLotLinesToEdit, |
|||
product: orderline.product.id |
|||
}); |
|||
if (confirmed) { |
|||
// Segregate the old and new packlot lines
|
|||
const modifiedPackLotLines = Object.fromEntries( |
|||
payload.newArray.filter(item => item.id).map(item => [item.id, item.text]) |
|||
); |
|||
const newPackLotLines = payload.newArray |
|||
.filter(item => !item.id) |
|||
.map(item => ({ lot_name: item.text })); |
|||
|
|||
orderline.setPackLotLines({ modifiedPackLotLines, newPackLotLines }); |
|||
} |
|||
this.order.select_orderline(event.detail.orderline); |
|||
} |
|||
} |
|||
Registries.Component.extend(OrderWidget, PoSOrderWidget); |
|||
return OrderWidget; |
|||
}); |
@ -0,0 +1,90 @@ |
|||
odoo.define('pos_traceability_validation.PoSProductScreen', function (require) { |
|||
'use strict'; |
|||
const ProductScreen = require('point_of_sale.ProductScreen'); |
|||
const Registries = require('point_of_sale.Registries'); |
|||
/** |
|||
* Extends ProductScreen for passing the product ID to the EditListPopup |
|||
* validation |
|||
*/ |
|||
const PoSProductScreen = (ProductScreen) => |
|||
class extends ProductScreen { |
|||
async _getAddProductOptions(product, base_code) { |
|||
let price_extra = 0.0; |
|||
let draftPackLotLines, weight, description, packLotLinesToEdit; |
|||
if (this.env.pos.config.product_configurator && _.some(product.attribute_line_ids, (id) => id in this.env.pos.attributes_by_ptal_id)) { |
|||
let attributes = _.map(product.attribute_line_ids, (id) => this.env.pos.attributes_by_ptal_id[id]) |
|||
.filter((attr) => attr !== undefined); |
|||
let { confirmed, payload } = await this.showPopup('ProductConfiguratorPopup', { |
|||
product: product, |
|||
attributes: attributes, |
|||
}); |
|||
if (confirmed) { |
|||
description = payload.selected_attributes.join(', '); |
|||
price_extra += payload.price_extra; |
|||
} else { |
|||
return; |
|||
} |
|||
} |
|||
// Gather lot information if required.
|
|||
if (['serial', 'lot'].includes(product.tracking) && (this.env.pos.picking_type.use_create_lots || this.env.pos.picking_type.use_existing_lots)) { |
|||
const isAllowOnlyOneLot = product.isAllowOnlyOneLot(); |
|||
if (isAllowOnlyOneLot) { |
|||
packLotLinesToEdit = []; |
|||
} else { |
|||
const orderline = this.currentOrder |
|||
.get_orderlines() |
|||
.filter(line => !line.get_discount()) |
|||
.find(line => line.product.id === product.id); |
|||
if (orderline) { |
|||
packLotLinesToEdit = orderline.getPackLotLinesToEdit(); |
|||
} else { |
|||
packLotLinesToEdit = []; |
|||
} |
|||
} |
|||
const { confirmed, payload } = await this.showPopup('EditListPopup', { |
|||
title: this.env._t('Lot/Serial Number(s) Required'), |
|||
isSingleItem: isAllowOnlyOneLot, |
|||
array: packLotLinesToEdit, |
|||
product: product.id |
|||
}); |
|||
if (confirmed) { |
|||
// Segregate the old and new packlot lines
|
|||
const modifiedPackLotLines = Object.fromEntries( |
|||
payload.newArray.filter(item => item.id).map(item => [item.id, item.text]) |
|||
); |
|||
const newPackLotLines = payload.newArray |
|||
.filter(item => !item.id) |
|||
.map(item => ({ lot_name: item.text })); |
|||
|
|||
draftPackLotLines = { modifiedPackLotLines, newPackLotLines }; |
|||
} else { |
|||
// We don't proceed on adding product.
|
|||
return; |
|||
} |
|||
} |
|||
// Take the weight if necessary.
|
|||
if (product.to_weight && this.env.pos.config.iface_electronic_scale) { |
|||
// Show the ScaleScreen to weigh the product.
|
|||
if (this.isScaleAvailable) { |
|||
const { confirmed, payload } = await this.showTempScreen('ScaleScreen', { |
|||
product, |
|||
}); |
|||
if (confirmed) { |
|||
weight = payload.weight; |
|||
} else { |
|||
// do not add the product;
|
|||
return; |
|||
} |
|||
} else { |
|||
await this._onScaleNotAvailable(); |
|||
} |
|||
} |
|||
if (base_code && this.env.pos.db.product_packaging_by_barcode[base_code.code]) { |
|||
weight = this.env.pos.db.product_packaging_by_barcode[base_code.code].qty; |
|||
} |
|||
return { draftPackLotLines, quantity: weight, description, price_extra }; |
|||
} |
|||
} |
|||
Registries.Component.extend(ProductScreen, PoSProductScreen); |
|||
return ProductScreen; |
|||
}); |
@ -1,74 +0,0 @@ |
|||
odoo.define('pos_traceability_validation.pos_models', function (require) { |
|||
"use strict"; |
|||
const EditListPopup = require('point_of_sale.EditListPopup'); |
|||
const Registries = require('point_of_sale.Registries'); |
|||
var rpc = require('web.rpc'); |
|||
|
|||
const PosEditlistpopup = (EditListPopup) => |
|||
class extends EditListPopup { |
|||
async confirm() { |
|||
|
|||
if (this.props.title == 'Lot/Serial Number(s) Required'){ |
|||
|
|||
var lot_string = this.state.array |
|||
var lot_names = []; |
|||
for (var i = 0; i < lot_string.length; i++) { |
|||
|
|||
if (lot_string[i].text != ""){ |
|||
lot_names.push(lot_string[i].text); |
|||
} |
|||
|
|||
} |
|||
|
|||
const result = await rpc.query({ |
|||
model: 'serial_no.validation', |
|||
method: 'validate_lots', |
|||
args: [lot_names] |
|||
}) |
|||
|
|||
if(result != true){ |
|||
if(result[0] == 'no_stock'){ |
|||
this.showPopup('ErrorPopup', { |
|||
'title': this.env._t('Insufficient stock'), |
|||
'body': this.env._t("Insufficient stock for " + result[1]), |
|||
}); |
|||
|
|||
} |
|||
else if(result[0] == 'duplicate'){ |
|||
this.showPopup('ErrorPopup', { |
|||
'title': this.env._t('Duplicate entry'), |
|||
'body': this.env._t("Duplicate entry for " + result[1]), |
|||
}); |
|||
} |
|||
else if(result[0] == 'except'){ |
|||
alert("Exception occured with " + result[1]) |
|||
this.showPopup('ErrorPopup', { |
|||
'title': this.env._t('Exception'), |
|||
'body': this.env._t("Exception occured with" + result[1]), |
|||
}); |
|||
} |
|||
} |
|||
else{ |
|||
this.props.resolve({ confirmed: true, payload: await this.getPayload() }); |
|||
this.trigger('close-popup'); |
|||
|
|||
} |
|||
} |
|||
else{ |
|||
this.props.resolve({ confirmed: true, payload: await this.getPayload() }); |
|||
this.trigger('close-popup'); |
|||
} |
|||
|
|||
} |
|||
|
|||
}; |
|||
|
|||
Registries.Component.extend(EditListPopup, PosEditlistpopup); |
|||
|
|||
return EditListPopup; |
|||
|
|||
|
|||
}); |
|||
|
|||
|
|||
|