fix(mappings): resolve 409 error on multi-CODMAT edit and make SKU editable

Batch create after soft-delete was rejected because create_mapping()
treated soft-deleted records as conflicts. Added auto_restore param
that restores+updates instead of 409 when called from edit flow.
Also removed readOnly on SKU input in edit modal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-03-16 13:31:03 +00:00
parent 25aa9e544c
commit 926543a2e4
4 changed files with 43 additions and 15 deletions

View File

@@ -52,6 +52,7 @@ class MappingLine(BaseModel):
class MappingBatchCreate(BaseModel):
sku: str
mappings: list[MappingLine]
auto_restore: bool = False
# HTML page
@router.get("/mappings", response_class=HTMLResponse)
@@ -141,7 +142,7 @@ async def create_batch_mapping(data: MappingBatchCreate):
try:
results = []
for m in data.mappings:
r = mapping_service.create_mapping(data.sku, m.codmat, m.cantitate_roa, m.procent_pret)
r = mapping_service.create_mapping(data.sku, m.codmat, m.cantitate_roa, m.procent_pret, auto_restore=data.auto_restore)
results.append(r)
# Mark SKU as resolved in missing_skus tracking
await sqlite_service.resolve_missing_sku(data.sku)

View File

@@ -145,8 +145,11 @@ def get_mappings(search: str = "", page: int = 1, per_page: int = 50,
"counts": counts,
}
def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret: float = 100):
"""Create a new mapping. Returns dict or raises HTTPException on duplicate."""
def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret: float = 100, auto_restore: bool = False):
"""Create a new mapping. Returns dict or raises HTTPException on duplicate.
When auto_restore=True, soft-deleted records are restored+updated instead of raising 409.
"""
if not sku or not sku.strip():
raise HTTPException(status_code=400, detail="SKU este obligatoriu")
if not codmat or not codmat.strip():
@@ -170,11 +173,22 @@ def create_mapping(sku: str, codmat: str, cantitate_roa: float = 1, procent_pret
WHERE sku = :sku AND codmat = :codmat AND sters = 1
""", {"sku": sku, "codmat": codmat})
if cur.fetchone()[0] > 0:
raise HTTPException(
status_code=409,
detail="Maparea a fost ștearsă anterior",
headers={"X-Can-Restore": "true"}
)
if auto_restore:
cur.execute("""
UPDATE ARTICOLE_TERTI SET sters = 0, activ = 1,
cantitate_roa = :cantitate_roa, procent_pret = :procent_pret,
data_modif = SYSDATE
WHERE sku = :sku AND codmat = :codmat AND sters = 1
""", {"sku": sku, "codmat": codmat,
"cantitate_roa": cantitate_roa, "procent_pret": procent_pret})
conn.commit()
return {"sku": sku, "codmat": codmat}
else:
raise HTTPException(
status_code=409,
detail="Maparea a fost ștearsă anterior",
headers={"X-Can-Restore": "true"}
)
cur.execute("""
INSERT INTO ARTICOLE_TERTI (sku, codmat, cantitate_roa, procent_pret, activ, sters, data_creare, id_util_creare)

View File

@@ -280,7 +280,7 @@ async function openEditModal(sku, codmat, cantitate, procent) {
editingMapping = { sku, codmat };
document.getElementById('addModalTitle').textContent = 'Editare Mapare';
document.getElementById('inputSku').value = sku;
document.getElementById('inputSku').readOnly = true;
document.getElementById('inputSku').readOnly = false;
document.getElementById('pctWarning').style.display = 'none';
const container = document.getElementById('codmatLines');
@@ -292,6 +292,15 @@ async function openEditModal(sku, codmat, cantitate, procent) {
const data = await res.json();
const allMappings = (data.mappings || []).filter(m => m.sku === sku && !m.sters);
// Show product name if available
const productName = allMappings[0]?.product_name || '';
const productNameEl = document.getElementById('addModalProductName');
const productNameText = document.getElementById('inputProductName');
if (productName && productNameEl && productNameText) {
productNameText.textContent = productName;
productNameEl.style.display = '';
}
if (allMappings.length === 0) {
// Fallback to single line with passed values
addCodmatLine();
@@ -307,6 +316,9 @@ async function openEditModal(sku, codmat, cantitate, procent) {
const lines = container.querySelectorAll('.codmat-line');
const line = lines[lines.length - 1];
line.querySelector('.cl-codmat').value = m.codmat;
if (m.denumire) {
line.querySelector('.cl-selected').textContent = m.denumire;
}
line.querySelector('.cl-cantitate').value = m.cantitate_roa;
line.querySelector('.cl-procent').value = m.procent_pret;
}
@@ -436,22 +448,23 @@ async function saveMapping() {
});
} else {
// Multi-CODMAT set: delete all existing then create new batch
const existRes = await fetch(`/api/mappings?search=${encodeURIComponent(editingMapping.sku)}&per_page=100`);
const oldSku = editingMapping.sku;
const existRes = await fetch(`/api/mappings?search=${encodeURIComponent(oldSku)}&per_page=100`);
const existData = await existRes.json();
const existing = (existData.mappings || []).filter(m => m.sku === editingMapping.sku && !m.sters);
const existing = (existData.mappings || []).filter(m => m.sku === oldSku && !m.sters);
// Delete each existing CODMAT
// Delete each existing CODMAT for old SKU
for (const m of existing) {
await fetch(`/api/mappings/${encodeURIComponent(m.sku)}/${encodeURIComponent(m.codmat)}`, {
method: 'DELETE'
});
}
// Create new batch
// Create new batch with auto_restore (handles just-soft-deleted records)
res = await fetch('/api/mappings/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sku, mappings })
body: JSON.stringify({ sku, mappings, auto_restore: true })
});
}
} else if (mappings.length === 1) {

View File

@@ -154,5 +154,5 @@
{% endblock %}
{% block scripts %}
<script src="/static/js/mappings.js?v=5"></script>
<script src="/static/js/mappings.js?v=7"></script>
{% endblock %}