feat(jobs): cleanup + seed scripts for demo data reset
cleanup_demo_data.py: deletes ALL fp.job, fp.job.step, timelogs, mrp.production, mrp.workorder, and dependent records (deliveries, certs, holds, portal jobs, racking inspections, uninvoiced SOs). Resets the fp.job sequence. Preserves masters. Force-cancels MOs/SOs via SQL UPDATE before unlink to bypass Odoo's _unlink_except_done and _unlink_except_draft_or_cancel guards. seed_demo_data.py: creates 31 fp.job rows distributed across all 6 states (draft=5, confirmed=6, in_progress=8, on_hold=3, done=6, cancelled=3). In_progress jobs have mixed step states with real timelogs to simulate a live shop floor. Falls back to direct fp.job creation when a customer's parts have no coating/recipe, ensuring customer variety even with sparse coating data. Both scripts: idempotent (safe to re-run), commit at end, walk dependents bottom-up to avoid FK violations. Used to reset entech demo data after the migration trial. Part of: native job model migration (spec 2026-04-25) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
148
fusion_plating/fusion_plating_jobs/scripts/cleanup_demo_data.py
Normal file
148
fusion_plating/fusion_plating_jobs/scripts/cleanup_demo_data.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# DESTRUCTIVE: deletes ALL fp.job, fp.job.step, fp.job.step.timelog,
|
||||
# mrp.production, mrp.workorder records and their dependent data
|
||||
# (deliveries, certs, thickness readings, holds, portal jobs, racking
|
||||
# inspections). Preserves masters (partners, parts, recipes, coating
|
||||
# configs, baths, tanks, work centres, users, groups, settings).
|
||||
#
|
||||
# Use only on demo/dev environments. Take a Proxmox snapshot first.
|
||||
|
||||
def run(env):
|
||||
print('=== Cleanup starting ===')
|
||||
|
||||
# Walk dependents bottom-up so FK cascades don't bite us.
|
||||
# 1. Time logs (cascades on step delete, but be explicit)
|
||||
n = env['fp.job.step.timelog'].search_count([])
|
||||
env['fp.job.step.timelog'].sudo().search([]).unlink()
|
||||
print(' Deleted %d fp.job.step.timelog rows' % n)
|
||||
|
||||
# 2. fp.job.node.override (cascades on job delete)
|
||||
n = env['fp.job.node.override'].search_count([])
|
||||
env['fp.job.node.override'].sudo().search([]).unlink()
|
||||
print(' Deleted %d fp.job.node.override rows' % n)
|
||||
|
||||
# 3. Deliveries linked to jobs OR with job_ref set OR linked to a SO that
|
||||
# we will delete. Delete ALL deliveries — they're test data.
|
||||
if 'fusion.plating.delivery' in env:
|
||||
deliveries = env['fusion.plating.delivery'].sudo().search([])
|
||||
n = len(deliveries)
|
||||
deliveries.unlink()
|
||||
print(' Deleted %d fusion.plating.delivery rows' % n)
|
||||
|
||||
# 4. Certificates linked to jobs/MOs
|
||||
if 'fp.certificate' in env:
|
||||
certs = env['fp.certificate'].sudo().search([])
|
||||
n = len(certs)
|
||||
certs.unlink()
|
||||
print(' Deleted %d fp.certificate rows' % n)
|
||||
|
||||
# 5. Thickness readings
|
||||
if 'fp.thickness.reading' in env:
|
||||
tr = env['fp.thickness.reading'].sudo().search([])
|
||||
n = len(tr)
|
||||
tr.unlink()
|
||||
print(' Deleted %d fp.thickness.reading rows' % n)
|
||||
|
||||
# 6. Quality holds linked to jobs/MOs
|
||||
if 'fusion.plating.quality.hold' in env:
|
||||
holds = env['fusion.plating.quality.hold'].sudo().search([])
|
||||
n = len(holds)
|
||||
holds.unlink()
|
||||
print(' Deleted %d fusion.plating.quality.hold rows' % n)
|
||||
|
||||
# 7. Portal jobs (linked to jobs OR legacy production)
|
||||
if 'fusion.plating.portal.job' in env:
|
||||
portals = env['fusion.plating.portal.job'].sudo().search([])
|
||||
n = len(portals)
|
||||
portals.unlink()
|
||||
print(' Deleted %d fusion.plating.portal.job rows' % n)
|
||||
|
||||
# 8. Racking inspections — required FK to mrp.production, so delete
|
||||
# BEFORE we kill the productions.
|
||||
if 'fp.racking.inspection' in env:
|
||||
insps = env['fp.racking.inspection'].sudo().search([])
|
||||
n = len(insps)
|
||||
insps.unlink()
|
||||
print(' Deleted %d fp.racking.inspection rows' % n)
|
||||
|
||||
# 9. Receiving records (required FK to sale.order — delete before SOs)
|
||||
if 'fp.receiving' in env:
|
||||
recs = env['fp.receiving'].sudo().search([])
|
||||
n = len(recs)
|
||||
recs.unlink()
|
||||
print(' Deleted %d fp.receiving rows' % n)
|
||||
|
||||
# 10. fp.job.step (cascade-safe via job_id, but be explicit)
|
||||
n = env['fp.job.step'].search_count([])
|
||||
env['fp.job.step'].sudo().search([]).unlink()
|
||||
print(' Deleted %d fp.job.step rows' % n)
|
||||
|
||||
# 11. fp.job
|
||||
n = env['fp.job'].search_count([])
|
||||
env['fp.job'].sudo().search([]).unlink()
|
||||
print(' Deleted %d fp.job rows' % n)
|
||||
|
||||
# 12. mrp.workorder (legacy)
|
||||
n = env['mrp.workorder'].search_count([])
|
||||
env['mrp.workorder'].sudo().search([]).unlink()
|
||||
print(' Deleted %d mrp.workorder rows' % n)
|
||||
|
||||
# 13. mrp.production (legacy) — force state via SQL so unlink() bypasses
|
||||
# Odoo's _unlink_except_done guard (which forbids deleting done MOs)
|
||||
# and the action_cancel guard (which forbids cancelling done MOs).
|
||||
# Demo data only.
|
||||
n = env['mrp.production'].search_count([])
|
||||
if n:
|
||||
# 'cancel' state is the only state mrp.production._unlink_except_done
|
||||
# explicitly permits.
|
||||
env.cr.execute("UPDATE mrp_production SET state='cancel'")
|
||||
# Also clear stock moves' state so cascaded checks pass
|
||||
env.cr.execute(
|
||||
"UPDATE stock_move SET state='cancel' "
|
||||
"WHERE raw_material_production_id IN (SELECT id FROM mrp_production) "
|
||||
"OR production_id IN (SELECT id FROM mrp_production)"
|
||||
)
|
||||
env.invalidate_all()
|
||||
env['mrp.production'].sudo().search([]).unlink()
|
||||
print(' Deleted %d mrp.production rows' % n)
|
||||
|
||||
# 14. Test SOs — delete ALL non-invoiced sale orders. Force state to
|
||||
# 'cancel' via SQL because Odoo's _unlink_except_draft_or_cancel
|
||||
# guard otherwise blocks the unlink. Demo data only.
|
||||
so_ids = env['sale.order'].sudo().search([
|
||||
('invoice_ids', '=', False),
|
||||
]).ids
|
||||
n = len(so_ids)
|
||||
if so_ids:
|
||||
env.cr.execute(
|
||||
"UPDATE sale_order SET state='cancel' WHERE id = ANY(%s)",
|
||||
(so_ids,),
|
||||
)
|
||||
# Also cancel stock pickings the SOs may have created (forces
|
||||
# Odoo's cascade-aware unlink to pass)
|
||||
env.cr.execute(
|
||||
"UPDATE stock_picking SET state='cancel' "
|
||||
"WHERE sale_id = ANY(%s)",
|
||||
(so_ids,),
|
||||
)
|
||||
env.invalidate_all()
|
||||
env['sale.order'].sudo().browse(so_ids).unlink()
|
||||
print(' Deleted %d sale.order rows (uninvoiced)' % n)
|
||||
|
||||
# 15. Reset fp.job sequence so new ones start from JOB/00001
|
||||
seq = env['ir.sequence'].sudo().search([('code', '=', 'fp.job')], limit=1)
|
||||
if seq:
|
||||
seq.number_next = 1
|
||||
print(' Reset fp.job sequence to start at 1')
|
||||
|
||||
env.cr.commit()
|
||||
print('=== Cleanup complete ===')
|
||||
|
||||
|
||||
try:
|
||||
run(env)
|
||||
except NameError:
|
||||
print('Run inside `odoo shell`.')
|
||||
434
fusion_plating/fusion_plating_jobs/scripts/seed_demo_data.py
Normal file
434
fusion_plating/fusion_plating_jobs/scripts/seed_demo_data.py
Normal file
@@ -0,0 +1,434 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# Seeds 5-8 fp.job rows in each lifecycle state to simulate a live
|
||||
# shop floor. Run after cleanup_demo_data.py.
|
||||
#
|
||||
# Strategy:
|
||||
# 1. Find seedable customer/part combos. Prefer parts with a coating
|
||||
# (so the SO-confirm flow runs end-to-end), but fall back to
|
||||
# direct fp.job creation with the only available recipe so we get
|
||||
# customer variety.
|
||||
# 2. For each target state, create N jobs and manipulate their
|
||||
# lifecycle state + step state to simulate a live shop.
|
||||
#
|
||||
# Usage: load this file from inside `odoo shell` via the standard
|
||||
# pattern documented in scripts/README.md.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
|
||||
random.seed(42) # reproducible
|
||||
|
||||
|
||||
def _build_combos(env):
|
||||
"""Return two lists:
|
||||
- via_so: (partner, part, coating) - for SO-confirm flow
|
||||
- direct: (partner, part_or_None, recipe) - for direct fp.job create
|
||||
|
||||
`via_so` requires a part with x_fc_default_coating_config_id whose
|
||||
recipe_id is set. `direct` covers all other customers/parts.
|
||||
"""
|
||||
via_so = []
|
||||
direct = []
|
||||
|
||||
# Prefer the canonical recipe; fall back to any recipe with operations.
|
||||
recipe = env['fusion.plating.process.node'].search([
|
||||
('node_type', '=', 'recipe'),
|
||||
('name', '=', 'ENP-ALUM-BASIC'),
|
||||
], limit=1)
|
||||
if not recipe:
|
||||
recipe = env['fusion.plating.process.node'].search([
|
||||
('node_type', '=', 'recipe'),
|
||||
], limit=1)
|
||||
if not recipe:
|
||||
print('ERROR: no recipes found. Cannot seed.')
|
||||
return via_so, direct, None
|
||||
|
||||
parts = env['fp.part.catalog'].search([])
|
||||
for p in parts:
|
||||
if not p.partner_id:
|
||||
continue
|
||||
if p.x_fc_default_coating_config_id and p.x_fc_default_coating_config_id.recipe_id:
|
||||
via_so.append((p.partner_id, p, p.x_fc_default_coating_config_id))
|
||||
else:
|
||||
direct.append((p.partner_id, p, recipe))
|
||||
|
||||
return via_so, direct, recipe
|
||||
|
||||
|
||||
def _create_so(env, partner, part, coating, qty, deadline_offset_days):
|
||||
"""Create + confirm a SO with one plating line. Returns (so, job)."""
|
||||
# fp.part.catalog has no product_id field — use a generic product
|
||||
# for the SO line. Plating-specific fields (x_fc_part_catalog_id,
|
||||
# x_fc_coating_config_id) carry the real linkage.
|
||||
fallback_product = env['product.product'].search(
|
||||
[('sale_ok', '=', True)], limit=1)
|
||||
if not fallback_product:
|
||||
fallback_product = env['product.product'].search([], limit=1)
|
||||
line_vals = {
|
||||
'product_id': fallback_product.id,
|
||||
'product_uom_qty': qty,
|
||||
'price_unit': 50.0 + qty * 2,
|
||||
}
|
||||
SOL_fields = env['sale.order.line']._fields
|
||||
if 'x_fc_part_catalog_id' in SOL_fields:
|
||||
line_vals['x_fc_part_catalog_id'] = part.id
|
||||
if 'x_fc_coating_config_id' in SOL_fields:
|
||||
line_vals['x_fc_coating_config_id'] = coating.id
|
||||
|
||||
so = env['sale.order'].sudo().create({
|
||||
'partner_id': partner.id,
|
||||
'client_order_ref': 'SEED-%s' % datetime.now().strftime('%H%M%S%f')[:10],
|
||||
'commitment_date': datetime.now() + timedelta(days=deadline_offset_days),
|
||||
'order_line': [(0, 0, line_vals)],
|
||||
})
|
||||
try:
|
||||
so.action_confirm()
|
||||
except Exception as e:
|
||||
print(' WARN: SO confirm failed for %s (%s) - %s' % (so.name, partner.name, e))
|
||||
return so, env['fp.job']
|
||||
job = env['fp.job'].sudo().search([('sale_order_id', '=', so.id)], limit=1)
|
||||
return so, job
|
||||
|
||||
|
||||
def _create_job_direct(env, partner, part, recipe, qty, deadline_offset_days):
|
||||
"""Direct fp.job create (skips the SO-confirm hook)."""
|
||||
Job = env['fp.job'].sudo()
|
||||
vals = {
|
||||
'partner_id': partner.id,
|
||||
'qty': qty,
|
||||
'date_deadline': datetime.now() + timedelta(days=deadline_offset_days),
|
||||
'recipe_id': recipe.id,
|
||||
'priority': random.choice(['low', 'normal', 'normal', 'high']),
|
||||
'quoted_revenue': 50.0 + qty * 2,
|
||||
}
|
||||
if part:
|
||||
vals['part_catalog_id'] = part.id
|
||||
# fp.part.catalog has no product_id field — leave fp.job.product_id
|
||||
# null. It's an optional field used as a "Reference Product".
|
||||
return Job.create(vals)
|
||||
|
||||
|
||||
def _operators(env):
|
||||
g = env.ref('fusion_plating.group_fusion_plating_operator',
|
||||
raise_if_not_found=False)
|
||||
if not g:
|
||||
return env['res.users']
|
||||
# Odoo 19: group <-> users m2m field on res.users is `all_group_ids`
|
||||
return env['res.users'].search([('all_group_ids', 'in', g.id)])
|
||||
|
||||
|
||||
def _confirm_and_steps(env, job):
|
||||
"""Drive a draft job through action_confirm + step generation."""
|
||||
if not job:
|
||||
return
|
||||
if job.state == 'draft':
|
||||
try:
|
||||
job.action_confirm()
|
||||
except Exception as e:
|
||||
print(' WARN: job %s action_confirm failed: %s' % (job.name, e))
|
||||
return
|
||||
if job.recipe_id and not job.step_ids:
|
||||
try:
|
||||
job._generate_steps_from_recipe()
|
||||
except Exception as e:
|
||||
print(' WARN: job %s step gen failed: %s' % (job.name, e))
|
||||
|
||||
|
||||
def run(env):
|
||||
print('=== Seeding fresh demo data ===')
|
||||
|
||||
via_so, direct, recipe = _build_combos(env)
|
||||
print(' via_so combos: %d' % len(via_so))
|
||||
print(' direct combos: %d' % len(direct))
|
||||
print(' recipe: %s' % (recipe.name if recipe else 'NONE'))
|
||||
if not recipe:
|
||||
return
|
||||
if not direct and not via_so:
|
||||
print('ERROR: no combos available. Cannot seed.')
|
||||
return
|
||||
|
||||
operators = _operators(env)
|
||||
print(' operators: %d' % len(operators))
|
||||
|
||||
counts = {
|
||||
'draft': 5,
|
||||
'confirmed': 6,
|
||||
'in_progress': 8,
|
||||
'on_hold': 3,
|
||||
'done': 6,
|
||||
'cancelled': 3,
|
||||
}
|
||||
|
||||
via_so_idx = 0
|
||||
direct_idx = 0
|
||||
|
||||
def _next_via_so():
|
||||
nonlocal via_so_idx
|
||||
if not via_so:
|
||||
return None
|
||||
c = via_so[via_so_idx % len(via_so)]
|
||||
via_so_idx += 1
|
||||
return c
|
||||
|
||||
def _next_direct():
|
||||
nonlocal direct_idx
|
||||
if not direct:
|
||||
return None
|
||||
c = direct[direct_idx % len(direct)]
|
||||
direct_idx += 1
|
||||
return c
|
||||
|
||||
def _next_combo(prefer_so=False):
|
||||
if prefer_so and via_so:
|
||||
return ('so', _next_via_so())
|
||||
if direct:
|
||||
return ('direct', _next_direct())
|
||||
if via_so:
|
||||
return ('so', _next_via_so())
|
||||
return (None, None)
|
||||
|
||||
created = {state: [] for state in counts}
|
||||
|
||||
# 1. DRAFT - direct create, do NOT confirm
|
||||
print('-- Creating draft jobs --')
|
||||
for i in range(counts['draft']):
|
||||
kind, combo = _next_combo()
|
||||
if not combo:
|
||||
break
|
||||
partner, part, coating_or_recipe = combo
|
||||
if kind == 'so':
|
||||
job = _create_job_direct(
|
||||
env, partner, part, coating_or_recipe.recipe_id,
|
||||
qty=random.choice([1, 5, 10, 25, 50]),
|
||||
deadline_offset_days=random.randint(7, 30),
|
||||
)
|
||||
if part.x_fc_default_coating_config_id:
|
||||
job.coating_config_id = part.x_fc_default_coating_config_id.id
|
||||
else:
|
||||
job = _create_job_direct(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([1, 5, 10, 25, 50]),
|
||||
deadline_offset_days=random.randint(7, 30),
|
||||
)
|
||||
created['draft'].append(job)
|
||||
print(' draft: %s (%s)' % (job.name, partner.name))
|
||||
|
||||
# 2. CONFIRMED
|
||||
print('-- Creating confirmed jobs --')
|
||||
for i in range(counts['confirmed']):
|
||||
prefer_so = (i % 2 == 0)
|
||||
kind, combo = _next_combo(prefer_so=prefer_so)
|
||||
if not combo:
|
||||
break
|
||||
partner, part, coating_or_recipe = combo
|
||||
if kind == 'so':
|
||||
so, job = _create_so(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([5, 10, 25, 50, 100]),
|
||||
deadline_offset_days=random.randint(5, 25),
|
||||
)
|
||||
_confirm_and_steps(env, job)
|
||||
else:
|
||||
job = _create_job_direct(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([5, 10, 25, 50, 100]),
|
||||
deadline_offset_days=random.randint(5, 25),
|
||||
)
|
||||
_confirm_and_steps(env, job)
|
||||
if job:
|
||||
created['confirmed'].append(job)
|
||||
print(' confirmed: %s (%s, %d steps)' % (
|
||||
job.name, partner.name, len(job.step_ids)))
|
||||
|
||||
# 3. IN_PROGRESS
|
||||
print('-- Creating in_progress jobs --')
|
||||
for i in range(counts['in_progress']):
|
||||
kind, combo = _next_combo()
|
||||
if not combo:
|
||||
break
|
||||
partner, part, coating_or_recipe = combo
|
||||
if kind == 'so':
|
||||
so, job = _create_so(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([5, 10, 25, 50]),
|
||||
deadline_offset_days=random.randint(3, 15),
|
||||
)
|
||||
else:
|
||||
job = _create_job_direct(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([5, 10, 25, 50]),
|
||||
deadline_offset_days=random.randint(3, 15),
|
||||
)
|
||||
if not job:
|
||||
continue
|
||||
_confirm_and_steps(env, job)
|
||||
job.state = 'in_progress'
|
||||
job.date_started = datetime.now() - timedelta(days=random.randint(1, 5))
|
||||
steps = job.step_ids.sorted('sequence')
|
||||
if not steps:
|
||||
print(' WARN: in_progress job %s has no steps' % job.name)
|
||||
created['in_progress'].append(job)
|
||||
continue
|
||||
for s in steps:
|
||||
if operators:
|
||||
s.assigned_user_id = operators[
|
||||
random.randrange(len(operators))
|
||||
]
|
||||
n_done = max(1, int(len(steps) * random.uniform(0.3, 0.6)))
|
||||
for s in steps[:n_done]:
|
||||
s.state = 'done'
|
||||
s.date_started = datetime.now() - timedelta(
|
||||
hours=random.randint(2, 48))
|
||||
s.date_finished = s.date_started + timedelta(
|
||||
minutes=random.randint(15, 240))
|
||||
s.duration_actual = (
|
||||
s.date_finished - s.date_started).total_seconds() / 60.0
|
||||
s.started_by_user_id = s.assigned_user_id or env.user
|
||||
s.finished_by_user_id = s.assigned_user_id or env.user
|
||||
if n_done < len(steps):
|
||||
cur = steps[n_done]
|
||||
cur.state = 'in_progress'
|
||||
cur.date_started = datetime.now() - timedelta(
|
||||
minutes=random.randint(5, 90))
|
||||
cur.started_by_user_id = cur.assigned_user_id or env.user
|
||||
env['fp.job.step.timelog'].sudo().create({
|
||||
'step_id': cur.id,
|
||||
'user_id': (cur.assigned_user_id.id
|
||||
if cur.assigned_user_id else env.user.id),
|
||||
'date_started': cur.date_started,
|
||||
})
|
||||
if n_done + 1 < len(steps):
|
||||
steps[n_done + 1].state = 'ready'
|
||||
created['in_progress'].append(job)
|
||||
print(' in_progress: %s (%s, %d/%d done)' % (
|
||||
job.name, partner.name, n_done, len(steps)))
|
||||
|
||||
# 4. ON_HOLD
|
||||
print('-- Creating on_hold jobs --')
|
||||
for i in range(counts['on_hold']):
|
||||
kind, combo = _next_combo()
|
||||
if not combo:
|
||||
break
|
||||
partner, part, coating_or_recipe = combo
|
||||
if kind == 'so':
|
||||
so, job = _create_so(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([5, 10, 25]),
|
||||
deadline_offset_days=random.randint(5, 20),
|
||||
)
|
||||
else:
|
||||
job = _create_job_direct(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([5, 10, 25]),
|
||||
deadline_offset_days=random.randint(5, 20),
|
||||
)
|
||||
if not job:
|
||||
continue
|
||||
_confirm_and_steps(env, job)
|
||||
steps = job.step_ids.sorted('sequence')
|
||||
for s in steps[:2]:
|
||||
s.state = 'done'
|
||||
s.date_finished = datetime.now() - timedelta(days=1)
|
||||
s.date_started = s.date_finished - timedelta(minutes=60)
|
||||
s.duration_actual = 60.0
|
||||
if len(steps) > 2:
|
||||
steps[2].state = 'paused'
|
||||
steps[2].date_started = datetime.now() - timedelta(hours=4)
|
||||
job.state = 'on_hold'
|
||||
created['on_hold'].append(job)
|
||||
print(' on_hold: %s (%s)' % (job.name, partner.name))
|
||||
|
||||
# 5. DONE
|
||||
print('-- Creating done jobs --')
|
||||
for i in range(counts['done']):
|
||||
kind, combo = _next_combo()
|
||||
if not combo:
|
||||
break
|
||||
partner, part, coating_or_recipe = combo
|
||||
if kind == 'so':
|
||||
so, job = _create_so(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([1, 5, 10, 25]),
|
||||
deadline_offset_days=random.randint(-5, 5),
|
||||
)
|
||||
else:
|
||||
job = _create_job_direct(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([1, 5, 10, 25]),
|
||||
deadline_offset_days=random.randint(-5, 5),
|
||||
)
|
||||
if not job:
|
||||
continue
|
||||
_confirm_and_steps(env, job)
|
||||
steps = job.step_ids.sorted('sequence')
|
||||
for j, s in enumerate(steps):
|
||||
s.state = 'done'
|
||||
offset = (len(steps) - j) * 30
|
||||
s.date_started = datetime.now() - timedelta(minutes=offset + 30)
|
||||
s.date_finished = datetime.now() - timedelta(minutes=offset)
|
||||
s.duration_actual = 30.0
|
||||
if operators:
|
||||
op = operators[random.randrange(len(operators))]
|
||||
s.assigned_user_id = op
|
||||
s.started_by_user_id = op
|
||||
s.finished_by_user_id = op
|
||||
# Set state directly to avoid downstream side effects (delivery
|
||||
# + cert auto-create) on demo data.
|
||||
job.state = 'done'
|
||||
job.date_finished = datetime.now() - timedelta(
|
||||
hours=random.randint(1, 48))
|
||||
job.date_started = datetime.now() - timedelta(days=2)
|
||||
created['done'].append(job)
|
||||
print(' done: %s (%s)' % (job.name, partner.name))
|
||||
|
||||
# 6. CANCELLED
|
||||
print('-- Creating cancelled jobs --')
|
||||
for i in range(counts['cancelled']):
|
||||
kind, combo = _next_combo()
|
||||
if not combo:
|
||||
break
|
||||
partner, part, coating_or_recipe = combo
|
||||
if kind == 'so':
|
||||
so, job = _create_so(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([5, 10]),
|
||||
deadline_offset_days=random.randint(10, 30),
|
||||
)
|
||||
else:
|
||||
job = _create_job_direct(
|
||||
env, partner, part, coating_or_recipe,
|
||||
qty=random.choice([5, 10]),
|
||||
deadline_offset_days=random.randint(10, 30),
|
||||
)
|
||||
if not job:
|
||||
continue
|
||||
_confirm_and_steps(env, job)
|
||||
try:
|
||||
job.action_cancel()
|
||||
except Exception:
|
||||
job.state = 'cancelled'
|
||||
created['cancelled'].append(job)
|
||||
print(' cancelled: %s (%s)' % (job.name, partner.name))
|
||||
|
||||
env.cr.commit()
|
||||
|
||||
print()
|
||||
print('=== Seed summary ===')
|
||||
for state, jobs in created.items():
|
||||
print(' %s: %d jobs' % (state, len(jobs)))
|
||||
|
||||
print()
|
||||
print('=== Verification ===')
|
||||
Job = env['fp.job']
|
||||
for state in counts:
|
||||
print(' fp.job state=%s: actual=%d' % (
|
||||
state, Job.search_count([('state', '=', state)])))
|
||||
|
||||
|
||||
try:
|
||||
run(env)
|
||||
except NameError:
|
||||
print('Run inside `odoo shell`.')
|
||||
Reference in New Issue
Block a user