Compare commits
No commits in common. "main" and "v2.3.16" have entirely different histories.
@ -9,7 +9,7 @@ from typing import List, Dict, Optional
|
|||||||
from datetime import datetime, date, timedelta
|
from datetime import datetime, date, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from app.core.database import execute_query, execute_insert, execute_update, execute_query_single, table_has_column
|
from app.core.database import execute_query, execute_insert, execute_update, execute_query_single
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.services.economic_service import get_economic_service
|
from app.services.economic_service import get_economic_service
|
||||||
from app.services.ollama_service import ollama_service
|
from app.services.ollama_service import ollama_service
|
||||||
@ -710,29 +710,8 @@ async def list_supplier_invoices(
|
|||||||
params.append(vendor_id)
|
params.append(vendor_id)
|
||||||
|
|
||||||
if sag_id:
|
if sag_id:
|
||||||
if table_has_column("supplier_invoices", "sag_id"):
|
query += " AND si.sag_id = %s"
|
||||||
query += " AND si.sag_id = %s"
|
params.append(sag_id)
|
||||||
params.append(sag_id)
|
|
||||||
elif (
|
|
||||||
table_has_column("supplier_invoice_relations", "supplier_invoice_id")
|
|
||||||
and table_has_column("supplier_invoice_relations", "relation_type")
|
|
||||||
and table_has_column("supplier_invoice_relations", "relation_id")
|
|
||||||
):
|
|
||||||
query += """
|
|
||||||
AND EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM supplier_invoice_relations sir
|
|
||||||
WHERE sir.supplier_invoice_id = si.id
|
|
||||||
AND sir.relation_type = 'sag'
|
|
||||||
AND sir.relation_id = %s
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
params.append(sag_id)
|
|
||||||
else:
|
|
||||||
logger.warning(
|
|
||||||
"⚠️ supplier invoice sag filter requested, but no schema link available (sag_id column/relation table missing)"
|
|
||||||
)
|
|
||||||
query += " AND 1 = 0"
|
|
||||||
|
|
||||||
if overdue_only:
|
if overdue_only:
|
||||||
query += " AND si.due_date < CURRENT_DATE AND si.paid_date IS NULL"
|
query += " AND si.due_date < CURRENT_DATE AND si.paid_date IS NULL"
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
from typing import List, Optional, Dict
|
from typing import List, Optional, Dict
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from fastapi import APIRouter, HTTPException, Query, UploadFile, File, Request, Form, Response, Body
|
from fastapi import APIRouter, HTTPException, Query, UploadFile, File, Request, Form, Response
|
||||||
from fastapi.responses import FileResponse, HTMLResponse
|
from fastapi.responses import FileResponse, HTMLResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
from app.core.database import execute_query, execute_query_single, table_has_column
|
from app.core.database import execute_query, execute_query_single, table_has_column
|
||||||
@ -1273,7 +1273,7 @@ async def delete_todo_step(step_id: int):
|
|||||||
raise HTTPException(status_code=500, detail="Failed to delete todo step")
|
raise HTTPException(status_code=500, detail="Failed to delete todo step")
|
||||||
|
|
||||||
@router.patch("/sag/{sag_id:int}")
|
@router.patch("/sag/{sag_id:int}")
|
||||||
async def update_sag(sag_id: int, updates: dict = Body(...)):
|
async def update_sag(sag_id: int, updates: dict):
|
||||||
"""Update a case."""
|
"""Update a case."""
|
||||||
try:
|
try:
|
||||||
# Check if case exists
|
# Check if case exists
|
||||||
@ -2892,7 +2892,7 @@ async def get_sale_item(sag_id: int, item_id: int):
|
|||||||
|
|
||||||
|
|
||||||
@router.patch("/sag/{sag_id}/sale-items/{item_id}")
|
@router.patch("/sag/{sag_id}/sale-items/{item_id}")
|
||||||
async def update_sale_item(sag_id: int, item_id: int, updates: dict = Body(...)):
|
async def update_sale_item(sag_id: int, item_id: int, updates: dict):
|
||||||
"""Update a sale item for a case."""
|
"""Update a sale item for a case."""
|
||||||
try:
|
try:
|
||||||
check = execute_query(
|
check = execute_query(
|
||||||
|
|||||||
@ -3461,7 +3461,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-1">
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
<label class="mb-0 text-secondary" style="font-size:0.8rem;">Status</label>
|
<label class="mb-0 text-secondary" style="font-size:0.8rem;">Status</label>
|
||||||
<select id="topbarStatusSelect" class="form-select form-select-sm bg-light" style="width: 62%;" onchange="saveCaseStatusFromTopbar()">
|
<select id="topbarStatusSelect" class="form-select form-select-sm bg-light" style="width: 62%;">
|
||||||
{% for st in status_options %}
|
{% for st in status_options %}
|
||||||
<option value="{{ st }}" {% if (case.status or '')|lower == st|lower %}selected{% endif %}>{{ st|capitalize }}</option>
|
<option value="{{ st }}" {% if (case.status or '')|lower == st|lower %}selected{% endif %}>{{ st|capitalize }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -3708,6 +3708,11 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
bindChange('topbarStatusSelect', async (el) => {
|
||||||
|
await patchCase({ status: el.value || 'åben' });
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
bindChange('topbarTypeSelect', async (el) => {
|
bindChange('topbarTypeSelect', async (el) => {
|
||||||
await patchCase({ type: String(el.value || 'ticket').toLowerCase() });
|
await patchCase({ type: String(el.value || 'ticket').toLowerCase() });
|
||||||
location.reload();
|
location.reload();
|
||||||
|
|||||||
@ -1267,7 +1267,7 @@ window.addEventListener('unhandledrejection', function(event) {
|
|||||||
<script src="/static/js/tag-picker.js?v=2.2"></script>
|
<script src="/static/js/tag-picker.js?v=2.2"></script>
|
||||||
<script src="/static/js/task-template-selector.js?v=1.1"></script>
|
<script src="/static/js/task-template-selector.js?v=1.1"></script>
|
||||||
<script src="/static/js/notifications.js?v=1.0"></script>
|
<script src="/static/js/notifications.js?v=1.0"></script>
|
||||||
<script src="/static/js/telefoni.js?v=2.3"></script>
|
<script src="/static/js/telefoni.js?v=2.2"></script>
|
||||||
<script src="/static/js/sms.js?v=1.0"></script>
|
<script src="/static/js/sms.js?v=1.0"></script>
|
||||||
<script src="/static/js/bug-report.js?v=1.0"></script>
|
<script src="/static/js/bug-report.js?v=1.0"></script>
|
||||||
<script src="/static/js/bottom-bar.js?v=2.23"></script>
|
<script src="/static/js/bottom-bar.js?v=2.23"></script>
|
||||||
|
|||||||
8
main.py
8
main.py
@ -536,18 +536,12 @@ if __name__ == "__main__":
|
|||||||
log_level="info"
|
log_level="info"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
api_workers_raw = os.getenv("API_WORKERS", "1").strip()
|
|
||||||
try:
|
|
||||||
api_workers = max(1, int(api_workers_raw))
|
|
||||||
except ValueError:
|
|
||||||
api_workers = 1
|
|
||||||
|
|
||||||
uvicorn.run(
|
uvicorn.run(
|
||||||
"main:app",
|
"main:app",
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=8000,
|
port=8000,
|
||||||
reload=False,
|
reload=False,
|
||||||
workers=api_workers,
|
workers=2,
|
||||||
timeout_keep_alive=65,
|
timeout_keep_alive=65,
|
||||||
access_log=True,
|
access_log=True,
|
||||||
log_level="info"
|
log_level="info"
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
-- Migration 190: Align sag_sager status constraint with current case status model
|
|
||||||
-- Fixes PATCH /api/v1/sag/{id} failures when using statuses beyond 'åben'/'lukket'.
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
constraint_row RECORD;
|
|
||||||
BEGIN
|
|
||||||
-- Drop legacy check constraints on sag_sager.status regardless of their generated name.
|
|
||||||
FOR constraint_row IN
|
|
||||||
SELECT c.conname
|
|
||||||
FROM pg_constraint c
|
|
||||||
JOIN pg_class t ON t.oid = c.conrelid
|
|
||||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
||||||
WHERE n.nspname = 'public'
|
|
||||||
AND t.relname = 'sag_sager'
|
|
||||||
AND c.contype = 'c'
|
|
||||||
AND pg_get_constraintdef(c.oid) ILIKE '%status%'
|
|
||||||
LOOP
|
|
||||||
EXECUTE format('ALTER TABLE public.sag_sager DROP CONSTRAINT IF EXISTS %I', constraint_row.conname);
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
|
|
||||||
UPDATE public.sag_sager
|
|
||||||
SET status = CASE
|
|
||||||
WHEN lower(trim(status)) IN ('aaben', 'open') THEN 'åben'
|
|
||||||
WHEN lower(trim(status)) IN ('i_gang', 'in_progress', 'under behandling') THEN 'under behandling'
|
|
||||||
WHEN lower(trim(status)) IN ('on_hold', 'waiting', 'afventer') THEN 'afventer'
|
|
||||||
WHEN lower(trim(status)) IN ('resolved', 'løst') THEN 'løst'
|
|
||||||
WHEN lower(trim(status)) IN ('closed', 'afsluttet', 'lukket') THEN 'lukket'
|
|
||||||
ELSE status
|
|
||||||
END;
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1
|
|
||||||
FROM pg_constraint c
|
|
||||||
JOIN pg_class t ON t.oid = c.conrelid
|
|
||||||
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
||||||
WHERE n.nspname = 'public'
|
|
||||||
AND t.relname = 'sag_sager'
|
|
||||||
AND c.conname = 'sag_sager_status_check'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE public.sag_sager
|
|
||||||
ADD CONSTRAINT sag_sager_status_check
|
|
||||||
CHECK (status IN ('åben', 'under behandling', 'afventer', 'løst', 'lukket'));
|
|
||||||
END IF;
|
|
||||||
END $$;
|
|
||||||
@ -226,16 +226,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
scheduleReconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
const proto = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
// Fallback to cookie-auth websocket when token is HttpOnly and cannot be read by JS.
|
const url = `${proto}://${window.location.host}/api/v1/telefoni/ws?token=${encodeURIComponent(token)}`;
|
||||||
const url = token
|
|
||||||
? `${proto}://${window.location.host}/api/v1/telefoni/ws?token=${encodeURIComponent(token)}`
|
|
||||||
: `${proto}://${window.location.host}/api/v1/telefoni/ws`;
|
|
||||||
ws = new WebSocket(url);
|
ws = new WebSocket(url);
|
||||||
|
|
||||||
ws.onopen = () => console.log('📞 Telefoni WS connected');
|
ws.onopen = () => console.log('📞 Telefoni WS connected');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user