printedparts stage 4: catalog mutations, item photos, detail + form
POST/PUT/DELETE for items: create mints the itemcode from the configured prefix plus the flushed row id, update refuses quantityonhand (ledger-managed - restock/adjust arrive next stage), delete soft-retires. The image upload/serve/delete trio replicates the models.py pattern into instance/printedpartsimages/ with a public GET. PrintedItemDetail follows the unified detail skeleton (hero photo, info list, transaction history table); PrintedItemForm covers create/edit plus photo management on edit.
This commit is contained in:
@@ -64,3 +64,149 @@ def get_item(item_id: int):
|
||||
.limit(25).all())
|
||||
data['recenttransactions'] = [t.to_dict() for t in recent]
|
||||
return success_response(data)
|
||||
|
||||
|
||||
# --- catalog mutations (stage 6 adds permission gates on top of jwt) --------
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
from flask import current_app
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from shopdb.api import Setting
|
||||
|
||||
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp'}
|
||||
IMAGE_URL_PREFIX = '/api/printedparts/image/'
|
||||
|
||||
EDITABLE_FIELDS = ('itemname', 'itemdescription', 'lowstockthreshold',
|
||||
'binlocation', 'printnotes')
|
||||
|
||||
|
||||
def _imagedir():
|
||||
return os.path.join(current_app.instance_path, 'printedpartsimages')
|
||||
|
||||
|
||||
def _mint_itemcode(item):
|
||||
"""Set itemcode from the configured prefix + the flushed row id."""
|
||||
prefix = Setting.get('printedparts_code_prefix') or '3DP'
|
||||
item.itemcode = f'{prefix}-{item.printeditemid:04d}'
|
||||
|
||||
|
||||
@printedparts_bp.route('/items', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_item():
|
||||
"""Create a printed item; the itemcode is minted from the row id."""
|
||||
data = request.get_json() or {}
|
||||
itemname = (data.get('itemname') or '').strip()
|
||||
if not itemname:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'itemname is required')
|
||||
|
||||
threshold = data.get('lowstockthreshold')
|
||||
if threshold is None:
|
||||
threshold = int(Setting.get('printedparts_default_threshold') or 5)
|
||||
|
||||
item = PrintedItem(
|
||||
itemname=itemname,
|
||||
itemdescription=data.get('itemdescription'),
|
||||
lowstockthreshold=threshold,
|
||||
binlocation=data.get('binlocation'),
|
||||
printnotes=data.get('printnotes'),
|
||||
quantityonhand=0,
|
||||
)
|
||||
db.session.add(item)
|
||||
db.session.flush() # assigns printeditemid
|
||||
_mint_itemcode(item)
|
||||
db.session.commit()
|
||||
return success_response(item.to_dict(), message='Printed item created',
|
||||
http_code=201)
|
||||
|
||||
|
||||
@printedparts_bp.route('/items/<int:item_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
def update_item(item_id: int):
|
||||
"""Update catalog fields. Quantity moves ONLY through the ledger."""
|
||||
item = db.session.get(PrintedItem, item_id)
|
||||
if not item:
|
||||
return error_response(ErrorCodes.NOT_FOUND,
|
||||
f'Printed item {item_id} not found', http_code=404)
|
||||
data = request.get_json() or {}
|
||||
if 'quantityonhand' in data:
|
||||
return error_response(
|
||||
ErrorCodes.VALIDATION_ERROR,
|
||||
'quantityonhand is ledger-managed; use restock or adjust')
|
||||
for field in EDITABLE_FIELDS:
|
||||
if field in data:
|
||||
setattr(item, field, data[field])
|
||||
db.session.commit()
|
||||
return success_response(item.to_dict(), message='Printed item updated')
|
||||
|
||||
|
||||
@printedparts_bp.route('/items/<int:item_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_item(item_id: int):
|
||||
"""Soft-retire an item; its ledger history stays."""
|
||||
item = db.session.get(PrintedItem, item_id)
|
||||
if not item:
|
||||
return error_response(ErrorCodes.NOT_FOUND,
|
||||
f'Printed item {item_id} not found', http_code=404)
|
||||
item.isactive = False
|
||||
db.session.commit()
|
||||
return success_response(message='Printed item retired')
|
||||
|
||||
|
||||
# --- item image: the models.py upload/serve/delete trio ---------------------
|
||||
|
||||
@printedparts_bp.route('/items/<int:item_id>/image', methods=['POST'])
|
||||
@jwt_required()
|
||||
def upload_item_image(item_id: int):
|
||||
"""Upload (or replace) the photo for an item (multipart file=<image>)."""
|
||||
item = db.session.get(PrintedItem, item_id)
|
||||
if not item:
|
||||
return error_response(ErrorCodes.NOT_FOUND,
|
||||
f'Printed item {item_id} not found', http_code=404)
|
||||
upload = request.files.get('file')
|
||||
if not upload or not upload.filename:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR, 'No file provided')
|
||||
ext = os.path.splitext(upload.filename)[1].lower()
|
||||
if ext not in IMAGE_EXTENSIONS:
|
||||
return error_response(ErrorCodes.VALIDATION_ERROR,
|
||||
f'Unsupported image type {ext}')
|
||||
|
||||
imagedir = _imagedir()
|
||||
os.makedirs(imagedir, exist_ok=True)
|
||||
for old in glob.glob(os.path.join(
|
||||
imagedir, secure_filename(f'printeditem-{item_id}') + '.*')):
|
||||
os.remove(old)
|
||||
|
||||
filename = secure_filename(f'printeditem-{item_id}{ext}')
|
||||
upload.save(os.path.join(imagedir, filename))
|
||||
item.imageurl = f'{IMAGE_URL_PREFIX}{filename}'
|
||||
db.session.commit()
|
||||
return success_response(item.to_dict(), message='Item image uploaded')
|
||||
|
||||
|
||||
@printedparts_bp.route('/image/<path:filename>', methods=['GET'])
|
||||
def serve_item_image(filename):
|
||||
"""Serve an uploaded item image (public - kiosk and list read it)."""
|
||||
from flask import send_from_directory
|
||||
return send_from_directory(_imagedir(), filename)
|
||||
|
||||
|
||||
@printedparts_bp.route('/items/<int:item_id>/image', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
def delete_item_image(item_id: int):
|
||||
"""Clear an item image; delete the file only if this plugin owns it."""
|
||||
item = db.session.get(PrintedItem, item_id)
|
||||
if not item:
|
||||
return error_response(ErrorCodes.NOT_FOUND,
|
||||
f'Printed item {item_id} not found', http_code=404)
|
||||
url = item.imageurl or ''
|
||||
if url.startswith(IMAGE_URL_PREFIX):
|
||||
filename = secure_filename(url[len(IMAGE_URL_PREFIX):])
|
||||
path = os.path.join(_imagedir(), filename)
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
item.imageurl = None
|
||||
db.session.commit()
|
||||
return success_response(item.to_dict(), message='Item image removed')
|
||||
|
||||
Reference in New Issue
Block a user