Build a Django inventory system with AI, and catch its mistakes
An inventory management system for an office IT store, built in Python and Django with an AI coding assistant: stock in and out across three locations, reorder alerts and a dashboard. Below are the rules that made it trustworthy, the code that matters, the four mistakes the AI made, and a checklist for reviewing AI-written code.
Try it: live demo (sample data, resets every night).
1. Give the AI rules, not screens
“Build me an inventory app” gets you an inventory-shaped app. These four rules got a correct one:
- Stock is never typed in. It is always the sum of every movement in and out.
- Never issue more than is on the shelf at that location (not just in total).
- Every movement has a reference: a purchase order, issue slip or help desk ticket.
- Write a test for each rule.
2. The data model
- Category, Supplier, Location: simple lists.
- Item: SKU, name, unit cost, reorder level.
- StockMovement: item, location, type (in, out, adjustment), signed quantity, reference, who and when.
There is no stock column. Current stock is calculated:
class ItemQuerySet(models.QuerySet):
def with_stock(self):
stock = Coalesce(Sum("movements__quantity"), Value(0))
return self.annotate(stock=stock).annotate(
value=ExpressionWrapper(
F("stock") * F("unit_cost"),
output_field=DecimalField(max_digits=14, decimal_places=2),
)
)
def low_stock(self):
return self.with_stock().filter(is_active=True, stock__lte=F("reorder_level"))
3. The rule that protects stock
This lives in the model’s clean(), so every form and the admin get the same check:
def clean(self):
self.normalise() # stock in = +, stock out = -
if not self.quantity:
raise ValidationError({"quantity": "Quantity cannot be zero."})
if self.quantity < 0 and self.item_id and self.location_id:
available = self.item.stock_at(self.location)
if available + self.quantity < 0:
raise ValidationError({"quantity": (
f"Only {available} {self.item.get_unit_display()} "
f"in stock at {self.location}.")})
4. The four mistakes the AI made
- Prices wrapped onto two lines on phones. Nothing was wrong in the code, so no test failed; it only showed up in phone screenshots. Fix:
white-space: nowrapon number columns. - Chart data encoded as JSON twice (once in the view, again by
json_script) and decoded twice in the browser. It worked, but was fragile. Fix: pass a plain dict tojson_script. - Restocks counted before they arrived in the sample data, leaving nine of twenty items low or empty. Fix: hold each purchase order until its arrival date.
- Issuing stock made it go up. The sample data used
bulk_create(), which never callssave(), so stock-out rows kept their positive quantity. Nothing crashed. Fix: set the sign on each row before the bulk insert, and a test that fails if any item is ever negative at any location.
m = StockMovement(item=item, location=loc, kind=kind, quantity=qty, reference=ref)
m.normalise() # bulk_create skips save(), so sign it here
movements.append(m)
...
StockMovement.objects.bulk_create(movements)
5. Run it
pip install "Django>=5.2,<5.3"
python manage.py migrate
python manage.py seed_demo
python manage.py test inventory
python manage.py runserver
6. Put it online for free
The demo runs on PythonAnywhere’s free plan and is deployed by GitHub Actions: a push runs the tests, then a small script uploads the code and a fresh sample database through PythonAnywhere’s API (the token lives in GitHub secrets) and reloads the site. The same workflow reloads fresh sample data every night.
- Demo mode: admin pages switched off, quantities capped, number of new records limited, debug off, secure cookies, secret key outside the code, and a banner saying the data resets nightly.
- The catch: a free PythonAnywhere site must be renewed every month (one click on the Web tab). If you forget, it is switched off, not deleted.
- Link to a short address on your own site (here,
howitwork.net/demo/inventory/) so the demo can move hosts without breaking old links.
7. Checklist before you trust AI code
- Give the business rules, and ask for a test for each one.
- Search for shortcuts that skip your rules:
bulk_create(),QuerySet.update(), raw SQL. - Check the numbers against something you know. If half the items are at zero, something is wrong.
- Look at it on a real phone.