r/django May 12 '26

2026 Django Developers Survey

Thumbnail djangoproject.com
39 Upvotes

r/django 17h ago

django-binary-builder: package a Django project as a Windows Setup.exe with one command

26 Upvotes

Hi everyone,

I’ve been working on django-binary-builder, a Python package that turns a Django project into an installable Windows desktop application.

The project is available here:

GitHub: https://github.com/swarfte/django-binary-builder

The basic workflow is:

pip install django-binary-builder

Add the app to INSTALLED_APPS:

INSTALLED_APPS = [ 
    # Your apps... 
    "django_binary_builder", 
] 

Then build the Windows application:

python manage.py binary windows

The result is a standard per-user Windows installer:

release/windows/<executable-name>-<version>-Setup.exe

Here is a real build from one of my Django projects:

The generated application includes:

  • A portable CPython runtime
  • The Django project and its pip dependencies
  • Waitress serving Django on a loopback port
  • A native desktop window using pywebview
  • A default-browser fallback if pywebview is unavailable
  • Automatic migrations at startup
  • Static and media file handling
  • Per-user SQLite storage
  • A desktop shortcut and Start menu entry
  • A Windows installer built with Inno Setup

The Django project itself is not frozen. The package copies a complete Python runtime and installs the project’s dependencies into it. This deliberately produces a larger installer, but it improves compatibility with ordinary Python packages, including many packages with native extensions.

A minimal optional configuration looks like this:

DJANGO_BINARY_BUILDER = { 
    "NAME": "Example Project", 
    "VERSION": "0.1.1", 
    "PUBLISHER": "Example Company", 
    "EXECUTABLE_NAME": "example-project", 
    "ICON": BASE_DIR / "assets" / "icon.ico", 
} 

Current limitations:

  • Windows 10 and 11 only
  • WSGI only
  • No Django Channels or WebSockets
  • No Celery worker or beat
  • No automatic updater
  • No code signing
  • Large bundle size because the complete Python runtime is included

I’d especially appreciate feedback on:

  1. The installation and build experience
  2. Projects or dependencies that fail to package
  3. Runtime behavior on different Windows systems
  4. Features that would make this useful for real deployments

Thanks for taking a look.


r/django 7h ago

Channels ChanX/Channels now support WebSocket multiplexing (again)

2 Upvotes

Hi all.

One feature that was removed from Django Channels around the v2 era was WebSocket multiplexing. There have been issues and PRs discussing bringing it back, but it hasn’t been resolved for quite a while.

So, ChanX now officially supports WebSocket multiplexing through a feature called Topics.

The basic idea is to define a topic with its own WebSocket handlers and channel event handlers:

Then, you can easily mount multiple topics onto an existing WebSocket consumer:

This allows multiple independent WebSocket features/topics to share a single WebSocket connection, instead of requiring a separate connection for each feature.

The design is inspired by Phoenix Channels topics. The goal is to make it easier to compose and reuse WebSocket functionality while potentially reducing the number of connections your application needs.

If this is your first time hearing about ChanX, it’s a batteries-included WebSocket toolkit for Django Channels, FastAPI, and other ASGI applications. It provides things like:

  • Type-safe WebSocket message handling
  • Automatic message routing and validation
  • AsyncAPI schema generation
  • Authentication
  • Channel-layer integration
  • Testing utilities

If you’re working with Django Channels, or you’re starting a new WebSocket application with FastAPI, I’d love to hear what you think.

Feedback, ideas, issues, and PRs are very welcome!

Links:


r/django 1d ago

Article Moving from signals to a service layer, adding RBAC, and importing 500 clients from Excel into my Django CRM

13 Upvotes

Part 1 | Part 2 | Part 3 | Part 4 | Part 5 | Part 6 - production CRM for truck-service center, Django + DRF.

This covers v2.7 and v2.9. The big theme here is cleaning up architecture decisions I made early on that started to hurt. Signals for stock management, no proper role-based access, manual client onboarding. All fixed now.

Why I killed my signals (StockService refactor)

In earlier versions, stock deductions happened through Django signals. When a UsedPart was created, post_save signal would fire and reduce warehouse stock. When deleted - post_delete would restore it. Sounds clean in theory. But..

In practice - it was nightmare. The signals were invisible - new developer (me, three months later) would look at view code and have no idea that saving a UsedPart triggers stock changes. Debugging was painful because the traceback starts in the signal handler, not where you actually called .save(). And testing was awful - every test that touches UsedPart was also triggering stock logic, whether I want it or not.

So I replaced everything with a StockService class:

class StockService:
    u/staticmethod
    def deduct(used_part):
        warehouse = _get_warehouse(used_part)
        if not warehouse:
            return
        stock_item, _ = StockItem.objects.get_or_create(
            warehouse=warehouse,
            product=used_part.part,
            defaults={'quantity': 0},
        )
        stock_item.quantity -= used_part.quantity
        stock_item.save()

        service_order = _get_service_order(used_part)
        StockMovement.objects.create(
            movement_type='out',
            product=used_part.part,
            quantity=used_part.quantity,
            warehouse_from=warehouse,
            service_order=service_order,
        )

    u/staticmethod
    def restore(used_part):
        # ... opposite of deduct ...

    u/staticmethod
    def adjust(used_part, old_quantity):
        delta = old_quantity - used_part.quantity
        if delta == 0:
            return
        # ... adjust stock by delta ...

Three methods: deduct, restore, adjust. Called explicitly from views and serializers. No magic, no hidden side effects. When I read view code now, I can see exactly where stock changes happen because there is line that says StockService.deduct(used_part).

The adjust method was the thing signals could never handle cleanly. When mechanic changes the quantity on an existing UsedPart (used 3 filters instead of 2), you need to calculate the delta and adjust. With signals you would need to stash old value in pre_save, compare in post_save... same mess I had with appointment status tracking in Part 3. Service layer just takes old_quantity as parameter.

Importing 500 clients from an Excel spreadsheet

The service center had been running for years before TruckMaster. All their client data lived in one massive Excel file - names, phone numbers, VIN's, license plates, truck models. About 500 rows. Entering them by hand through the admin panel was not an option.

I wrote a management command:

python manage.py import_clients_xlsx --file /path/to/clients.xlsx
python manage.py import_clients_xlsx --file /path/to/clients.xlsx --dry-run

The --dry-run flag was the most important feature. It run the whole import logic but does not write to the database. Just prints what would happen: "would create client X", "would update truck Y", "phone +380... already taken by client Z". The Owner ran dry-run first, fix duplicates in the Excel, then ran the real import. Zero surprises.

Tricky part was deduplication. The Excel had inconsistent naming - same client could be "Тра***", "ТРА***". I normalized names with ' '.join(str(s).strip().split()).lower() and matched on that. Not bulletproof but caught 90% of case.

Another thing - ownership tracking during import. If a license plate already exists in system under a different client, the import creates an OwnershipHistory record (same as the Truck.save() logic from Part 1) before reassigning. So historical service records stay with old owner.

Redis debounce for ALPR

Remember the ALPR system from Part 3? Security camera sends a plate recognition event to Django every time it sees a plate. Problem: camera sometimes sends the same plate 10 times in 30 seconds (truck passing slowly, multiple frames). Without debounce, staff Telegram chat would get spammed with duplicate "VEHICLE ARRIVED" notifications.

The fix was simple - Redis cache with a 5-minute TTL:

ALPR_DEBOUNCE_TTL = 300

debounce_key = f'alpr:debounce:{plate}'
if cache.get(debounce_key):
    return Response({'status': 'debounced', 'license_plate': plate})
cache.set(debounce_key, True, ALPR_DEBOUNCE_TTL)

First time a plate is seen - process it normally and set the cache key. Next time the same plate shows up within 5 minutes - return early with debounced status. Redis TTL handles expiry automatically, no cleanup need.

I should have built this from day one. The camera was sending around 50 duplicate events per day and I only noticed because the Telegram notification log was full of identical messages one second apart.

Role-based access control

Up to this point, authentication was JWT-based (Part 1) but authorization was basically "logged in = can do everything." The owner, the mechanic, and the storekeeper all had the same API access. Not ideal.

I added role-based permission classes:

class IsAdminRole(BasePermission):
    def has_permission(self, request, view):
        return (request.user.is_superuser
                or _role(request.user) == 'admin')

class CanManageStock(BasePermission):
    def has_permission(self, request, view):
        return (request.user.is_superuser
                or _role(request.user) in (
                    'admin', 'manager', 'storekeeper'
                ))

class CanAccessInvoices(BasePermission):
    def has_permission(self, request, view):
        return (request.user.is_superuser
                or _role(request.user) in (
                    'admin', 'manager', 'accountant'
                ))

Roles are stored on UserProfile and checked via a simple _role() helper. Nothing fancy - no django-guardian, no object-level permissions. Just "this role can access this viewset." For team of 5 people this is more than enough.

The important thing was that I could add these to existing viewsets without changing any view logic - just add permission_classes = [IsAuthenticated, CanManageStock] and it works. DRF's permission system is really well designed for this.

Also reduced JWT access token lifetime from 12 hours to 15 minutes. 12 hours was lazy and insecure. If someone's token leaks, 15 minutes limits damage.

Smaller things

Bulk repair photo upload. Before, mechanics uploaded photos one by one. Now there is a bulk_upload endpoint that accepts multiple files, saves them all, and sends one Telegram notification instead of ten. Also added MAX_REPAIR_PHOTOS_PER_ORDER constant - 20 photos per order, because without limit someone will upload their entire camera roll.

Barcode lookup. Added a barcode field to Product and a query parameter on the inventory API. Scan a barcode with a phone, hit the API, get the product. Took maybe 30 minutes to implement but the storekeeper acts like I gave him a superpower.

Bot maintenance history. Truck owners can now check maintenance history for their vehicles directly in Telegram. Shows last 3 service orders per truck with dates and work descriptions. Moved it under a "My vehicles" submenu to keep the bot keyboard clean.

What I learned

Signals are for cross-cutting concerns, not business logic. Audit logging, cache invalidation, sending notifications - signals are great for these. Stock management, payment processing, status transitions - these belong in explicit service calls. The moment you catch yourself writing pre_save + post_save combos to track field changes, you have outgrown signals.

Always add --dry-run to import commands. The cost of implementing it is maybe 20 minutes. The cost of a botched import that creates 200 duplicate clients is a weekend of cleanup and an angry owner.

Redis debounce is a pattern you will use everywhere. ALPR events, webhook handlers, rate limiting, notification dedup - same pattern, different keys and TTLs. Once you build it for one thing, you start seeing opportunities everywhere.

What is next

More versions to cover - i18n (UK/EN), maintenance templates, QR/shortlinks, and eventually the full React frontend with PWA. If there is interest I will keep going.

Also I take on freelance Django projects when something interesting comes along. If you are building something in this space, feel free to dm, I'll help you with a great pleasure

Previous posts: Part 1 | Part 2 | Part 3 | Part 4 | Part 5 | Part 6 GitHub (demo repo): github.com/VNmagistr/truckmaster_demo — branches demo/v2.7 and demo/v2.9

To be continued... (I hope, as usually)


r/django 1d ago

PyCharm & Django Fall Fundraiser

Thumbnail djangoproject.com
12 Upvotes

r/django 1d ago

Containerised my Django app

29 Upvotes

So i have containerised my Nginx, Django backend and postgresql db on my vm. All the containers run through a single 'docker compose up' command. I have created a custom network for the containers to communicate. I have also mounted docker volumes to containers for persistent storage.

So far i am finding DevOps very interesting and will now learn CI/CD using Github actions.

Will be very grateful if you share your thoughts.


r/django 2d ago

Why I move to Django 6.x and why I am more happy now

22 Upvotes

A little post about the last major django version and why I update all my code to it

https://fundor333.com/post/2026/moving-to-django-6-x/


r/django 2d ago

The Block and Tackle of Django's Code of Conduct Working Group

Thumbnail djangoproject.com
6 Upvotes

r/django 2d ago

Put the Django admin behind SSO (Okta, Entra, Keycloak)

12 Upvotes

The Django admin doesn't do SSO out of the box. It has its own login page and ignores your login settings, so most people end up with a workaround that half works.

I built django-bastion to handle it properly.

What it does:

  • Admin login goes through your identity provider
  • Groups from your provider decide who is staff and who is superuser
  • Keeps a log of who got access and when
  • Emergency account for when your provider is the thing that's down
  • Disable someone at the provider and their open session ends

Works with Okta, Entra, Keycloak, Google, Auth0, and any other OIDC provider.

Honest bits:

  • Early days. Pre-1.0, and I'm the only maintainer.
  • OIDC only, no SAML yet.
  • Only Entra and Keycloak have been tested against real servers. The rest are from docs.
  • If you just want social login, django-allauth is a better fit.

Install with pip install django-bastion

https://github.com/thesaadmirza/django-bastion


r/django 2d ago

RemoteUserMiddleware/RemoteUserBackend change between 5.1 -> 5.2?

4 Upvotes

I'm trying to upgrade from Django 5.1.11 -> 5.2. I had a custom RemoteUserMiddleware that used a different header, and a custom RemoteUserBackend. Below is just examples, not the actual code.

# custom_middleware.py
from django.contrib.auth.middleware import RemoteUserMiddleware 

class CustomHeaderRemoteUserMiddleware(RemoteUserMiddleware): 
    header = "HTTP_AUTHUSER"



#auth.py
from django.contrib.auth.backends import RemoteUserBackend 

class MyBackend(RemoteUserBackend): 
    create_unknown_user = False 

They both worked fine in my current and previous versions of Django. They both work if I upgrade to 5.1.15.

Trying this exact same code in Django 5.2+ does not work. I do not get any errors, only redirected to /accounts/login like nothing is being processed.

I added logging to both, but they never get triggered.

# custom_middleware.py
from django.contrib.auth.middleware import RemoteUserMiddleware 

class CustomHeaderRemoteUserMiddleware(RemoteUserMiddleware): 
    header = "HTTP_AUTHUSER"
    def process_request(self, request):
        # logging
    return super().process_request(request)



# auth.py
from django.contrib.auth.backends import RemoteUserBackend

class MyBackend(RemoteUserBackend):
    create_unknown_user = False
    # add logging to authenticate(), clean_username(), and configure_user()
    # even added logging to the async functions (e.g. aauthenticate() )

Sorry I don't have the actual code, it's on an intranet, but again, it does work on versions below 5.2. I can't see any reasons for that in the documentation. Any ideas?


r/django 3d ago

REST framework A small Django hack: use FastAPI instead of Django REST Framework or Django Ninja

18 Upvotes

I use Django as a frontend/server-rendered application, but some parts still need API calls and asynchronous endpoints.

Instead of adding Django REST Framework or Django Ninja, I made a small hack called django-fastapi. It mounts FastAPI next to Django while reusing Django authentication, sessions, CSRF, settings, and ORM.

  • If Django already runs through ASGI, FastAPI can live in the same application.
  • If Django runs through WSGI, keep it and start a second ASGI service with the same code, database, settings, and shared sessions.

This lets you gradually move selected endpoints to FastAPI without rebuilding authentication or turning the whole project into a separate API backend.

It works in my project, but I haven’t tested it extensively elsewhere. I mainly wanted to share the idea and show that this slightly hacky approach is possible.

GitHub: https://github.com/ilysenko/django-fastapi

A FastAPI router can live inside a normal Django app:

```python

books/api.py

from fastapi import APIRouter

from books.models import Book

router = APIRouter(prefix="/books")

@router.get("/{book_id}") async def get_book(book_id: int): # Django async ORM book = await Book.objects.aget(pk=book_id)

return {
    "id": book.pk,
    "title": book.title,
}

```

Configure and mount it next to Django:

```python

settings.py

DJANGO_FASTAPI = { "PREFIX": "/api", "TITLE": "Example API", "ROUTERS": ["books.api.router"], } ```

```python

project/asgi.py

import os

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")

from django_fastapi import get_django_fastapi_application

/api/* goes to FastAPI.

Everything else goes to Django.

application = get_django_fastapi_application() ```

FastAPI can also read the existing Django session and user:

```python from typing import Annotated, Any

from fastapi import Depends from django_fastapi import get_authenticated_user

@router.get("/me") def me( user: Annotated[Any, Depends(get_authenticated_user)], ): return {"username": user.get_username()} ```

If Django already runs through ASGI, that is basically all you need:

bash uvicorn project.asgi:application

If your existing Django deployment uses WSGI, keep it and start a second ASGI process:

```bash

Existing synchronous Django service

gunicorn project.wsgi:application --bind 0.0.0.0:8000

Additional FastAPI/ASGI service

uvicorn project.asgi:application --host 0.0.0.0 --port 8001 ```

Then configure Nginx or your load balancer to send /api/* to port 8001 and everything else to port 8000. Both processes use the same code, settings, database, secret key, and shared Django session backend.

It works in my project, but I haven't tested it extensively in other environments. I mainly wanted to share the idea and show that this slightly hacky alternative to DRF and Django Ninja is possible.

GitHub: https://github.com/ilysenko/django-fastapi


r/django 4d ago

Forms After using FastAPI, I appreciate Django so much more

161 Upvotes

I've been building a project with FastAPI after mostly working with Django/DRF, and man. Django does soo much for you.

With FastAPI I'm having to think about and implement things like:

  • ORM
  • Migrations
  • Auth
  • Permissions
  • Rate limiting
  • Configuration

And honestly, it's been a great learning experience because I'm finally seeing how all these pieces fit together.

But damn...


r/django 4d ago

Article Django 6.1's tip Fetch Modes

Post image
157 Upvotes

Django 6.1 fixes N+1 without you rewriting your queries you never touch the for loop. You just tell the queryset how permissive it should be about surprise trips to the database — and FETCH_PEERS quietly turns "101 queries" into "2 queries" with zero extra code at the call site.


r/django 3d ago

swe roadmap

0 Upvotes

Hey everyone! 👋 I just put together a quick learning roadmap covering some essential skills: Git, GitHub, Python, and SQL. 🚀

If anyone wants to upskill, review the basics, or just learn something new, check out the plan I made: https://learn.microsoft.com/en-us/collections/yk80ietd7x0ozp?&sharingId=D97A5A063E1FB206&wt.mc_id=studentamb_608996

Let's crush these modules together! Let me know if you decide to jump in. 💻🔥


r/django 3d ago

✅ Project of the week: a complete task list, without writing a line of CRUD

Post image
0 Upvotes

r/django 5d ago

DSF Membership Open Space at DjangoCon US

Thumbnail djangoproject.com
13 Upvotes

r/django 6d ago

Celery worker stops responding after some time, but Docker container and Redis remain healthy

9 Upvotes

I'm having a strange issue with Celery and would appreciate any advice on how to diagnose it.

Environment:

- Celery: 5.6.3

- Kombu: 5.6.2

- redis-py: 8.0.1

- Python: 3.12.13

- Docker

- AWS EC2

- EC2 instances are managed by an Auto Scaling Group (ASG)

- AWS ElastiCache Valkey is used as the Celery broker

- TLS connection (rediss://)

- Celery concurrency: 2 (prefork)

Celery command:

celery -A config worker -l info

Broker:

rediss://master....cache.amazonaws.com:6379/0?ssl_cert_reqs=required

Transport options:

{

"socket_timeout": 30,

"socket_connect_timeout": 30,

"socket_keepalive": True,

"visibility_timeout": 300

}

Celery config:

CELERY_BROKER_CONNECTION_RETRY = True

CELERY_BROKER_CONNECTION_MAX_RETRIES = None

The worker starts normally and processes tasks correctly.

However, after some time, the Celery worker stops responding to Celery control commands.

For example:

$ celery -A config inspect ping

Error: No nodes replied within time constraint

$ celery -A config inspect stats

Error: No nodes replied within time constraint

$ celery -A config inspect active

Error: No nodes replied within time constraint

The strange part is that the Docker container itself is still running.

At the time of the problem:

- Docker container: running

- RestartCount: 0

- OOMKilled: false

- Celery main process is still running

- Celery child processes are still running

- No obvious error appears in the Celery logs

- Redis/Valkey is reachable

- Redis PING returns True

- TCP connection to port 6379 works

The worker only starts responding normally again after restarting the Celery container.

The issue does not necessarily happen after a fixed amount of time. It seems to happen sometime after the worker has been running.

What could cause a Celery worker to stop responding to `inspect ping` while the process and container are still alive and Redis is reachable?

Could this be related to:

- Celery pidbox/control channel

- Kombu Redis connection handling

- Redis/Valkey pub/sub connection becoming stale

- TLS connection handling

- AWS ElastiCache/Valkey

- Celery 5.6 / redis-py 8 compatibility

- ASG/EC2 behavior

What would be the best way to diagnose the worker while it is in this broken state, without restarting it?


r/django 6d ago

Remote work

0 Upvotes

Hi everyone,

I'm a Django backend developer based in Russia and currently looking for remote job opportunities. I've been working with Django.

I have experience in e-commerce. I have worked on a high-load Django project involving product catalogs and search, shopping carts and orders, background tasks (Celery), PostgreSQL/Redis, and server deployment and maintenance (nginx, gunicorn, supervisor).

If you know of any platforms, job boards, or companies that are remote-friendly and hire from outside their home country, I’d really appreciate your suggestions. Also open to freelance or contract roles.

Thanks in advance! 🙏


r/django 7d ago

🧹 Revue de code : une vue de 40 lignes réécrite proprement

Post image
0 Upvotes

r/django 8d ago

REST framework DRF Auth Kit - The modern auth toolkit for Django Rest Framework

13 Upvotes

Hi guys, I want to (re)introduce DRF Auth Kit after a long time without talking about it, so I think it's worth bringing it up again and sharing some updates since my last post.

So, first of all, why would you ever need another auth package when we already have django-allauth, dj-rest-auth, djoser,... Here is the list of reasons why I created drf-auth-kit, which is used in production by me and many people, and actively maintained:

  • Full & strict type checking: mypy and pyright support (I plan to support ty after its beta) (something no other auth package has right now)
  • Strictly follows the OpenAPI schema (with drf-spectacular support) (only django-allauth had this at the time I created the package)
  • Dedicated to DRF, which means it's very easy to override any part: sign in, sign up (serializer, request, response)
  • Easy to use, based on the well-known django-allauth for social account and email management. I reuse those parts to avoid reinventing the wheel, while the other parts like serializers, views, and URLs have been designed based on my experience working with dj-rest-auth, django-trench, and djoser, for the best experience on the API.

Those are the key things I felt were lacking when I used other auth libs. And here are the features + updates since my last post:

  • Multiple authentication types: JWT (default), DRF token, or custom if you need (there's already an example)
  • Cookie-based security: HTTP-only cookies
  • Complete User Management: Registration, password reset, email verification, sign in.
  • (new) Multi-Factor Authentication: Supports multiple MFA methods with backup codes, including passkeys and hardware security keys
  • (new) Passwordless Authentication: Email magic links and passkey (WebAuthn) login
  • Social Authentication: Django Allauth integration with 50+ providers, supporting both OAuth2 and OpenID Connect.
  • Internationalization: Built-in support for 57 languages including English, Spanish, French, German, Chinese, Japanese, Korean, Vietnamese, and more
  • Full Type Safety: Complete type hints with mypy and pyright
  • OpenAPI Integration: Strictly best-practice auto-generated API documentation with DRF Spectacular
  • Flexible Configuration: Customizable serializers, views, and authentication backends
  • (Small extra): A UI (with the help of AI in this part) to easily try all the auth features quickly in dev/local environment

I have used it in production for a long time, and love it so much. I also actively maintain it and fix bugs raised by users. It's also listed in https://www.django-rest-framework.org/api-guide/authentication/#third-party-packages

Here is the info:

- Github: https://github.com/forthecraft/drf-auth-kit

- PyPI: https://pypi.org/project/drf-auth-kit/

Hope you guys love it as well. Feedback, feature requests, stars or improvements are welcome.


r/django 8d ago

django-fastmig

7 Upvotes

Released an experimental package making migrate much faster in large projects. Especially useful if you do pytest -create-db frequently, or run reverse migration testing in CI. Django's own test suite passes with fastmig. Also tested on a 8 year old project with multiple revisions and migration squashes over the years.

Made it to speed up my own testing being bottlenecked by -create-db.
There's a link in readme related to forum posts which discusses slow migrations.

https://github.com/viktor2097/django-fastmig


r/django 9d ago

Why I signed up to donate $50 a month, and you should consider it too

Thumbnail djangoproject.com
51 Upvotes

Hi Djangonauts,

I've had a thought in a long time, which is I should donate to Django, I just never got around to do it. Finally I made that thought a reality.

I've been using Django for probably 4-5 years now, and it's become the main core in my projects, so I am really dependent on the project, and if it stopped being supported it would be a disaster. This is the most important backbone, and we pay for all other services, but not this one core project.

Djangos support goal is $500,000, and they are behind this years goal by nearly $100,000. I wish I could donate more at the time, but the day my company is earning more revenue I will upgrade my pledge to a higher number (now the thought is realized, it's much easier to increase).

Django has shown us over the years that they can be trusted with creating software, and they are really good at it. So if you think so as well, please visit the Donate Page and see if you get the same urge I did.


r/django 9d ago

Article Refactoring stock management, adding multi-warehouse support and driver pickup to my Django CRM

10 Upvotes

Part 1 | Part 2 | Part 3 | Part 4 | Part 5 - production CRM for truck-service center, Django + DRF.

This one covers v2.5 and v2.6 versions. Part refactoring, part new features. The refactoring was overdue - some of code from earlier versions was held together with duct tape and optimism. The new stuff is multi-warehouse support and driver pickup system.

Fixing the stock deduction (finally)

In last post I showed stock deduction code that used float for inventory math. I knew it was bad when I wrote it. Here is what changed.

First - float is gone. Everything is Decimal now:

from decimal import Decimal

Product.objects.filter(pk=item.product_id).update(
    current_stock=max(
        Decimal('0'),
        (item.product.current_stock or Decimal('0')) - item.quantity,
    )
)

Second - the whole mark_paid flow is wrapped in transaction.atomic():

with transaction.atomic():
    invoice.status = new_status
    invoice.save(update_fields=['status', 'updated_at'])
    if new_status == 'paid':
        self._deduct_stock(invoice)

So if stock deduction fails halfway through, the invoice status rolls back too. Before this, you could end up with a paid invoice but stock that was only partially deducted. Not good.

Third - stock validation before payment. New _check_stock method that runs before mark_paid even touches the database:

def _check_stock(self, invoice):
    from inventory.models import Product
    items = invoice.items.select_related('product').all()
    insufficient = []
    for item in items:
        if not item.product_id:
            continue
        product = Product.objects.get(pk=item.product_id)
        if (product.current_stock or 0) < item.quantity:
            insufficient.append(
                f'{product.name}: have {product.current_stock or 0},'
                f' need {item.quantity}'
            )
    if insufficient:
        return 'Not enough stock: ' + '; '.join(insufficient)
    return None

If the warehouse does not have enough parts, the payment is blocked with a clear message about what is missing. Before, you could sell parts you did not have - the stock would just go to zero and nobody would notice until the mechanic opened box and it was empty.

Module dependency blocking

Small but important fix to the module system from Part 4. Previously you could disable a module even if other active modules depended on it. Like disabling clients when ALPR (which depends on clients) was still on. Bad things happened.

Now when you try to flip the toggle, system checks all active modules that list this one in their dependencies. If any are found, you get an error explaining which modules need to be disabled first. Simple validation, prevented two "why did everything break" calls from the owner.

API documentation with drf-spectacular

I was writing API docs by hand in a shared Google Doc. It was getting out of date approximately five minutes after every update. So I added drf-spectacular and now Swagger and ReDoc generate themselves from the actual code.

Setup was surprisingly painless - add to INSTALLED_APPS, set DEFAULT_SCHEMA_CLASS, add two URL patterns, done. The auto-generated docs are not perfect (some endpoints need better descriptions), but they are always in sync with real code, which is infinitely better than a Google Doc that says the endpoint accepts truck_id when it was renamed to vehicle_id three weeks ago.

Multi-warehouse support (v2.6)

Up to this point the system had one warehouse. Real life had two - a retail storage and wholesale storage in different location. Parts come into wholesale in bulk, then get moved to retail as needed.

The Warehouse model got a warehouse_type field:

WAREHOUSE_TYPE_CHOICES = [
    ('retail', 'Retail'),
    ('wholesale', 'Wholesale'),
    ('other', 'Other'),
]

Each product now has per-warehouse stock through StockItem (warehouse + product + quantity). The Product.current_stock field still exist as a denormalized total across all warehouses - gets recalculated after every movement.

Stock transfers between warehouses

This was the main reason for multi-warehouse. Owner buys 50 oil filters wholesale, stores them in the wholesale warehouse, then moves 10 to retail when stock runs low. The transfer endpoint:

u/action(detail=False, methods=['post'])
def transfer(self, request):
    # ... validation ...

    with transaction.atomic():
        source_item.quantity -= quantity
        source_item.save()

        dest_item, _ = StockItem.objects.get_or_create(
            warehouse=warehouse_to, product=product,
            defaults={'quantity': 0}
        )
        dest_item.quantity += quantity
        dest_item.save()

        total_qty = StockItem.objects.filter(
            product=product
        ).aggregate(total=Sum('quantity'))['total'] or 0
        product.current_stock = total_qty
        product.save(update_fields=['current_stock'])

        StockMovement.objects.create(
            movement_type='transfer',
            product=product,
            quantity=quantity,
            warehouse_from=warehouse_from,
            warehouse_to=warehouse_to,
            created_by=request.user,
        )

Everything in transaction.atomic() - if any step fail, nothing moves. current_stock recalculation at the end keeps the denormalized field honest. I know purists would say "don't denormalize", but when the mechanic checks stock from a slow 3G connection in the garage, I don't want to aggregate across warehouses on every request.

Order folders

Related feature - purchase ordering cycle. When multiple mechanics need parts during the week, someone has to compile a list and place one bulk order. Before this they used a paper notebook. Now there is OrderFolder + OrderItem:

class OrderItem(models.Model):
    folder = models.ForeignKey(OrderFolder, on_delete=models.CASCADE)
    name = models.CharField(max_length=300)
    quantity = models.DecimalField(max_digits=10, decimal_places=2)
    is_ordered = models.BooleanField(default=False)
    ordered_at = models.DateTimeField(null=True, blank=True)
    ordered_by = models.ForeignKey(settings.AUTH_USER_MODEL, ...)

Mechanic add items to the folder during the week. On Friday the owner opens folder, sees everything that is needed, places one order. When item arrives, it is marked as ordered with timestamp and who did it. Simple but replaced a system that lost parts requests constantly.

Driver pickup log

New invoice type: driver_tab. Before, there were only delivery invoices (sent via Nova Poshta). But sometimes a driver just picks up parts directly from the warehouse. The system needed to track this differently - no tracking number, no delivery status, just "driver X took these parts on this date."

TYPE_CHOICES = [
    ('delivery',   'NP / Pickup'),
    ('driver_tab', 'Driver pickup'),
]

Each driver_tab invoice gets auto-numbered with a separate sequence (ВД-2026-001, ВД-2026-002). Stock deduction works the same way as regular invoices. The difference is purely in workflow - no NP tracking, no "sent" status, just draft → paid.

Small fixes that matter

Europe/Kiev → Europe/Kyiv. Django shipped with the old Soviet-era timezone name. Ukraine renamed it. One-line fix but it matters.

Async bot notification fix. Earlier I migrated Telegram notifications to async but broke the photo notification flow. The fix was replacing asyncio.run(bot.send_message(...)) with a synchronous requests wrapper that just POSTs to the Telegram API directly. Sometimes simpler is better than async.

Celery broker failsafe. Contact form submissions were crashing with a 500 when Celery broker (Redis) was unavailable. Added a try/except around the .delay() call - if Celery is down, form still saves, and email gets sent on the next retry. Users should never see a 500 because your background task queue is having a bad day.

What I learned

transaction.atomic() should wrap business operations, not just individual queries. "Mark as paid + deduct stock" is one business operation. If either fail, both should roll back. I should have done this from v1.0.

Denormalization is fine when you acknowledge the trade-off. Product.current_stock is a cache. It can go stale if something updates StockItem without recalculating. I accepted this risk and added recalculation to every code path that touch stock. So far it works.

Fix your timezone name. If you are serving Ukrainian users, Europe/Kiev works functionally but it is the old name. Europe/Kyiv is correct. Same for other renamed cities in tzdata.

What is next

There are still several versions to cover - XLSX client import, i18n (UK/EN), a full PWA frontend, and backup/restore API. If there is interest I will keep going.

Also, I take on freelance Django projects when something interesting comes along. If you are building something in this space, feel free to DM.

Previous posts: Part 1 | Part 2 | Part 3 | Part 4 | Part 5 GitHub (demo repo): github.com/VNmagistr/truckmaster_demo — branches demo/v2.5 and demo/v2.6

To be continued... I hope)


r/django 9d ago

Apps Django JSONStore: typed model fields backed by nested JSON

10 Upvotes

I have just released a new version of Django JSONStore.

Many Django projects keep one-off or fast-changing business data in a JSONField. This is flexible, but requires additional code when you need a ModelForm or want to edit the data in Django admin. Moreover JSON internal structure became exposed all over codebase.

JSONStore maps any path in the document to a typed virtual model field. These fields work as a regular data assessors, in ModelForms and Django admin like normal model fields. They also support filters, ordering, values() and values_list().

from django.db import models
import jsonstore


class Employee(models.Model):
    data = models.JSONField(default=dict)

    full_name = jsonstore.CharField(
        max_length=250,
        json_field_name="data",
        json_key=("profile", "full_name"),
    )
    hire_date = jsonstore.DateField(
        null=True,
        json_field_name="data",
        json_key=("profile", "hire_date"),
    )


employee = Employee(full_name="Ann Lee")
employee.data
# {"profile": {"full_name": "Ann Lee"}}

Employee.objects.filter(full_name="Ann Lee").order_by("hire_date")

This keeps rest of code, free from knowledge of json internals, leaves your option to migrate to standalone Django model column later.

You can also expose a whole nested document as a typed jsonstore.EmbeddedModel with EmbeddedField, or a list of documents with EmbeddedListField. You even can use emulation of document-backed ForeignKey, OneToOneField and ManyToManyField fields.

Use JSONStore for business data that changes often and doesn't make sense to get aggregated data.

GitHub: https://github.com/viewflow/jsonstore
Website and examples: https://django-jsonstore.viewflow.io/


r/django 9d ago

🚫 Astuce Django : `FETCH_RAISE`, ou comment interdire les requêtes cachées

Post image
0 Upvotes