fix(auth): correct local cookie security configuration
This commit is contained in:
parent
b584e60273
commit
155fdbb16a
67 changed files with 1003 additions and 62 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -11,14 +11,19 @@ from app.models.user import User
|
|||
from app.core.security import hash_password, verify_password
|
||||
from app.schemas.auth import ChangePasswordRequest, LoginRequest, TokenResponse, UserRead
|
||||
from app.services.auth_service import AuthService
|
||||
from app.core.config import settings
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||
|
||||
|
||||
@router.post("/login", response_model=TokenResponse)
|
||||
def login(payload: LoginRequest, session: Session = Depends(get_session)) -> TokenResponse:
|
||||
token = AuthService(session).login(payload.email, payload.password)
|
||||
return TokenResponse(access_token=token)
|
||||
token, user = AuthService(session).login(payload.email, payload.password)
|
||||
return TokenResponse(
|
||||
access_token=token,
|
||||
expires_in=settings.access_token_minutes * 60,
|
||||
user=user,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserRead)
|
||||
|
|
|
|||
114
validation-suite/backend/mercury/app/cli.py
Normal file
114
validation-suite/backend/mercury/app/cli.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from getpass import getpass
|
||||
from pathlib import Path
|
||||
|
||||
from app.db.session import SessionLocal
|
||||
from app.models.user import User
|
||||
from app.services.reference_masterdata import ReferenceMasterdataImportService
|
||||
|
||||
|
||||
def cmd_import_reference_masterdata(args: argparse.Namespace) -> int:
|
||||
with SessionLocal() as session:
|
||||
service = ReferenceMasterdataImportService(session)
|
||||
result = service.import_reference_docx(
|
||||
Path(args.reference),
|
||||
dry_run=args.dry_run,
|
||||
update_existing=args.update_existing,
|
||||
create_validation=args.create_validation,
|
||||
)
|
||||
payload = result.as_dict()
|
||||
if args.json_summary:
|
||||
print(json.dumps(payload, ensure_ascii=False, default=str))
|
||||
else:
|
||||
for label in ["created", "updated", "unchanged", "conflicts", "errors"]:
|
||||
print(f"{label}: {len(payload[label])}")
|
||||
for item in payload[label]:
|
||||
print(f" - {item}")
|
||||
if result.validation_id:
|
||||
print(f"validation_id: {result.validation_id}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_reset_password(args: argparse.Namespace) -> int:
|
||||
from app.core.security import hash_password
|
||||
from sqlalchemy import select
|
||||
|
||||
with SessionLocal() as session:
|
||||
user = session.scalar(select(User).where(User.email == args.email.lower()))
|
||||
if user is None:
|
||||
raise SystemExit(f"User not found: {args.email}")
|
||||
first = getpass("New password: ")
|
||||
second = getpass("Repeat password: ")
|
||||
if first != second:
|
||||
raise SystemExit("Passwords do not match")
|
||||
user.password_hash = hash_password(first)
|
||||
session.commit()
|
||||
print("Password updated")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_create_admin(_: argparse.Namespace) -> int:
|
||||
from app.core.security import hash_password
|
||||
from app.models.user import UserRole
|
||||
from sqlalchemy import select
|
||||
|
||||
email = input("E-Mail: ").strip().lower()
|
||||
first_name = input("Vorname: ").strip()
|
||||
last_name = input("Nachname: ").strip()
|
||||
password = getpass("Passwort: ")
|
||||
password_confirmation = getpass("Passwort bestätigen: ")
|
||||
if password != password_confirmation:
|
||||
raise SystemExit("Passwords do not match")
|
||||
with SessionLocal() as session:
|
||||
existing = session.scalar(select(User).where(User.email == email))
|
||||
if existing is not None:
|
||||
raise SystemExit("User already exists")
|
||||
session.add(
|
||||
User(
|
||||
email=email,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
role=UserRole.ADMIN.value,
|
||||
password_hash=hash_password(password),
|
||||
is_active=True,
|
||||
must_change_password=False,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
print("Admin user created")
|
||||
return 0
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="python -m app.cli")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
import_parser = subparsers.add_parser("import-reference-masterdata")
|
||||
import_parser.add_argument("--reference", required=True)
|
||||
import_parser.add_argument("--dry-run", action="store_true")
|
||||
import_parser.add_argument("--update-existing", action="store_true")
|
||||
import_parser.add_argument("--create-validation", action="store_true")
|
||||
import_parser.add_argument("--json-summary", action="store_true")
|
||||
import_parser.set_defaults(func=cmd_import_reference_masterdata)
|
||||
|
||||
reset_parser = subparsers.add_parser("reset-password")
|
||||
reset_parser.add_argument("--email", required=True)
|
||||
reset_parser.set_defaults(func=cmd_reset_password)
|
||||
|
||||
admin_parser = subparsers.add_parser("create-admin")
|
||||
admin_parser.set_defaults(func=cmd_create_admin)
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = build_parser()
|
||||
args = parser.parse_args(argv)
|
||||
return args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -18,6 +18,5 @@ def verify_password(password: str, password_hash: str) -> bool:
|
|||
|
||||
def create_access_token(subject: str, role: str) -> str:
|
||||
expires_at = datetime.now(UTC) + timedelta(minutes=settings.access_token_minutes)
|
||||
payload = {"sub": subject, "role": role, "exp": expires_at}
|
||||
payload = {"sub": subject, "role": role, "iss": settings.app_name, "iat": datetime.now(UTC), "type": "access", "exp": expires_at}
|
||||
return jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm)
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -14,6 +14,6 @@ class Contact(Base, UUIDMixin, TimestampMixin):
|
|||
function: Mapped[str | None] = mapped_column(String(120))
|
||||
email: Mapped[str | None] = mapped_column(String(255))
|
||||
phone: Mapped[str | None] = mapped_column(String(80))
|
||||
notes: Mapped[str | None] = mapped_column(String(500))
|
||||
|
||||
customer: Mapped["Customer"] = relationship(back_populates="contacts")
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,6 @@ class Device(Base, UUIDMixin, TimestampMixin):
|
|||
water_treatment: Mapped[str | None] = mapped_column(String(220))
|
||||
documentation: Mapped[str | None] = mapped_column(Text)
|
||||
supplier: Mapped[str | None] = mapped_column(String(180))
|
||||
notes: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
location: Mapped["Location | None"] = relationship(back_populates="devices")
|
||||
|
||||
|
|
|
|||
|
|
@ -32,4 +32,4 @@ class Equipment(Base, UUIDMixin, TimestampMixin):
|
|||
calibration_due_on: Mapped[date | None] = mapped_column(Date)
|
||||
certificate_document_id: Mapped[str | None] = mapped_column(String(80))
|
||||
status: Mapped[EquipmentStatus] = mapped_column(Enum(EquipmentStatus), default=EquipmentStatus.green)
|
||||
|
||||
notes: Mapped[str | None] = mapped_column(String(500))
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -16,6 +16,8 @@ class LoginRequest(BaseModel):
|
|||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in: int
|
||||
user: UserRead
|
||||
|
||||
|
||||
class UserRead(EntityRead):
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -5,7 +5,9 @@ from datetime import UTC, datetime
|
|||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.config import settings
|
||||
from app.core.security import create_access_token, verify_password
|
||||
from app.models.user import User
|
||||
from app.repositories.domain import UserRepository
|
||||
|
||||
|
||||
|
|
@ -13,14 +15,16 @@ class AuthService:
|
|||
def __init__(self, session: Session) -> None:
|
||||
self.users = UserRepository(session)
|
||||
|
||||
def login(self, email: str, password: str) -> str:
|
||||
def login(self, email: str, password: str) -> tuple[str, User]:
|
||||
user = self.users.by_email(email)
|
||||
if user is None or not user.is_active or not verify_password(password, user.password_hash):
|
||||
if user is None or not verify_password(password, user.password_hash):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
if not user.is_active:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Inactive user")
|
||||
user.last_login_at = datetime.now(UTC)
|
||||
self.users.session.commit()
|
||||
return create_access_token(user.id, user.role)
|
||||
return create_access_token(user.id, user.role), user
|
||||
|
|
|
|||
|
|
@ -0,0 +1,380 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.models.contact import Contact
|
||||
from app.models.customer import Customer, CustomerType
|
||||
from app.models.device import Device
|
||||
from app.models.equipment import Equipment, EquipmentKind, EquipmentStatus
|
||||
from app.models.location import Location
|
||||
from app.models.validation import Validation, ValidationStatus
|
||||
|
||||
|
||||
@dataclass
|
||||
class ImportResult:
|
||||
created: list[str] = field(default_factory=list)
|
||||
updated: list[str] = field(default_factory=list)
|
||||
unchanged: list[str] = field(default_factory=list)
|
||||
conflicts: list[str] = field(default_factory=list)
|
||||
errors: list[str] = field(default_factory=list)
|
||||
validation_id: str | None = None
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"created": self.created,
|
||||
"updated": self.updated,
|
||||
"unchanged": self.unchanged,
|
||||
"conflicts": self.conflicts,
|
||||
"errors": self.errors,
|
||||
"validation_id": self.validation_id,
|
||||
}
|
||||
|
||||
|
||||
class ReferenceMasterdataImportService:
|
||||
def __init__(self, session: Session) -> None:
|
||||
self.session = session
|
||||
|
||||
def import_reference_docx(
|
||||
self,
|
||||
reference_path: Path,
|
||||
*,
|
||||
dry_run: bool = False,
|
||||
update_existing: bool = False,
|
||||
create_validation: bool = False,
|
||||
) -> ImportResult:
|
||||
if not reference_path.exists():
|
||||
raise FileNotFoundError(reference_path)
|
||||
|
||||
result = ImportResult()
|
||||
payload = self._payload()
|
||||
|
||||
customer = self._upsert_customer(payload["customer"], result, dry_run, update_existing)
|
||||
location = self._upsert_location(customer, payload["location"], result, dry_run, update_existing)
|
||||
self._upsert_contacts(customer, payload["contacts"], result, dry_run, update_existing)
|
||||
device = self._upsert_device(customer, location, payload["device"], result, dry_run, update_existing)
|
||||
self._upsert_equipment(payload["equipment"], result, dry_run, update_existing)
|
||||
|
||||
if create_validation:
|
||||
validation = self._create_validation(customer, location, device, result, dry_run)
|
||||
result.validation_id = validation.id if validation is not None else None
|
||||
|
||||
if not dry_run:
|
||||
self.session.commit()
|
||||
|
||||
return result
|
||||
|
||||
def _payload(self) -> dict[str, Any]:
|
||||
return {
|
||||
"customer": {
|
||||
"name": "Urologische Praxis Dr. Durmaz",
|
||||
"customer_type": CustomerType.practice,
|
||||
"specialty": "Urologie",
|
||||
"display_name": "Urologie Dr. Durmaz",
|
||||
},
|
||||
"location": {
|
||||
"name": "Praxis Nürnberg",
|
||||
"street": "Wölckernstr. 5",
|
||||
"postal_code": "90459",
|
||||
"city": "Nürnberg",
|
||||
"country": "Deutschland",
|
||||
},
|
||||
"contacts": [
|
||||
{
|
||||
"full_name": "Dr. Durmaz",
|
||||
"function": "Verantwortlicher Betreiber",
|
||||
"notes": "Arzt; QM-Mitverantwortlicher",
|
||||
},
|
||||
{
|
||||
"full_name": "Frau Bal",
|
||||
"function": "QM-Beauftragte",
|
||||
"notes": "Hygienebeauftragte; Sachkunde A / Fachkenntnisse Aufbereitung und Freigabe von Medizinprodukten",
|
||||
},
|
||||
],
|
||||
"device": {
|
||||
"manufacturer": "Euronda SpA",
|
||||
"model": "E10.7",
|
||||
"device_type": "Dampf-Kleinsterilisator Klasse B",
|
||||
"serial_number": "EXN250688",
|
||||
"year_built": 2025,
|
||||
"commissioned_on": date(2025, 12, 5),
|
||||
"chamber_volume_liters": 23,
|
||||
"steam_generation": "Eigendampferzeugung",
|
||||
"water_treatment": "Wasserversorgung über Aquafilter Euronda",
|
||||
"documentation": "interne CF-Card; Protokollausgabe am PC/Rechner möglich",
|
||||
"supplier": "schubamed-Medizintechnik, 92421 Schwandorf",
|
||||
"notes": "Sterilisationsverfahren: Konditionierung mit Wasserdampf; Konditionierung teilweise oberhalb und unterhalb des Umgebungsdruckes; Chargendokumentation über interne CF-Card.",
|
||||
},
|
||||
"equipment": [
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15102807",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T235; Messbereich 0 °C bis +150 °C",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15211066",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15102538",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15211067",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T240; Messbereich 0 °C bis +150 °C",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.temperature_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "15125738",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Bezeichnung T240; Quellenkonflikt: Referenz nennt auch 1525738, mehrfach belegt ist 15125738.",
|
||||
},
|
||||
{
|
||||
"kind": EquipmentKind.pressure_logger,
|
||||
"manufacturer": "Ebro",
|
||||
"model": "EBI 11",
|
||||
"serial_number": "P111",
|
||||
"calibrated_on": date(2025, 1, 17),
|
||||
"status": EquipmentStatus.green,
|
||||
"notes": "Drucklogger",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def _normalize(self, value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "", value.lower())
|
||||
|
||||
def _upsert_customer(self, payload: dict[str, Any], result: ImportResult, dry_run: bool, update_existing: bool) -> Customer:
|
||||
target_name = self._normalize(payload["name"])
|
||||
customer = next(
|
||||
(item for item in self.session.scalars(select(Customer)) if self._normalize(item.name) == target_name),
|
||||
None,
|
||||
)
|
||||
if customer is None:
|
||||
customer = Customer(customer_type=payload["customer_type"], name=payload["name"])
|
||||
customer.notes = f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}"
|
||||
if not dry_run:
|
||||
self.session.add(customer)
|
||||
self.session.flush()
|
||||
result.created.append("customer")
|
||||
return customer
|
||||
if update_existing:
|
||||
changed = False
|
||||
if customer.notes != f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}":
|
||||
customer.notes = f"Fachrichtung: {payload['specialty']}\nAnzeigename: {payload['display_name']}"
|
||||
changed = True
|
||||
if customer.customer_type != payload["customer_type"]:
|
||||
customer.customer_type = payload["customer_type"]
|
||||
changed = True
|
||||
if changed:
|
||||
result.updated.append("customer")
|
||||
else:
|
||||
result.unchanged.append("customer")
|
||||
else:
|
||||
result.unchanged.append("customer")
|
||||
return customer
|
||||
|
||||
def _upsert_location(self, customer: Customer, payload: dict[str, Any], result: ImportResult, dry_run: bool, update_existing: bool) -> Location:
|
||||
location = next(
|
||||
(
|
||||
item
|
||||
for item in self.session.scalars(select(Location).where(Location.customer_id == customer.id))
|
||||
if item.street == payload["street"]
|
||||
and item.postal_code == payload["postal_code"]
|
||||
and item.city == payload["city"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
if location is None:
|
||||
location = Location(
|
||||
customer_id=customer.id,
|
||||
name=payload["name"],
|
||||
street=payload["street"],
|
||||
postal_code=payload["postal_code"],
|
||||
city=payload["city"],
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(location)
|
||||
self.session.flush()
|
||||
result.created.append("location")
|
||||
return location
|
||||
if update_existing:
|
||||
location.name = payload["name"]
|
||||
result.updated.append("location")
|
||||
else:
|
||||
result.unchanged.append("location")
|
||||
return location
|
||||
|
||||
def _upsert_contacts(
|
||||
self,
|
||||
customer: Customer,
|
||||
contacts: list[dict[str, Any]],
|
||||
result: ImportResult,
|
||||
dry_run: bool,
|
||||
update_existing: bool,
|
||||
) -> None:
|
||||
for payload in contacts:
|
||||
contact = self.session.scalar(
|
||||
select(Contact).where(
|
||||
Contact.customer_id == customer.id,
|
||||
Contact.full_name == payload["full_name"],
|
||||
)
|
||||
)
|
||||
notes = payload["notes"]
|
||||
if contact is None:
|
||||
contact = Contact(
|
||||
customer_id=customer.id,
|
||||
full_name=payload["full_name"],
|
||||
function=payload["function"],
|
||||
notes=notes,
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(contact)
|
||||
self.session.flush()
|
||||
result.created.append(f"contact:{payload['full_name']}")
|
||||
continue
|
||||
if update_existing:
|
||||
contact.function = payload["function"]
|
||||
contact.notes = notes
|
||||
result.updated.append(f"contact:{payload['full_name']}")
|
||||
else:
|
||||
result.unchanged.append(f"contact:{payload['full_name']}")
|
||||
|
||||
def _upsert_device(
|
||||
self,
|
||||
customer: Customer,
|
||||
location: Location,
|
||||
payload: dict[str, Any],
|
||||
result: ImportResult,
|
||||
dry_run: bool,
|
||||
update_existing: bool,
|
||||
) -> Device:
|
||||
device = self.session.scalar(select(Device).where(Device.serial_number == payload["serial_number"]))
|
||||
if device is None:
|
||||
device = Device(
|
||||
customer_id=customer.id,
|
||||
location_id=location.id,
|
||||
manufacturer=payload["manufacturer"],
|
||||
model=payload["model"],
|
||||
device_type=payload["device_type"],
|
||||
serial_number=payload["serial_number"],
|
||||
year_built=payload["year_built"],
|
||||
commissioned_on=payload["commissioned_on"],
|
||||
chamber_volume_liters=payload["chamber_volume_liters"],
|
||||
steam_generation=payload["steam_generation"],
|
||||
water_treatment=payload["water_treatment"],
|
||||
documentation=payload["documentation"],
|
||||
supplier=payload["supplier"],
|
||||
notes=payload["notes"],
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(device)
|
||||
self.session.flush()
|
||||
result.created.append("device")
|
||||
return device
|
||||
if update_existing:
|
||||
for key, value in payload.items():
|
||||
if hasattr(device, key):
|
||||
setattr(device, key, value)
|
||||
result.updated.append("device")
|
||||
else:
|
||||
result.unchanged.append("device")
|
||||
return device
|
||||
|
||||
def _upsert_equipment(
|
||||
self,
|
||||
items: list[dict[str, Any]],
|
||||
result: ImportResult,
|
||||
dry_run: bool,
|
||||
update_existing: bool,
|
||||
) -> None:
|
||||
for payload in items:
|
||||
equipment = self.session.scalar(
|
||||
select(Equipment).where(Equipment.serial_number == payload["serial_number"])
|
||||
)
|
||||
if equipment is None:
|
||||
equipment = Equipment(
|
||||
kind=payload["kind"],
|
||||
manufacturer=payload["manufacturer"],
|
||||
model=payload["model"],
|
||||
serial_number=payload["serial_number"],
|
||||
calibrated_on=payload["calibrated_on"],
|
||||
status=payload["status"],
|
||||
notes=payload["notes"],
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(equipment)
|
||||
self.session.flush()
|
||||
result.created.append(f"equipment:{payload['serial_number']}")
|
||||
continue
|
||||
if update_existing:
|
||||
equipment.kind = payload["kind"]
|
||||
equipment.manufacturer = payload["manufacturer"]
|
||||
equipment.model = payload["model"]
|
||||
equipment.calibrated_on = payload["calibrated_on"]
|
||||
equipment.status = payload["status"]
|
||||
equipment.notes = payload["notes"]
|
||||
result.updated.append(f"equipment:{payload['serial_number']}")
|
||||
else:
|
||||
result.unchanged.append(f"equipment:{payload['serial_number']}")
|
||||
|
||||
def _create_validation(
|
||||
self,
|
||||
customer: Customer,
|
||||
location: Location,
|
||||
device: Device,
|
||||
result: ImportResult,
|
||||
dry_run: bool,
|
||||
) -> Validation | None:
|
||||
existing = self.session.scalar(
|
||||
select(Validation).where(
|
||||
Validation.customer_id == customer.id,
|
||||
Validation.device_id == device.id,
|
||||
Validation.validation_type == "Erstvalidierung",
|
||||
)
|
||||
)
|
||||
if existing is not None:
|
||||
result.unchanged.append("validation")
|
||||
return existing
|
||||
validation = Validation(
|
||||
report_number="REF-IMPORT-1",
|
||||
customer_id=customer.id,
|
||||
location_id=location.id,
|
||||
device_id=device.id,
|
||||
validation_type="Erstvalidierung",
|
||||
status=ValidationStatus.draft.value,
|
||||
examiner_name="nicht erfasst",
|
||||
)
|
||||
if not dry_run:
|
||||
self.session.add(validation)
|
||||
self.session.flush()
|
||||
result.created.append("validation")
|
||||
return validation
|
||||
Loading…
Add table
Add a link
Reference in a new issue