feat: dodaj mjesecni izvjestaj servisera i troskova u kalendar widget
Some checks failed
ERP CI/CD Pipeline / test (push) Has been cancelled
ERP CI/CD Pipeline / Deploy (server git pull + compose) (push) Has been cancelled

- 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:
mariomitte
2026-07-31 13:31:58 +02:00
parent 8b82481bb4
commit fe739b14c0
5 changed files with 672 additions and 174 deletions

View File

@@ -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'),
]

View File

@@ -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):