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',
|
'name': 'Fusion Plating — Shop Floor',
|
||||||
'version': '19.0.36.0.3',
|
'version': '19.0.36.1.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -189,6 +189,35 @@ class FpWorkspaceController(http.Controller):
|
|||||||
} for dmg in rec.damage_ids],
|
} 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 {
|
return {
|
||||||
'ok': True,
|
'ok': True,
|
||||||
'job': {
|
'job': {
|
||||||
@@ -246,6 +275,7 @@ class FpWorkspaceController(http.Controller):
|
|||||||
],
|
],
|
||||||
'required_certs': required_certs,
|
'required_certs': required_certs,
|
||||||
'receivings': receivings_payload,
|
'receivings': receivings_payload,
|
||||||
|
'shipping': shipping_payload,
|
||||||
# 2026-05-24 — is_manager surfaces to the JS so it can offer
|
# 2026-05-24 — is_manager surfaces to the JS so it can offer
|
||||||
# the manager-bypass affordance (e.g. on the required-inputs
|
# the manager-bypass affordance (e.g. on the required-inputs
|
||||||
# gate dialog). Server-side endpoints re-check the group
|
# gate dialog). Server-side endpoints re-check the group
|
||||||
@@ -622,3 +652,78 @@ class FpWorkspaceController(http.Controller):
|
|||||||
_logger.exception("workspace/damage_delete failed")
|
_logger.exception("workspace/damage_delete failed")
|
||||||
return {'ok': False, 'error': str(exc)}
|
return {'ok': False, 'error': str(exc)}
|
||||||
return {'ok': True}
|
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