feat(plating): demo stage-filler — every workflow step now has data
Companion to fp_demo_seed.py. Bridges the gaps the original seeder
left after the team-skills + timer-audit + presence-aware Manager Desk
work landed (commit 0d12902). Idempotent.
Eight steps, each wrapped in a safe() driver so a failure in one
doesn't abort the rest:
1. Fill x_fc_work_role_id on any WO that doesn't have one yet.
Keyword map (mask/rack/plat/bake/oven/inspect/rework) → role
code, falls back to plating_op. The auto-promotion tracker
can't credit a worker without a role on the WO.
2. Backfill the four timer audit fields (started_by/at,
finished_by/at) on done WOs. Pulls from time_ids when the
productivity records exist, otherwise synthesises timestamps
from create_date + duration.
3. Seed a diverse team of six operators with distinct role
coverage and lead-hand permissions:
- Marie Dubois — masking + racking (lead: masking)
- James O'Connor — plating_op + demask (lead: plating_op)
- Priya Sharma — oven + inspection (lead: oven, inspection)
- Diego Ramirez — racking + plating_op (TRAINING: 2/3 masking)
- Aisha Khan — inspection + rework
- Carlos Silva — every role (lead: every role)
Each gets a backing res.users so the Manager Desk dropdown
can assign them.
3b. Redistribute ~40 historical done WOs across the new team so
their Task Proficiency lists aren't empty. Plan targets
realistic per-role counts (Marie 8 masking + 5 racking,
James 12 plating + 4 demask, etc.) and re-stamps the timer
audit so finished_by reflects the new owner.
4. Wipe + rebuild fp.operator.proficiency from completed WOs so
the per-(employee, role) tally is deterministic. Auto-promotion
fires naturally during the rebuild — workers who already cleared
the threshold get promoted=True with timestamps. Diego is
deliberately seeded at 2/3 on masking so the demo shows the
"one more job away from promotion" state live.
5. Clock three operators in via hr.attendance (4-hour shift).
Wipes any stale open records first because earlier script
iterations left future-dated check_in timestamps that the
attendance validator refused to close.
6a. Two extra quality holds (damaged + out_of_spec).
6b. Mark the in-progress WO with a started_at but no finished_at
so the demo has a "paused for lunch" exemplar.
6c. Three portal RFQs (one per workflow state: new / under_review
/ quoted) so the funnel front-end has data.
6d. Push one draft SO to "sent" so the quotation pipeline has
data in every column (was draft → confirmed previously).
Verified on entech: 21 of 21 workflow stages now ✅, including
Diego's 2/3 masking row that shows the auto-promotion mechanic
in flight.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
546
fusion_plating/scripts/fp_demo_stage_filler.py
Normal file
546
fusion_plating/scripts/fp_demo_stage_filler.py
Normal file
@@ -0,0 +1,546 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
"""Demo stage-filler — fills the gaps left after fp_demo_seed.py.
|
||||
|
||||
The base seeder gives us customers, SOs, MOs, WOs, deliveries, invoices
|
||||
and payments. After the team-skills + timer-audit + presence-aware
|
||||
Manager Desk work landed (commit 0d12902) the demo needs:
|
||||
|
||||
1. Timer audit fields (x_fc_started_*, x_fc_finished_*) backfilled
|
||||
on done WOs so the new "Timer Audit" group has values
|
||||
2. WO role tags filled in on any leftover WOs (auto-promotion needs
|
||||
a role on the WO to credit the operator)
|
||||
3. A diverse team — Marie / James / Priya / Diego / Aisha / Carlos
|
||||
covering different role combinations including lead hands
|
||||
4. Proficiency records seeded from completed WOs so the "Task
|
||||
Proficiency" list on the employee form has rich data and a few
|
||||
auto-promotions are already on record
|
||||
5. Three employees clocked in right now via hr.attendance so the
|
||||
Manager Desk "Present X / Y" chip has data and the worker
|
||||
dropdown shows the bucket cues working
|
||||
6. Two extra quality holds + one paused WO so every stage of the
|
||||
workflow is represented somewhere in the demo
|
||||
|
||||
Run via odoo-shell:
|
||||
|
||||
su - odoo -s /bin/bash -c "odoo shell -c /etc/odoo/odoo.conf -d admin \\
|
||||
--no-http --stop-after-init < fp_demo_stage_filler.py"
|
||||
|
||||
Idempotent — re-runs are safe.
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from odoo import fields
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Backfill x_fc_work_role_id on WOs that don't have one yet
|
||||
# ---------------------------------------------------------------------------
|
||||
# Simple keyword map: WO/workcenter name → role code. Anything that
|
||||
# doesn't match falls back to plating_op (the most common role).
|
||||
ROLE_KEYWORDS = [
|
||||
('mask', 'masking'),
|
||||
('rack', 'racking'),
|
||||
('demask', 'demask'),
|
||||
('derack', 'derack'),
|
||||
('plat', 'plating_op'),
|
||||
('bake', 'oven'),
|
||||
('oven', 'oven'),
|
||||
('inspect','inspection'),
|
||||
('rework', 'rework'),
|
||||
]
|
||||
|
||||
|
||||
def _fill_wo_roles(env):
|
||||
Role = env['fp.work.role']
|
||||
role_by_code = {r.code: r for r in Role.search([])}
|
||||
fallback = role_by_code.get('plating_op')
|
||||
fixed = 0
|
||||
wos = env['mrp.workorder'].search([('x_fc_work_role_id', '=', False)])
|
||||
for wo in wos:
|
||||
haystack = ((wo.name or '') + ' ' + (wo.workcenter_id.name or '')).lower()
|
||||
match = next(
|
||||
(role_by_code[code] for kw, code in ROLE_KEYWORDS
|
||||
if kw in haystack and code in role_by_code),
|
||||
fallback,
|
||||
)
|
||||
if match:
|
||||
wo.x_fc_work_role_id = match.id
|
||||
fixed += 1
|
||||
print(f"[1] Filled work_role on {fixed} WOs (fallback = plating_op)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Backfill timer audit on done WOs
|
||||
# ---------------------------------------------------------------------------
|
||||
def _backfill_timer_audit(env):
|
||||
"""Stamp started_at/finished_at on done WOs from their time_ids.
|
||||
|
||||
Where time_ids isn't populated (some demo WOs were created without
|
||||
going through the productivity flow) we synthesise the timestamps
|
||||
from create_date + duration so the WO header isn't empty.
|
||||
"""
|
||||
fixed = 0
|
||||
wos = env['mrp.workorder'].search([
|
||||
('state', '=', 'done'),
|
||||
('x_fc_started_at', '=', False),
|
||||
])
|
||||
for wo in wos:
|
||||
# Prefer real time_ids data — that's what the live override would
|
||||
# have captured if the WO had been finished after the upgrade.
|
||||
if wo.time_ids:
|
||||
first = wo.time_ids.sorted('date_start')[:1]
|
||||
last = wo.time_ids.sorted('date_end', reverse=True)[:1]
|
||||
start_dt = first.date_start if first else False
|
||||
end_dt = last.date_end if last else False
|
||||
start_uid = first.user_id.id if first and first.user_id else False
|
||||
end_uid = last.user_id.id if last and last.user_id else False
|
||||
else:
|
||||
start_dt = wo.create_date
|
||||
dur = wo.duration or 0
|
||||
end_dt = (
|
||||
wo.create_date + timedelta(minutes=dur)
|
||||
if wo.create_date else False
|
||||
)
|
||||
uid = (
|
||||
wo.x_fc_assigned_user_id.id
|
||||
if wo.x_fc_assigned_user_id else env.uid
|
||||
)
|
||||
start_uid = end_uid = uid
|
||||
# Always falls back to the assigned worker if we couldn't pull a
|
||||
# user from time_ids — that's the operator who SHOULD get credit.
|
||||
if not start_uid:
|
||||
start_uid = (
|
||||
wo.x_fc_assigned_user_id.id
|
||||
if wo.x_fc_assigned_user_id else env.uid
|
||||
)
|
||||
if not end_uid:
|
||||
end_uid = start_uid
|
||||
wo.write({
|
||||
'x_fc_started_at': start_dt,
|
||||
'x_fc_started_by_user_id': start_uid,
|
||||
'x_fc_finished_at': end_dt,
|
||||
'x_fc_finished_by_user_id': end_uid,
|
||||
})
|
||||
fixed += 1
|
||||
print(f"[2] Backfilled timer audit on {fixed} done WOs")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Add five diverse operators + a senior lead hand
|
||||
# ---------------------------------------------------------------------------
|
||||
TEAM = [
|
||||
{
|
||||
'name': 'Marie Dubois',
|
||||
'work_email': 'marie.dubois@entech.demo',
|
||||
'job_title': 'Masking Specialist',
|
||||
'role_codes': ['masking', 'racking'],
|
||||
'lead_codes': ['masking'],
|
||||
},
|
||||
{
|
||||
'name': "James O'Connor",
|
||||
'work_email': 'james.oconnor@entech.demo',
|
||||
'job_title': 'Senior Plating Operator',
|
||||
'role_codes': ['plating_op', 'demask'],
|
||||
'lead_codes': ['plating_op'],
|
||||
},
|
||||
{
|
||||
'name': 'Priya Sharma',
|
||||
'work_email': 'priya.sharma@entech.demo',
|
||||
'job_title': 'Quality Inspector',
|
||||
'role_codes': ['oven', 'inspection'],
|
||||
'lead_codes': ['oven', 'inspection'],
|
||||
},
|
||||
{
|
||||
# Diego is the "still in training" employee — racks and plates
|
||||
# but isn't qualified for masking yet. The proficiency tracker
|
||||
# will promote him once he's finished N masking WOs.
|
||||
'name': 'Diego Ramirez',
|
||||
'work_email': 'diego.ramirez@entech.demo',
|
||||
'job_title': 'Plating Operator',
|
||||
'role_codes': ['racking', 'plating_op'],
|
||||
'lead_codes': [],
|
||||
},
|
||||
{
|
||||
'name': 'Aisha Khan',
|
||||
'work_email': 'aisha.khan@entech.demo',
|
||||
'job_title': 'Inspection & Rework',
|
||||
'role_codes': ['inspection', 'rework'],
|
||||
'lead_codes': [],
|
||||
},
|
||||
{
|
||||
# Carlos is the senior — lead hand for everything, can cover
|
||||
# any shift. Manager Desk surfaces him on every dropdown.
|
||||
'name': 'Carlos Silva',
|
||||
'work_email': 'carlos.silva@entech.demo',
|
||||
'job_title': 'Shift Supervisor',
|
||||
'role_codes': ['masking', 'racking', 'plating_op', 'demask',
|
||||
'oven', 'derack', 'inspection', 'rework'],
|
||||
'lead_codes': ['masking', 'racking', 'plating_op', 'demask',
|
||||
'oven', 'derack', 'inspection', 'rework'],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _seed_team(env):
|
||||
Role = env['fp.work.role']
|
||||
Emp = env['hr.employee']
|
||||
Users = env['res.users']
|
||||
role_by_code = {r.code: r for r in Role.search([])}
|
||||
operator_group = env.ref(
|
||||
'fusion_plating.group_fusion_plating_operator',
|
||||
raise_if_not_found=False,
|
||||
)
|
||||
created = 0
|
||||
updated = 0
|
||||
for spec in TEAM:
|
||||
emp = Emp.search([('name', '=', spec['name'])], limit=1)
|
||||
role_ids = [
|
||||
role_by_code[c].id for c in spec['role_codes']
|
||||
if c in role_by_code
|
||||
]
|
||||
lead_ids = [
|
||||
role_by_code[c].id for c in spec['lead_codes']
|
||||
if c in role_by_code
|
||||
]
|
||||
vals = {
|
||||
'name': spec['name'],
|
||||
'work_email': spec['work_email'],
|
||||
'job_title': spec['job_title'],
|
||||
'x_fc_work_role_ids': [(6, 0, role_ids)],
|
||||
'x_fc_lead_hand_role_ids': [(6, 0, lead_ids)],
|
||||
}
|
||||
if emp:
|
||||
emp.write(vals)
|
||||
updated += 1
|
||||
else:
|
||||
emp = Emp.create(vals)
|
||||
created += 1
|
||||
|
||||
# Make sure each employee has a backing res.users so the Manager
|
||||
# Desk dropdown can pick them. Without a user the WO can't be
|
||||
# assigned (x_fc_assigned_user_id targets res.users).
|
||||
if not emp.user_id:
|
||||
user = Users.search([('login', '=', spec['work_email'])], limit=1)
|
||||
if not user:
|
||||
# Odoo 19 renamed groups_id → group_ids on res.users.
|
||||
user_vals = {
|
||||
'name': spec['name'],
|
||||
'login': spec['work_email'],
|
||||
'email': spec['work_email'],
|
||||
}
|
||||
if operator_group:
|
||||
user_vals['group_ids'] = [(4, operator_group.id)]
|
||||
user = Users.create(user_vals)
|
||||
emp.user_id = user.id
|
||||
print(f"[3] Seeded team: {created} new, {updated} updated")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Backfill proficiency records from completed WOs
|
||||
# ---------------------------------------------------------------------------
|
||||
def _redistribute_completed_wos(env):
|
||||
"""Re-assign a slice of historical done WOs across the new team.
|
||||
|
||||
Without this, all the existing 82 done WOs stay credited to the
|
||||
two original users (Administrator + Andrew) and the new team
|
||||
members (Marie/James/Priya/etc.) look like blank slates with no
|
||||
completion history. Spread the credit so the demo shows realistic
|
||||
proficiency variance — Marie has 8 masking jobs done, James has
|
||||
12 plating_op, Carlos has touched everything, Diego is mid-training.
|
||||
"""
|
||||
Emp = env['hr.employee']
|
||||
Role = env['fp.work.role']
|
||||
role_by_code = {r.code: r for r in Role.search([])}
|
||||
|
||||
# Plan: each (operator, role) gets a target completion count.
|
||||
# Diego stays at 2 masking on purpose (still in training).
|
||||
plan = {
|
||||
'Marie Dubois': {'masking': 8, 'racking': 5},
|
||||
"James O'Connor": {'plating_op': 12, 'demask': 4},
|
||||
'Priya Sharma': {'oven': 6, 'inspection': 9},
|
||||
'Aisha Khan': {'inspection': 4, 'rework': 3},
|
||||
'Carlos Silva': {'masking': 3, 'racking': 3, 'plating_op': 4, 'oven': 2,
|
||||
'demask': 2, 'derack': 2, 'inspection': 3, 'rework': 1},
|
||||
}
|
||||
|
||||
moved = 0
|
||||
for emp_name, role_targets in plan.items():
|
||||
emp = Emp.search([('name', '=', emp_name)], limit=1)
|
||||
if not emp or not emp.user_id:
|
||||
continue
|
||||
for role_code, target_count in role_targets.items():
|
||||
role = role_by_code.get(role_code)
|
||||
if not role:
|
||||
continue
|
||||
# Find done WOs of this role that aren't already on this user
|
||||
wos = env['mrp.workorder'].search([
|
||||
('state', '=', 'done'),
|
||||
('x_fc_work_role_id', '=', role.id),
|
||||
('x_fc_assigned_user_id', '!=', emp.user_id.id),
|
||||
], limit=target_count)
|
||||
for wo in wos:
|
||||
wo.x_fc_assigned_user_id = emp.user_id.id
|
||||
# Re-stamp the audit so finished_by reflects the new owner.
|
||||
wo.x_fc_started_by_user_id = emp.user_id.id
|
||||
wo.x_fc_finished_by_user_id = emp.user_id.id
|
||||
moved += 1
|
||||
print(f"[3b] Re-credited {moved} historical WOs to the new team")
|
||||
|
||||
|
||||
def _seed_proficiency(env):
|
||||
"""Walk every done WO and credit the assigned worker for the role.
|
||||
|
||||
This rebuilds the proficiency tally as if every historical WO had
|
||||
gone through the new button_finish override. Auto-promotion fires
|
||||
naturally on the way: if Marie has 8 completed masking WOs and
|
||||
masking.mastery_required is 3, she'll get promoted (which is a
|
||||
no-op since she's already qualified) but the proficiency row will
|
||||
show promoted=True with a real promoted_at timestamp.
|
||||
"""
|
||||
Prof = env['fp.operator.proficiency']
|
||||
Prof.search([]).unlink() # reset so re-runs are deterministic
|
||||
|
||||
counts = {} # (employee_id, role_id) → count
|
||||
first_seen = {}
|
||||
last_seen = {}
|
||||
done_wos = env['mrp.workorder'].search([
|
||||
('state', '=', 'done'),
|
||||
('x_fc_assigned_user_id', '!=', False),
|
||||
('x_fc_work_role_id', '!=', False),
|
||||
])
|
||||
for wo in done_wos:
|
||||
emp = wo.x_fc_assigned_user_id.employee_id
|
||||
if not emp:
|
||||
continue
|
||||
key = (emp.id, wo.x_fc_work_role_id.id)
|
||||
counts[key] = counts.get(key, 0) + 1
|
||||
ts = wo.x_fc_finished_at or wo.write_date
|
||||
if key not in first_seen or ts < first_seen[key]:
|
||||
first_seen[key] = ts
|
||||
if key not in last_seen or ts > last_seen[key]:
|
||||
last_seen[key] = ts
|
||||
|
||||
created = 0
|
||||
promoted = 0
|
||||
for (emp_id, role_id), count in counts.items():
|
||||
rec = Prof.create({
|
||||
'employee_id': emp_id,
|
||||
'role_id': role_id,
|
||||
'completed_count': count,
|
||||
'first_completed_at': first_seen[(emp_id, role_id)],
|
||||
'last_completed_at': last_seen[(emp_id, role_id)],
|
||||
})
|
||||
rec._maybe_promote() # fires promotion + chatter when applicable
|
||||
created += 1
|
||||
if rec.promoted:
|
||||
promoted += 1
|
||||
|
||||
# Give Diego a partial-mastery row on masking so we can SEE the
|
||||
# progress label "2 / 3" and the not-yet-promoted state — that's
|
||||
# the most interesting demo case.
|
||||
diego = env['hr.employee'].search([('name', '=', 'Diego Ramirez')], limit=1)
|
||||
masking = env['fp.work.role'].search([('code', '=', 'masking')], limit=1)
|
||||
if diego and masking and not Prof.search_count([
|
||||
('employee_id', '=', diego.id), ('role_id', '=', masking.id)
|
||||
]):
|
||||
Prof.create({
|
||||
'employee_id': diego.id,
|
||||
'role_id': masking.id,
|
||||
'completed_count': max(masking.mastery_required - 1, 1),
|
||||
'first_completed_at': fields.Datetime.now() - timedelta(days=10),
|
||||
'last_completed_at': fields.Datetime.now() - timedelta(days=1),
|
||||
})
|
||||
print(f" + Diego seeded with {max(masking.mastery_required - 1, 1)} "
|
||||
f"masking completions (one more away from promotion)")
|
||||
print(f"[4] Seeded {created} proficiency rows ({promoted} auto-promoted)")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. Clock three employees in right now (hr.attendance open record)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _clock_in_team(env):
|
||||
Att = env['hr.attendance']
|
||||
# Wipe any stale open records before creating fresh ones. We unlink
|
||||
# rather than close because earlier script runs may have left
|
||||
# check_in timestamps in the future (a .replace(hour=12) bug); the
|
||||
# validator then refuses any check_out and the close fails.
|
||||
Att.search([('check_out', '=', False)]).sudo().unlink()
|
||||
|
||||
targets = ['Marie Dubois', 'James O\'Connor', 'Carlos Silva']
|
||||
started = []
|
||||
# Always 4 hours ago — guaranteed before "now", regardless of when
|
||||
# the close-stale-records step ran. The earlier .replace() approach
|
||||
# could land in the future if the script ran after noon UTC, which
|
||||
# the validator rejected.
|
||||
check_in = fields.Datetime.now() - timedelta(hours=4)
|
||||
for name in targets:
|
||||
emp = env['hr.employee'].search([('name', '=', name)], limit=1)
|
||||
if not emp:
|
||||
continue
|
||||
Att.create({
|
||||
'employee_id': emp.id,
|
||||
'check_in': check_in,
|
||||
})
|
||||
started.append(name)
|
||||
print(f"[5] Clocked in: {', '.join(started) or '(no targets found)'}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. Top up extra demo records — quality holds + paused WO + RFQs
|
||||
# ---------------------------------------------------------------------------
|
||||
def _add_quality_holds(env):
|
||||
Hold = env['fusion.plating.quality.hold']
|
||||
if Hold.search_count([]) >= 3:
|
||||
print("[6a] Quality holds already populated, skipping")
|
||||
return
|
||||
wos = env['mrp.workorder'].search([
|
||||
('state', '=', 'done'),
|
||||
], limit=2)
|
||||
# Valid hold_reason values: damaged / out_of_spec / contamination /
|
||||
# customer_complaint / process_deviation / other.
|
||||
reasons = ['damaged', 'out_of_spec']
|
||||
descriptions = [
|
||||
'Light scratch on the masked face — flagging for re-inspection.',
|
||||
'Thickness reading 0.4 mils, target 0.5 ± 0.05. Out of spec.',
|
||||
]
|
||||
created = 0
|
||||
for wo, reason, desc in zip(wos, reasons, descriptions):
|
||||
Hold.create({
|
||||
'workorder_id': wo.id,
|
||||
'production_id': wo.production_id.id,
|
||||
'part_ref': wo.production_id.product_id.default_code or 'PART-X',
|
||||
'qty_on_hold': 2,
|
||||
'qty_original': int(wo.production_id.product_qty or 5),
|
||||
'hold_reason': reason,
|
||||
'description': desc,
|
||||
'state': 'on_hold',
|
||||
})
|
||||
created += 1
|
||||
print(f"[6a] Added {created} quality holds")
|
||||
|
||||
|
||||
def _add_paused_wo(env):
|
||||
"""Show one WO mid-flight with pause/resume history.
|
||||
|
||||
The audit fields show started_by but no finished_at — exactly the
|
||||
"in progress, paused for lunch" state a manager would see live.
|
||||
"""
|
||||
progress = env['mrp.workorder'].search([('state', '=', 'progress')], limit=1)
|
||||
if progress and progress.x_fc_started_at:
|
||||
print("[6b] Already have a progress WO with audit, skipping")
|
||||
return
|
||||
if not progress:
|
||||
# Promote a ready WO to progress so the demo has at least one.
|
||||
ready = env['mrp.workorder'].search([
|
||||
('state', 'in', ('ready', 'waiting', 'pending')),
|
||||
], limit=1)
|
||||
if not ready:
|
||||
print("[6b] No ready WO to promote to progress")
|
||||
return
|
||||
progress = ready
|
||||
user = progress.x_fc_assigned_user_id or env.user
|
||||
progress.write({
|
||||
'x_fc_started_at': fields.Datetime.now() - timedelta(hours=2),
|
||||
'x_fc_started_by_user_id': user.id,
|
||||
})
|
||||
print(f"[6b] Paused-WO marker set on {progress.display_name}")
|
||||
|
||||
|
||||
def _mark_quote_sent(env):
|
||||
"""Bump one draft SO into the 'sent' state so the funnel has data
|
||||
in every workflow column.
|
||||
|
||||
Without this, the dashboard "Sent" stage is always empty — the
|
||||
seeder jumps straight from draft to confirmed.
|
||||
"""
|
||||
sent = env['sale.order'].search([('state', '=', 'sent')], limit=1)
|
||||
if sent:
|
||||
print("[6d] Already have a sent quote, skipping")
|
||||
return
|
||||
draft = env['sale.order'].search(
|
||||
[('state', '=', 'draft'), ('order_line', '!=', False)], limit=1,
|
||||
)
|
||||
if not draft:
|
||||
print("[6d] No draft SO with lines available")
|
||||
return
|
||||
# action_quotation_send opens a wizard — skip the wizard and just
|
||||
# flip the state directly with a chatter line, which is what the
|
||||
# wizard would do anyway after the email is sent.
|
||||
draft.write({'state': 'sent'})
|
||||
draft.message_post(body='Quotation marked as sent (demo data).')
|
||||
print(f"[6d] Marked {draft.name} as sent")
|
||||
|
||||
|
||||
def _add_quote_requests(env):
|
||||
QR = env.get('fusion.plating.quote.request')
|
||||
if QR is None:
|
||||
print("[6c] Portal module not installed — skipping RFQ seed")
|
||||
return
|
||||
if QR.search_count([]) >= 3:
|
||||
print("[6c] Quote requests already populated, skipping")
|
||||
return
|
||||
customers = env['res.partner'].search(
|
||||
[('customer_rank', '>', 0)], limit=3,
|
||||
)
|
||||
# State selection on fusion.plating.quote.request: validate against
|
||||
# the live model so this works even if the workflow gets renamed.
|
||||
valid_states = {v for v, _ in QR._fields['state'].selection}
|
||||
candidate_states = [
|
||||
s for s in ('new', 'under_review', 'quoted', 'accepted', 'rejected')
|
||||
if s in valid_states
|
||||
][:3]
|
||||
|
||||
notes = [
|
||||
'<p>Need a quote for 200 brass fittings — Type II passivation, urgent.</p>',
|
||||
'<p>Recurring customer, standard EN on 50 housings, 0.5 mil target.</p>',
|
||||
'<p>Aerospace job, AS9100 + Nadcap CoC required, 12 turbine vanes.</p>',
|
||||
]
|
||||
currency = env.company.currency_id
|
||||
created = 0
|
||||
for partner, state, note in zip(customers, candidate_states, notes):
|
||||
QR.create({
|
||||
'name': f'RFQ-DEMO-{partner.id}',
|
||||
'partner_id': partner.id,
|
||||
'state': state,
|
||||
'currency_id': currency.id,
|
||||
'part_description': note,
|
||||
'quantity': 50,
|
||||
})
|
||||
created += 1
|
||||
print(f"[6c] Added {created} quote requests (states: {', '.join(candidate_states)})")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Driver
|
||||
# ---------------------------------------------------------------------------
|
||||
def _safe(label, fn):
|
||||
"""Run a step, log + swallow any error so partial success persists."""
|
||||
try:
|
||||
fn(env)
|
||||
env.cr.commit()
|
||||
except Exception as exc:
|
||||
env.cr.rollback()
|
||||
print(f"!! {label} FAILED: {exc!r}")
|
||||
|
||||
|
||||
print("\n=========================================================")
|
||||
print("FUSION PLATING — DEMO STAGE FILLER")
|
||||
print("=========================================================")
|
||||
_safe('1. fill WO roles', _fill_wo_roles)
|
||||
_safe('3. seed team', _seed_team)
|
||||
_safe('2. backfill timer audit', _backfill_timer_audit)
|
||||
_safe('3b. redistribute WOs', _redistribute_completed_wos)
|
||||
_safe('4. seed proficiency', _seed_proficiency)
|
||||
_safe('5. clock in team', _clock_in_team)
|
||||
_safe('6a. add quality holds', _add_quality_holds)
|
||||
_safe('6b. mark paused WO', _add_paused_wo)
|
||||
_safe('6c. add quote requests', _add_quote_requests)
|
||||
_safe('6d. mark one quote sent', _mark_quote_sent)
|
||||
print("=========================================================")
|
||||
print("Done. Re-run anytime — script is idempotent.")
|
||||
print("=========================================================\n")
|
||||
Reference in New Issue
Block a user