feat: dodaj mjesecni izvjestaj servisera i troskova u kalendar widget
- Dva nova Django API endpointa (GET /api/fleet/reports/monthly-servicer/ i /api/fleet/reports/monthly-costs/) koji generiraju DOCX u A4 landscape formatu - TaskCalendarWidget prosiren s report panelom: dva gumba iznad kalendara (Izvjestaj servisera / Izvjestaj troskova) sirenjem drawer-a na 680 px - Preview tablice s podacima iz postojeceg tasks/workOrders store-a - downloadMonthlyServiserReport i downloadMonthlyCostsReport u store-u - TaskCalendarPortal prosljeduje activeWorkOrders prop widgetu Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,7 @@ from django.urls import path
|
||||
from .views import (
|
||||
CraneViewSet, VehicleViewSet, WorkOrderViewSet, VehicleServiceRecordViewSet,
|
||||
WorkOrderInvoiceViewSet, ServiceContextNoteViewSet, VehicleNotificationViewSet, VehicleServicePhotoViewSet, VehicleServiceAttachmentViewSet, pusher_auth,
|
||||
monthly_servicer_report_docx, monthly_costs_report_docx,
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
@@ -20,4 +21,6 @@ urlpatterns = router.urls
|
||||
|
||||
urlpatterns += [
|
||||
path('pusher-auth/', pusher_auth, name='pusher-auth'),
|
||||
path('reports/monthly-servicer/', monthly_servicer_report_docx, name='monthly-servicer-report'),
|
||||
path('reports/monthly-costs/', monthly_costs_report_docx, name='monthly-costs-report'),
|
||||
]
|
||||
@@ -2108,6 +2108,254 @@ def _dispatch_work_order_email_background(task_kwargs):
|
||||
return 'thread'
|
||||
|
||||
|
||||
_MONTH_NAMES_HR = [
|
||||
'Siječanj', 'Veljača', 'Ožujak', 'Travanj', 'Svibanj', 'Lipanj',
|
||||
'Srpanj', 'Kolovoz', 'Rujan', 'Listopad', 'Studeni', 'Prosinac',
|
||||
]
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([permissions.IsAuthenticated])
|
||||
def monthly_servicer_report_docx(request):
|
||||
"""Download monthly servicer report as DOCX (landscape table)."""
|
||||
from datetime import date as _dt_date
|
||||
try:
|
||||
year = int(request.query_params.get('year', _dt_date.today().year))
|
||||
month = int(request.query_params.get('month', _dt_date.today().month))
|
||||
if not (1 <= month <= 12):
|
||||
raise ValueError()
|
||||
except (ValueError, TypeError):
|
||||
return Response({'detail': 'Nevažeći year/month parametar.'}, status=400)
|
||||
|
||||
try:
|
||||
from modules.task_management.models import Task as _TaskModel
|
||||
except ImportError:
|
||||
return Response({'detail': 'Task model nije dostupan.'}, status=500)
|
||||
|
||||
tasks_qs = (
|
||||
_TaskModel.objects
|
||||
.filter(
|
||||
assigned_to=request.user,
|
||||
is_active=True,
|
||||
scheduled_date__year=year,
|
||||
scheduled_date__month=month,
|
||||
)
|
||||
.select_related('work_order', 'work_order__vehicle', 'work_order__vehicle__client')
|
||||
.order_by('scheduled_date', 'created_at')
|
||||
)
|
||||
|
||||
rows = []
|
||||
for task in tasks_qs:
|
||||
wo = task.work_order
|
||||
datum = task.scheduled_date.strftime('%d.%m.%Y') if task.scheduled_date else '-'
|
||||
opis_posla = task.title or '-'
|
||||
br_dizalice = '-'
|
||||
komitent = '-'
|
||||
mjesto_rada = '-'
|
||||
pocetak_rada = '-'
|
||||
kraj_rada = '-'
|
||||
redovan_rad = '-'
|
||||
prekovremeni = '0'
|
||||
radni_nalog = '-'
|
||||
|
||||
if wo:
|
||||
vehicle = getattr(wo, 'vehicle', None)
|
||||
if vehicle:
|
||||
sn = str(getattr(vehicle, 'crane_serial_number', '') or '').strip()
|
||||
br_dizalice = sn or '-'
|
||||
client_obj = getattr(vehicle, 'client', None)
|
||||
komitent = str(getattr(client_obj, 'name', '') or '').strip() or '-'
|
||||
mjesto_rada = wo.location or '-'
|
||||
ts = wo.travel_start_at
|
||||
te = wo.travel_end_at
|
||||
if ts:
|
||||
pocetak_rada = timezone.localtime(ts).strftime('%H:%M')
|
||||
if te:
|
||||
kraj_rada = timezone.localtime(te).strftime('%H:%M')
|
||||
if ts and te and te > ts:
|
||||
delta_h = (te - ts).total_seconds() / 3600.0
|
||||
reg = min(delta_h, 8.0)
|
||||
ovt = max(0.0, delta_h - 8.0)
|
||||
redovan_rad = f"{reg:.2f}".rstrip('0').rstrip('.')
|
||||
prekovremeni = f"{ovt:.2f}".rstrip('0').rstrip('.') if ovt > 0 else '0'
|
||||
radni_nalog = str(wo.display_code or '').strip() or '-'
|
||||
|
||||
rows.append([datum, opis_posla, br_dizalice, komitent, mjesto_rada,
|
||||
pocetak_rada, kraj_rada, redovan_rad, prekovremeni, radni_nalog])
|
||||
|
||||
from docx import Document as _DocxDoc
|
||||
from docx.shared import Pt, Cm
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.enum.section import WD_ORIENT
|
||||
|
||||
doc = _DocxDoc()
|
||||
for section in doc.sections:
|
||||
section.orientation = WD_ORIENT.LANDSCAPE
|
||||
section.page_width = Cm(29.7)
|
||||
section.page_height = Cm(21.0)
|
||||
section.left_margin = Cm(1.5)
|
||||
section.right_margin = Cm(1.5)
|
||||
section.top_margin = Cm(1.5)
|
||||
section.bottom_margin = Cm(1.5)
|
||||
|
||||
month_label = _MONTH_NAMES_HR[month - 1]
|
||||
servicer_name = _user_display_name(request.user) or getattr(request.user, 'username', str(request.user))
|
||||
|
||||
p_title = doc.add_paragraph()
|
||||
p_title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r_title = p_title.add_run('MJESEČNI IZVJEŠTAJ SERVISERA')
|
||||
r_title.bold = True
|
||||
r_title.font.size = Pt(14)
|
||||
|
||||
p_sub = doc.add_paragraph()
|
||||
p_sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r_sub = p_sub.add_run(f"{servicer_name} — {month_label} {year}")
|
||||
r_sub.font.size = Pt(11)
|
||||
|
||||
headers = ['DATUM', 'OPIS POSLA', 'BR. DIZALICE', 'KOMITENT', 'MJESTO RADA',
|
||||
'POČETAK RADA', 'KRAJ RADA', 'REDOVAN RAD (h)', 'PREKOVREMENI (h)', 'RADNI NALOG']
|
||||
col_widths = [Cm(2.2), Cm(5.5), Cm(2.5), Cm(3.5), Cm(3.5),
|
||||
Cm(2.2), Cm(2.2), Cm(2.4), Cm(2.4), Cm(2.4)]
|
||||
|
||||
table = doc.add_table(rows=1 + len(rows), cols=len(headers))
|
||||
table.style = 'Table Grid'
|
||||
|
||||
hdr_cells = table.rows[0].cells
|
||||
for i, (hdr, w) in enumerate(zip(headers, col_widths)):
|
||||
hdr_cells[i].width = w
|
||||
hdr_cells[i].text = hdr
|
||||
if hdr_cells[i].paragraphs[0].runs:
|
||||
run_h = hdr_cells[i].paragraphs[0].runs[0]
|
||||
run_h.bold = True
|
||||
run_h.font.size = Pt(8)
|
||||
|
||||
for ri, row_data in enumerate(rows):
|
||||
data_cells = table.rows[ri + 1].cells
|
||||
for ci, (val, w) in enumerate(zip(row_data, col_widths)):
|
||||
data_cells[ci].width = w
|
||||
data_cells[ci].text = str(val)
|
||||
if data_cells[ci].paragraphs[0].runs:
|
||||
data_cells[ci].paragraphs[0].runs[0].font.size = Pt(8)
|
||||
|
||||
buf = BytesIO()
|
||||
doc.save(buf)
|
||||
buf.seek(0)
|
||||
|
||||
month_key = f"{year}-{month:02d}"
|
||||
safe_name = re.sub(r'[^\w\-]', '_', servicer_name)
|
||||
fname = f"{safe_name}.izvjestaj-servisera.{month_key}.docx"
|
||||
resp = HttpResponse(
|
||||
buf.read(),
|
||||
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
)
|
||||
resp['Content-Disposition'] = f'attachment; filename="{fname}"'
|
||||
return resp
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([permissions.IsAuthenticated])
|
||||
def monthly_costs_report_docx(request):
|
||||
"""Download monthly servicer costs (invoices) report as DOCX."""
|
||||
from datetime import date as _dt_date
|
||||
try:
|
||||
year = int(request.query_params.get('year', _dt_date.today().year))
|
||||
month = int(request.query_params.get('month', _dt_date.today().month))
|
||||
if not (1 <= month <= 12):
|
||||
raise ValueError()
|
||||
except (ValueError, TypeError):
|
||||
return Response({'detail': 'Nevažeći year/month parametar.'}, status=400)
|
||||
|
||||
invoices_qs = (
|
||||
WorkOrderInvoice.objects
|
||||
.filter(
|
||||
work_order__work_order_tasks__assigned_to=request.user,
|
||||
work_order__work_order_tasks__is_active=True,
|
||||
datum__year=year,
|
||||
datum__month=month,
|
||||
)
|
||||
.select_related('work_order')
|
||||
.distinct()
|
||||
.order_by('datum', 'naziv_racuna')
|
||||
)
|
||||
|
||||
rows = []
|
||||
for inv in invoices_qs:
|
||||
wo = inv.work_order
|
||||
rows.append([
|
||||
inv.datum.strftime('%d.%m.%Y') if inv.datum else '-',
|
||||
inv.naziv_racuna or '-',
|
||||
inv.lokacija or '-',
|
||||
inv.opis or '-',
|
||||
str(getattr(wo, 'display_code', '') or '').strip() or '-',
|
||||
])
|
||||
|
||||
from docx import Document as _DocxDoc
|
||||
from docx.shared import Pt, Cm
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
from docx.enum.section import WD_ORIENT
|
||||
|
||||
doc = _DocxDoc()
|
||||
for section in doc.sections:
|
||||
section.orientation = WD_ORIENT.LANDSCAPE
|
||||
section.page_width = Cm(29.7)
|
||||
section.page_height = Cm(21.0)
|
||||
section.left_margin = Cm(1.5)
|
||||
section.right_margin = Cm(1.5)
|
||||
section.top_margin = Cm(1.5)
|
||||
section.bottom_margin = Cm(1.5)
|
||||
|
||||
month_label = _MONTH_NAMES_HR[month - 1]
|
||||
servicer_name = _user_display_name(request.user) or getattr(request.user, 'username', str(request.user))
|
||||
|
||||
p_title = doc.add_paragraph()
|
||||
p_title.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r_title = p_title.add_run('MJESEČNI IZVJEŠTAJ TROŠKOVA SERVISERA')
|
||||
r_title.bold = True
|
||||
r_title.font.size = Pt(14)
|
||||
|
||||
p_sub = doc.add_paragraph()
|
||||
p_sub.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
r_sub = p_sub.add_run(f"{servicer_name} — {month_label} {year}")
|
||||
r_sub.font.size = Pt(11)
|
||||
|
||||
headers = ['DATUM', 'NAZIV RAČUNA', 'LOKACIJA', 'OPIS', 'RADNI NALOG']
|
||||
col_widths = [Cm(2.5), Cm(6.0), Cm(4.0), Cm(8.0), Cm(3.0)]
|
||||
|
||||
table = doc.add_table(rows=1 + len(rows), cols=len(headers))
|
||||
table.style = 'Table Grid'
|
||||
|
||||
hdr_cells = table.rows[0].cells
|
||||
for i, (hdr, w) in enumerate(zip(headers, col_widths)):
|
||||
hdr_cells[i].width = w
|
||||
hdr_cells[i].text = hdr
|
||||
if hdr_cells[i].paragraphs[0].runs:
|
||||
run_h = hdr_cells[i].paragraphs[0].runs[0]
|
||||
run_h.bold = True
|
||||
run_h.font.size = Pt(9)
|
||||
|
||||
for ri, row_data in enumerate(rows):
|
||||
data_cells = table.rows[ri + 1].cells
|
||||
for ci, (val, w) in enumerate(zip(row_data, col_widths)):
|
||||
data_cells[ci].width = w
|
||||
data_cells[ci].text = str(val)
|
||||
if data_cells[ci].paragraphs[0].runs:
|
||||
data_cells[ci].paragraphs[0].runs[0].font.size = Pt(9)
|
||||
|
||||
buf = BytesIO()
|
||||
doc.save(buf)
|
||||
buf.seek(0)
|
||||
|
||||
month_key = f"{year}-{month:02d}"
|
||||
safe_name = re.sub(r'[^\w\-]', '_', servicer_name)
|
||||
fname = f"{safe_name}.troskovi-servisera.{month_key}.docx"
|
||||
resp = HttpResponse(
|
||||
buf.read(),
|
||||
content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
)
|
||||
resp['Content-Disposition'] = f'attachment; filename="{fname}"'
|
||||
return resp
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([permissions.IsAuthenticated])
|
||||
def pusher_auth(request):
|
||||
|
||||
Reference in New Issue
Block a user