fix: dohvat mjesecnih racuna za troskove
Uklanja ovisnost o workOrders.invoices i dohvaća račune direktno kroz API filtriran po mjesecu. Dodaje i display code putnog naloga u serializer kako bi prikaz u izvještaju ostao potpun. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -379,12 +379,14 @@ class WorkOrderSerializer(serializers.ModelSerializer):
|
|||||||
class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
|
class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
|
||||||
image_url = serializers.SerializerMethodField()
|
image_url = serializers.SerializerMethodField()
|
||||||
image_content_type = serializers.SerializerMethodField()
|
image_content_type = serializers.SerializerMethodField()
|
||||||
|
work_order_display_code = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = WorkOrderInvoice
|
model = WorkOrderInvoice
|
||||||
fields = [
|
fields = [
|
||||||
'id',
|
'id',
|
||||||
'work_order',
|
'work_order',
|
||||||
|
'work_order_display_code',
|
||||||
'naziv_racuna',
|
'naziv_racuna',
|
||||||
'lokacija',
|
'lokacija',
|
||||||
'datum',
|
'datum',
|
||||||
@@ -427,6 +429,15 @@ class WorkOrderInvoiceSerializer(serializers.ModelSerializer):
|
|||||||
return 'image/gif'
|
return 'image/gif'
|
||||||
return 'application/octet-stream'
|
return 'application/octet-stream'
|
||||||
|
|
||||||
|
def get_work_order_display_code(self, obj):
|
||||||
|
work_order = getattr(obj, 'work_order', None)
|
||||||
|
if work_order is None:
|
||||||
|
return None
|
||||||
|
display_code = str(getattr(work_order, 'display_code', '') or '').strip()
|
||||||
|
if display_code:
|
||||||
|
return display_code
|
||||||
|
return str(getattr(work_order, 'id', '') or '').strip() or None
|
||||||
|
|
||||||
def validate_image(self, value):
|
def validate_image(self, value):
|
||||||
if not value:
|
if not value:
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -3126,16 +3126,30 @@ class WorkOrderInvoiceViewSet(viewsets.ModelViewSet):
|
|||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
work_order_id = self.request.query_params.get('work_order_id')
|
work_order_id = self.request.query_params.get('work_order_id')
|
||||||
|
year = self.request.query_params.get('year')
|
||||||
|
month = self.request.query_params.get('month')
|
||||||
qs = WorkOrderInvoice.objects.select_related('work_order', 'work_order__vehicle', 'created_by').filter(is_active=True)
|
qs = WorkOrderInvoice.objects.select_related('work_order', 'work_order__vehicle', 'created_by').filter(is_active=True)
|
||||||
|
if year and month:
|
||||||
|
try:
|
||||||
|
year_value = int(year)
|
||||||
|
month_value = int(month)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
year_value = None
|
||||||
|
month_value = None
|
||||||
|
if year_value and month_value and 1 <= month_value <= 12:
|
||||||
|
qs = qs.filter(
|
||||||
|
work_order__work_order_tasks__scheduled_date__year=year_value,
|
||||||
|
work_order__work_order_tasks__scheduled_date__month=month_value,
|
||||||
|
)
|
||||||
if user.is_staff:
|
if user.is_staff:
|
||||||
if work_order_id:
|
if work_order_id:
|
||||||
qs = qs.filter(work_order_id=work_order_id)
|
qs = qs.filter(work_order_id=work_order_id)
|
||||||
return qs
|
return qs.distinct()
|
||||||
allowed_orders = _work_orders_queryset_for_user(user).values('id')
|
allowed_orders = _work_orders_queryset_for_user(user).values('id')
|
||||||
qs = qs.filter(work_order_id__in=allowed_orders)
|
qs = qs.filter(work_order_id__in=allowed_orders)
|
||||||
if work_order_id:
|
if work_order_id:
|
||||||
qs = qs.filter(work_order_id=work_order_id)
|
qs = qs.filter(work_order_id=work_order_id)
|
||||||
return qs
|
return qs.distinct()
|
||||||
|
|
||||||
def get_serializer_context(self):
|
def get_serializer_context(self):
|
||||||
context = super().get_serializer_context()
|
context = super().get_serializer_context()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getStatusLabel } from '../../stores/taskStore';
|
|||||||
import {
|
import {
|
||||||
downloadMonthlyCostsReport,
|
downloadMonthlyCostsReport,
|
||||||
downloadMonthlyServiserReport,
|
downloadMonthlyServiserReport,
|
||||||
|
fetchMonthlyCostInvoices,
|
||||||
fetchMonthlyServicerEntries,
|
fetchMonthlyServicerEntries,
|
||||||
upsertMonthlyServicerEntry,
|
upsertMonthlyServicerEntry,
|
||||||
} from '../../stores/fleetDashboardStore';
|
} from '../../stores/fleetDashboardStore';
|
||||||
@@ -65,7 +66,9 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders
|
|||||||
const [reportType, setReportType] = useState(null);
|
const [reportType, setReportType] = useState(null);
|
||||||
const [downloading, setDownloading] = useState(false);
|
const [downloading, setDownloading] = useState(false);
|
||||||
const [manualEntries, setManualEntries] = useState([]);
|
const [manualEntries, setManualEntries] = useState([]);
|
||||||
|
const [costInvoices, setCostInvoices] = useState([]);
|
||||||
const [loadingManualEntries, setLoadingManualEntries] = useState(false);
|
const [loadingManualEntries, setLoadingManualEntries] = useState(false);
|
||||||
|
const [loadingCostInvoices, setLoadingCostInvoices] = useState(false);
|
||||||
const [manualEntryTarget, setManualEntryTarget] = useState(null);
|
const [manualEntryTarget, setManualEntryTarget] = useState(null);
|
||||||
const [savingManualEntry, setSavingManualEntry] = useState(false);
|
const [savingManualEntry, setSavingManualEntry] = useState(false);
|
||||||
|
|
||||||
@@ -148,6 +151,28 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders
|
|||||||
};
|
};
|
||||||
}, [reportOpen, reportType, viewYear, viewMonth]);
|
}, [reportOpen, reportType, viewYear, viewMonth]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!reportOpen || reportType !== 'costs') return undefined;
|
||||||
|
let active = true;
|
||||||
|
setLoadingCostInvoices(true);
|
||||||
|
fetchMonthlyCostInvoices(viewYear, viewMonth + 1)
|
||||||
|
.then((rows) => {
|
||||||
|
if (!active) return;
|
||||||
|
setCostInvoices(Array.isArray(rows) ? rows : []);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (!active) return;
|
||||||
|
setCostInvoices([]);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!active) return;
|
||||||
|
setLoadingCostInvoices(false);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
active = false;
|
||||||
|
};
|
||||||
|
}, [reportOpen, reportType, viewYear, viewMonth]);
|
||||||
|
|
||||||
const servicerRows = useMemo(() => {
|
const servicerRows = useMemo(() => {
|
||||||
if (reportType !== 'servicer') return [];
|
if (reportType !== 'servicer') return [];
|
||||||
const rows = [];
|
const rows = [];
|
||||||
@@ -248,28 +273,14 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders
|
|||||||
|
|
||||||
const costsRows = useMemo(() => {
|
const costsRows = useMemo(() => {
|
||||||
if (reportType !== 'costs') return [];
|
if (reportType !== 'costs') return [];
|
||||||
const monthPrefix = `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}`;
|
return costInvoices.map((invoice) => ([
|
||||||
const workOrderIds = new Set(
|
invoice?.datum ? new Date(invoice.datum).toLocaleDateString('hr-HR') : '-',
|
||||||
tasks
|
invoice?.naziv_racuna || '-',
|
||||||
.filter((task) => {
|
invoice?.lokacija || '-',
|
||||||
const key = toDateKey(task.scheduled_date);
|
invoice?.opis || '-',
|
||||||
return Boolean(key && key.startsWith(monthPrefix) && task.work_order);
|
invoice?.work_order_display_code || '-',
|
||||||
})
|
]));
|
||||||
.map((task) => String(task.work_order))
|
}, [costInvoices, reportType]);
|
||||||
);
|
|
||||||
|
|
||||||
return workOrders
|
|
||||||
.filter((workOrder) => workOrderIds.has(String(workOrder.id)) && Array.isArray(workOrder.invoices))
|
|
||||||
.flatMap((workOrder) => (
|
|
||||||
workOrder.invoices.map((invoice) => ([
|
|
||||||
invoice?.datum ? new Date(invoice.datum).toLocaleDateString('hr-HR') : '-',
|
|
||||||
invoice?.naziv_racuna || '-',
|
|
||||||
invoice?.lokacija || '-',
|
|
||||||
invoice?.opis || '-',
|
|
||||||
workOrder.display_code || '-',
|
|
||||||
]))
|
|
||||||
));
|
|
||||||
}, [reportType, tasks, viewYear, viewMonth, workOrders]);
|
|
||||||
|
|
||||||
const selectedDayKey = selectedDay
|
const selectedDayKey = selectedDay
|
||||||
? `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(selectedDay).padStart(2, '0')}`
|
? `${viewYear}-${String(viewMonth + 1).padStart(2, '0')}-${String(selectedDay).padStart(2, '0')}`
|
||||||
@@ -628,7 +639,9 @@ export default function TaskCalendarWidget({ tasks = [], notes = [], workOrders
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{reportType === 'costs' && (
|
{reportType === 'costs' && (
|
||||||
costsRows.length === 0 ? (
|
loadingCostInvoices ? (
|
||||||
|
<p className="px-4 py-8 text-center text-sm text-text-muted">Ucitam troskove...</p>
|
||||||
|
) : costsRows.length === 0 ? (
|
||||||
<p className="px-4 py-8 text-center text-sm text-text-muted">
|
<p className="px-4 py-8 text-center text-sm text-text-muted">
|
||||||
Nema troskova za {MONTH_NAMES[viewMonth]} {viewYear}.
|
Nema troskova za {MONTH_NAMES[viewMonth]} {viewYear}.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -920,6 +920,13 @@ export async function fetchMonthlyServicerEntries(year, month) {
|
|||||||
return Array.isArray(payload) ? payload : (payload?.results ?? []);
|
return Array.isArray(payload) ? payload : (payload?.results ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchMonthlyCostInvoices(year, month) {
|
||||||
|
const payload = await api.get(
|
||||||
|
`fleet/work-order-invoices/?year=${year}&month=${month}`
|
||||||
|
);
|
||||||
|
return Array.isArray(payload) ? payload : (payload?.results ?? []);
|
||||||
|
}
|
||||||
|
|
||||||
export async function upsertMonthlyServicerEntry(data = {}) {
|
export async function upsertMonthlyServicerEntry(data = {}) {
|
||||||
if (!data.entry_date) {
|
if (!data.entry_date) {
|
||||||
throw new Error('Datum dnevnog unosa je obavezan.');
|
throw new Error('Datum dnevnog unosa je obavezan.');
|
||||||
|
|||||||
Reference in New Issue
Block a user