Skip to content

[18.0][MIG] punchout: cXML / OCI / IDS punchout for Odoo 18#1333

Draft
bosd wants to merge 15 commits into
OCA:18.0from
bosd:18.0-mig_punchout
Draft

[18.0][MIG] punchout: cXML / OCI / IDS punchout for Odoo 18#1333
bosd wants to merge 15 commits into
OCA:18.0from
bosd:18.0-mig_punchout

Conversation

@bosd
Copy link
Copy Markdown
Contributor

@bosd bosd commented Apr 25, 2026

Summary

This PR ports the punchout modules to Odoo 18.0, drawing from two earlier
contributions:

The modules are reorganised into a protocol-agnostic base plus per-protocol
add-ons, with optional purchase-order integration glue:

Module Purpose
`punchout` Backend / session models, UoM mapping, protocol-agnostic base. Depends on `uom_unece` from `OCA/community-data-files`.
`punchout_cxml` cXML protocol handler
`punchout_oci` OCI protocol handler
`punchout_ids` IDS protocol handler (German)
`punchout_purchase` Purchase order creation from cart data
`punchout_cxml_purchase` cXML → PO glue (auto-installed)
`punchout_oci_purchase` OCI → PO glue (auto-installed)
`punchout_ids_purchase` IDS → PO glue (auto-installed)

Notable design points

  • UoM resolution — `punchout.uom.mapping` resolves a supplier UoM code
    through six tiers: backend-specific → supplier-specific → global → UNECE
    (via `uom_unece`) → `uom.uom.name` match → caller default. Common
    non-UNECE codes (STUECK, ST, STK, PC, PCS, EACH, KG, M, L) ship as
    `noupdate`-flagged global defaults.
  • Product matching — uses standard `product.supplierinfo`
    (vendor product code) rather than a custom mapping model.
  • Auto-creation — when a cart item doesn't match an existing product
    and `auto_create_products` is enabled, the protocol glue creates a new
    `product.product` carrying the supplier-provided UoM.

Migration source

Branched off OCA/edi `18.0` and ported via `oca-port` from the original
contributions. Original copyright headers are preserved on touched files;
migration credit is added alongside, per OCA convention.

Test plan

  • `pre-commit run --all-files` clean on every touched file
  • `punchout` — install + 24 tests pass (UoM resolution chain,
    backend/session creation, scope constraints)
  • `punchout_cxml` / `_oci` / `_ids` — install + 4 / 9 / 9 tests pass
    (protocol parsing, controllers, session storage)
  • `punchout_purchase` — install + 9 tests pass (PO creation flow)
  • `punchout_cxml_purchase` / `_oci_purchase` / `_ids_purchase` —
    install + 9 / 10 / 9 tests pass (cart → PO line mapping, UoM
    mapping, malformed payloads, wrong-protocol deferral, regression
    against permissive matching)
  • End-to-end: drive a real OCI cart through the full flow against a
    local mock supplier and confirm a draft PO is created with the
    expected lines and UoMs

Total: 65 tests across the 8 modules, all green on a fresh `punchout_sandbox`
build (Odoo 18.0 + uom_unece 18.0).

Open items

  • Manual review desired on the cart parsers (cXML `ItemIn`, OCI form-data
    decoding, IDS `OrderItem`) — these were the most adapted areas going
    from 13.0 / 16.0 to 18.0.
  • The IDS XML schema fields (`KUNDENUMMER`, `ARTIKELNUMMER`, etc.) come
    from the Therp PR; if any IDS-using OCA member has a sample payload
    from a real supplier I'd love to validate against it.

@bosd bosd marked this pull request as draft April 25, 2026 18:23
@bosd bosd force-pushed the 18.0-mig_punchout branch from 42f2fd5 to 5249d7f Compare April 25, 2026 18:25
@OCA-git-bot OCA-git-bot added series:18.0 mod:punchout Module punchout mod:punchout_cxml Module punchout_cxml mod:punchout_ids Module punchout_ids mod:punchout_oci Module punchout_oci mod:punchout_cxml_purchase Module punchout_cxml_purchase mod:punchout_ids_purchase Module punchout_ids_purchase mod:punchout_oci_purchase Module punchout_oci_purchase mod:punchout_purchase Module punchout_purchase labels Apr 25, 2026
@bosd bosd force-pushed the 18.0-mig_punchout branch from 027e751 to b9a08cb Compare April 25, 2026 18:56
@OCA-git-bot OCA-git-bot added the mod:punchout_oci_mock_supplier Module punchout_oci_mock_supplier label Apr 25, 2026
@bosd bosd force-pushed the 18.0-mig_punchout branch from c62d2ff to a517c35 Compare April 27, 2026 10:45
bosd and others added 9 commits April 27, 2026 14:15
…ction hardening

Carries the base ``punchout`` module forward from 13.0 with a
protocol-agnostic refactor (cXML-specific logic moved out into
``punchout_cxml``) plus the production-hardening work that landed
during PR review:

* Protocol modularity: ``_get_post_punchout_setup_url``,
  ``_store_punchout_session_response`` and ``_validate_response``
  are now thin overridable hooks; the 13.0 cXML-only implementation
  moved to ``punchout_cxml``.

* ``punchout.uom.mapping`` model: 6-tier resolution
  (backend → supplier → global → UNECE → uom name → caller default)
  with ``data/uom_mapping_data.xml`` shipping common non-UNECE codes
  as global defaults (STUECK, ST, STK, PC, PCS, EACH, KG, M, L).

* Stored ``name`` field on ``punchout.session`` for human-readable
  Many2one displays.

* ``punchout.backend`` inherits ``mail.thread`` and tracks state,
  protocol, URL, callback URL and session duration. Smart button
  to sessions. ``session_retention_days`` field + daily GC cron.
  ``max_response_size`` field + ``_check_response_size`` helper
  used by protocol controllers as a first-line flooding guard.

* Hide the "Received" pane on the session form when
  ``setup_request_response`` is empty (only cXML fills it).

* ``_get_browser_form_post_url`` produces RFC-clean URLs.

* Redirect to the session form after the cart is received.

* Dutch translation.

Co-Authored-By: Thomas Binsfeld <thomas.binsfeld@acsone.eu>
Co-Authored-By: Benjamin Willig <benjamin.willig@acsone.eu>
Co-Authored-By: Holger Brunn <hbrunn@hunki-enterprises.com>
Carries the cXML protocol module forward from 13.0 (where its logic
lived inside ``punchout``). After the protocol-modular refactor in
the base module, this module owns:

* QWeb template for ``PunchOutSetupRequest`` rendering with proper
  cXML DTD declaration and ``cXML/`` payload structure.

* HTTP setup-request POST + response parsing
  (``_get_post_punchout_setup_url``, ``_check_punchout_request_ok``).

* Cart-callback parsing — extracts ``BuyerCookie`` from the returning
  ``PunchOutOrderMessage`` and matches it to the originating session.

* Optional DTD validation against an admin-uploaded DTD file.

* ``punchout.backend`` extended with cXML credentials (From/To/Sender
  identities, shared secret, user agent, deployment mode).

* Receive controller at ``/punchout/cxml/receive/<backend_id>``,
  marked ``readonly=False`` so Odoo 18's speculative read-only
  cursor doesn't trigger a warning-and-retry on every callback.

* ``SELECT ... FOR UPDATE`` lock on the matched session so a
  concurrent callback (replay, retry) can't double-process.

* Cart-payload size cap via ``backend._check_response_size``.

* ``[punchout.cxml.*]`` log prefix for ops triage.

* **Test Connection** button on the backend form — sends a real
  setup request and verifies a valid response. Catches wrong URL /
  wrong credentials / expired DTD link before the user clicks Access
  and sees a confusing redirect failure mid-flow.

* HTTP-level controller tests covering happy path, unknown buyer
  cookie, and oversized payload.

Co-Authored-By: Thomas Binsfeld <thomas.binsfeld@acsone.eu>
Co-Authored-By: Benjamin Willig <benjamin.willig@acsone.eu>
OCI 3.0 / 4.0 (SELECT) catalog-browse protocol support. Inherits the
protocol-modular base from ``punchout``:

* Builds an OCI catalog URL with the supplier's URL + custom auth
  parameters + ``HOOK_URL`` for the cart-return endpoint.

* Receive controller at ``/punchout/oci/receive/<backend_id>``,
  marked ``readonly=False``. Parses the supplier's form-encoded
  ``NEW_ITEM-*`` payload back into a session.

* HOOK_URL carries a ``punchout_session_token`` query param (the
  session's buyer cookie) so the callback can pin the returning
  cart unambiguously to the originating session. Without this OCI
  has no per-session correlator and concurrent sessions on the same
  backend get mis-routed. Legacy "most recent draft session" lookup
  kept as fallback for backends configured before the token
  mechanism existed.

* ``SELECT ... FOR UPDATE`` lock on the matched session so a
  concurrent callback can't double-process.

* Cart-payload size cap via ``backend._check_response_size``.

* ``oci_version`` selection field on backend (3.0, 4.0, 5.0).
  4.0+ extended modes (DETAIL, SOURCING, etc.) are not yet
  implemented — see ROADMAP.

* ``[punchout.oci.*]`` log prefix for ops triage.

* HTTP-level controller tests covering happy path with token,
  unknown token, and oversized payload.

Co-Authored-By: Holger Brunn <hbrunn@hunki-enterprises.com>
IDS (German industry standard) catalog-browse protocol. Inherits the
protocol-modular base from ``punchout``:

* Builds an IDS catalog URL with name_kunde / kndnr / pw_kunde
  authentication parameters + hook_url for the cart-return endpoint.

* Receive controller at ``/punchout/ids/receive/<backend_id>``,
  marked ``readonly=False``. Parses the supplier's ``warenkorb`` XML
  payload back into a session.

* hook_url carries a ``punchout_session_token`` query param so the
  callback can pin the returning cart unambiguously to the
  originating session. Same rationale as the OCI sibling — IDS has
  no built-in per-session correlator. Legacy "most recent draft
  session" lookup kept as fallback.

* ``SELECT ... FOR UPDATE`` lock on the matched session.

* Cart-payload size cap via ``backend._check_response_size``.

* ``[punchout.ids.*]`` log prefix for ops triage.

* HTTP-level controller tests covering happy path with token,
  unknown token, and oversized payload.

Co-Authored-By: Holger Brunn <hbrunn@hunki-enterprises.com>
The protocol-agnostic glue that turns a returning punchout cart into
an Odoo purchase order, plus the user-facing entry points that
surface punchout where purchasers actually work:

* ``punchout.session.purchase_order_id`` link + auto-process: when
  the cart-receive callback flips the session to ``to_process`` and
  the backend has a ``partner_id``, the PO is created (or the cart's
  lines appended to a pre-linked PO) automatically. The session
  redirect lands the user on the PO.

* Browse Supplier Catalog button on draft purchase orders (vendor
  must have at least one Open punchout backend) — pre-links the
  session to the current PO so returning cart lines are appended
  rather than creating a new PO.

* Browse Supplier Catalog button on the vendor (res.partner) form —
  always creates a new PO from the returning cart.

* Open at supplier deep-link: ``product_url_template`` field on
  res.partner (e.g.
  ``https://supplier.example.com/parts/{vendor_code}``); the
  product template form and per-vendor row in the product Vendors
  list each get an "Open at supplier" link that substitutes the
  seller's product_code into the template.

* PO-line ``punchout_session_id`` tag column (admin-only) so
  purchasers can tell at a glance which lines came from which
  punchout cart vs were added manually.

* PO smart button counts every distinct session that contributed
  lines and opens a list view when more than one is involved.
  Admin-only (matches ``punchout.session`` security).

* Cart-vs-product chatter warnings on the PO: per-line UoM
  mismatches (cart UoM != product primary UoM, where Odoo
  silently coerces) and PO-level currency mismatches (cart prices
  in a different currency than the PO's pricelist resolved to;
  Odoo stores raw cart numbers as ``price_unit`` without conversion).

* Auto-process failures post a chatter message on the session and
  on the pre-linked PO so the purchaser is notified next to the
  affected record instead of having to read server logs.

* PO and chatter messages produced by the supplier-callback
  auto-process flow are attributed to the session's ``user_id``
  (the purchaser who initiated the punchout) instead of the
  sudo'd public user the callback runs as.

* Backend form surfaces ``partner_id``, ``company_id``,
  ``product_category_id`` and ``auto_create_products`` (the model
  carried these but the form never showed them).

* USAGE.md documenting all entry points and the tax-handling
  convention (cart prices land tax-excluded by default).

Co-Authored-By: Holger Brunn <hbrunn@hunki-enterprises.com>
Glue between ``punchout_cxml`` and ``punchout_purchase`` —
implements the protocol-specific cart parsing for cXML's
``PunchOutOrderMessage`` payload:

* Parses ``ItemIn`` elements into purchase order line vals.

* Auto-creates products when ``backend.auto_create_products`` is
  set, populating supplier code, name, price, currency and lead
  time from cart fields. ``currency_id`` resolved from cXML's
  ``UnitPrice/Money[@Currency]`` ISO code, falling back to the
  company currency (Odoo 18 made the field NOT NULL).

* UoM lookup routed through
  ``punchout.uom.mapping._get_uom_by_supplier_code`` for full
  6-tier resolution. Auto-created products carry the supplier-
  provided UoM as their primary UoM so the resulting order line
  doesn't trip same-category constraints.

* Warns (logger) when a supplier-part code matches multiple
  products for the same partner — picks the first deterministically
  rather than silently. Surfaces stale supplierinfo data without
  breaking the punchout flow.

Co-Authored-By: Thomas Binsfeld <thomas.binsfeld@acsone.eu>
Glue between ``punchout_oci`` and ``punchout_purchase`` —
implements the protocol-specific cart parsing for OCI's
form-encoded ``NEW_ITEM-*`` payload:

* Parses ``NEW_ITEM-*[index]`` form fields into purchase order
  line vals. Handles ``LONGTEXT_*:132[]`` long-description fields.

* Auto-creates products when ``backend.auto_create_products`` is
  set, populating supplier code (``VENDORMAT``), name, price,
  currency (``CURRENCY``) and lead time (``LEADTIME``).
  ``currency_id`` falls back to the company currency when the
  cart's code is unknown / absent (Odoo 18 made the field NOT NULL).

* UoM lookup routed through
  ``punchout.uom.mapping._get_uom_by_supplier_code`` for full
  6-tier resolution.

* Warns (logger) when a vendor code matches multiple products for
  the same partner — picks the first deterministically rather
  than silently.

Co-Authored-By: Holger Brunn <hbrunn@hunki-enterprises.com>
Glue between ``punchout_ids`` and ``punchout_purchase`` —
implements the protocol-specific cart parsing for IDS'
``warenkorb`` XML payload:

* Parses ``OrderItem`` elements (``ArtNo``, ``Kurztext``, ``Qty``,
  ``QU``, ``NetPrice``, ``VAT``, optional ``EAN``, optional
  ``Langtext``) into purchase order line vals.

* Product matching: by ``(seller.partner_id, ArtNo)`` and/or by
  ``barcode = EAN``. The previous ``OR barcode = False`` fallback
  matched almost any product in the database and grafted unrelated
  sellers onto them — fixed.

* Auto-creates products when ``backend.auto_create_products`` is
  set. Tax matching: looks up an existing purchase tax with the
  cart's ``VAT`` percentage on the same company.

* Delivery-date extraction: ``OrderInfo/DeliveryDate`` first, then
  ``DeliveryWeek``/``DeliveryYear`` (Friday of that week), else
  today.

* UoM lookup routed through
  ``punchout.uom.mapping._get_uom_by_supplier_code`` for full
  6-tier resolution. Odoo 19 dropped ``uom.uom.category_id`` and
  the reference-UoM concept; the supplier UoM is used directly as
  the product's primary UoM.

* Warns (logger) when an ArtNo matches multiple products for the
  same partner.

Co-Authored-By: Holger Brunn <hbrunn@hunki-enterprises.com>
…emos

Self-contained mock OCI supplier so the punchout flow can be
demonstrated end-to-end on a single Odoo instance — no external
supplier needed for runboat / demo / smoke testing.

* Public ``/oci/mock/catalog`` and ``/oci/mock/cart`` endpoints
  rendering a catalog browse page and a cart-submit page with the
  HOOK_URL the buyer supplied.

* Demo backend pre-configured to point at these endpoints, with
  ``partner_id`` already set so the cart-to-PO auto-process flow
  works out of the box on a fresh install.

* Ships the OCA icon.
@bosd bosd force-pushed the 18.0-mig_punchout branch from a517c35 to 1e5ae5d Compare April 27, 2026 13:12
bosd added 4 commits April 27, 2026 20:16
Empty hook fired once per newly-created product from an OCI cart,
so private / glue modules can enrich the product (image, dimensions,
HS code, brand) from the supplier's REST API without monkey-patching
``_get_or_create_product_oci``.

* Hook fires only on auto-create — re-using an existing product
  (matched via supplierinfo) does NOT call the hook. Keeps quota
  burn down for known products on overrides that hit the supplier's
  REST API.
* ``raw_data`` is the OCI ``NEW_ITEM-*`` cart-line dict so overrides
  can pull protocol-specific fields (VENDORMAT, LONGTEXT, CURRENCY,
  …) without re-parsing the whole cart.
* Tests cover both call-once-on-create and skipped-on-match paths.
Empty hook fired once per newly-created product from a cXML cart,
so private / glue modules can enrich the product (image, dimensions,
HS code, brand) from the supplier's REST API without monkey-patching
``_get_or_create_product_cxml``.

* Hook fires only on auto-create — re-using an existing product
  (matched via supplierinfo) does NOT call the hook.
* ``raw_data`` is a dict with ``supplier_part_id``, ``description``,
  ``unit_price``, and the raw ``item_detail`` lxml element so
  overrides can pull cXML-specific fields without re-parsing.
* Tests cover both call-once-on-create and skipped-on-match paths.
Empty hook fired once per newly-created product from an IDS cart,
so private / glue modules can enrich the product (image, dimensions,
HS code, brand) from the supplier's REST API without monkey-patching
``_get_or_create_product_ids``.

* Hook fires only on auto-create — re-using an existing product
  (matched via supplierinfo) does NOT call the hook.
* ``raw_data`` is the parsed IDS ``OrderItem`` lxml objectify
  element so overrides can pull IDS-specific fields (ArtNo, EAN,
  Langtext) without re-parsing.
* Tests cover both call-once-on-create and skipped-on-match paths.
…ocess author resolution

Two related fixes from real-world deployment testing:

1. **Hide "Browse Supplier Catalog" buttons when the vendor has no
   open punchout backend.**

   New computed field ``has_punchout_backend`` on ``purchase.order``
   (driven by ``partner_id``) and on ``res.partner`` (driven by
   ``supplier_rank``). Used by the ``invisible`` expression on the
   PO header button, the PO line area "Browse supplier catalog"
   link, and the vendor form smart button.

   Previously the buttons appeared on every draft PO and every
   supplier; clicking on a vendor without punchout raised a
   UserError. Now the affordance only shows when it's actionable.

2. **Harden ``action_create_purchase_order`` and
   ``_notify_auto_process_failure`` against empty ``env.user``.**

   The supplier-callback controller is ``auth="none"`` so
   ``env.user`` may be empty (or the public user). When
   ``session.user_id`` is also empty / unresolvable, ``author``
   ended up empty and ``with_user(empty)`` poisoned the env —
   ``env.uid`` resolved to nothing, ``env.user`` became an empty
   recordset, and the product-create chain crashed deep in stock's
   ``_default_responsible_id`` on
   ``self.env.user._is_superuser()`` (Expected singleton).

   Fix:
   * ``author = self.user_id or self.env.user or admin or
     SUPERUSER`` — always resolves to a real user.
   * ``_notify_auto_process_failure`` wraps both message_posts in
     try/except; losing the chatter notification entirely is worse
     than attributing it to admin, but crashing the whole callback
     is worse than losing the chatter.
   * Same hardening on the failure-notification path so a failure
     during auto-process can't itself cascade into a 500.

Tests cover the four ``has_punchout_backend`` cases (open backend
true, no backend false, draft backend false, non-supplier false)
on both purchase.order and res.partner.
@bosd bosd force-pushed the 18.0-mig_punchout branch from cd8ef11 to 381d01b Compare April 27, 2026 21:15
… is empty

Real-world deployment surfaced a residual issue from the previous
auto-process hardening: the chatter showed
"Auto-creation of the purchase order failed. Error: Expected
singleton: res.users()" even though the 500 was prevented. The
deeper auto-process chain (product create → mail.thread.create →
default_get → stock._default_responsible_id → env.user._is_superuser)
still tripped on an empty env.user, and the state-tracking message
was attributed to "unknown user" rather than a system actor.

**Root cause:** the supplier-callback controller is ``auth="none"``,
so ``env.user`` is the public user or an empty recordset. ``sudo()``
in Odoo 18+ flips the superuser flag but doesn't change ``env.uid``.
``with_user(empty)`` returns self unchanged (per the v19 docstring:
"if not user: return self"), so my previous ``with_user(author)``
fallback inside ``action_create_purchase_order`` only worked when
``author`` was a real user — it didn't help when the env was already
poisoned upstream.

**Fix:** detect controller-path writes (``vals.get("state") ==
"to_process"`` AND env.user / env.uid empty) at the very top of
``write()`` and re-enter the write under OdooBot
(``base.user_root``, the SUPERUSER). This:

* Attributes the state-tracking message to OdooBot rather than
  "unknown user" — clearly signals "system action", avoids
  confusion with manual admin edits.
* Provides a real ``self.env.user`` for the auto-process flow that
  follows. ``with_user(SUPERUSER)`` implicitly enables superuser
  mode (per Odoo docstring: "by convention, the superuser is always
  in superuser mode") so the deeper product-create chain bypasses
  partner / company ACLs that would otherwise resolve env.user to
  empty.
* Per-line / per-PO chatter attribution to the punchout-initiating
  user (``session.user_id``) is preserved via ``with_user(author)``
  for the actual writes inside ``action_create_purchase_order`` —
  only the system-level entry point is OdooBot.

Also simplified ``action_create_purchase_order`` and
``_notify_auto_process_failure`` to use ``base.user_root`` directly
as the system fallback (was ``base.user_admin`` → SUPERUSER chain).

Test: ``test_state_change_with_empty_env_user_does_not_crash``
simulates the controller path with an explicit empty-user env and
verifies the write completes without raising and state transitions
correctly. Pre-fix this test would crash with the
``Expected singleton: res.users()`` error.
… / storable / tracking on backend

Replaces the previously hardcoded ``type="consu"`` in
``_get_or_create_product_oci/cxml/ids`` with a backend-driven set of
defaults. Three new fields on ``punchout.backend``:

* ``default_product_type`` (Selection: Goods / Service, default
  Goods) — type set on auto-created products.
* ``default_is_storable`` (Boolean, default False) — when set, the
  auto-created product is marked storable so the warehouse tracks
  inventory. Typical for spare-parts vendors. Silently ignored when
  the ``stock`` module isn't installed (the field doesn't exist on
  product.template).
* ``default_tracking`` (Selection: By Quantity / By Lots / By Unique
  Serial Number, default By Quantity) — inventory-tracking method,
  only meaningful when ``default_is_storable`` is True.

New helper ``backend._get_auto_create_product_defaults()`` returns
the appropriate ``vals`` dict; the per-protocol create methods
splat it into their own product_vals build, replacing the previous
``"type": "consu"``, ``"purchase_ok": True``, and ``categ_id``
copy-paste in three places.

Stock-aware fields (``is_storable``, ``tracking``) are emitted only
when the corresponding ``product.template`` field exists in the
install — keeps the punchout stack usable on installs without
``stock``. The ``tracking`` key is emitted only when
``is_storable=True`` (avoids setting tracking on non-storable
products which Odoo would silently ignore anyway).

Note on Odoo 19: ``product.template.type`` no longer has the
``product`` (storable) value — all physical products are ``consu``
("Goods") and the storable flag is the separate ``is_storable``
boolean. The new fields surface this 19-era split cleanly. Odoo 18
treats the same fields the same way (``is_storable`` already exists
in 18 too), so the change is portable.

Backwards-compatible: the defaults (``type="consu"``,
``is_storable=False``) match the previous hardcoded behaviour.
Customers opt in to storable per-backend.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mod:punchout_cxml_purchase Module punchout_cxml_purchase mod:punchout_cxml Module punchout_cxml mod:punchout_ids_purchase Module punchout_ids_purchase mod:punchout_ids Module punchout_ids mod:punchout_oci_mock_supplier Module punchout_oci_mock_supplier mod:punchout_oci_purchase Module punchout_oci_purchase mod:punchout_oci Module punchout_oci mod:punchout_purchase Module punchout_purchase mod:punchout Module punchout series:18.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants