feat(shopfloor): tablet shipping endpoints + /load shipping payload
generate_label (sudo'd FedEx machinery) and mark_shipped (as the technician), both enforcing the order-level ship-together gate. /load now returns a shipping block (carrier/service/weight + readiness + any existing label) when the job is awaiting_ship. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'version': '19.0.36.0.3',
|
||||
'version': '19.0.36.1.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
||||
'description': """
|
||||
|
||||
@@ -189,6 +189,35 @@ class FpWorkspaceController(http.Controller):
|
||||
} for dmg in rec.damage_ids],
|
||||
})
|
||||
|
||||
# ---- Shipping (awaiting_ship jobs) ------------------------------
|
||||
# Spec 2026-05-29. Surfaces the order-level ship-readiness gate +
|
||||
# the carrier/service/weight inputs + any label already on the
|
||||
# order's single outbound shipment. job is already sudo() here, so
|
||||
# the helper's reads are fine for display. ship_rec (not rec) to
|
||||
# avoid shadowing the receivings loop variable above.
|
||||
shipping_payload = None
|
||||
if job.state == 'awaiting_ship':
|
||||
info = job._fp_order_ship_state()
|
||||
ship_rec = info['order_receiving']
|
||||
shipment = ship_rec.x_fc_outbound_shipment_id if ship_rec else False
|
||||
carriers = env['delivery.carrier'].sudo().search([], limit=50)
|
||||
shipping_payload = {
|
||||
'ready': info['ready'],
|
||||
'not_ready': info['not_ready'],
|
||||
'receiving_id': ship_rec.id if ship_rec else False,
|
||||
'carrier_id': (ship_rec.x_fc_carrier_id.id
|
||||
if ship_rec and ship_rec.x_fc_carrier_id else False),
|
||||
'carrier_options': [{'id': c.id, 'name': c.name} for c in carriers],
|
||||
'service_type': (ship_rec.x_fc_outbound_service_type or '') if ship_rec else '',
|
||||
'service_options': env['fp.receiving']._fp_get_service_type_selection(),
|
||||
'weight': (ship_rec.x_fc_weight or 0.0) if ship_rec else 0.0,
|
||||
'has_label': bool(ship_rec.x_fc_has_label) if ship_rec else False,
|
||||
'tracking_number': (shipment.tracking_number or '') if shipment else '',
|
||||
'label_attachment_id': (shipment.label_attachment_id.id
|
||||
if shipment and shipment.label_attachment_id
|
||||
else False),
|
||||
}
|
||||
|
||||
return {
|
||||
'ok': True,
|
||||
'job': {
|
||||
@@ -246,6 +275,7 @@ class FpWorkspaceController(http.Controller):
|
||||
],
|
||||
'required_certs': required_certs,
|
||||
'receivings': receivings_payload,
|
||||
'shipping': shipping_payload,
|
||||
# 2026-05-24 — is_manager surfaces to the JS so it can offer
|
||||
# the manager-bypass affordance (e.g. on the required-inputs
|
||||
# gate dialog). Server-side endpoints re-check the group
|
||||
@@ -622,3 +652,78 @@ class FpWorkspaceController(http.Controller):
|
||||
_logger.exception("workspace/damage_delete failed")
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
return {'ok': True}
|
||||
|
||||
# ======================================================================
|
||||
# Shipping — generate outbound label + mark shipped (2026-05-29)
|
||||
# ======================================================================
|
||||
# Spec D3/D4. mark_shipped runs as the technician (real chatter
|
||||
# attribution; the method has no group gate). generate_label sudo's
|
||||
# the carrier/stock/shipment machinery — technicians intentionally
|
||||
# don't hold those ACLs. Both re-check the order-level "ship together"
|
||||
# gate server-side via fp.job._fp_order_ship_state.
|
||||
|
||||
@http.route('/fp/workspace/mark_shipped', type='jsonrpc', auth='user')
|
||||
def mark_shipped(self, job_id):
|
||||
env = request.env
|
||||
job = env['fp.job'].browse(int(job_id)) # as the tech
|
||||
if not job.exists():
|
||||
return {'ok': False, 'error': f'Job {job_id} not found'}
|
||||
try:
|
||||
shipped = job._fp_mark_order_shipped()
|
||||
except UserError as e:
|
||||
return {'ok': False, 'error': str(e.args[0]) if e.args else str(e)}
|
||||
except Exception as exc:
|
||||
_logger.exception("workspace/mark_shipped failed")
|
||||
return {'ok': False, 'error': str(exc)}
|
||||
return {'ok': True, 'shipped': shipped.mapped('display_wo_name')}
|
||||
|
||||
@http.route('/fp/workspace/generate_label', type='jsonrpc', auth='user')
|
||||
def generate_label(self, job_id, weight, service_type='', carrier_id=False):
|
||||
env = request.env
|
||||
job = env['fp.job'].browse(int(job_id))
|
||||
if not job.exists():
|
||||
return {'ok': False, 'error': f'Job {job_id} not found'}
|
||||
info = job._fp_order_ship_state()
|
||||
if not info['ready']:
|
||||
names = ', '.join(n['wo_name'] for n in info['not_ready']) or 'none'
|
||||
return {'ok': False,
|
||||
'error': 'Not all jobs on the order are finished: %s' % names,
|
||||
'not_ready': info['not_ready']}
|
||||
rec = info['order_receiving']
|
||||
if not rec:
|
||||
return {'ok': False,
|
||||
'error': 'No receiving record on this order to ship from.'}
|
||||
try:
|
||||
w = float(weight or 0)
|
||||
except (TypeError, ValueError):
|
||||
w = 0.0
|
||||
if w <= 0:
|
||||
return {'ok': False,
|
||||
'error': 'Enter a non-zero weight before generating the label.'}
|
||||
# sudo: carrier write triggers delivery.carrier read; the actual
|
||||
# generate synthesizes a stock.picking + fusion.shipment + label
|
||||
# attachment — all privileged. The carrier choice came from the
|
||||
# sudo'd options list in /load, so this is safe.
|
||||
rec = rec.sudo()
|
||||
vals = {'x_fc_weight': w,
|
||||
'x_fc_outbound_service_type': service_type or False}
|
||||
if carrier_id:
|
||||
vals['x_fc_carrier_id'] = int(carrier_id)
|
||||
try:
|
||||
rec.write(vals)
|
||||
rec._fp_actually_generate_outbound_label()
|
||||
except UserError as e:
|
||||
return {'ok': False, 'error': str(e.args[0]) if e.args else str(e)}
|
||||
except Exception as exc:
|
||||
_logger.exception("workspace/generate_label failed")
|
||||
return {'ok': False, 'error': 'Label generation failed: %s' % exc}
|
||||
# The model returns a manual-wizard action (no raise) on API
|
||||
# failure — so success is "a label landed on the shipment".
|
||||
shipment = rec.x_fc_outbound_shipment_id
|
||||
if not (shipment and shipment.label_attachment_id):
|
||||
return {'ok': False, 'error': (
|
||||
'The carrier did not return a label. Ask the office to '
|
||||
'generate it from the receiving record.')}
|
||||
return {'ok': True,
|
||||
'tracking_number': shipment.tracking_number or '',
|
||||
'label_attachment_id': shipment.label_attachment_id.id}
|
||||
|
||||
Reference in New Issue
Block a user