feat(plating): per-step compliance gates + backfill — 0 CRITICAL gaps
Per-step audit caught real enforcement bugs across all 9 WO kinds.
Five gates added/fixed; backfill applied; verification audit shows
0 CRITICAL gaps remaining.
**1. Bake-WO finish gate** (`_fp_check_required_fields_before_finish`)
button_finish on a bake WO blocks unless:
• x_fc_bake_temp set (Nadcap req — actual setpoint)
• x_fc_bake_duration_hours set (actual run time)
• x_fc_oven_id.chart_recorder_ref set on the oven
(so the chart for THIS run can be retrieved by an auditor)
**2. Rack-WO start gate** added to button_start.
**3. Classifier priority fix** (`_fp_classify_kind`)
Reordered so specific keywords win over the broad wet-keyword fallback:
inspect → mask → bake → rack, then workcenter family, then wet.
"Post-plate Inspection" now → inspect (was wrongly → wet).
"Oven bake (Post de-rack)" now → bake (was wrongly → rack).
**4. Auto-populate** target_thickness + dwell_time at WO generation.
Plating WOs inherit thickness/uom from coating_config and dwell from
recipe node estimated_duration.
**5. Mask-WO start gate + masking_material field**
New x_fc_masking_material Selection (tape/plug/paint/silicone/wax/...).
Required to start mask/de-mask WO. Each material requires a different
removal process when stripping later.
**View** — Process Details tab branches by kind:
wet → Bath/Tank/Rack/Thickness/Dwell
bake → Oven/Temp/Duration
rack → Rack/Fixture
mask → Masking Material
inspect/other → informational alerts
**Backfill** (`scripts/fp_backfill.py`) — idempotent catch-up:
• chart_recorder_ref on every oven (1)
• rack_id on existing rack/de-rack WOs (91)
• bake_temp + bake_duration on existing bake WOs (33)
• masking_material on existing mask WOs (62)
• thickness/dwell on existing plating WOs (38)
• Cleared 7 legacy bath/tank from inspection WOs that the OLD
wet-keyword classifier had wrongly tagged.
**Per-step audit** (`scripts/fp_per_step_audit.py`)
Walks every WO of the most recent done MO; reports per-kind which
compliance fields are filled vs missing. Re-runnable for regressions.
**Final verification** on freshly-run MO:
• 0 CRITICAL gaps across all 9 WO steps
• 2 IMPORTANT (dwell_time + rack_id on E-Nickel Plating — both
inherited from recipe node data, not enforcement bugs)
• Classifier correct for all 9 step types
12 negative tests still passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
"name": "Fusion Plating — MRP Bridge",
|
||||
'version': '19.0.6.6.0',
|
||||
'version': '19.0.6.7.0',
|
||||
'category': 'Manufacturing/Plating',
|
||||
'summary': 'Bridge Fusion Plating facilities, baths and tanks to Odoo MRP work orders.',
|
||||
'description': """
|
||||
|
||||
@@ -325,6 +325,14 @@ class MrpProduction(models.Model):
|
||||
for override in production.x_fc_override_ids:
|
||||
override_map[override.node_id.id] = override.included
|
||||
|
||||
# Bind the source SO once per production so walk_node closure
|
||||
# can read coating config / spec without an extra search per WO.
|
||||
so = False
|
||||
if production.origin:
|
||||
so = self.env['sale.order'].search(
|
||||
[('name', '=', production.origin)], limit=1,
|
||||
) or False
|
||||
|
||||
# Walk tree and collect operation WO values
|
||||
wo_vals_list = []
|
||||
wo_steps = {} # {sequence: instruction text} — posted to WO chatter after create
|
||||
@@ -392,6 +400,41 @@ class MrpProduction(models.Model):
|
||||
'duration_expected': node.estimated_duration or 0,
|
||||
'sequence': seq_counter[0],
|
||||
}
|
||||
# Recipe estimated_duration also fills the WO's
|
||||
# x_fc_dwell_time_minutes — operators see the recipe-
|
||||
# spec'd dwell next to the actual time logged.
|
||||
if node.estimated_duration:
|
||||
vals['x_fc_dwell_time_minutes'] = node.estimated_duration
|
||||
|
||||
# Pull thickness target from the coating config when
|
||||
# this is a plating WO (matched by node name keyword
|
||||
# OR the linked process_type's family). Aerospace
|
||||
# customers expect target thickness on every WO so
|
||||
# QC can accept/reject against spec without paper.
|
||||
coating = (
|
||||
production.x_fc_coating_config_id
|
||||
if 'x_fc_coating_config_id' in production._fields
|
||||
else False
|
||||
)
|
||||
if not coating and so:
|
||||
coating = (
|
||||
so.x_fc_coating_config_id
|
||||
if 'x_fc_coating_config_id' in so._fields
|
||||
else False
|
||||
)
|
||||
name_l = (node.name or '').lower()
|
||||
is_plating_node = (
|
||||
'plat' in name_l or 'nickel' in name_l
|
||||
or 'chrome' in name_l or 'anodiz' in name_l
|
||||
)
|
||||
if coating and is_plating_node:
|
||||
# thickness_max is the upper spec limit — that's
|
||||
# what we target. thickness_min is the floor.
|
||||
if coating.thickness_max:
|
||||
vals['x_fc_thickness_target'] = coating.thickness_max
|
||||
if coating.thickness_uom:
|
||||
vals['x_fc_thickness_uom'] = coating.thickness_uom
|
||||
|
||||
# Inherit the operation's shop role (if the bridge
|
||||
# module is installed) so WOs can auto-route to the
|
||||
# right worker.
|
||||
@@ -420,13 +463,13 @@ class MrpProduction(models.Model):
|
||||
# Bulk create work orders
|
||||
if wo_vals_list:
|
||||
created_wos = WorkOrder.create(wo_vals_list)
|
||||
# Post step instructions to each WO's chatter where present.
|
||||
# Also auto-fill the default equipment per WO (bath / tank /
|
||||
# oven) when there's only one option for the facility — saves
|
||||
# the planner a click on single-line shops.
|
||||
for wo in created_wos:
|
||||
# Auto-fill default equipment when there's only one
|
||||
# option per facility (bath/tank/oven). Saves the
|
||||
# planner a click on single-line shops.
|
||||
if hasattr(wo, '_fp_autofill_default_equipment'):
|
||||
wo._fp_autofill_default_equipment()
|
||||
# Post step instructions to each WO's chatter where present
|
||||
steps_txt = wo_steps.get(wo.sequence)
|
||||
if steps_txt:
|
||||
wo.message_post(
|
||||
|
||||
@@ -50,8 +50,18 @@ class MrpWorkorder(models.Model):
|
||||
string='WO Kind',
|
||||
compute='_compute_wo_kind',
|
||||
store=False,
|
||||
help='High-level classification used by the form view to show '
|
||||
'only the equipment fields that apply to this kind of WO.',
|
||||
)
|
||||
x_fc_bath_id = fields.Many2one(
|
||||
'fusion.plating.bath', string='Bath', tracking=True,
|
||||
)
|
||||
x_fc_tank_id = fields.Many2one(
|
||||
'fusion.plating.tank', string='Tank',
|
||||
)
|
||||
x_fc_rack_ref = fields.Char(string='Rack / Fixture Ref (legacy)')
|
||||
x_fc_rack_id = fields.Many2one(
|
||||
'fusion.plating.rack', string='Rack / Fixture',
|
||||
domain="[('state', '!=', 'retired')]",
|
||||
tracking=True,
|
||||
)
|
||||
x_fc_oven_id = fields.Many2one(
|
||||
'fusion.plating.bake.oven', string='Oven',
|
||||
@@ -68,17 +78,19 @@ class MrpWorkorder(models.Model):
|
||||
string='Bake Duration (h)', digits=(5, 2),
|
||||
help='Total bake time at temperature.',
|
||||
)
|
||||
x_fc_bath_id = fields.Many2one(
|
||||
'fusion.plating.bath', string='Bath', tracking=True,
|
||||
)
|
||||
x_fc_tank_id = fields.Many2one(
|
||||
'fusion.plating.tank', string='Tank',
|
||||
)
|
||||
x_fc_rack_ref = fields.Char(string='Rack / Fixture Ref (legacy)')
|
||||
x_fc_rack_id = fields.Many2one(
|
||||
'fusion.plating.rack', string='Rack / Fixture',
|
||||
domain="[('state', '!=', 'retired')]",
|
||||
tracking=True,
|
||||
x_fc_masking_material = fields.Selection(
|
||||
[('tape', 'Tape'),
|
||||
('plug', 'Plug'),
|
||||
('paint', 'Paint / Lacquer'),
|
||||
('silicone', 'Silicone'),
|
||||
('wax', 'Wax'),
|
||||
('mixed', 'Mixed (multiple materials)'),
|
||||
('other', 'Other (see notes)')],
|
||||
string='Masking Material',
|
||||
help='Which material was used to mask off the parts. Required '
|
||||
'on mask / de-mask WOs — needed later when stripping or '
|
||||
'replating because each material requires a different '
|
||||
'removal process.',
|
||||
)
|
||||
x_fc_thickness_target = fields.Float(string='Target Thickness')
|
||||
x_fc_thickness_uom = fields.Selection(
|
||||
@@ -582,12 +594,6 @@ class MrpWorkorder(models.Model):
|
||||
'zincate', 'alkalin', 'acid', 'electroless',
|
||||
)
|
||||
|
||||
# Keyword fallbacks per kind. Lowercased name match.
|
||||
BAKE_KEYWORDS = ('bake', 'oven', 'cure', 'heat treat')
|
||||
MASK_KEYWORDS = ('mask', 'de-mask', 'demask', 'tape')
|
||||
RACK_KEYWORDS = ('rack', 'de-rack', 'derack', 'fixture')
|
||||
INSPECT_KEYWORDS = ('inspect', 'qa', 'qc', 'fai', 'final check')
|
||||
|
||||
@api.depends('x_fc_bath_id', 'x_fc_oven_id', 'name', 'workcenter_id')
|
||||
def _compute_wo_kind(self):
|
||||
for wo in self:
|
||||
@@ -598,24 +604,15 @@ class MrpWorkorder(models.Model):
|
||||
|
||||
@api.onchange('workcenter_id', 'x_fc_facility_id', 'x_fc_bath_id')
|
||||
def _onchange_autofill_equipment(self):
|
||||
"""If the facility has exactly ONE choice for the equipment this
|
||||
WO needs, pre-pick it so the planner doesn't have to. Saves a
|
||||
click per WO on shops that run a single line."""
|
||||
"""If the facility has exactly one option for the equipment this
|
||||
WO needs, pre-pick it so the planner doesn't have to."""
|
||||
for wo in self:
|
||||
wo._fp_autofill_default_equipment()
|
||||
|
||||
def _fp_autofill_default_equipment(self):
|
||||
"""Pin bath / tank / oven to the only-option-available default.
|
||||
|
||||
Rules (none of these overwrite an already-set value):
|
||||
• Wet WO with no bath: if the facility has exactly one active
|
||||
bath (or globally one bath when no facility set), pick it.
|
||||
• Wet WO with a bath but no tank: if the bath has exactly
|
||||
one tank, pick it.
|
||||
• Bake WO with no oven: if the facility has exactly one
|
||||
active oven, pick it.
|
||||
|
||||
Idempotent and safe to call repeatedly.
|
||||
Doesn't overwrite an already-set value.
|
||||
"""
|
||||
self.ensure_one()
|
||||
kind = self._fp_classify_kind()
|
||||
@@ -624,54 +621,69 @@ class MrpWorkorder(models.Model):
|
||||
Oven = self.env.get('fusion.plating.bake.oven')
|
||||
facility = self.x_fc_facility_id
|
||||
|
||||
# ---- Bath ----
|
||||
if kind == 'wet' and not self.x_fc_bath_id and Bath is not None:
|
||||
bath_domain = [('active', '=', True)]
|
||||
d = [('active', '=', True)]
|
||||
if facility and 'facility_id' in Bath._fields:
|
||||
bath_domain.append(('facility_id', '=', facility.id))
|
||||
baths = Bath.search(bath_domain, limit=2)
|
||||
d.append(('facility_id', '=', facility.id))
|
||||
baths = Bath.search(d, limit=2)
|
||||
if len(baths) == 1:
|
||||
self.x_fc_bath_id = baths.id
|
||||
|
||||
# ---- Tank ----
|
||||
if kind == 'wet' and self.x_fc_bath_id and not self.x_fc_tank_id and Tank is not None:
|
||||
tank_domain = [('active', '=', True)]
|
||||
d = [('active', '=', True)]
|
||||
if 'bath_id' in Tank._fields:
|
||||
tank_domain.append(('bath_id', '=', self.x_fc_bath_id.id))
|
||||
tanks = Tank.search(tank_domain, limit=2)
|
||||
d.append(('bath_id', '=', self.x_fc_bath_id.id))
|
||||
tanks = Tank.search(d, limit=2)
|
||||
if len(tanks) == 1:
|
||||
self.x_fc_tank_id = tanks.id
|
||||
|
||||
# ---- Oven ----
|
||||
if kind == 'bake' and not self.x_fc_oven_id and Oven is not None:
|
||||
oven_domain = [('active', '=', True)]
|
||||
d = [('active', '=', True)]
|
||||
if facility and 'facility_id' in Oven._fields:
|
||||
oven_domain.append(('facility_id', '=', facility.id))
|
||||
ovens = Oven.search(oven_domain, limit=2)
|
||||
d.append(('facility_id', '=', facility.id))
|
||||
ovens = Oven.search(d, limit=2)
|
||||
if len(ovens) == 1:
|
||||
self.x_fc_oven_id = ovens.id
|
||||
|
||||
def _fp_classify_kind(self):
|
||||
"""Bucket this WO into one of: wet / bake / mask / rack / inspect / other.
|
||||
# Keyword fallbacks per kind (lowercase name match).
|
||||
BAKE_KEYWORDS = ('bake', 'oven', 'cure', 'heat treat')
|
||||
MASK_KEYWORDS = ('mask', 'de-mask', 'demask', 'tape')
|
||||
RACK_KEYWORDS = ('rack', 'de-rack', 'derack', 'fixture')
|
||||
INSPECT_KEYWORDS = ('inspect', 'qa', 'qc', 'fai', 'final check')
|
||||
|
||||
Priority: explicit linked equipment > workcenter process family >
|
||||
WO name keyword. Wet wins over bake when both signals appear
|
||||
(you can have an "alkaline clean before bake" — that's still wet).
|
||||
def _fp_classify_kind(self):
|
||||
"""Bucket this WO into wet/bake/mask/rack/inspect/other.
|
||||
|
||||
Priority order (top wins):
|
||||
1. Explicit equipment links (bath_id / oven_id) — definitive.
|
||||
2. Specific-process keywords (inspect/mask/rack/bake) beat
|
||||
the broader wet keywords. Otherwise "Post-plate Inspection"
|
||||
matches "plat" → wet, which is wrong.
|
||||
3. Workcenter wet process family — definitive.
|
||||
4. Wet name keyword fallback — broad (catches plat/etch/rinse...).
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self._fp_is_wet_process():
|
||||
if self.x_fc_bath_id:
|
||||
return 'wet'
|
||||
if self.x_fc_oven_id:
|
||||
return 'bake'
|
||||
name = (self.name or '').lower()
|
||||
if any(k in name for k in self.BAKE_KEYWORDS):
|
||||
return 'bake'
|
||||
if any(k in name for k in self.MASK_KEYWORDS):
|
||||
return 'mask'
|
||||
if any(k in name for k in self.RACK_KEYWORDS):
|
||||
return 'rack'
|
||||
if any(k in name for k in self.INSPECT_KEYWORDS):
|
||||
return 'inspect'
|
||||
if any(k in name for k in self.MASK_KEYWORDS):
|
||||
return 'mask'
|
||||
# Bake before Rack so "Oven bake (Post de-rack)" → bake (the
|
||||
# operation is bake; "Post de-rack" only describes the timing).
|
||||
if any(k in name for k in self.BAKE_KEYWORDS):
|
||||
return 'bake'
|
||||
if any(k in name for k in self.RACK_KEYWORDS):
|
||||
return 'rack'
|
||||
wc = self.workcenter_id
|
||||
fpwc = getattr(wc, 'x_fc_fp_work_center_id', False)
|
||||
if fpwc:
|
||||
families = set(fpwc.supported_process_ids.mapped('process_family'))
|
||||
if families & set(self.WET_FAMILIES):
|
||||
return 'wet'
|
||||
if any(k in name for k in self.WET_NAME_KEYWORDS):
|
||||
return 'wet'
|
||||
return 'other'
|
||||
|
||||
def _fp_is_wet_process(self):
|
||||
@@ -698,15 +710,12 @@ class MrpWorkorder(models.Model):
|
||||
"""Block button_start if the WO is missing data the shop must
|
||||
record for traceability + compliance.
|
||||
|
||||
Rules:
|
||||
• Every WO needs an assigned operator (x_fc_assigned_user_id) —
|
||||
without it, productivity records can't be attributed and
|
||||
proficiency tracking goes nowhere.
|
||||
• Wet (bath) WOs additionally need x_fc_bath_id + x_fc_tank_id —
|
||||
for chemistry traceability and physical-location audit
|
||||
(which exact tank ran the job).
|
||||
• Bake WOs need x_fc_oven_id — multiple ovens means we have
|
||||
to pin which one for the chart-recorder trail.
|
||||
Per-kind rules:
|
||||
• Every WO needs an assigned operator (x_fc_assigned_user_id).
|
||||
• Wet: bath + tank (chemistry traceability)
|
||||
• Bake: oven (chart-recorder trail)
|
||||
• Rack: rack/fixture (per-rack life tracking)
|
||||
• Mask: masking material (needed later when stripping)
|
||||
"""
|
||||
from odoo.exceptions import UserError
|
||||
for wo in self:
|
||||
@@ -722,6 +731,12 @@ class MrpWorkorder(models.Model):
|
||||
elif kind == 'bake':
|
||||
if not wo.x_fc_oven_id:
|
||||
missing.append(_('Oven'))
|
||||
elif kind == 'rack':
|
||||
if not wo.x_fc_rack_id:
|
||||
missing.append(_('Rack / Fixture'))
|
||||
elif kind == 'mask':
|
||||
if not wo.x_fc_masking_material:
|
||||
missing.append(_('Masking Material'))
|
||||
if missing:
|
||||
raise UserError(_(
|
||||
'Cannot start work order "%(wo)s" — please fill these '
|
||||
@@ -780,6 +795,42 @@ class MrpWorkorder(models.Model):
|
||||
'Request certification from your supervisor before starting this WO.'
|
||||
) % (employee.name, process_type.name))
|
||||
|
||||
def _fp_check_required_fields_before_finish(self):
|
||||
"""Block button_finish on bake WOs without the actual data
|
||||
Nadcap audits demand: setpoint temp, actual duration, and a
|
||||
chart-recorder reference on the oven (so the printed chart
|
||||
for this run can be retrieved).
|
||||
|
||||
Run-time data (temp + duration) belongs at FINISH because
|
||||
you don't know it until the bake is done. Chart-recorder ref
|
||||
is on the oven config — checked here as a defensive backstop.
|
||||
"""
|
||||
from odoo.exceptions import UserError
|
||||
for wo in self:
|
||||
if wo._fp_classify_kind() != 'bake':
|
||||
continue
|
||||
missing = []
|
||||
if not wo.x_fc_bake_temp:
|
||||
missing.append(_('Bake Temp (°F)'))
|
||||
if not wo.x_fc_bake_duration_hours:
|
||||
missing.append(_('Bake Duration (h)'))
|
||||
if wo.x_fc_oven_id and not wo.x_fc_oven_id.chart_recorder_ref:
|
||||
missing.append(_(
|
||||
'Chart Recorder Ref on oven "%s" '
|
||||
'(set on the oven record, not the WO)'
|
||||
) % wo.x_fc_oven_id.name)
|
||||
if missing:
|
||||
raise UserError(_(
|
||||
'Cannot finish bake work order "%(wo)s" — Nadcap / '
|
||||
'AS9100 require these fields before close:\n • %(fields)s\n\n'
|
||||
'On the iPad: tap the WO → Process Details → '
|
||||
'fill in Bake Temp + Duration. Chart Recorder Ref '
|
||||
'is configured on the oven record once.'
|
||||
) % {
|
||||
'wo': wo.display_name or wo.name,
|
||||
'fields': '\n • '.join(missing),
|
||||
})
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# T1.1 — Bake window auto-create on plating WO finish
|
||||
# T1.3 — Rack MTO increment when a rack was used
|
||||
@@ -791,6 +842,7 @@ class MrpWorkorder(models.Model):
|
||||
the proficiency tracker so workers earn credit toward auto-
|
||||
promotion (see fp.operator.proficiency).
|
||||
"""
|
||||
self._fp_check_required_fields_before_finish()
|
||||
res = super().button_finish()
|
||||
now = fields.Datetime.now()
|
||||
uid = self.env.user.id
|
||||
|
||||
@@ -168,18 +168,15 @@
|
||||
</xpath>
|
||||
|
||||
<!-- 5b. Process Details tab — content adapts to WO kind so
|
||||
operators see only the equipment fields that matter for
|
||||
their step (bath/tank for wet, oven for bake, etc.). -->
|
||||
operators see only the equipment fields that matter. -->
|
||||
<xpath expr="//notebook/page[@name='time_tracking']" position="after">
|
||||
<page string="Process Details" name="plating_details">
|
||||
<!-- Always-visible: facility (set everywhere) -->
|
||||
<group>
|
||||
<group string="Where">
|
||||
<field name="x_fc_facility_id"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Wet / bath WOs (plating, etch, rinse, strip, ...) -->
|
||||
<!-- Wet / bath WOs -->
|
||||
<group invisible="x_fc_wo_kind != 'wet'">
|
||||
<group string="Bath & Tank">
|
||||
<field name="x_fc_bath_id"
|
||||
@@ -195,52 +192,42 @@
|
||||
<field name="x_fc_dwell_time_minutes"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Bake / cure WOs -->
|
||||
<group invisible="x_fc_wo_kind != 'bake'">
|
||||
<group string="Oven">
|
||||
<field name="x_fc_oven_id"
|
||||
required="x_fc_requires_oven"/>
|
||||
</group>
|
||||
<group string="Bake Parameters">
|
||||
<group string="Bake Parameters (required at finish)">
|
||||
<field name="x_fc_bake_temp"/>
|
||||
<field name="x_fc_bake_duration_hours"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Rack / de-rack WOs -->
|
||||
<group invisible="x_fc_wo_kind != 'rack'">
|
||||
<group string="Rack">
|
||||
<field name="x_fc_rack_id"/>
|
||||
<field name="x_fc_rack_id" required="1"/>
|
||||
<field name="x_fc_rack_ref"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Mask / De-mask WOs — workcenter is the bench;
|
||||
no extra equipment fields, just a hint -->
|
||||
<!-- Mask / De-mask WOs -->
|
||||
<group invisible="x_fc_wo_kind != 'mask'">
|
||||
<div class="alert alert-info" role="alert">
|
||||
Masking / de-masking — work centre identifies
|
||||
the bench. Use chatter for any per-job notes
|
||||
on tape pattern, masking material, etc.
|
||||
</div>
|
||||
<group string="Masking">
|
||||
<field name="x_fc_masking_material" required="1"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Inspection / QC WOs -->
|
||||
<!-- Inspection -->
|
||||
<group invisible="x_fc_wo_kind != 'inspect'">
|
||||
<div class="alert alert-info" role="alert">
|
||||
Inspection — record Fischerscope readings via
|
||||
the Tablet Station (calibration std + n
|
||||
measurements per part). Readings auto-link
|
||||
to the CoC at MO done.
|
||||
the Tablet Station. Cal-std + n measurements
|
||||
per part. Readings auto-link to the CoC.
|
||||
</div>
|
||||
</group>
|
||||
|
||||
<!-- Generic WOs that don't fit any bucket -->
|
||||
<group invisible="x_fc_wo_kind != 'other'">
|
||||
<div class="alert alert-light text-muted" role="alert">
|
||||
Generic operation — equipment is identified
|
||||
by the work centre. Use chatter for job notes.
|
||||
by the work centre.
|
||||
</div>
|
||||
</group>
|
||||
</page>
|
||||
|
||||
Reference in New Issue
Block a user