Browse Source

Oct 10: [FIX] Bug fixed 'pos_auto_lot_selection'

17.0
Risvana Cybro 4 days ago
parent
commit
e25d6f969f
  1. 1
      pos_auto_lot_selection/models/__init__.py
  2. 66
      pos_auto_lot_selection/models/pos_order.py
  3. 109
      pos_auto_lot_selection/models/stock_lot.py
  4. 124
      pos_auto_lot_selection/static/src/js/product.js

1
pos_auto_lot_selection/models/__init__.py

@ -20,3 +20,4 @@
#
###############################################################################
from . import stock_lot
from . import pos_order

66
pos_auto_lot_selection/models/pos_order.py

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from odoo import api, models
class PosOrder(models.Model):
_inherit = 'pos.order'
@api.model
def _process_order(self, order, draft, existing_order):
"""
Ensure lots are properly updated after POS order validation.
"""
# Call the original method
pos_order_id = super(PosOrder, self)._process_order(order, draft, existing_order)
# Make sure we have the record, not just an ID
pos_order = pos_order_id
if isinstance(pos_order_id, int):
pos_order = self.browse(pos_order_id)
# Now safely loop through order lines
for order_line in pos_order.lines:
if order_line.pack_lot_ids:
self._update_lot_stock(order_line)
return pos_order_id
def _update_lot_stock(self, order_line):
"""Deduct quantities from the correct lots when POS order is validated"""
product = order_line.product_id
pos_config = order_line.order_id.session_id.config_id
location_id = pos_config.picking_type_id.default_location_src_id.id
for pack_lot in order_line.pack_lot_ids:
lot = self.env['stock.lot'].search([
('name', '=', pack_lot.lot_name),
('product_id', '=', product.id)
], limit=1)
if not lot:
continue
# Get all quants for this lot at POS location
quants = self.env['stock.quant'].sudo().search([
('product_id', '=', product.id),
('lot_id', '=', lot.id),
('location_id', '=', location_id)
])
if not quants:
continue
qty_to_deduct = abs(order_line.qty) # ensure positive qty
for quant in quants:
if qty_to_deduct <= 0:
break
if quant.quantity >= qty_to_deduct:
quant.quantity -= qty_to_deduct
qty_to_deduct = 0
else:
qty_to_deduct -= quant.quantity
quant.quantity = 0
# Optional: mark as taken if fully used
remaining_qty = sum(quants.mapped('quantity'))
lot.is_taken = remaining_qty <= 0

109
pos_auto_lot_selection/models/stock_lot.py

@ -30,30 +30,89 @@ class StockLot(models.Model):
help='If enables this lot number is taken')
@api.model
def get_available_lots_for_pos(self, product_id):
"""Get available lots for a product suitable for the Point of Sale
(PoS).This method retrieves the available lots for a specific product
that are suitable for the Point of Sale (PoS) based on the configured
removal strategy. The lots are sorted based on the expiration date or
creation date,depending on the removal strategy."""
def check_product_stock_in_location(self, product_id, location_id):
"""Check if product has positive stock in the given location"""
product = self.env['product.product'].browse(product_id)
# Get on hand quantity using Odoo's built-in method
stock_quant = self.env['stock.quant'].search([
('product_id', '=', product_id),
('location_id', '=', location_id)
])
total_qty = sum(stock_quant.mapped('quantity'))
return {
'has_positive_stock': total_qty > 0,
'total_quantity': total_qty
}
@api.model
def get_available_lots_for_pos(self, product_id, pos_config_id=None):
"""Get available lots for a product in POS location"""
# Get POS location
if pos_config_id:
pos_config = self.env['pos.config'].browse(pos_config_id)
location_id = pos_config.picking_type_id.default_location_src_id.id
else:
pos_session = self.env['pos.session'].search([
('state', '=', 'opened'),
('user_id', '=', self.env.uid)
], limit=1)
if pos_session:
location_id = pos_session.config_id.picking_type_id.default_location_src_id.id
else:
return {'has_positive_stock': False, 'lots': []}
# Check total stock in location
stock_check = self.check_product_stock_in_location(product_id, location_id)
# If no positive stock, return immediately
if not stock_check['has_positive_stock']:
return {
'has_positive_stock': False,
'total_quantity': stock_check['total_quantity'],
'lots': []
}
# Stock is positive, get lots with FEFO/FIFO
product = self.env['product.product'].browse(product_id)
company_id = self.env.company.id
removal_strategy_id = (self.env['product.template'].browse(
self.env['product.product'].browse(product_id).product_tmpl_id.id)
.categ_id.removal_strategy_id.method)
if removal_strategy_id == 'fefo':
lots = self.sudo().search(
["&", ["product_id", "=", product_id],"&",["is_taken","=",False],
"|", ["company_id", "=", company_id],
["company_id", "=", False]],
order='expiration_date asc')
removal_strategy = product.product_tmpl_id.categ_id.removal_strategy_id.method or 'fifo'
# Get all lots for this product
lot_domain = [
('product_id', '=', product_id),
'|', ('company_id', '=', company_id), ('company_id', '=', False)
]
if removal_strategy == 'fefo':
lots = self.sudo().search(lot_domain, order='expiration_date asc')
else:
lots = self.sudo().search(
["&", ["product_id", "=", product_id], "|",
["company_id", "=", company_id],
["company_id", "=", False], ],
order='create_date asc')
lots = lots.filtered(lambda l: float_compare(
l.product_qty, 0,
precision_digits=l.product_uom_id.rounding) > 0)[:1]
lots.is_taken = True
return lots.mapped("name")
lots = self.sudo().search(lot_domain, order='create_date asc')
# Get lots with positive quantity in the location
available_lots = []
for lot in lots:
quants = self.env['stock.quant'].sudo().search([
('lot_id', '=', lot.id),
('location_id', '=', location_id),
('product_id', '=', product_id)
])
lot_qty = sum(quants.mapped('quantity'))
if lot_qty > 0:
available_lots.append({
'lot_name': lot.name,
'lot_id': lot.id,
'available_qty': lot_qty,
'expiration_date': str(lot.expiration_date) if lot.expiration_date else False,
})
return {
'has_positive_stock': True,
'total_quantity': stock_check['total_quantity'],
'lots': available_lots
}

124
pos_auto_lot_selection/static/src/js/product.js

@ -1,8 +1,7 @@
/** @odoo-module **/
import { patch } from "@web/core/utils/patch";
import { ProductScreen } from "@point_of_sale/app/screens/product_screen/product_screen";
import { Product } from "@point_of_sale/app/store/models";
import { jsonrpc } from "@web/core/network/rpc_service";
import { _t } from "@web/core/l10n/translation";
import { ComboConfiguratorPopup } from "@point_of_sale/app/store/combo_configurator_popup/combo_configurator_popup";
patch(Product.prototype, {
@ -12,9 +11,11 @@ patch(Product.prototype, {
let quantity = 1;
let comboLines = [];
let attribute_custom_values = {};
if (code && this.pos.db.product_packaging_by_barcode[code.code]) {
quantity = this.pos.db.product_packaging_by_barcode[code.code].qty;
}
if (this.isConfigurable()) {
const { confirmed, payload } = await this.openConfigurator({ initQuantity: quantity });
if (confirmed) {
@ -26,12 +27,14 @@ patch(Product.prototype, {
return;
}
}
if (this.combo_ids.length) {
const { confirmed, payload } = await this.env.services.popup.add(
ComboConfiguratorPopup,
{ product: this, keepBehind: true }
);
if (!confirmed) {
return;
}
comboLines = payload;
}
@ -46,35 +49,127 @@ patch(Product.prototype, {
.find((line) => line.product.id === this.id)
?.getPackLotLinesToEdit()) ||
[];
// if the lot information exists in the barcode, we don't need to ask it from the user.
// If lot from barcode
if (code && code.type === "lot") {
// consider the old and new packlot lines
const modifiedPackLotLines = Object.fromEntries(
packLotLinesToEdit.filter((item) => item.id).map((item) => [item.id, item.text])
);
const newPackLotLines = [{ lot_name: code.code }];
draftPackLotLines = { modifiedPackLotLines, newPackLotLines };
} else {
let result = await this.env.services.orm.call(
"stock.lot", "get_available_lots_for_pos",[], {product_id: this.id});
const modifiedPackLotLines = result[0];
const newPackLotLines = result.map(item => ({ lot_name: result[0] }));
draftPackLotLines = { modifiedPackLotLines, newPackLotLines };
// Check location stock and get lots
try {
const result = await this.env.services.orm.call(
"stock.lot",
"get_available_lots_for_pos",
[],
{
product_id: this.id,
pos_config_id: this.pos.config.id
}
);
console.log("=========================================");
console.log("Product:", this.display_name);
console.log("Has Positive Stock:", result.has_positive_stock);
console.log("Total Qty in Location:", result.total_quantity);
console.log("Available Lots:", result.lots);
console.log("=========================================");
// DECISION: Auto-assign if positive stock, manual if not
if (result.has_positive_stock === true) {
console.log("→ POSITIVE STOCK - AUTO ASSIGNING LOT");
if (result.lots && result.lots.length > 0) {
// Calculate already used quantities
const usedLotQty = {};
const orderLines = this.pos.selectedOrder.get_orderlines();
for (const line of orderLines) {
if (line.product.id === this.id && line.pack_lot_lines) {
for (const lotLine of line.pack_lot_lines) {
if (lotLine.lot_name) {
usedLotQty[lotLine.lot_name] =
(usedLotQty[lotLine.lot_name] || 0) + line.quantity;
}
}
}
}
console.log("Used quantities:", usedLotQty);
// Find first lot with remaining quantity
let selectedLot = null;
for (const lot of result.lots) {
const used = usedLotQty[lot.lot_name] || 0;
const remaining = lot.available_qty - used;
console.log(`Lot ${lot.lot_name}: available=${lot.available_qty}, used=${used}, remaining=${remaining}`);
if (remaining > 0) {
selectedLot = lot;
break;
}
}
if (selectedLot) {
console.log("✓ Selected lot:", selectedLot.lot_name);
draftPackLotLines = {
modifiedPackLotLines: {},
newPackLotLines: [{ lot_name: selectedLot.lot_name }]
};
} else {
console.log("✗ All lots exhausted in order");
await this.env.services.popup.add('ErrorPopup', {
title: _t('No Available Stock'),
body: _t('All available lots have been added to the order.'),
});
return;
}
} else {
console.log("✗ No lots found but stock is positive");
// Continue without assigning lot - will be handled by standard flow
draftPackLotLines = {
modifiedPackLotLines: Object.fromEntries(
packLotLinesToEdit.filter((item) => item.id).map((item) => [item.id, item.text])
),
newPackLotLines: []
};
}
} else {
console.log("→ NO POSITIVE STOCK - ADDING PRODUCT WITHOUT LOT");
console.log("User can add lot manually from orderline");
// Add product without lot - user can edit from orderline
draftPackLotLines = {
modifiedPackLotLines: Object.fromEntries(
packLotLinesToEdit.filter((item) => item.id).map((item) => [item.id, item.text])
),
newPackLotLines: []
};
}
} catch (error) {
console.error("ERROR:", error);
// Fallback - add without lot
draftPackLotLines = {
modifiedPackLotLines: Object.fromEntries(
packLotLinesToEdit.filter((item) => item.id).map((item) => [item.id, item.text])
),
newPackLotLines: []
};
}
}
if (!draftPackLotLines) {
return;
}
}
// Take the weight if necessary.
if (this.to_weight && this.pos.config.iface_electronic_scale) {
// Show the ScaleScreen to weigh the product.
if (this.isScaleAvailable) {
const product = this;
const { confirmed, payload } = await this.env.services.pos.showTempScreen(
"ScaleScreen",
{
product,
}
{ product: this }
);
if (confirmed) {
quantity = payload.weight;
@ -85,6 +180,7 @@ patch(Product.prototype, {
await this._onScaleNotAvailable();
}
}
return {
draftPackLotLines,
quantity,

Loading…
Cancel
Save