Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions website_sale_one_time_delivery_address/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

======================================
Website Sale One Time Delivery Address
======================================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:bbb4c0ac6f4dc68d3f0e8be3ee9aafd262305ea2f1d9c28d935ff8033597fbc5
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fe--commerce-lightgray.png?logo=github
:target: https://github.com/OCA/e-commerce/tree/19.0/website_sale_one_time_delivery_address
:alt: OCA/e-commerce
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/e-commerce-19-0/e-commerce-19-0-website_sale_one_time_delivery_address
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/e-commerce&target_branch=19.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module extends the website checkout delivery flow for
reseller-driven orders.

When the shopper enables the one-time delivery option during checkout,
the delivery address entered on the website is stored as a child contact
of the reseller with type ``one_time_delivery``.

This allows downstream processes such as EDI to keep a dedicated
recipient contact on the sale order while preserving the reseller as the
billing partner.

Main behavior:

- adds an *Allow Drop-shipping* flag on the customer that gates the
whole flow: the one-time delivery option is only shown for customers
that allow drop-shipping
- adds a one-time delivery toggle to the checkout page
- creates checkout delivery contacts with type ``one_time_delivery``
- keeps the invoice address on the reseller
- keeps the shipping address on the temporary end-customer contact
- makes one-time delivery contacts available in the checkout delivery
address list

**Table of contents**

.. contents::
:local:

Configuration
=============

The one-time delivery flow is only offered to customers that are allowed
to drop-ship.

To enable it for a customer:

1. Open the customer's contact form.
2. Tick *Allow Drop-shipping*.

The one-time delivery option then appears on the website checkout for
orders placed by that customer (the flag is read from the customer's
commercial entity, so it applies to all of its contacts). Customers
without the flag never see the option.

Usage
=====

To use this module, you need to:

1. Add products to the cart and go to the checkout.
2. In the delivery section, enable *One-Time Delivery Address*.
3. Enter the final recipient address.
4. Submit the delivery address form.

Result:

- a child contact is created on the reseller with type
``one_time_delivery``
- the sale order shipping partner points to that new contact
- the sale order invoice partner remains the reseller, even if the
browser submits a delivery-as-billing value
- the *Same as delivery address* toggle is hidden while the option is
enabled, so the temporary delivery address can never be reused as the
billing address

If the shopper disables the option, the standard website sale delivery
address behavior is kept and a regular ``delivery`` address is created
instead.

Automatic archiving
-------------------

One-time delivery contacts are temporary by nature. As soon as the sale
order is confirmed, its ``one_time_delivery`` shipping contact is
automatically archived so it stops cluttering the address book.
Archiving is reversible and the contact remains readable on the related
stock pickings and order history.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/e-commerce/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/e-commerce/issues/new?body=module:%20website_sale_one_time_delivery_address%0Aversion:%2019.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Camptocamp

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/e-commerce <https://github.com/OCA/e-commerce/tree/19.0/website_sale_one_time_delivery_address>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 2 additions & 0 deletions website_sale_one_time_delivery_address/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
25 changes: 25 additions & 0 deletions website_sale_one_time_delivery_address/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2026 Camptocamp
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

{
"name": "Website Sale One Time Delivery Address",
"summary": "Create one-time delivery contacts from checkout delivery addresses",
"version": "19.0.1.0.0",
"development_status": "Beta",
"category": "Website",
"website": "https://github.com/OCA/e-commerce",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"application": False,
"depends": ["sale_stock", "website_sale"],
"data": [
"views/res_partner_views.xml",
"views/templates.xml",
],
"assets": {
"web.assets_frontend": [
"website_sale_one_time_delivery_address/static/src/interactions/*.js",
],
},
"installable": True,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import main
125 changes: 125 additions & 0 deletions website_sale_one_time_delivery_address/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright 2026 Camptocamp
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from werkzeug.exceptions import Forbidden
Comment thread
Ricardoalso marked this conversation as resolved.

from odoo.http import request, route

from odoo.addons.website_sale.controllers.main import WebsiteSale


class WebsiteSaleOneTimeDeliveryAddress(WebsiteSale):
def _prepare_checkout_page_values(self, order_sudo, **kwargs):
result = super()._prepare_checkout_page_values(order_sudo, **kwargs)
result["one_time_delivery"] = order_sudo.one_time_delivery
return result

@route("/shop/update_address", type="jsonrpc", auth="public", website=True)
def shop_update_address(self, partner_id, address_type="billing", **kw):
"""Keep one_time_delivery in sync with the selected delivery partner.

Selecting a delivery address that is a one_time_delivery contact puts the
order in one-time mode (billing must stay on the reseller); selecting any
other delivery address leaves one-time mode. The parent security check
excludes one_time_delivery contacts from the allowed children, so for
those we validate ownership and set the shipping partner directly.
"""
order_sudo = request.cart
if order_sudo and address_type == "delivery":
ResPartner = request.env["res.partner"].sudo()
partner_sudo = ResPartner.browse(int(partner_id)).exists()
if partner_sudo.type == "one_time_delivery":
if (
not order_sudo.allow_dropship
or partner_sudo.commercial_partner_id
!= order_sudo.partner_id.commercial_partner_id
):
raise Forbidden()
order_sudo.one_time_delivery = True
if partner_sudo != order_sudo.partner_shipping_id:
order_sudo._update_address(partner_sudo.id, {"partner_shipping_id"})
return
order_sudo.one_time_delivery = False
return super().shop_update_address(partner_id, address_type=address_type, **kw)

@route(
"/shop/address/submit",
type="http",
methods=["POST"],
auth="public",
website=True,
sitemap=False,
)
def shop_address_submit(
self,
partner_id=None,
address_type="billing",
use_delivery_as_billing=None,
callback=None,
**form_data,
):
"""Override to enforce billing-stays-reseller when one_time_delivery is active.

When the order has one_time_delivery=True and a delivery address is being
submitted, we force use_delivery_as_billing=None so that:
- portal never sets type='other'
- partner_invoice_id is never updated by the parent logic
"""
order_sudo = request.cart
if order_sudo and order_sudo.one_time_delivery and address_type == "delivery":
use_delivery_as_billing = None
return super().shop_address_submit(
partner_id=partner_id,
address_type=address_type,
use_delivery_as_billing=use_delivery_as_billing,
callback=callback,
**form_data,
)

def _complete_address_values(
self,
address_values,
address_type,
use_delivery_as_billing,
*args,
order_sudo=False,
**kwargs,
):
result = super()._complete_address_values(
address_values,
address_type,
use_delivery_as_billing,
*args,
order_sudo=order_sudo,
**kwargs,
)
# After all parent logic has set the type, override it to one_time_delivery
# when the order is in one-time delivery mode and we are creating
# a delivery address. use_delivery_as_billing is already stripped
# to None by shop_address_submit, so
# this check is safe to run unconditionally for delivery addresses.
if address_type == "delivery" and order_sudo and order_sudo.one_time_delivery:
address_values["type"] = "one_time_delivery"
return result

@route(
"/shop/update_one_time_delivery",
type="jsonrpc",
auth="public",
website=True,
methods=["POST"],
)
def shop_update_one_time_delivery(self, one_time_delivery=False, **kwargs):
"""Toggle one-time delivery mode on the current cart.

The mode can only be enabled for customers that allow drop-shipping;
for any other customer the flag is forced to False, regardless of what
the request asks for.
"""
order_sudo = request.cart
if not order_sudo:
return {}
order_sudo.one_time_delivery = (
bool(one_time_delivery) and order_sudo.allow_dropship
)
return {}
2 changes: 2 additions & 0 deletions website_sale_one_time_delivery_address/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import res_partner
from . import sale_order
37 changes: 37 additions & 0 deletions website_sale_one_time_delivery_address/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2026 Camptocamp
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models
from odoo.fields import Domain


class ResPartner(models.Model):
_inherit = "res.partner"

type = fields.Selection(
selection_add=[("one_time_delivery", "One-Time Delivery Address")],
ondelete={"one_time_delivery": "set default"},
)
allow_dropship = fields.Boolean(
string="Allow Drop-shipping",
help=(
"When enabled, this customer can use the one-time delivery flow "
"during the website checkout: the delivery address is stored as a "
"temporary 'one_time_delivery' contact while billing stays on the "
"customer. The one-time delivery option is hidden for customers "
"that do not allow drop-shipping."
),
)

def _get_delivery_address_domain(self):
# Extend the delivery address domain to also list one_time_delivery
# contacts alongside the standard 'delivery'/'other' addresses.
res = super()._get_delivery_address_domain()
if all(partner.allow_dropship for partner in self):
res |= Domain(
[
("commercial_partner_id", "in", self.commercial_partner_id.ids),
("type", "=", "one_time_delivery"),
]
)
return res
38 changes: 38 additions & 0 deletions website_sale_one_time_delivery_address/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2026 Camptocamp
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).

from odoo import fields, models


class SaleOrder(models.Model):
_inherit = "sale.order"

one_time_delivery = fields.Boolean(
string="One-Time Delivery Address",
help=(
"When enabled, the delivery address created during the website "
"checkout is stored as a temporary contact with type "
"'one_time_delivery' instead of the standard 'delivery' type. "
"This keeps the recipient out of the customer's regular address "
"book and is useful for reseller orders shipped to an end customer."
),
)
allow_dropship = fields.Boolean(
related="partner_id.commercial_partner_id.allow_dropship",
)

def action_confirm(self):
"""Archive one-time delivery contacts once the order is confirmed.

A one_time_delivery contact is a temporary recipient that is no longer
needed for editing once the order is placed. Archiving (not deleting)
keeps it out of the address book while remaining readable on the
related pickings and order history.
"""
res = super().action_confirm()
one_time_partners = self.partner_shipping_id.filtered(
lambda partner: partner.type == "one_time_delivery"
)
if one_time_partners:
one_time_partners.action_archive()
return res
3 changes: 3 additions & 0 deletions website_sale_one_time_delivery_address/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
Loading
Loading