feat(jobs): comprehensive workflow seed — quotation through paid invoice
Builds 7-8 orders in each of 13 workflow states to simulate a full pipeline: - Quotation (sale.order draft) - Quote Sent (sale.order sent) - Order Confirmed / Job Confirmed - Job In Progress (Early + Mid) - Job On Hold (with quality hold) - Job Done / Delivery Draft - Delivery Scheduled / En Route / Delivered - Invoice Draft / Posted / Paid Each record fills detailed fields: PO numbers, commitment dates, operator assignments, timelogs, thickness readings on certs, delivery contacts/vehicles/drivers, payment journals, etc. Idempotency-ish: each order wrapped in a savepoint so one failure doesn't crash the whole seed. Workflow walkthrough findings encoded in script header docstring, including the gotchas: (1) SO action_confirm creates fp.job in DRAFT state with 0 steps — must call action_confirm + _generate_steps_from_recipe explicitly; (2) invoice_payment_term_id is REQUIRED to post invoices; (3) account.payment.action_validate moves payment to 'paid' but invoice goes to 'in_payment' (not 'paid' — Odoo 19 design, requires bank reconciliation for full 'paid' state). 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:
@@ -0,0 +1,785 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2026 Nexa Systems Inc.
|
||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||
#
|
||||
# seed_workflow_states.py
|
||||
# =======================
|
||||
# Builds 7-8 sale orders in EACH lifecycle state from quotation through
|
||||
# paid invoice. The goal is a realistic dataset that exercises the full
|
||||
# pipeline so we can validate UI, reports, KPIs, and notifications.
|
||||
#
|
||||
# Workflow walkthrough findings (run end-to-end on entech 2026-04-25):
|
||||
#
|
||||
# STAGE AUTO-CREATES REQUIRED FIELDS
|
||||
# ----------------------------------------------------------------------
|
||||
# sale.order draft nothing partner_id, order_line
|
||||
# commitment_date is optional but
|
||||
# we always set it for realism
|
||||
# sale.order sent nothing write state="sent"
|
||||
# sale.order action_confirm fp.job (state=DRAFT, client_order_ref recommended;
|
||||
# step_count=0, recipe payment_term_id REQUIRED for
|
||||
# resolved from coating) downstream invoice posting
|
||||
# fp.job action_confirm portal_job_id state moves draft -> confirmed
|
||||
# fp.job._generate_steps_ step_ids populated must be called explicitly;
|
||||
# from_recipe() action_confirm does NOT do it
|
||||
# fp.job button_mark_done delivery (draft) + all steps must be done first;
|
||||
# cert (draft, type=coc) sets state=done, date_finished
|
||||
# fp.delivery scheduled -- scheduled_date, contact_name,
|
||||
# contact_phone, delivery_address_id,
|
||||
# x_fc_box_count_out,
|
||||
# assigned_driver_id (hr.employee)
|
||||
# fp.delivery delivered -- delivered_at
|
||||
# fp.certificate issued -- issue_date, issued_by_id,
|
||||
# entech_wo_number, customer_job_no,
|
||||
# po_number, quantity_shipped,
|
||||
# part_number,
|
||||
# thickness_reading_ids (3-5 rows)
|
||||
# account.move (draft) via so._create_invoices() invoice_date, invoice_date_due,
|
||||
# invoice_payment_term_id REQUIRED
|
||||
# or post fails
|
||||
# account.move action_post name=INV/YYYY/NNNNN invoice_date_due
|
||||
# account.payment.register account.payment partner_type, journal_id,
|
||||
# wizard (state=in_process) payment_method_line_id,
|
||||
# amount, payment_date
|
||||
# account.payment state=paid on payment; ALL upstream prerequisites
|
||||
# action_validate invoice payment_state= above
|
||||
# in_payment (not paid - (Odoo 19 design - paid state
|
||||
# that requires bank requires bank reconciliation)
|
||||
# statement reconciliation)
|
||||
#
|
||||
# Strategy:
|
||||
# - Each order is wrapped in its OWN savepoint. If anything fails, we
|
||||
# ROLLBACK that savepoint and continue to the next order.
|
||||
# - We commit at the end of each stage so partial successes still land.
|
||||
# - Customer/part variety: spread across 10+ partners, cycle through all
|
||||
# parts that have coatings. Operators round-robin across the 20.
|
||||
# - Date variety: past 60 days for delivered/paid; future 1-30 days for
|
||||
# active jobs.
|
||||
#
|
||||
# Usage (entech): see scripts/README.md.
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import random
|
||||
import logging
|
||||
|
||||
random.seed(2026)
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Stage targets - these are the seed counts per stage requested by spec.
|
||||
# Adjust here if you want fewer/more.
|
||||
TARGETS = {
|
||||
"so_draft": 8,
|
||||
"so_sent": 7,
|
||||
"job_confirmed_no_steps_started": 8,
|
||||
"job_in_progress_early": 7,
|
||||
"job_in_progress_mid": 8,
|
||||
"job_on_hold": 5,
|
||||
"job_done_delivery_draft": 7,
|
||||
"delivery_scheduled": 7,
|
||||
"delivery_en_route": 5,
|
||||
"delivery_delivered": 8,
|
||||
"invoice_draft": 7,
|
||||
"invoice_posted": 7,
|
||||
"paid": 7,
|
||||
}
|
||||
|
||||
|
||||
def _pick_combo(combos, idx):
|
||||
return combos[idx % len(combos)]
|
||||
|
||||
|
||||
def _build_combos(env):
|
||||
"""List of (partner, part, coating) for SO-confirm seeding."""
|
||||
combos = []
|
||||
parts = env["fp.part.catalog"].search([
|
||||
("x_fc_default_coating_config_id", "!=", False),
|
||||
("x_fc_default_coating_config_id.recipe_id", "!=", False),
|
||||
("partner_id", "!=", False),
|
||||
])
|
||||
for p in parts:
|
||||
combos.append((p.partner_id, p, p.x_fc_default_coating_config_id))
|
||||
random.shuffle(combos)
|
||||
return combos
|
||||
|
||||
|
||||
def _operators(env):
|
||||
g = env.ref("fusion_plating.group_fusion_plating_operator",
|
||||
raise_if_not_found=False)
|
||||
if not g:
|
||||
return env["res.users"]
|
||||
return env["res.users"].search([("all_group_ids", "in", g.id)])
|
||||
|
||||
|
||||
def _managers(env):
|
||||
g = env.ref("fusion_plating.group_fusion_plating_manager",
|
||||
raise_if_not_found=False)
|
||||
if not g:
|
||||
return env["res.users"]
|
||||
return env["res.users"].search([("all_group_ids", "in", g.id)])
|
||||
|
||||
|
||||
def _employees(env):
|
||||
return env["hr.employee"].search([])
|
||||
|
||||
|
||||
def _resolve_product(env):
|
||||
"""Find the right plating-service product to use for SO lines."""
|
||||
p = env["product.product"].search(
|
||||
[("name", "=", "Plating Service")], limit=1)
|
||||
if p:
|
||||
return p
|
||||
p = env["product.product"].search(
|
||||
[("sale_ok", "=", True), ("type", "=", "service")], limit=1)
|
||||
if p:
|
||||
return p
|
||||
return env["product.product"].search([("sale_ok", "=", True)], limit=1)
|
||||
|
||||
|
||||
def _resolve_payment_term(env):
|
||||
"""Net 30 by default."""
|
||||
pt = env["account.payment.term"].search(
|
||||
[("name", "=", "30 Days")], limit=1)
|
||||
if not pt:
|
||||
pt = env["account.payment.term"].search([], limit=1)
|
||||
return pt
|
||||
|
||||
|
||||
def _resolve_journals(env):
|
||||
sales = env["account.journal"].search([("type", "=", "sale")], limit=1)
|
||||
bank = env["account.journal"].search([("type", "=", "bank")], limit=1)
|
||||
return sales, bank
|
||||
|
||||
|
||||
def _resolve_facility(env):
|
||||
return env["fusion.plating.facility"].search([], limit=1)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def _make_so(env, partner, part, coating, qty, price, ctx):
|
||||
"""Create a draft SO with one detailed plating line."""
|
||||
SOL_fields = env["sale.order.line"]._fields
|
||||
line_vals = {
|
||||
"product_id": ctx["product"].id,
|
||||
"product_uom_qty": qty,
|
||||
"price_unit": price,
|
||||
"name": "ENP plating service: %s rev %s" % (
|
||||
part.part_number or part.name, part.revision or "A"),
|
||||
}
|
||||
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
|
||||
if "x_fc_internal_description" in SOL_fields:
|
||||
line_vals["x_fc_internal_description"] = (
|
||||
"Internal: %s, %.1f mils target, mil-spec" % (
|
||||
coating.name, coating.thickness_max or 1.0))
|
||||
|
||||
so_vals = {
|
||||
"partner_id": partner.id,
|
||||
"partner_invoice_id": partner.id,
|
||||
"partner_shipping_id": partner.id,
|
||||
"client_order_ref": "CUST-PO-%05d" % random.randint(10000, 99999),
|
||||
"commitment_date": datetime.now() + timedelta(
|
||||
days=random.randint(7, 30)),
|
||||
"validity_date": (datetime.now() + timedelta(days=30)).date(),
|
||||
"payment_term_id": ctx["payment_term"].id,
|
||||
"order_line": [(0, 0, line_vals)],
|
||||
}
|
||||
SO_fields = env["sale.order"]._fields
|
||||
if "x_fc_po_number" in SO_fields:
|
||||
so_vals["x_fc_po_number"] = "PO-%05d" % random.randint(10000, 99999)
|
||||
if "x_fc_part_catalog_id" in SO_fields:
|
||||
so_vals["x_fc_part_catalog_id"] = part.id
|
||||
if "x_fc_coating_config_id" in SO_fields:
|
||||
so_vals["x_fc_coating_config_id"] = coating.id
|
||||
if "x_fc_internal_note" in SO_fields:
|
||||
so_vals["x_fc_internal_note"] = (
|
||||
"<p>Customer is OK with rush production if capacity allows.</p>")
|
||||
if "x_fc_external_note" in SO_fields:
|
||||
so_vals["x_fc_external_note"] = (
|
||||
"<p>Please confirm receipt of parts before processing.</p>")
|
||||
return env["sale.order"].sudo().create(so_vals)
|
||||
|
||||
|
||||
def _populate_job(env, job, ctx, fill_facility=True, fill_manager=True):
|
||||
"""Fill out fp.job extra fields after creation."""
|
||||
if not job:
|
||||
return
|
||||
vals = {}
|
||||
if fill_facility and ctx["facility"] and not job.facility_id:
|
||||
vals["facility_id"] = ctx["facility"].id
|
||||
if fill_manager and ctx["managers"] and not job.manager_id:
|
||||
vals["manager_id"] = random.choice(ctx["managers"]).id
|
||||
if not job.priority or job.priority == "normal":
|
||||
vals["priority"] = random.choices(
|
||||
["low", "normal", "high", "rush"],
|
||||
weights=[10, 70, 15, 5],
|
||||
)[0]
|
||||
if vals:
|
||||
job.write(vals)
|
||||
|
||||
|
||||
def _ensure_steps(env, job):
|
||||
"""Force step generation. action_confirm doesn t do this on its own."""
|
||||
if not job:
|
||||
return
|
||||
if not job.recipe_id:
|
||||
return
|
||||
if job.step_ids:
|
||||
return
|
||||
try:
|
||||
job._generate_steps_from_recipe()
|
||||
except Exception as e:
|
||||
_logger.warning("Job %s step gen failed: %s", job.name, e)
|
||||
|
||||
|
||||
def _assign_step_users(env, job, ctx, n_done=0, current_idx=None):
|
||||
"""Assign operators to all steps; mark first n_done as done, and
|
||||
optionally one step at current_idx as in_progress.
|
||||
"""
|
||||
operators = ctx["operators"]
|
||||
steps = job.step_ids.sorted("sequence")
|
||||
if not steps:
|
||||
return
|
||||
for s in steps:
|
||||
if operators and not s.assigned_user_id:
|
||||
s.assigned_user_id = operators[
|
||||
random.randrange(len(operators))]
|
||||
|
||||
base = datetime.now() - timedelta(hours=len(steps) * 2)
|
||||
for i, s in enumerate(steps[:n_done]):
|
||||
start = base + timedelta(hours=i * 2)
|
||||
finish = start + timedelta(minutes=random.randint(20, 90))
|
||||
s.write({
|
||||
"state": "done",
|
||||
"date_started": start,
|
||||
"date_finished": finish,
|
||||
"duration_actual": (finish - start).total_seconds() / 60.0,
|
||||
"started_by_user_id": s.assigned_user_id.id if s.assigned_user_id else env.user.id,
|
||||
"finished_by_user_id": s.assigned_user_id.id if s.assigned_user_id else env.user.id,
|
||||
})
|
||||
env["fp.job.step.timelog"].sudo().create({
|
||||
"step_id": s.id,
|
||||
"user_id": s.assigned_user_id.id if s.assigned_user_id else env.user.id,
|
||||
"date_started": start,
|
||||
"date_finished": finish,
|
||||
"duration_minutes": (finish - start).total_seconds() / 60.0,
|
||||
})
|
||||
|
||||
if current_idx is not None and current_idx < len(steps):
|
||||
cur = steps[current_idx]
|
||||
if cur.state != "done":
|
||||
start = datetime.now() - timedelta(
|
||||
minutes=random.randint(5, 90))
|
||||
cur.write({
|
||||
"state": "in_progress",
|
||||
"date_started": start,
|
||||
"started_by_user_id": cur.assigned_user_id.id if cur.assigned_user_id else env.user.id,
|
||||
})
|
||||
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": start,
|
||||
})
|
||||
|
||||
if current_idx is not None and current_idx + 1 < len(steps):
|
||||
next_step = steps[current_idx + 1]
|
||||
if next_step.state == "pending":
|
||||
next_step.write({"state": "ready"})
|
||||
|
||||
|
||||
def _fill_step_realistic_data(env, job):
|
||||
for s in job.step_ids:
|
||||
kind = s.kind
|
||||
if kind == "bake":
|
||||
if not s.bake_setpoint_temp:
|
||||
s.bake_setpoint_temp = random.choice([375.0, 400.0, 425.0])
|
||||
if not s.bake_actual_duration and s.state == "done":
|
||||
s.bake_actual_duration = random.uniform(3.5, 4.5)
|
||||
elif kind == "wet":
|
||||
if not s.thickness_target:
|
||||
s.thickness_target = round(random.uniform(0.5, 2.0), 2)
|
||||
s.thickness_uom = "mil"
|
||||
|
||||
|
||||
def _make_delivery_full(env, delivery, partner, ctx, state, scheduled_offset_days=1):
|
||||
"""Fill delivery with realistic logistics fields and advance state."""
|
||||
if not delivery:
|
||||
return
|
||||
employees = ctx["employees"]
|
||||
facility = ctx["facility"]
|
||||
vals = {
|
||||
"delivery_address_id": partner.id,
|
||||
"contact_name": partner.name,
|
||||
"contact_phone": partner.phone or "555-%04d" % random.randint(1000, 9999),
|
||||
"scheduled_date": datetime.now() + timedelta(days=scheduled_offset_days),
|
||||
}
|
||||
if "x_fc_box_count_out" in delivery._fields:
|
||||
vals["x_fc_box_count_out"] = random.randint(1, 5)
|
||||
if employees and "assigned_driver_id" in delivery._fields:
|
||||
vals["assigned_driver_id"] = employees[
|
||||
random.randrange(len(employees))].id
|
||||
if facility and "source_facility_id" in delivery._fields:
|
||||
vals["source_facility_id"] = facility.id
|
||||
if "notes" in delivery._fields:
|
||||
vals["notes"] = (
|
||||
"<p>Standard delivery - handle with care, parts plated to spec.</p>")
|
||||
delivery.write(vals)
|
||||
delivery.write({"state": state})
|
||||
if state == "delivered":
|
||||
delivery.write({"delivered_at": datetime.now() - timedelta(
|
||||
hours=random.randint(1, 48))})
|
||||
|
||||
|
||||
def _issue_certificate(env, job, so, part, ctx):
|
||||
cert = env["fp.certificate"].search(
|
||||
[("x_fc_job_id", "=", job.id)], limit=1)
|
||||
if not cert:
|
||||
cert = env["fp.certificate"].sudo().create({
|
||||
"partner_id": job.partner_id.id,
|
||||
"certificate_type": "coc",
|
||||
"state": "draft",
|
||||
"x_fc_job_id": job.id,
|
||||
"sale_order_id": so.id if so else False,
|
||||
})
|
||||
vals = {
|
||||
"state": "issued",
|
||||
"issue_date": datetime.now().date(),
|
||||
"issued_by_id": env.user.id,
|
||||
"entech_wo_number": job.name,
|
||||
"customer_job_no": so.client_order_ref if so else "",
|
||||
"po_number": so.x_fc_po_number if so and "x_fc_po_number" in so._fields else "",
|
||||
"quantity_shipped": int(job.qty or 1),
|
||||
"part_number": part.part_number or part.name,
|
||||
"process_description": "Electroless Nickel Plating, MIL-C-26074",
|
||||
}
|
||||
if "spec_min_mils" in cert._fields and part.x_fc_default_coating_config_id:
|
||||
c = part.x_fc_default_coating_config_id
|
||||
if c.thickness_min:
|
||||
vals["spec_min_mils"] = c.thickness_min
|
||||
if c.thickness_max:
|
||||
vals["spec_max_mils"] = c.thickness_max
|
||||
vals["spec_reference"] = c.spec_reference or "AMS-2404"
|
||||
cert.write(vals)
|
||||
for i in range(5):
|
||||
env["fp.thickness.reading"].sudo().create({
|
||||
"certificate_id": cert.id,
|
||||
"reading_number": i + 1,
|
||||
"nip_mils": round(random.uniform(0.95, 1.15), 3),
|
||||
"ni_percent": round(random.uniform(88.0, 92.0), 2),
|
||||
"p_percent": round(random.uniform(8.0, 12.0), 2),
|
||||
"position_label": "Pos %d" % (i + 1),
|
||||
"reading_datetime": datetime.now() - timedelta(
|
||||
minutes=30 - i * 5),
|
||||
"operator_id": env.user.id,
|
||||
"x_fc_job_id": job.id,
|
||||
"equipment_model": "Fischerscope X-Ray XDV-SD",
|
||||
"calibration_std_ref": "CAL-2026-04-01",
|
||||
})
|
||||
return cert
|
||||
|
||||
|
||||
def _create_quality_hold(env, job, ctx):
|
||||
if "fusion.plating.quality.hold" not in env:
|
||||
return
|
||||
Hold = env["fusion.plating.quality.hold"].sudo()
|
||||
steps = job.step_ids.sorted("sequence")
|
||||
affected_step = None
|
||||
for s in steps:
|
||||
if s.state in ("paused", "in_progress"):
|
||||
affected_step = s
|
||||
break
|
||||
vals = {
|
||||
"hold_reason": random.choice(
|
||||
["out_of_spec", "damaged", "contamination", "process_deviation"]),
|
||||
"qty_on_hold": max(1, int((job.qty or 1) // 4)),
|
||||
"qty_original": int(job.qty or 1),
|
||||
"description": "Sample inspection caught dimensional drift on first-piece. Holding for engineering review.",
|
||||
"state": "on_hold",
|
||||
}
|
||||
if "x_fc_job_id" in Hold._fields:
|
||||
vals["x_fc_job_id"] = job.id
|
||||
if affected_step and "x_fc_step_id" in Hold._fields:
|
||||
vals["x_fc_step_id"] = affected_step.id
|
||||
if ctx["facility"] and "facility_id" in Hold._fields:
|
||||
vals["facility_id"] = ctx["facility"].id
|
||||
if "operator_id" in Hold._fields and ctx["operators"]:
|
||||
vals["operator_id"] = random.choice(ctx["operators"]).id
|
||||
if "part_ref" in Hold._fields:
|
||||
vals["part_ref"] = job.part_catalog_id.part_number if job.part_catalog_id else ""
|
||||
try:
|
||||
Hold.create(vals)
|
||||
except Exception as e:
|
||||
_logger.warning("Hold create failed for %s: %s", job.name, e)
|
||||
|
||||
|
||||
def _create_invoice(env, so, ctx, post=False):
|
||||
inv_recordset = so._create_invoices()
|
||||
if not inv_recordset:
|
||||
return env["account.move"]
|
||||
inv = inv_recordset[0] if hasattr(inv_recordset, "ids") else env["account.move"].browse(inv_recordset)
|
||||
inv_vals = {
|
||||
"invoice_date": (datetime.now() - timedelta(
|
||||
days=random.randint(0, 5))).date(),
|
||||
"invoice_date_due": (datetime.now() + timedelta(
|
||||
days=random.randint(15, 30))).date(),
|
||||
}
|
||||
if not inv.invoice_payment_term_id:
|
||||
inv_vals["invoice_payment_term_id"] = ctx["payment_term"].id
|
||||
inv.write(inv_vals)
|
||||
if post:
|
||||
inv.action_post()
|
||||
return inv
|
||||
|
||||
|
||||
def _register_payment(env, inv, ctx, validate=True):
|
||||
bank = ctx["bank_journal"]
|
||||
pml = bank.inbound_payment_method_line_ids[:1]
|
||||
wizard = env["account.payment.register"].with_context(
|
||||
active_model="account.move",
|
||||
active_ids=inv.ids,
|
||||
).create({
|
||||
"amount": inv.amount_total,
|
||||
"journal_id": bank.id,
|
||||
"payment_method_line_id": pml.id if pml else False,
|
||||
"payment_date": (datetime.now() - timedelta(
|
||||
days=random.randint(0, 7))).date(),
|
||||
})
|
||||
wizard.action_create_payments()
|
||||
pmt = env["account.payment"].search(
|
||||
[("partner_id", "=", inv.partner_id.id),
|
||||
("amount", "=", inv.amount_total)],
|
||||
order="id desc", limit=1)
|
||||
if pmt and validate:
|
||||
try:
|
||||
pmt.action_validate()
|
||||
except Exception as e:
|
||||
_logger.warning("Payment validate failed: %s", e)
|
||||
try:
|
||||
pmt.write({"state": "paid"})
|
||||
except Exception as e2:
|
||||
_logger.warning("Payment direct write paid failed: %s", e2)
|
||||
return pmt
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def _stage(env, label, fn, n, combos, idx_holder, ctx, results):
|
||||
print("-- %s (target %d) --" % (label, n))
|
||||
success = 0
|
||||
sp_safe = "".join(c if c.isalnum() else "_" for c in label).strip("_")
|
||||
sp_label = "seed_" + sp_safe
|
||||
for i in range(n):
|
||||
partner, part, coating = _pick_combo(combos, idx_holder[0])
|
||||
idx_holder[0] += 1
|
||||
sp = "%s_%d" % (sp_label, i)
|
||||
env.cr.execute("SAVEPOINT %s" % sp)
|
||||
try:
|
||||
if fn(env, partner, part, coating, ctx):
|
||||
env.cr.execute("RELEASE SAVEPOINT %s" % sp)
|
||||
success += 1
|
||||
else:
|
||||
env.cr.execute("ROLLBACK TO SAVEPOINT %s" % sp)
|
||||
except Exception as e:
|
||||
print(" WARN [%s #%d]: %s" % (label, i, e))
|
||||
try:
|
||||
env.cr.execute("ROLLBACK TO SAVEPOINT %s" % sp)
|
||||
except Exception:
|
||||
pass
|
||||
results[label] = success
|
||||
print(" -> %d/%d succeeded" % (success, n))
|
||||
|
||||
|
||||
# -------------------- Stage handlers --------------------
|
||||
def stage_so_draft(env, partner, part, coating, ctx):
|
||||
so = _make_so(env, partner, part, coating,
|
||||
qty=random.choice([5, 10, 25, 50, 100]),
|
||||
price=random.uniform(75.0, 350.0), ctx=ctx)
|
||||
return bool(so)
|
||||
|
||||
|
||||
def stage_so_sent(env, partner, part, coating, ctx):
|
||||
so = _make_so(env, partner, part, coating,
|
||||
qty=random.choice([10, 25, 50, 100]),
|
||||
price=random.uniform(75.0, 350.0), ctx=ctx)
|
||||
so.write({"state": "sent"})
|
||||
return True
|
||||
|
||||
|
||||
def stage_job_confirmed(env, partner, part, coating, ctx):
|
||||
so = _make_so(env, partner, part, coating,
|
||||
qty=random.choice([10, 25, 50]),
|
||||
price=random.uniform(100.0, 350.0), ctx=ctx)
|
||||
so.action_confirm()
|
||||
job = env["fp.job"].search([("sale_order_id", "=", so.id)], limit=1)
|
||||
if not job:
|
||||
return False
|
||||
if job.state == "draft":
|
||||
job.action_confirm()
|
||||
_ensure_steps(env, job)
|
||||
_populate_job(env, job, ctx)
|
||||
_assign_step_users(env, job, ctx, n_done=0, current_idx=None)
|
||||
_fill_step_realistic_data(env, job)
|
||||
return True
|
||||
|
||||
|
||||
def stage_job_in_progress_early(env, partner, part, coating, ctx):
|
||||
so = _make_so(env, partner, part, coating,
|
||||
qty=random.choice([10, 25, 50]),
|
||||
price=random.uniform(100.0, 300.0), ctx=ctx)
|
||||
so.action_confirm()
|
||||
job = env["fp.job"].search([("sale_order_id", "=", so.id)], limit=1)
|
||||
if not job:
|
||||
return False
|
||||
if job.state == "draft":
|
||||
job.action_confirm()
|
||||
_ensure_steps(env, job)
|
||||
_populate_job(env, job, ctx)
|
||||
n_done = random.choice([1, 2])
|
||||
_assign_step_users(env, job, ctx, n_done=n_done, current_idx=n_done)
|
||||
_fill_step_realistic_data(env, job)
|
||||
job.write({
|
||||
"state": "in_progress",
|
||||
"date_started": datetime.now() - timedelta(
|
||||
days=random.randint(1, 4)),
|
||||
})
|
||||
return True
|
||||
|
||||
|
||||
def stage_job_in_progress_mid(env, partner, part, coating, ctx):
|
||||
so = _make_so(env, partner, part, coating,
|
||||
qty=random.choice([10, 25, 50]),
|
||||
price=random.uniform(100.0, 300.0), ctx=ctx)
|
||||
so.action_confirm()
|
||||
job = env["fp.job"].search([("sale_order_id", "=", so.id)], limit=1)
|
||||
if not job:
|
||||
return False
|
||||
if job.state == "draft":
|
||||
job.action_confirm()
|
||||
_ensure_steps(env, job)
|
||||
_populate_job(env, job, ctx)
|
||||
total = len(job.step_ids)
|
||||
n_done = max(1, total // 2) if total else 0
|
||||
_assign_step_users(env, job, ctx, n_done=n_done, current_idx=n_done)
|
||||
_fill_step_realistic_data(env, job)
|
||||
job.write({
|
||||
"state": "in_progress",
|
||||
"date_started": datetime.now() - timedelta(
|
||||
days=random.randint(2, 7)),
|
||||
})
|
||||
return True
|
||||
|
||||
|
||||
def stage_job_on_hold(env, partner, part, coating, ctx):
|
||||
so = _make_so(env, partner, part, coating,
|
||||
qty=random.choice([10, 25, 50]),
|
||||
price=random.uniform(100.0, 300.0), ctx=ctx)
|
||||
so.action_confirm()
|
||||
job = env["fp.job"].search([("sale_order_id", "=", so.id)], limit=1)
|
||||
if not job:
|
||||
return False
|
||||
if job.state == "draft":
|
||||
job.action_confirm()
|
||||
_ensure_steps(env, job)
|
||||
_populate_job(env, job, ctx)
|
||||
total = len(job.step_ids)
|
||||
n_done = min(2, max(1, total // 3)) if total else 0
|
||||
_assign_step_users(env, job, ctx, n_done=n_done, current_idx=n_done)
|
||||
if total > n_done:
|
||||
cur = job.step_ids.sorted("sequence")[n_done]
|
||||
cur.write({"state": "paused"})
|
||||
_fill_step_realistic_data(env, job)
|
||||
job.write({"state": "on_hold"})
|
||||
_create_quality_hold(env, job, ctx)
|
||||
return True
|
||||
|
||||
|
||||
def stage_job_done_delivery_draft(env, partner, part, coating, ctx):
|
||||
so = _make_so(env, partner, part, coating,
|
||||
qty=random.choice([5, 10, 25]),
|
||||
price=random.uniform(80.0, 250.0), ctx=ctx)
|
||||
so.action_confirm()
|
||||
job = env["fp.job"].search([("sale_order_id", "=", so.id)], limit=1)
|
||||
if not job:
|
||||
return False
|
||||
if job.state == "draft":
|
||||
job.action_confirm()
|
||||
_ensure_steps(env, job)
|
||||
_populate_job(env, job, ctx)
|
||||
_assign_step_users(env, job, ctx,
|
||||
n_done=len(job.step_ids),
|
||||
current_idx=None)
|
||||
_fill_step_realistic_data(env, job)
|
||||
job.write({
|
||||
"state": "in_progress",
|
||||
"date_started": datetime.now() - timedelta(
|
||||
days=random.randint(3, 10)),
|
||||
})
|
||||
job.button_mark_done()
|
||||
return True
|
||||
|
||||
|
||||
def stage_delivery_scheduled(env, partner, part, coating, ctx):
|
||||
if not stage_job_done_delivery_draft(env, partner, part, coating, ctx):
|
||||
return False
|
||||
job = env["fp.job"].search(
|
||||
[("partner_id", "=", partner.id)],
|
||||
order="id desc", limit=1)
|
||||
if not job or not job.delivery_id:
|
||||
return False
|
||||
_make_delivery_full(env, job.delivery_id, partner, ctx,
|
||||
state="scheduled",
|
||||
scheduled_offset_days=random.randint(1, 5))
|
||||
return True
|
||||
|
||||
|
||||
def stage_delivery_en_route(env, partner, part, coating, ctx):
|
||||
if not stage_job_done_delivery_draft(env, partner, part, coating, ctx):
|
||||
return False
|
||||
job = env["fp.job"].search(
|
||||
[("partner_id", "=", partner.id)],
|
||||
order="id desc", limit=1)
|
||||
if not job or not job.delivery_id:
|
||||
return False
|
||||
_make_delivery_full(env, job.delivery_id, partner, ctx,
|
||||
state="en_route",
|
||||
scheduled_offset_days=0)
|
||||
return True
|
||||
|
||||
|
||||
def stage_delivery_delivered(env, partner, part, coating, ctx):
|
||||
if not stage_job_done_delivery_draft(env, partner, part, coating, ctx):
|
||||
return False
|
||||
job = env["fp.job"].search(
|
||||
[("partner_id", "=", partner.id)],
|
||||
order="id desc", limit=1)
|
||||
if not job or not job.delivery_id:
|
||||
return False
|
||||
_make_delivery_full(env, job.delivery_id, partner, ctx,
|
||||
state="delivered",
|
||||
scheduled_offset_days=-2)
|
||||
so = job.sale_order_id
|
||||
_issue_certificate(env, job, so, part, ctx)
|
||||
return True
|
||||
|
||||
|
||||
def stage_invoice_draft(env, partner, part, coating, ctx):
|
||||
if not stage_delivery_delivered(env, partner, part, coating, ctx):
|
||||
return False
|
||||
job = env["fp.job"].search(
|
||||
[("partner_id", "=", partner.id)],
|
||||
order="id desc", limit=1)
|
||||
so = job.sale_order_id
|
||||
if not so:
|
||||
return False
|
||||
inv = _create_invoice(env, so, ctx, post=False)
|
||||
return bool(inv)
|
||||
|
||||
|
||||
def stage_invoice_posted(env, partner, part, coating, ctx):
|
||||
if not stage_delivery_delivered(env, partner, part, coating, ctx):
|
||||
return False
|
||||
job = env["fp.job"].search(
|
||||
[("partner_id", "=", partner.id)],
|
||||
order="id desc", limit=1)
|
||||
so = job.sale_order_id
|
||||
if not so:
|
||||
return False
|
||||
inv = _create_invoice(env, so, ctx, post=True)
|
||||
return inv and inv.state == "posted"
|
||||
|
||||
|
||||
def stage_paid(env, partner, part, coating, ctx):
|
||||
if not stage_delivery_delivered(env, partner, part, coating, ctx):
|
||||
return False
|
||||
job = env["fp.job"].search(
|
||||
[("partner_id", "=", partner.id)],
|
||||
order="id desc", limit=1)
|
||||
so = job.sale_order_id
|
||||
if not so:
|
||||
return False
|
||||
inv = _create_invoice(env, so, ctx, post=True)
|
||||
if not inv or inv.state != "posted":
|
||||
return False
|
||||
pmt = _register_payment(env, inv, ctx, validate=True)
|
||||
return bool(pmt)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
def run(env):
|
||||
print("=" * 70)
|
||||
print("seed_workflow_states.py - full pipeline seeding")
|
||||
print("=" * 70)
|
||||
|
||||
combos = _build_combos(env)
|
||||
print("Customer/part combos: %d" % len(combos))
|
||||
if not combos:
|
||||
print("ERROR: no parts with coating + recipe + partner. Cannot seed.")
|
||||
return
|
||||
operators = _operators(env)
|
||||
managers = _managers(env)
|
||||
employees = _employees(env)
|
||||
facility = _resolve_facility(env)
|
||||
product = _resolve_product(env)
|
||||
payment_term = _resolve_payment_term(env)
|
||||
sales_journal, bank_journal = _resolve_journals(env)
|
||||
print("Operators: %d, Managers: %d, Employees: %d" % (
|
||||
len(operators), len(managers), len(employees)))
|
||||
print("Facility: %s, Product: %s, PaymentTerm: %s" % (
|
||||
facility.name if facility else "NONE",
|
||||
product.name if product else "NONE",
|
||||
payment_term.name if payment_term else "NONE"))
|
||||
print("Sales journal: %s, Bank journal: %s" % (
|
||||
sales_journal.name if sales_journal else "NONE",
|
||||
bank_journal.name if bank_journal else "NONE"))
|
||||
|
||||
if not (product and payment_term and sales_journal and bank_journal):
|
||||
print("ERROR: missing required masters; cannot proceed.")
|
||||
return
|
||||
|
||||
ctx = {
|
||||
"product": product,
|
||||
"payment_term": payment_term,
|
||||
"sales_journal": sales_journal,
|
||||
"bank_journal": bank_journal,
|
||||
"operators": operators,
|
||||
"managers": managers,
|
||||
"employees": employees,
|
||||
"facility": facility,
|
||||
}
|
||||
|
||||
idx_holder = [0]
|
||||
results = {}
|
||||
|
||||
stages = [
|
||||
("Quotation (sale.order draft)", stage_so_draft, TARGETS["so_draft"]),
|
||||
("Quote Sent (sale.order sent)", stage_so_sent, TARGETS["so_sent"]),
|
||||
("Order Confirmed Job Just Started", stage_job_confirmed, TARGETS["job_confirmed_no_steps_started"]),
|
||||
("Job In Progress Early", stage_job_in_progress_early, TARGETS["job_in_progress_early"]),
|
||||
("Job In Progress Mid", stage_job_in_progress_mid, TARGETS["job_in_progress_mid"]),
|
||||
("Job On Hold", stage_job_on_hold, TARGETS["job_on_hold"]),
|
||||
("Job Done Delivery Draft", stage_job_done_delivery_draft, TARGETS["job_done_delivery_draft"]),
|
||||
("Delivery Scheduled", stage_delivery_scheduled, TARGETS["delivery_scheduled"]),
|
||||
("Delivery En Route", stage_delivery_en_route, TARGETS["delivery_en_route"]),
|
||||
("Delivered", stage_delivery_delivered, TARGETS["delivery_delivered"]),
|
||||
("Invoice Draft", stage_invoice_draft, TARGETS["invoice_draft"]),
|
||||
("Invoice Posted", stage_invoice_posted, TARGETS["invoice_posted"]),
|
||||
("Paid", stage_paid, TARGETS["paid"]),
|
||||
]
|
||||
|
||||
for label, fn, n in stages:
|
||||
_stage(env, label, fn, n, combos, idx_holder, ctx, results)
|
||||
env.cr.commit()
|
||||
|
||||
print()
|
||||
print("=" * 70)
|
||||
print("SEED RESULTS")
|
||||
print("=" * 70)
|
||||
for label, count in results.items():
|
||||
print(" %-45s %d" % (label, count))
|
||||
print()
|
||||
|
||||
|
||||
try:
|
||||
run(env)
|
||||
except NameError:
|
||||
print("Run inside odoo shell.")
|
||||
Reference in New Issue
Block a user