fase 2.2: impuestos configurables por instancia (tasa por servicio/articulo, calculo por linea, desglose en POS/tickets/estado de cuenta, reporte de impuestos, permiso ventas.impuestos) con defaults apagados
This commit is contained in:
@@ -5,12 +5,36 @@ from odoo.exceptions import ValidationError
|
||||
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo.addons.skeen_citas.clinic_settings import get_impuestos
|
||||
|
||||
|
||||
def _fmt_mxn(v):
|
||||
"""Importe con formato mexicano ($1,234.56) para los avisos del chatter."""
|
||||
return '$%s' % format(round(float(v or 0.0), 2), ',.2f')
|
||||
|
||||
|
||||
def impuestos_activos(env):
|
||||
"""True si la instancia tiene el cálculo de impuestos encendido."""
|
||||
return bool(get_impuestos(env).get('activo'))
|
||||
|
||||
|
||||
def tasa_efectiva(record, env=None):
|
||||
"""Tasa (%) que corresponde a un servicio/artículo al momento de vender.
|
||||
|
||||
- Con 'usar tasa por defecto' (o sin registro) se toma la tasa por defecto
|
||||
de la configuración vigente: NO se congela al crear el catálogo.
|
||||
- Con una tasa propia se respeta ese valor.
|
||||
Devuelve (tasa, usa_default).
|
||||
"""
|
||||
env = env or record.env
|
||||
cfg = get_impuestos(env)
|
||||
if record is None or not getattr(record, 'exists', lambda: False)():
|
||||
return float(cfg.get('tasa_default') or 0.0), True
|
||||
if getattr(record, 'tax_use_default', False):
|
||||
return float(cfg.get('tasa_default') or 0.0), True
|
||||
return float(record.tax_rate or 0.0), False
|
||||
|
||||
|
||||
def _etiqueta_line(line):
|
||||
"""Nombre legible de una línea de venta (descripción, servicio o artículo)."""
|
||||
return line.description or line.service_id.name or line.item_id.name or 'la línea'
|
||||
@@ -43,10 +67,20 @@ class SkeenVenta(models.Model):
|
||||
# Totales
|
||||
subtotal = fields.Float(string='Subtotal', compute='_compute_totals', store=True)
|
||||
discount = fields.Float(string='Descuento', default=0.0)
|
||||
tax = fields.Float(string='Impuesto', default=0.0)
|
||||
tax = fields.Float(
|
||||
string='Impuesto', default=0.0,
|
||||
help='Con los impuestos de la clínica ACTIVOS, este campo refleja la '
|
||||
'suma de los impuestos de las líneas (se recalcula al vender). '
|
||||
'Con los impuestos apagados se conserva el monto capturado a mano.')
|
||||
total = fields.Float(string='Total', compute='_compute_totals', store=True)
|
||||
amount_paid = fields.Float(string='Pagado', default=0.0)
|
||||
amount_due = fields.Float(string='Adeudo', compute='_compute_totals', store=True)
|
||||
|
||||
# Desglose consultable para reportes (base sin impuesto + impuesto de las
|
||||
# líneas). Con los impuestos apagados quedan en 0/igual al subtotal, así
|
||||
# que no alteran ninguna cifra existente.
|
||||
tax_total = fields.Float(string='Impuesto (líneas)', compute='_compute_tax_breakdown', store=True)
|
||||
subtotal_sin_impuesto = fields.Float(string='Base sin impuesto', compute='_compute_tax_breakdown', store=True)
|
||||
card_amount = fields.Float(
|
||||
string='Pagado con tarjeta', default=0.0,
|
||||
help="Monto pagado con terminal física (payment_method='card'). "
|
||||
@@ -105,14 +139,37 @@ class SkeenVenta(models.Model):
|
||||
# Notas
|
||||
notes = fields.Text(string='Notas')
|
||||
|
||||
@api.depends('line_ids.subtotal', 'discount', 'tax', 'amount_paid', 'points_discount')
|
||||
@api.depends('line_ids.subtotal', 'line_ids.tax_amount', 'discount', 'tax',
|
||||
'amount_paid', 'points_discount')
|
||||
def _compute_totals(self):
|
||||
for venta in self:
|
||||
subtotal = sum(line.subtotal for line in venta.line_ids)
|
||||
venta.subtotal = subtotal
|
||||
# La fórmula de siempre: subtotal - descuento - puntos + impuesto.
|
||||
# Con impuestos apagados `tax` conserva el valor manual, así que el
|
||||
# total sale idéntico al de antes.
|
||||
venta.total = max(0.0, subtotal - venta.discount - venta.points_discount + venta.tax)
|
||||
venta.amount_due = max(0.0, venta.total - venta.amount_paid)
|
||||
|
||||
@api.depends('line_ids.subtotal', 'line_ids.tax_amount')
|
||||
def _compute_tax_breakdown(self):
|
||||
for venta in self:
|
||||
venta.subtotal_sin_impuesto = sum(line.subtotal for line in venta.line_ids)
|
||||
venta.tax_total = sum(line.tax_amount for line in venta.line_ids)
|
||||
|
||||
def _sync_tax_from_lines(self):
|
||||
"""Impuestos ACTIVOS: `tax` = suma de los impuestos de las líneas.
|
||||
|
||||
Con los impuestos apagados NO se toca: el monto capturado a mano
|
||||
(compatibilidad con clientes que lo mandaban por API) se conserva igual.
|
||||
"""
|
||||
if not impuestos_activos(self.env):
|
||||
return
|
||||
for venta in self:
|
||||
impuesto = sum((line.tax_amount or 0.0) for line in venta.line_ids)
|
||||
if abs((venta.tax or 0.0) - impuesto) > 0.000001:
|
||||
venta.write({'tax': impuesto})
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
@@ -497,7 +554,9 @@ class SkeenVenta(models.Model):
|
||||
head = {}
|
||||
if discount is not None:
|
||||
head['discount'] = float(discount or 0.0)
|
||||
if tax is not None:
|
||||
# Con impuestos ACTIVOS el impuesto se deriva de las líneas: no se
|
||||
# acepta un monto manual (el renglón de cada concepto manda).
|
||||
if tax is not None and not impuestos_activos(self.env):
|
||||
head['tax'] = float(tax or 0.0)
|
||||
if notes is not None:
|
||||
head['notes'] = notes or ''
|
||||
@@ -698,7 +757,12 @@ class SkeenVentaLine(models.Model):
|
||||
description = fields.Char(string='Descripción')
|
||||
quantity = fields.Float(string='Cantidad', default=1.0, required=True)
|
||||
price_unit = fields.Float(string='Precio Unitario', required=True)
|
||||
subtotal = fields.Float(string='Subtotal', compute='_compute_subtotal', store=True)
|
||||
subtotal = fields.Float(string='Subtotal', compute='_compute_amounts', store=True)
|
||||
# Impuesto propio de la línea: tasa (%) e importe. Se resuelve al vender
|
||||
# (tasa del catálogo o la por defecto de la configuración); con los
|
||||
# impuestos apagados la tasa queda en 0 y el importe en 0.
|
||||
tax_rate = fields.Float(string='Impuesto (%)', default=0.0)
|
||||
tax_amount = fields.Float(string='Impuesto', compute='_compute_amounts', store=True)
|
||||
# Artículo recetado por médico (base para comisiones)
|
||||
is_prescribed = fields.Boolean(string='Recetado', default=False,
|
||||
help='Marcar si este artículo fue recetado por un médico (entra al cálculo de comisiones).')
|
||||
@@ -726,22 +790,82 @@ class SkeenVentaLine(models.Model):
|
||||
'ventas.'
|
||||
) % {'nombre': line.prescribed_by_id.name})
|
||||
|
||||
@api.depends('quantity', 'price_unit')
|
||||
def _compute_subtotal(self):
|
||||
@api.depends('quantity', 'price_unit', 'tax_rate')
|
||||
def _compute_amounts(self):
|
||||
"""Subtotal e impuesto de la línea.
|
||||
|
||||
Con los impuestos apagados (o tasa 0) el subtotal es cantidad × precio
|
||||
y el impuesto 0: idéntico a como se calculaba antes. Con
|
||||
'precios_incluyen_impuesto' el precio capturado ya trae impuesto, así
|
||||
que el subtotal se desglosa hacia atrás.
|
||||
"""
|
||||
cfg = get_impuestos(self.env)
|
||||
activo = bool(cfg.get('activo'))
|
||||
incluye = bool(cfg.get('precios_incluyen_impuesto'))
|
||||
for line in self:
|
||||
line.subtotal = line.quantity * line.price_unit
|
||||
bruto = (line.quantity or 0.0) * (line.price_unit or 0.0)
|
||||
tasa = (line.tax_rate or 0.0) if activo else 0.0
|
||||
if tasa <= 0:
|
||||
line.subtotal = bruto
|
||||
line.tax_amount = 0.0
|
||||
elif incluye:
|
||||
base = bruto / (1.0 + tasa / 100.0)
|
||||
line.subtotal = base
|
||||
line.tax_amount = bruto - base
|
||||
else:
|
||||
line.subtotal = bruto
|
||||
line.tax_amount = bruto * tasa / 100.0
|
||||
|
||||
def _tasa_para_linea(self, vals):
|
||||
"""Tasa (%) a aplicar a una línea nueva cuando el cliente no la manda."""
|
||||
cfg = get_impuestos(self.env)
|
||||
if not cfg.get('activo'):
|
||||
return 0.0
|
||||
if vals.get('service_id'):
|
||||
servicio = self.env['skeen.servicio'].browse(vals['service_id'])
|
||||
if servicio.exists() and not servicio.tax_use_default:
|
||||
return float(servicio.tax_rate or 0.0)
|
||||
if vals.get('item_id'):
|
||||
item = self.env['skeen.inventario.item'].browse(vals['item_id'])
|
||||
if item.exists() and not item.tax_use_default:
|
||||
return float(item.tax_rate or 0.0)
|
||||
return float(cfg.get('tasa_default') or 0.0)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('tax_rate') is None:
|
||||
vals['tax_rate'] = self._tasa_para_linea(vals)
|
||||
lines = super(SkeenVentaLine, self).create(vals_list)
|
||||
lines.mapped('venta_id')._sync_tax_from_lines()
|
||||
return lines
|
||||
|
||||
def write(self, vals):
|
||||
res = super(SkeenVentaLine, self).write(vals)
|
||||
self.mapped('venta_id')._sync_tax_from_lines()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
ventas = self.mapped('venta_id')
|
||||
res = super(SkeenVentaLine, self).unlink()
|
||||
ventas._sync_tax_from_lines()
|
||||
return res
|
||||
|
||||
@api.onchange('service_id')
|
||||
def _onchange_service(self):
|
||||
if self.service_id:
|
||||
self.price_unit = self.service_id.price
|
||||
self.description = self.service_id.name
|
||||
if impuestos_activos(self.env):
|
||||
self.tax_rate = self._tasa_para_linea({'service_id': self.service_id.id})
|
||||
|
||||
@api.onchange('item_id')
|
||||
def _onchange_item(self):
|
||||
if self.item_id:
|
||||
self.price_unit = self.item_id.price
|
||||
self.description = self.item_id.name
|
||||
if impuestos_activos(self.env):
|
||||
self.tax_rate = self._tasa_para_linea({'item_id': self.item_id.id})
|
||||
|
||||
|
||||
class SkeenCorteCaja(models.Model):
|
||||
|
||||
Reference in New Issue
Block a user