Oleada UX 3: POS (repetir ultima venta, aviso de paquetes, atajos, caja desde POS), Ventas (cancelacion auditada con reversion de monedero y stock, detalle inline, presets de fecha), Cortes (historial comparativo y export)
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
|
||||
class SkeenVenta(models.Model):
|
||||
_name = 'skeen.venta'
|
||||
@@ -55,6 +57,13 @@ class SkeenVenta(models.Model):
|
||||
refund_reason = fields.Text(string='Motivo Devolución')
|
||||
refunded_at = fields.Datetime(string='Fecha Devolución')
|
||||
|
||||
# Cancelación (nota de crédito): motivo obligatorio y auditoría
|
||||
cancel_reason = fields.Text(string='Motivo Cancelación')
|
||||
cancelled_by = fields.Char(string='Cancelada por', readonly=True, copy=False)
|
||||
cancelled_at = fields.Datetime(string='Fecha Cancelación', readonly=True, copy=False)
|
||||
cancel_points_reversed = fields.Integer(string='Puntos Revertidos', default=0, readonly=True, copy=False)
|
||||
cancel_points_restored = fields.Integer(string='Puntos Restaurados', default=0, readonly=True, copy=False)
|
||||
|
||||
# Fechas
|
||||
date = fields.Date(string='Fecha', required=True, default=fields.Date.today)
|
||||
create_date = fields.Datetime(string='Fecha Creación', readonly=True, default=fields.Datetime.now)
|
||||
@@ -107,6 +116,118 @@ class SkeenVenta(models.Model):
|
||||
def action_cancel(self):
|
||||
self.write({'state': 'cancelled'})
|
||||
|
||||
def _reverse_wallet_for_cancel(self):
|
||||
"""Revierte los puntos que ESTA venta otorgó y devuelve los que canjeó.
|
||||
|
||||
Se identifican por `reference == venta.name` (misma convención que usa
|
||||
action_mark_paid). Los ajustes negativos hacen piso en 0 para no dejar
|
||||
saldo negativo si el paciente ya gastó los puntos.
|
||||
Devuelve (puntos_revertidos, puntos_restaurados).
|
||||
"""
|
||||
self.ensure_one()
|
||||
monedero = self.env['skeen.monedero'].search([('partner_id', '=', self.partner_id.id)], limit=1)
|
||||
if not monedero:
|
||||
return 0, 0
|
||||
txs = self.env['skeen.monedero.transaction'].search([
|
||||
('monedero_id', '=', monedero.id),
|
||||
('reference', '=', self.name),
|
||||
])
|
||||
revertidos = 0
|
||||
restaurados = 0
|
||||
for tx in txs:
|
||||
if tx.type == 'accrual' and tx.points > 0:
|
||||
applied = monedero.adjust_points(
|
||||
-tx.points,
|
||||
description=f'Cancelación venta {self.name}',
|
||||
reference=self.name,
|
||||
)
|
||||
revertidos += -applied
|
||||
elif tx.type == 'redemption' and tx.points < 0:
|
||||
monedero.adjust_points(
|
||||
-tx.points,
|
||||
description=f'Reintegro de puntos por cancelación {self.name}',
|
||||
reference=self.name,
|
||||
)
|
||||
restaurados += -tx.points
|
||||
return revertidos, restaurados
|
||||
|
||||
def _restore_stock_for_cancel(self):
|
||||
"""Devuelve al inventario lo que esta venta descontó.
|
||||
|
||||
Determinista: el cobro POS crea un `skeen.inventario.move` type='venta'
|
||||
con reference = venta.name por cada línea de artículo. Se registra la
|
||||
entrada inversa (type='cancelacion') con la misma referencia.
|
||||
Devuelve la lista de movimientos creados.
|
||||
"""
|
||||
self.ensure_one()
|
||||
Move = self.env['skeen.inventario.move']
|
||||
salidas = Move.search([('reference', '=', self.name), ('type', '=', 'venta')])
|
||||
creados = Move
|
||||
for m in salidas:
|
||||
creados |= Move.create({
|
||||
'item_id': m.item_id.id,
|
||||
'type': 'cancelacion',
|
||||
'qty': m.qty,
|
||||
'reference': self.name,
|
||||
'notes': f'Cancelación de venta {self.name}',
|
||||
})
|
||||
return creados
|
||||
|
||||
def action_cancel_with_reason(self, reason='', cancelled_by='', restore_stock=True):
|
||||
"""Cancela la venta (nota de crédito) con motivo obligatorio.
|
||||
|
||||
- Revierte los puntos otorgados y reintegra los canjeados por la venta.
|
||||
- Devuelve al inventario los artículos descontados por la venta.
|
||||
- Registra auditoría en el chatter y en los campos cancelled_*.
|
||||
La venta NO se borra: queda en state='cancelled' y los reportes la excluyen.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.state == 'cancelled':
|
||||
raise ValidationError(_('La venta ya está cancelada'))
|
||||
reason = (reason or '').strip()
|
||||
if not reason:
|
||||
raise ValidationError(_('El motivo de cancelación es obligatorio'))
|
||||
|
||||
revertidos, restaurados = self._reverse_wallet_for_cancel()
|
||||
movimientos = self._restore_stock_for_cancel() if restore_stock else self.env['skeen.inventario.move']
|
||||
|
||||
self.write({
|
||||
'state': 'cancelled',
|
||||
'cancel_reason': reason,
|
||||
'cancelled_by': cancelled_by or '',
|
||||
'cancelled_at': fields.Datetime.now(),
|
||||
'cancel_points_reversed': revertidos,
|
||||
'cancel_points_restored': restaurados,
|
||||
})
|
||||
|
||||
# Auditoría en el chatter. En peticiones auth='none' (frontend) el uid
|
||||
# de Odoo es nulo y message_post necesita un autor: se publica como el
|
||||
# usuario administrador y el login real queda en el cuerpo y en
|
||||
# cancelled_by.
|
||||
author = self.env.ref('base.user_admin', raise_if_not_found=False)
|
||||
if not author:
|
||||
author = self.env['res.users'].sudo().search([('login', '=', 'admin')], limit=1)
|
||||
auditor = self.with_user(author) if author else self
|
||||
auditor.message_post(body=Markup(
|
||||
'<b>Venta cancelada (nota de crédito)</b><br/>'
|
||||
'Motivo: {}<br/>'
|
||||
'Cancelada por: {}<br/>'
|
||||
'Puntos revertidos: {} · Puntos reintegrados: {}<br/>'
|
||||
'Artículos devueltos al inventario: {}'
|
||||
).format(
|
||||
reason,
|
||||
cancelled_by or 'sistema',
|
||||
revertidos,
|
||||
restaurados,
|
||||
len(movimientos),
|
||||
))
|
||||
|
||||
return {
|
||||
'points_reversed': revertidos,
|
||||
'points_restored': restaurados,
|
||||
'stock_moves': len(movimientos),
|
||||
}
|
||||
|
||||
def action_refund(self, amount=None, reason=''):
|
||||
"""Devolución total o parcial de una venta. Reversible: no borra nada.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user