Oleada UX 4: Pagos/Adeudos (cobro en 1 click con recibo, plan de pagos), Monedero (movimientos narrados, ajustes auditados, MXN+USD), Inventario (alta rapida, conteo fisico con ajuste e historial, alertas accionables) + fix de folios de pago y cantidades de movimientos

This commit is contained in:
2026-09-17 20:31:52 +00:00
parent ac5896b1fc
commit 9ee3343e22
20 changed files with 2173 additions and 165 deletions

View File

@@ -10,6 +10,7 @@
'data': [
'security/ir.model.access.csv',
'views/inventario_views.xml',
'data/sequences.xml',
'data/cron.xml',
],
'installable': True,

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="seq_skeen_inventario_conteo" model="ir.sequence">
<field name="name">SKEEN Conteo físico de inventario</field>
<field name="code">skeen.inventario.conteo</field>
<field name="prefix">CONTEO-</field>
<field name="padding">4</field>
<field name="number_next">1</field>
<field name="number_increment">1</field>
</record>
</data>
</odoo>

View File

@@ -107,22 +107,119 @@ class SkeenInventarioMove(models.Model):
@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:
# Las existencias anterior/resultante se calculan ANTES del INSERT: si
# se escribieran después con write(), Odoo omite el UPDATE cuando el
# valor coincide con el de la caché (p. ej. after_qty = 0) y la columna
# quedaría NULL en la base.
Item = self.env['skeen.inventario.item'].sudo()
for vals in vals_list:
item = Item.browse(vals.get('item_id'))
if not item.exists():
continue
before = item.qty or 0.0
q = m.qty or 0.0
if m.type in ('compra', 'cancelacion'):
q = vals.get('qty') or 0.0
mtype = vals.get('type')
if mtype in ('compra', 'cancelacion'):
after = before + q
elif m.type in ('venta', 'baja'):
elif mtype in ('venta', 'baja'):
after = before - q
elif m.type == 'ajuste':
elif mtype == '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})
vals['before_qty'] = before
vals['after_qty'] = max(0.0, after)
moves = super().create(vals_list)
for m in moves:
if not m.item_id:
continue
vals_item = {'qty': m.after_qty}
if m.type == 'ajuste':
vals_item['last_count_date'] = fields.Date.today()
m.item_id.write(vals_item)
return moves
class SkeenInventarioConteo(models.Model):
"""Conteo físico cíclico de inventario.
Guarda la foto del conteo (sistema vs contado por item) y los ajustes
generados, para poder consultar el historial y reimprimir el resumen.
"""
_name = 'skeen.inventario.conteo'
_description = 'Conteo físico de inventario SKEEN'
_order = 'date desc, id desc'
name = fields.Char(string='Referencia', readonly=True, copy=False)
date = fields.Datetime(string='Fecha', default=fields.Datetime.now)
user_name = fields.Char(string='Realizado por')
notes = fields.Text(string='Notas')
line_ids = fields.One2many('skeen.inventario.conteo.line', 'conteo_id', string='Líneas')
total_items = fields.Integer(string='Artículos contados', compute='_compute_totals', store=True)
total_diferencias = fields.Integer(string='Con diferencia', compute='_compute_totals', store=True)
total_faltante = fields.Float(string='Faltante', compute='_compute_totals', store=True)
total_sobrante = fields.Float(string='Sobrante', compute='_compute_totals', store=True)
total_value_diff = fields.Float(string='Diferencia valorizada', compute='_compute_totals', store=True)
@api.depends('line_ids.difference', 'line_ids.value_diff')
def _compute_totals(self):
for c in self:
c.total_items = len(c.line_ids)
c.total_diferencias = len(c.line_ids.filtered(lambda l: abs(l.difference) > 1e-9))
c.total_faltante = sum(l.value_diff for l in c.line_ids if l.difference < 0)
c.total_sobrante = sum(l.value_diff for l in c.line_ids if l.difference > 0)
c.total_value_diff = c.total_faltante + c.total_sobrante
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('name'):
vals['name'] = self.env['ir.sequence'].next_by_code('skeen.inventario.conteo') or 'CONTEO-001'
return super().create(vals_list)
def apply_count(self):
"""Genera un movimiento type='ajuste' por cada línea con diferencia.
El movimiento de ajuste fija la existencia del artículo al valor
contado (ver SkeenInventarioMove.create) y sella la fecha de conteo.
Devuelve la cantidad de ajustes creados.
"""
Move = self.env['skeen.inventario.move']
ajustes = 0
for line in self.line_ids:
if abs(line.difference) <= 1e-9:
continue
move = Move.create({
'item_id': line.item_id.id,
'type': 'ajuste',
'qty': line.qty_contado,
'reference': self.name,
'notes': f'Conteo físico {self.name}',
})
line.write({'move_id': move.id})
ajustes += 1
return ajustes
class SkeenInventarioConteoLine(models.Model):
_name = 'skeen.inventario.conteo.line'
_description = 'Línea de conteo físico'
_order = 'id'
conteo_id = fields.Many2one('skeen.inventario.conteo', string='Conteo', required=True, ondelete='cascade')
item_id = fields.Many2one('skeen.inventario.item', string='Artículo', required=True, ondelete='cascade')
unit = fields.Char(related='item_id.unit', string='Unidad', readonly=True)
qty_sistema = fields.Float(string='Sistema')
qty_contado = fields.Float(string='Contado')
difference = fields.Float(string='Diferencia', compute='_compute_difference', store=True)
cost = fields.Float(string='Costo unitario')
value_diff = fields.Float(string='Diferencia valorizada', compute='_compute_difference', store=True)
move_id = fields.Many2one('skeen.inventario.move', string='Ajuste generado', readonly=True)
@api.depends('qty_sistema', 'qty_contado', 'cost')
def _compute_difference(self):
for line in self:
line.difference = (line.qty_contado or 0.0) - (line.qty_sistema or 0.0)
line.value_diff = line.difference * (line.cost or 0.0)

View File

@@ -1,3 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_skeen_inventario_item_user,Acceso a Items de Inventario SKEEN,model_skeen_inventario_item,base.group_user,1,1,1,1
access_skeen_inventario_move_user,Acceso a Movimientos de Inventario SKEEN,model_skeen_inventario_move,base.group_user,1,1,1,1
access_skeen_inventario_conteo_user,Acceso a Conteos de Inventario SKEEN,model_skeen_inventario_conteo,base.group_user,1,1,1,1
access_skeen_inventario_conteo_line_user,Acceso a Lineas de Conteo SKEEN,model_skeen_inventario_conteo_line,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_skeen_inventario_item_user Acceso a Items de Inventario SKEEN model_skeen_inventario_item base.group_user 1 1 1 1
3 access_skeen_inventario_move_user Acceso a Movimientos de Inventario SKEEN model_skeen_inventario_move base.group_user 1 1 1 1
4 access_skeen_inventario_conteo_user Acceso a Conteos de Inventario SKEEN model_skeen_inventario_conteo base.group_user 1 1 1 1
5 access_skeen_inventario_conteo_line_user Acceso a Lineas de Conteo SKEEN model_skeen_inventario_conteo_line base.group_user 1 1 1 1