# -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 (Odoo Proprietary License v1.0) # Part of the Fusion Plating product family. from dateutil.relativedelta import relativedelta from odoo import api, fields, models class FpSds(models.Model): """Safety Data Sheet library entry. A Safety Data Sheet (SDS) is a 16-section document supplied by a chemical manufacturer that describes the hazards, handling, storage, exposure controls and emergency information for a product. Under the WHMIS 2015 / GHS framework an SDS is considered current for three years from its issue date — after which a refresh from the supplier is required. Each SDS in the library carries supplier metadata, hazard classification, GHS pictogram codes, language coverage and a link to the original PDF via ir.attachment. """ _name = 'fusion.plating.sds' _description = 'Fusion Plating — Safety Data Sheet' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'product_name, version desc, issue_date desc' _rec_name = 'name' name = fields.Char( string='Reference', required=True, tracking=True, help='Internal reference for this SDS entry, often the product name.', ) product_name = fields.Char( string='Product Name', tracking=True, ) supplier_name = fields.Char( string='Supplier (Text)', tracking=True, help='Free-text supplier name as printed on the SDS, used when ' 'no res.partner record exists yet.', ) supplier_id = fields.Many2one( 'res.partner', string='Supplier', tracking=True, ) cas_number = fields.Char( string='CAS Number', tracking=True, ) version = fields.Char( string='Version', tracking=True, ) issue_date = fields.Date( string='Issue Date', tracking=True, ) expiry_date = fields.Date( string='Expiry Date', compute='_compute_expiry_date', store=True, tracking=True, help='Computed as issue date + 3 years per WHMIS / GHS rule. ' 'Refresh from the supplier is required before this date.', ) state = fields.Selection( [ ('current', 'Current'), ('expiring_soon', 'Expiring Soon'), ('expired', 'Expired'), ('withdrawn', 'Withdrawn'), ], string='Status', compute='_compute_state', store=True, tracking=True, ) hazard_class = fields.Selection( [ ('flammable', 'Flammable'), ('oxidizer', 'Oxidizer'), ('compressed_gas', 'Compressed Gas'), ('corrosive', 'Corrosive'), ('toxic', 'Toxic'), ('carcinogen', 'Carcinogen'), ('reproductive_toxin', 'Reproductive Toxin'), ('sensitizer', 'Sensitizer'), ('aquatic_toxin', 'Aquatic Toxin'), ('other', 'Other'), ], string='Primary Hazard Class', tracking=True, ) ghs_pictograms = fields.Char( string='GHS Pictograms', help='Comma-separated GHS pictogram codes, e.g. GHS01,GHS02,GHS05.', ) language = fields.Selection( [ ('en', 'English'), ('fr', 'French'), ('both', 'Bilingual (EN/FR)'), ], string='Language', default='en', ) attachment_id = fields.Many2one( 'ir.attachment', string='SDS Document', help='The original SDS PDF supplied by the manufacturer.', ) notes = fields.Html( string='Notes', ) withdrawn = fields.Boolean( string='Withdrawn', tracking=True, help='Manually mark this SDS as withdrawn (e.g. product discontinued).', ) active = fields.Boolean(default=True) company_id = fields.Many2one( 'res.company', string='Company', default=lambda self: self.env.company, ) chemical_ids = fields.One2many( 'fusion.plating.chemical', 'sds_id', string='Chemicals', ) chemical_count = fields.Integer( compute='_compute_chemical_count', ) # ========================================================================== # Computes # ========================================================================== @api.depends('issue_date') def _compute_expiry_date(self): for rec in self: if rec.issue_date: rec.expiry_date = rec.issue_date + relativedelta(years=3) else: rec.expiry_date = False @api.depends('expiry_date', 'withdrawn') def _compute_state(self): today = fields.Date.context_today(self) warn_window = today + relativedelta(months=3) for rec in self: if rec.withdrawn: rec.state = 'withdrawn' elif not rec.expiry_date: rec.state = 'current' elif rec.expiry_date < today: rec.state = 'expired' elif rec.expiry_date <= warn_window: rec.state = 'expiring_soon' else: rec.state = 'current' def _compute_chemical_count(self): for rec in self: rec.chemical_count = len(rec.chemical_ids) # ========================================================================== # Actions # ========================================================================== def action_mark_withdrawn(self): self.write({'withdrawn': True}) def action_mark_active(self): self.write({'withdrawn': False})