Files
SKEEN-Proyecto/odoo-addons/skeen_inventario/models/inventario.py
Consultoría Alcaraz Salazar 01f6007e30 Mejoras integrales: visitas, POS, WACRM, reportes, temas y módulos legacy
- Módulo Visitas completo (auto desde agenda, insumos con descargo de
  inventario, fotos antes/después y documentos, receta imprimible)
- Punto de Venta (catálogo + ticket sticky, cobro con cambio, pago con
  puntos monedero, ticket imprimible)
- WACRM: leads automáticos desde WhatsApp, asignación de conversaciones
  y leads a agentes, conversión lead→paciente, ficha del paciente en chat
- Pacientes: completitud de expediente, alertas clínicas, historial
  unificado con detalle, foto, WhatsApp, estado de cuenta, filtros
  rápidos (VIP/recientes/médico), documentos (expediente escaneado + galería)
- Agenda: vistas por médico y por hora, filtros rápidos (libres, primera
  vez, check-in, no-show), modal de acciones, bloqueos por médico,
  drag&drop para mover citas
- Reportes: 18 pestañas (diario, cortes, ingresos, inventario, adeudos,
  comisiones, pagos, devoluciones, top clientes, horas, paquetes,
  vendedores, concentrado, recomendaciones, KPIs) con exportación Excel
- Temas: nuevo tema Clásico (look legacy AdminLTE) con submenús tipo
  treeview, selector de tema; accesos rápidos personalizables con 3
  presentaciones; búsqueda global; notificaciones reales
- Configuración: secciones (clínica, usuarios con permisos por sección,
  recetas, catálogos de diagnósticos y procedimientos)
- Inventario: alertas de caducidad y sugerencia de compra, cron diario
  que descuenta artículos caducados, compras/bajas
- Consultas Médicas, página Expedientes, importadores delta
  (citas/visitas legacy idempotentes), depuración de duplicados
- Infra: tema Tailwind conectado (@config), gzip en nginx, secuencias
  Odoo corregidas (noupdate, company_id), rollback en validaciones
2026-08-13 23:30:08 +00:00

125 lines
4.7 KiB
Python

# -*- 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