feat: migración PZ La Casita, fix motor N/A/RUEDA, cache-buster catálogo y variant_ids
This commit is contained in:
@@ -161,8 +161,9 @@ def create_customer():
|
|||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO customers
|
INSERT INTO customers
|
||||||
(branch_id, name, rfc, razon_social, regimen_fiscal, uso_cfdi,
|
(branch_id, name, rfc, razon_social, regimen_fiscal, uso_cfdi,
|
||||||
cp, email, phone, address, price_tier, credit_limit, vehicle_info)
|
cp, email, phone, address, price_tier, credit_limit,
|
||||||
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
max_discount_pct, vehicle_info)
|
||||||
|
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (
|
""", (
|
||||||
branch_id, data['name'], data.get('rfc'), data.get('razon_social'),
|
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('cp'), data.get('email'), data.get('phone'),
|
||||||
data.get('address'), data.get('price_tier', 1),
|
data.get('address'), data.get('price_tier', 1),
|
||||||
data.get('credit_limit', 0),
|
data.get('credit_limit', 0),
|
||||||
|
data.get('max_discount_pct', 0),
|
||||||
json.dumps(data['vehicle_info']) if data.get('vehicle_info') else None
|
json.dumps(data['vehicle_info']) if data.get('vehicle_info') else None
|
||||||
))
|
))
|
||||||
customer_id = cur.fetchone()[0]
|
customer_id = cur.fetchone()[0]
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ MIGRATIONS = {
|
|||||||
"v4.2": "v4.2_meli_sync_queue.sql",
|
"v4.2": "v4.2_meli_sync_queue.sql",
|
||||||
"v4.3": "v4.3_facturapi.sql",
|
"v4.3": "v4.3_facturapi.sql",
|
||||||
"v4.4": "v4.4_workshop.sql",
|
"v4.4": "v4.4_workshop.sql",
|
||||||
|
"v4.5": "v4.5_customer_max_discount.sql",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
5
pos/migrations/v4.5_customer_max_discount.sql
Normal file
5
pos/migrations/v4.5_customer_max_discount.sql
Normal file
@@ -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;
|
||||||
@@ -349,11 +349,28 @@ def get_engines(master_conn, model_id, year_id, mye_ids=None):
|
|||||||
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 = ANY(%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, mye.id_mye
|
||||||
""", tuple(params))
|
""", tuple(params))
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
cur.close()
|
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):
|
def get_categories(master_conn, mye_id, allowed_brands=None):
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
brandId: null,
|
brandId: null,
|
||||||
model: null,
|
model: null,
|
||||||
modelId: null,
|
modelId: null,
|
||||||
|
modelVariantIds: null, // array of TecDoc model ids for grouped variants
|
||||||
year: null,
|
year: null,
|
||||||
yearId: null,
|
yearId: null,
|
||||||
engine: null,
|
engine: null,
|
||||||
@@ -63,7 +64,7 @@
|
|||||||
|
|
||||||
reset: function() {
|
reset: function() {
|
||||||
this.state = 'brands';
|
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._allBrands = [];
|
||||||
this._lastItems = [];
|
this._lastItems = [];
|
||||||
this._offset = 0;
|
this._offset = 0;
|
||||||
@@ -96,7 +97,7 @@
|
|||||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectBrand(' + JSON.stringify(this.nav.brand) + ',' + this.nav.brandId + ')\'>' + escapeHtml(this.nav.brand) + '</a>');
|
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectBrand(' + JSON.stringify(this.nav.brand) + ',' + this.nav.brandId + ')\'>' + escapeHtml(this.nav.brand) + '</a>');
|
||||||
}
|
}
|
||||||
if (this.nav.model) {
|
if (this.nav.model) {
|
||||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectModel(' + this.nav.modelId + ',' + JSON.stringify(this.nav.model) + ')\'>' + escapeHtml(this.nav.model) + '</a>');
|
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectModel(' + this.nav.modelId + ',' + JSON.stringify(this.nav.model) + ',' + JSON.stringify(this.nav.modelVariantIds || [this.nav.modelId]) + ')\'>' + escapeHtml(this.nav.model) + '</a>');
|
||||||
}
|
}
|
||||||
if (this.nav.year) {
|
if (this.nav.year) {
|
||||||
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectYear(' + this.nav.yearId + ',' + this.nav.year + ')\'>' + this.nav.year + '</a>');
|
parts.push('<a href="javascript:void(0)" class="breadcrumb__link" onclick=\'BrandCatalog.selectYear(' + this.nav.yearId + ',' + this.nav.year + ')\'>' + this.nav.year + '</a>');
|
||||||
@@ -202,7 +203,8 @@
|
|||||||
renderModelList: function(models) {
|
renderModelList: function(models) {
|
||||||
var html = '<div class="nav-grid">';
|
var html = '<div class="nav-grid">';
|
||||||
models.forEach(function(m) {
|
models.forEach(function(m) {
|
||||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectModel(' + m.id_model + ',' + JSON.stringify(m.display_name || m.name_model) + ')\'>' +
|
var variantIds = (m.variant_ids && m.variant_ids.length) ? m.variant_ids : [m.id_model];
|
||||||
|
html += '<div class="nav-card" onclick=\'BrandCatalog.selectModel(' + m.id_model + ',' + JSON.stringify(m.display_name || m.name_model) + ',' + JSON.stringify(variantIds) + ')\'>' +
|
||||||
'<div class="nav-card__name">' + escapeHtml(m.display_name || m.name_model) + '</div>' +
|
'<div class="nav-card__name">' + escapeHtml(m.display_name || m.name_model) + '</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
});
|
});
|
||||||
@@ -210,20 +212,22 @@
|
|||||||
this.setContent(html);
|
this.setContent(html);
|
||||||
},
|
},
|
||||||
|
|
||||||
selectModel: function(modelId, modelName) {
|
selectModel: function(modelId, modelName, variantIds) {
|
||||||
this.nav.model = modelName;
|
this.nav.model = modelName;
|
||||||
this.nav.modelId = modelId;
|
this.nav.modelId = modelId;
|
||||||
this.loadYears(modelId);
|
this.nav.modelVariantIds = variantIds && variantIds.length ? variantIds : [modelId];
|
||||||
|
this.loadYears(this.nav.modelVariantIds);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------- YEARS ----------
|
// ---------- YEARS ----------
|
||||||
loadYears: function(modelId) {
|
loadYears: function(modelIds) {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
this.state = 'years';
|
this.state = 'years';
|
||||||
this.setSearch('');
|
this.setSearch('');
|
||||||
this.buildBreadcrumb();
|
this.buildBreadcrumb();
|
||||||
var self = this;
|
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) {
|
.then(function(r) {
|
||||||
if (!self._checkAuth(r)) return null;
|
if (!self._checkAuth(r)) return null;
|
||||||
return r.json();
|
return r.json();
|
||||||
@@ -258,17 +262,18 @@
|
|||||||
selectYear: function(yearId, yearCar) {
|
selectYear: function(yearId, yearCar) {
|
||||||
this.nav.year = yearCar;
|
this.nav.year = yearCar;
|
||||||
this.nav.yearId = yearId;
|
this.nav.yearId = yearId;
|
||||||
this.loadEngines(this.nav.modelId, yearId);
|
this.loadEngines(this.nav.modelVariantIds || [this.nav.modelId], yearId);
|
||||||
},
|
},
|
||||||
|
|
||||||
// ---------- ENGINES ----------
|
// ---------- ENGINES ----------
|
||||||
loadEngines: function(modelId, yearId) {
|
loadEngines: function(modelIds, yearId) {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
this.state = 'engines';
|
this.state = 'engines';
|
||||||
this.setSearch('');
|
this.setSearch('');
|
||||||
this.buildBreadcrumb();
|
this.buildBreadcrumb();
|
||||||
var self = this;
|
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) {
|
.then(function(r) {
|
||||||
if (!self._checkAuth(r)) return null;
|
if (!self._checkAuth(r)) return null;
|
||||||
return r.json();
|
return r.json();
|
||||||
@@ -292,8 +297,9 @@
|
|||||||
renderEngineList: function(engines) {
|
renderEngineList: function(engines) {
|
||||||
var html = '<div class="nav-grid">';
|
var html = '<div class="nav-grid">';
|
||||||
engines.forEach(function(e) {
|
engines.forEach(function(e) {
|
||||||
html += '<div class="nav-card" onclick=\'BrandCatalog.selectEngine(' + e.id_mye + ',' + JSON.stringify(e.name_engine) + ')\'>' +
|
var name = (e.name_engine && e.name_engine !== 'N/A') ? e.name_engine : 'Sin especificar';
|
||||||
'<div class="nav-card__name">' + escapeHtml(e.name_engine) + '</div>' +
|
html += '<div class="nav-card" onclick=\'BrandCatalog.selectEngine(' + e.id_mye + ',' + JSON.stringify(name) + ')\'>' +
|
||||||
|
'<div class="nav-card__name">' + escapeHtml(name) + '</div>' +
|
||||||
'<div class="nav-card__sub">' + escapeHtml(e.trim_level || '') + '</div>' +
|
'<div class="nav-card__sub">' + escapeHtml(e.trim_level || '') + '</div>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -500,19 +500,26 @@
|
|||||||
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; }
|
||||||
|
|
||||||
|
// 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 only one engine, auto-select it
|
||||||
if (data.data.length === 1) {
|
if (data.data.length === 1) {
|
||||||
var e = data.data[0];
|
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();
|
loadCategoriesForMode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navGrid.className = 'nav-grid';
|
navGrid.className = 'nav-grid';
|
||||||
navGrid.innerHTML = data.data.map(function (e) {
|
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 '<div class="nav-card" role="listitem" data-mye-id="' + e.id_mye + '" data-name="' + esc(label) + '">' +
|
return '<div class="nav-card" role="listitem" data-mye-id="' + e.id_mye + '" data-name="' + esc(label) + '">' +
|
||||||
'<div class="nav-card__name">' + esc(e.name_engine) + '</div>' +
|
'<div class="nav-card__name">' + esc(name) + '</div>' +
|
||||||
(e.trim_level ? '<div class="nav-card__sub">' + esc(e.trim_level) + '</div>' : '') +
|
(e.trim_level ? '<div class="nav-card__sub">' + esc(e.trim_level) + '</div>' : '') +
|
||||||
'</div>';
|
'</div>';
|
||||||
}).join('');
|
}).join('');
|
||||||
@@ -1860,7 +1867,8 @@
|
|||||||
if (!engines) return;
|
if (!engines) return;
|
||||||
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
||||||
engines.map(function (e) {
|
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 '<option value="' + e.id_mye + '">' + esc(label) + '</option>';
|
return '<option value="' + e.id_mye + '">' + esc(label) + '</option>';
|
||||||
}).join('');
|
}).join('');
|
||||||
// If only 1 engine, auto-select
|
// If only 1 engine, auto-select
|
||||||
@@ -2101,7 +2109,8 @@
|
|||||||
if (!engines) return;
|
if (!engines) return;
|
||||||
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
vsEngine.innerHTML = '<option value="">Motor...</option>' +
|
||||||
engines.map(function (e) {
|
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 '<option value="' + e.id_mye + '">' + esc(elabel) + '</option>';
|
return '<option value="' + e.id_mye + '">' + esc(elabel) + '</option>';
|
||||||
}).join('');
|
}).join('');
|
||||||
vsEngine.disabled = false;
|
vsEngine.disabled = false;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
// The fetch handler normalizes static asset URLs (strips ?v= query strings)
|
||||||
// so templates can use cache-busting query params freely.
|
// so templates can use cache-busting query params freely.
|
||||||
|
|
||||||
const CACHE_NAME = 'nexus-pos-v18';
|
const CACHE_NAME = 'nexus-pos-v20';
|
||||||
|
|
||||||
const APP_SHELL = [
|
const APP_SHELL = [
|
||||||
'/pos/static/css/tokens.css',
|
'/pos/static/css/tokens.css',
|
||||||
|
|||||||
@@ -321,7 +321,7 @@
|
|||||||
<script src="/pos/static/js/splash-loader.js?v=1" defer></script>
|
<script src="/pos/static/js/splash-loader.js?v=1" defer></script>
|
||||||
<script src="/pos/static/js/pos-utils.js?v=2" defer></script>
|
<script src="/pos/static/js/pos-utils.js?v=2" defer></script>
|
||||||
<script src="/pos/static/js/sidebar.js" defer></script>
|
<script src="/pos/static/js/sidebar.js" defer></script>
|
||||||
<script src="/pos/static/js/catalog.js?v=7" defer></script>
|
<script src="/pos/static/js/catalog.js?v=8" defer></script>
|
||||||
<script src="/pos/static/js/offline-banner.js" defer></script>
|
<script src="/pos/static/js/offline-banner.js" defer></script>
|
||||||
<script src="/pos/static/js/chat.js" defer></script>
|
<script src="/pos/static/js/chat.js" defer></script>
|
||||||
<script src="/pos/static/js/sync-engine.js" defer></script>
|
<script src="/pos/static/js/sync-engine.js" defer></script>
|
||||||
@@ -341,6 +341,6 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<script src="/pos/static/js/pwa-install.js" defer></script>
|
<script src="/pos/static/js/pwa-install.js" defer></script>
|
||||||
<script src="/pos/static/js/brand-catalog.js?v=9" defer></script>
|
<script src="/pos/static/js/brand-catalog.js?v=10" defer></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
229
scripts/migrate_pz_to_tenant.py
Normal file
229
scripts/migrate_pz_to_tenant.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Migración de respaldo Punto Zero (MySQL) a tenant PostgreSQL de Nexus.
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
MYSQL_HOST=127.0.0.1 MYSQL_PORT=3307 MYSQL_DB=datos1 \
|
||||||
|
TENANT_DB_URL=postgresql://postgres@localhost/tenant_refaccionaria_la_casita \
|
||||||
|
BRANCH_ID=1 \
|
||||||
|
python3 scripts/migrate_pz_to_tenant.py
|
||||||
|
|
||||||
|
Requiere pymysql y psycopg2. Instalar con:
|
||||||
|
pip3 install --target /tmp/pylibs pymysql psycopg2-binary
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Librerías instaladas fuera del sistema porque el venv del proyecto no tiene pip
|
||||||
|
sys.path.insert(0, "/tmp/pylibs")
|
||||||
|
|
||||||
|
import pymysql
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import execute_values
|
||||||
|
|
||||||
|
MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
|
||||||
|
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3307"))
|
||||||
|
MYSQL_DB = os.getenv("MYSQL_DB", "datos1")
|
||||||
|
MYSQL_USER = os.getenv("MYSQL_USER", "root")
|
||||||
|
MYSQL_PASS = os.getenv("MYSQL_PASS", "")
|
||||||
|
|
||||||
|
PG_URL = os.getenv(
|
||||||
|
"TENANT_DB_URL",
|
||||||
|
"postgresql://postgres@localhost/tenant_refaccionaria_la_casita",
|
||||||
|
)
|
||||||
|
BRANCH_ID = int(os.getenv("BRANCH_ID", "1"))
|
||||||
|
|
||||||
|
|
||||||
|
def mysql_conn():
|
||||||
|
return pymysql.connect(
|
||||||
|
host=MYSQL_HOST,
|
||||||
|
port=MYSQL_PORT,
|
||||||
|
user=MYSQL_USER,
|
||||||
|
password=MYSQL_PASS,
|
||||||
|
db=MYSQL_DB,
|
||||||
|
charset="latin1",
|
||||||
|
cursorclass=pymysql.cursors.Cursor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pg_conn():
|
||||||
|
return psycopg2.connect(PG_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def clean(s):
|
||||||
|
if s is None:
|
||||||
|
return ""
|
||||||
|
return str(s).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_customers(mysql, pg):
|
||||||
|
cur = mysql.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT Clave, Nombre, RFC, Domicilio, Colonia, CP, Ciudad, Estado,
|
||||||
|
Telefono1, Email, LimiteCredito, Precio, Desc1, Suspendido
|
||||||
|
FROM clientes
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
for r in cur.fetchall():
|
||||||
|
name = clean(r[1])
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
address_parts = [clean(r[3]), clean(r[4]), clean(r[6]), clean(r[7])]
|
||||||
|
address = ", ".join([p for p in address_parts if p]) or None
|
||||||
|
phone = clean(r[8]) or None
|
||||||
|
email = clean(r[9]) or None
|
||||||
|
rfc = clean(r[2]) or None
|
||||||
|
cp = clean(r[5]) or None
|
||||||
|
credit_limit = float(r[10] or 0)
|
||||||
|
price_tier = int(r[11] if r[11] is not None else 1)
|
||||||
|
if price_tier not in (1, 2, 3):
|
||||||
|
price_tier = 1
|
||||||
|
max_discount = float(r[12] or 0)
|
||||||
|
is_active = int(r[13] or 0) == 0
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
BRANCH_ID,
|
||||||
|
name,
|
||||||
|
rfc,
|
||||||
|
None, # razon_social
|
||||||
|
None, # regimen_fiscal
|
||||||
|
None, # uso_cfdi
|
||||||
|
cp,
|
||||||
|
email,
|
||||||
|
phone,
|
||||||
|
address,
|
||||||
|
price_tier,
|
||||||
|
credit_limit,
|
||||||
|
0.0, # credit_balance
|
||||||
|
is_active,
|
||||||
|
None, # vehicle_info
|
||||||
|
max_discount,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print("No hay clientes para migrar.")
|
||||||
|
return
|
||||||
|
|
||||||
|
pgcur = pg.cursor()
|
||||||
|
execute_values(
|
||||||
|
pgcur,
|
||||||
|
"""
|
||||||
|
INSERT INTO customers (
|
||||||
|
branch_id, name, rfc, razon_social, regimen_fiscal, uso_cfdi,
|
||||||
|
cp, email, phone, address, price_tier, credit_limit,
|
||||||
|
credit_balance, is_active, vehicle_info, max_discount_pct
|
||||||
|
) VALUES %s
|
||||||
|
""",
|
||||||
|
rows,
|
||||||
|
)
|
||||||
|
pg.commit()
|
||||||
|
pgcur.close()
|
||||||
|
print(f"Migrados {len(rows)} clientes.")
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_inventory(mysql, pg):
|
||||||
|
cur = mysql.cursor()
|
||||||
|
cur.execute("SELECT Id, Marca FROM marcas")
|
||||||
|
brand_map = {row[0]: clean(row[1]) for row in cur.fetchall()}
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT p.Clave, p.Descrip, p.DescripDetallada, p.Costo, p.Precio1,
|
||||||
|
p.Precio2, p.Precio3, p.IVA, p.Marca, p.Linea, p.SubLinea,
|
||||||
|
p.UnidadMedida, p.Ubicacion, p.ClaveSAT, p.EnLista, p.Bloqueado
|
||||||
|
FROM productos p
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = []
|
||||||
|
for r in cur.fetchall():
|
||||||
|
sku = clean(r[0])
|
||||||
|
name = clean(r[1])
|
||||||
|
if not sku or not name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
cost = float(r[3] or 0)
|
||||||
|
price_1 = float(r[4] or 0)
|
||||||
|
price_2 = float(r[5] or 0)
|
||||||
|
price_3 = float(r[6] or 0)
|
||||||
|
iva = r[7]
|
||||||
|
tax_rate = float(iva) / 100.0 if iva else 0.16
|
||||||
|
brand = brand_map.get(r[8]) or None
|
||||||
|
is_active = int(r[14] or 0) == 1 and int(r[15] or 0) == 0
|
||||||
|
location = clean(r[12]) or None
|
||||||
|
unit = "PZA"
|
||||||
|
|
||||||
|
rows.append(
|
||||||
|
(
|
||||||
|
BRANCH_ID,
|
||||||
|
sku,
|
||||||
|
None, # barcode
|
||||||
|
name,
|
||||||
|
None, # description (se omite descripción detallada por instrucción del usuario)
|
||||||
|
None, # category_id
|
||||||
|
brand,
|
||||||
|
None, # vehicle_compatibility
|
||||||
|
unit,
|
||||||
|
cost,
|
||||||
|
price_1,
|
||||||
|
price_2,
|
||||||
|
price_3,
|
||||||
|
tax_rate,
|
||||||
|
0, # min_stock
|
||||||
|
0, # max_stock
|
||||||
|
location,
|
||||||
|
None, # image_url
|
||||||
|
is_active,
|
||||||
|
None, # catalog_part_id
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print("No hay productos para migrar.")
|
||||||
|
return
|
||||||
|
|
||||||
|
pgcur = pg.cursor()
|
||||||
|
execute_values(
|
||||||
|
pgcur,
|
||||||
|
"""
|
||||||
|
INSERT INTO inventory (
|
||||||
|
branch_id, part_number, barcode, name, description, category_id,
|
||||||
|
brand, vehicle_compatibility, unit, cost, price_1, price_2, price_3,
|
||||||
|
tax_rate, min_stock, max_stock, location, image_url, is_active,
|
||||||
|
catalog_part_id
|
||||||
|
) VALUES %s
|
||||||
|
""",
|
||||||
|
rows,
|
||||||
|
)
|
||||||
|
pg.commit()
|
||||||
|
pgcur.close()
|
||||||
|
print(f"Migrados {len(rows)} productos.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
mysql = mysql_conn()
|
||||||
|
pg = pg_conn()
|
||||||
|
pgcur = pg.cursor()
|
||||||
|
|
||||||
|
# Tenant está limpio; truncamos para partir de cero
|
||||||
|
pgcur.execute(
|
||||||
|
"TRUNCATE TABLE customers, inventory, inventory_stock RESTART IDENTITY CASCADE"
|
||||||
|
)
|
||||||
|
pg.commit()
|
||||||
|
pgcur.close()
|
||||||
|
print("Tablas destino truncadas.")
|
||||||
|
|
||||||
|
migrate_customers(mysql, pg)
|
||||||
|
migrate_inventory(mysql, pg)
|
||||||
|
|
||||||
|
mysql.close()
|
||||||
|
pg.close()
|
||||||
|
print("Migración completada.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user