fix(catalog): unifica modelos duplicados por variante de carroceria/generacion
- catalog_service.get_models ahora agrupa variantes (p. ej. AVEO Saloon, AVEO Hatchback) bajo un unico display_name y devuelve variant_ids. - Se elige el id_model mas bajo como canonico para presentacion. - /catalog/years y /catalog/engines aceptan model_id como lista separada por comas para consultar todos los MYEs de las variantes agrupadas. - catalog.js usa variant_ids al cargar años/motores y en el selector desplegable (incluyendo carga desde VIN).
This commit is contained in:
@@ -150,12 +150,18 @@ def models():
|
|||||||
@catalog_bp.route('/years', methods=['GET'])
|
@catalog_bp.route('/years', methods=['GET'])
|
||||||
@require_auth('catalog.view')
|
@require_auth('catalog.view')
|
||||||
def years():
|
def years():
|
||||||
model_id = request.args.get('model_id', type=int)
|
model_id_param = request.args.get('model_id', '')
|
||||||
if not model_id:
|
if not model_id_param:
|
||||||
|
return jsonify({'error': 'model_id required'}), 400
|
||||||
|
try:
|
||||||
|
model_ids = [int(x) for x in model_id_param.split(',') if x]
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({'error': 'model_id must be a comma-separated list of integers'}), 400
|
||||||
|
if not model_ids:
|
||||||
return jsonify({'error': 'model_id required'}), 400
|
return jsonify({'error': 'model_id required'}), 400
|
||||||
def _do(master, tenant, branch_id):
|
def _do(master, tenant, branch_id):
|
||||||
mye_ids = catalog_service._get_mye_ids_with_parts(tenant, tenant_id=g.tenant_id, master_conn=master) if tenant else None
|
mye_ids = catalog_service._get_mye_ids_with_parts(tenant, tenant_id=g.tenant_id, master_conn=master) if tenant else None
|
||||||
data = catalog_service.get_years(master, model_id, mye_ids=mye_ids)
|
data = catalog_service.get_years(master, model_ids, mye_ids=mye_ids)
|
||||||
return jsonify({'data': data})
|
return jsonify({'data': data})
|
||||||
return _with_conns(_do)
|
return _with_conns(_do)
|
||||||
|
|
||||||
@@ -176,13 +182,19 @@ def years_all():
|
|||||||
@catalog_bp.route('/engines', methods=['GET'])
|
@catalog_bp.route('/engines', methods=['GET'])
|
||||||
@require_auth('catalog.view')
|
@require_auth('catalog.view')
|
||||||
def engines():
|
def engines():
|
||||||
model_id = request.args.get('model_id', type=int)
|
model_id_param = request.args.get('model_id', '')
|
||||||
year_id = request.args.get('year_id', type=int)
|
year_id = request.args.get('year_id', type=int)
|
||||||
if not model_id or not year_id:
|
if not model_id_param or not year_id:
|
||||||
return jsonify({'error': 'model_id and year_id required'}), 400
|
return jsonify({'error': 'model_id and year_id required'}), 400
|
||||||
|
try:
|
||||||
|
model_ids = [int(x) for x in model_id_param.split(',') if x]
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({'error': 'model_id must be a comma-separated list of integers'}), 400
|
||||||
|
if not model_ids:
|
||||||
|
return jsonify({'error': 'model_id required'}), 400
|
||||||
def _do(master, tenant, branch_id):
|
def _do(master, tenant, branch_id):
|
||||||
mye_ids = catalog_service._get_mye_ids_with_parts(tenant, tenant_id=g.tenant_id, master_conn=master) if tenant else None
|
mye_ids = catalog_service._get_mye_ids_with_parts(tenant, tenant_id=g.tenant_id, master_conn=master) if tenant else None
|
||||||
data = catalog_service.get_engines(master, model_id, year_id, mye_ids=mye_ids)
|
data = catalog_service.get_engines(master, model_ids, year_id, mye_ids=mye_ids)
|
||||||
return jsonify({'data': data})
|
return jsonify({'data': data})
|
||||||
return _with_conns(_do)
|
return _with_conns(_do)
|
||||||
|
|
||||||
|
|||||||
@@ -285,19 +285,23 @@ def get_models(master_conn, brand_id, year_id=None, brand_name=None, mye_ids=Non
|
|||||||
# Filter to North America models only, add clean display name, deduplicate
|
# Filter to North America models only, add clean display name, deduplicate
|
||||||
filtered = [r for r in rows if is_na_model(brand_name, r[1])]
|
filtered = [r for r in rows if is_na_model(brand_name, r[1])]
|
||||||
|
|
||||||
# Group by (display_name, raw name) so distinct body-style variants
|
# Group by display_name so body-style/generation variants
|
||||||
# (e.g. AVEO vs AVEO SALOON) remain selectable.
|
# (e.g. AVEO Saloon, AVEO Hatchback) are shown as a single model.
|
||||||
seen = set()
|
groups = {}
|
||||||
results = []
|
|
||||||
for r in filtered:
|
for r in filtered:
|
||||||
display = _clean_model_name(r[1])
|
display = _clean_model_name(r[1])
|
||||||
key = (display, r[1])
|
groups.setdefault(display, []).append(r)
|
||||||
if key not in seen:
|
|
||||||
seen.add(key)
|
results = []
|
||||||
|
for display, variants in groups.items():
|
||||||
|
# Sort by raw model id ascending; first becomes the canonical id.
|
||||||
|
variants.sort(key=lambda x: x[0])
|
||||||
|
canonical = variants[0]
|
||||||
results.append({
|
results.append({
|
||||||
'id_model': r[0],
|
'id_model': canonical[0],
|
||||||
'name_model': r[1],
|
'name_model': canonical[1],
|
||||||
'display_name': display,
|
'display_name': display,
|
||||||
|
'variant_ids': [v[0] for v in variants],
|
||||||
})
|
})
|
||||||
|
|
||||||
# Sort by display_name
|
# Sort by display_name
|
||||||
@@ -306,34 +310,37 @@ def get_models(master_conn, brand_id, year_id=None, brand_name=None, mye_ids=Non
|
|||||||
|
|
||||||
|
|
||||||
def get_years(master_conn, model_id, mye_ids=None):
|
def get_years(master_conn, model_id, mye_ids=None):
|
||||||
"""Get distinct years for a model via MYE (fast, no vehicle_parts scan). Ordered DESC."""
|
"""Get distinct years for a model (or list of model variants) via MYE.
|
||||||
|
Ordered DESC."""
|
||||||
cur = master_conn.cursor()
|
cur = master_conn.cursor()
|
||||||
|
model_ids = model_id if isinstance(model_id, (list, tuple, set)) else [model_id]
|
||||||
if mye_ids:
|
if mye_ids:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT DISTINCT y.id_year, y.year_car
|
SELECT DISTINCT y.id_year, y.year_car
|
||||||
FROM years y
|
FROM years y
|
||||||
JOIN model_year_engine mye ON mye.year_id = y.id_year
|
JOIN model_year_engine mye ON mye.year_id = y.id_year
|
||||||
WHERE mye.model_id = %s AND mye.id_mye = ANY(%s)
|
WHERE mye.model_id = ANY(%s) AND mye.id_mye = ANY(%s)
|
||||||
ORDER BY y.year_car DESC
|
ORDER BY y.year_car DESC
|
||||||
""", (model_id, mye_ids))
|
""", (list(model_ids), mye_ids))
|
||||||
else:
|
else:
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT DISTINCT y.id_year, y.year_car
|
SELECT DISTINCT y.id_year, y.year_car
|
||||||
FROM years y
|
FROM years y
|
||||||
JOIN model_year_engine mye ON mye.year_id = y.id_year
|
JOIN model_year_engine mye ON mye.year_id = y.id_year
|
||||||
WHERE mye.model_id = %s
|
WHERE mye.model_id = ANY(%s)
|
||||||
ORDER BY y.year_car DESC
|
ORDER BY y.year_car DESC
|
||||||
""", (model_id,))
|
""", (list(model_ids),))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
cur.close()
|
cur.close()
|
||||||
return [{'id_year': r[0], 'year_car': r[1]} for r in rows]
|
return [{'id_year': r[0], 'year_car': r[1]} for r in rows]
|
||||||
|
|
||||||
|
|
||||||
def get_engines(master_conn, model_id, year_id, mye_ids=None):
|
def get_engines(master_conn, model_id, year_id, mye_ids=None):
|
||||||
"""Get MYE entries (engine + trim) for a model+year combo."""
|
"""Get MYE entries (engine + trim) for a model (or list of variants) + year combo."""
|
||||||
cur = master_conn.cursor()
|
cur = master_conn.cursor()
|
||||||
|
model_ids = model_id if isinstance(model_id, (list, tuple, set)) else [model_id]
|
||||||
mye_filter = ""
|
mye_filter = ""
|
||||||
params = [model_id, year_id]
|
params = [list(model_ids), year_id]
|
||||||
if mye_ids:
|
if mye_ids:
|
||||||
mye_filter = " AND mye.id_mye = ANY(%s)"
|
mye_filter = " AND mye.id_mye = ANY(%s)"
|
||||||
params.append(mye_ids)
|
params.append(mye_ids)
|
||||||
@@ -341,7 +348,7 @@ def get_engines(master_conn, model_id, year_id, mye_ids=None):
|
|||||||
SELECT mye.id_mye, e.name_engine, mye.trim_level
|
SELECT mye.id_mye, e.name_engine, mye.trim_level
|
||||||
FROM model_year_engine mye
|
FROM model_year_engine mye
|
||||||
JOIN engines e ON e.id_engine = mye.engine_id
|
JOIN engines e ON e.id_engine = mye.engine_id
|
||||||
WHERE mye.model_id = %s AND mye.year_id = %s{mye_filter}
|
WHERE mye.model_id = ANY(%s) AND mye.year_id = %s{mye_filter}
|
||||||
ORDER BY e.name_engine, mye.trim_level
|
ORDER BY e.name_engine, mye.trim_level
|
||||||
""", tuple(params))
|
""", tuple(params))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
|
|||||||
@@ -427,6 +427,12 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function modelIdsParam(model) {
|
||||||
|
if (!model) return '';
|
||||||
|
if (model.variant_ids && model.variant_ids.length) return model.variant_ids.join(',');
|
||||||
|
return String(model.id);
|
||||||
|
}
|
||||||
|
|
||||||
function loadModels() {
|
function loadModels() {
|
||||||
nav.level = 'models';
|
nav.level = 'models';
|
||||||
pushNavState();
|
pushNavState();
|
||||||
@@ -440,14 +446,15 @@
|
|||||||
if (!data || !data.data || !data.data.length) { showEmpty('Sin modelos', 'No hay modelos con partes para ' + nav.brand.name); return; }
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin modelos', 'No hay modelos con partes para ' + nav.brand.name); return; }
|
||||||
navGrid.className = 'nav-grid';
|
navGrid.className = 'nav-grid';
|
||||||
navGrid.innerHTML = data.data.map(function (m) {
|
navGrid.innerHTML = data.data.map(function (m) {
|
||||||
return '<div class="nav-card" role="listitem" data-model-id="' + m.id_model + '" data-name="' + esc(m.display_name || m.name_model) + '">' +
|
return '<div class="nav-card" role="listitem" data-model-id="' + m.id_model + '" data-variant-ids="' + esc((m.variant_ids || [m.id_model]).join(',')) + '" data-name="' + esc(m.display_name || m.name_model) + '">' +
|
||||||
'<div class="nav-card__name">' + esc(m.display_name || m.name_model) + '</div>' +
|
'<div class="nav-card__name">' + esc(m.display_name || m.name_model) + '</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
||||||
card.addEventListener('click', function () {
|
card.addEventListener('click', function () {
|
||||||
nav.model = { id: parseInt(this.dataset.modelId), name: this.dataset.name };
|
var variantIds = (this.dataset.variantIds || this.dataset.modelId).split(',').map(function(x){ return parseInt(x); });
|
||||||
|
nav.model = { id: parseInt(this.dataset.modelId), name: this.dataset.name, variant_ids: variantIds };
|
||||||
loadYears();
|
loadYears();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -462,7 +469,7 @@
|
|||||||
setupLevelFilter(false);
|
setupLevelFilter(false);
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
apiFetch(API + '/years?model_id=' + nav.model.id).then(function (data) {
|
apiFetch(API + '/years?model_id=' + modelIdsParam(nav.model)).then(function (data) {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
if (!data || !data.data || !data.data.length) { showEmpty('Sin anios', 'No hay anios con partes para este modelo.'); return; }
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin anios', 'No hay anios con partes para este modelo.'); return; }
|
||||||
navGrid.className = 'nav-grid nav-grid--years';
|
navGrid.className = 'nav-grid nav-grid--years';
|
||||||
@@ -489,7 +496,7 @@
|
|||||||
setupLevelFilter(false);
|
setupLevelFilter(false);
|
||||||
showLoading();
|
showLoading();
|
||||||
|
|
||||||
apiFetch(API + '/engines?model_id=' + nav.model.id + '&year_id=' + nav.year.id).then(function (data) {
|
apiFetch(API + '/engines?model_id=' + modelIdsParam(nav.model) + '&year_id=' + nav.year.id).then(function (data) {
|
||||||
hideLoading();
|
hideLoading();
|
||||||
if (!data || !data.data || !data.data.length) { showEmpty('Sin motores', 'No hay configuraciones de motor para esta combinacion.'); return; }
|
if (!data || !data.data || !data.data.length) { showEmpty('Sin motores', 'No hay configuraciones de motor para esta combinacion.'); return; }
|
||||||
|
|
||||||
@@ -1829,7 +1836,8 @@
|
|||||||
if (!models) return;
|
if (!models) return;
|
||||||
vsModel.innerHTML = '<option value="">Modelo...</option>' +
|
vsModel.innerHTML = '<option value="">Modelo...</option>' +
|
||||||
models.map(function (m) {
|
models.map(function (m) {
|
||||||
return '<option value="' + m.id_model + '">' + esc(m.display_name || m.name_model) + '</option>';
|
var variants = (m.variant_ids || [m.id_model]).join(',');
|
||||||
|
return '<option value="' + m.id_model + '" data-variant-ids="' + esc(variants) + '">' + esc(m.display_name || m.name_model) + '</option>';
|
||||||
}).join('');
|
}).join('');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1837,13 +1845,17 @@
|
|||||||
function vsModelChanged() {
|
function vsModelChanged() {
|
||||||
var modelId = vsModel.value;
|
var modelId = vsModel.value;
|
||||||
var yearVal = vsYear.value;
|
var yearVal = vsYear.value;
|
||||||
|
var selectedOption = vsModel.options[vsModel.selectedIndex];
|
||||||
|
var variantIds = selectedOption && selectedOption.dataset.variantIds
|
||||||
|
? selectedOption.dataset.variantIds.split(',').map(function(x){ return parseInt(x); })
|
||||||
|
: (modelId ? [parseInt(modelId)] : []);
|
||||||
vsEngine.innerHTML = '<option value="">Motor...</option>';
|
vsEngine.innerHTML = '<option value="">Motor...</option>';
|
||||||
vsEngine.disabled = true;
|
vsEngine.disabled = true;
|
||||||
|
|
||||||
if (!modelId || !yearVal) return;
|
if (!modelId || !yearVal || !variantIds.length) return;
|
||||||
|
|
||||||
vsEngine.disabled = false;
|
vsEngine.disabled = false;
|
||||||
apiFetch(API + '/engines?model_id=' + modelId + '&year_id=' + yearVal).then(function (data) {
|
apiFetch(API + '/engines?model_id=' + variantIds.join(',') + '&year_id=' + yearVal).then(function (data) {
|
||||||
var engines = data.data || data;
|
var engines = data.data || data;
|
||||||
if (!engines) return;
|
if (!engines) return;
|
||||||
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
||||||
@@ -1869,8 +1881,12 @@
|
|||||||
var modelText = vsModel.options[vsModel.selectedIndex].text;
|
var modelText = vsModel.options[vsModel.selectedIndex].text;
|
||||||
var engineText = vsEngine.options[vsEngine.selectedIndex].text;
|
var engineText = vsEngine.options[vsEngine.selectedIndex].text;
|
||||||
|
|
||||||
|
var selectedModelOption = vsModel.options[vsModel.selectedIndex];
|
||||||
|
var modelVariantIds = selectedModelOption && selectedModelOption.dataset.variantIds
|
||||||
|
? selectedModelOption.dataset.variantIds.split(',').map(function(x){ return parseInt(x); })
|
||||||
|
: [parseInt(vsModel.value)];
|
||||||
nav.brand = { id: parseInt(vsBrand.value), name: brandText };
|
nav.brand = { id: parseInt(vsBrand.value), name: brandText };
|
||||||
nav.model = { id: parseInt(vsModel.value), name: modelText };
|
nav.model = { id: parseInt(vsModel.value), name: modelText, variant_ids: modelVariantIds };
|
||||||
nav.year = { id: parseInt(vsYear.value), year: yearText };
|
nav.year = { id: parseInt(vsYear.value), year: yearText };
|
||||||
nav.engine = { id_mye: parseInt(myeId), name: engineText };
|
nav.engine = { id_mye: parseInt(myeId), name: engineText };
|
||||||
nav.level = 'categories';
|
nav.level = 'categories';
|
||||||
@@ -2058,14 +2074,29 @@
|
|||||||
if (!models) return;
|
if (!models) return;
|
||||||
vsModel.innerHTML = '<option value="">Modelo...</option>' +
|
vsModel.innerHTML = '<option value="">Modelo...</option>' +
|
||||||
models.map(function (m) {
|
models.map(function (m) {
|
||||||
return '<option value="' + m.id_model + '">' + esc(m.display_name || m.name_model) + '</option>';
|
var variants = (m.variant_ids || [m.id_model]).join(',');
|
||||||
|
return '<option value="' + m.id_model + '" data-variant-ids="' + esc(variants) + '">' + esc(m.display_name || m.name_model) + '</option>';
|
||||||
}).join('');
|
}).join('');
|
||||||
vsModel.disabled = false;
|
vsModel.disabled = false;
|
||||||
|
|
||||||
if (match.model_id) {
|
if (match.model_id) {
|
||||||
|
// The VIN match may point to a variant that is now grouped under
|
||||||
|
// a canonical model; select the option whose variants include it.
|
||||||
|
var matchedOption = Array.from(vsModel.options).find(function (opt) {
|
||||||
|
if (!opt.value) return false;
|
||||||
|
var vids = (opt.dataset.variantIds || opt.value).split(',').map(function(x){ return parseInt(x); });
|
||||||
|
return vids.indexOf(match.model_id) !== -1;
|
||||||
|
});
|
||||||
|
if (matchedOption) {
|
||||||
|
vsModel.value = matchedOption.value;
|
||||||
|
} else {
|
||||||
vsModel.value = String(match.model_id);
|
vsModel.value = String(match.model_id);
|
||||||
|
}
|
||||||
|
var selectedVariantIds = vsModel.selectedIndex >= 0 && vsModel.options[vsModel.selectedIndex].dataset.variantIds
|
||||||
|
? vsModel.options[vsModel.selectedIndex].dataset.variantIds.split(',').map(function(x){ return parseInt(x); })
|
||||||
|
: [match.model_id];
|
||||||
// Load engines
|
// Load engines
|
||||||
apiFetch(API + '/engines?model_id=' + match.model_id + '&year_id=' + match.year_id).then(function (engData) {
|
apiFetch(API + '/engines?model_id=' + selectedVariantIds.join(',') + '&year_id=' + match.year_id).then(function (engData) {
|
||||||
var engines = engData && (engData.data || engData);
|
var engines = engData && (engData.data || engData);
|
||||||
if (!engines) return;
|
if (!engines) return;
|
||||||
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
||||||
|
|||||||
Reference in New Issue
Block a user