mirror of
https://github.com/hay-kot/homebox.git
synced 2025-07-03 17:18:36 +00:00
feat: import export rewrite (#290)
* WIP: initial work * refactoring * fix failing JS tests * update import docs * fix import headers * fix column headers * update refs on import * remove demo status * finnnneeeee * formatting
This commit is contained in:
parent
a005fa5b9b
commit
a6bcb36c5b
41 changed files with 1616 additions and 796 deletions
|
@ -5,6 +5,27 @@
|
|||
Import a CSV file containing your items, labels, and locations. See documentation for more information on the
|
||||
required format.
|
||||
</p>
|
||||
<div class="alert alert-warning shadow-lg mt-4">
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="stroke-current flex-shrink-0 h-6 w-6 mb-auto"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm">
|
||||
Behavior for imports with existing import_refs has changed. If an import_ref is present in the CSV file, the
|
||||
item will be updated with the values in the CSV file.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitCsvFile">
|
||||
<div class="flex flex-col gap-2 py-6">
|
||||
|
|
|
@ -4,27 +4,27 @@ import { UserClient } from "../../user";
|
|||
import { factories } from "../factories";
|
||||
|
||||
type ImportObj = {
|
||||
ImportRef: string;
|
||||
Location: string;
|
||||
Labels: string;
|
||||
Quantity: string;
|
||||
Name: string;
|
||||
Description: string;
|
||||
Insured: boolean;
|
||||
SerialNumber: string;
|
||||
ModelNumber: string;
|
||||
Manufacturer: string;
|
||||
Notes: string;
|
||||
PurchaseFrom: string;
|
||||
PurchasedPrice: number;
|
||||
PurchasedTime: string;
|
||||
LifetimeWarranty: boolean;
|
||||
WarrantyExpires: string;
|
||||
WarrantyDetails: string;
|
||||
SoldTo: string;
|
||||
SoldPrice: number;
|
||||
SoldTime: string;
|
||||
SoldNotes: string;
|
||||
[`HB.import_ref`]: string;
|
||||
[`HB.location`]: string;
|
||||
[`HB.labels`]: string;
|
||||
[`HB.quantity`]: number;
|
||||
[`HB.name`]: string;
|
||||
[`HB.description`]: string;
|
||||
[`HB.insured`]: boolean;
|
||||
[`HB.serial_number`]: string;
|
||||
[`HB.model_number`]: string;
|
||||
[`HB.manufacturer`]: string;
|
||||
[`HB.notes`]: string;
|
||||
[`HB.purchase_price`]: number;
|
||||
[`HB.purchase_from`]: string;
|
||||
[`HB.purchase_time`]: string;
|
||||
[`HB.lifetime_warranty`]: boolean;
|
||||
[`HB.warranty_expires`]: string;
|
||||
[`HB.warranty_details`]: string;
|
||||
[`HB.sold_to`]: string;
|
||||
[`HB.sold_price`]: number;
|
||||
[`HB.sold_time`]: string;
|
||||
[`HB.sold_notes`]: string;
|
||||
};
|
||||
|
||||
function toCsv(data: ImportObj[]): string {
|
||||
|
@ -36,7 +36,7 @@ function toCsv(data: ImportObj[]): string {
|
|||
}
|
||||
|
||||
function importFileGenerator(entries: number): ImportObj[] {
|
||||
const imports: ImportObj[] = [];
|
||||
const imports: Partial<ImportObj>[] = [];
|
||||
|
||||
const pick = (arr: string[]) => arr[Math.floor(Math.random() * arr.length)];
|
||||
|
||||
|
@ -45,37 +45,41 @@ function importFileGenerator(entries: number): ImportObj[] {
|
|||
|
||||
const half = Math.floor(entries / 2);
|
||||
|
||||
// YYYY-MM-DD
|
||||
const formatDate = (date: Date) => date.toISOString().split("T")[0];
|
||||
|
||||
for (let i = 0; i < entries; i++) {
|
||||
imports.push({
|
||||
ImportRef: faker.database.mongodbObjectId(),
|
||||
Location: pick(locations),
|
||||
Labels: labels,
|
||||
Quantity: faker.random.numeric(1),
|
||||
Name: faker.random.words(3),
|
||||
Description: "",
|
||||
Insured: faker.datatype.boolean(),
|
||||
SerialNumber: faker.random.alphaNumeric(5),
|
||||
ModelNumber: faker.random.alphaNumeric(5),
|
||||
Manufacturer: faker.random.alphaNumeric(5),
|
||||
Notes: "",
|
||||
PurchaseFrom: faker.name.fullName(),
|
||||
PurchasedPrice: faker.datatype.number(100),
|
||||
PurchasedTime: faker.date.past().toDateString(),
|
||||
LifetimeWarranty: half > i,
|
||||
WarrantyExpires: faker.date.future().toDateString(),
|
||||
WarrantyDetails: "",
|
||||
SoldTo: faker.name.fullName(),
|
||||
SoldPrice: faker.datatype.number(100),
|
||||
SoldTime: faker.date.past().toDateString(),
|
||||
SoldNotes: "",
|
||||
[`HB.import_ref`]: faker.database.mongodbObjectId(),
|
||||
[`HB.location`]: pick(locations),
|
||||
[`HB.labels`]: labels,
|
||||
[`HB.quantity`]: Number(faker.random.numeric(2)),
|
||||
[`HB.name`]: faker.random.words(3),
|
||||
[`HB.description`]: "",
|
||||
[`HB.insured`]: faker.datatype.boolean(),
|
||||
[`HB.serial_number`]: faker.random.alphaNumeric(5),
|
||||
[`HB.model_number`]: faker.random.alphaNumeric(5),
|
||||
[`HB.manufacturer`]: faker.random.alphaNumeric(5),
|
||||
[`HB.notes`]: "",
|
||||
[`HB.purchase_from`]: faker.name.fullName(),
|
||||
[`HB.purchase_price`]: faker.datatype.number(100),
|
||||
[`HB.purchase_time`]: faker.date.past().toDateString(),
|
||||
[`HB.lifetime_warranty`]: half > i,
|
||||
[`HB.warranty_details`]: "",
|
||||
[`HB.sold_to`]: faker.name.fullName(),
|
||||
[`HB.sold_price`]: faker.datatype.number(100),
|
||||
[`HB.sold_time`]: formatDate(faker.date.past()),
|
||||
[`HB.sold_notes`]: "",
|
||||
});
|
||||
}
|
||||
|
||||
return imports;
|
||||
return imports as ImportObj[];
|
||||
}
|
||||
|
||||
describe("group related statistics tests", () => {
|
||||
const TOTAL_ITEMS = 30;
|
||||
const labelData: Record<string, number> = {};
|
||||
const locationData: Record<string, number> = {};
|
||||
|
||||
let tAPI: UserClient | undefined;
|
||||
const imports = importFileGenerator(TOTAL_ITEMS);
|
||||
|
@ -97,10 +101,26 @@ describe("group related statistics tests", () => {
|
|||
const setupResp = await client.items.import(new Blob([csv], { type: "text/csv" }));
|
||||
|
||||
expect(setupResp.status).toBe(204);
|
||||
|
||||
for (const item of imports) {
|
||||
const labels = item[`HB.labels`].split(";");
|
||||
for (const label of labels) {
|
||||
if (labelData[label]) {
|
||||
labelData[label] += item[`HB.purchase_price`];
|
||||
} else {
|
||||
labelData[label] = item[`HB.purchase_price`];
|
||||
}
|
||||
}
|
||||
|
||||
const location = item[`HB.location`];
|
||||
if (locationData[location]) {
|
||||
locationData[location] += item[`HB.purchase_price`];
|
||||
} else {
|
||||
locationData[location] = item[`HB.purchase_price`];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Write to file system for debugging
|
||||
// fs.writeFileSync("test.csv", csv);
|
||||
test("Validate Group Statistics", async () => {
|
||||
const { status, data } = await api().stats.group();
|
||||
expect(status).toBe(200);
|
||||
|
@ -112,17 +132,6 @@ describe("group related statistics tests", () => {
|
|||
expect(data.totalWithWarranty).toEqual(Math.floor(TOTAL_ITEMS / 2));
|
||||
});
|
||||
|
||||
const labelData: Record<string, number> = {};
|
||||
const locationData: Record<string, number> = {};
|
||||
|
||||
for (const item of imports) {
|
||||
for (const label of item.Labels.split(";")) {
|
||||
labelData[label] = (labelData[label] || 0) + item.PurchasedPrice;
|
||||
}
|
||||
|
||||
locationData[item.Location] = (locationData[item.Location] || 0) + item.PurchasedPrice;
|
||||
}
|
||||
|
||||
test("Validate Labels Statistics", async () => {
|
||||
const { status, data } = await api().stats.labels();
|
||||
expect(status).toBe(200);
|
||||
|
|
|
@ -13,4 +13,10 @@ export class ActionsAPI extends BaseAPI {
|
|||
url: route("/actions/zero-item-time-fields"),
|
||||
});
|
||||
}
|
||||
|
||||
ensureImportRefs() {
|
||||
return this.http.post<void, ActionAmountResult>({
|
||||
url: route("/actions/ensure-import-refs"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,10 +45,10 @@
|
|||
Imports the standard CSV format for Homebox. This will <b>not</b> overwrite any existing items in your
|
||||
inventory. It will only add new items.
|
||||
</DetailAction>
|
||||
<!-- <DetailAction>
|
||||
<DetailAction @action="getExportTSV()">
|
||||
<template #title>Export Inventory</template>
|
||||
Exports the standard CSV format for Homebox. This will export all items in your inventory.
|
||||
</DetailAction> -->
|
||||
</DetailAction>
|
||||
</div>
|
||||
</BaseCard>
|
||||
<BaseCard>
|
||||
|
@ -68,6 +68,11 @@
|
|||
current asset_id field in the database and applying the next value to each item that has an unset asset_id
|
||||
field. This is done in order of the created_at field.
|
||||
</DetailAction>
|
||||
<DetailAction @action="ensureImportRefs">
|
||||
<template #title>Ensures Import Refs</template>
|
||||
Ensures that all items in your inventory have a valid import_ref field. This is done by randomly generating
|
||||
a 8 character string for each item that has an unset import_ref field.
|
||||
</DetailAction>
|
||||
<DetailAction @click="resetItemDateTimes">
|
||||
<template #title> Zero Item Date Times</template>
|
||||
Resets the time value for all date time fields in your inventory to the beginning of the date. This is to
|
||||
|
@ -103,7 +108,13 @@
|
|||
const notify = useNotifier();
|
||||
|
||||
function getBillOfMaterials() {
|
||||
api.reports.billOfMaterialsURL();
|
||||
const url = api.reports.billOfMaterialsURL();
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
function getExportTSV() {
|
||||
const url = api.items.exportURL();
|
||||
window.open(url, "_blank");
|
||||
}
|
||||
|
||||
async function ensureAssetIDs() {
|
||||
|
@ -125,6 +136,25 @@
|
|||
notify.success(`${result.data.completed} assets have been updated.`);
|
||||
}
|
||||
|
||||
async function ensureImportRefs() {
|
||||
const { isCanceled } = await confirm.open(
|
||||
"Are you sure you want to ensure all assets have an import_ref? This can take a while and cannot be undone."
|
||||
);
|
||||
|
||||
if (isCanceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await api.actions.ensureImportRefs();
|
||||
|
||||
if (result.error) {
|
||||
notify.error("Failed to ensure import refs.");
|
||||
return;
|
||||
}
|
||||
|
||||
notify.success(`${result.data.completed} assets have been updated.`);
|
||||
}
|
||||
|
||||
async function resetItemDateTimes() {
|
||||
const { isCanceled } = await confirm.open(
|
||||
"Are you sure you want to reset all date and time values? This can take a while and cannot be undone."
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue