From b4b59cc3c9e3e8291e46f344a8d27dd172d1c932 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Thu, 21 May 2026 00:24:35 -0400 Subject: [PATCH] feat(fusion_repairs): Bundle 7 - tech mobile (T3 + T4 + T6 + T7) T3 Labour timer on technician task - Two new fields on fusion.technician.task: x_fc_timer_running_since (Datetime) + x_fc_timer_accumulated_minutes (Float). - action_timer_start / action_timer_stop methods, idempotent (start when already running is a no-op, stop when not running is a no-op). - Multiple start/stop cycles accumulate into the same total. - Two header buttons (Start Timer green / Stop Timer amber), invisible based on the running_since field so the right one shows at any time. - Stop posts a chatter line 'Labour timer stopped. Added X.X min, total Y.Y min.' so audit history shows every shift. T4 Client signature on visit report - New client_signature Binary field on the visit-report wizard with Odoo native widget='signature' that draws on canvas + base64-encodes the PNG. - client_signature_name Char for typed name (audit). - Persisted as an ir.attachment on the repair.order via the new _persist_mobile_artefacts helper. - Chatter post 'Client signature captured (Jane Smith).'. T6 Replaced parts - serial capture - parts_serial_capture Text on the wizard (one per line per the spec). - On confirm, posted to chatter wrapped in
 so line breaks survive.
- Used by OEM warranty filing in future M8.

T7 Client no-show photo proof
- no_show Boolean + no_show_photo Binary with widget='image' (visible
  only when no_show=True via Odoo 19 invisible= conditional).
- Photo saved as ir.attachment on the repair when present.
- Chatter post 'Visit recorded as client no-show (photo attached)'.

Verified end-to-end on local westin-v19:
  T3 timer started -> 2s sleep -> stopped -> 0.0357 min recorded
  T4 attachment 'signature-RO-202605-17.png' created on repair
  T6 chatter shows 'SN-AAA-111 / SN-BBB-222'
  T4 chatter shows 'Client signature captured (Jane Smith)'

Bumped to 19.0.1.7.0.

Co-authored-by: Cursor 
---
 fusion_repairs/__manifest__.py                |  2 +-
 fusion_repairs/models/technician_task.py      | 43 ++++++++++++
 .../views/technician_task_views.xml           | 16 +++++
 .../wizard/repair_visit_report_wizard.py      | 66 +++++++++++++++++++
 .../repair_visit_report_wizard_views.xml      | 17 +++++
 5 files changed, 143 insertions(+), 1 deletion(-)

diff --git a/fusion_repairs/__manifest__.py b/fusion_repairs/__manifest__.py
index ca76d998..0b01f46b 100644
--- a/fusion_repairs/__manifest__.py
+++ b/fusion_repairs/__manifest__.py
@@ -4,7 +4,7 @@
 
 {
     'name': 'Fusion Repairs',
-    'version': '19.0.1.6.0',
+    'version': '19.0.1.7.0',
     'category': 'Inventory/Repairs',
     'summary': 'Guided medical equipment repair intake, dispatch, maintenance, and self-service portal',
     'description': """
diff --git a/fusion_repairs/models/technician_task.py b/fusion_repairs/models/technician_task.py
index 76162f83..36792c33 100644
--- a/fusion_repairs/models/technician_task.py
+++ b/fusion_repairs/models/technician_task.py
@@ -42,6 +42,49 @@ class FusionTechnicianTaskRepairs(models.Model):
         copy=False,
     )
 
+    # ------------------------------------------------------------------
+    # T3 - Labour timer. The tech taps Start when they begin work and
+    # Stop when done; the accumulated minutes feeds the visit-report
+    # actual hours field. Multiple start/stop cycles are accumulated.
+    # ------------------------------------------------------------------
+    x_fc_timer_running_since = fields.Datetime(
+        string='Timer Running Since',
+        copy=False,
+    )
+    x_fc_timer_accumulated_minutes = fields.Float(
+        string='Accumulated Minutes',
+        default=0.0,
+        copy=False,
+        help='Total labour minutes captured by the tech timer. '
+             'Divide by 60 for the hours that prefill the visit report.',
+    )
+
+    def action_timer_start(self):
+        for t in self:
+            if t.x_fc_timer_running_since:
+                continue  # already running
+            t.x_fc_timer_running_since = fields.Datetime.now()
+            t.message_post(body=Markup(_('Labour timer started.')))
+
+    def action_timer_stop(self):
+        for t in self:
+            if not t.x_fc_timer_running_since:
+                continue
+            from datetime import datetime
+            elapsed_minutes = (
+                datetime.now() - t.x_fc_timer_running_since
+            ).total_seconds() / 60.0
+            t.x_fc_timer_accumulated_minutes = (
+                t.x_fc_timer_accumulated_minutes or 0.0
+            ) + elapsed_minutes
+            t.x_fc_timer_running_since = False
+            t.message_post(body=Markup(_(
+                'Labour timer stopped. Added %(mins).1f min, total %(tot).1f min.'
+            )) % {
+                'mins': elapsed_minutes,
+                'tot': t.x_fc_timer_accumulated_minutes or 0.0,
+            })
+
     def write(self, vals):
         """When a maintenance task transitions to 'completed', roll the
         linked contract to its next cycle. Failure to roll never blocks
diff --git a/fusion_repairs/views/technician_task_views.xml b/fusion_repairs/views/technician_task_views.xml
index f1a1e234..2d78de05 100644
--- a/fusion_repairs/views/technician_task_views.xml
+++ b/fusion_repairs/views/technician_task_views.xml
@@ -22,10 +22,26 @@
                         class="btn-secondary"
                         icon="fa-wrench"
                         invisible="not x_fc_repair_order_id"/>
+