folder rename

This commit is contained in:
gsinghpal
2026-04-16 20:53:53 -04:00
parent 3f3ddcbab4
commit 7c7ef06057
634 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
# Fusion Plating — Logistics
Part of the Fusion Plating product family by Nexa Systems Inc.
Adds pickup & delivery management on top of the `fusion_plating` core:
- **Vehicle master** — insurance, registration, service, TDG status, home
facility, current driver
- **Driver tracking** — extends `hr.employee` with licence class, licence
expiry, TDG certification and expiry (`x_fc_*` fields)
- **Pickup requests** — customer-initiated pickup of parts to be processed,
with full state machine (new → scheduled → en_route → picked_up → received)
- **Deliveries** — scheduled delivery of finished parts back to the customer
(draft → scheduled → en_route → delivered / refused / returned)
- **Routes** — combine pickups and deliveries into a single run for one
driver and vehicle, with drag-to-reorder stops, calendar view, and total
km tracking
- **Chain of custody** — append-only audit trail written automatically as
pickups and deliveries move through their lifecycle
- **Proof of delivery** — recipient signature, photos, GPS, delivery
timestamp
## Dependencies
- `fusion_plating` (core)
- `hr`
- `mail`
Works on both Odoo Community and Enterprise. The Enterprise `fleet` module
is **not** required — the vehicle master is a lightweight CE-compatible
model sized to what a plating shop needs.
## Security
Reuses the core `fusion_plating` groups (Operator / Supervisor / Manager /
Administrator) via the `res.groups.privilege` mechanism. No new groups are
defined by this module.
- Operators: read-only on all logistics records
- Supervisors: read / write / create on routes, deliveries, pickup
requests, vehicles, route stops, custody events, PODs
- Managers: full CRUD (adds unlink)
Multi-company isolation is enforced by global `ir.rule` records on every
new model.
## Menu
Adds a `Logistics` section under the `Plating` app menu with:
- Pickup Requests
- Deliveries
- Routes
- Chain of Custody
- Proof of Delivery
Adds `Vehicles` under `Plating → Configuration`.
## Field naming
- New dedicated models use the `fusion.plating.*` namespace consistent with
the core module.
- Extensions of base Odoo models (`hr.employee`) use the `x_fc_` prefix per
the Fusion Central convention.
## Odoo 19 compliance
- `res.groups.privilege` is reused from the core module — no
`category_id` on `res.groups`.
- No `users` field on groups.
- All models inherit `mail.thread` / `mail.activity.mixin` via the
`_inherit` list.
- `<chatter/>` tag used in form views.
- SCSS is theme-aware — no hardcoded colours, only CSS custom properties
from Odoo / Bootstrap, and `color-mix()` for semantic tints.
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.

View File

@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from . import models

View File

@@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
{
'name': 'Fusion Plating — Logistics',
'version': '19.0.1.0.0',
'category': 'Manufacturing/Plating',
'summary': (
'Pickup & delivery for plating shops: vehicle master, driver '
'tracking, route planning, chain of custody, proof of delivery.'
),
'description': """
Fusion Plating — Logistics
==========================
Part of the Fusion Plating product family by Nexa Systems Inc.
Adds pickup & delivery management to the Fusion Plating core:
* Vehicle master (with insurance, registration, service, TDG status)
* Driver tracking (extends hr.employee with licence + TDG fields)
* Pickup requests — customer-initiated pickup of parts to be processed
* Deliveries — scheduled delivery of finished parts back to customer
* Routes — combine multiple pickups + deliveries into a single run
* Chain of custody — every custody event logged for audit trail
* Proof of delivery — signature, photos, GPS
Depends on the core fusion_plating module. Works on both Odoo Community
and Enterprise editions.
Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
""",
'author': 'Nexa Systems Inc.',
'website': 'https://www.nexasystems.ca',
'maintainer': 'Nexa Systems Inc.',
'support': 'support@nexasystems.ca',
'license': 'OPL-1',
'price': 0.00,
'currency': 'CAD',
'depends': [
'fusion_plating',
'hr',
'mail',
],
'data': [
'security/fp_logistics_security.xml',
'security/ir.model.access.csv',
'data/fp_sequence_data.xml',
'views/fp_vehicle_views.xml',
'views/fp_pickup_request_views.xml',
'views/fp_delivery_views.xml',
'views/fp_route_views.xml',
'views/fp_chain_of_custody_views.xml',
'views/fp_proof_of_delivery_views.xml',
'views/hr_employee_views.xml',
'views/fp_menu.xml',
],
'demo': [
'data/fp_demo_logistics_data.xml',
],
'assets': {
'web.assets_backend': [
'fusion_plating_logistics/static/src/scss/fusion_plating_logistics.scss',
],
},
'installable': True,
'auto_install': False,
'application': False,
}

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2026 Nexa Systems Inc. -->
<!-- License OPL-1 (Odoo Proprietary License v1.0) -->
<odoo noupdate="1">
<!-- ===================================================================
Vehicles
=================================================================== -->
<record id="demo_vehicle_van" model="fusion.plating.vehicle">
<field name="name">Van 1</field>
<field name="license_plate">DEMO-VAN-01</field>
<field name="make">Ford</field>
<field name="model_year">2024</field>
<field name="vehicle_type">van</field>
<field name="capacity_kg">1200</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="state">available</field>
<field name="tdg_certified" eval="True"/>
<field name="insurance_expiry" eval="(DateTime.today() + timedelta(days=180)).strftime('%Y-%m-%d')"/>
<field name="registration_expiry" eval="(DateTime.today() + timedelta(days=300)).strftime('%Y-%m-%d')"/>
</record>
<record id="demo_vehicle_truck_box" model="fusion.plating.vehicle">
<field name="name">Box Truck 07</field>
<field name="license_plate">DEMO-BOX-07</field>
<field name="make">Isuzu</field>
<field name="model_year">2023</field>
<field name="vehicle_type">truck_box</field>
<field name="capacity_kg">3500</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="state">in_use</field>
<field name="tdg_certified" eval="False"/>
<field name="insurance_expiry" eval="(DateTime.today() + timedelta(days=120)).strftime('%Y-%m-%d')"/>
<field name="registration_expiry" eval="(DateTime.today() + timedelta(days=240)).strftime('%Y-%m-%d')"/>
</record>
<record id="demo_vehicle_pickup" model="fusion.plating.vehicle">
<field name="name">Pickup 3</field>
<field name="license_plate">DEMO-PU-03</field>
<field name="make">RAM</field>
<field name="model_year">2025</field>
<field name="vehicle_type">truck_pickup</field>
<field name="capacity_kg">900</field>
<field name="facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="state">maintenance</field>
<field name="tdg_certified" eval="False"/>
<field name="next_service_date" eval="(DateTime.today() + timedelta(days=3)).strftime('%Y-%m-%d')"/>
</record>
<!-- ===================================================================
Pickup Requests
=================================================================== -->
<record id="demo_pickup_new" model="fusion.plating.pickup.request">
<field name="name">PU-DEMO-001</field>
<field name="partner_id" ref="fusion_plating.demo_partner_aeroparts"/>
<field name="contact_name">Jane Smith</field>
<field name="contact_phone">905-555-0101</field>
<field name="requested_date" eval="DateTime.now().strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="destination_facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="item_description" type="html"><p>20 steel brackets for zinc plating</p></field>
<field name="estimated_weight_kg">45</field>
<field name="state">new</field>
</record>
<record id="demo_pickup_scheduled" model="fusion.plating.pickup.request">
<field name="name">PU-DEMO-002</field>
<field name="partner_id" ref="fusion_plating.demo_partner_aeroparts"/>
<field name="contact_name">Mike Johnson</field>
<field name="contact_phone">416-555-0202</field>
<field name="requested_date" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="scheduled_date" eval="(DateTime.now() + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="vehicle_id" ref="demo_vehicle_van"/>
<field name="destination_facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="item_description" type="html"><p>50 aluminium housings — anodize black</p></field>
<field name="estimated_weight_kg">120</field>
<field name="state">scheduled</field>
</record>
<record id="demo_pickup_picked_up" model="fusion.plating.pickup.request">
<field name="name">PU-DEMO-003</field>
<field name="partner_id" ref="fusion_plating.demo_partner_aeroparts"/>
<field name="contact_name">Sarah Lee</field>
<field name="contact_phone">647-555-0303</field>
<field name="requested_date" eval="(DateTime.now() - timedelta(days=2)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="scheduled_date" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="picked_up_at" eval="(DateTime.now() - timedelta(hours=3)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="vehicle_id" ref="demo_vehicle_truck_box"/>
<field name="destination_facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="item_description" type="html"><p>10 stainless steel panels — electropolish</p></field>
<field name="estimated_weight_kg">85</field>
<field name="tdg_required" eval="True"/>
<field name="state">picked_up</field>
</record>
<!-- ===================================================================
Deliveries
=================================================================== -->
<record id="demo_delivery_draft" model="fusion.plating.delivery">
<field name="name">DLV-DEMO-001</field>
<field name="partner_id" ref="fusion_plating.demo_partner_precision"/>
<field name="contact_name">Tom Brown</field>
<field name="contact_phone">905-555-0401</field>
<field name="job_ref">JOB-2026-0042</field>
<field name="source_facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="notes" type="html"><p>Customer requested morning delivery</p></field>
<field name="state">draft</field>
</record>
<record id="demo_delivery_en_route" model="fusion.plating.delivery">
<field name="name">DLV-DEMO-002</field>
<field name="partner_id" ref="fusion_plating.demo_partner_precision"/>
<field name="contact_name">Lisa Chen</field>
<field name="contact_phone">416-555-0502</field>
<field name="job_ref">JOB-2026-0039</field>
<field name="scheduled_date" eval="DateTime.now().strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="vehicle_id" ref="demo_vehicle_van"/>
<field name="source_facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="state">en_route</field>
</record>
<record id="demo_delivery_delivered" model="fusion.plating.delivery">
<field name="name">DLV-DEMO-003</field>
<field name="partner_id" ref="fusion_plating.demo_partner_precision"/>
<field name="contact_name">Dave Wilson</field>
<field name="contact_phone">647-555-0603</field>
<field name="job_ref">JOB-2026-0035</field>
<field name="scheduled_date" eval="(DateTime.now() - timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="delivered_at" eval="(DateTime.now() - timedelta(hours=5)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="vehicle_id" ref="demo_vehicle_truck_box"/>
<field name="source_facility_id" ref="fusion_plating.demo_facility_main"/>
<field name="notes" type="html"><p>Delivered to loading dock B — signed by Dave Wilson</p></field>
<field name="state">delivered</field>
</record>
</odoo>

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo noupdate="1">
<record id="seq_fp_pickup_request" model="ir.sequence">
<field name="name">Fusion Plating: Pickup Request</field>
<field name="code">fusion.plating.pickup.request</field>
<field name="prefix">PU/%(year)s/</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_fp_delivery" model="ir.sequence">
<field name="name">Fusion Plating: Delivery</field>
<field name="code">fusion.plating.delivery</field>
<field name="prefix">DLV/%(year)s/</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_fp_route" model="ir.sequence">
<field name="name">Fusion Plating: Route</field>
<field name="code">fusion.plating.route</field>
<field name="prefix">RT/%(year)s%(month)s/</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
<record id="seq_fp_proof_of_delivery" model="ir.sequence">
<field name="name">Fusion Plating: Proof of Delivery</field>
<field name="code">fusion.plating.proof.of.delivery</field>
<field name="prefix">POD/%(year)s/</field>
<field name="padding">4</field>
<field name="company_id" eval="False"/>
</record>
</odoo>

View File

@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from . import fp_vehicle
from . import hr_employee
from . import fp_pickup_request
from . import fp_delivery
from . import fp_route
from . import fp_route_stop
from . import fp_chain_of_custody
from . import fp_proof_of_delivery

View File

@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class FpChainOfCustody(models.Model):
"""A single custody event — the audit trail for parts in transit.
A chain of custody record is created every time parts change hands:
received from the customer, loaded on a vehicle, entered a facility,
transferred between facilities, or delivered to the customer.
These records are append-only in practice and form the evidence
trail that quality and compliance audits rely on. They are linked
to either a pickup request or a delivery (or both, for a full
round-trip).
"""
_name = 'fusion.plating.chain.of.custody'
_description = 'Fusion Plating — Chain of Custody Event'
_order = 'event_datetime desc, id desc'
_rec_name = 'display_name'
name = fields.Char(
string='Reference',
)
display_name = fields.Char(
compute='_compute_display_name',
store=True,
)
event_datetime = fields.Datetime(
string='Event Time',
required=True,
default=fields.Datetime.now,
)
event_type = fields.Selection(
[
('received_from_customer', 'Received from Customer'),
('entered_facility', 'Entered Facility'),
('exited_facility', 'Exited Facility'),
('loaded_on_vehicle', 'Loaded on Vehicle'),
('delivered_to_customer', 'Delivered to Customer'),
('transferred_between_facilities', 'Transferred Between Facilities'),
],
string='Event Type',
required=True,
)
from_party = fields.Char(
string='From',
)
to_party = fields.Char(
string='To',
)
pickup_request_id = fields.Many2one(
'fusion.plating.pickup.request',
string='Pickup Request',
ondelete='set null',
)
delivery_id = fields.Many2one(
'fusion.plating.delivery',
string='Delivery',
ondelete='set null',
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Facility',
)
company_id = fields.Many2one(
'res.company',
related='facility_id.company_id',
store=True,
readonly=True,
)
recorded_by_id = fields.Many2one(
'res.users',
string='Recorded By',
default=lambda self: self.env.user,
)
notes = fields.Text(
string='Notes',
)
@api.depends('event_type', 'event_datetime', 'from_party', 'to_party')
def _compute_display_name(self):
selection = dict(self._fields['event_type']._description_selection(self.env))
for rec in self:
label = selection.get(rec.event_type, rec.event_type or '')
when = fields.Datetime.to_string(rec.event_datetime) if rec.event_datetime else ''
rec.display_name = f'{label} @ {when}' if when else label

View File

@@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class FpDelivery(models.Model):
"""Scheduled delivery of finished parts back to a customer.
Lifecycle:
draft → scheduled → en_route → delivered
→ refused
→ returned
→ cancelled
A delivery references a job by soft reference (`job_ref`) because
the job module is not yet built. Once the job module ships, this
field can be deprecated in favour of a proper Many2one without
migration.
"""
_name = 'fusion.plating.delivery'
_description = 'Fusion Plating — Delivery'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'scheduled_date desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
default=lambda self: self._default_name(),
tracking=True,
)
partner_id = fields.Many2one(
'res.partner',
string='Customer',
required=True,
tracking=True,
)
delivery_address_id = fields.Many2one(
'res.partner',
string='Delivery Address',
help='Leave blank to use the customer default address.',
)
contact_name = fields.Char(
string='Contact Name',
)
contact_phone = fields.Char(
string='Contact Phone',
)
job_ref = fields.Char(
string='Job Reference',
help='Soft reference to the job this delivery belongs to. '
'Will become a Many2one once the job module ships.',
tracking=True,
)
scheduled_date = fields.Datetime(
string='Scheduled Date',
tracking=True,
)
assigned_driver_id = fields.Many2one(
'hr.employee',
string='Assigned Driver',
tracking=True,
domain=[('x_fc_is_driver', '=', True)],
)
vehicle_id = fields.Many2one(
'fusion.plating.vehicle',
string='Vehicle',
tracking=True,
)
source_facility_id = fields.Many2one(
'fusion.plating.facility',
string='Source Facility',
tracking=True,
)
company_id = fields.Many2one(
'res.company',
related='source_facility_id.company_id',
store=True,
readonly=True,
)
tdg_required = fields.Boolean(
string='TDG Required',
tracking=True,
)
coc_attachment_id = fields.Many2one(
'ir.attachment',
string='Certificate of Conformance',
)
packing_list_attachment_id = fields.Many2one(
'ir.attachment',
string='Packing List',
)
state = fields.Selection(
[
('draft', 'Draft'),
('scheduled', 'Scheduled'),
('en_route', 'En Route'),
('delivered', 'Delivered'),
('refused', 'Refused'),
('returned', 'Returned'),
('cancelled', 'Cancelled'),
],
string='Status',
default='draft',
required=True,
tracking=True,
)
delivered_at = fields.Datetime(
string='Delivered At',
readonly=True,
)
pod_id = fields.Many2one(
'fusion.plating.proof.of.delivery',
string='Proof of Delivery',
copy=False,
)
notes = fields.Html(
string='Notes',
)
custody_event_ids = fields.One2many(
'fusion.plating.chain.of.custody',
'delivery_id',
string='Custody Events',
)
custody_event_count = fields.Integer(
compute='_compute_custody_count',
)
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.delivery')
return seq or '/'
def _compute_custody_count(self):
for rec in self:
rec.custody_event_count = len(rec.custody_event_ids)
def _log_custody_event(self, event_type, from_party=None, to_party=None):
self.ensure_one()
self.env['fusion.plating.chain.of.custody'].create({
'event_datetime': fields.Datetime.now(),
'event_type': event_type,
'from_party': from_party or '',
'to_party': to_party or '',
'delivery_id': self.id,
'facility_id': self.source_facility_id.id,
'recorded_by_id': self.env.user.id,
})
# ==========================================================================
# Actions
# ==========================================================================
def action_schedule(self):
self.write({'state': 'scheduled'})
def action_start_route(self):
for rec in self:
rec.write({'state': 'en_route'})
rec._log_custody_event(
'loaded_on_vehicle',
from_party=(rec.source_facility_id.display_name or 'Facility'),
to_party=(rec.assigned_driver_id.display_name
or rec.vehicle_id.display_name
or 'Driver'),
)
def action_mark_delivered(self):
for rec in self:
rec.write({
'state': 'delivered',
'delivered_at': fields.Datetime.now(),
})
rec._log_custody_event(
'delivered_to_customer',
from_party=(rec.assigned_driver_id.display_name
or rec.vehicle_id.display_name
or 'Driver'),
to_party=rec.partner_id.display_name,
)
def action_mark_refused(self):
self.write({'state': 'refused'})
def action_mark_returned(self):
self.write({'state': 'returned'})
def action_cancel(self):
self.write({'state': 'cancelled'})
def action_reset_to_draft(self):
self.write({'state': 'draft'})
def action_create_pod(self):
"""Create a blank POD record for this delivery and open it."""
self.ensure_one()
pod = self.env['fusion.plating.proof.of.delivery'].create({
'delivery_id': self.id,
'delivered_at': fields.Datetime.now(),
})
self.pod_id = pod.id
return {
'type': 'ir.actions.act_window',
'res_model': 'fusion.plating.proof.of.delivery',
'res_id': pod.id,
'view_mode': 'form',
'target': 'current',
}

View File

@@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class FpPickupRequest(models.Model):
"""Customer-initiated request for pickup of parts to be processed.
Lifecycle:
new → scheduled → en_route → picked_up → received → (cancelled)
A pickup request is created when a customer phones or emails asking
for parts to be collected. Dispatch schedules a driver + vehicle,
the driver updates status on the road, and the receiving facility
confirms arrival of the parts. Chain of custody events are written
automatically as the state transitions.
"""
_name = 'fusion.plating.pickup.request'
_description = 'Fusion Plating — Pickup Request'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'requested_date desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
default=lambda self: self._default_name(),
tracking=True,
)
partner_id = fields.Many2one(
'res.partner',
string='Customer',
required=True,
tracking=True,
)
pickup_address_id = fields.Many2one(
'res.partner',
string='Pickup Address',
help='Leave blank to use the customer default address.',
)
contact_name = fields.Char(
string='Contact Name',
)
contact_phone = fields.Char(
string='Contact Phone',
)
requested_date = fields.Datetime(
string='Requested Date',
default=fields.Datetime.now,
tracking=True,
)
scheduled_date = fields.Datetime(
string='Scheduled Date',
tracking=True,
)
assigned_driver_id = fields.Many2one(
'hr.employee',
string='Assigned Driver',
tracking=True,
domain=[('x_fc_is_driver', '=', True)],
)
vehicle_id = fields.Many2one(
'fusion.plating.vehicle',
string='Vehicle',
tracking=True,
)
destination_facility_id = fields.Many2one(
'fusion.plating.facility',
string='Destination Facility',
tracking=True,
)
company_id = fields.Many2one(
'res.company',
related='destination_facility_id.company_id',
store=True,
readonly=True,
)
item_description = fields.Html(
string='Items to Pick Up',
)
estimated_weight_kg = fields.Float(
string='Est. Weight (kg)',
)
tdg_required = fields.Boolean(
string='TDG Required',
tracking=True,
help='Check if the load qualifies as Transportation of Dangerous '
'Goods. Only TDG-certified drivers and vehicles may be '
'assigned.',
)
state = fields.Selection(
[
('new', 'New'),
('scheduled', 'Scheduled'),
('en_route', 'En Route'),
('picked_up', 'Picked Up'),
('received', 'Received at Facility'),
('cancelled', 'Cancelled'),
],
string='Status',
default='new',
required=True,
tracking=True,
)
picked_up_at = fields.Datetime(
string='Picked Up At',
readonly=True,
)
received_at = fields.Datetime(
string='Received At',
readonly=True,
)
notes = fields.Html(
string='Notes',
)
custody_event_ids = fields.One2many(
'fusion.plating.chain.of.custody',
'pickup_request_id',
string='Custody Events',
)
custody_event_count = fields.Integer(
compute='_compute_custody_count',
)
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.pickup.request')
return seq or '/'
def _compute_custody_count(self):
for rec in self:
rec.custody_event_count = len(rec.custody_event_ids)
def _log_custody_event(self, event_type, from_party=None, to_party=None):
self.ensure_one()
self.env['fusion.plating.chain.of.custody'].create({
'event_datetime': fields.Datetime.now(),
'event_type': event_type,
'from_party': from_party or '',
'to_party': to_party or '',
'pickup_request_id': self.id,
'facility_id': self.destination_facility_id.id,
'recorded_by_id': self.env.user.id,
})
# ==========================================================================
# Actions
# ==========================================================================
def action_schedule(self):
self.write({'state': 'scheduled'})
def action_start_route(self):
self.write({'state': 'en_route'})
def action_mark_picked_up(self):
for rec in self:
rec.write({
'state': 'picked_up',
'picked_up_at': fields.Datetime.now(),
})
rec._log_custody_event(
'received_from_customer',
from_party=rec.partner_id.display_name,
to_party=(rec.assigned_driver_id.display_name
or rec.vehicle_id.display_name
or 'Driver'),
)
def action_mark_received(self):
for rec in self:
rec.write({
'state': 'received',
'received_at': fields.Datetime.now(),
})
rec._log_custody_event(
'entered_facility',
from_party=(rec.assigned_driver_id.display_name
or rec.vehicle_id.display_name
or 'Driver'),
to_party=(rec.destination_facility_id.display_name
or 'Facility'),
)
def action_cancel(self):
self.write({'state': 'cancelled'})
def action_reset_to_new(self):
self.write({'state': 'new'})

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class FpProofOfDelivery(models.Model):
"""Proof of delivery record — captured at the delivery point.
Captures:
* recipient name
* recipient signature (binary image)
* photos of the delivered load
* GPS coordinates at time of capture
* delivery timestamp
A POD is typically created via the delivery's "Create POD" action,
which pre-fills the delivery_id and timestamps the record.
"""
_name = 'fusion.plating.proof.of.delivery'
_description = 'Fusion Plating — Proof of Delivery'
_order = 'delivered_at desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
default=lambda self: self._default_name(),
)
delivery_id = fields.Many2one(
'fusion.plating.delivery',
string='Delivery',
ondelete='cascade',
)
partner_id = fields.Many2one(
'res.partner',
related='delivery_id.partner_id',
store=True,
readonly=True,
)
recipient_name = fields.Char(
string='Recipient Name',
)
recipient_signature = fields.Binary(
string='Signature',
attachment=True,
)
delivered_at = fields.Datetime(
string='Delivered At',
default=fields.Datetime.now,
)
photo_ids = fields.Many2many(
'ir.attachment',
'fp_pod_photo_rel',
'pod_id',
'attachment_id',
string='Photos',
)
notes = fields.Text(
string='Notes',
)
gps_lat = fields.Float(
string='GPS Latitude',
digits=(9, 6),
)
gps_lon = fields.Float(
string='GPS Longitude',
digits=(9, 6),
)
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.proof.of.delivery')
return seq or '/'

View File

@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import api, fields, models
class FpRoute(models.Model):
"""A single run combining pickups and deliveries for one driver.
Lifecycle:
draft → planned → in_progress → completed → (cancelled)
Dispatch drops pickup requests and deliveries on the route as stops,
re-orders them using the `sequence` handle, then hands the route to
the driver. The driver ticks off stops as they work, and the route
captures total distance (km) and elapsed time for costing.
"""
_name = 'fusion.plating.route'
_description = 'Fusion Plating — Route'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'route_date desc, id desc'
name = fields.Char(
string='Reference',
required=True,
copy=False,
default=lambda self: self._default_name(),
tracking=True,
)
route_date = fields.Date(
string='Route Date',
required=True,
default=fields.Date.context_today,
tracking=True,
)
driver_id = fields.Many2one(
'hr.employee',
string='Driver',
required=True,
tracking=True,
domain=[('x_fc_is_driver', '=', True)],
)
vehicle_id = fields.Many2one(
'fusion.plating.vehicle',
string='Vehicle',
required=True,
tracking=True,
)
company_id = fields.Many2one(
'res.company',
related='vehicle_id.company_id',
store=True,
readonly=True,
)
state = fields.Selection(
[
('draft', 'Draft'),
('planned', 'Planned'),
('in_progress', 'In Progress'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
],
string='Status',
default='draft',
required=True,
tracking=True,
)
start_time = fields.Datetime(
string='Start Time',
)
end_time = fields.Datetime(
string='End Time',
)
total_km = fields.Float(
string='Total Distance (km)',
)
stop_ids = fields.One2many(
'fusion.plating.route.stop',
'route_id',
string='Stops',
)
stop_count = fields.Integer(
compute='_compute_stop_count',
)
notes = fields.Html(
string='Notes',
)
@api.model
def _default_name(self):
seq = self.env['ir.sequence'].next_by_code('fusion.plating.route')
return seq or '/'
def _compute_stop_count(self):
for rec in self:
rec.stop_count = len(rec.stop_ids)
# ==========================================================================
# Actions
# ==========================================================================
def action_plan(self):
self.write({'state': 'planned'})
def action_start(self):
self.write({
'state': 'in_progress',
'start_time': fields.Datetime.now(),
})
def action_complete(self):
self.write({
'state': 'completed',
'end_time': fields.Datetime.now(),
})
def action_cancel(self):
self.write({'state': 'cancelled'})
def action_reset_to_draft(self):
self.write({'state': 'draft'})

View File

@@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
class FpRouteStop(models.Model):
"""A single stop on a route.
A stop is one of: pickup, delivery, transfer (between facilities),
fuel, or break. Pickup and delivery stops link back to the source
records so the driver can open them from the route. Non-transactional
stops (fuel, break) just carry an address and a planned time.
"""
_name = 'fusion.plating.route.stop'
_description = 'Fusion Plating — Route Stop'
_order = 'route_id, sequence, id'
route_id = fields.Many2one(
'fusion.plating.route',
string='Route',
required=True,
ondelete='cascade',
)
sequence = fields.Integer(
string='Sequence',
default=10,
)
stop_type = fields.Selection(
[
('pickup', 'Pickup'),
('delivery', 'Delivery'),
('transfer', 'Facility Transfer'),
('fuel', 'Fuel'),
('break', 'Break'),
],
string='Stop Type',
default='pickup',
required=True,
)
pickup_request_id = fields.Many2one(
'fusion.plating.pickup.request',
string='Pickup Request',
)
delivery_id = fields.Many2one(
'fusion.plating.delivery',
string='Delivery',
)
address_id = fields.Many2one(
'res.partner',
string='Address',
)
planned_time = fields.Datetime(
string='Planned Time',
)
actual_time = fields.Datetime(
string='Actual Time',
)
state = fields.Selection(
[
('pending', 'Pending'),
('arrived', 'Arrived'),
('completed', 'Completed'),
('skipped', 'Skipped'),
],
string='Status',
default='pending',
required=True,
)
notes = fields.Text(
string='Notes',
)
def action_mark_arrived(self):
self.write({
'state': 'arrived',
'actual_time': fields.Datetime.now(),
})
def action_mark_completed(self):
self.write({'state': 'completed'})
def action_mark_skipped(self):
self.write({'state': 'skipped'})

View File

@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
class FpVehicle(models.Model):
"""Vehicle master for the plating shop's pickup & delivery fleet.
A vehicle belongs to a facility (its home base), carries insurance,
registration and service dates, and tracks its current driver and
state. Vehicles flagged `tdg_certified` are approved to carry TDG
(Transportation of Dangerous Goods) loads — e.g. stripped parts with
residue, waste drums, spent chemistry.
The module works without Odoo's Enterprise Fleet module: all
fleet-level concerns are modelled here so that CE-only installations
are fully supported.
"""
_name = 'fusion.plating.vehicle'
_description = 'Fusion Plating — Vehicle'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'name'
name = fields.Char(
string='Vehicle',
required=True,
tracking=True,
help='Internal name — e.g. "Van 1" or "Box Truck 07".',
)
license_plate = fields.Char(
string='License Plate',
required=True,
tracking=True,
)
make = fields.Char(
string='Make',
)
model_year = fields.Integer(
string='Model Year',
)
vehicle_type = fields.Selection(
[
('van', 'Van'),
('truck_box', 'Box Truck'),
('truck_pickup', 'Pickup Truck'),
('flatbed', 'Flatbed'),
('trailer', 'Trailer'),
('other', 'Other'),
],
string='Type',
default='van',
required=True,
tracking=True,
)
capacity_kg = fields.Float(
string='Capacity (kg)',
help='Maximum payload capacity in kilograms.',
)
facility_id = fields.Many2one(
'fusion.plating.facility',
string='Home Facility',
tracking=True,
)
company_id = fields.Many2one(
'res.company',
related='facility_id.company_id',
store=True,
readonly=True,
)
active = fields.Boolean(
default=True,
)
insurance_expiry = fields.Date(
string='Insurance Expiry',
tracking=True,
)
registration_expiry = fields.Date(
string='Registration Expiry',
tracking=True,
)
next_service_date = fields.Date(
string='Next Service',
tracking=True,
)
state = fields.Selection(
[
('available', 'Available'),
('in_use', 'In Use'),
('maintenance', 'Maintenance'),
('out_of_service', 'Out of Service'),
],
string='Status',
default='available',
required=True,
tracking=True,
)
current_driver_id = fields.Many2one(
'hr.employee',
string='Current Driver',
tracking=True,
domain=[('x_fc_is_driver', '=', True)],
)
tdg_certified = fields.Boolean(
string='TDG Certified',
tracking=True,
help='Vehicle is approved to carry Transportation of Dangerous '
'Goods loads.',
)
notes = fields.Text(
string='Notes',
)
_sql_constraints = [
(
'fp_vehicle_license_plate_uniq',
'unique(license_plate)',
'License plate must be unique.',
),
]
def action_mark_available(self):
self.write({'state': 'available'})
def action_mark_in_use(self):
self.write({'state': 'in_use'})
def action_mark_maintenance(self):
self.write({'state': 'maintenance'})
def action_mark_out_of_service(self):
self.write({'state': 'out_of_service'})

View File

@@ -0,0 +1,61 @@
# -*- coding: utf-8 -*-
# Copyright 2026 Nexa Systems Inc.
# License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family.
from odoo import fields, models
class HrEmployee(models.Model):
"""Extend hr.employee with driver fields.
Drivers are just employees with the `x_fc_is_driver` flag set. This
avoids creating a parallel driver model and keeps time tracking, HR
records, and payroll integrations working out of the box.
Uses the `x_fc_` prefix per the Fusion Central field naming
convention for extensions of base Odoo models.
"""
_inherit = 'hr.employee'
x_fc_is_driver = fields.Boolean(
string='Is Driver',
help='Check if this employee is authorised to drive company vehicles '
'for pickups and deliveries.',
)
x_fc_driver_licence_class = fields.Char(
string='Licence Class',
help='Driver licence class — e.g. G, G2, AZ, DZ.',
)
x_fc_licence_expiry = fields.Date(
string='Licence Expiry',
)
x_fc_tdg_certified = fields.Boolean(
string='TDG Certified',
help='Certified to transport Transportation of Dangerous Goods '
'(hazmat) loads.',
)
x_fc_tdg_expiry = fields.Date(
string='TDG Certificate Expiry',
)
x_fc_pickup_request_ids = fields.One2many(
'fusion.plating.pickup.request',
'assigned_driver_id',
string='Pickup Requests',
)
x_fc_delivery_ids = fields.One2many(
'fusion.plating.delivery',
'assigned_driver_id',
string='Deliveries',
)
x_fc_pickup_request_count = fields.Integer(
compute='_compute_logistics_counts',
)
x_fc_delivery_count = fields.Integer(
compute='_compute_logistics_counts',
)
def _compute_logistics_counts(self):
for rec in self:
rec.x_fc_pickup_request_count = len(rec.x_fc_pickup_request_ids)
rec.x_fc_delivery_count = len(rec.x_fc_delivery_ids)

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<!-- ================================================================== -->
<!-- RECORD RULES — Multi-company isolation -->
<!-- Reuses the core fusion_plating groups (operator / supervisor / -->
<!-- manager / admin). No new groups are defined here. -->
<!-- ================================================================== -->
<record id="fp_vehicle_company_rule" model="ir.rule">
<field name="name">Fusion Plating: Vehicle — multi-company</field>
<field name="model_id" ref="model_fusion_plating_vehicle"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_pickup_request_company_rule" model="ir.rule">
<field name="name">Fusion Plating: Pickup Request — multi-company</field>
<field name="model_id" ref="model_fusion_plating_pickup_request"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_delivery_company_rule" model="ir.rule">
<field name="name">Fusion Plating: Delivery — multi-company</field>
<field name="model_id" ref="model_fusion_plating_delivery"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_route_company_rule" model="ir.rule">
<field name="name">Fusion Plating: Route — multi-company</field>
<field name="model_id" ref="model_fusion_plating_route"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
<record id="fp_chain_of_custody_company_rule" model="ir.rule">
<field name="name">Fusion Plating: Chain of Custody — multi-company</field>
<field name="model_id" ref="model_fusion_plating_chain_of_custody"/>
<field name="global" eval="True"/>
<field name="domain_force">['|', ('company_id', '=', False), ('company_id', 'in', company_ids)]</field>
</record>
</odoo>

View File

@@ -0,0 +1,22 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_fp_vehicle_operator,fp.vehicle.operator,model_fusion_plating_vehicle,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_vehicle_supervisor,fp.vehicle.supervisor,model_fusion_plating_vehicle,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_vehicle_manager,fp.vehicle.manager,model_fusion_plating_vehicle,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_pickup_request_operator,fp.pickup.request.operator,model_fusion_plating_pickup_request,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_pickup_request_supervisor,fp.pickup.request.supervisor,model_fusion_plating_pickup_request,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_pickup_request_manager,fp.pickup.request.manager,model_fusion_plating_pickup_request,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_delivery_operator,fp.delivery.operator,model_fusion_plating_delivery,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_delivery_supervisor,fp.delivery.supervisor,model_fusion_plating_delivery,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_delivery_manager,fp.delivery.manager,model_fusion_plating_delivery,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_route_operator,fp.route.operator,model_fusion_plating_route,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_route_supervisor,fp.route.supervisor,model_fusion_plating_route,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_route_manager,fp.route.manager,model_fusion_plating_route,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_route_stop_operator,fp.route.stop.operator,model_fusion_plating_route_stop,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_route_stop_supervisor,fp.route.stop.supervisor,model_fusion_plating_route_stop,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_route_stop_manager,fp.route.stop.manager,model_fusion_plating_route_stop,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_chain_of_custody_operator,fp.chain.of.custody.operator,model_fusion_plating_chain_of_custody,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_chain_of_custody_supervisor,fp.chain.of.custody.supervisor,model_fusion_plating_chain_of_custody,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_chain_of_custody_manager,fp.chain.of.custody.manager,model_fusion_plating_chain_of_custody,fusion_plating.group_fusion_plating_manager,1,1,1,1
access_fp_proof_of_delivery_operator,fp.proof.of.delivery.operator,model_fusion_plating_proof_of_delivery,fusion_plating.group_fusion_plating_operator,1,0,0,0
access_fp_proof_of_delivery_supervisor,fp.proof.of.delivery.supervisor,model_fusion_plating_proof_of_delivery,fusion_plating.group_fusion_plating_supervisor,1,1,1,0
access_fp_proof_of_delivery_manager,fp.proof.of.delivery.manager,model_fusion_plating_proof_of_delivery,fusion_plating.group_fusion_plating_manager,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_fp_vehicle_operator fp.vehicle.operator model_fusion_plating_vehicle fusion_plating.group_fusion_plating_operator 1 0 0 0
3 access_fp_vehicle_supervisor fp.vehicle.supervisor model_fusion_plating_vehicle fusion_plating.group_fusion_plating_supervisor 1 1 1 0
4 access_fp_vehicle_manager fp.vehicle.manager model_fusion_plating_vehicle fusion_plating.group_fusion_plating_manager 1 1 1 1
5 access_fp_pickup_request_operator fp.pickup.request.operator model_fusion_plating_pickup_request fusion_plating.group_fusion_plating_operator 1 0 0 0
6 access_fp_pickup_request_supervisor fp.pickup.request.supervisor model_fusion_plating_pickup_request fusion_plating.group_fusion_plating_supervisor 1 1 1 0
7 access_fp_pickup_request_manager fp.pickup.request.manager model_fusion_plating_pickup_request fusion_plating.group_fusion_plating_manager 1 1 1 1
8 access_fp_delivery_operator fp.delivery.operator model_fusion_plating_delivery fusion_plating.group_fusion_plating_operator 1 0 0 0
9 access_fp_delivery_supervisor fp.delivery.supervisor model_fusion_plating_delivery fusion_plating.group_fusion_plating_supervisor 1 1 1 0
10 access_fp_delivery_manager fp.delivery.manager model_fusion_plating_delivery fusion_plating.group_fusion_plating_manager 1 1 1 1
11 access_fp_route_operator fp.route.operator model_fusion_plating_route fusion_plating.group_fusion_plating_operator 1 0 0 0
12 access_fp_route_supervisor fp.route.supervisor model_fusion_plating_route fusion_plating.group_fusion_plating_supervisor 1 1 1 0
13 access_fp_route_manager fp.route.manager model_fusion_plating_route fusion_plating.group_fusion_plating_manager 1 1 1 1
14 access_fp_route_stop_operator fp.route.stop.operator model_fusion_plating_route_stop fusion_plating.group_fusion_plating_operator 1 0 0 0
15 access_fp_route_stop_supervisor fp.route.stop.supervisor model_fusion_plating_route_stop fusion_plating.group_fusion_plating_supervisor 1 1 1 0
16 access_fp_route_stop_manager fp.route.stop.manager model_fusion_plating_route_stop fusion_plating.group_fusion_plating_manager 1 1 1 1
17 access_fp_chain_of_custody_operator fp.chain.of.custody.operator model_fusion_plating_chain_of_custody fusion_plating.group_fusion_plating_operator 1 0 0 0
18 access_fp_chain_of_custody_supervisor fp.chain.of.custody.supervisor model_fusion_plating_chain_of_custody fusion_plating.group_fusion_plating_supervisor 1 1 1 0
19 access_fp_chain_of_custody_manager fp.chain.of.custody.manager model_fusion_plating_chain_of_custody fusion_plating.group_fusion_plating_manager 1 1 1 1
20 access_fp_proof_of_delivery_operator fp.proof.of.delivery.operator model_fusion_plating_proof_of_delivery fusion_plating.group_fusion_plating_operator 1 0 0 0
21 access_fp_proof_of_delivery_supervisor fp.proof.of.delivery.supervisor model_fusion_plating_proof_of_delivery fusion_plating.group_fusion_plating_supervisor 1 1 1 0
22 access_fp_proof_of_delivery_manager fp.proof.of.delivery.manager model_fusion_plating_proof_of_delivery fusion_plating.group_fusion_plating_manager 1 1 1 1

View File

@@ -0,0 +1,157 @@
// =============================================================================
// Fusion Plating — Logistics backend styles
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// THEME AWARENESS
// ---------------
// Matches the core fusion_plating module approach: no hardcoded backgrounds
// or text colours. All surface colours come from Odoo / Bootstrap CSS custom
// properties so the components render correctly in BOTH light and dark mode:
//
// background: var(--bs-body-bg)
// surface: var(--o-view-background-color)
// foreground: var(--bs-body-color)
// muted text: var(--bs-secondary-color)
// border: var(--bs-border-color)
// primary: var(--o-action)
//
// Semantic status colours use color-mix() against the Bootstrap tokens so
// badges adapt to light / dark automatically.
// =============================================================================
// -----------------------------------------------------------------------------
// Local helper — tint a badge against a semantic colour var
// -----------------------------------------------------------------------------
@mixin fpl-tint($color-var, $amount: 12%) {
background-color: color-mix(in srgb, var(#{$color-var}) #{$amount}, transparent);
color: var(#{$color-var});
border: 1px solid color-mix(in srgb, var(#{$color-var}) 35%, transparent);
}
// -----------------------------------------------------------------------------
// Vehicle kanban — state-aware card with a left-border accent
// -----------------------------------------------------------------------------
.o_fp_vehicle_kanban {
.o_fp_vehicle_card {
border-left-width: 4px;
&[data-state="available"] {
border-left-color: var(--bs-success);
}
&[data-state="in_use"] {
border-left-color: var(--bs-info, var(--o-action));
}
&[data-state="maintenance"] {
border-left-color: var(--bs-warning);
}
&[data-state="out_of_service"] {
border-left-color: var(--bs-secondary-color);
}
}
.o_fp_badge {
display: inline-block;
padding: 2px 8px;
font-size: 0.72rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.02em;
border-radius: 999px;
&[data-state="available"] {
@include fpl-tint(--bs-success);
}
&[data-state="in_use"] {
@include fpl-tint(--bs-info);
}
&[data-state="maintenance"] {
@include fpl-tint(--bs-warning);
}
&[data-state="out_of_service"] {
@include fpl-tint(--bs-secondary-color);
}
&[data-state="tdg"] {
@include fpl-tint(--bs-danger);
}
}
}
// -----------------------------------------------------------------------------
// Pickup / Delivery kanbans — share the TDG badge styling
// -----------------------------------------------------------------------------
.o_fp_pickup_kanban,
.o_fp_delivery_kanban {
.o_fp_badge {
display: inline-block;
padding: 2px 8px;
font-size: 0.72rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.02em;
border-radius: 999px;
&[data-state="tdg"] {
@include fpl-tint(--bs-danger);
}
}
}
// -----------------------------------------------------------------------------
// Route stop — inline status dot (used when rendered outside a kanban)
// -----------------------------------------------------------------------------
.o_fp_route_stop {
display: flex;
align-items: center;
gap: 8px;
color: var(--bs-body-color);
.o_fp_route_stop_dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--bs-secondary-color);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-secondary-color) 25%, transparent);
&[data-status="pending"] {
background-color: var(--bs-secondary-color);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-secondary-color) 25%, transparent);
}
&[data-status="arrived"] {
background-color: var(--bs-warning);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-warning) 25%, transparent);
}
&[data-status="completed"] {
background-color: var(--bs-success);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-success) 25%, transparent);
}
&[data-status="skipped"] {
background-color: var(--bs-danger);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--bs-danger) 25%, transparent);
}
}
}
// -----------------------------------------------------------------------------
// Proof of Delivery — signature capture surface
// -----------------------------------------------------------------------------
.o_fp_pod_signature_box {
background-color: var(--o-view-background-color, var(--bs-body-bg));
border: 1px dashed var(--bs-border-color);
border-radius: 10px;
padding: 12px;
min-height: 160px;
img {
max-width: 100%;
max-height: 150px;
}
}

View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_chain_of_custody_list" model="ir.ui.view">
<field name="name">fp.chain.of.custody.list</field>
<field name="model">fusion.plating.chain.of.custody</field>
<field name="arch" type="xml">
<list string="Chain of Custody" create="false">
<field name="event_datetime"/>
<field name="event_type"/>
<field name="from_party"/>
<field name="to_party"/>
<field name="pickup_request_id"/>
<field name="delivery_id"/>
<field name="facility_id"/>
<field name="recorded_by_id"/>
</list>
</field>
</record>
<record id="view_fp_chain_of_custody_form" model="ir.ui.view">
<field name="name">fp.chain.of.custody.form</field>
<field name="model">fusion.plating.chain.of.custody</field>
<field name="arch" type="xml">
<form string="Custody Event">
<sheet>
<group>
<group>
<field name="event_datetime"/>
<field name="event_type"/>
<field name="facility_id"/>
<field name="recorded_by_id" readonly="1"/>
</group>
<group>
<field name="from_party"/>
<field name="to_party"/>
<field name="pickup_request_id"/>
<field name="delivery_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<group string="Notes">
<field name="notes" nolabel="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record id="view_fp_chain_of_custody_search" model="ir.ui.view">
<field name="name">fp.chain.of.custody.search</field>
<field name="model">fusion.plating.chain.of.custody</field>
<field name="arch" type="xml">
<search string="Chain of Custody">
<field name="from_party"/>
<field name="to_party"/>
<field name="pickup_request_id"/>
<field name="delivery_id"/>
<field name="facility_id"/>
<filter name="filter_pickups" string="Pickups Only" domain="[('pickup_request_id','!=',False)]"/>
<filter name="filter_deliveries" string="Deliveries Only" domain="[('delivery_id','!=',False)]"/>
<group>
<filter name="group_event_type" string="Event Type" context="{'group_by':'event_type'}"/>
<filter name="group_facility" string="Facility" context="{'group_by':'facility_id'}"/>
<filter name="group_date" string="Date" context="{'group_by':'event_datetime:day'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_chain_of_custody" model="ir.actions.act_window">
<field name="name">Chain of Custody</field>
<field name="res_model">fusion.plating.chain.of.custody</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Custody events will appear here
</p>
<p>
Custody events are created automatically as pickup requests
and deliveries move through their lifecycle. They form the
evidence trail for compliance audits.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,200 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_delivery_list" model="ir.ui.view">
<field name="name">fp.delivery.list</field>
<field name="model">fusion.plating.delivery</field>
<field name="arch" type="xml">
<list string="Deliveries"
decoration-muted="state in ('cancelled','returned')"
decoration-danger="state == 'refused'"
decoration-success="state == 'delivered'">
<field name="name"/>
<field name="partner_id"/>
<field name="job_ref"/>
<field name="scheduled_date"/>
<field name="assigned_driver_id"/>
<field name="vehicle_id"/>
<field name="source_facility_id"/>
<field name="tdg_required" widget="boolean_toggle" optional="show"/>
<field name="state" widget="badge"
decoration-info="state == 'draft'"
decoration-warning="state == 'scheduled'"
decoration-primary="state == 'en_route'"
decoration-success="state == 'delivered'"
decoration-danger="state == 'refused'"
decoration-muted="state in ('returned','cancelled')"/>
</list>
</field>
</record>
<record id="view_fp_delivery_form" model="ir.ui.view">
<field name="name">fp.delivery.form</field>
<field name="model">fusion.plating.delivery</field>
<field name="arch" type="xml">
<form string="Delivery">
<header>
<button name="action_schedule" string="Schedule" type="object"
class="oe_highlight" invisible="state != 'draft'"/>
<button name="action_start_route" string="Start Route" type="object"
invisible="state != 'scheduled'"/>
<button name="action_mark_delivered" string="Mark Delivered" type="object"
class="oe_highlight" invisible="state != 'en_route'"/>
<button name="action_create_pod" string="Create POD" type="object"
invisible="state not in ('en_route','delivered') or pod_id"/>
<button name="action_mark_refused" string="Refused" type="object"
invisible="state != 'en_route'"/>
<button name="action_mark_returned" string="Returned" type="object"
invisible="state not in ('refused','en_route')"/>
<button name="action_cancel" string="Cancel" type="object"
invisible="state in ('delivered','cancelled')"/>
<button name="action_reset_to_draft" string="Reset to Draft" type="object"
invisible="state != 'cancelled'"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,scheduled,en_route,delivered"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group string="Customer">
<field name="partner_id"/>
<field name="delivery_address_id"/>
<field name="contact_name"/>
<field name="contact_phone" widget="phone"/>
<field name="job_ref"/>
</group>
<group string="Schedule">
<field name="scheduled_date"/>
<field name="source_facility_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="delivered_at" readonly="1"/>
</group>
</group>
<group>
<group string="Assignment">
<field name="assigned_driver_id"/>
<field name="vehicle_id"/>
<field name="tdg_required" widget="boolean_toggle"/>
</group>
<group string="Documents">
<field name="coc_attachment_id"/>
<field name="packing_list_attachment_id"/>
<field name="pod_id" readonly="1"/>
</group>
</group>
<notebook>
<page string="Notes">
<field name="notes"/>
</page>
<page string="Chain of Custody">
<field name="custody_event_ids" readonly="1">
<list>
<field name="event_datetime"/>
<field name="event_type"/>
<field name="from_party"/>
<field name="to_party"/>
<field name="recorded_by_id"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_delivery_kanban" model="ir.ui.view">
<field name="name">fp.delivery.kanban</field>
<field name="model">fusion.plating.delivery</field>
<field name="arch" type="xml">
<kanban default_group_by="state" class="o_fp_delivery_kanban">
<field name="id"/>
<field name="name"/>
<field name="partner_id"/>
<field name="scheduled_date"/>
<field name="assigned_driver_id"/>
<field name="vehicle_id"/>
<field name="tdg_required"/>
<field name="job_ref"/>
<templates>
<t t-name="card">
<div class="o_fp_card">
<div class="d-flex align-items-start justify-content-between">
<div>
<strong class="o_fp_card_title"><field name="name"/></strong>
<div class="small text-muted"><field name="partner_id"/></div>
</div>
<i class="fa fa-arrow-up text-muted" aria-hidden="true"/>
</div>
<div class="mt-2 small">
<field name="scheduled_date"/>
</div>
<div class="mt-1 small text-muted">
<field name="assigned_driver_id"/>
<span t-if="record.vehicle_id.raw_value"><field name="vehicle_id"/></span>
</div>
<div class="mt-2" t-if="record.tdg_required.raw_value">
<span class="o_fp_badge" data-state="tdg">TDG</span>
</div>
<div class="mt-1 small text-muted" t-if="record.job_ref.raw_value">
Job: <field name="job_ref"/>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_delivery_search" model="ir.ui.view">
<field name="name">fp.delivery.search</field>
<field name="model">fusion.plating.delivery</field>
<field name="arch" type="xml">
<search string="Deliveries">
<field name="name"/>
<field name="partner_id"/>
<field name="assigned_driver_id"/>
<field name="vehicle_id"/>
<field name="job_ref"/>
<field name="source_facility_id"/>
<filter name="filter_draft" string="Draft" domain="[('state','=','draft')]"/>
<filter name="filter_scheduled" string="Scheduled" domain="[('state','=','scheduled')]"/>
<filter name="filter_en_route" string="En Route" domain="[('state','=','en_route')]"/>
<filter name="filter_delivered" string="Delivered" domain="[('state','=','delivered')]"/>
<filter name="filter_tdg" string="TDG Only" domain="[('tdg_required','=',True)]"/>
<group>
<filter name="group_state" string="Status" context="{'group_by':'state'}"/>
<filter name="group_driver" string="Driver" context="{'group_by':'assigned_driver_id'}"/>
<filter name="group_facility" string="Source Facility" context="{'group_by':'source_facility_id'}"/>
<filter name="group_partner" string="Customer" context="{'group_by':'partner_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_delivery" model="ir.actions.act_window">
<field name="name">Deliveries</field>
<field name="res_model">fusion.plating.delivery</field>
<field name="view_mode">kanban,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No deliveries yet
</p>
<p>
Schedule deliveries of finished parts back to your customers.
Attach a Certificate of Conformance and packing list, track
chain of custody, and capture proof of delivery on site.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<!-- ===== LOGISTICS (top-level under the core Plating app) ===== -->
<menuitem id="menu_fp_logistics"
name="Logistics"
parent="fusion_plating.menu_fp_root"
sequence="50"
groups="fusion_plating.group_fusion_plating_operator"/>
<menuitem id="menu_fp_pickup_requests"
name="Pickup Requests"
parent="menu_fp_logistics"
action="action_fp_pickup_request"
sequence="10"/>
<menuitem id="menu_fp_deliveries"
name="Deliveries"
parent="menu_fp_logistics"
action="action_fp_delivery"
sequence="20"/>
<menuitem id="menu_fp_routes"
name="Routes"
parent="menu_fp_logistics"
action="action_fp_route"
sequence="30"/>
<menuitem id="menu_fp_chain_of_custody"
name="Chain of Custody"
parent="menu_fp_logistics"
action="action_fp_chain_of_custody"
sequence="40"/>
<menuitem id="menu_fp_proof_of_delivery"
name="Proof of Delivery"
parent="menu_fp_logistics"
action="action_fp_proof_of_delivery"
sequence="50"/>
<!-- ===== VEHICLES under Configuration ===== -->
<menuitem id="menu_fp_vehicles"
name="Vehicles"
parent="fusion_plating.menu_fp_config"
action="action_fp_vehicle"
sequence="60"/>
</odoo>

View File

@@ -0,0 +1,193 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_pickup_request_list" model="ir.ui.view">
<field name="name">fp.pickup.request.list</field>
<field name="model">fusion.plating.pickup.request</field>
<field name="arch" type="xml">
<list string="Pickup Requests"
decoration-muted="state == 'cancelled'"
decoration-info="state == 'new'"
decoration-success="state == 'received'">
<field name="name"/>
<field name="partner_id"/>
<field name="requested_date"/>
<field name="scheduled_date"/>
<field name="assigned_driver_id"/>
<field name="vehicle_id"/>
<field name="destination_facility_id"/>
<field name="tdg_required" widget="boolean_toggle" optional="show"/>
<field name="state" widget="badge"
decoration-info="state == 'new'"
decoration-warning="state == 'scheduled'"
decoration-primary="state == 'en_route'"
decoration-success="state in ('picked_up','received')"
decoration-muted="state == 'cancelled'"/>
</list>
</field>
</record>
<record id="view_fp_pickup_request_form" model="ir.ui.view">
<field name="name">fp.pickup.request.form</field>
<field name="model">fusion.plating.pickup.request</field>
<field name="arch" type="xml">
<form string="Pickup Request">
<header>
<button name="action_schedule" string="Schedule" type="object"
class="oe_highlight" invisible="state != 'new'"/>
<button name="action_start_route" string="Start Route" type="object"
invisible="state != 'scheduled'"/>
<button name="action_mark_picked_up" string="Mark Picked Up" type="object"
invisible="state != 'en_route'"/>
<button name="action_mark_received" string="Mark Received" type="object"
class="oe_highlight" invisible="state != 'picked_up'"/>
<button name="action_cancel" string="Cancel" type="object"
invisible="state in ('received','cancelled')"/>
<button name="action_reset_to_new" string="Reset to New" type="object"
invisible="state != 'cancelled'"/>
<field name="state" widget="statusbar"
statusbar_visible="new,scheduled,en_route,picked_up,received"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group string="Customer">
<field name="partner_id"/>
<field name="pickup_address_id"/>
<field name="contact_name"/>
<field name="contact_phone" widget="phone"/>
</group>
<group string="Schedule">
<field name="requested_date"/>
<field name="scheduled_date"/>
<field name="destination_facility_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<group>
<group string="Assignment">
<field name="assigned_driver_id"/>
<field name="vehicle_id"/>
</group>
<group string="Load">
<field name="estimated_weight_kg"/>
<field name="tdg_required" widget="boolean_toggle"/>
<field name="picked_up_at" readonly="1"/>
<field name="received_at" readonly="1"/>
</group>
</group>
<notebook>
<page string="Items">
<field name="item_description" placeholder="Describe the parts to be picked up..."/>
</page>
<page string="Notes">
<field name="notes"/>
</page>
<page string="Chain of Custody">
<field name="custody_event_ids" readonly="1">
<list>
<field name="event_datetime"/>
<field name="event_type"/>
<field name="from_party"/>
<field name="to_party"/>
<field name="recorded_by_id"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_pickup_request_kanban" model="ir.ui.view">
<field name="name">fp.pickup.request.kanban</field>
<field name="model">fusion.plating.pickup.request</field>
<field name="arch" type="xml">
<kanban default_group_by="state" class="o_fp_pickup_kanban">
<field name="id"/>
<field name="name"/>
<field name="partner_id"/>
<field name="scheduled_date"/>
<field name="assigned_driver_id"/>
<field name="vehicle_id"/>
<field name="tdg_required"/>
<field name="state"/>
<templates>
<t t-name="card">
<div class="o_fp_card">
<div class="d-flex align-items-start justify-content-between">
<div>
<strong class="o_fp_card_title"><field name="name"/></strong>
<div class="small text-muted"><field name="partner_id"/></div>
</div>
<i class="fa fa-arrow-down text-muted" aria-hidden="true"/>
</div>
<div class="mt-2 small">
<field name="scheduled_date"/>
</div>
<div class="mt-1 small text-muted">
<field name="assigned_driver_id"/>
<span t-if="record.vehicle_id.raw_value"><field name="vehicle_id"/></span>
</div>
<div class="mt-2" t-if="record.tdg_required.raw_value">
<span class="o_fp_badge" data-state="tdg">TDG</span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_pickup_request_search" model="ir.ui.view">
<field name="name">fp.pickup.request.search</field>
<field name="model">fusion.plating.pickup.request</field>
<field name="arch" type="xml">
<search string="Pickup Requests">
<field name="name"/>
<field name="partner_id"/>
<field name="assigned_driver_id"/>
<field name="vehicle_id"/>
<field name="destination_facility_id"/>
<filter name="filter_new" string="New" domain="[('state','=','new')]"/>
<filter name="filter_scheduled" string="Scheduled" domain="[('state','=','scheduled')]"/>
<filter name="filter_en_route" string="En Route" domain="[('state','=','en_route')]"/>
<filter name="filter_tdg" string="TDG Only" domain="[('tdg_required','=',True)]"/>
<group>
<filter name="group_state" string="Status" context="{'group_by':'state'}"/>
<filter name="group_driver" string="Driver" context="{'group_by':'assigned_driver_id'}"/>
<filter name="group_facility" string="Destination" context="{'group_by':'destination_facility_id'}"/>
<filter name="group_partner" string="Customer" context="{'group_by':'partner_id'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_pickup_request" model="ir.actions.act_window">
<field name="name">Pickup Requests</field>
<field name="res_model">fusion.plating.pickup.request</field>
<field name="view_mode">kanban,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No pickup requests yet
</p>
<p>
A pickup request is raised when a customer asks for parts
to be collected. Dispatch schedules a driver and vehicle,
and the chain of custody is logged automatically as the
request moves through its lifecycle.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_proof_of_delivery_list" model="ir.ui.view">
<field name="name">fp.proof.of.delivery.list</field>
<field name="model">fusion.plating.proof.of.delivery</field>
<field name="arch" type="xml">
<list string="Proof of Delivery">
<field name="name"/>
<field name="delivery_id"/>
<field name="partner_id"/>
<field name="recipient_name"/>
<field name="delivered_at"/>
</list>
</field>
</record>
<record id="view_fp_proof_of_delivery_form" model="ir.ui.view">
<field name="name">fp.proof.of.delivery.form</field>
<field name="model">fusion.plating.proof.of.delivery</field>
<field name="arch" type="xml">
<form string="Proof of Delivery">
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group>
<field name="delivery_id"/>
<field name="partner_id" readonly="1"/>
<field name="recipient_name"/>
<field name="delivered_at"/>
</group>
<group>
<field name="gps_lat"/>
<field name="gps_lon"/>
</group>
</group>
<group string="Signature">
<div class="o_fp_pod_signature_box">
<field name="recipient_signature" widget="image"
options="{'size': [400, 150]}" nolabel="1"/>
</div>
</group>
<notebook>
<page string="Photos">
<field name="photo_ids" widget="many2many_binary"/>
</page>
<page string="Notes">
<field name="notes" nolabel="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="view_fp_proof_of_delivery_search" model="ir.ui.view">
<field name="name">fp.proof.of.delivery.search</field>
<field name="model">fusion.plating.proof.of.delivery</field>
<field name="arch" type="xml">
<search string="Proof of Delivery">
<field name="name"/>
<field name="delivery_id"/>
<field name="partner_id"/>
<field name="recipient_name"/>
<group>
<filter name="group_partner" string="Customer" context="{'group_by':'partner_id'}"/>
<filter name="group_date" string="Delivered Date" context="{'group_by':'delivered_at:day'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_proof_of_delivery" model="ir.actions.act_window">
<field name="name">Proof of Delivery</field>
<field name="res_model">fusion.plating.proof.of.delivery</field>
<field name="view_mode">list,form</field>
</record>
</odoo>

View File

@@ -0,0 +1,189 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_route_list" model="ir.ui.view">
<field name="name">fp.route.list</field>
<field name="model">fusion.plating.route</field>
<field name="arch" type="xml">
<list string="Routes"
decoration-muted="state == 'cancelled'"
decoration-success="state == 'completed'"
decoration-primary="state == 'in_progress'">
<field name="name"/>
<field name="route_date"/>
<field name="driver_id"/>
<field name="vehicle_id"/>
<field name="stop_count"/>
<field name="total_km"/>
<field name="state" widget="badge"
decoration-info="state == 'draft'"
decoration-warning="state == 'planned'"
decoration-primary="state == 'in_progress'"
decoration-success="state == 'completed'"
decoration-muted="state == 'cancelled'"/>
</list>
</field>
</record>
<record id="view_fp_route_form" model="ir.ui.view">
<field name="name">fp.route.form</field>
<field name="model">fusion.plating.route</field>
<field name="arch" type="xml">
<form string="Route">
<header>
<button name="action_plan" string="Plan" type="object"
class="oe_highlight" invisible="state != 'draft'"/>
<button name="action_start" string="Start Route" type="object"
class="oe_highlight" invisible="state != 'planned'"/>
<button name="action_complete" string="Complete" type="object"
invisible="state != 'in_progress'"/>
<button name="action_cancel" string="Cancel" type="object"
invisible="state in ('completed','cancelled')"/>
<button name="action_reset_to_draft" string="Reset to Draft" type="object"
invisible="state not in ('cancelled','planned')"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,planned,in_progress,completed"/>
</header>
<sheet>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" readonly="1"/></h1>
</div>
<group>
<group>
<field name="route_date"/>
<field name="driver_id"/>
<field name="vehicle_id"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="start_time" readonly="1"/>
<field name="end_time" readonly="1"/>
<field name="total_km"/>
</group>
</group>
<notebook>
<page string="Stops">
<field name="stop_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="stop_type"/>
<field name="pickup_request_id"/>
<field name="delivery_id"/>
<field name="address_id"/>
<field name="planned_time"/>
<field name="actual_time"/>
<field name="state" widget="badge"
decoration-info="state == 'pending'"
decoration-warning="state == 'arrived'"
decoration-success="state == 'completed'"
decoration-muted="state == 'skipped'"/>
</list>
</field>
</page>
<page string="Notes">
<field name="notes"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_route_kanban" model="ir.ui.view">
<field name="name">fp.route.kanban</field>
<field name="model">fusion.plating.route</field>
<field name="arch" type="xml">
<kanban default_group_by="state" class="o_fp_route_kanban">
<field name="id"/>
<field name="name"/>
<field name="route_date"/>
<field name="driver_id"/>
<field name="vehicle_id"/>
<field name="stop_count"/>
<field name="total_km"/>
<templates>
<t t-name="card">
<div class="o_fp_card">
<div class="d-flex align-items-start justify-content-between">
<div>
<strong class="o_fp_card_title"><field name="name"/></strong>
<div class="small text-muted"><field name="route_date"/></div>
</div>
<i class="fa fa-road text-muted" aria-hidden="true"/>
</div>
<div class="mt-2 small">
<field name="driver_id"/>
</div>
<div class="mt-1 small text-muted">
<field name="vehicle_id"/>
</div>
<div class="mt-2 d-flex gap-3 small">
<div><strong><field name="stop_count"/></strong> stops</div>
<div><strong><field name="total_km"/></strong> km</div>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_route_calendar" model="ir.ui.view">
<field name="name">fp.route.calendar</field>
<field name="model">fusion.plating.route</field>
<field name="arch" type="xml">
<calendar string="Routes" date_start="route_date" mode="month" color="driver_id">
<field name="driver_id"/>
<field name="vehicle_id"/>
<field name="state"/>
</calendar>
</field>
</record>
<record id="view_fp_route_search" model="ir.ui.view">
<field name="name">fp.route.search</field>
<field name="model">fusion.plating.route</field>
<field name="arch" type="xml">
<search string="Routes">
<field name="name"/>
<field name="driver_id"/>
<field name="vehicle_id"/>
<filter name="filter_today" string="Today" domain="[('route_date','=', context_today().strftime('%Y-%m-%d'))]"/>
<filter name="filter_draft" string="Draft" domain="[('state','=','draft')]"/>
<filter name="filter_in_progress" string="In Progress" domain="[('state','=','in_progress')]"/>
<filter name="filter_completed" string="Completed" domain="[('state','=','completed')]"/>
<group>
<filter name="group_state" string="Status" context="{'group_by':'state'}"/>
<filter name="group_driver" string="Driver" context="{'group_by':'driver_id'}"/>
<filter name="group_vehicle" string="Vehicle" context="{'group_by':'vehicle_id'}"/>
<filter name="group_date" string="Route Date" context="{'group_by':'route_date'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_route" model="ir.actions.act_window">
<field name="name">Routes</field>
<field name="res_model">fusion.plating.route</field>
<field name="view_mode">kanban,list,calendar,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Plan your first route
</p>
<p>
A route combines multiple pickups and deliveries into a
single run for one driver and vehicle. Drag stops to
re-order, track actual vs planned times, and capture
total distance for costing.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,179 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_fp_vehicle_list" model="ir.ui.view">
<field name="name">fp.vehicle.list</field>
<field name="model">fusion.plating.vehicle</field>
<field name="arch" type="xml">
<list string="Vehicles" decoration-muted="state == 'out_of_service'"
decoration-warning="state == 'maintenance'"
decoration-success="state == 'available'">
<field name="name"/>
<field name="license_plate"/>
<field name="vehicle_type"/>
<field name="make"/>
<field name="model_year"/>
<field name="capacity_kg"/>
<field name="facility_id"/>
<field name="current_driver_id"/>
<field name="tdg_certified" widget="boolean_toggle"/>
<field name="state" widget="badge"
decoration-success="state == 'available'"
decoration-info="state == 'in_use'"
decoration-warning="state == 'maintenance'"
decoration-muted="state == 'out_of_service'"/>
<field name="active" widget="boolean_toggle" optional="hide"/>
</list>
</field>
</record>
<record id="view_fp_vehicle_form" model="ir.ui.view">
<field name="name">fp.vehicle.form</field>
<field name="model">fusion.plating.vehicle</field>
<field name="arch" type="xml">
<form string="Vehicle">
<header>
<button name="action_mark_available" string="Mark Available" type="object"
class="oe_highlight" invisible="state == 'available'"/>
<button name="action_mark_in_use" string="Mark In Use" type="object"
invisible="state == 'in_use'"/>
<button name="action_mark_maintenance" string="Send to Maintenance" type="object"
invisible="state == 'maintenance'"/>
<button name="action_mark_out_of_service" string="Out of Service" type="object"
invisible="state == 'out_of_service'"/>
<field name="state" widget="statusbar"
statusbar_visible="available,in_use,maintenance,out_of_service"/>
</header>
<sheet>
<widget name="web_ribbon" title="Archived" bg_color="text-bg-danger" invisible="active"/>
<div class="oe_title">
<label for="name"/>
<h1><field name="name" placeholder="e.g. Van 1"/></h1>
<div class="text-muted">
<field name="license_plate" placeholder="ABCD-123"/>
</div>
</div>
<group>
<group string="Vehicle">
<field name="vehicle_type"/>
<field name="make"/>
<field name="model_year"/>
<field name="capacity_kg"/>
<field name="tdg_certified" widget="boolean_toggle"/>
</group>
<group string="Assignment">
<field name="facility_id"/>
<field name="current_driver_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="active" widget="boolean_toggle"/>
</group>
</group>
<group string="Compliance">
<group>
<field name="insurance_expiry"/>
<field name="registration_expiry"/>
</group>
<group>
<field name="next_service_date"/>
</group>
</group>
<notebook>
<page string="Notes">
<field name="notes" placeholder="Maintenance history, special notes..."/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<record id="view_fp_vehicle_kanban" model="ir.ui.view">
<field name="name">fp.vehicle.kanban</field>
<field name="model">fusion.plating.vehicle</field>
<field name="arch" type="xml">
<kanban class="o_fp_vehicle_kanban">
<field name="id"/>
<field name="name"/>
<field name="license_plate"/>
<field name="vehicle_type"/>
<field name="state"/>
<field name="current_driver_id"/>
<field name="tdg_certified"/>
<templates>
<t t-name="card">
<div class="o_fp_card o_fp_vehicle_card" t-att-data-state="record.state.raw_value">
<div class="d-flex align-items-start justify-content-between">
<div>
<strong class="o_fp_card_title"><field name="name"/></strong>
<div class="text-muted small"><field name="license_plate"/></div>
</div>
<i class="fa fa-truck text-muted" aria-hidden="true"/>
</div>
<div class="mt-2 small">
<field name="vehicle_type"/>
</div>
<div class="mt-1 small text-muted">
<field name="current_driver_id"/>
</div>
<div class="mt-2">
<span class="o_fp_badge" t-att-data-state="record.state.raw_value">
<field name="state"/>
</span>
<span t-if="record.tdg_certified.raw_value" class="o_fp_badge ms-1" data-state="tdg">
TDG
</span>
</div>
</div>
</t>
</templates>
</kanban>
</field>
</record>
<record id="view_fp_vehicle_search" model="ir.ui.view">
<field name="name">fp.vehicle.search</field>
<field name="model">fusion.plating.vehicle</field>
<field name="arch" type="xml">
<search string="Vehicles">
<field name="name"/>
<field name="license_plate"/>
<field name="current_driver_id"/>
<field name="facility_id"/>
<filter name="filter_available" string="Available" domain="[('state','=','available')]"/>
<filter name="filter_in_use" string="In Use" domain="[('state','=','in_use')]"/>
<filter name="filter_maintenance" string="In Maintenance" domain="[('state','=','maintenance')]"/>
<filter name="filter_tdg" string="TDG Certified" domain="[('tdg_certified','=',True)]"/>
<separator/>
<filter name="filter_archived" string="Archived" domain="[('active','=',False)]"/>
<group>
<filter name="group_facility" string="Facility" context="{'group_by':'facility_id'}"/>
<filter name="group_state" string="Status" context="{'group_by':'state'}"/>
<filter name="group_type" string="Type" context="{'group_by':'vehicle_type'}"/>
</group>
</search>
</field>
</record>
<record id="action_fp_vehicle" model="ir.actions.act_window">
<field name="name">Vehicles</field>
<field name="res_model">fusion.plating.vehicle</field>
<field name="view_mode">kanban,list,form</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Add your first vehicle
</p>
<p>
Vehicles carry pickups from customers and deliveries back.
Flag a vehicle "TDG Certified" if it is approved to carry
Transportation of Dangerous Goods loads.
</p>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
-->
<odoo>
<record id="view_hr_employee_form_inherit_fp_logistics" model="ir.ui.view">
<field name="name">hr.employee.form.inherit.fp.logistics</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr.view_employee_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Driver" name="fp_driver">
<group>
<group string="Driver Status">
<field name="x_fc_is_driver" widget="boolean_toggle"/>
<field name="x_fc_driver_licence_class" invisible="not x_fc_is_driver"/>
<field name="x_fc_licence_expiry" invisible="not x_fc_is_driver"/>
</group>
<group string="TDG (Hazmat)">
<field name="x_fc_tdg_certified" widget="boolean_toggle" invisible="not x_fc_is_driver"/>
<field name="x_fc_tdg_expiry" invisible="not x_fc_tdg_certified"/>
</group>
</group>
<group string="Logistics History" invisible="not x_fc_is_driver">
<field name="x_fc_pickup_request_ids" nolabel="1" readonly="1">
<list string="Pickup Requests" limit="20">
<field name="name"/>
<field name="partner_id"/>
<field name="scheduled_date"/>
<field name="state"/>
</list>
</field>
<field name="x_fc_delivery_ids" nolabel="1" readonly="1">
<list string="Deliveries" limit="20">
<field name="name"/>
<field name="partner_id"/>
<field name="scheduled_date"/>
<field name="state"/>
</list>
</field>
</group>
</page>
</xpath>
</field>
</record>
</odoo>