Selling products with lot required on sale point

We have a customer which produces and sells preserved fish. They have a factory near the sea and they have a small point of sale next to the factory where they sell their products.

As their product has an expiry date, all the products have the lot required and they have an expiration date once manufactured.

When selling products using the sale point module it is not possible to create a stock move because the move created by the sale point does not have a lot and this is required.

For our use case we planned to assign lots available in the sale point location using FEFO but I’m wondering if it will be a good idea to have some kind of lot assignation mechanism in the default sale_point module (or any additional module).

What do you think it will be the right design/solution to solve this problem?

1 Like

No it makes no sense to have assignation on POS because when the product is sold, it has already been picked (by the customer usually).
For me if you really need to manage lot (for which I have doubt), you must have a way to encode it on the POS (probably using barcode).

So you propose that we should add the lot field on the sale point line and let the user select it?
Because for now, there is no such field on the model.

Is the incoming supply and production of the fish also tracked by lot numbers of others or is lot tracking required by the authorities for this kind of business?

Food traceabily is enforced by the Regulation (EC) No 178/2002 of the European Parliament and of the Council of 28 January 2002 laying down the general principles and requirements of food law, establishing the European Food Safety Authority and laying down procedures in matters of food safety in their article 18. This includes the distribution:

Not in standard because for me POS does not have to track lots.

If we do not track lots how do you plan to manage product traceability which is required by the law?

It depends heavily on how the fish is processed and then sold. Is it to other business or private customers. Is the fish consumed directly (e.g. salted herring with onions) at the store / restaurant or the same day, or is it prepared for longer shelf-live and even frozen. If not, and the fish is fresh you generate automatically a new lot with the date of today and the expiration date. Those information is then printed on a label and put on the box. So you want to sell a product and create a lot.

When processed for long shelf-live I suppose that when the fish enters the store, it already has a lot number which is added during processing. In this case you basically want to sell a lot.

This is our case. It is processed for long shelf-live and it has a lot with a expiry date which is created with a production.

The stock of the sale point is filled using internal shipments from the main warehouse (which is filled by production).

But now we do not have any option to set the lot on the sale point, so the program just complaints about a missing lot and you can not sell the product.

To be honest, I don’t know much about the lots but if every ‘box’ gets a unique lot number, maybe you can use that lot number as barcode instead of using the product code. Based on that, you can search your lots and find the right product. You then have all the required information to create a move.

It’s not only needed to follow laws. Also when selling e.g. computer parts, you always collect the serial numbers of the parts you sell. This is done e.g. as a security measure against return fraud or to fulfill supplier requirements.

But I really find all the separation between product and lot/serial numbers in Tryton POS unnecessarily complicated for enhancements.

IMHO the POS should handle scanning a product code or a lot/serial number or at least any bar-code in the same way:

  1. Scan a bar code
  2. Test if bar-code is a valid product code:
    2.1 If valid, lookup if lot required: Open form to enter/scan lot
  3. Test if bar-code is some other code:
    3.1 If code is known lot/serial number, use the connected product.
    3.2 e.g. if code is known recipe number, open form and prepare a customer return process…
    3.3 e.g. if code is a known coupon, subtract it from the actual bill
  4. If code is an unknown number: Open form to choose product to connect number as lot/serial number or identifier.

All the lot/serial number handling should be optional, pluggable by extension modules and not required by default, because we should not support getting stuck in any sale in the default.

Requirements for the several parts are leave to the customization. Also the restriction of the bar codes to be available in the system is IMHO a special case. I know small or specific businesses, which are just re-using all the codes from their different suppliers…

You do not because you do not need.

Do not require it.

Ok, as there is no interest on having this feature as standard, I’m sharing my code which implements the FEFO assignation.

class SalePointSaleLine(metaclass=PoolMeta):
    __name__ = 'sale.point.sale.line'

    def get_stock_move(self):
        pool = Pool()
        try:
            Lot = pool.get('stock.lot')
        except KeyError:
            Lot = None
        move = super().get_stock_move()
        if Lot is None:
            return move

        def create_lot(move):
            lot = Lot()
            lot.number = 'DESCONOCIDO'
            lot.product = move.product
            lot.save()
            return lot

        if (move.product
                and move.product.lot_is_required(
                    move.from_location, move.to_location)):
            with Transaction().set_context(self._search_lot_context(move)):
                lots = Lot.search(
                    self._search_lot_domain(move),
                    order=self._search_lot_order(move))
                move.lot = None
                for lot in lots:
                    if lot.quantity > move.quantity:
                        move.lot = lot
                        break
                    else:
                        new_move = copy(move)
                        new_move.quantity = lot.quantity
                        new_move.lot = lot
                        new_move.id = None
                        new_move.save()
                        move.quantity = move.uom.round(
                            move.quantity - lot.quantity)
                if not move.lot:
                    move.lot = create_lot(move)
        return move

    def _search_lot_context(self, move):
        return {
            'locations': [move.from_location.id],
            'stock_date_end': move.effective_date,
            }

    def _search_lot_domain(self, move):
        return [
            ('quantity', '>', 0),
            ('product', '=', move.product.id),
            ]

    def _search_lot_order(self, move):
        order = []
        # Add first expiry dates if available
        if getattr(move.product, 'shelf_life_state', 'none') != 'none':
            order.insert(('shelf_life_expiration_date', 'ASC NULLS LAST'))
        if getattr(move.product, 'expiration_state', 'none') != 'none':
            order.insert(('expiration_date', 'ASC NULLS LAST'))
        order.append(('number', 'ASC'))
        order.append(('id', 'ASC'))
        return order
1 Like

One point is missing for companies that have different business needs of tracking lots in some warehouses and not in other like this case: B2B warehouse lot could be required but not in POS storage location.
I guess it could be solved with an option to exclude a warehouse from the lot requirement for storage and customer. So such warehouse would be used for POS.

This would prevent to have to fill with fake lot number the POS lines.

Another point but it will be doable only when an implementation of Allow to scan product barcodes on sale point is available, is to store on the POS line the lot if it is available on the barcode. This would be only a best effort.

In our case using fake lots (or not the exact ones) it is not a real issue as there will be only just (as much) two or three lots available on the same time in the pos storage. In case there is any problem with some of the lots all the afected lots will be retired. For this reason we are using the FEFO assignation.