[ADD] website_sale_aplicoop: online payment per group order

Members can now pay their eskaera at checkout, through the standard Odoo
payment machinery. Enabled per group order with a new `online_payment`
boolean, off by default: an order without it behaves exactly as before,
members save a draft and the cutoff cron confirms them in bulk.

The flow mirrors website_sale's: the checkout button becomes "Confirm and
pay", saving the cart redirects to a new /eskaera/<slug>/payment step that
renders `payment.form` from `sale`'s `_get_payment_values`, and the standard
/my/orders/<id>/transaction route takes it from there. This addon ships no
provider and configures none; the co-op publishes whichever it wants.

`website_sale`'s `_get_shop_payment_values` is deliberately not reused: it
runs `_get_shop_payment_errors`, which blocks on shippable products without a
delivery method — exactly an eskaera order, collected at the co-op with no
carrier. For the same reason the transaction route stays the portal one,
which does not call `_check_cart_is_ready_to_be_paid()`.

Payment confirms the order, which has three consequences handled here:

* `payment.transaction._check_amount_and_confirm_order` now confirms group
  orders with `from_orderpoint=True`, the way the cutoff cron already does.
  Without it a product with a broken replenishment route raises inside
  `_post_process`, and `/payment/status/poll` rolls back and re-raises: the
  member sees a payment error over a `done` transaction and the retry cron
  fails forever.
* `_confirm_linked_sale_orders` also sweeps the cycle's already confirmed
  orders into the picking batch, scoped by `pickup_date`. Its early return on
  "no drafts" ran before any batching, so a fully prepaid cycle produced no
  batch at all. `_cron_batch_paid_orders_of_closed_cycles` covers the same
  hole for cycles closed by hand.
* A duplicate-order guard answers 409 on save-order, add-to-cart and
  load-draft, and shows a notice on the shop, so a member whose order is
  already placed cannot build and pay for a second one.

The payment policy lives in the model rather than the controller: there are
three sale.order creation paths and two are live, so `_compute_require_payment`
and `_compute_prepayment_percent` are extended instead of patching five vals
dicts. Orders are also created under the group order's company, which is what
filters the payment providers.

Along the way: eskaera drafts were invisible in /my/orders. The portal rule is
`message_partner_ids child_of` and sale.order only subscribes the customer on
send or confirm, never on a draft create, so the `_prepare_orders_domain`
override that includes drafts never had any effect. Fixed with an explicit
`message_subscribe`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
GitHub Copilot 2026-08-16 21:59:35 +02:00
parent a67181ab42
commit 6ba554c91b
21 changed files with 1993 additions and 37 deletions

View file

@ -62,6 +62,33 @@ class SaleOrder(models.Model):
help="Whether this order includes home delivery",
)
@api.depends("company_id", "group_order_id", "group_order_id.online_payment")
def _compute_require_payment(self): # pylint: disable=missing-return
"""Let the group order decide whether its members pay online.
Orders outside a group order keep the company default.
No return: compute methods assign fields, and pylint-odoo's
`missing-return` does not know that about a `super()` call.
"""
super()._compute_require_payment()
for order in self:
if order.group_order_id:
order.require_payment = order.group_order_id.online_payment
@api.depends("require_payment", "group_order_id")
def _compute_prepayment_percent(self): # pylint: disable=missing-return
"""Group orders are paid in full, never with a down payment.
Written together with `require_payment` on purpose: the core compute
would otherwise pull `company_id.prepayment_percent`, which
`_check_prepayment_percent` rejects unless it is in (0, 1].
"""
super()._compute_prepayment_percent()
for order in self:
if order.group_order_id and order.require_payment:
order.prepayment_percent = 1.0
@api.depends(
"group_order_id",
"group_order_id.next_pickup_slot_id",