| @ -1,42 +0,0 @@ | |||||
| # -*- coding: utf-8 -*- |  | ||||
| ############################################################################## |  | ||||
| # |  | ||||
| #    Cybrosys Technologies Pvt. Ltd. |  | ||||
| # |  | ||||
| #    Copyright (C) 2023-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). |  | ||||
| #    Author: Jumana Jabin MP(<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 api, models |  | ||||
| 
 |  | ||||
| 
 |  | ||||
| class SerialNoValidation(models.Model): |  | ||||
|     """Serial Number Validation Model.This model is used for serial number |  | ||||
|        validation in Odoo.""" |  | ||||
|     _name = 'serial_no.validation' |  | ||||
| 
 |  | ||||
|     @api.model |  | ||||
|     def validate_lots(self, lots): |  | ||||
|         """ This method validates a list of lots.""" |  | ||||
|         processed = [] |  | ||||
|         LotObj = self.env['stock.lot'] |  | ||||
|         for lot in lots: |  | ||||
|             lot_id = LotObj.search([('name', '=', lot)], limit=1) |  | ||||
|             if lot_id.product_qty > 0 and lot not in processed: |  | ||||
|                 processed.append(lot) |  | ||||
|                 continue |  | ||||
|             if lot in processed: |  | ||||
|                 return ['duplicate', lot] |  | ||||
|             return ['no_stock', lot] |  | ||||
|         return True |  | ||||
| @ -0,0 +1,72 @@ | |||||
|  | # -*- coding: utf-8 -*- | ||||
|  | ############################################################################## | ||||
|  | # | ||||
|  | #    Cybrosys Technologies Pvt. Ltd. | ||||
|  | # | ||||
|  | #    Copyright (C) 2023-TODAY Cybrosys Technologies(<http://www.cybrosys.com>). | ||||
|  | #    Author: Jumana Jabin MP(<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 api, models | ||||
|  | 
 | ||||
|  | 
 | ||||
|  | class StockLot(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.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 | ||||
| Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 134 KiB | 
| After Width: | Height: | Size: 290 KiB | 
| After Width: | Height: | Size: 352 KiB | 
| Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 129 KiB | 
| Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 143 KiB | 
| Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 137 KiB | 
| Before Width: | Height: | Size: 231 KiB After Width: | Height: | Size: 68 KiB | 
| Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 249 KiB | 
| Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 290 KiB | 
| Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 281 KiB | 
| After Width: | Height: | Size: 257 KiB | 
| Before Width: | Height: | Size: 383 KiB After Width: | Height: | Size: 475 KiB | 
| @ -0,0 +1,77 @@ | |||||
|  | odoo.define('pos_traceability_validation.PoSEditListPopup', function (require) { | ||||
|  | "use strict"; | ||||
|  |     const EditListPopup = require('point_of_sale.EditListPopup'); | ||||
|  |     const Registries = require('point_of_sale.Registries'); | ||||
|  |     var rpc = require('web.rpc'); | ||||
|  |      /** | ||||
|  |      * EditListPopup Override | ||||
|  |      * | ||||
|  |      * This module overrides the EditListPopup component in the Point of Sale (POS) module | ||||
|  |      * to add custom behavior for 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.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': this.env._t('Out of stock'), | ||||
|  |                                 'body': this.env._t("The product is out of 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] == 'invalid'){ | ||||
|  |                             this.showPopup('ErrorPopup', { | ||||
|  |                                 'title': this.env._t('Invalid Lot/ Serial Number'), | ||||
|  |                                 'body': this.env._t("The Lot/ Serial Number " + result[1]+ ' is not available for this product.'), | ||||
|  |                             }); | ||||
|  |                         } | ||||
|  |                     } | ||||
|  |                     else{ | ||||
|  |                         this.env.posbus.trigger('close-popup', { | ||||
|  |                             popupId: this.props.id, | ||||
|  |                             response: { confirmed: true, payload: await this.getPayload() }, | ||||
|  |                         }); | ||||
|  |                     } | ||||
|  |                 } | ||||
|  |                 else{ | ||||
|  |                     this.env.posbus.trigger('close-popup', { | ||||
|  |                         popupId: this.props.id, | ||||
|  |                         response: { confirmed: true, payload: await this.getPayload() }, | ||||
|  |                     }); | ||||
|  |                 } | ||||
|  |             } | ||||
|  |         }; | ||||
|  |     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,69 +0,0 @@ | |||||
| odoo.define('pos_traceability_validation.pos_models', function (require) { |  | ||||
| "use strict"; |  | ||||
|  /** |  | ||||
|      * EditListPopup Override |  | ||||
|      * |  | ||||
|      * This module overrides the EditListPopup component in the Point of Sale (POS) module |  | ||||
|      * to add custom behavior for serial number validation. |  | ||||
|      */ |  | ||||
|     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 { |  | ||||
|         /** |  | ||||
|              * Confirm Override |  | ||||
|              * |  | ||||
|              * Overrides the base confirm method to handle serial number validation. |  | ||||
|              * If the title of the popup is 'Lot/Serial Number(s) Required', it validates |  | ||||
|              * the entered lot numbers using the 'serial_no.validation' model. |  | ||||
|              */ |  | ||||
|              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 occurred with " + result[1]) |  | ||||
|                                     this.showPopup('ErrorPopup', { |  | ||||
|                                         'title': this.env._t('Exception'), |  | ||||
|                                         'body': this.env._t("Exception occurred 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; |  | ||||
| }); |  | ||||