Administration

Import & Export

What Settings → Import / Export actually does for each entity, exactly how row validation and duplicate handling work, and the full mechanics of the bulk Purchase Order importer.

Where it lives

Both products have their own Import / Export tab, because each imports a different set of entities: Equipt Settings → Import / Export and Landscapt Settings → Import / Export. Each tab is self-contained — an Export Data panel of entity tiles at the top, an Import Data panel of entity tiles below it. There is no cross-product import screen; Vendors (shared between Equipt and Landscapt) are imported from the Equipt tab only.

On an org without Equipt enabled, the Equipt-only tiles (Work Orders, Assets, Vehicles, Parts) are hidden from both the export and import panels — only Vendors, Requisitions, Products, and Purchase Orders remain, since those are shared with the PO backbone.

Exporting data

Export is real, and it is genuinely a full data export, not a template stub — each tile downloads a CSV of every current record for that entity (not just headers), in the same column order and naming as that entity's import template. Clicking a tile with no records in it does nothing (the export handler returns early rather than downloading an empty file).

Money fields are converted from stored cents to a formatted dollar string on the way out (e.g. Parts' unit cost, Purchase Orders' grand total). Everything else is written as-is from the record.

There is no dedicated "Download Template" button in this panel — that's a separate, older component (ImportExportMenu, used on a few standalone list pages like Parts and Products) that isn't wired into the Settings Import/Export tab. In practice, an export doubles as the template: its headers match exactly what the matching import tile expects, so exporting an entity with a few existing rows and clearing the data is the fastest way to see the exact columns before a bulk import.

How an import actually runs

  1. Click an entity tile in the Import Data panel — this opens a file picker restricted to.csv.
  2. The app reads your CSV's header row and tries to auto-map every expected column against it: exact match first, then a case/punctuation-insensitive match, then a table of common aliases (e.g. a column literally titled "Part #", "SKU", or "Part No" all auto-map to partNumber; "Qty" or "On Hand" map to quantityOnHand).
  3. If every expected column matched automatically, it skips straight to the preview. If anything didn't match, a Map Columns dialog opens listing every expected field with a dropdown of your CSV's actual columns (or "Skip") — this dialog is effectively the field reference, since there's no separate template download.
  4. Fields marked with a red asterisk are required for that entity. You cannot continue past the mapping dialog until every required field has a column assigned.
  5. A preview step shows the first 5 mapped rows in a table and the total row count, plus an Import Error state if the file was empty, unreadable, or missing a required mapping. This is the only pre-commit check — there is no server-side dry run separate from this client-side preview.
  6. Clicking Import N Rows sends every mapped row to the entity's import handler at once. A success or error banner appears above the Export/Import panels when it finishes.

What you can import, by entity

Equipt (Settings → Equipt → Import / Export):

EntityRequired columnsOther columns the template accepts
Work OrderstitleworkOrderNumber, description, priority, status, category, assetName, assignedToName, dueDate, createdAt
Assetsname, assetTagequipmentNumber, assetType, make, model, year, serialNumber, location, status, purchaseVendorName, purchaseDate, purchasePrice, paymentMethod, financeInstitution
Vehiclesname, assetTagmake, model, year, licensePlate, vin, fuelType, status, assignedCrew, purchaseVendorName, purchaseDate, purchasePrice, paymentMethod, financeInstitution
Partsname, partNumberdescription, category, unitCost, quantityOnHand, minimumStock, vendorName, location
VendorsnamecontactName, email, phone, address, vendorType, website, notes
RequisitionstitlevendorName, notes
Productsname, partNumber, categorydescription, unitCost, price, quantityOnHand, vendorName, isInventory
Purchase Ordersnone enforced — see dedicated section belowPurchase Order #, Vendor, Status, Created On, Approved On, Completed On, Due Date, Line Type, Line Name, Part Number, Unit Cost, Ordered Quantity, Ordered Cost

Landscapt (Settings → Landscapt → Import / Export):

EntityRequired columnsOther columns the template accepts
ClientsdisplayNameaccountType, primaryPhone, primaryEmail, billingAddress/City/State/Zip, serviceAddress/City/State/Zip, source, accountNumber
LeadsdisplayNameaccountType, primaryPhone, primaryEmail, billingAddress/City/State/Zip, source
EstimatesclientName, descriptionestimateDate, validUntilDate, poNumber, stage
InvoicesclientName, description, amountinvoiceDate, dueDate, poNumber, status, taxAmount
PaymentsclientName, amountpaymentDate, method, reference, memo, invoiceNumber
TicketssubjectclientName, type, status, priority, category, body, dueDate
Servicesnamecode, category, unit, defaultRate, productionRate, isActive
Schedulesname, frequency, dayOfWeekweekPattern, anchorDate, seasonStart, seasonEnd, weekOfMonth
EmployeesfirstName, lastNameemail, phone, cellPhone, address, city, state, zip, dateHired, resourceCode, hourlyRate
Column names in this table are the internal field names the mapping dialog shows (in Title Case, e.g. partNumber renders as "Part Number"). Your CSV's actual header text almost never needs to match exactly — the alias matching described above handles the common variants.

Worked example: importing a batch of vendors

Say you're bringing over 40 vendors from a spreadsheet with columns Supplier, Contact, Phone, Email, and Type.

  1. Settings → Equipt → Import / Export → click the Vendors tile under Import Data.
  2. Upload the file. name is the only required field for Vendors, and "Supplier" doesn't auto-match it (it's not in the alias list), so the Map Columns dialog opens with Name still unmapped.
  3. Map Name → Supplier, Contact Name → Contact, Email → Email, Phone → Phone, Vendor Type → Type. Address, Website, and Notes have no matching column in your sheet, so leave them as "Skip" — they aren't required.
  4. Continue to Preview — confirm the first 5 rows look right, then click Import 40 Rows.
  5. Behind the scenes, rows import one at a time. Any row missing a name is silently dropped from the count entirely (not created, not reported as an error). For every remaining row, a vendor with that exact name already on file is updated in place with the new row's contact info, address, type, website, notes, and active flag — it does not create a duplicate vendor. Everything else inserts as a new vendor.

The same shape applies to Parts (keyed on partNumber instead of name) and most other entities — see the table in the next section for what's keyed on what.

Validation, duplicates, and what gets skipped

The FAQ's short version — "the importer validates each row before committing" — is really two separate mechanisms:

  • Required-field validation happens twice: once at the column-mapping step (you can't proceed if a required field has no column assigned), and again per-row at import time (a row whose required value is blank after trimming whitespace is dropped and counted as "skipped" — it never reaches the database, and it does not stop the rest of the batch).
  • Duplicate handling is not a validation failure — it's a merge. Rows are inserted one at a time; when the database rejects an insert because of a unique constraint (Postgres error code 23505), the importer treats that as "this record already exists" and updates the existing row instead, rather than erroring or creating a second copy.
  • Any other database error (not a duplicate-key violation) is not swallowed — it throws, which stops the mutation and surfaces the error message in the preview dialog's error banner. Rows already committed earlier in that same batch stay committed; rows after the failure are never attempted. There is no all-or-nothing transaction wrapping the whole CSV.
EntityMatched onOn match
Vendorsname (exact match, case-sensitive on lookup, org-scoped)Existing vendor is updated with the new row's contact info, address, type, website, notes, and active flag.
PartspartNumber (unique per org)Existing part is updated with the new row's name, description, category, cost, quantity, min stock, vendor name, and location.
ClientsaccountNumber, only when the row supplies oneExisting client is updated with the new row's contact and address fields. Clients without an account number always insert as new — the org auto-assigns one.
LeadsprimaryEmail or primaryPhone against every existing non-deleted clientNo new record is created. Instead the matched client is tagged “imported-lead” and the row is counted separately as “matched,” not “created.”
Purchase Orderspo_number (derived as PO-{year}-{csv PO #})The row is skipped outright — not merged, not counted as imported. Existing POs are never overwritten by a re-import.

Purchase Order import, in detail

Purchase Orders import differently from every other entity — it's the one importer built for a denormalized export format (multiple CSV rows per PO: one row per line item, repeating the PO-level fields on every row) rather than one row per record. No column is marked required in the UI, because the importer groups rows by Purchase Order # itself — a row with no PO number is silently dropped from the group before anything is grouped.

  1. All rows sharing the same Purchase Order # are grouped together; the first row in the group supplies the PO-level fields (Vendor, Status, Created On, Approved On, Completed On, Due Date).
  2. Vendor is matched by name (case-insensitive) against existing vendors; if no match exists, a new vendor is created on the fly from just that name.
  3. Rows are split by Line Type: PART lines become PO line items and drive the subtotal; PERCENT_TAXABLE lines set the tax rate (parsed out of the line name, e.g. "CT Sales Tax (6.35%)") and tax amount; AMOUNT_TAXABLE lines are summed into shipping cost. The PO's po_number is generated as PO-{year}-{csv PO #}, where the year comes from the CSV's Created On date, not today's date.
  4. If a PO with that exact generated number already exists, the whole PO (and all its lines) is skipped — not merged, not updated, not counted as imported. Re-running the same export is safe; it won't duplicate POs.
  5. For each PART line, the importer resolves a Products catalog entry by Part Number first, falling back to matching by item name only when no part number is given. If nothing matches either way, a new catalog entry is created (category defaults to maintenance_part). A line with neither a usable part number nor a name is skipped rather than inserted without a catalog reference — every PO line item must reference a Products catalog entry per this platform's data rules.
  6. When the resolved catalog entry has a part number, the importer also syncs it into CMMS Parts inventory: it links to an existing Part with that product_item_id or part_number if one exists, or creates a new Part record at zero quantity on hand otherwise. Quantity on hand is not incremented by a PO import — that only happens through the separate Goods Receipt flow.

The name-conflict audit trail, worked example

The Products catalog and Parts inventory each keep one name per part number — whichever name they saw first. If a later PO import uses a different name for a part number that's already on file, the importer does not overwrite the existing name and does not silently keep the newer one either — it leaves the catalog untouched and writes a name_conflict entry to that record's audit trail so the mismatch can be reviewed instead of lost.

Example. Your first PO import brings in part number OF-4521 labeled "Oil Filter" from Vendor A on PO-2024-1002. Six months later you import a second batch that includes the same part number, OF-4521, from Vendor B, but their line item is labeled "Premium Oil Filter" on PO-2024-1140. The catalog entry for OF-4521 stays "Oil Filter" — it is not renamed. Instead, an audit trail entry is written on that product (and on the linked Part, if one exists) reading something like: "Import name conflict on PO-2024-1140 (not applied): catalog has 'Oil Filter', import line used 'Premium Oil Filter'."

This is intentionally conservative: if the two names really do describe the same interchangeable part, no action is needed — the catalog is still correct and the PO line still links to the right record. If they turn out to be two genuinely different parts that happen to share a part number by data-entry mistake, the audit trail entry is what tells you to go split them apart rather than assuming a later import quietly relabeled something.

The same check runs independently for the Products catalog entry and, when one is linked, the CMMS Part record — so a single conflicting import line can produce up to two audit trail entries: one on the product, one on the part.

Limitations

  • CSV only. No Excel (.xlsx), JSON, or other format is accepted by either the file picker or the underlying parser.
  • No standalone template download in this panel. Use an export of the same entity (even with a couple of dummy rows deleted afterward) as your column reference, or rely on the Map Columns dialog, which lists every expected field.
  • No pre-commit server-side dry run. The 5-row preview table is a client-side rendering of your mapped CSV, not a validation pass against the database — duplicate and error handling only happens once you click Import.
  • Import only creates or merges — it never deletes. There is no way to remove records via CSV; re-importing an export you haven't edited is a safe no-op for entities that dedupe by a stable key, but it will not clean up records removed from the source system.
  • Requisitions import has no vendor auto-create logic the way Purchase Orders does — check your vendor names are already in the system (or import Vendors first) if you're bulk-loading requisitions with a vendorName column.
© 2026 Landscapt. All rights reserved.