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:
gsinghpal
2026-04-25 09:56:36 -04:00
parent 7d71b77e14
commit 128d51755d

View File

@@ -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.")