diff --git a/pos/blueprints/customers_bp.py b/pos/blueprints/customers_bp.py
index bd00503..2a43ba3 100644
--- a/pos/blueprints/customers_bp.py
+++ b/pos/blueprints/customers_bp.py
@@ -161,8 +161,9 @@ def create_customer():
cur.execute("""
INSERT INTO customers
(branch_id, name, rfc, razon_social, regimen_fiscal, uso_cfdi,
- cp, email, phone, address, price_tier, credit_limit, vehicle_info)
- VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
+ cp, email, phone, address, price_tier, credit_limit,
+ max_discount_pct, vehicle_info)
+ VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
RETURNING id
""", (
branch_id, data['name'], data.get('rfc'), data.get('razon_social'),
@@ -170,6 +171,7 @@ def create_customer():
data.get('cp'), data.get('email'), data.get('phone'),
data.get('address'), data.get('price_tier', 1),
data.get('credit_limit', 0),
+ data.get('max_discount_pct', 0),
json.dumps(data['vehicle_info']) if data.get('vehicle_info') else None
))
customer_id = cur.fetchone()[0]
diff --git a/pos/migrations/runner.py b/pos/migrations/runner.py
index 14863f4..9044273 100755
--- a/pos/migrations/runner.py
+++ b/pos/migrations/runner.py
@@ -51,6 +51,7 @@ MIGRATIONS = {
"v4.2": "v4.2_meli_sync_queue.sql",
"v4.3": "v4.3_facturapi.sql",
"v4.4": "v4.4_workshop.sql",
+ "v4.5": "v4.5_customer_max_discount.sql",
}
diff --git a/pos/migrations/v4.5_customer_max_discount.sql b/pos/migrations/v4.5_customer_max_discount.sql
new file mode 100644
index 0000000..22bc602
--- /dev/null
+++ b/pos/migrations/v4.5_customer_max_discount.sql
@@ -0,0 +1,5 @@
+-- /home/Autopartes/pos/migrations/v4.5_customer_max_discount.sql
+-- Tenant DB schema v4.5 — add per-customer maximum discount percentage.
+
+ALTER TABLE customers
+ ADD COLUMN IF NOT EXISTS max_discount_pct NUMERIC(5,2) DEFAULT 0;
diff --git a/pos/services/catalog_service.py b/pos/services/catalog_service.py
index b74037a..a82b64f 100644
--- a/pos/services/catalog_service.py
+++ b/pos/services/catalog_service.py
@@ -349,11 +349,28 @@ def get_engines(master_conn, model_id, year_id, mye_ids=None):
FROM model_year_engine mye
JOIN engines e ON e.id_engine = mye.engine_id
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, mye.id_mye
""", tuple(params))
rows = cur.fetchall()
cur.close()
- return [{'id_mye': r[0], 'name_engine': r[1], 'trim_level': r[2] or ''} for r in rows]
+
+ def _clean_engine_name(name):
+ if not name or name.strip().upper() in ('N/A', 'RUEDA', ''):
+ return 'Sin especificar'
+ return name.strip()
+
+ # Deduplicate identical (name, trim) entries so the user doesn't see
+ # multiple indistinguishable "Sin especificar" options.
+ seen = set()
+ results = []
+ for id_mye, name_engine, trim_level in rows:
+ clean_name = _clean_engine_name(name_engine)
+ key = (clean_name, trim_level or '')
+ if key in seen:
+ continue
+ seen.add(key)
+ results.append({'id_mye': id_mye, 'name_engine': clean_name, 'trim_level': key[1]})
+ return results
def get_categories(master_conn, mye_id, allowed_brands=None):
diff --git a/pos/static/js/brand-catalog.js b/pos/static/js/brand-catalog.js
index 2ca59f9..8b32ade 100644
--- a/pos/static/js/brand-catalog.js
+++ b/pos/static/js/brand-catalog.js
@@ -14,6 +14,7 @@
brandId: null,
model: null,
modelId: null,
+ modelVariantIds: null, // array of TecDoc model ids for grouped variants
year: null,
yearId: null,
engine: null,
@@ -63,7 +64,7 @@
reset: function() {
this.state = 'brands';
- this.nav = { brand: null, brandId: null, model: null, modelId: null, year: null, yearId: null, engine: null, myeId: null, category: null, categoryId: null };
+ this.nav = { brand: null, brandId: null, model: null, modelId: null, modelVariantIds: null, year: null, yearId: null, engine: null, myeId: null, category: null, categoryId: null };
this._allBrands = [];
this._lastItems = [];
this._offset = 0;
@@ -96,7 +97,7 @@
parts.push('' + escapeHtml(this.nav.brand) + '');
}
if (this.nav.model) {
- parts.push('' + escapeHtml(this.nav.model) + '');
+ parts.push('' + escapeHtml(this.nav.model) + '');
}
if (this.nav.year) {
parts.push('' + this.nav.year + '');
@@ -202,7 +203,8 @@
renderModelList: function(models) {
var html = '
';
models.forEach(function(m) {
- html += '
' +
+ var variantIds = (m.variant_ids && m.variant_ids.length) ? m.variant_ids : [m.id_model];
+ html += '
' +
'
' + escapeHtml(m.display_name || m.name_model) + '
' +
'
';
});
@@ -210,20 +212,22 @@
this.setContent(html);
},
- selectModel: function(modelId, modelName) {
+ selectModel: function(modelId, modelName, variantIds) {
this.nav.model = modelName;
this.nav.modelId = modelId;
- this.loadYears(modelId);
+ this.nav.modelVariantIds = variantIds && variantIds.length ? variantIds : [modelId];
+ this.loadYears(this.nav.modelVariantIds);
},
// ---------- YEARS ----------
- loadYears: function(modelId) {
+ loadYears: function(modelIds) {
this.loading(true);
this.state = 'years';
this.setSearch('');
this.buildBreadcrumb();
var self = this;
- fetch('/pos/api/catalog/years?model_id=' + encodeURIComponent(modelId), { headers: this._headers() })
+ var modelIdParam = Array.isArray(modelIds) ? modelIds.join(',') : String(modelIds);
+ fetch('/pos/api/catalog/years?model_id=' + encodeURIComponent(modelIdParam), { headers: this._headers() })
.then(function(r) {
if (!self._checkAuth(r)) return null;
return r.json();
@@ -258,17 +262,18 @@
selectYear: function(yearId, yearCar) {
this.nav.year = yearCar;
this.nav.yearId = yearId;
- this.loadEngines(this.nav.modelId, yearId);
+ this.loadEngines(this.nav.modelVariantIds || [this.nav.modelId], yearId);
},
// ---------- ENGINES ----------
- loadEngines: function(modelId, yearId) {
+ loadEngines: function(modelIds, yearId) {
this.loading(true);
this.state = 'engines';
this.setSearch('');
this.buildBreadcrumb();
var self = this;
- fetch('/pos/api/catalog/engines?model_id=' + encodeURIComponent(modelId) + '&year_id=' + encodeURIComponent(yearId), { headers: this._headers() })
+ var modelIdParam = Array.isArray(modelIds) ? modelIds.join(',') : String(modelIds);
+ fetch('/pos/api/catalog/engines?model_id=' + encodeURIComponent(modelIdParam) + '&year_id=' + encodeURIComponent(yearId), { headers: this._headers() })
.then(function(r) {
if (!self._checkAuth(r)) return null;
return r.json();
@@ -292,8 +297,9 @@
renderEngineList: function(engines) {
var html = '
';
engines.forEach(function(e) {
- html += '
' +
- '
' + escapeHtml(e.name_engine) + '
' +
+ var name = (e.name_engine && e.name_engine !== 'N/A') ? e.name_engine : 'Sin especificar';
+ html += '
' +
+ '
' + escapeHtml(name) + '
' +
'
' + escapeHtml(e.trim_level || '') + '
' +
'
';
});
diff --git a/pos/static/js/catalog.js b/pos/static/js/catalog.js
index e6304e4..4ebc938 100644
--- a/pos/static/js/catalog.js
+++ b/pos/static/js/catalog.js
@@ -500,19 +500,26 @@
hideLoading();
if (!data || !data.data || !data.data.length) { showEmpty('Sin motores', 'No hay configuraciones de motor para esta combinacion.'); return; }
+ // Helper: avoid showing raw "N/A" as engine name
+ function engineLabel(e) {
+ var name = (e.name_engine && e.name_engine !== 'N/A') ? e.name_engine : 'Sin especificar';
+ return name + (e.trim_level ? ' — ' + e.trim_level : '');
+ }
+
// If only one engine, auto-select it
if (data.data.length === 1) {
var e = data.data[0];
- nav.engine = { id_mye: e.id_mye, name: e.name_engine + (e.trim_level ? ' ' + e.trim_level : '') };
+ nav.engine = { id_mye: e.id_mye, name: engineLabel(e) };
loadCategoriesForMode();
return;
}
navGrid.className = 'nav-grid';
navGrid.innerHTML = data.data.map(function (e) {
- var label = e.name_engine + (e.trim_level ? ' — ' + e.trim_level : '');
+ var name = (e.name_engine && e.name_engine !== 'N/A') ? e.name_engine : 'Sin especificar';
+ var label = name + (e.trim_level ? ' — ' + e.trim_level : '');
return '
' +
- '
' + esc(e.name_engine) + '
' +
+ '
' + esc(name) + '
' +
(e.trim_level ? '
' + esc(e.trim_level) + '
' : '') +
'
';
}).join('');
@@ -1860,7 +1867,8 @@
if (!engines) return;
vsEngine.innerHTML = '
' +
engines.map(function (e) {
- var label = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
+ var name = (e.name_engine && e.name_engine !== 'N/A') ? e.name_engine : 'Sin especificar';
+ var label = name + (e.trim_level ? ' (' + e.trim_level + ')' : '');
return '
';
}).join('');
// If only 1 engine, auto-select
@@ -2101,7 +2109,8 @@
if (!engines) return;
vsEngine.innerHTML = '
' +
engines.map(function (e) {
- var elabel = e.name_engine + (e.trim_level ? ' (' + e.trim_level + ')' : '');
+ var ename = (e.name_engine && e.name_engine !== 'N/A') ? e.name_engine : 'Sin especificar';
+ var elabel = ename + (e.trim_level ? ' (' + e.trim_level + ')' : '');
return '
';
}).join('');
vsEngine.disabled = false;
diff --git a/pos/static/pwa/sw.js b/pos/static/pwa/sw.js
index 4793439..98cd6bd 100644
--- a/pos/static/pwa/sw.js
+++ b/pos/static/pwa/sw.js
@@ -6,7 +6,7 @@
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
// so templates can use cache-busting query params freely.
-const CACHE_NAME = 'nexus-pos-v18';
+const CACHE_NAME = 'nexus-pos-v20';
const APP_SHELL = [
'/pos/static/css/tokens.css',
diff --git a/pos/templates/catalog.html b/pos/templates/catalog.html
index 0148f0f..c203525 100644
--- a/pos/templates/catalog.html
+++ b/pos/templates/catalog.html
@@ -321,7 +321,7 @@
-
+
@@ -341,6 +341,6 @@
}
-
+