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'])
|
||||
@require_auth('catalog.view')
|
||||
def years():
|
||||
model_id = request.args.get('model_id', type=int)
|
||||
if not model_id:
|
||||
model_id_param = request.args.get('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
|
||||
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
|
||||
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 _with_conns(_do)
|
||||
|
||||
@@ -176,13 +182,19 @@ def years_all():
|
||||
@catalog_bp.route('/engines', methods=['GET'])
|
||||
@require_auth('catalog.view')
|
||||
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)
|
||||
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
|
||||
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):
|
||||
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 _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
|
||||
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
|
||||
# (e.g. AVEO vs AVEO SALOON) remain selectable.
|
||||
seen = set()
|
||||
results = []
|
||||
# Group by display_name so body-style/generation variants
|
||||
# (e.g. AVEO Saloon, AVEO Hatchback) are shown as a single model.
|
||||
groups = {}
|
||||
for r in filtered:
|
||||
display = _clean_model_name(r[1])
|
||||
key = (display, r[1])
|
||||
if key not in seen:
|
||||
seen.add(key)
|
||||
groups.setdefault(display, []).append(r)
|
||||
|
||||
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({
|
||||
'id_model': r[0],
|
||||
'name_model': r[1],
|
||||
'id_model': canonical[0],
|
||||
'name_model': canonical[1],
|
||||
'display_name': display,
|
||||
'variant_ids': [v[0] for v in variants],
|
||||
})
|
||||
|
||||
# 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):
|
||||
"""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()
|
||||
model_ids = model_id if isinstance(model_id, (list, tuple, set)) else [model_id]
|
||||
if mye_ids:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT y.id_year, y.year_car
|
||||
FROM years y
|
||||
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
|
||||
""", (model_id, mye_ids))
|
||||
""", (list(model_ids), mye_ids))
|
||||
else:
|
||||
cur.execute("""
|
||||
SELECT DISTINCT y.id_year, y.year_car
|
||||
FROM years y
|
||||
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
|
||||
""", (model_id,))
|
||||
""", (list(model_ids),))
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
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):
|
||||
"""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()
|
||||
model_ids = model_id if isinstance(model_id, (list, tuple, set)) else [model_id]
|
||||
mye_filter = ""
|
||||
params = [model_id, year_id]
|
||||
params = [list(model_ids), year_id]
|
||||
if mye_ids:
|
||||
mye_filter = " AND mye.id_mye = ANY(%s)"
|
||||
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
|
||||
FROM model_year_engine mye
|
||||
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
|
||||
""", tuple(params))
|
||||
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() {
|
||||
nav.level = 'models';
|
||||
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; }
|
||||
navGrid.className = 'nav-grid';
|
||||
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>';
|
||||
}).join('');
|
||||
|
||||
navGrid.querySelectorAll('.nav-card').forEach(function (card) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -462,7 +469,7 @@
|
||||
setupLevelFilter(false);
|
||||
showLoading();
|
||||
|
||||
apiFetch(API + '/years?model_id=' + nav.model.id).then(function (data) {
|
||||
apiFetch(API + '/years?model_id=' + modelIdsParam(nav.model)).then(function (data) {
|
||||
hideLoading();
|
||||
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';
|
||||
@@ -489,7 +496,7 @@
|
||||
setupLevelFilter(false);
|
||||
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();
|
||||
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;
|
||||
vsModel.innerHTML = '<option value="">Modelo...</option>' +
|
||||
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('');
|
||||
});
|
||||
}
|
||||
@@ -1837,13 +1845,17 @@
|
||||
function vsModelChanged() {
|
||||
var modelId = vsModel.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.disabled = true;
|
||||
|
||||
if (!modelId || !yearVal) return;
|
||||
if (!modelId || !yearVal || !variantIds.length) return;
|
||||
|
||||
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;
|
||||
if (!engines) return;
|
||||
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
||||
@@ -1869,8 +1881,12 @@
|
||||
var modelText = vsModel.options[vsModel.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.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.engine = { id_mye: parseInt(myeId), name: engineText };
|
||||
nav.level = 'categories';
|
||||
@@ -2058,14 +2074,29 @@
|
||||
if (!models) return;
|
||||
vsModel.innerHTML = '<option value="">Modelo...</option>' +
|
||||
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('');
|
||||
vsModel.disabled = false;
|
||||
|
||||
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);
|
||||
}
|
||||
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
|
||||
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);
|
||||
if (!engines) return;
|
||||
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
||||
|
||||
Reference in New Issue
Block a user