# -*- coding: utf-8 -*- """Campos de costo/margen que Compras agrega al artículo de inventario. `costo_promedio` y `ultimo_costo` son NUEVOS: `cost` (Costo unitario) se queda como estaba porque lo usan el valor de inventario y los reportes existentes, de modo que una instancia con `compras.activo=false` conserva sus cifras idénticas. El margen se calcula contra `costo_promedio` (con respaldo en `cost`) y el precio de venta ya sin impuesto. """ from odoo import models, fields, api class SkeenInventarioItem(models.Model): _inherit = 'skeen.inventario.item' costo_promedio = fields.Float( string='Costo promedio', default=0.0, help='Costo promedio ponderado de las compras recibidas. Editable por ' 'administración como ajuste de inventario.') ultimo_costo = fields.Float( string='Último costo', default=0.0, help='Costo unitario de la última recepción de compra.') margen = fields.Float( string='Margen', compute='_compute_margen', help='Precio de venta sin impuesto menos costo promedio.') margen_pct = fields.Float( string='Margen (%)', compute='_compute_margen') @api.depends('price', 'cost', 'costo_promedio', 'tax_rate', 'tax_use_default') def _compute_margen(self): from odoo.addons.skeen_citas.clinic_settings import get_impuestos try: imp = get_impuestos(self.env) except Exception: imp = {'activo': False, 'tasa_default': 0.0, 'precios_incluyen_impuesto': False} for it in self: precio = it.price or 0.0 if imp.get('activo'): tasa = (imp.get('tasa_default') or 0.0) if it.tax_use_default \ else (it.tax_rate or 0.0) if imp.get('precios_incluyen_impuesto') and tasa: precio = precio / (1.0 + tasa / 100.0) costo = it.costo_promedio or it.cost or 0.0 it.margen = precio - costo it.margen_pct = (it.margen / precio * 100.0) if precio else 0.0 def _actualizar_costo_recepcion(self, cantidad, costo_unitario, actualizar_promedio=True): """Aplica el costo de una recepción sobre el artículo. Fórmula ponderada: (stock_actual*costo_actual + cantidad*costo_recibido) / (stock_actual + cantidad). Si el stock actual es <= 0 (cero o negativo) el promedio se reinicia al costo recibido, porque mezclar un costo viejo con existencias negativas daría un número sin sentido. Debe llamarse ANTES de crear el movimiento de entrada, para que `stock_actual` sea la existencia previa a la recepción. """ self.ensure_one() stock_actual = self.qty or 0.0 cantidad = float(cantidad or 0.0) costo_unitario = float(costo_unitario or 0.0) vals = {'ultimo_costo': costo_unitario} if actualizar_promedio and cantidad > 0: if stock_actual > 0: # Sin historial de compras el promedio arranca del costo unitario # ya cargado (`cost`); si no, mezclar contra 0 daría un promedio # artificialmente bajo. costo_actual = self.costo_promedio or self.cost or 0.0 nuevo = ((stock_actual * costo_actual) + (cantidad * costo_unitario)) / (stock_actual + cantidad) else: nuevo = costo_unitario vals['costo_promedio'] = nuevo self.write(vals) return self.costo_promedio class SkeenInventarioMove(models.Model): """Lote y caducidad de la mercancía recibida. Columnas nuevas y opcionales: el kardex actual no cambia. Las recepciones de compra las llenan; los demás movimientos las dejan vacías. """ _inherit = 'skeen.inventario.move' lote = fields.Char(string='Lote') fecha_caducidad = fields.Date(string='Caducidad')