feat(service-auto): phase 3 — PACK_AUTO callproc + câmpuri extinse formular

Migrare completă de la SP_CREEAZA_COMANDA_PROTOTIP la PACK_AUTO.dev_adauga_lucrare (18 params).
Formular ComandaNoua extins cu toate câmpurile din SP: observații, defecțiuni, km, ore motor, termen, nr. dosar.

- schema: solicitari → observatii (opțional); adăugat defectiuni, km_int, ore_functionare, nr_dosar, termen
- service: callproc cablat pe câmpurile noi; pc_nr cu milisecunde (evită colizii sub-secundă)
- error mapper: range 20001→20000 (ORA-20000 era neacoperit → 500 în loc de 422)
- onboarding_roa_web.sql: grant pe PACK_AUTO (înlocuiește SP prototip)
- ComandaNoua.vue: InputNumber km/ore, Calendar termen, Textarea defecțiuni, InputText nr_dosar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-04-13 10:35:26 +00:00
parent 31d1f511c3
commit ee6d857e9d
5 changed files with 147 additions and 27 deletions

View File

@@ -81,6 +81,7 @@ async def creeaza_comanda(
return await ComandaService.creeaza_comanda( return await ComandaService.creeaza_comanda(
data=data, data=data,
username=current_user.username, username=current_user.username,
user_id=current_user.user_id,
) )
except NotImplementedError as e: except NotImplementedError as e:
raise HTTPException(status_code=501, detail=str(e)) raise HTTPException(status_code=501, detail=str(e))

View File

@@ -1,3 +1,4 @@
from datetime import date
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
@@ -6,9 +7,14 @@ from pydantic import BaseModel
class ComandaRequest(BaseModel): class ComandaRequest(BaseModel):
tip_id: int tip_id: int
id_masiniclient: int id_masiniclient: int
solicitari: str
id_firma: int id_firma: int
id_sucursala: Optional[int] = None id_sucursala: Optional[int] = None
observatii: str = ""
defectiuni: Optional[str] = None
km_int: int = 0
ore_functionare: int = 0
nr_dosar: str = ""
termen: Optional[date] = None
class ComandaResponse(BaseModel): class ComandaResponse(BaseModel):

View File

@@ -1,5 +1,5 @@
import re import re
from datetime import date from datetime import date, datetime
from typing import List, NoReturn, Optional from typing import List, NoReturn, Optional
import oracledb import oracledb
@@ -10,6 +10,8 @@ from ..schemas.comanda import (
) )
from .. import logger from .. import logger
_SCHEMA = "MARIUSM_AUTO"
_MAX_PER_PAGE = 100 _MAX_PER_PAGE = 100
@@ -18,7 +20,7 @@ def _handle_oracle_error(e: Exception) -> NoReturn:
Map Oracle error codes to FastAPI HTTPExceptions. Always raises. Map Oracle error codes to FastAPI HTTPExceptions. Always raises.
Code ranges: Code ranges:
20001-20999 → 422 Unprocessable (business rule errors from RAISE_APPLICATION_ERROR) 20000-20999 → 422 Unprocessable (business rule errors from RAISE_APPLICATION_ERROR)
12541/12170/12154/12560 → 503 Service Unavailable (Oracle unreachable / network) 12541/12170/12154/12560 → 503 Service Unavailable (Oracle unreachable / network)
1017 → 500 + CRITICAL log (bad credentials — config error) 1017 → 500 + CRITICAL log (bad credentials — config error)
942 → 500 + CRITICAL log (missing object/grant — deployment error) 942 → 500 + CRITICAL log (missing object/grant — deployment error)
@@ -28,7 +30,7 @@ def _handle_oracle_error(e: Exception) -> NoReturn:
code = getattr(err, "code", 0) code = getattr(err, "code", 0)
raw_message = getattr(err, "message", str(e)) raw_message = getattr(err, "message", str(e))
if 20001 <= code <= 20999: if 20000 <= code <= 20999:
# Strip "ORA-2xxxx: " prefix injected by Oracle; expose the business message only # Strip "ORA-2xxxx: " prefix injected by Oracle; expose the business message only
clean = re.sub(r"^ORA-\d+:\s*", "", raw_message).strip() clean = re.sub(r"^ORA-\d+:\s*", "", raw_message).strip()
raise HTTPException(status_code=422, detail=clean) raise HTTPException(status_code=422, detail=clean)
@@ -66,7 +68,12 @@ class ComandaService:
async def creeaza_comanda( async def creeaza_comanda(
data: ComandaRequest, data: ComandaRequest,
username: str, username: str,
user_id: Optional[int] = None,
) -> ComandaResponse: ) -> ComandaResponse:
now = datetime.now()
# pcNr serves as NOM_LUCRARI.NRORD — must be unique; timestamp gives collision-safe value.
pc_nr = f"W{now.strftime('%Y%m%d%H%M%S')}{now.microsecond // 1000:03d}"
logger.info( logger.info(
"service_auto.create_comanda START", "service_auto.create_comanda START",
extra={ extra={
@@ -74,32 +81,47 @@ class ComandaService:
"tip": data.tip_id, "tip": data.tip_id,
"client_id": data.id_masiniclient, "client_id": data.id_masiniclient,
"id_firma": data.id_firma, "id_firma": data.id_firma,
"pc_nr": pc_nr,
"km": data.km_int,
"ore": data.ore_functionare,
}, },
) )
async with oracle_pool.get_connection("mariusm_test") as connection: async with oracle_pool.get_connection("mariusm_test") as connection:
try: try:
with connection.cursor() as cursor: with connection.cursor() as cursor:
# pnIdOrdl is IN OUT — setvalue(0, 0) sets the IN side to 0;
# Oracle overwrites it with the new DEV_ORDL.ID_ORDL.
out_id_ordl = cursor.var(oracledb.NUMBER) out_id_ordl = cursor.var(oracledb.NUMBER)
out_nrord = cursor.var(oracledb.STRING) out_id_ordl.setvalue(0, 0)
cursor.callproc( cursor.callproc(
"MARIUSM_AUTO.SP_CREEAZA_COMANDA_PROTOTIP", f"{_SCHEMA}.PACK_AUTO.dev_adauga_lucrare",
[ [
data.tip_id, # p_tip IN NUMBER _SCHEMA, # v_gcs IN VARCHAR2
data.id_masiniclient, # p_id_masiniclient IN NUMBER now.year, # tnan IN NUMBER
data.solicitari, # p_solicitari IN VARCHAR2 now.month, # tnluna IN NUMBER
data.id_firma, # p_id_firma IN NUMBER user_id or 0, # tnIdUtil IN NUMBER (Oracle ID_UTIL)
data.id_sucursala, # p_id_sucursala IN NUMBER (None for parent firm) pc_nr, # pcNr IN VARCHAR2 (NOM_LUCRARI.NRORD)
out_id_ordl, # p_id_ordl OUT NUMBER None, # pnIdInsp IN NUMBER
out_nrord, # p_nrord OUT VARCHAR2 None, # pnIdAsig IN NUMBER
data.nr_dosar or "", # pcNrDosar IN VARCHAR2
data.id_masiniclient, # pnIdMC IN NUMBER
data.km_int, # pnKmInt IN NUMBER
data.ore_functionare, # pnOreFct IN NUMBER (≥0; NOT NULL in DEV_MASINICLIENTI)
data.termen, # pdTermen IN DATE
data.tip_id, # pnTipCom IN NUMBER
None, # pcSirIdOperatii IN VARCHAR2 — MUST be None, NOT ''
data.observatii or None, # pcObservatii IN VARCHAR2 DEFAULT NULL
data.defectiuni or None, # pcDefectiuni IN VARCHAR2 DEFAULT NULL
0, # pnIdPartRef IN NUMBER (decode(0) → NULL inside SP)
out_id_ordl, # pnIdOrdl IN OUT NUMBER
], ],
) )
connection.commit() connection.commit()
id_ordl = int(out_id_ordl.getvalue()) id_ordl = int(out_id_ordl.getvalue())
nrord = out_nrord.getvalue() or ""
except oracledb.DatabaseError as e: except oracledb.DatabaseError as e:
try: try:
connection.rollback() connection.rollback()
@@ -109,13 +131,13 @@ class ComandaService:
logger.info( logger.info(
"service_auto.create_comanda OK", "service_auto.create_comanda OK",
extra={"user": username, "id_ordl": id_ordl, "nrord": nrord}, extra={"user": username, "id_ordl": id_ordl, "nrord": pc_nr},
) )
return ComandaResponse( return ComandaResponse(
id_ordl=id_ordl, id_ordl=id_ordl,
nrord=nrord, nrord=pc_nr,
mesaj=f"Comanda {nrord} creata cu succes.", mesaj=f"Comanda {pc_nr} creata cu succes.",
) )
@staticmethod @staticmethod

View File

@@ -6,7 +6,7 @@
-- Prerequisite : ROA_WEB user creat (onboarding_roa_web_user.sql) -- Prerequisite : ROA_WEB user creat (onboarding_roa_web_user.sql)
-- ============================================================================= -- =============================================================================
GRANT EXECUTE ON :SCHEMA_NAME.SP_CREEAZA_COMANDA_PROTOTIP TO ROA_WEB; GRANT EXECUTE ON :SCHEMA_NAME.PACK_AUTO TO ROA_WEB;
GRANT SELECT ON :SCHEMA_NAME.AUTO_VMASINICLIENTI TO ROA_WEB; GRANT SELECT ON :SCHEMA_NAME.AUTO_VMASINICLIENTI TO ROA_WEB;
GRANT SELECT ON :SCHEMA_NAME.DEV_TIP_DEVIZ TO ROA_WEB; GRANT SELECT ON :SCHEMA_NAME.DEV_TIP_DEVIZ TO ROA_WEB;
GRANT SELECT ON :SCHEMA_NAME.CALENDAR TO ROA_WEB; -- period selector AppHeader GRANT SELECT ON :SCHEMA_NAME.CALENDAR TO ROA_WEB; -- period selector AppHeader

View File

@@ -65,20 +65,95 @@
/> />
</div> </div>
<!-- Operații solicitate --> <!-- Observații client -->
<div class="field"> <div class="field">
<label style="font-weight: var(--font-medium); font-size: var(--text-sm); color: var(--text-color-secondary);"> <label style="font-weight: var(--font-medium); font-size: var(--text-sm); color: var(--text-color-secondary);">
Operații solicitate * Observații client
</label> </label>
<Textarea <Textarea
v-model="form.solicitari" v-model="form.observatii"
rows="4" rows="3"
placeholder="Descrieți operațiile solicitate de client..." placeholder="Solicitări, observații client..."
:disabled="isSubmitting" :disabled="isSubmitting"
style="width: 100%; resize: vertical;" style="width: 100%; resize: vertical;"
/> />
</div> </div>
<!-- Defecțiuni constatate la recepție -->
<div class="field">
<label style="font-weight: var(--font-medium); font-size: var(--text-sm); color: var(--text-color-secondary);">
Defecțiuni constatate la recepție
</label>
<Textarea
v-model="form.defectiuni"
rows="3"
placeholder="Defecțiuni observate la preluarea vehiculului..."
:disabled="isSubmitting"
style="width: 100%; resize: vertical;"
/>
</div>
<!-- Km + Ore funcționare (same row) -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-md);">
<div class="field">
<label style="font-weight: var(--font-medium); font-size: var(--text-sm); color: var(--text-color-secondary);">
Kilometraj la recepție
</label>
<InputNumber
v-model="form.km_int"
:min="0"
:max="9999999"
:use-grouping="true"
suffix=" km"
:disabled="isSubmitting"
class="w-full"
/>
</div>
<div class="field">
<label style="font-weight: var(--font-medium); font-size: var(--text-sm); color: var(--text-color-secondary);">
Ore funcționare motor
</label>
<InputNumber
v-model="form.ore_functionare"
:min="0"
:max="999999"
:use-grouping="true"
suffix=" ore"
:disabled="isSubmitting"
class="w-full"
/>
</div>
</div>
<!-- Termen estimat -->
<div class="field">
<label style="font-weight: var(--font-medium); font-size: var(--text-sm); color: var(--text-color-secondary);">
Termen estimat finalizare
</label>
<Calendar
v-model="form.termen"
date-format="dd.mm.yy"
placeholder="Selectează data..."
:disabled="isSubmitting"
:min-date="new Date()"
show-icon
class="w-full"
/>
</div>
<!-- Număr dosar asigurare -->
<div class="field">
<label style="font-weight: var(--font-medium); font-size: var(--text-sm); color: var(--text-color-secondary);">
Nr. dosar asigurare
</label>
<InputText
v-model="form.nr_dosar"
placeholder="Completați dacă e comandă de asigurare..."
:disabled="isSubmitting"
class="w-full"
/>
</div>
<!-- Submit --> <!-- Submit -->
<div style="display: flex; justify-content: flex-end; padding-top: var(--space-sm);"> <div style="display: flex; justify-content: flex-end; padding-top: var(--space-sm);">
<Button <Button
@@ -103,6 +178,9 @@ import Dropdown from 'primevue/dropdown'
import Textarea from 'primevue/textarea' import Textarea from 'primevue/textarea'
import Button from 'primevue/button' import Button from 'primevue/button'
import Toast from 'primevue/toast' import Toast from 'primevue/toast'
import InputNumber from 'primevue/inputnumber'
import InputText from 'primevue/inputtext'
import Calendar from 'primevue/calendar'
import serviceAutoApi from '../services/api.js' import serviceAutoApi from '../services/api.js'
import { useCompanyStore } from '../stores/sharedStores.js' import { useCompanyStore } from '../stores/sharedStores.js'
@@ -170,7 +248,12 @@ const emptyForm = () => ({
id_firma: null, id_firma: null,
tip_id: null, tip_id: null,
id_masiniclient: null, id_masiniclient: null,
solicitari: '', observatii: '',
defectiuni: '',
km_int: 0,
ore_functionare: 0,
nr_dosar: '',
termen: null,
}) })
const form = ref(emptyForm()) const form = ref(emptyForm())
@@ -181,8 +264,7 @@ const idSucursala = computed(() => selectedFirma.value?.id_mama != null ? form.v
const isFormValid = computed(() => const isFormValid = computed(() =>
form.value.id_firma !== null && form.value.id_firma !== null &&
form.value.tip_id !== null && form.value.tip_id !== null &&
form.value.id_masiniclient !== null && form.value.id_masiniclient !== null
form.value.solicitari.trim().length > 0
) )
// ─── Submit ──────────────────────────────────────────────────────────────── // ─── Submit ────────────────────────────────────────────────────────────────
@@ -192,12 +274,21 @@ async function submitComanda() {
isSubmitting.value = true isSubmitting.value = true
try { try {
const termen = form.value.termen
? new Date(form.value.termen).toISOString().split('T')[0]
: null
const { data } = await serviceAutoApi.creeazaComanda({ const { data } = await serviceAutoApi.creeazaComanda({
tip_id: form.value.tip_id, tip_id: form.value.tip_id,
id_masiniclient: form.value.id_masiniclient, id_masiniclient: form.value.id_masiniclient,
solicitari: form.value.solicitari.trim(),
id_firma: form.value.id_firma, id_firma: form.value.id_firma,
id_sucursala: idSucursala.value, id_sucursala: idSucursala.value,
observatii: form.value.observatii.trim() || '',
defectiuni: form.value.defectiuni.trim() || null,
km_int: form.value.km_int ?? 0,
ore_functionare: form.value.ore_functionare ?? 0,
nr_dosar: form.value.nr_dosar.trim() || '',
termen,
}) })
toast.add({ toast.add({