Replace em-dashes and en-dashes with hyphens across 789 shipped source files (py/xml/js/scss) so the delivered module reads as human-written; em-dashes had become a recognizable AI-generated tell. Internal .md dev notes are excluded. The WO-sticker mojibake strippers keep their dash search targets (now written — / –). No logic changes: comments and display strings only; validated with py_compile + lxml parse. Rewrite the 7 customer notification emails to be intake-neutral (ship-in / drop-off / pickup) and repair-aware, and fix the Shipped email documents line (packing slip vs bill of lading; certificate only when issued). Subjects use a hyphen separator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
354 lines
14 KiB
Python
354 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Create a brand-new Hard Anodize Type III + Dye + Seal recipe with
|
|
rich operator instructions + measurement prompts on every step,
|
|
then create an SO for ABC Manufacturing and confirm it so the
|
|
operator-facing job is ready to run.
|
|
|
|
After running, the user navigates to:
|
|
- Recipe form (Process Recipes menu) - verify instructions present
|
|
- Simple Recipe Editor - verify per-step Instructions + Measurements
|
|
- Sale Orders - verify the new SO with line referencing the recipe
|
|
- Plating Jobs - verify job created with all steps
|
|
- Click Mark Done on any step → verify operator wizard shows
|
|
instructions + measurement prompts
|
|
"""
|
|
|
|
import json
|
|
from odoo import fields
|
|
|
|
Node = env['fusion.plating.process.node']
|
|
NodeInput = env['fusion.plating.process.node.input']
|
|
Template = env['fp.step.template']
|
|
|
|
print('\n========== Build Hard Anodize Type III Recipe ==========\n')
|
|
|
|
# Clean up any prior run of this script (prior recipes + their variants + jobs + SOs)
|
|
prior_recipes = Node.search([
|
|
'|', ('name', '=', 'Hard Anodize Type III + Dye + Seal'),
|
|
('name', 'ilike', 'Hard Anodize Type III + Dye + Seal - '),
|
|
])
|
|
if prior_recipes:
|
|
print('Cleaning up %d prior recipe(s)...' % len(prior_recipes))
|
|
# Cancel + delete jobs that reference these recipes
|
|
prior_jobs = env['fp.job'].search([('recipe_id', 'in', prior_recipes.ids)])
|
|
for j in prior_jobs:
|
|
try:
|
|
j.write({'state': 'cancel'})
|
|
except Exception:
|
|
pass
|
|
prior_sos = env['sale.order'].search([
|
|
('order_line.x_fc_process_variant_id', 'in', prior_recipes.ids),
|
|
('state', 'in', ('draft', 'sent')),
|
|
])
|
|
prior_sos.unlink()
|
|
prior_jobs.unlink()
|
|
prior_recipes.unlink()
|
|
env.cr.commit()
|
|
|
|
# ----- Recipe root -----
|
|
recipe = Node.create({
|
|
'name': 'Hard Anodize Type III + Dye + Seal',
|
|
'code': 'HARD_ANO_T3',
|
|
'node_type': 'recipe',
|
|
'is_template': True,
|
|
'description': '''<p><strong>Hard Anodize Type III per MIL-A-8625F</strong></p>
|
|
<p>Aluminum substrate. Black sulfuric dye. Hot nickel acetate seal.</p>
|
|
<p>Tolerances: 0.0015"-0.0020" coating thickness, 60kV breakdown
|
|
voltage. Hardness ≥350HV300.</p>''',
|
|
})
|
|
print('Created recipe:', recipe.id, recipe.name)
|
|
|
|
# ----- Step definitions -----
|
|
# Each spec: name, kind, description (operator instructions), extra_prompts (additional manually-authored)
|
|
STEPS = [
|
|
{
|
|
'name': '1. Receiving',
|
|
'kind': 'receiving',
|
|
'description': '''<p><strong>Verify quantity and condition on arrival.</strong></p>
|
|
<ul><li>Count parts against the packing slip</li>
|
|
<li>Visually inspect for damage, corrosion, oil residue</li>
|
|
<li>Photograph any damage or non-conformance</li>
|
|
<li>Sign off below before parts move to staging</li></ul>''',
|
|
},
|
|
{
|
|
'name': '2. Contract Review (QA-005)',
|
|
'kind': 'contract_review',
|
|
'description': '''<p><strong>Verify the order against quality requirements.</strong></p>
|
|
<ul><li>Confirm spec matches PO (MIL-A-8625F Type III, Class 2 Black)</li>
|
|
<li>Cross-check thickness, hardness, salt-spray requirements</li>
|
|
<li>Open the QA-005 contract review form (separate dialog)</li>
|
|
<li>Approve only after all line items reconciled</li></ul>''',
|
|
},
|
|
{
|
|
'name': '3. Incoming Inspection',
|
|
'kind': 'inspect',
|
|
'description': '''<p><strong>First-piece inspection before processing.</strong></p>
|
|
<ul><li>Verify part number against drawing</li>
|
|
<li>Inspect surface for prior plating remnants, scratches, pitting</li>
|
|
<li>Photograph any defects</li>
|
|
<li>Record incoming dimensional spot-check</li></ul>''',
|
|
},
|
|
{
|
|
'name': '4. Racking',
|
|
'kind': 'racking',
|
|
'description': '''<p><strong>Mount parts on titanium rack.</strong></p>
|
|
<ul><li>Use Rack T-12 or similar (titanium, NOT aluminum)</li>
|
|
<li>Ensure firm electrical contact at rack hooks</li>
|
|
<li>Apply masking to threaded holes if required (see drawing)</li>
|
|
<li>Photograph the loaded rack before submerging</li></ul>''',
|
|
},
|
|
{
|
|
'name': '5. Alkaline Clean (Tank A-1)',
|
|
'kind': 'cleaning',
|
|
'description': '''<p><strong>Soak clean - Aluminum Etch Cleaner.</strong></p>
|
|
<ul><li>Tank: A-1, Bath: ALKCLEAN-1</li>
|
|
<li>Time: 4-6 minutes</li>
|
|
<li>Temperature: 140-160°F</li>
|
|
<li>Confirm titration done within last 24 hours</li></ul>''',
|
|
},
|
|
{
|
|
'name': '6. Rinse - Cascade DI (Tank A-2)',
|
|
'kind': 'rinse',
|
|
'description': '''<p><strong>Triple cascade DI rinse.</strong></p>
|
|
<ul><li>Tank A-2 (DI rinse, conductivity < 50 µS/cm)</li>
|
|
<li>Time: 30 seconds, agitate</li>
|
|
<li>Verify conductivity reading on bath log</li></ul>''',
|
|
},
|
|
{
|
|
'name': '7. Etch (Tank A-3)',
|
|
'kind': 'etch',
|
|
'description': '''<p><strong>Caustic etch - sodium hydroxide bath.</strong></p>
|
|
<ul><li>Tank: A-3, Bath: ETCH-1</li>
|
|
<li>Time: 30-90 seconds (per drawing - heavy etch removes 0.0005"/side)</li>
|
|
<li>Temperature: 130-150°F</li>
|
|
<li>Concentration: 4-6 oz/gal NaOH</li>
|
|
<li>HE-risk parts (high-strength) require post-bake - flag accordingly</li></ul>''',
|
|
},
|
|
{
|
|
'name': '8. Rinse - Cascade DI (Tank A-4)',
|
|
'kind': 'rinse',
|
|
'description': '<p>Triple cascade DI rinse - Tank A-4. 30 sec agitate.</p>',
|
|
},
|
|
{
|
|
'name': '9. Desmut / Deoxidize (Tank A-5)',
|
|
'kind': 'etch',
|
|
'description': '''<p><strong>Acid desmut to remove black smut from etch.</strong></p>
|
|
<ul><li>Tank: A-5, Bath: DEOX-1 (HNO3-based)</li>
|
|
<li>Time: 30-60 seconds</li>
|
|
<li>Temperature: ambient</li>
|
|
<li>Surface should be water-break-free after this step</li></ul>''',
|
|
},
|
|
{
|
|
'name': '10. Rinse - Cascade DI (Tank A-6)',
|
|
'kind': 'rinse',
|
|
'description': '<p>Final pre-anodize rinse - Tank A-6. Conductivity must be < 50 µS/cm.</p>',
|
|
},
|
|
{
|
|
'name': '11. Hard Anodize Type III (Tank A-9)',
|
|
'kind': 'plate',
|
|
'description': '''<p><strong>HARD ANODIZE - the primary process step.</strong></p>
|
|
<ul><li>Tank: A-9, Bath: HARDANO-1 (15% sulfuric acid)</li>
|
|
<li>Temperature: 28-32°F (chilled bath - confirm chiller is running)</li>
|
|
<li>Current density: 24-36 ASF</li>
|
|
<li>Voltage ramp: 0-80V over first 5 minutes</li>
|
|
<li>Time at voltage: 60 minutes (gives ~0.002" coating)</li>
|
|
<li>Record amperage every 15 minutes</li>
|
|
<li>Check thickness midway with Fischerscope on witness coupon</li>
|
|
<li>If color reading off, halt and call supervisor</li></ul>''',
|
|
},
|
|
{
|
|
'name': '12. Rinse - Cold (Tank A-12)',
|
|
'kind': 'rinse',
|
|
'description': '<p>Cold cascade rinse to remove sulfuric residue. Tank A-12.</p>',
|
|
},
|
|
{
|
|
'name': '13. Black Dye Bath (Tank A-14)',
|
|
'kind': 'plate',
|
|
'description': '''<p><strong>Sulfo Black BL dye absorption.</strong></p>
|
|
<ul><li>Tank: A-14, Bath: DYE-BL-1</li>
|
|
<li>Temperature: 130-150°F</li>
|
|
<li>Time: 12-18 minutes</li>
|
|
<li>Maintain pH 5.0-6.0</li>
|
|
<li>Visually verify uniform black with no streaks before sealing</li></ul>''',
|
|
},
|
|
{
|
|
'name': '14. Rinse - Warm (Tank A-15)',
|
|
'kind': 'rinse',
|
|
'description': '<p>Warm rinse before sealing. Tank A-15. ~110°F.</p>',
|
|
},
|
|
{
|
|
'name': '15. Hot Nickel Acetate Seal (Tank A-16)',
|
|
'kind': 'bake',
|
|
'description': '''<p><strong>Nickel acetate seal - locks in dye, improves corrosion resistance.</strong></p>
|
|
<ul><li>Tank: A-16, Bath: SEAL-NA-1</li>
|
|
<li>Temperature: 195-205°F</li>
|
|
<li>Time: 18-22 minutes</li>
|
|
<li>pH: 5.5-6.0</li>
|
|
<li>Attach AMS-2759 chart-recorder file as photo before unloading</li>
|
|
<li>Quality of seal verified post-process by dye absorption test</li></ul>''',
|
|
},
|
|
{
|
|
'name': '16. Hot DI Rinse (Tank A-17)',
|
|
'kind': 'rinse',
|
|
'description': '<p>Final hot DI rinse. Tank A-17. 180°F+. Drives off residual seal solution.</p>',
|
|
},
|
|
{
|
|
'name': '17. Drying (Hot Air Knife)',
|
|
'kind': 'dry',
|
|
'description': '''<p><strong>Hot-air knife dry - leave parts on rack.</strong></p>
|
|
<ul><li>Hot air knife @ 180°F</li>
|
|
<li>Time: 5 minutes minimum</li>
|
|
<li>Verify parts fully dry before unracking - water spotting is a defect</li></ul>''',
|
|
},
|
|
{
|
|
'name': '18. De-Racking',
|
|
'kind': 'derack',
|
|
'description': '''<p><strong>Remove parts from rack carefully.</strong></p>
|
|
<ul><li>Wear lint-free gloves</li>
|
|
<li>Inspect each part for rack-mark touch-up needs</li>
|
|
<li>Place on padded staging cart</li></ul>''',
|
|
},
|
|
{
|
|
'name': '19. Final Visual Inspection',
|
|
'kind': 'final_inspect',
|
|
'description': '''<p><strong>Visual + dimensional + thickness QC.</strong></p>
|
|
<ul><li>Visual: uniform black, no streaks, scratches, or burn</li>
|
|
<li>Dimensional: spot-check 3 critical dims per part</li>
|
|
<li>Thickness: 5-point Fischerscope reading per drawing locations</li>
|
|
<li>Photograph any defects, route to NCR if rework needed</li>
|
|
<li>Inspector signs off below</li></ul>''',
|
|
},
|
|
{
|
|
'name': '20. Microhardness Test (Witness Coupon)',
|
|
'kind': 'hardness_test',
|
|
'description': '''<p><strong>HV300 hardness measurement on witness coupon.</strong></p>
|
|
<ul><li>Equipment: LECO LM247AT microhardness tester (verify cal date current)</li>
|
|
<li>Load: 300 gf, dwell 10s</li>
|
|
<li>Take 3 indents, log each + average</li>
|
|
<li>Spec: ≥350 HV300 (MIL-A-8625F Type III)</li></ul>''',
|
|
},
|
|
{
|
|
'name': '21. Salt Spray (Witness, ASTM B117)',
|
|
'kind': 'salt_spray',
|
|
'description': '''<p><strong>336-hour salt spray on witness coupon.</strong></p>
|
|
<ul><li>Submit 3 coupons to lab</li>
|
|
<li>Test duration: 336 hours minimum</li>
|
|
<li>Acceptance: zero red rust, < 5% white corrosion</li>
|
|
<li>Attach lab certificate when received</li></ul>''',
|
|
},
|
|
{
|
|
'name': '22. Packaging',
|
|
'kind': 'packaging',
|
|
'description': '''<p><strong>Wrap and stage for shipment.</strong></p>
|
|
<ul><li>Wrap each part in VCI paper</li>
|
|
<li>Place in foam-lined box, max 4 parts per box</li>
|
|
<li>Include CoC + dimensional report inside the box</li>
|
|
<li>Seal with company tape</li></ul>''',
|
|
},
|
|
{
|
|
'name': '23. Shipping',
|
|
'kind': 'ship',
|
|
'description': '''<p><strong>Outbound - confirm carrier and BoL.</strong></p>
|
|
<ul><li>Carrier per SO (UPS / FedEx / customer pickup)</li>
|
|
<li>Print BoL, attach to package</li>
|
|
<li>Photograph sealed shipment for proof-of-shipment</li>
|
|
<li>Update tracking # in this SO</li></ul>''',
|
|
},
|
|
]
|
|
|
|
# ----- Build steps -----
|
|
DEFAULTS = Template.DEFAULT_INPUTS_BY_KIND
|
|
total_prompts = 0
|
|
for idx, spec in enumerate(STEPS):
|
|
step = Node.create({
|
|
'name': spec['name'],
|
|
# node_type='operation' is required for fp.job to materialize a
|
|
# work-order step from this node. 'step' nodes are sub-elements
|
|
# under an operation (e.g. child rinses), not job-step builders.
|
|
'node_type': 'operation',
|
|
'parent_id': recipe.id,
|
|
'sequence': (idx + 1) * 10,
|
|
'default_kind': spec['kind'],
|
|
'description': spec['description'],
|
|
'collect_measurements': True,
|
|
})
|
|
# Seed prompts based on kind
|
|
for input_spec in DEFAULTS.get(spec['kind'], []):
|
|
NodeInput.create({
|
|
'node_id': step.id,
|
|
'name': input_spec['name'],
|
|
'input_type': input_spec.get('input_type', 'text'),
|
|
'kind': 'step_input',
|
|
'collect': True,
|
|
'sequence': input_spec.get('sequence', 10),
|
|
'required': input_spec.get('required', False),
|
|
'hint': input_spec.get('hint', ''),
|
|
'selection_options': input_spec.get('selection_options', ''),
|
|
'target_unit': input_spec.get('target_unit', False),
|
|
})
|
|
total_prompts += 1
|
|
|
|
env.cr.commit()
|
|
print('Built %d steps with %d total prompts' % (len(STEPS), total_prompts))
|
|
|
|
# ----- Create SO for ABC Manufacturing -----
|
|
print('\n========== Create SO for ABC ==========\n')
|
|
|
|
abc = env['res.partner'].browse(943)
|
|
part = env['fp.part.catalog'].browse(148) # ABC part 4321
|
|
prod = env['product.product'].search([], limit=1)
|
|
|
|
so = env['sale.order'].create({
|
|
'partner_id': abc.id,
|
|
'x_fc_planned_start_date': fields.Date.today(),
|
|
'x_fc_po_number': 'BT-PO-HARDANO-001',
|
|
'client_order_ref': 'BT-PO-HARDANO-001',
|
|
})
|
|
sol = env['sale.order.line'].create({
|
|
'order_id': so.id,
|
|
'product_id': prod.id,
|
|
'product_uom_qty': 10,
|
|
'price_unit': 350.0,
|
|
'name': 'Hard Anodize Type III + Black Dye + Seal',
|
|
'x_fc_part_catalog_id': part.id,
|
|
'x_fc_process_variant_id': recipe.id,
|
|
})
|
|
print('Created SO:', so.name, 'line', sol.id)
|
|
|
|
# Confirm - triggers fp.job creation
|
|
so.action_confirm()
|
|
print('Confirmed SO. State =', so.state)
|
|
env.cr.commit()
|
|
|
|
# Find resulting job
|
|
job = env['fp.job'].search([('origin', '=', so.name)], limit=1)
|
|
print('\n========== Job created ==========\n')
|
|
print('Job:', job.id, job.name, '| recipe variant:', job.recipe_id.name if job.recipe_id else None)
|
|
job_steps = env['fp.job.step'].search([('job_id', '=', job.id)], order='sequence')
|
|
print('Steps on job:', len(job_steps))
|
|
|
|
print('\n========== Verification ==========\n')
|
|
prompts_visible = 0
|
|
instructions_visible = 0
|
|
for js in job_steps:
|
|
rn = js.recipe_node_id
|
|
if rn:
|
|
ins = bool(rn.description)
|
|
prompts = len(rn.input_ids.filtered(lambda i: i.collect))
|
|
instructions_visible += int(ins)
|
|
prompts_visible += prompts
|
|
print(' [%2d] %s - instructions=%s prompts=%d kind=%s' % (
|
|
js.sequence, js.name, '✓' if ins else '✗', prompts, rn.default_kind or '-'
|
|
))
|
|
|
|
print('\nSummary:')
|
|
print(' Steps with instructions:', instructions_visible, '/', len(job_steps))
|
|
print(' Total prompts visible to operators:', prompts_visible)
|
|
print('\n========== URLs to verify ==========')
|
|
print('Recipe: /odoo/action-fusion_plating.action_fp_process_node/%d' % recipe.id)
|
|
print('Sale Order:/odoo/sales/%d' % so.id)
|
|
print('Job: /odoo/action-fusion_plating_jobs.action_fp_job/%d' % job.id)
|
|
print(' → click any step → "Mark Done" → operator wizard should show:')
|
|
print(' - Step description (instructions for operator)')
|
|
print(' - All %d collect=True prompts as input fields' % prompts_visible)
|