# -*- coding: utf-8 -*- # Copyright 2024-2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) """Store labor warranty. Distinct from the manufacturer warranty. This is what we extend at point of sale: "5-year labor warranty - bring it to the store, we fix the labour for free". Carve-outs (user negligence, gross negligence, misuse, etc.) are tracked explicitly so the visit-report wizard can VOID the warranty in real time when the tech encounters one. Important boundary - WHAT THE WARRANTY COVERS: - In-store labour: FREE - Home callout (tech dispatched): callout fee STILL applies (it includes inspection / report); the hourly labour beyond 30 min is free - Parts: NEVER free unless covered by separate manufacturer warranty - Travel: ALWAYS charged when over the distance threshold """ from dateutil.relativedelta import relativedelta from markupsafe import Markup from odoo import _, api, fields, models from odoo.exceptions import UserError VOID_REASONS = [ ('user_negligence', 'User Negligence'), ('gross_negligence', 'Gross Negligence'), ('misuse', 'Misuse'), ('over_recommended_use', 'Over-Recommended Use'), ('accidental_damage', 'Accidental Damage'), ('not_covered_part', 'Part Not Covered'), ('other', 'Other (see notes)'), ] class FusionRepairLaborWarranty(models.Model): _name = 'fusion.repair.labor.warranty' _inherit = ['mail.thread'] _description = 'Store Labor Warranty' _order = 'end_date desc, id desc' name = fields.Char( string='Reference', default='New', copy=False, readonly=True, tracking=True, ) partner_id = fields.Many2one( 'res.partner', string='Client', required=True, tracking=True, index=True, ) product_id = fields.Many2one( 'product.product', string='Equipment', required=True, tracking=True, ) lot_id = fields.Many2one( 'stock.lot', string='Serial', tracking=True, ) sale_order_id = fields.Many2one( 'sale.order', string='Sold On', ondelete='set null', tracking=True, ) warranty_years = fields.Integer( string='Years', default=5, required=True, tracking=True, ) start_date = fields.Date( string='Start', default=fields.Date.context_today, required=True, tracking=True, ) end_date = fields.Date( string='Ends', compute='_compute_end_date', store=True, tracking=True, ) state = fields.Selection( [ ('active', 'Active'), ('expired', 'Expired'), ('void', 'Void'), ('consumed', 'Consumed'), ], string='Status', default='active', tracking=True, compute='_compute_state', store=True, ) # When voided void_reason = fields.Selection( VOID_REASONS, string='Void Reason', tracking=True, ) void_notes = fields.Text(string='Void Notes') voided_at = fields.Datetime(string='Voided At', copy=False) voided_by_id = fields.Many2one('res.users', string='Voided By', copy=False) company_id = fields.Many2one( 'res.company', default=lambda self: self.env.company, ) _name_unique = models.Constraint( 'unique(name)', 'Labor-warranty references must be unique.', ) # ------------------------------------------------------------------ # CRUD # ------------------------------------------------------------------ @api.model_create_multi def create(self, vals_list): for vals in vals_list: if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code( 'fusion.repair.labor.warranty' ) or 'LW/NEW' return super().create(vals_list) # ------------------------------------------------------------------ # COMPUTES # ------------------------------------------------------------------ @api.depends('start_date', 'warranty_years') def _compute_end_date(self): for r in self: if r.start_date and r.warranty_years: r.end_date = r.start_date + relativedelta(years=r.warranty_years) else: r.end_date = False @api.depends('void_reason', 'end_date') def _compute_state(self): today = fields.Date.context_today(self) for r in self: if r.state == 'consumed': continue if r.void_reason: r.state = 'void' elif r.end_date and r.end_date < today: r.state = 'expired' else: r.state = 'active' # ------------------------------------------------------------------ # LOOKUP # ------------------------------------------------------------------ @api.model def find_active_for(self, partner, product=None, lot=None): """Find the active labor warranty covering (partner, product/lot). Specificity order: 1. exact lot match 2. product + partner match 3. partner-only match (last resort) """ if not partner: return self.browse() today = fields.Date.context_today(self) base_domain = [ ('partner_id', '=', partner.id), ('state', '=', 'active'), ('end_date', '>=', today), ] if lot: hit = self.sudo().search( base_domain + [('lot_id', '=', lot.id)], order='end_date desc', limit=1, ) if hit: return hit if product: hit = self.sudo().search( base_domain + [('product_id', '=', product.id)], order='end_date desc', limit=1, ) if hit: return hit return self.browse() # ------------------------------------------------------------------ # VOID # ------------------------------------------------------------------ def action_void(self, reason='other', notes=''): if not reason: raise UserError(_('A void reason is required.')) for r in self: r.write({ 'void_reason': reason, 'void_notes': notes, 'voided_at': fields.Datetime.now(), 'voided_by_id': self.env.uid, }) r.message_post(body=Markup(_( 'Warranty voided by %(user)s. Reason: %(reason)s.' )) % { 'user': self.env.user.name, 'reason': dict(VOID_REASONS).get(reason, reason), }) def action_reinstate(self): for r in self: r.write({ 'void_reason': False, 'void_notes': False, 'voided_at': False, 'voided_by_id': False, }) r.message_post(body=_('Warranty reinstated.'))