building a stock opname data collection app at alfamart
another project i built during my internship at alfamart supply & distribution was a web application for stock opname data collection. warehouse staff use pda devices to count inventory across zones. like the other two systems i worked on, this was part of the same migration program moving warehouse tooling off legacy visual basic and oracle onto flask and postgresql. this is a case study on building a full-stack flask app for that workflow, and what the gap looks like between a working app and a production-ready one.
the workflow
stock opname is the process of physically counting all items in a warehouse to verify inventory records. at alfamart distribution centers, this happens periodically across zones. operators log in, select the stock opname date, choose their assigned zone, and then walk through the racks entering quantities for each item. when a zone is complete, they finalize it. a supervisor can then review and finalize the entire stock opname session.
the application needed to support this multi-stage workflow: items start as unprocessed (flag_ptl null), move to processed (flag_ptl = 1) when an operator enters a quantity, and become finalized (flag_ptl = 2) when the zone is closed. the backend enforces that all items in a zone must be at flag_ptl = 1 before the zone can be finalized to flag_ptl = 2. this prevents partial finalization.
system architecture
the stack is flask 2.3.3 with jinja2 templates, tailwindcss v4 for the frontend, jquery for dom manipulation, and postgresql with psycopg2 connection pooling. the application is approximately 2,900 lines of python across routes, models, and configuration. the application code was a solo build: every commit in the repository is mine. the docker setup that exists today came later and from someone else, which i cover at the end.
the multilingual requirement
alfamart distribution centers employ workers from multiple countries. the application needed to support five languages: english, indonesian, bengali, tagalog, and vietnamese. i built a custom internationalization system using flask middleware and json translation files. the language selection is stored in the session and persists across requests. the translation engine covers 150+ strings across all templates.
# simplified i18n middleware
def get_translation(key, lang=None):
if lang is None:
lang = session.get('lang', 'id')
translations = load_translations(lang)
return translations.get(key, key) # fallback to key if missing
@app.before_request
def set_language():
if 'lang' in request.args:
session['lang'] = request.args['lang'] the fallback to the key string is intentional: if a translation is missing, the operator sees the key name rather than an empty string or an error. this made it easier to identify missing translations during testing.
the finalization validation problem
the most technically interesting part was the zone finalization check. before a zone can be finalized, every item in that zone must have been counted (flag_ptl = 1). the naive approach of checking this in application code is unreliable under concurrent access: two requests could both pass the check and both attempt to finalize. i pushed the validation into the database:
-- check all items in zone are processed before finalizing
SELECT COUNT(*) as unprocessed
FROM so_detail
WHERE so_id = %(so_id)s
AND zona = %(zona)s
AND (flag_ptl IS NULL OR flag_ptl = 0);
-- only proceed if unprocessed = 0 parameterized queries are used throughout the application to prevent sql injection. this is not optional for a system where operators are entering arbitrary text into quantity fields.
what is missing and why it matters
this application works. operators can log in, count items, and finalize zones. but there are gaps that matter for production use.
the first is credential storage, which is not hardened to a standard i would choose. i want to be accurate about why, because this one is not a shortcut i took. the specification i was given called for it, and i raised the question early, asking whether credentials should be hashed rather than stored and compared directly. the answer was to keep the existing behaviour. i implemented what was specified. i would still argue the other way if the question came up again, and i would want that argument settled before the system handles real credentials in production, but the decision was not mine to make.
there are also no automated tests. the application was tested manually during development, but there are no pytest tests for the business logic. the finalization validation, the language switching, the connection pool behavior under concurrent requests: none of these have automated coverage. adding pytest with a test database fixture would be the first thing i would do before moving this toward production.
on infrastructure i should be clear about what is mine and what is not. during development
i ran the application with flask run or gunicorn directly and did not containerize
it. docker was added later by my mentor, because our team did not have deployment access and
that part sat outside what we could do ourselves. so the system is containerized today, but
that is not my work and i will not claim it. there is still no ci/cd pipeline, and adding one
to run tests on every push would be the next infrastructure step.
what i learned
building this application taught me that the gap between "it works on my machine" and "it is ready for production" is not primarily about features. it is about the things that are invisible when everything goes right: password security, test coverage, deployment reproducibility, and error handling for edge cases that only appear under real usage.
the multilingual requirement was also a useful constraint. supporting five languages forces you to separate content from presentation in a way that single-language applications do not require. every string has to be externalized. every template has to be written without hardcoded text. that discipline makes the codebase cleaner even for the parts that are not related to translation.
the stock opname workflow is also a good example of how business logic shapes technical decisions. the three-state flag system (null, 1, 2) exists because the business has three distinct operational states that need to be tracked and enforced. the database validation before finalization exists because partial finalization would corrupt inventory records. understanding why those constraints exist made it easier to implement them correctly.