folder rename
This commit is contained in:
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)
|
||||
Reference in New Issue
Block a user