This commit is contained in:
gsinghpal
2026-04-29 03:35:33 -04:00
parent 6ac6d24da6
commit a2fe1fcbcc
61 changed files with 4655 additions and 667 deletions

View File

@@ -16,12 +16,40 @@
# cancelled (rework reverts here)
# on_hold can be entered from confirmed or in_progress.
import pytz
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class FpJob(models.Model):
_name = 'fp.job'
def fp_format_local(self, dt, fmt='%Y-%m-%d %H:%M'):
"""Format a UTC datetime in the viewer's local timezone.
Used by report templates: QWeb's eval scope doesn't expose pytz
or format_datetime, but record methods are always callable, so
templates do `<span t-esc="job.fp_format_local(dt, '%H:%M')"/>`.
Resolution order matches the rest of the module: env.user.tz →
company.x_fc_default_tz → UTC.
"""
if not dt:
return ''
tz_name = (
self.env.user.tz
or ('x_fc_default_tz' in self.env.company._fields
and self.env.company.x_fc_default_tz)
or 'UTC'
)
try:
tz = pytz.timezone(tz_name)
except Exception:
tz = pytz.UTC
if dt.tzinfo is None:
dt = pytz.UTC.localize(dt)
return dt.astimezone(tz).strftime(fmt)
_description = 'Plating Job'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'priority desc, date_deadline asc, id desc'

View File

@@ -168,6 +168,56 @@ class FpJobStep(models.Model):
)
qty_at_step_start = fields.Integer(string='Qty at Step Start')
qty_at_step_finish = fields.Integer(string='Qty at Step Finish')
# Live "qty currently parked at this step" — drives partial-qty
# workflows. = (incoming moves' qty outgoing moves' qty), with a
# first-step seed: the lowest-sequence step on a confirmed job
# implicitly receives the full job qty when the job starts (no
# explicit "kickoff" move record). Without that seed, the first
# step would always show 0 here until the operator manually moved
# parts in, which doesn't match how the floor thinks about it.
qty_at_step = fields.Integer(
string='Qty Here',
compute='_compute_qty_at_step',
help='Quantity currently parked at this step. Drains as moves '
'transfer parts to later steps. The Move dialog defaults '
'to this value and blocks moves above it.',
)
@api.depends('move_ids.qty_moved', 'move_ids.to_step_id',
'incoming_move_ids.qty_moved',
'incoming_move_ids.from_step_id',
'state', 'job_id.qty', 'job_id.step_ids',
'job_id.step_ids.sequence', 'sequence')
def _compute_qty_at_step(self):
for rec in self:
# Terminal states: nothing parked here anymore. Operators
# don't care if "done" steps technically have qty residue —
# surfacing zero keeps the column readable.
if rec.state in ('done', 'cancelled', 'skipped'):
rec.qty_at_step = 0
continue
# Self-loop moves (from_step == to_step, transfer_type='step')
# are how the Record Inputs wizard logs measurements; they
# don't move qty so we exclude them on both sides.
incoming = sum(
m.qty_moved for m in rec.incoming_move_ids
if m.from_step_id != rec
)
outgoing = sum(
m.qty_moved for m in rec.move_ids
if m.to_step_id != rec
)
# First-step seed: the earliest non-terminal step on a job
# implicitly receives the full job qty when the job kicks
# off (no explicit kickoff move). Without this seed, qty
# here would read 0 even when the floor has the full batch.
if not incoming and rec.job_id and rec.job_id.qty:
first_active = rec.job_id.step_ids.filtered(
lambda s: s.state not in ('done', 'cancelled', 'skipped')
).sorted('sequence')[:1]
if rec == first_active:
incoming = int(rec.job_id.qty)
rec.qty_at_step = max(0, incoming - outgoing)
@api.depends('rack_id')
def _compute_is_racked(self):
@@ -226,7 +276,7 @@ class FpJobStep(models.Model):
) % (step.name, step.state))
now = fields.Datetime.now()
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
open_log.write({'date_finished': now})
open_log.write({'date_finished': now, 'state': 'paused'})
step.state = 'paused'
step.message_post(body=_('Step paused by %s') % self.env.user.name)
return True
@@ -269,7 +319,7 @@ class FpJobStep(models.Model):
) % (step.name, step.state))
now = fields.Datetime.now()
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
open_log.write({'date_finished': now})
open_log.write({'date_finished': now, 'state': 'stopped'})
step.state = 'cancelled'
step.message_post(body=_('Step cancelled by %s') % self.env.user.name)
return True
@@ -305,7 +355,7 @@ class FpJobStep(models.Model):
now = fields.Datetime.now()
# Close the open timelog (the one with no date_finished)
open_log = step.time_log_ids.filtered(lambda l: not l.date_finished)
open_log.write({'date_finished': now})
open_log.write({'date_finished': now, 'state': 'stopped'})
step.state = 'done'
# First-finish audit (mirrors button_start first-start guard)
if not step.date_finished: