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>
166 lines
6.5 KiB
Python
166 lines
6.5 KiB
Python
import logging
|
|
|
|
from odoo.http import request
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_detected_language(self, request_obj=None, **post):
|
|
req = request_obj or request
|
|
url_lang = req.params.get("lang")
|
|
post_lang = post.get("lang")
|
|
cookie_lang = req.httprequest.cookies.get("lang")
|
|
context_lang = req.env.context.get("lang")
|
|
user_lang = req.env.user.lang or "es_ES"
|
|
|
|
detected = None
|
|
if url_lang:
|
|
detected = url_lang
|
|
elif post_lang:
|
|
detected = post_lang
|
|
elif cookie_lang:
|
|
detected = cookie_lang
|
|
elif context_lang:
|
|
detected = context_lang
|
|
else:
|
|
detected = user_lang
|
|
|
|
_logger.info(
|
|
"🌐 Language detection: url=%s, post=%s, cookie=%s, context=%s, user=%s → DETECTED=%s",
|
|
url_lang,
|
|
post_lang,
|
|
cookie_lang,
|
|
context_lang,
|
|
user_lang,
|
|
detected,
|
|
)
|
|
return detected
|
|
|
|
|
|
def _get_translated_labels(self, lang=None, request_obj=None):
|
|
req = request_obj or request
|
|
if lang is None:
|
|
lang = _get_detected_language(self, request_obj=req)
|
|
|
|
env_lang = req.env(context=dict(req.env.context, lang=lang))
|
|
lang_code = (lang or "").split("_")[0]
|
|
|
|
fallbacks = {
|
|
"es": {
|
|
"Browse Product Categories": "Explorar categorías de productos",
|
|
"Save Draft": "Grabar pedido",
|
|
"Save order as draft": "Grabar pedido",
|
|
"Order saved as draft": "Pedido grabado",
|
|
"This order's cart is empty.": "El carrito de este pedido está vacío.",
|
|
"This order's cart is empty": "El carrito de este pedido está vacío",
|
|
},
|
|
"eu": {
|
|
"Browse Product Categories": "Produktu kategoriak arakatu",
|
|
"Save Draft": "Eskaera gorde",
|
|
"Save order as draft": "Eskaera gorde",
|
|
"Order saved as draft": "Eskaera gordeta",
|
|
"This order's cart is empty.": "Eskaera honen saskia hutsik dago.",
|
|
"This order's cart is empty": "Eskaera honen saskia hutsik dago",
|
|
},
|
|
"ca": {
|
|
"Browse Product Categories": "Explora categories de productes",
|
|
"Save Draft": "Desa l'Esborrany",
|
|
"Save order as draft": "Desa la comanda com a esborrany",
|
|
"Order saved as draft": "Comanda desada com a esborrany",
|
|
"This order's cart is empty.": "El carret d'aquesta comanda està buit.",
|
|
"This order's cart is empty": "El carret d'aquesta comanda està buit",
|
|
},
|
|
}
|
|
|
|
def tr(source):
|
|
translated = env_lang._(source)
|
|
if translated == source:
|
|
translated = fallbacks.get(lang_code, {}).get(source, translated)
|
|
return translated
|
|
|
|
labels = {
|
|
"product": tr("Product"),
|
|
"quantity": tr("Quantity"),
|
|
"price": tr("Price"),
|
|
"subtotal": tr("Subtotal"),
|
|
"total": tr("Total"),
|
|
"empty": tr("This order's cart is empty."),
|
|
"empty_cart": tr("This order's cart is empty."),
|
|
"browse_categories": tr("Browse Product Categories"),
|
|
"all_categories": tr("Browse Product Categories"),
|
|
"save_draft": tr("Save Draft"),
|
|
"save_order_as_draft": tr("Save order as draft"),
|
|
"order_saved_as_draft": tr("Order saved as draft"),
|
|
"confirm_and_pay": tr("Confirm and pay"),
|
|
"confirm_and_pay_hint": tr("Confirm the order and go to payment"),
|
|
"order_ready_for_payment": tr("Order ready for payment"),
|
|
"already_placed": tr("You already placed an order for this cycle."),
|
|
"no_payment_method": tr(
|
|
"Online payment is not available right now. Please contact your group."
|
|
),
|
|
"save_cart": tr("Save Cart"),
|
|
"reload_cart": tr("Reload Cart"),
|
|
"proceed_to_checkout": tr("Proceed to Checkout"),
|
|
"confirm_order": tr("Confirm Order"),
|
|
"back_to_cart": tr("Back to Cart"),
|
|
"remove_item": tr("Remove Item"),
|
|
"cancel": tr("Cancel"),
|
|
"confirm": tr("Confirm"),
|
|
"confirmation": tr("Confirmation"),
|
|
"items": tr("items"),
|
|
"items_placeholder": tr("items"),
|
|
"added_to_cart": tr("added to cart"),
|
|
"cart_restored": tr("Your cart has been restored"),
|
|
"order_loaded": tr("Order loaded"),
|
|
"invalid_quantity": tr("Please enter a valid quantity"),
|
|
"connection_error": tr("Connection error"),
|
|
"error_unknown": tr("Unknown error"),
|
|
"error_saving_draft": tr("Error saving cart"),
|
|
"error_loading_draft": tr("Error loading draft"),
|
|
"error_processing_response": tr("Error processing response"),
|
|
"draft_saved": tr("Order saved as draft"),
|
|
"draft_saved_success": tr("Cart saved as draft successfully"),
|
|
"draft_loaded_success": tr("Draft order loaded successfully"),
|
|
"reload_draft_confirm": tr(
|
|
"Are you sure you want to load your last saved draft?"
|
|
),
|
|
"reload_draft_replace": tr("This will replace the current items in your cart"),
|
|
"reload_draft_with": tr("with the saved draft."),
|
|
"clear_cart_confirm": tr(
|
|
"Are you sure you want to clear the cart? This will also cancel any saved draft order."
|
|
),
|
|
"cart_cleared": tr("Cart cleared"),
|
|
"draft_cancelled": tr("draft order cancelled"),
|
|
"home_delivery": tr("Home Delivery"),
|
|
"delivery_information": tr("Delivery Information"),
|
|
"delivery_info_template": tr(
|
|
"Delivery Information: Your order will be delivered at {pickup_day} {pickup_date}"
|
|
),
|
|
# Announced in the search results live region (realtime_search.js)
|
|
"no_results": tr("No products found"),
|
|
"products_found": tr("products found"),
|
|
# Accessible name of the notification dismiss button (website_sale.js)
|
|
"close": tr("Close"),
|
|
"important": tr("Important"),
|
|
"confirm_order_warning": tr(
|
|
"Once you confirm this order, you will not be able to modify it. Please review carefully before confirming."
|
|
),
|
|
"pickup_day_label": tr("Pickup Day"),
|
|
}
|
|
|
|
return labels
|
|
|
|
|
|
def _translate_labels(self, labels_dict, lang):
|
|
# Minimal fallback translator kept here; prefers env translations
|
|
translations = {
|
|
"es_ES": {},
|
|
}
|
|
lang_translations = translations.get(lang, {})
|
|
translated = {}
|
|
for key, english_label in labels_dict.items():
|
|
translated[key] = lang_translations.get(english_label, english_label)
|
|
_logger.info(
|
|
"[_translate_labels] Language: %s, Translated %d labels", lang, len(translated)
|
|
)
|
|
return translated
|