changes
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user