# -*- coding: utf-8 -*- """Assessment Visit — bundles the assessments done during one home visit. A sales rep + occupational therapist visit a client for 30-45 min and may do several assessments (an ADP wheelchair plus accessibility products like a stair lift, ramp, or tub cutout). The Visit is the hub that holds the client/context ONCE and, on completion, groups its assessments by FUNDING WORKFLOW (x_fc_sale_type) and creates ONE draft sale order per workflow — never one combined SO, and never a separate SO per item within the same funding. See docs/superpowers/specs/2026-06-02-assessment-visit-funding-design.md. """ import logging from markupsafe import Markup from odoo import api, fields, models, _ from odoo.exceptions import UserError _logger = logging.getLogger(__name__) # Accessibility funding source -> sale.order x_fc_sale_type. Mirrors # fusion.accessibility.assessment._create_draft_sale_order so a grouped Visit # routes exactly the way a single accessibility assessment would. ACCESSIBILITY_SALE_TYPE_MAP = { 'march_of_dimes': 'march_of_dimes', 'odsp': 'odsp', 'wsib': 'wsib', 'hardship': 'hardship', 'insurance': 'insurance', 'direct_private': 'direct_private', 'other': 'other', } class FusionAssessmentVisit(models.Model): _name = 'fusion.assessment.visit' _description = 'Assessment Visit' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'visit_date desc, id desc' name = fields.Char( string='Visit Reference', required=True, readonly=True, copy=False, default=lambda self: _('New'), ) state = fields.Selection( selection=[ ('measuring', 'Measuring'), ('client_pending', 'Client Details Pending'), ('done', 'Completed'), ('cancelled', 'Cancelled'), ], default='measuring', tracking=True, copy=False, ) # --- Shared client + context (entered once for the whole visit) --------- partner_id = fields.Many2one('res.partner', string='Client', tracking=True) client_name = fields.Char(string='Client Name', tracking=True) client_phone = fields.Char(string='Phone') client_email = fields.Char(string='Email') client_address = fields.Char(string='Address') visit_date = fields.Date( string='Visit Date', default=fields.Date.context_today, tracking=True, ) sales_rep_id = fields.Many2one( 'res.users', string='Sales Rep', default=lambda self: self.env.user, tracking=True, ) authorizer_id = fields.Many2one( 'res.partner', string='Occupational Therapist', tracking=True, ) # --- March of Dimes funding context (informational; spec §4.1) ---------- # MOD covers up to $15,000 per person (lifetime), income-gated. x_fc_income_under_mod_threshold = fields.Selection( selection=[ ('yes', 'Yes — under threshold (full $15k available)'), ('no', 'No — over threshold (may be denied / partial)'), ('unknown', 'Unknown'), ], string='Income under MOD threshold?', default='unknown', help='March of Dimes funds up to $15,000 per person (lifetime) when the ' "client's income is under that year's threshold; over it, MOD may " 'deny or partially approve. Reminder only — no automatic cap ' 'enforcement in this version.', ) # --- Assessments performed during this visit ---------------------------- adp_assessment_ids = fields.One2many( 'fusion.assessment', 'visit_id', string='ADP Assessments', ) accessibility_assessment_ids = fields.One2many( 'fusion.accessibility.assessment', 'visit_id', string='Accessibility Assessments', ) # --- Sale orders produced — one per funding workflow -------------------- sale_order_ids = fields.One2many( 'sale.order', 'visit_id', string='Sale Orders', ) assessment_count = fields.Integer(compute='_compute_counts') sale_order_count = fields.Integer(compute='_compute_counts') has_mod_items = fields.Boolean(compute='_compute_has_mod_items') @api.depends('adp_assessment_ids', 'accessibility_assessment_ids', 'sale_order_ids') def _compute_counts(self): for visit in self: visit.assessment_count = ( len(visit.adp_assessment_ids) + len(visit.accessibility_assessment_ids) ) visit.sale_order_count = len(visit.sale_order_ids) @api.depends('accessibility_assessment_ids.x_fc_funding_source') def _compute_has_mod_items(self): for visit in self: visit.has_mod_items = any( a.x_fc_funding_source == 'march_of_dimes' for a in visit.accessibility_assessment_ids ) @api.model_create_multi def create(self, vals_list): for vals in vals_list: if not vals.get('name') or vals['name'] == _('New'): seq = self.env['ir.sequence'].next_by_code('fusion.assessment.visit') vals['name'] = seq or _('New') return super().create(vals_list) # ------------------------------------------------------------------ # Completion — group assessments by funding workflow → one SO each # ------------------------------------------------------------------ def _ensure_partner(self): """Resolve the client partner for the visit, reusing an assessment's partner/_ensure_partner when one is already set.""" self.ensure_one() if self.partner_id: return self.partner_id # Borrow a child assessment's partner resolution if available. for assessment in self.accessibility_assessment_ids: if assessment.partner_id: return assessment.partner_id if hasattr(assessment, '_ensure_partner'): return assessment._ensure_partner() for assessment in self.adp_assessment_ids: if assessment.partner_id: return assessment.partner_id if self.client_name: return self.env['res.partner'].sudo().create({ 'name': self.client_name, 'phone': self.client_phone or False, 'email': self.client_email or False, }) raise UserError(_('Set a client (or client name) on the visit first.')) def _create_grouped_sale_order(self, partner, sale_type, accessibility_assessments): """Create ONE draft sale order for a set of same-funding accessibility assessments, link them all to it, and post each one's spec to chatter. Mirrors fusion.accessibility.assessment._create_draft_sale_order but for a group sharing one funding workflow.""" self.ensure_one() SaleOrder = self.env['sale.order'].sudo() so_vals = { 'partner_id': partner.id, 'user_id': self.sales_rep_id.id if self.sales_rep_id else self.env.user.id, 'state': 'draft', 'origin': _('Visit %s (%s)') % (self.name, sale_type), 'x_fc_sale_type': sale_type, 'visit_id': self.id, } if self.authorizer_id: so_vals['x_fc_authorizer_id'] = self.authorizer_id.id # MOD: pre-fill the accessibility specialist from the sales rep. if sale_type == 'march_of_dimes' and self.sales_rep_id and self.sales_rep_id.partner_id: so_vals['x_fc_mod_accessibility_specialist_id'] = self.sales_rep_id.partner_id.id sale_order = SaleOrder.create(so_vals) for assessment in accessibility_assessments: assessment.sale_order_id = sale_order.id assessment._add_assessment_tag(sale_order) sale_order.message_post( body=Markup(assessment._format_assessment_html_table()), message_type='comment', subtype_xmlid='mail.mt_note', ) assessment.write({'state': 'completed'}) _logger.info( "Visit %s created %s sale order %s grouping %d accessibility assessment(s)", self.name, sale_type, sale_order.name, len(accessibility_assessments), ) return sale_order def action_complete_visit(self): """Group the visit's accessibility assessments by funding workflow and create one draft SO per workflow. ADP equipment assessments keep their existing one-assessment-one-SO completion for now (ADP multi-device grouping arrives in Phase 2).""" self.ensure_one() if self.state == 'done': raise UserError(_('This visit is already completed.')) if not (self.accessibility_assessment_ids or self.adp_assessment_ids): raise UserError(_('Add at least one assessment before completing the visit.')) partner = self._ensure_partner() # Group accessibility assessments by their funding -> sale type. by_sale_type = {} for assessment in self.accessibility_assessment_ids: if assessment.sale_order_id: continue # already has an SO; don't duplicate sale_type = ACCESSIBILITY_SALE_TYPE_MAP.get( assessment.x_fc_funding_source, 'direct_private') by_sale_type.setdefault(sale_type, []).append(assessment) for sale_type, group in by_sale_type.items(): self._create_grouped_sale_order(partner, sale_type, group) # ADP equipment assessments: complete individually (one SO each) until # Phase 2 multi-device lets several share one ADP order. for assessment in self.adp_assessment_ids: if assessment.sale_order_id: continue so = assessment.action_complete() if so: so.visit_id = self.id self.write({'state': 'done', 'partner_id': partner.id}) return self._action_view_sale_orders() def _action_view_sale_orders(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': _('Visit Sale Orders'), 'res_model': 'sale.order', 'view_mode': 'list,form', 'domain': [('visit_id', '=', self.id)], 'context': {'create': False}, }