# -*- coding: utf-8 -*- from odoo import models, fields, api class SkeenInventarioItem(models.Model): _name = 'skeen.inventario.item' _description = 'Item de Inventario / Consumible SKEEN' _order = 'name asc' name = fields.Char(string='Nombre', required=True, index=True) kind = fields.Selection([ ('producto', 'Producto'), ('consumible', 'Consumible'), ], string='Tipo', required=True, default='producto', index=True) sku = fields.Char(string='SKU / Código', index=True) category = fields.Char(string='Línea / Categoría') unit = fields.Char(string='Unidad', default='pieza') qty = fields.Float(string='Existencia', default=0.0) qty_optimal = fields.Float(string='Nivel óptimo', default=0.0) qty_min = fields.Float(string='Stock mínimo', default=0.0) cost = fields.Float(string='Costo unitario', default=0.0) inventory_value = fields.Float(string='Valor inventario', compute='_compute_value', store=True) expiry_date = fields.Date(string='Caducidad') last_count_date = fields.Date(string='Último conteo físico') active = fields.Boolean(string='Activo', default=True) notes = fields.Text(string='Notas') move_ids = fields.One2many('skeen.inventario.move', 'item_id', string='Movimientos') stock_level = fields.Selection([ ('out', 'Sin existencias'), ('critical', 'Crítico'), ('low', 'Bajo'), ('optimal', 'Óptimo'), ], string='Nivel', compute='_compute_level', store=True) @api.depends('qty', 'cost') def _compute_value(self): for rec in self: rec.inventory_value = (rec.qty or 0.0) * (rec.cost or 0.0) @api.depends('qty', 'qty_min', 'qty_optimal') def _compute_level(self): for rec in self: q = rec.qty or 0.0 if q <= 0: rec.stock_level = 'out' elif rec.qty_min and q <= rec.qty_min: rec.stock_level = 'critical' elif rec.qty_optimal and q < rec.qty_optimal: rec.stock_level = 'low' else: rec.stock_level = 'optimal' @api.model def _cron_descontar_caducados(self): """Descuenta (baja a cero) los artículos caducados con existencias. Corre diario vía ir.cron; la baja queda como movimiento tipo 'baja'.""" hoy = fields.Date.today() caducados = self.search([ ('expiry_date', '!=', False), ('expiry_date', '<', hoy), ('qty', '>', 0), ('active', '=', True), ]) Move = self.env['skeen.inventario.move'].sudo() for item in caducados: Move.create({ 'item_id': item.id, 'type': 'baja', 'qty': item.qty, 'reference': 'CADUCIDAD', 'notes': f'Descuento automático por caducidad {item.expiry_date}', }) if caducados: import logging logging.getLogger(__name__).info( 'Inventario: %s artículos caducados descontados', len(caducados)) return len(caducados) class SkeenInventarioMove(models.Model): _name = 'skeen.inventario.move' _description = 'Movimiento de Inventario SKEEN' _order = 'date desc, id desc' item_id = fields.Many2one('skeen.inventario.item', string='Item', required=True, ondelete='cascade', index=True) type = fields.Selection([ ('compra', 'Compra (entrada)'), ('venta', 'Venta (salida)'), ('baja', 'Baja (merma)'), ('ajuste', 'Ajuste (conteo físico)'), ], string='Tipo', required=True) qty = fields.Float(string='Cantidad', required=True, default=0.0) before_qty = fields.Float(string='Existencia anterior', readonly=True) after_qty = fields.Float(string='Existencia resultante', readonly=True) date = fields.Datetime(string='Fecha', default=fields.Datetime.now) reference = fields.Char(string='Referencia') notes = fields.Char(string='Notas') @api.model_create_multi def create(self, vals_list): moves = super().create(vals_list) for m in moves: item = m.item_id if not item: continue before = item.qty or 0.0 q = m.qty or 0.0 if m.type == 'compra': after = before + q elif m.type in ('venta', 'baja'): after = before - q elif m.type == 'ajuste': after = q # ajuste fija la existencia al valor del conteo else: after = before after = max(0.0, after) item.write({'qty': after, 'last_count_date': fields.Date.today() if m.type == 'ajuste' else item.last_count_date}) m.write({'before_qty': before, 'after_qty': after}) return moves