From 89467432a72cb937b675806e08fd2a7dddc4a496 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Tue, 2 Jun 2026 02:31:38 -0400 Subject: [PATCH] feat(fusion_portal): Phase 2b - ADP multi-device grouping + combination guard The visit groups its ADP assessments by funding type onto ONE ADP order (first device creates the SO via the existing express completion; the rest attach), enforcing the combination rule: at most one seated-mobility device (manual WC / power WC / scooter) + optionally one walker, no duplicates. Also fixes a Phase 1b bug - it called action_complete() (needs signatures, returns an action dict) for ADP; now uses action_complete_express() which returns the SO. Untested locally (Enterprise dep) - clone verification pending. Co-Authored-By: Claude Opus 4.8 (1M context) --- fusion_portal/models/visit.py | 51 +++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/fusion_portal/models/visit.py b/fusion_portal/models/visit.py index ccd934f5..010b78ee 100644 --- a/fusion_portal/models/visit.py +++ b/fusion_portal/models/visit.py @@ -190,6 +190,22 @@ class FusionAssessmentVisit(models.Model): ) return sale_order + def _validate_adp_combination(self, adp_assessments): + """Enforce ADP device-combination rules: at most one seated-mobility + device (manual wheelchair / power wheelchair / scooter), optionally one + walker/rollator, no duplicates.""" + seated_types = {'wheelchair', 'powerchair', 'scooter'} + seated = [a for a in adp_assessments if a.equipment_type in seated_types] + walkers = [a for a in adp_assessments if a.equipment_type == 'rollator'] + labels = dict(self.env['fusion.assessment']._fields['equipment_type'].selection) + if len(seated) > 1: + raise UserError(_( + 'An ADP order can include only one seated-mobility device ' + '(manual wheelchair, power wheelchair, or scooter). This visit has: %s.' + ) % ', '.join(labels.get(a.equipment_type, a.equipment_type) for a in seated)) + if len(walkers) > 1: + raise UserError(_('An ADP order can include only one walker / rollator.')) + 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 @@ -215,14 +231,39 @@ class FusionAssessmentVisit(models.Model): 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. + # ADP equipment assessments: one ADP order per funding type, with the + # device-combination guard, reusing the existing (prod-tested) express + # completion. The first device creates the SO; the rest attach to it. + adp_by_type = {} for assessment in self.adp_assessment_ids: if assessment.sale_order_id: continue - so = assessment.action_complete() - if so: - so.visit_id = self.id + adp_by_type.setdefault(self._assessment_sale_type(assessment), []).append(assessment) + labels = dict(self.env['fusion.assessment']._fields['equipment_type'].selection) + for sale_type, group in adp_by_type.items(): + self._validate_adp_combination(group) + # Make sure each device carries the visit's client + OT so the + # existing completion logic has what it needs. + for assessment in group: + vals = {} + if not assessment.client_name: + vals['client_name'] = self.client_name or partner.name + if not assessment.authorizer_id and self.authorizer_id: + vals['authorizer_id'] = self.authorizer_id.id + if not assessment.partner_id: + vals['partner_id'] = partner.id + if vals: + assessment.write(vals) + primary = group[0] + sale_order = primary.action_complete_express() + sale_order.write({'visit_id': self.id, 'x_fc_sale_type': sale_type}) + for extra in group[1:]: + extra.write({'state': 'completed', 'sale_order_id': sale_order.id}) + sale_order.message_post( + body=Markup('

Additional ADP device on this order: %s

') + % labels.get(extra.equipment_type, extra.equipment_type or 'device'), + subtype_xmlid='mail.mt_note', + ) self.write({'state': 'done', 'partner_id': partner.id}) return self._action_view_sale_orders()