Odoo

Thin integration over Core. Adapts Werkzeug FileStorage from Odoo controllers and maps errors to JSON. Optional Odoo 17/18 addon wires settings, a service model, and an HTTP route. Python 3.10–3.12.

Install

Shell
pip install uploadkit-odoo uploadkit-security
Shell
uv add uploadkit-odoo uploadkit-security
Shell
poetry add uploadkit-odoo uploadkit-security

Adapter glue

Policy setup, validators, UploaderError, and the JSON response shape are shared — see Common patterns. Supply your own StorageProvider; creating ir.attachment records after upload is left to your module.

Uses as_uploadable() on Werkzeug FileStorage and json_error_response().

controllers.py
from odoo import http
from odoo.http import request
from uploadkit import Uploader, UploadPolicy, UploaderError
from uploadkit_odoo import as_uploadable, json_error_response
from uploadkit_security import default_validators


def notify(result):
    ...


class MyController(http.Controller):
    @http.route("/my/upload", type="http", auth="user", methods=["POST"], csrf=True)
    def upload(self, **kw):
        storage = get_provider()  # your StorageProvider factory
        policy = UploadPolicy(
            max_size=5 * 1024 * 1024,
            allowed_extensions=frozenset({"png"}),
            allowed_mime_types=frozenset({"image/png"}),
            validators=default_validators(),
        )
        uploaded = kw.get("file")
        try:
            result = Uploader(policy, storage).upload(
                as_uploadable(uploaded),
                bucket="uploads",
                object_name=uploaded.filename,
                after_upload=notify,  # or Celery-like .delay
            )
        except UploaderError as exc:
            return json_error_response(exc)
        return request.make_json_response(result.as_task_kwargs())

AWS: leave endpoint_url unset. MinIO: set it to your endpoint. Full class also in Storage.

storage.py
# my_module/storage.py
import boto3
from botocore.client import Config
from odoo.tools import config


class Boto3S3Storage:
    def __init__(
        self,
        *,
        access_key: str,
        secret_key: str,
        region: str = "us-east-1",
        endpoint_url: str | None = None,
    ) -> None:
        kwargs: dict = {
            "service_name": "s3",
            "aws_access_key_id": access_key,
            "aws_secret_access_key": secret_key,
            "region_name": region,
            "config": Config(signature_version="s3v4"),
        }
        if endpoint_url:
            kwargs["endpoint_url"] = endpoint_url
        self.client = boto3.client(**kwargs)

    def put(self, *, bucket, object_name, body, content_type):
        resp = self.client.put_object(
            Bucket=bucket,
            Key=object_name,
            Body=body,
            ContentType=content_type,
        )
        return resp.get("ETag")


def get_provider():
    """Factory used by uploadkit.storage_provider config parameter."""
    return Boto3S3Storage(
        access_key=config.get("uploadkit_access_key", ""),
        secret_key=config.get("uploadkit_secret_key", ""),
        region=config.get("uploadkit_region", "us-east-1"),
        endpoint_url=config.get("uploadkit_endpoint_url") or None,
    )

Add this repo’s addons/ to Odoo addons_path, install UploadKit in Apps, then configure under Settings → UploadKit (storage factory, bucket, optional prefix / max size).

service.py
# After installing the UploadKit Odoo addon:
result = env["uploadkit.service"].upload(
    file_storage,
    object_name="docs/a.pdf",
)
# result is UploadResult.as_task_kwargs() — no after_upload parameter
# Enqueue from the dict, or call Uploader.upload(..., after_upload=...) yourself

# Or POST multipart to /uploadkit/upload (auth=user, CSRF)
# field: file  |  optional: object_name

After-upload on library controllers: Common patterns · Full Boto3S3Storage class: Storage · uploadkit-odoo README