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 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') help='If enables this lot number is taken')
@api.model @api.model
def get_available_lots_for_pos(self, product_id): def check_product_stock_in_location(self, product_id, location_id):
"""Get available lots for a product suitable for the Point of Sale """Check if product has positive stock in the given location"""
(PoS).This method retrieves the available lots for a specific product product = self.env['product.product'].browse(product_id)
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 # Get on hand quantity using Odoo's built-in method
creation date,depending on the removal strategy.""" 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 company_id = self.env.company.id
removal_strategy_id = (self.env['product.template'].browse( removal_strategy = product.product_tmpl_id.categ_id.removal_strategy_id.method or 'fifo'
self.env['product.product'].browse(product_id).product_tmpl_id.id)
.categ_id.removal_strategy_id.method) # Get all lots for this product
if removal_strategy_id == 'fefo': lot_domain = [
lots = self.sudo().search( ('product_id', '=', product_id),
["&", ["product_id", "=", product_id],"&",["is_taken","=",False], '|', ('company_id', '=', company_id), ('company_id', '=', False)
"|", ["company_id", "=", company_id], ]
["company_id", "=", False]],
order='expiration_date asc') if removal_strategy == 'fefo':
lots = self.sudo().search(lot_domain, order='expiration_date asc')
else: else:
lots = self.sudo().search( lots = self.sudo().search(lot_domain, order='create_date asc')
["&", ["product_id", "=", product_id], "|",
["company_id", "=", company_id], # Get lots with positive quantity in the location
["company_id", "=", False], ], available_lots = []
order='create_date asc') for lot in lots:
lots = lots.filtered(lambda l: float_compare( quants = self.env['stock.quant'].sudo().search([
l.product_qty, 0, ('lot_id', '=', lot.id),
precision_digits=l.product_uom_id.rounding) > 0)[:1] ('location_id', '=', location_id),
lots.is_taken = True ('product_id', '=', product_id)
return lots.mapped("name") ])
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 **/ /** @odoo-module **/
import { patch } from "@web/core/utils/patch"; 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 { 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"; import { ComboConfiguratorPopup } from "@point_of_sale/app/store/combo_configurator_popup/combo_configurator_popup";
patch(Product.prototype, { patch(Product.prototype, {
@ -12,9 +11,11 @@ patch(Product.prototype, {
let quantity = 1; let quantity = 1;
let comboLines = []; let comboLines = [];
let attribute_custom_values = {}; let attribute_custom_values = {};
if (code && this.pos.db.product_packaging_by_barcode[code.code]) { if (code && this.pos.db.product_packaging_by_barcode[code.code]) {
quantity = this.pos.db.product_packaging_by_barcode[code.code].qty; quantity = this.pos.db.product_packaging_by_barcode[code.code].qty;
} }
if (this.isConfigurable()) { if (this.isConfigurable()) {
const { confirmed, payload } = await this.openConfigurator({ initQuantity: quantity }); const { confirmed, payload } = await this.openConfigurator({ initQuantity: quantity });
if (confirmed) { if (confirmed) {
@ -26,12 +27,14 @@ patch(Product.prototype, {
return; return;
} }
} }
if (this.combo_ids.length) { if (this.combo_ids.length) {
const { confirmed, payload } = await this.env.services.popup.add( const { confirmed, payload } = await this.env.services.popup.add(
ComboConfiguratorPopup, ComboConfiguratorPopup,
{ product: this, keepBehind: true } { product: this, keepBehind: true }
); );
if (!confirmed) { if (!confirmed) {
return;
} }
comboLines = payload; comboLines = payload;
} }
@ -46,35 +49,127 @@ patch(Product.prototype, {
.find((line) => line.product.id === this.id) .find((line) => line.product.id === this.id)
?.getPackLotLinesToEdit()) || ?.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") { if (code && code.type === "lot") {
// consider the old and new packlot lines
const modifiedPackLotLines = Object.fromEntries( const modifiedPackLotLines = Object.fromEntries(
packLotLinesToEdit.filter((item) => item.id).map((item) => [item.id, item.text]) packLotLinesToEdit.filter((item) => item.id).map((item) => [item.id, item.text])
); );
const newPackLotLines = [{ lot_name: code.code }]; const newPackLotLines = [{ lot_name: code.code }];
draftPackLotLines = { modifiedPackLotLines, newPackLotLines }; draftPackLotLines = { modifiedPackLotLines, newPackLotLines };
} else { } else {
let result = await this.env.services.orm.call( // Check location stock and get lots
"stock.lot", "get_available_lots_for_pos",[], {product_id: this.id}); try {
const modifiedPackLotLines = result[0]; const result = await this.env.services.orm.call(
const newPackLotLines = result.map(item => ({ lot_name: result[0] })); "stock.lot",
draftPackLotLines = { modifiedPackLotLines, newPackLotLines }; "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) { if (!draftPackLotLines) {
return; return;
} }
} }
// Take the weight if necessary. // Take the weight if necessary.
if (this.to_weight && this.pos.config.iface_electronic_scale) { if (this.to_weight && this.pos.config.iface_electronic_scale) {
// Show the ScaleScreen to weigh the product.
if (this.isScaleAvailable) { if (this.isScaleAvailable) {
const product = this;
const { confirmed, payload } = await this.env.services.pos.showTempScreen( const { confirmed, payload } = await this.env.services.pos.showTempScreen(
"ScaleScreen", "ScaleScreen",
{ { product: this }
product,
}
); );
if (confirmed) { if (confirmed) {
quantity = payload.weight; quantity = payload.weight;
@ -85,6 +180,7 @@ patch(Product.prototype, {
await this._onScaleNotAvailable(); await this._onScaleNotAvailable();
} }
} }
return { return {
draftPackLotLines, draftPackLotLines,
quantity, quantity,

Loading…
Cancel
Save