Fix account dropdowns: vendor auto-select + sub-type filtering

Inventory vendor auto-select: match the dropdown off the Manufacturer
field (almost always populated and equal to the vendor for the shop's
distributors) instead of the AI's price-conditional vendorName, which was
only returned when a price was scraped. Centralizes the logic in a shared
inventory-vendor-match.js used by catalog lookup, AI lookup, label scan,
and manual entry; skips brands sold by multiple distributors (PPG, KP
Pigments) so those stay manual.

Account dropdowns filtered by sub-type now filter by parent AccountType,
so accounts a company classifies under a non-standard sub-type still
appear: Inventory account (Asset), AP account (Liability), pay-from/bank
and Bank Reconciliation pickers (Asset + Liability).

Deposit account is now a user-selectable dropdown on the Job and Quote
deposit modals (Asset + Liability accounts) instead of a silent auto-pick
of the first Checking/Cash account; falls back to the old behavior when
left blank, and validates the chosen account belongs to the company.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 09:26:52 -04:00
parent 8b9a3dff41
commit 687aedf7a4
15 changed files with 186 additions and 48 deletions
@@ -0,0 +1,67 @@
/**
* Shared vendor-dropdown auto-select for the Inventory Create/Edit forms.
*
* Why this exists: catalog lookup, AI lookup, label scan, and manual manufacturer entry
* all need to pick the right Vendor option, and they used to each carry their own copy of
* the matching logic. They disagreed on WHAT to match on — the AI path keyed off the
* price-derived `vendorName` (which is null unless a price was scraped), so the vendor
* only got selected "sometimes". This centralizes the rule:
*
* For ~95% of powders the manufacturer IS the vendor (Prismatic, Columbia,
* All Powder Paints, Tiger, Powder Buy The Pound). So match on the Manufacturer
* field first — it's almost always populated — and only fall back to the
* AI/catalog-supplied vendor name when the manufacturer is blank.
*
* Brands sold by more than one distributor (e.g. PPG, KP Pigments) are intentionally
* skipped so the user picks the vendor manually rather than getting a wrong guess.
*/
(function () {
'use strict';
// Brands carried by multiple distributors — never auto-pick a vendor for these.
// Lowercase; matched as a substring against the manufacturer name. Extend as needed.
const AMBIGUOUS_BRANDS = ['ppg', 'kp pigments', 'kp pigment'];
function normalize(s) {
return (s || '').toLowerCase().trim();
}
function isAmbiguousBrand(name) {
const n = normalize(name);
return n.length > 0 && AMBIGUOUS_BRANDS.some(b => n.includes(b));
}
/**
* Selects the vendor dropdown option that best matches a manufacturer/vendor name.
*
* @param {HTMLSelectElement} vendorSelect the #field-vendor element
* @param {string} manufacturerName primary name to match on (the Manufacturer field)
* @param {string} fallbackVendorName AI/catalog vendor name, used only if manufacturer is blank
* @param {{force?: boolean}} [opts] force=true overrides an existing selection (bad-match retry)
* @returns {boolean} true if a vendor option was selected.
*/
window.matchInventoryVendor = function (vendorSelect, manufacturerName, fallbackVendorName, opts) {
opts = opts || {};
if (!vendorSelect) return false;
// Don't clobber a choice the user (or a prior fill) already made, unless forcing a re-fill.
if (vendorSelect.value && !opts.force) return false;
// Manufacturer drives the match; the price-derived vendor name is only a fallback.
const name = normalize(manufacturerName) || normalize(fallbackVendorName);
if (!name) return false;
// Brands sold by multiple distributors stay manual — don't guess.
if (isAmbiguousBrand(name)) return false;
const match = Array.from(vendorSelect.options).find(function (o) {
const t = normalize(o.text);
// Skip the placeholder and the "Add new vendor" sentinel; require a real name to
// avoid spurious substring hits (e.g. empty option text matches everything).
if (!o.value || o.value === '__new__' || t.length < 3) return false;
return t.includes(name) || name.includes(t);
});
if (match) { vendorSelect.value = match.value; return true; }
return false;
};
})();