|
@@ -58,7 +58,7 @@ class WorkRecordService:
|
|
|
return work_record.to_dict(), None
|
|
return work_record.to_dict(), None
|
|
|
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
|
- def get_all(person_id=None, start_date=None, end_date=None, year=None, month=None):
|
|
|
|
|
|
|
+ def get_all(person_id=None, start_date=None, end_date=None, year=None, month=None, is_settled=None):
|
|
|
"""Get all work records with optional filters.
|
|
"""Get all work records with optional filters.
|
|
|
|
|
|
|
|
All filters are applied as intersection (AND logic).
|
|
All filters are applied as intersection (AND logic).
|
|
@@ -69,6 +69,7 @@ class WorkRecordService:
|
|
|
end_date: Filter by end date (optional)
|
|
end_date: Filter by end date (optional)
|
|
|
year: Filter by year (optional, used with month)
|
|
year: Filter by year (optional, used with month)
|
|
|
month: Filter by month 1-12 (optional, used with year)
|
|
month: Filter by month 1-12 (optional, used with year)
|
|
|
|
|
+ is_settled: Filter by settlement status (optional, True/False)
|
|
|
|
|
|
|
|
Returns:
|
|
Returns:
|
|
|
List of work record dictionaries
|
|
List of work record dictionaries
|
|
@@ -78,6 +79,10 @@ class WorkRecordService:
|
|
|
if person_id is not None:
|
|
if person_id is not None:
|
|
|
query = query.filter(WorkRecord.person_id == person_id)
|
|
query = query.filter(WorkRecord.person_id == person_id)
|
|
|
|
|
|
|
|
|
|
+ # Apply settlement status filter
|
|
|
|
|
+ if is_settled is not None:
|
|
|
|
|
+ query = query.filter(WorkRecord.is_settled == is_settled)
|
|
|
|
|
+
|
|
|
# Apply month filter if both year and month are provided
|
|
# Apply month filter if both year and month are provided
|
|
|
if year is not None and month is not None:
|
|
if year is not None and month is not None:
|
|
|
month_start = date(year, month, 1)
|
|
month_start = date(year, month, 1)
|
|
@@ -199,7 +204,7 @@ class WorkRecordService:
|
|
|
person_id: Filter by person ID (optional)
|
|
person_id: Filter by person ID (optional)
|
|
|
|
|
|
|
|
Returns:
|
|
Returns:
|
|
|
- Dictionary with daily summary data
|
|
|
|
|
|
|
+ Dictionary with daily summary data grouped by person and supplier
|
|
|
"""
|
|
"""
|
|
|
if isinstance(work_date, str):
|
|
if isinstance(work_date, str):
|
|
|
work_date = datetime.fromisoformat(work_date).date()
|
|
work_date = datetime.fromisoformat(work_date).date()
|
|
@@ -211,33 +216,44 @@ class WorkRecordService:
|
|
|
|
|
|
|
|
work_records = query.all()
|
|
work_records = query.all()
|
|
|
|
|
|
|
|
- # Group by person
|
|
|
|
|
- summary_by_person = {}
|
|
|
|
|
|
|
+ # Group by person and supplier
|
|
|
|
|
+ summary_by_person_supplier = {}
|
|
|
for wr in work_records:
|
|
for wr in work_records:
|
|
|
person_name = wr.person.name
|
|
person_name = wr.person.name
|
|
|
- if person_name not in summary_by_person:
|
|
|
|
|
- summary_by_person[person_name] = {
|
|
|
|
|
|
|
+ supplier_name = wr.item.supplier.name if wr.item.supplier else ''
|
|
|
|
|
+ key = (wr.person_id, person_name, supplier_name)
|
|
|
|
|
+
|
|
|
|
|
+ if key not in summary_by_person_supplier:
|
|
|
|
|
+ summary_by_person_supplier[key] = {
|
|
|
'person_id': wr.person_id,
|
|
'person_id': wr.person_id,
|
|
|
'person_name': person_name,
|
|
'person_name': person_name,
|
|
|
|
|
+ 'supplier_name': supplier_name,
|
|
|
'total_items': 0,
|
|
'total_items': 0,
|
|
|
'total_value': 0.0,
|
|
'total_value': 0.0,
|
|
|
'items': []
|
|
'items': []
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- summary_by_person[person_name]['total_items'] += wr.quantity
|
|
|
|
|
- summary_by_person[person_name]['total_value'] += wr.total_price
|
|
|
|
|
- summary_by_person[person_name]['items'].append({
|
|
|
|
|
|
|
+ summary_by_person_supplier[key]['total_items'] += wr.quantity
|
|
|
|
|
+ summary_by_person_supplier[key]['total_value'] += wr.total_price
|
|
|
|
|
+ summary_by_person_supplier[key]['items'].append({
|
|
|
'item_name': wr.item.name,
|
|
'item_name': wr.item.name,
|
|
|
|
|
+ 'supplier_name': supplier_name,
|
|
|
'unit_price': wr.item.unit_price,
|
|
'unit_price': wr.item.unit_price,
|
|
|
'quantity': wr.quantity,
|
|
'quantity': wr.quantity,
|
|
|
'total_price': wr.total_price
|
|
'total_price': wr.total_price
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ # Sort by person_name, then supplier_name
|
|
|
|
|
+ summary_list = sorted(
|
|
|
|
|
+ summary_by_person_supplier.values(),
|
|
|
|
|
+ key=lambda x: (x['person_name'], x['supplier_name'])
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
'date': work_date.isoformat(),
|
|
'date': work_date.isoformat(),
|
|
|
- 'summary': list(summary_by_person.values()),
|
|
|
|
|
- 'grand_total_items': sum(p['total_items'] for p in summary_by_person.values()),
|
|
|
|
|
- 'grand_total_value': sum(p['total_value'] for p in summary_by_person.values())
|
|
|
|
|
|
|
+ 'summary': summary_list,
|
|
|
|
|
+ 'grand_total_items': sum(p['total_items'] for p in summary_list),
|
|
|
|
|
+ 'grand_total_value': sum(p['total_value'] for p in summary_list)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
@@ -252,9 +268,12 @@ class WorkRecordService:
|
|
|
Returns:
|
|
Returns:
|
|
|
Dictionary with yearly summary data including:
|
|
Dictionary with yearly summary data including:
|
|
|
- year: The year
|
|
- year: The year
|
|
|
- - persons: List of person summaries with monthly_earnings and yearly_total
|
|
|
|
|
|
|
+ - persons: List of person summaries with monthly_earnings, yearly_total,
|
|
|
|
|
+ settled_total, and unsettled_total
|
|
|
- monthly_totals: Array of 12 monthly totals across all persons
|
|
- monthly_totals: Array of 12 monthly totals across all persons
|
|
|
- grand_total: Overall yearly total
|
|
- grand_total: Overall yearly total
|
|
|
|
|
+ - settled_grand_total: Total settled earnings for the year
|
|
|
|
|
+ - unsettled_grand_total: Total unsettled earnings for the year
|
|
|
"""
|
|
"""
|
|
|
# Calculate date range for the year
|
|
# Calculate date range for the year
|
|
|
start_date = date(year, 1, 1)
|
|
start_date = date(year, 1, 1)
|
|
@@ -267,6 +286,7 @@ class WorkRecordService:
|
|
|
).join(Person).order_by(Person.name).all()
|
|
).join(Person).order_by(Person.name).all()
|
|
|
|
|
|
|
|
# Calculate totals by person and month (same logic as ExportService)
|
|
# Calculate totals by person and month (same logic as ExportService)
|
|
|
|
|
+ # Also track settlement status per person
|
|
|
person_monthly_totals = {}
|
|
person_monthly_totals = {}
|
|
|
for record in work_records:
|
|
for record in work_records:
|
|
|
person_id = record.person_id
|
|
person_id = record.person_id
|
|
@@ -277,14 +297,24 @@ class WorkRecordService:
|
|
|
person_monthly_totals[person_id] = {
|
|
person_monthly_totals[person_id] = {
|
|
|
'person_id': person_id,
|
|
'person_id': person_id,
|
|
|
'person_name': person_name,
|
|
'person_name': person_name,
|
|
|
- 'monthly_data': {m: 0.0 for m in range(1, 13)}
|
|
|
|
|
|
|
+ 'monthly_data': {m: 0.0 for m in range(1, 13)},
|
|
|
|
|
+ 'settled_total': 0.0,
|
|
|
|
|
+ 'unsettled_total': 0.0
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
person_monthly_totals[person_id]['monthly_data'][month] += record.total_price
|
|
person_monthly_totals[person_id]['monthly_data'][month] += record.total_price
|
|
|
|
|
+
|
|
|
|
|
+ # Track settlement status
|
|
|
|
|
+ if record.is_settled:
|
|
|
|
|
+ person_monthly_totals[person_id]['settled_total'] += record.total_price
|
|
|
|
|
+ else:
|
|
|
|
|
+ person_monthly_totals[person_id]['unsettled_total'] += record.total_price
|
|
|
|
|
|
|
|
# Build persons list sorted alphabetically by name
|
|
# Build persons list sorted alphabetically by name
|
|
|
persons = []
|
|
persons = []
|
|
|
monthly_grand_totals = [0.0] * 12
|
|
monthly_grand_totals = [0.0] * 12
|
|
|
|
|
+ settled_grand_total = 0.0
|
|
|
|
|
+ unsettled_grand_total = 0.0
|
|
|
|
|
|
|
|
for person_data in sorted(person_monthly_totals.values(), key=lambda x: x['person_name']):
|
|
for person_data in sorted(person_monthly_totals.values(), key=lambda x: x['person_name']):
|
|
|
monthly_earnings = []
|
|
monthly_earnings = []
|
|
@@ -296,12 +326,21 @@ class WorkRecordService:
|
|
|
yearly_total += value
|
|
yearly_total += value
|
|
|
monthly_grand_totals[month - 1] += value
|
|
monthly_grand_totals[month - 1] += value
|
|
|
|
|
|
|
|
|
|
+ settled_total = round(person_data['settled_total'], 2)
|
|
|
|
|
+ unsettled_total = round(person_data['unsettled_total'], 2)
|
|
|
|
|
+
|
|
|
persons.append({
|
|
persons.append({
|
|
|
'person_id': person_data['person_id'],
|
|
'person_id': person_data['person_id'],
|
|
|
'person_name': person_data['person_name'],
|
|
'person_name': person_data['person_name'],
|
|
|
'monthly_earnings': monthly_earnings,
|
|
'monthly_earnings': monthly_earnings,
|
|
|
- 'yearly_total': round(yearly_total, 2)
|
|
|
|
|
|
|
+ 'yearly_total': round(yearly_total, 2),
|
|
|
|
|
+ 'settled_total': settled_total,
|
|
|
|
|
+ 'unsettled_total': unsettled_total
|
|
|
})
|
|
})
|
|
|
|
|
+
|
|
|
|
|
+ # Accumulate grand totals for settlement
|
|
|
|
|
+ settled_grand_total += settled_total
|
|
|
|
|
+ unsettled_grand_total += unsettled_total
|
|
|
|
|
|
|
|
# Round monthly totals
|
|
# Round monthly totals
|
|
|
monthly_totals = [round(total, 2) for total in monthly_grand_totals]
|
|
monthly_totals = [round(total, 2) for total in monthly_grand_totals]
|
|
@@ -311,7 +350,9 @@ class WorkRecordService:
|
|
|
'year': year,
|
|
'year': year,
|
|
|
'persons': persons,
|
|
'persons': persons,
|
|
|
'monthly_totals': monthly_totals,
|
|
'monthly_totals': monthly_totals,
|
|
|
- 'grand_total': grand_total
|
|
|
|
|
|
|
+ 'grand_total': grand_total,
|
|
|
|
|
+ 'settled_grand_total': round(settled_grand_total, 2),
|
|
|
|
|
+ 'unsettled_grand_total': round(unsettled_grand_total, 2)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
@staticmethod
|
|
@staticmethod
|
|
@@ -326,8 +367,11 @@ class WorkRecordService:
|
|
|
Dictionary with monthly summary data including:
|
|
Dictionary with monthly summary data including:
|
|
|
- total_records: Total number of work records
|
|
- total_records: Total number of work records
|
|
|
- total_earnings: Total earnings for the month
|
|
- total_earnings: Total earnings for the month
|
|
|
|
|
+ - settled_earnings: Total settled earnings for the month
|
|
|
|
|
+ - unsettled_earnings: Total unsettled earnings for the month
|
|
|
- top_performers: List of persons ranked by earnings (descending)
|
|
- top_performers: List of persons ranked by earnings (descending)
|
|
|
- - item_breakdown: List of items with quantity and earnings
|
|
|
|
|
|
|
+ - item_breakdown: List of items with quantity, earnings, and supplier_name
|
|
|
|
|
+ - supplier_breakdown: List of person-by-supplier earnings with is_settled field
|
|
|
"""
|
|
"""
|
|
|
# Calculate month date range
|
|
# Calculate month date range
|
|
|
month_start = date(year, month, 1)
|
|
month_start = date(year, month, 1)
|
|
@@ -342,9 +386,11 @@ class WorkRecordService:
|
|
|
WorkRecord.work_date < month_end
|
|
WorkRecord.work_date < month_end
|
|
|
).all()
|
|
).all()
|
|
|
|
|
|
|
|
- # Calculate totals
|
|
|
|
|
|
|
+ # Calculate totals including settlement status
|
|
|
total_records = len(work_records)
|
|
total_records = len(work_records)
|
|
|
total_earnings = sum(wr.total_price for wr in work_records)
|
|
total_earnings = sum(wr.total_price for wr in work_records)
|
|
|
|
|
+ settled_earnings = sum(wr.total_price for wr in work_records if wr.is_settled)
|
|
|
|
|
+ unsettled_earnings = sum(wr.total_price for wr in work_records if not wr.is_settled)
|
|
|
|
|
|
|
|
# Group by person for top performers
|
|
# Group by person for top performers
|
|
|
person_earnings = {}
|
|
person_earnings = {}
|
|
@@ -365,14 +411,16 @@ class WorkRecordService:
|
|
|
reverse=True
|
|
reverse=True
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
- # Group by item for breakdown
|
|
|
|
|
|
|
+ # Group by item for breakdown (with supplier_name)
|
|
|
item_breakdown_dict = {}
|
|
item_breakdown_dict = {}
|
|
|
for wr in work_records:
|
|
for wr in work_records:
|
|
|
item_id = wr.item_id
|
|
item_id = wr.item_id
|
|
|
if item_id not in item_breakdown_dict:
|
|
if item_id not in item_breakdown_dict:
|
|
|
|
|
+ supplier_name = wr.item.supplier.name if wr.item.supplier else ''
|
|
|
item_breakdown_dict[item_id] = {
|
|
item_breakdown_dict[item_id] = {
|
|
|
'item_id': item_id,
|
|
'item_id': item_id,
|
|
|
'item_name': wr.item.name,
|
|
'item_name': wr.item.name,
|
|
|
|
|
+ 'supplier_name': supplier_name,
|
|
|
'quantity': 0,
|
|
'quantity': 0,
|
|
|
'earnings': 0.0
|
|
'earnings': 0.0
|
|
|
}
|
|
}
|
|
@@ -386,11 +434,118 @@ class WorkRecordService:
|
|
|
reverse=True
|
|
reverse=True
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
+ # Group by person, supplier, and settlement status for supplier_breakdown
|
|
|
|
|
+ person_supplier_earnings = {}
|
|
|
|
|
+ for wr in work_records:
|
|
|
|
|
+ person_id = wr.person_id
|
|
|
|
|
+ supplier_name = wr.item.supplier.name if wr.item.supplier else ''
|
|
|
|
|
+ is_settled = wr.is_settled
|
|
|
|
|
+ key = (person_id, supplier_name, is_settled)
|
|
|
|
|
+
|
|
|
|
|
+ if key not in person_supplier_earnings:
|
|
|
|
|
+ person_supplier_earnings[key] = {
|
|
|
|
|
+ 'person_id': person_id,
|
|
|
|
|
+ 'person_name': wr.person.name,
|
|
|
|
|
+ 'supplier_name': supplier_name,
|
|
|
|
|
+ 'earnings': 0.0,
|
|
|
|
|
+ 'is_settled': is_settled
|
|
|
|
|
+ }
|
|
|
|
|
+ person_supplier_earnings[key]['earnings'] += wr.total_price
|
|
|
|
|
+
|
|
|
|
|
+ # Sort by person_name, then supplier_name, then is_settled (settled first)
|
|
|
|
|
+ supplier_breakdown = sorted(
|
|
|
|
|
+ person_supplier_earnings.values(),
|
|
|
|
|
+ key=lambda x: (x['person_name'], x['supplier_name'], not x['is_settled'])
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
return {
|
|
return {
|
|
|
'year': year,
|
|
'year': year,
|
|
|
'month': month,
|
|
'month': month,
|
|
|
'total_records': total_records,
|
|
'total_records': total_records,
|
|
|
'total_earnings': total_earnings,
|
|
'total_earnings': total_earnings,
|
|
|
|
|
+ 'settled_earnings': settled_earnings,
|
|
|
|
|
+ 'unsettled_earnings': unsettled_earnings,
|
|
|
'top_performers': top_performers,
|
|
'top_performers': top_performers,
|
|
|
- 'item_breakdown': item_breakdown
|
|
|
|
|
|
|
+ 'item_breakdown': item_breakdown,
|
|
|
|
|
+ 'supplier_breakdown': supplier_breakdown
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def toggle_settlement(work_record_id):
|
|
|
|
|
+ """Toggle the settlement status of a work record.
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ work_record_id: Work record's ID
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ Tuple of (work_record_dict, error_message)
|
|
|
|
|
+ On success: (work_record_dict, None)
|
|
|
|
|
+ On failure: (None, error_message)
|
|
|
|
|
+ """
|
|
|
|
|
+ work_record = db.session.get(WorkRecord, work_record_id)
|
|
|
|
|
+ if not work_record:
|
|
|
|
|
+ return None, f"未找到ID为 {work_record_id} 的工作记录"
|
|
|
|
|
+
|
|
|
|
|
+ work_record.is_settled = not work_record.is_settled
|
|
|
|
|
+ db.session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ return work_record.to_dict(), None
|
|
|
|
|
+
|
|
|
|
|
+ @staticmethod
|
|
|
|
|
+ def batch_update_settlement(year, month, is_settled, person_id=None, supplier_id=None):
|
|
|
|
|
+ """Batch update settlement status for work records matching criteria.
|
|
|
|
|
+
|
|
|
|
|
+ All filters are applied as intersection (AND logic).
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ year: Year to filter by (required)
|
|
|
|
|
+ month: Month to filter by 1-12 (required)
|
|
|
|
|
+ is_settled: New settlement status (required)
|
|
|
|
|
+ person_id: Filter by person ID (optional)
|
|
|
|
|
+ supplier_id: Filter by supplier ID (optional), use "none" to filter items without supplier
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ Tuple of (result_dict, error_message)
|
|
|
|
|
+ On success: (result_dict with updated_count, None)
|
|
|
|
|
+ On failure: (None, error_message)
|
|
|
|
|
+ """
|
|
|
|
|
+ # Calculate month date range
|
|
|
|
|
+ month_start = date(year, month, 1)
|
|
|
|
|
+ if month == 12:
|
|
|
|
|
+ month_end = date(year + 1, 1, 1)
|
|
|
|
|
+ else:
|
|
|
|
|
+ month_end = date(year, month + 1, 1)
|
|
|
|
|
+
|
|
|
|
|
+ # Build query
|
|
|
|
|
+ query = WorkRecord.query.filter(
|
|
|
|
|
+ WorkRecord.work_date >= month_start,
|
|
|
|
|
+ WorkRecord.work_date < month_end
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ if person_id is not None:
|
|
|
|
|
+ query = query.filter(WorkRecord.person_id == person_id)
|
|
|
|
|
+
|
|
|
|
|
+ if supplier_id is not None:
|
|
|
|
|
+ # Join with Item to filter by supplier_id
|
|
|
|
|
+ query = query.join(Item)
|
|
|
|
|
+ if supplier_id == 'none':
|
|
|
|
|
+ # Filter items without supplier
|
|
|
|
|
+ query = query.filter(Item.supplier_id.is_(None))
|
|
|
|
|
+ else:
|
|
|
|
|
+ query = query.filter(Item.supplier_id == supplier_id)
|
|
|
|
|
+
|
|
|
|
|
+ # Get matching records and update
|
|
|
|
|
+ work_records = query.all()
|
|
|
|
|
+ updated_count = 0
|
|
|
|
|
+
|
|
|
|
|
+ for wr in work_records:
|
|
|
|
|
+ if wr.is_settled != is_settled:
|
|
|
|
|
+ wr.is_settled = is_settled
|
|
|
|
|
+ updated_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ db.session.commit()
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ 'updated_count': updated_count,
|
|
|
|
|
+ 'total_matched': len(work_records)
|
|
|
|
|
+ }, None
|