From 3341b7aaab37774ffe1ef872da034c6f3da4895e Mon Sep 17 00:00:00 2001 From: Kworz Date: Tue, 19 Mar 2024 08:16:06 +0100 Subject: [PATCH] import articles --- src/lib/i18n/lang/en.json | 3 +- src/lib/i18n/lang/fr.json | 9 +- .../(scm)/scm/articles/import/+page.server.ts | 77 ++++++++++++++--- .../(scm)/scm/articles/import/+page.svelte | 85 ++++++++++++++----- 4 files changed, 137 insertions(+), 37 deletions(-) diff --git a/src/lib/i18n/lang/en.json b/src/lib/i18n/lang/en.json index d7fafd50..8718cd2e 100644 --- a/src/lib/i18n/lang/en.json +++ b/src/lib/i18n/lang/en.json @@ -85,7 +85,8 @@ "store_location": "Store location", "store_name": "Store name", "store": "Store", - "store_list_linked": "List linked store" + "store_list_linked": "List linked store", + "warning": "Warning" }, "time": { "hours": "Hours" diff --git a/src/lib/i18n/lang/fr.json b/src/lib/i18n/lang/fr.json index ed8c5b8b..ea463331 100644 --- a/src/lib/i18n/lang/fr.json +++ b/src/lib/i18n/lang/fr.json @@ -127,7 +127,11 @@ "false": "Non" }, "ok": "Ok", - "required_quantity": "Quantité nécéssaire" + "required_quantity": "Quantité nécéssaire", + "warning": "Attention", + "order_quantity": "Quantité de commande minimum", + "unit_of_work_quantity": "Quantité d'unité d'oeuvre", + "non_physical": "Article non physique" }, "time": { "hours": "Heures" @@ -377,5 +381,6 @@ }, "generic": "Une erreur est survenue. Consulter la console serveur pour avoir plus de détails.", "unauthed": "Action interdite, vous devez vous authentifier." - } + }, + "internal": "Article interne" } diff --git a/src/routes/app/(scm)/scm/articles/import/+page.server.ts b/src/routes/app/(scm)/scm/articles/import/+page.server.ts index 70241305..357b83fc 100644 --- a/src/routes/app/(scm)/scm/articles/import/+page.server.ts +++ b/src/routes/app/(scm)/scm/articles/import/+page.server.ts @@ -1,44 +1,95 @@ +import { unit_of_work } from "$lib/prisma-enums"; import type { scm_article } from "@prisma/client"; -import { redirect, type Actions } from "@sveltejs/kit"; +import { fail, redirect, type Actions } from "@sveltejs/kit"; export const actions: Actions = { import: async ({ request, locals }) => { + + const warnings = []; + try { // TODO: Check this const form = await request.formData(); const file = form.get("file"); - const columnOrder = form.get("columnOrder"); + const columnOrder = form.get("column_order"); + + const colSplitter = form.get("col_splitter")?.toString(); + const rowSplitter = form.get("row_splitter")?.toString(); if(!(file instanceof File)) - throw "article.import.file.missing"; + return fail(400, { import: { error: "errors.article.import.missing_file" }}); if(columnOrder === null) - throw "article.import.columnOrder.missing"; + return fail(500, { import: { error: "errors.article.import.column_order_invalid" }}); + + if(colSplitter === undefined || !([",", ";"].includes(colSplitter))) + return fail(400, { import: { error : "errors.article.import.col_splitter_invalid" }}); + + if(rowSplitter === undefined || !(["n", "rn"].includes(rowSplitter))) + return fail(400, { import: { error: "errors.article.import.row_splitter_invalid" }}); const fileContent = await file.text(); const columns = JSON.parse(columnOrder.toString()) as (string | null)[]; - const lines = fileContent.split("\r\n").map(line => line.split(",")); + console.log(columns); - for(const line of lines) + const lineSplitter = rowSplitter === "rn" ? "\r\n" : "\n"; + + const lines = fileContent.split(lineSplitter).map(line => line.split(colSplitter)); + + for(const line of lines.slice(1)) { - const article = { + + const nonPhysicalValue = line[columns.indexOf("non_physical")]; + const consumableValue = line[columns.indexOf("consumable")]; + const internalValue = line[columns.indexOf("internal")]; + + const article: Partial = { name: line[columns.indexOf("name")], - reference: line[columns.indexOf("reference")], - brand: line[columns.indexOf("manufacturer")], - - } satisfies Partial; + reference: line[columns.indexOf("sku")], + brand: line[columns.indexOf("brand")], + + order_quantity: Number(line[columns.indexOf("order_quantity")]) || undefined, + critical_quantity: Number(line[columns.indexOf("critical_quantity")]) || undefined, + + non_physical: nonPhysicalValue === "true" || nonPhysicalValue === "1", + consumable: consumableValue === "true" || consumableValue === "1", + internal: internalValue === "true" || internalValue === "1", + }; + + // Parse article unit + const articleUnit = line[columns.indexOf("unit_of_work")]; + if(articleUnit !== "") + { + if(Object.keys(unit_of_work).includes(articleUnit)) + { + article.unit = articleUnit as unit_of_work; + article.unit_quantity = Number(line[columns.indexOf("unit_of_work_quantity")]) || null; + } + else + { + warnings.push(`Found unit ${articleUnit} which is not compatible with ${Object.keys(unit_of_work).join(",")} for article ${article.reference}`); + } + } + + if(article.brand === undefined || article.name === undefined || article.reference === undefined) // skip rows with missing data + continue; + + console.log("dry-run", article); await locals.prisma.scm_article.create({ data: article }); } - - throw redirect(303, "/app/scm/articles"); + + if(warnings.length > 0) + return { import: { success: true, warnings }}; } catch(ex) { console.log(ex); return { error: true } } + + return redirect(303, "/app/scm/articles"); } } \ No newline at end of file diff --git a/src/routes/app/(scm)/scm/articles/import/+page.svelte b/src/routes/app/(scm)/scm/articles/import/+page.svelte index 6291391e..6861ada4 100644 --- a/src/routes/app/(scm)/scm/articles/import/+page.svelte +++ b/src/routes/app/(scm)/scm/articles/import/+page.svelte @@ -4,77 +4,120 @@ import Flex from "$lib/components/generics/layout/flex.svelte"; import Table from "$lib/components/generics/table/Table.svelte"; import TableCell from "$lib/components/generics/table/TableCell.svelte"; + import { _ } from "svelte-i18n"; + import type { ActionData } from "./$types"; + import Modal from "$lib/components/generics/modal/Modal.svelte"; + + export let form: ActionData; let file: File | undefined = undefined; let columns: (string | null)[] = []; - const availableColumns = ["Référence", "Désignation", "Fabricant"]; + let splitter: "," | ";" = ","; + let rowSplitter: "n" | "rn" = "n"; + + let importSuspense = false; - const parseCSV = async (file: File): Promise<{ headers: string[], lines: string[][]}> => { + const availableColumns = ["sku", "name", "brand", "non_physical", "consumable", "internal", "unit_of_work", "unit_of_work_quantity", "order_quantity", "critical_quantity"]; + + const parseCSV = async (file: File, splitter: "," | ";" = ",", lineSplitter: "n" | "rn" = "n"): Promise<{ headers: string[], lines: string[][]}> => { const text = await file.text(); - const lines = text.split("\r\n"); - const headers = lines[0].split(","); + const lineSplitterRegex = lineSplitter === "n" ? /\n/ : /\r\n/; + + const lines = text.split(lineSplitterRegex); + const headers = lines[0].split(splitter); // field splitter const headersLength = headers.length; const linesArray = []; for(const line of lines.slice(1)) { - const lineArray = line.split(","); - - console.log(lineArray.length, lineArray); + const lineArray = line.split(splitter); // field splitter if(lineArray.length === headersLength) linesArray.push(lineArray); - else if(lineArray.length > 0) - linesArray.push([...lineArray, ...Array(headersLength - linesArray.length).fill("") as string[]].slice(0, headersLength)); + else if(lineArray.length > headersLength) // trim excess columns + linesArray.push(lineArray.slice(0, headersLength)); + else if(lineArray.length < headersLength) // add null values underfilled columns + linesArray.push([...lineArray, ...Array(headersLength - lineArray.length).fill("") as string[]]); + } - columns = Array(headersLength).fill(null); + columns = Array(headersLength).fill(""); return { headers, lines: linesArray } } + Article — Import +{#if form?.import !== undefined && "error" in form.import} + form = null}> +

{$_(form.import.error)}

+
+{/if} + +{#if form?.import !== undefined && "warnings" in form.import} + form = null}> +
    + {#each form.import.warnings as warning} +
  • {warning}
  • + {/each} +
+
+ +
+
+{/if} +

Importer des articles

Importer des articles a l'aide d'un fichier CSV.

-
+ importSuspense = true}> + + + + + + + + + + { file = e.target?.files.item(0); }} /> - + {#if file !== undefined} - + {/if} -
{#if file !== undefined} - {#await parseCSV(file)} -

Parsing CSV file

+ {#await parseCSV(file, splitter)} +

Parsing CSV file...

{:then parsed} ({ label: h }))}> - {#each parsed.headers as _header, i} + {#each parsed.headers as header, i (header)} - - - {#each availableColumns.filter(ac => !(columns.filter((column, j) => i !== j).includes(ac))) as ac} - + + + + {#each availableColumns as ac} + {/each}