printedparts stage 11: low-stock email alerts on threshold crossing
Some checks failed
CI / backend (push) Successful in 1m43s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 8s

Contract 0.12.0: send_email/send_alert join the plugin surface (the
mailer was core-only), PLUGIN-HOOKS and status docs updated, manifest
pins the new floor. The alert fires inside _ledger_write only when a
decrement CROSSES the item's threshold - one alert per depletion,
rearmed by restocking above - and is best-effort after the commit so
mail trouble can never fail a take. Recipients come from
printedparts_alert_email, falling back to the site alert_recipients.
on_enable re-seeds settings idempotently so existing installs pick up
new keys. Crossing/rearm semantics proven by test.
This commit is contained in:
cproudlock
2026-07-17 08:15:50 -04:00
parent fc0d48a6a7
commit df918ed38f
10 changed files with 110 additions and 6 deletions

View File

@@ -228,8 +228,12 @@ def _ledger_write(item, transactiontype, quantitychange, sso, name, reason=None)
"""Append a ledger row and move the cached quantity in ONE commit.
The single-commit invariant is what keeps quantityonhand equal to the
ledger sum; every write path must go through here.
ledger sum; every write path must go through here. Fires the low-stock
alert when this write CROSSES the item's threshold downward - crossing
(not being below) is the natural debounce: one alert per depletion, and
restocking above the threshold rearms it.
"""
quantitybefore = item.quantityonhand
item.quantityonhand += quantitychange
db.session.add(PrintedItemTransaction(
printeditemid=item.printeditemid,
@@ -240,6 +244,37 @@ def _ledger_write(item, transactiontype, quantitychange, sso, name, reason=None)
reason=reason,
))
db.session.commit()
if (quantitychange < 0
and quantitybefore > item.lowstockthreshold
and item.quantityonhand <= item.lowstockthreshold):
_send_lowstock_alert(item)
def _send_lowstock_alert(item):
"""Best-effort email when an item crosses its low-stock threshold.
Recipients: Setting printedparts_alert_email (comma-separated), falling
back to the site's alert_recipients. Never fails the transaction - the
ledger write already committed."""
from shopdb.api import send_email, send_alert
subject = (f'Low stock: {item.itemname} ({item.itemcode}) - '
f'{item.quantityonhand} left')
html = (f'<p><strong>{item.itemname}</strong> ({item.itemcode}) is down '
f'to <strong>{item.quantityonhand}</strong> '
f'(threshold {item.lowstockthreshold}).</p>'
f'<p>Bin: {item.binlocation or "-"}</p>'
f'<p>Time to print more.</p>')
try:
recipients = (Setting.get('printedparts_alert_email') or '').strip()
if recipients:
send_email([address.strip() for address in recipients.split(',')
if address.strip()], subject, html)
else:
send_alert(subject, html)
except Exception:
import logging
logging.getLogger(__name__).exception(
'Low-stock alert failed for %s', item.itemcode)
@printedparts_bp.route('/items/<int:item_id>/restock', methods=['POST'])