Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df791e0c28 | ||
|
|
feda1283d4 |
No files matched your search
@@ -14,7 +14,6 @@ type RowResult = {
|
|||||||
row: number;
|
row: number;
|
||||||
title: string;
|
title: string;
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
action?: "created" | "updated";
|
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,11 +91,9 @@ const ProductImportPanel = () => {
|
|||||||
<div className="px-6 py-4">
|
<div className="px-6 py-4">
|
||||||
<Text size="small" className="text-ui-fg-subtle">
|
<Text size="small" className="text-ui-fg-subtle">
|
||||||
Columnas: <code>titulo, descripcion, precio, categoria, coleccion, cantidad, fotos</code>. Varias fotos se
|
Columnas: <code>titulo, descripcion, precio, categoria, coleccion, cantidad, fotos</code>. Varias fotos se
|
||||||
separan con <code>;</code> dentro de la misma celda. Si <code>categoria</code>/<code>coleccion</code> no
|
separan con <code>;</code> dentro de la misma celda. <code>categoria</code>/<code>coleccion</code> deben
|
||||||
coinciden con una ya existente, se crean nuevas. Si <code>titulo</code> coincide con un producto ya
|
escribirse igual que en la tienda o se dejan vacías. Cada foto se descarga y se guarda en el almacenamiento
|
||||||
importado, no se duplica: se le agregan las fotos nuevas y se actualiza la descripción si la fila trae una.
|
propio de la tienda (no queda enlazada al sitio de origen).
|
||||||
Cada foto se descarga y se guarda en el almacenamiento propio de la tienda (no queda enlazada al sitio de
|
|
||||||
origen).
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -125,7 +122,7 @@ const ProductImportPanel = () => {
|
|||||||
<td className="py-2">{result.title}</td>
|
<td className="py-2">{result.title}</td>
|
||||||
<td className="py-2">
|
<td className="py-2">
|
||||||
{result.ok ? (
|
{result.ok ? (
|
||||||
<Badge color="green">{result.action === "updated" ? "Actualizado (ya existía)" : "Importado"}</Badge>
|
<Badge color="green">Importado</Badge>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-rose-600">{result.error}</span>
|
<span className="text-rose-600">{result.error}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
createInventoryItemsWorkflow,
|
createInventoryItemsWorkflow,
|
||||||
createProductCategoriesWorkflow,
|
createProductCategoriesWorkflow,
|
||||||
createProductsWorkflow,
|
createProductsWorkflow,
|
||||||
updateProductsWorkflow,
|
|
||||||
uploadFilesWorkflow,
|
uploadFilesWorkflow,
|
||||||
} from "@medusajs/medusa/core-flows";
|
} from "@medusajs/medusa/core-flows";
|
||||||
import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
|
import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
|
||||||
@@ -32,18 +31,6 @@ import { csvRowsToObjects, parseCsv } from "../../../lib/csv";
|
|||||||
// handle del producto: el default de Medusa no quita tildes/enies y
|
// handle del producto: el default de Medusa no quita tildes/enies y
|
||||||
// rompe la ruta publica.
|
// rompe la ruta publica.
|
||||||
//
|
//
|
||||||
// Re-importar el mismo titulo (ej. para agregar mas fotos o actualizar
|
|
||||||
// la descripcion de un producto ya cargado) actualiza el producto
|
|
||||||
// existente en vez de crear uno nuevo -- antes cada re-importacion
|
|
||||||
// generaba un producto duplicado con handle distinto (confirmado en
|
|
||||||
// vivo con "Hello Kitty & Friends": misma fila re-subida para sumar
|
|
||||||
// fotos, dos productos con el mismo titulo en la tienda). El match es
|
|
||||||
// por titulo exacto (case-insensitive/trim), igual que categoria y
|
|
||||||
// coleccion; en un match, las fotos nuevas se agregan a las que ya
|
|
||||||
// tenia (no se reemplazan) y la descripcion se actualiza si la fila
|
|
||||||
// trae una. No toca precio/cantidad/categoria/coleccion del producto
|
|
||||||
// existente -- eso se edita a mano en el admin si hace falta.
|
|
||||||
//
|
|
||||||
// Las fotos se descargan del origen y se re-alojan en el mismo
|
// Las fotos se descargan del origen y se re-alojan en el mismo
|
||||||
// MinIO/S3 que ya usa el resto del catalogo (uploadFilesWorkflow ->
|
// MinIO/S3 que ya usa el resto del catalogo (uploadFilesWorkflow ->
|
||||||
// mismo File Module Provider de medusa-config.ts, ya corre en el NAS).
|
// mismo File Module Provider de medusa-config.ts, ya corre en el NAS).
|
||||||
@@ -74,7 +61,6 @@ type RowResult = {
|
|||||||
row: number;
|
row: number;
|
||||||
title: string;
|
title: string;
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
action?: "created" | "updated";
|
|
||||||
error?: string;
|
error?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +150,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
|
|||||||
const salesChannelService = req.scope.resolve<ISalesChannelModuleService>(Modules.SALES_CHANNEL);
|
const salesChannelService = req.scope.resolve<ISalesChannelModuleService>(Modules.SALES_CHANNEL);
|
||||||
const productService = req.scope.resolve<IProductModuleService>(Modules.PRODUCT);
|
const productService = req.scope.resolve<IProductModuleService>(Modules.PRODUCT);
|
||||||
|
|
||||||
const [locations, shippingProfiles, salesChannels, categories, collections, existingProducts] = await Promise.all([
|
const [locations, shippingProfiles, salesChannels, categories, collections] = await Promise.all([
|
||||||
stockLocationService.listStockLocations({}),
|
stockLocationService.listStockLocations({}),
|
||||||
fulfillmentService.listShippingProfiles({}),
|
fulfillmentService.listShippingProfiles({}),
|
||||||
salesChannelService.listSalesChannels({}),
|
salesChannelService.listSalesChannels({}),
|
||||||
@@ -175,7 +161,6 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
|
|||||||
// los trae completos).
|
// los trae completos).
|
||||||
productService.listProductCategories({}, { select: ["id", "name"] }),
|
productService.listProductCategories({}, { select: ["id", "name"] }),
|
||||||
productService.listProductCollections({}, { select: ["id", "title"] }),
|
productService.listProductCollections({}, { select: ["id", "title"] }),
|
||||||
productService.listProducts({}, { select: ["id", "title"], relations: ["images"] }),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const locationId = locations[0]?.id;
|
const locationId = locations[0]?.id;
|
||||||
@@ -195,9 +180,6 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
|
|||||||
const collectionByName = new Map(
|
const collectionByName = new Map(
|
||||||
collections.filter((c) => c.title).map((c) => [c.title.toLowerCase().trim(), c.id]),
|
collections.filter((c) => c.title).map((c) => [c.title.toLowerCase().trim(), c.id]),
|
||||||
);
|
);
|
||||||
const productByTitle = new Map(
|
|
||||||
existingProducts.filter((p) => p.title).map((p) => [p.title.toLowerCase().trim(), p]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const results: RowResult[] = [];
|
const results: RowResult[] = [];
|
||||||
|
|
||||||
@@ -228,23 +210,6 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingProduct = productByTitle.get(title.toLowerCase());
|
|
||||||
if (existingProduct) {
|
|
||||||
await updateProductsWorkflow(req.scope).run({
|
|
||||||
input: {
|
|
||||||
products: [
|
|
||||||
{
|
|
||||||
id: existingProduct.id,
|
|
||||||
description: row.descripcion?.trim() || undefined,
|
|
||||||
images: [...(existingProduct.images ?? []).map((image) => ({ url: image.url })), ...uploadedFiles.map((f) => ({ url: f.url }))],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
results.push({ row: rowNumber, title, ok: true, action: "updated" });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const quantity = Number(row.cantidad);
|
const quantity = Number(row.cantidad);
|
||||||
const { result: inventoryItems } = await createInventoryItemsWorkflow(req.scope).run({
|
const { result: inventoryItems } = await createInventoryItemsWorkflow(req.scope).run({
|
||||||
input: {
|
input: {
|
||||||
@@ -318,7 +283,7 @@ export async function POST(req: MedusaRequest<{ csv: string }>, res: MedusaRespo
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
results.push({ row: rowNumber, title, ok: true, action: "created" });
|
results.push({ row: rowNumber, title, ok: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
results.push({ row: rowNumber, title: title || `Fila ${rowNumber}`, ok: false, error: (error as Error).message });
|
results.push({ row: rowNumber, title: title || `Fila ${rowNumber}`, ok: false, error: (error as Error).message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ spec:
|
|||||||
memory: 64Mi
|
memory: 64Mi
|
||||||
|
|
||||||
- name: migrations
|
- name: migrations
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.223
|
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.225
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
command:
|
command:
|
||||||
- node
|
- node
|
||||||
@@ -86,7 +86,7 @@ spec:
|
|||||||
|
|
||||||
containers:
|
containers:
|
||||||
- name: app
|
- name: app
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.223
|
image: gitea.cruzcloud.net/devops/ecommerce-medusa:v1.0.225
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ spec:
|
|||||||
runAsUser: 1001
|
runAsUser: 1001
|
||||||
containers:
|
containers:
|
||||||
- name: app
|
- name: app
|
||||||
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.219
|
image: gitea.cruzcloud.net/devops/ecommerce-frontend:v1.0.226
|
||||||
env:
|
env:
|
||||||
- name: HEADLESS_PROVIDER
|
- name: HEADLESS_PROVIDER
|
||||||
value: medusa
|
value: medusa
|
||||||
|
|||||||
Reference in new issue
Block a user