Skip to content

Commit e8bc030

Browse files
committed
brief - new change
1 parent 2234736 commit e8bc030

File tree

6 files changed

+455
-47
lines changed

6 files changed

+455
-47
lines changed

blueprints/finance/routes.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,146 @@ def delete_bill_permanent(bill_id):
208208
flash('Bill and all its associated items have been permanently deleted.', 'success')
209209
return redirect(url_for('finance.expenses'))
210210

211+
@finance_bp.route('/reconcile/atomic', methods=['POST'])
212+
def atomic_reconcile():
213+
user = _require_user()
214+
if not user or user.role not in ['admin', 'pantryHead']:
215+
return jsonify({'error': 'Unauthorized'}), 403
216+
217+
data = request.json
218+
if not data:
219+
return jsonify({'error': 'No data provided'}), 400
220+
221+
bill_id = data.get('bill_id')
222+
reconciliations = data.get('reconciliations', []) # List of {procurement_id: X, cost: Y}
223+
224+
if not bill_id:
225+
return jsonify({'error': 'Bill ID is required'}), 400
226+
227+
bill = tenant_filter(Bill.query).filter_by(id=bill_id).first()
228+
if not bill:
229+
return jsonify({'error': 'Bill not found'}), 404
230+
231+
try:
232+
total_reconciled_cost = 0
233+
for rec in reconciliations:
234+
proc_id = rec.get('procurement_id')
235+
cost = float(rec.get('cost') or 0)
236+
237+
item = tenant_filter(ProcurementItem.query).filter_by(id=proc_id).first()
238+
if item and (user.role == 'admin' or item.floor == bill.floor):
239+
item.actual_cost = cost
240+
item.bill_id = bill.id
241+
item.status = 'completed'
242+
item.expense_recorded_at = datetime.utcnow()
243+
total_reconciled_cost += cost
244+
245+
# Update bill total if it was a manual reconciliation adding to an existing bill
246+
# Or if we want the bill to reflect the sum of its items
247+
current_total = tenant_filter(db.session.query(func.sum(ProcurementItem.actual_cost))).filter(ProcurementItem.bill_id == bill.id).scalar() or 0
248+
bill.total_amount = current_total
249+
250+
db.session.commit()
251+
return jsonify({'success': True, 'reconciled_count': len(reconciliations), 'new_total': float(bill.total_amount)})
252+
except Exception as e:
253+
db.session.rollback()
254+
return jsonify({'error': str(e)}), 500
255+
256+
@finance_bp.route('/procurement/unbilled', methods=['GET'])
257+
def get_unbilled_procurement():
258+
user = _require_user()
259+
if not user:
260+
return jsonify({'error': 'Unauthorized'}), 401
261+
262+
floor = _get_active_floor(user)
263+
# Get items that are EITHER completed and unbilled, OR still pending
264+
# This allows matching a bill to something that was just bought but not yet marked done
265+
items = tenant_filter(ProcurementItem.query).filter(
266+
ProcurementItem.floor == floor,
267+
ProcurementItem.bill_id == None
268+
).order_by(ProcurementItem.status.desc(), ProcurementItem.created_at.desc()).all()
269+
270+
return jsonify({
271+
'items': [{
272+
'id': i.id,
273+
'name': i.item_name,
274+
'quantity': i.quantity,
275+
'status': i.status,
276+
'category': i.category,
277+
'created_at': i.created_at.strftime('%Y-%m-%d')
278+
} for i in items]
279+
})
280+
281+
@finance_bp.route('/reconcile/atomic/full', methods=['POST'])
282+
def atomic_reconcile_full():
283+
user = _require_user()
284+
if not user or user.role not in ['admin', 'pantryHead']:
285+
return jsonify({'error': 'Unauthorized'}), 403
286+
287+
data = request.json
288+
if not data:
289+
return jsonify({'error': 'No data provided'}), 400
290+
291+
floor = _get_active_floor(user)
292+
try:
293+
bill_date_str = data.get('bill_date')
294+
bill_date = datetime.strptime(bill_date_str, '%Y-%m-%d').date() if bill_date_str else date.today()
295+
296+
# 1. Create the Bill
297+
bill = Bill(
298+
bill_no=data.get('bill_no') or f"REC-{int(datetime.utcnow().timestamp())}",
299+
bill_date=bill_date,
300+
shop_name=data.get('shop_name') or 'Generic Vendor',
301+
total_amount=data.get('total_amount', 0),
302+
floor=floor,
303+
source='receipt_scan',
304+
original_filename=data.get('filename'),
305+
tenant_id=getattr(g, 'tenant_id', None)
306+
)
307+
db.session.add(bill)
308+
db.session.flush()
309+
310+
# 2. Create NEW ProcurementItems
311+
for item_data in data.get('new_items', []):
312+
item = ProcurementItem(
313+
item_name=item_data.get('name'),
314+
quantity=item_data.get('quantity'),
315+
category='other',
316+
priority='medium',
317+
status='completed',
318+
floor=floor,
319+
created_by_id=user.id,
320+
actual_cost=item_data.get('cost'),
321+
expense_recorded_at=datetime.utcnow(),
322+
bill_id=bill.id,
323+
tenant_id=getattr(g, 'tenant_id', None)
324+
)
325+
db.session.add(item)
326+
327+
# 3. Reconcile EXISTING ProcurementItems
328+
for rec in data.get('reconciliations', []):
329+
proc_id = rec.get('procurement_id')
330+
cost = float(rec.get('cost') or 0)
331+
332+
item = tenant_filter(ProcurementItem.query).filter_by(id=proc_id).first()
333+
if item and (user.role == 'admin' or item.floor == floor):
334+
item.actual_cost = cost
335+
item.bill_id = bill.id
336+
item.status = 'completed'
337+
item.expense_recorded_at = datetime.utcnow()
338+
339+
# 4. Final Total Sync
340+
db.session.flush() # Ensure all items have costs applied
341+
final_total = tenant_filter(db.session.query(func.sum(ProcurementItem.actual_cost))).filter(ProcurementItem.bill_id == bill.id).scalar() or 0
342+
bill.total_amount = final_total
343+
344+
db.session.commit()
345+
return jsonify({'success': True, 'bill_id': bill.id})
346+
except Exception as e:
347+
db.session.rollback()
348+
logging.error(f"Atomic Full Reconcile Error: {str(e)}")
349+
return jsonify({'error': str(e)}), 500
350+
211351
@finance_bp.route('/budgets/add', methods=['POST'])
212352
def add_budget():
213353
user = _require_user()

blueprints/pantry/routes.py

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from flask import render_template, request, redirect, url_for, session, flash, abort, jsonify, g
22
from app import db
3-
from models import User, Dish, Menu, Feedback, Request, ProcurementItem, Team, TeamMember, TeaTask, FloorLendBorrow, SpecialEvent, Announcement, Suggestion, SuggestionVote, Expense
3+
from models import User, Dish, Menu, Feedback, Request, ProcurementItem, Team, TeamMember, TeaTask, FloorLendBorrow, SpecialEvent, Announcement, Suggestion, SuggestionVote, Expense, Budget
44
from datetime import datetime, date, timedelta
55
from sqlalchemy import or_, func
66
from sqlalchemy.orm import joinedload
@@ -192,6 +192,88 @@ def dashboard():
192192
# Sort notifications by time
193193
notifications.sort(key=lambda x: x['time'], reverse=True)
194194

195+
# Morning Brief for Pantry Heads
196+
morning_brief = None
197+
if user.role == 'pantryHead':
198+
morning_brief = {
199+
'absent_staff_count': 0,
200+
'breakfast_rating': 0,
201+
'breakfast_feedback_count': 0,
202+
'budget_usage_pct': 0,
203+
'budget_remaining_days': 0,
204+
'pending_procurement_count': 0,
205+
'breakfast_dish_name': 'Breakfast'
206+
}
207+
208+
# 1. Staff Absent today
209+
morning_brief['absent_staff_count'] = tenant_filter(Request.query).filter(
210+
Request.floor == floor,
211+
Request.request_type == 'absence',
212+
Request.status == 'approved',
213+
Request.start_date <= today,
214+
Request.end_date >= today
215+
).count()
216+
217+
# 2. Breakfast Feedback (Most recent breakfast today or yesterday)
218+
recent_breakfast = tenant_filter(Menu.query).filter(
219+
Menu.floor == floor,
220+
Menu.meal_type == 'breakfast',
221+
Menu.date <= today
222+
).order_by(Menu.date.desc()).first()
223+
224+
if recent_breakfast:
225+
morning_brief['breakfast_dish_name'] = recent_breakfast.title
226+
feedback_stats = tenant_filter(db.session.query(
227+
func.avg(Feedback.rating),
228+
func.count(Feedback.id)
229+
)).filter(Feedback.menu_id == recent_breakfast.id).first()
230+
231+
if feedback_stats and feedback_stats[1] > 0:
232+
morning_brief['breakfast_rating'] = round(float(feedback_stats[0]), 1)
233+
morning_brief['breakfast_feedback_count'] = feedback_stats[1]
234+
235+
# 3. Budget Alert
236+
start_of_week = today - timedelta(days=today.weekday())
237+
end_of_week = start_of_week + timedelta(days=6)
238+
239+
# Get all budgets that overlap with the current week
240+
active_budgets = tenant_filter(Budget.query).filter(
241+
Budget.floor == floor,
242+
Budget.start_date <= end_of_week,
243+
or_(Budget.end_date >= start_of_week, Budget.end_date.is_(None))
244+
).all()
245+
246+
if active_budgets:
247+
total_allocated = sum(float(b.amount_allocated) for b in active_budgets)
248+
249+
# Spent this week: Legacy Expenses + Finalized Procurement
250+
spent_proc = tenant_filter(db.session.query(func.sum(ProcurementItem.actual_cost))).filter(
251+
ProcurementItem.floor == floor,
252+
ProcurementItem.status == 'completed',
253+
ProcurementItem.bill_id.isnot(None),
254+
ProcurementItem.expense_recorded_at >= datetime.combine(start_of_week, datetime.min.time())
255+
).scalar() or 0
256+
257+
spent_legacy = tenant_filter(db.session.query(func.sum(Expense.amount))).filter(
258+
Expense.floor == floor,
259+
Expense.date >= start_of_week
260+
).scalar() or 0
261+
262+
total_spent = float(spent_proc) + float(spent_legacy)
263+
264+
if total_allocated > 0:
265+
morning_brief['budget_usage_pct'] = min(100, round((total_spent / total_allocated) * 100))
266+
267+
# Days remaining in the current week or until the earliest end_date
268+
remaining_days = (end_of_week - today).days
269+
morning_brief['budget_remaining_days'] = max(0, remaining_days)
270+
271+
# 4. Procurement Pending
272+
morning_brief['pending_procurement_count'] = tenant_filter(ProcurementItem.query).filter(
273+
ProcurementItem.floor == floor,
274+
ProcurementItem.status != 'completed'
275+
).count()
276+
195277
return render_template(
196278
'dashboard.html',
197279
user=user,
@@ -206,6 +288,7 @@ def dashboard():
206288
today=today,
207289
upcoming_until=upcoming_until,
208290
current_user=user,
291+
morning_brief=morning_brief
209292
)
210293

211294
@pantry_bp.route('/home')

squeez.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
✦ To make AJS Pantry the definitive platform for pantry management, we need to shift the Pantry Head's (PH) role from Data Entry Clerk to
2+
Operations Director.
3+
4+
Here is a senior-level automation strategy focused on "squeezing" manual effort out of every module, keeping your existing models.py and
5+
ROUTE_MAP.md in mind.
6+
7+
---
8+
9+
10+
1. The "Menu Blueprint" System (Pantry Module)
11+
Current Effort: Manually creating 21 meals (Breakfast/Lunch/Dinner) every week.
12+
The "Squeeze": Implement Recursive Templates.
13+
* The Idea: Allow PHs to save a "Standard Week" as a template. With one click, they can "Project" that template onto any future week.
14+
* Intelligence: The system checks the Request table for absences during that future week and warns the PH: "Your assigned Menu Head (User X)
15+
is on leave on Tuesday; please re-assign."
16+
* Benefit: Reduces 30 minutes of manual clicking to 10 seconds of verification.
17+
18+
19+
20+
21+
22+
Implementation Priority Plan:
23+
1. High Impact / Low Effort: Global Absence Shield (Dropdown filtering).
24+
2. High Impact / Med Effort: Recursive Menu Templates (One-click weekly planning).
25+
3. Maximum Impact / High Effort: Atomic Bill Reconciliation (Linking OCR to Procurement).
26+
27+
28+
Which of these directions aligns most with your current needs? I can start detailing the logic for whichever you choose.

static/style.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,3 +1011,11 @@ body {
10111011
main {
10121012
flex: 1 0 auto;
10131013
}
1014+
1015+
/* Utility classes for soft backgrounds */
1016+
.bg-soft-primary { background-color: rgba(33, 150, 243, 0.1) !important; color: #2196F3 !important; }
1017+
.bg-soft-success { background-color: rgba(76, 175, 80, 0.1) !important; color: #4CAF50 !important; }
1018+
.bg-soft-warning { background-color: rgba(255, 152, 0, 0.1) !important; color: #FF9800 !important; }
1019+
.bg-soft-danger { background-color: rgba(244, 67, 54, 0.1) !important; color: #F44336 !important; }
1020+
.bg-soft-info { background-color: rgba(33, 150, 243, 0.1) !important; color: #2196F3 !important; }
1021+
.bg-soft-teal { background-color: rgba(38, 166, 154, 0.1) !important; color: #26A69A !important; }

0 commit comments

Comments
 (0)