folder rename
This commit is contained in:
77
fusion_plating/fusion_plating_logistics/README.md
Normal file
77
fusion_plating/fusion_plating_logistics/README.md
Normal 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.
|
||||
6
fusion_plating/fusion_plating_logistics/__init__.py
Normal file
6
fusion_plating/fusion_plating_logistics/__init__.py
Normal 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
|
||||
71
fusion_plating/fusion_plating_logistics/__manifest__.py
Normal file
71
fusion_plating/fusion_plating_logistics/__manifest__.py
Normal 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,
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
13
fusion_plating/fusion_plating_logistics/models/__init__.py
Normal file
13
fusion_plating/fusion_plating_logistics/models/__init__.py
Normal 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
|
||||
@@ -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
|
||||
211
fusion_plating/fusion_plating_logistics/models/fp_delivery.py
Normal file
211
fusion_plating/fusion_plating_logistics/models/fp_delivery.py
Normal 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',
|
||||
}
|
||||
@@ -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'})
|
||||
@@ -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 '/'
|
||||
123
fusion_plating/fusion_plating_logistics/models/fp_route.py
Normal file
123
fusion_plating/fusion_plating_logistics/models/fp_route.py
Normal 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'})
|
||||
@@ -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'})
|
||||
134
fusion_plating/fusion_plating_logistics/models/fp_vehicle.py
Normal file
134
fusion_plating/fusion_plating_logistics/models/fp_vehicle.py
Normal 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'})
|
||||
@@ -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)
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
53
fusion_plating/fusion_plating_logistics/views/fp_menu.xml
Normal file
53
fusion_plating/fusion_plating_logistics/views/fp_menu.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
189
fusion_plating/fusion_plating_logistics/views/fp_route_views.xml
Normal file
189
fusion_plating/fusion_plating_logistics/views/fp_route_views.xml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user