diff --git a/README.md b/README.md index cae33ff..2f42be1 100644 --- a/README.md +++ b/README.md @@ -236,11 +236,11 @@ Addresses for an entity. Auth via company + entity ownership. | Method | Signature | Returns | | --- | --- | --- | | `list` | `(googleAuthId, companyId, entityId)` | `EntityAddress[]` | -| `create` | `(googleAuthId, companyId, entityId, { addressType, firstName?, name?, street?, streetNumber?, zip?, city?, county? })` | `EntityAddress` | +| `create` | `(googleAuthId, companyId, entityId, { addressType, firstName?, name?, street?, streetNumber?, zip?, city?, county?, sort? })` | `EntityAddress` | | `update` | `(googleAuthId, companyId, entityId, id, patch)` | `EntityAddress` | | `delete` | `(googleAuthId, companyId, entityId, id)` | `void` | -`addressType` required. Errors: `Company not found`, `Not authorized`, `Entity not found`, `Address type required`, `Address not found`. +`addressType` required. `sort` defaults to next index for the entity (`0…n`). List ordered by `sort`, `id`. Errors: `Company not found`, `Not authorized`, `Entity not found`, `Address type required`, `Address not found`, `Invalid sort`. --- @@ -250,12 +250,12 @@ Contacts for an entity (`contactType` e.g. `email`, `phone`). Auth via company + | Method | Signature | Returns | | --- | --- | --- | -| `list` | `(googleAuthId, companyId, entityId)` | `{ id, entityId, contactType, contact }[]` | -| `create` | `(googleAuthId, companyId, entityId, { contactType, contact })` | row | -| `update` | `(googleAuthId, companyId, entityId, id, { contactType?, contact? })` | row | +| `list` | `(googleAuthId, companyId, entityId)` | `{ id, entityId, contactType, contact, sort }[]` | +| `create` | `(googleAuthId, companyId, entityId, { contactType, contact, sort? })` | row | +| `update` | `(googleAuthId, companyId, entityId, id, { contactType?, contact?, sort? })` | row | | `delete` | `(googleAuthId, companyId, entityId, id)` | `void` | -Errors: `Company not found`, `Not authorized`, `Entity not found`, `Contact required`, `Contact not found`. +`sort` defaults to next index for the entity. List ordered by `sort`, `id`. Errors: `Company not found`, `Not authorized`, `Entity not found`, `Contact required`, `Contact not found`, `Invalid sort`. --- @@ -301,7 +301,6 @@ type DeliveryLine = { sku: string | null, amount: number, net: number, - transactedAmount: number, rebate: number, tax: number, // tax.rate comment: string | null, @@ -310,7 +309,7 @@ type DeliveryLine = { type Delivery = DeliverySummary & { lines: DeliveryLine[] } ``` -`create` inserts header + lines in one transaction. `supplierId` null or a Lieferanten `entity` of the company (else `Supplier not found`). Each line requires `sku` (resolved in the company category tree → `Article not found`), `amount`, `net`, `tax` (rate → `tax.ensure`); `rebate` defaults `0`; `transactedAmount` defaults to `amount`. Empty/invalid `number` or `date` → `Invalid delivery`; bad line payload → `Invalid lines`. +`create` inserts header + lines in one transaction. `supplierId` null or a Lieferanten `entity` of the company (else `Supplier not found`). Each line requires `sku` (resolved in the company category tree → `Article not found`), `amount`, `net`, `tax` (rate → `tax.ensure`); `rebate` defaults `0`. Empty/invalid `number` or `date` → `Invalid delivery`; bad line payload → `Invalid lines`. `update` patches header fields that are present. If `lines` is present, replace-all lines; if omitted, leave lines unchanged. Returns full `Delivery`. @@ -318,6 +317,23 @@ type Delivery = DeliverySummary & { lines: DeliveryLine[] } --- +## `ledger` + +Append-only SQL Server ledger table (`WITH (LEDGER = ON (APPEND_ONLY = ON))`). Rows cannot be updated or deleted. References `manifestarticle` with `ON DELETE NO ACTION` (a line with ledger entries cannot be removed). No store API yet — INSERT only via future store methods / direct SQL. + +| Column | Type | Notes | +| --- | --- | --- | +| `id` | `INT IDENTITY` | PK | +| `manifestarticle_id` | `INT` | FK → `manifestarticle` | +| `transaction_type` | `NVARCHAR(100)` | | +| `amount` | `FLOAT` | | +| `transaction_text` | `NVARCHAR(MAX)` | | +| `prev_hash` | `NVARCHAR(128)` | nullable (genesis row) | + +Engine also maintains `ledger_start_transaction_id` / `ledger_start_sequence_number`. `purge`/`reset-db` soft-drops the table (`MSSQL_DroppedLedgerTable_*` remains; a new `ledger` can still be created). + +--- + ## `posterminal` Company-scoped POS terminals. Auth via `resolveCompanyAccess`. diff --git a/generate-schema-graph.js b/generate-schema-graph.js index eb43386..d4d20d4 100644 --- a/generate-schema-graph.js +++ b/generate-schema-graph.js @@ -231,6 +231,62 @@ function edgeKey(edge) { return `${edge.from}|${edge.to}|${edge.label}|${edge.inferred ? 'inferred' : 'fk'}`; } +/** + * Only a spanning forest of FK edges constrains Graphviz ranks. Extra FKs + * from the same table get constraint=false so they may connect peers on the + * same level instead of forcing another rank (saves vertical space). + * + * Per source table, pick one "primary" parent for ranking: + * 1. real FK over inferred + * 2. ownership-style parent (parent name is a prefix of the child, e.g. + * manifest → manifestarticle) + * 3. highest in-degree target (hubs like company stay above) + */ +function selectRankingEdgeKeys(edges) { + const ranking = new Set(); + const inDegree = new Map(); + + for (const edge of edges) { + if (edge.from === edge.to) continue; + inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1); + } + + const ownershipScore = (child, parent) => { + const c = child.toLowerCase(); + const p = parent.toLowerCase(); + if (c.startsWith(p) && c.length > p.length) return p.length; + return 0; + }; + + const bySource = new Map(); + for (const edge of edges) { + if (edge.from === edge.to) continue; + if (!bySource.has(edge.from)) bySource.set(edge.from, []); + bySource.get(edge.from).push(edge); + } + + for (const group of bySource.values()) { + group.sort((a, b) => { + const aInferred = a.inferred ? 1 : 0; + const bInferred = b.inferred ? 1 : 0; + if (aInferred !== bInferred) return aInferred - bInferred; + + const aOwn = ownershipScore(a.from, a.to); + const bOwn = ownershipScore(b.from, b.to); + if (aOwn !== bOwn) return bOwn - aOwn; + + const aDeg = inDegree.get(a.to) ?? 0; + const bDeg = inDegree.get(b.to) ?? 0; + if (aDeg !== bDeg) return bDeg - aDeg; + + return a.label.localeCompare(b.label); + }); + ranking.add(edgeKey(group[0])); + } + + return ranking; +} + function assignEdgeColors(edges) { const colors = new Map(); let next = 0; @@ -309,7 +365,7 @@ const CLUSTERS = [ { id: 'commerce', label: 'Pricing & Documents', - tables: ['tax', 'price', 'manifest', 'manifestarticle'], + tables: ['tax', 'price', 'manifest', 'manifestarticle', 'ledger'], }, { id: 'units', label: 'Units', tables: ['unit'] }, ]; @@ -381,14 +437,142 @@ function parsePlainPositions(plain) { for (const line of plain.split('\n')) { if (!line.startsWith('node ')) continue; // node name x y width height ... - const match = line.match(/^node\s+(\S+)\s+([-\d.]+)\s+([-\d.]+)\s+/); + const match = line.match( + /^node\s+(\S+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)/ + ); if (!match) continue; - positions.set(match[1], { x: Number(match[2]), y: Number(match[3]) }); + positions.set(match[1], { + x: Number(match[2]), + y: Number(match[3]), + width: Number(match[4]), + height: Number(match[5]), + }); } return positions; } +function flipCompassLR(compass) { + return ( + { e: 'w', w: 'e', ne: 'nw', nw: 'ne', se: 'sw', sw: 'se' }[compass] ?? + compass + ); +} + +function flipCompassNS(compass) { + return ( + { ne: 'se', se: 'ne', nw: 'sw', sw: 'nw', e: 'e', w: 'w' }[compass] ?? + compass + ); +} + +/** + * Greedy exit/port tweaks against real Graphviz splines. Tries L/R flips, + * N/S diagonal flips, and adjacent head-port swaps; keeps moves that cut + * SVG segment crossings. + */ +function optimizeAnchorsWithLayout(graphviz, tables, edges, positions, anchors) { + const tailCompass = new Map(anchors.tailCompass); + const headPort = new Map(anchors.headPort); + const inboundCounts = anchors.inboundCounts; + let current = { headPort, tailCompass, inboundCounts }; + + const measure = () => { + const dot = buildDot(tables, edges, positions, current); + const svg = fixSelfLoopSvg(graphviz.layout(dot, 'svg', 'dot')); + return { svg, crossings: countSvgCrossings(svg), dot }; + }; + + let { crossings: best, svg: bestSvg, dot: bestDot } = measure(); + + const acceptIfBetter = (trial) => { + if (trial.crossings < best) { + best = trial.crossings; + bestSvg = trial.svg; + bestDot = trial.dot; + return true; + } + return false; + }; + + const trySeed = (seedCompass) => { + const saved = new Map(tailCompass); + for (const [k, v] of seedCompass) tailCompass.set(k, v); + current = { headPort, tailCompass, inboundCounts }; + const trial = measure(); + if (!acceptIfBetter(trial)) { + for (const [k, v] of saved) tailCompass.set(k, v); + current = { headPort, tailCompass, inboundCounts }; + } + }; + + // Also try the globally mirrored L/R assignment as an alternate seed. + trySeed( + new Map([...tailCompass.entries()].map(([k, v]) => [k, flipCompassLR(v)])) + ); + + const byTarget = new Map(); + for (const edge of edges) { + if (edge.from === edge.to) continue; + if (!byTarget.has(edge.to)) byTarget.set(edge.to, []); + byTarget.get(edge.to).push(edge); + } + + let improved = true; + for (let sweep = 0; sweep < 4 && improved; sweep++) { + improved = false; + + for (const edge of edges) { + if (edge.from === edge.to) continue; + const key = edgeKey(edge); + const prev = tailCompass.get(key); + if (!prev) continue; + + for (const flip of [flipCompassLR, flipCompassNS]) { + const next = flip(prev); + if (next === prev) continue; + tailCompass.set(key, next); + current = { headPort, tailCompass, inboundCounts }; + const trial = measure(); + if (acceptIfBetter(trial)) { + improved = true; + break; + } + tailCompass.set(key, prev); + current = { headPort, tailCompass, inboundCounts }; + } + } + + for (const group of byTarget.values()) { + const keys = group + .map((e) => edgeKey(e)) + .sort( + (a, b) => + Number(String(headPort.get(a)).replace(/^in/, '')) - + Number(String(headPort.get(b)).replace(/^in/, '')) + ); + for (let i = 0; i < keys.length - 1; i++) { + const a = headPort.get(keys[i]); + const b = headPort.get(keys[i + 1]); + if (!a || !b || a === b) continue; + headPort.set(keys[i], b); + headPort.set(keys[i + 1], a); + current = { headPort, tailCompass, inboundCounts }; + const trial = measure(); + if (acceptIfBetter(trial)) { + improved = true; + } else { + headPort.set(keys[i], a); + headPort.set(keys[i + 1], b); + current = { headPort, tailCompass, inboundCounts }; + } + } + } + } + + return { anchors: current, crossings: best, svg: bestSvg, dot: bestDot }; +} + /** * Sort cluster members by their first-pass x so Graphviz's crossing * minimizer starts from a better initial order within each cluster. @@ -468,7 +652,8 @@ function buildTableLabel(table, inboundCount = 0) { * Head: unique in* ports on the target title bar, ordered by source x. * Tail: FK column port, always leaving sideways or diagonally * (e/w/ne/nw/se/sw) — never straight up/down, which stacks stubs on the - * row edge. + * row edge. Exit side aims at the head-port x (not just the target center) + * so left-header arrivals leave west and right-header arrivals leave east. */ function assignEdgeAnchors(edges, positions) { const headPort = new Map(); @@ -516,10 +701,24 @@ function assignEdgeAnchors(edges, positions) { return; } - const fromX = positions?.get(edge.from)?.x ?? 0; - const toX = positions?.get(edge.to)?.x ?? fromX; - const fromY = positions?.get(edge.from)?.y ?? 0; - const toY = positions?.get(edge.to)?.y ?? fromY; + const from = positions?.get(edge.from); + const to = positions?.get(edge.to); + const fromX = from?.x ?? 0; + const fromY = from?.y ?? 0; + const toY = to?.y ?? fromY; + + // Aim at the specific header port, not the node center — otherwise + // edges to the left/right of a wide header pick the wrong exit side. + const key = edgeKey(edge); + const hp = headPort.get(key) ?? 'in0'; + const hi = Number(String(hp).replace(/^in/, '')) || 0; + const nIn = Math.max(1, inboundCounts.get(edge.to) ?? 1); + const toW = to?.width ?? 1; + const toX = + to != null + ? to.x - toW / 2 + ((hi + 0.5) / nIn) * toW + : fromX; + const dx = toX - fromX; const dy = toY - fromY; const absDx = Math.abs(dx); @@ -560,7 +759,7 @@ function assignEdgeAnchors(edges, positions) { : 'nw'; } - tailCompass.set(edgeKey(edge), compass); + tailCompass.set(key, compass); }); } @@ -592,18 +791,17 @@ function buildLegendLabel() { ); } -function buildDot(tables, edges, positions = null) { +function buildDot(tables, edges, positions = null, anchors = null) { const edgeColors = assignEdgeColors(edges); - const { headPort, tailCompass, inboundCounts } = assignEdgeAnchors( - edges, - positions - ); + const rankingEdgeKeys = selectRankingEdgeKeys(edges); + const { headPort, tailCompass, inboundCounts } = + anchors ?? assignEdgeAnchors(edges, positions); const lines = [ 'digraph schema {', // TB = landscape for this schema (many tables per rank spread horizontally). // mclimit bumps Graphviz's own crossing-minimization iterations. ` graph [rankdir=TB, splines=spline, overlap=false, bgcolor="${COLORS.background}",`, - ' nodesep=0.55, ranksep=1.0, pad=0.5, fontname="Helvetica",', + ' nodesep=0.95, ranksep=1.0, pad=0.5, fontname="Helvetica",', ' compound=true, newrank=true, mclimit=4.0];', ' node [shape=plaintext, fontname="Helvetica"];', ' edge [fontname="Helvetica", fontsize=9, penwidth=1.4, arrowsize=0.7];', @@ -673,12 +871,21 @@ function buildDot(tables, edges, positions = null) { // of those rows (e→e). Node-level ne→nw looked above the table but // detached from the actual columns; column→header with :ne still dipped SE. if (edge.from === edge.to) { + attrs.push('constraint=false'); lines.push( ` ${edge.from}:${sourcePort}:e -> ${edge.to}:id:e [${attrs.join(', ')}];` ); continue; } + // Non-primary FKs may share a rank with their target (lateral links). + if (!rankingEdgeKeys.has(key)) { + attrs.push('constraint=false', 'weight=1'); + } else { + // Prefer ranking edges in crossing minimization / length tradeoffs. + attrs.push('weight=3'); + } + lines.push( ` ${edge.from}:${sourcePort}:${tail} -> ${edge.to}:${targetPort}:n [${attrs.join(', ')}];` ); @@ -692,14 +899,16 @@ function countSvgCrossings(svg) { const paths = [ ...svg.matchAll(/]*d="([^"]+)"/g), ].map((m) => m[1]); - const segs = []; + const edgeSegs = []; for (const d of paths) { const nums = [...d.matchAll(/([-\d.]+)[,\s]+([-\d.]+)/g)].map((m) => [ Number(m[1]), Number(m[2]), ]); + const segs = []; for (let i = 0; i < nums.length - 1; i++) segs.push([nums[i], nums[i + 1]]); + edgeSegs.push(segs); } const orient = (a, b, c) => @@ -713,19 +922,26 @@ function countSvgCrossings(svg) { }; let n = 0; - for (let i = 0; i < segs.length; i++) { - for (let j = i + 1; j < segs.length; j++) { - const [a, b] = segs[i]; - const [c, d] = segs[j]; - if ( - (a[0] === c[0] && a[1] === c[1]) || - (a[0] === d[0] && a[1] === d[1]) || - (b[0] === c[0] && b[1] === c[1]) || - (b[0] === d[0] && b[1] === d[1]) - ) { - continue; + for (let e1 = 0; e1 < edgeSegs.length; e1++) { + for (let e2 = e1 + 1; e2 < edgeSegs.length; e2++) { + let pairCrosses = false; + outer: for (const [a, b] of edgeSegs[e1]) { + for (const [c, d] of edgeSegs[e2]) { + if ( + (a[0] === c[0] && a[1] === c[1]) || + (a[0] === d[0] && a[1] === d[1]) || + (b[0] === c[0] && b[1] === c[1]) || + (b[0] === d[0] && b[1] === d[1]) + ) { + continue; + } + if (crosses(a, b, c, d)) { + pairCrosses = true; + break outer; + } + } } - if (crosses(a, b, c, d)) n++; + if (pairCrosses) n++; } } @@ -805,30 +1021,35 @@ if (unclustered.length > 0) { const graphviz = await Graphviz.load(); -// Two layout passes: learn positions, then reorder columns/cluster members -// (barycenter) so FK ports and neighbor tables line up and crossings drop. +// Layout passes: learn positions, reorder columns/cluster members (barycenter). let positions = null; -for (let pass = 0; pass < 2; pass++) { +for (let pass = 0; pass < 3; pass++) { const probeDot = buildDot(tables, edges, positions); const plain = graphviz.layout(probeDot, 'plain', 'dot'); positions = parsePlainPositions(plain); reorderColumnsForCrossingReduction(tables, edges, positions); } -const dot = buildDot(tables, edges, positions); -writeFileSync(dotOutputPath, dot, 'utf8'); +// Hill-climb L/R exits against real spline crossings. +const baseAnchors = assignEdgeAnchors(edges, positions); +const optimized = optimizeAnchorsWithLayout( + graphviz, + tables, + edges, + positions, + baseAnchors +); -const svg = fixSelfLoopSvg(graphviz.layout(dot, 'svg', 'dot')); -writeFileSync(outputPath, svg, 'utf8'); +writeFileSync(dotOutputPath, optimized.dot, 'utf8'); +writeFileSync(outputPath, optimized.svg, 'utf8'); -const dim = svg.match(/width="([\d.]+)pt" height="([\d.]+)pt"/); -const crossings = countSvgCrossings(svg); +const dim = optimized.svg.match(/width="([\d.]+)pt" height="([\d.]+)pt"/); console.log(`Parsed ${tables.size} tables and ${edges.length} foreign keys`); console.log(`Written ${dotOutputPath}`); console.log(`Written ${outputPath}`); if (dim) { console.log( - `Layout ${dim[1]}pt × ${dim[2]}pt · ~${crossings} segment crossings` + `Layout ${dim[1]}pt × ${dim[2]}pt · ~${optimized.crossings} edge-pair crossings` ); } \ No newline at end of file diff --git a/schema-graph.dot b/schema-graph.dot index d8b2770..6e3ae78 100644 --- a/schema-graph.dot +++ b/schema-graph.dot @@ -1,6 +1,6 @@ digraph schema { graph [rankdir=TB, splines=spline, overlap=false, bgcolor="#ebf4ff", - nodesep=0.55, ranksep=1.0, pad=0.5, fontname="Helvetica", + nodesep=0.95, ranksep=1.0, pad=0.5, fontname="Helvetica", compound=true, newrank=true, mclimit=4.0]; node [shape=plaintext, fontname="Helvetica"]; edge [fontname="Helvetica", fontsize=9, penwidth=1.4, arrowsize=0.7]; @@ -10,59 +10,60 @@ digraph schema { style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; customergroup [label=<
customergroup
idINT PK
company_id?INT FK
nameNVARCHAR(255)
>]; - entityaddress [label=<
entityaddress
idINT PK
entity_idINT FK
first_name?NVARCHAR(255)
name?NVARCHAR(255)
street?NVARCHAR(255)
streetnumber?NVARCHAR(50)
zip?NVARCHAR(50)
city?NVARCHAR(255)
county?NVARCHAR(255)
addresstypeNVARCHAR(50)
>]; - numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; - entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
>]; - company [label=<
company
idINT PK
default_language_id?INT FK
root_category_id?INT FK
poscustomergroup_idINT FK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
nameNVARCHAR(255)
>]; - syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; - entitycontact [label=<
entitycontact
idINT PK
entity_idINT FK
contacttypeNVARCHAR(50)
contactNVARCHAR(255)
>]; meta [label=<
meta
keyNVARCHAR(255)
valueNVARCHAR(MAX)
>]; - companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
>]; - entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; + numbers [label=<
numbers
idINT PK
company_idINT FK
typeNVARCHAR(100)
prefixNVARCHAR(50)
paddingINT
nextINT
>]; + entityaddress [label=<
entityaddress
idINT PK
entity_idINT FK
first_name?NVARCHAR(255)
name?NVARCHAR(255)
street?NVARCHAR(255)
streetnumber?NVARCHAR(50)
zip?NVARCHAR(50)
city?NVARCHAR(255)
county?NVARCHAR(255)
addresstypeNVARCHAR(50)
sortINT
>]; + entity [label=<
entity
idINT PK
customergroup_idINT FK
first_name?NVARCHAR(255)
nameNVARCHAR(255)
created_atDATETIME2
updated_atDATETIME2
>]; companyuser [label=<
companyuser
idINT PK
company_idINT FK
google_auth_idINT FK
>]; + entitycontact [label=<
entitycontact
idINT PK
entity_idINT FK
contacttypeNVARCHAR(50)
contactNVARCHAR(255)
sortINT
>]; + company [label=<
company
idINT PK
taxrate?INT FK
reducedtaxrate?INT FK
taxfreerate?INT FK
poscustomergroup_idINT FK
root_category_id?INT FK
default_language_id?INT FK
nameNVARCHAR(255)
>]; + syncpos [label=<
syncpos
idINT PK
company_idINT FK
paircodeNVARCHAR(50)
token?NVARCHAR(255)
>]; + entityhistory [label=<
entityhistory
idINT PK
entity_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; + companysetting [label=<
companysetting
idINT PK
company_idINT FK
keyNVARCHAR(255)
valueNVARCHAR(MAX)
frontendBIT
>]; google_auth [label=<
google_auth
idINT PK
selected_company?INT FK
emailNVARCHAR(320)
picture?NVARCHAR(2048)
picturebinary?VARBINARY(MAX)
sub?NVARCHAR(255)
created_atDATETIME2
>]; } subgraph cluster_i18n { label="Localization"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - unittext [label=<
unittext
idINT PK
language_idINT FK
unit_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; language [label=<
language
idINT PK
iso639NVARCHAR(10)
>]; - categorytext [label=<
categorytext
idINT PK
language_idINT FK
category_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; - articletext [label=<
articletext
idINT PK
language_idINT FK
article_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + unittext [label=<
unittext
idINT PK
language_idINT FK
unit_idINT FK
symbolNVARCHAR(50)
nameNVARCHAR(255)
>]; + articletext [label=<
articletext
idINT PK
article_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; + categorytext [label=<
categorytext
idINT PK
category_idINT FK
language_idINT FK
nameNVARCHAR(255)
slugNVARCHAR(255)
seo?NVARCHAR(MAX)
desc?NVARCHAR(MAX)
>]; } subgraph cluster_catalog { label="Catalog"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - articlelistarticle [label=<
articlelistarticle
idINT PK
articlelist_idINT FK
article_idINT FK
countINT
sortINT
>]; - articlelist [label=<
articlelist
idINT PK
>]; - article [label=<
article
idINT PK
unit_idINT FK
articlelist_id?INT FK
fatherarticle_id?INT FK
optionslist_id?INT FK
brand_id?INT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; - articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; - optionslist [label=<
optionslist
idINT PK
>]; - optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; - articlehistory [label=<
articlehistory
idINT PK
article_idINT FK
companyuser_id?INT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; brand [label=<
brand
idINT PK
company_idINT FK
nameNVARCHAR(255)
gsprpinfo?NVARCHAR(MAX)
>]; + articlehistory [label=<
articlehistory
idINT PK
companyuser_id?INT FK
article_idINT FK
dateDATETIME2
textNVARCHAR(MAX)
>]; + article [label=<
article
idINT PK
brand_id?INT FK
fatherarticle_id?INT FK
articlelist_id?INT FK
optionslist_id?INT FK
unit_idINT FK
sku?NVARCHAR(100)
unitamountFLOAT
weightFLOAT
is_fatherBIT
fathersortINT
created_atDATETIME2
updated_atDATETIME2
>]; + articleean [label=<
articleean
idINT PK
article_idINT FK
eanNVARCHAR(50)
>]; category [label=<
category
idINT PK
parent_id?INT FK
sortINT
>]; + articlelist [label=<
articlelist
idINT PK
>]; categoryarticle [label=<
categoryarticle
idINT PK
article_idINT FK
category_idINT FK
>]; + articlelistarticle [label=<
articlelistarticle
idINT PK
article_idINT FK
articlelist_idINT FK
countINT
sortINT
>]; + optionslistarticle [label=<
optionslistarticle
idINT PK
article_idINT FK
optionslist_idINT FK
countINT
sortINT
mandatedBIT
>]; + optionslist [label=<
optionslist
idINT PK
>]; } subgraph cluster_media { label="Media"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - brandpic [label=<
brandpic
idINT PK
brand_idINT FK
pic_idINT FK
sortINT
>]; - articlepic [label=<
articlepic
idINT PK
article_idINT FK
pic_idINT FK
sortINT
>]; pic [label=<
pic
idINT PK
wINT
hINT
sizeBIGINT
hashNVARCHAR(64)
thumbwINT
thumbhINT
thumbhashNVARCHAR(64)
sourceNVARCHAR(MAX)
fullbinaryVARBINARY(MAX)
thumbbinaryVARBINARY(MAX)
>]; - categorypic [label=<
categorypic
idINT PK
category_idINT FK
pic_idINT FK
sortINT
>]; + articlepic [label=<
articlepic
idINT PK
pic_idINT FK
article_idINT FK
sortINT
>]; + brandpic [label=<
brandpic
idINT PK
pic_idINT FK
brand_idINT FK
sortINT
>]; + categorypic [label=<
categorypic
idINT PK
pic_idINT FK
category_idINT FK
sortINT
>]; } subgraph cluster_commerce { label="Pricing & Documents"; labeljust="l"; fontsize=15; style="rounded,filled"; fillcolor="#ffffff60"; color="#a0aec0"; fontcolor="#1a365d"; margin=14; - tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; - price [label=<
price
idINT PK
article_idINT FK
customergroup_idINT FK
tax_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + ledger [label=<
ledger
idINT PK
manifestarticle_idINT FK
transaction_typeNVARCHAR(100)
amountFLOAT
transaction_textNVARCHAR(MAX)
prev_hash?NVARCHAR(128)
>]; + manifestarticle [label=<
manifestarticle
idINT PK
manifest_idINT FK
tax_idINT FK
article_id?INT FK
amountFLOAT
netFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; manifest [label=<
manifest
idINT PK
entity_id?INT FK
company_idINT FK
numberNVARCHAR(100)
typeNVARCHAR(100)
dateDATETIME2
>]; - manifestarticle [label=<
manifestarticle
idINT PK
article_id?INT FK
tax_idINT FK
manifest_idINT FK
amountFLOAT
netFLOAT
transactedamountFLOAT
comment?NVARCHAR(MAX)
rebateFLOAT
>]; + price [label=<
price
idINT PK
tax_idINT FK
customergroup_idINT FK
article_idINT FK
minmultiplierDECIMAL(18,4)
netFLOAT
>]; + tax [label=<
tax
idINT PK
rateDECIMAL(18,4)
>]; } subgraph cluster_units { label="Units"; labeljust="l"; fontsize=15; @@ -70,67 +71,68 @@ digraph schema { fontcolor="#1a365d"; margin=14; unit [label=<
unit
idINT PK
company_idINT FK
siNVARCHAR(50)
basesiNVARCHAR(50)
basesifactorFLOAT
>]; } - posterminalcategory [label=<
posterminalcategory
idINT PK
category_idINT FK
posterminal_idINT FK
>]; posterminal [label=<
posterminal
idINT PK
company_idINT FK
nameNVARCHAR(255)
token?NVARCHAR(255)
pairingpinNVARCHAR(50)
>]; + posterminalcategory [label=<
posterminalcategory
idINT PK
category_idINT FK
posterminal_idINT FK
>]; __legend [label=<
Legend
PKprimary key
FKforeign key
?nullable column
—FOREIGN KEY constraint
- -naming convention only, no constraint
>]; - google_auth:selected_company:w -> company:in8:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company"]; - category:parent_id:e -> category:id:e [color="#2f855a", fontcolor="#2f855a", tooltip="FK_category_parent"]; - company:root_category_id:w -> category:in5:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_root_category"]; - company:default_language_id:w -> language:in3:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_company_default_language"]; - company:taxrate:e -> tax:in2:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_company_taxrate"]; - company:reducedtaxrate:e -> tax:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_company_reducedtaxrate"]; - company:taxfreerate:e -> tax:in1:n [color="#319795", fontcolor="#319795", tooltip="FK_company_taxfreerate"]; - customergroup:company_id:se -> company:in3:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company"]; - company:poscustomergroup_id:nw -> customergroup:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup"]; - entity:customergroup_id:sw -> customergroup:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup"]; - entityaddress:entity_id:se -> entity:in0:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_entityaddress_entity"]; - entitycontact:entity_id:sw -> entity:in1:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_entitycontact_entity"]; - entityhistory:entity_id:sw -> entity:in2:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_entityhistory_entity"]; - entityhistory:companyuser_id:se -> companyuser:in1:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_entityhistory_companyuser"]; - unit:company_id:e -> company:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_unit_company"]; - unittext:unit_id:se -> unit:in0:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_unittext_unit"]; - unittext:language_id:se -> language:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_unittext_language"]; - brand:company_id:e -> company:in1:n [color="#744210", fontcolor="#744210", tooltip="FK_brand_company"]; - article:unit_id:sw -> unit:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_article_unit"]; - article:brand_id:e -> brand:in0:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_article_brand"]; - article:articlelist_id:sw -> articlelist:in1:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_article_articlelist"]; - article:optionslist_id:se -> optionslist:in0:n [color="#38a169", fontcolor="#38a169", tooltip="FK_article_optionslist"]; - article:fatherarticle_id:e -> article:id:e [color="#3182ce", fontcolor="#3182ce", tooltip="FK_article_fatherarticle"]; - articlehistory:article_id:w -> article:in5:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articlehistory_article"]; - articlehistory:companyuser_id:e -> companyuser:in0:n [color="#c53030", fontcolor="#c53030", tooltip="FK_articlehistory_companyuser"]; - articleean:article_id:sw -> article:in3:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articleean_article"]; - articlepic:article_id:w -> article:in7:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_articlepic_article"]; - articlepic:pic_id:se -> pic:in1:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_articlepic_pic"]; - brandpic:brand_id:w -> brand:in1:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_brandpic_brand"]; - brandpic:pic_id:se -> pic:in0:n [color="#c05621", fontcolor="#c05621", tooltip="FK_brandpic_pic"]; - articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#319795", fontcolor="#319795", tooltip="FK_articlelistarticle_articlelist"]; - articlelistarticle:article_id:se -> article:in1:n [color="#97266d", fontcolor="#97266d", tooltip="FK_articlelistarticle_article"]; - optionslistarticle:optionslist_id:sw -> optionslist:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_optionslistarticle_optionslist"]; - optionslistarticle:article_id:sw -> article:in4:n [color="#276749", fontcolor="#276749", tooltip="FK_optionslistarticle_article"]; - categoryarticle:category_id:sw -> category:in2:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categoryarticle_category"]; - categoryarticle:article_id:w -> article:in6:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categoryarticle_article"]; - categorypic:category_id:sw -> category:in4:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_categorypic_category"]; - categorypic:pic_id:sw -> pic:in2:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_categorypic_pic"]; - categorytext:category_id:e -> category:in0:n [color="#22543d", fontcolor="#22543d", tooltip="FK_categorytext_category"]; - categorytext:language_id:sw -> language:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_categorytext_language"]; - articletext:article_id:e -> article:in0:n [color="#702459", fontcolor="#702459", tooltip="FK_articletext_article"]; - articletext:language_id:sw -> language:in2:n [color="#744210", fontcolor="#744210", tooltip="FK_articletext_language"]; - price:tax_id:sw -> tax:in3:n [color="#285e61", fontcolor="#285e61", tooltip="FK_price_tax"]; - price:article_id:w -> article:in8:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_price_article"]; - price:customergroup_id:w -> customergroup:in2:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_price_customergroup"]; - manifest:company_id:sw -> company:in9:n [color="#38a169", fontcolor="#38a169", tooltip="FK_manifest_company"]; - manifest:entity_id:w -> entity:in3:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_manifest_entity"]; - manifestarticle:manifest_id:sw -> manifest:in0:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_manifestarticle_manifest"]; - manifestarticle:article_id:w -> article:in9:n [color="#c53030", fontcolor="#c53030", tooltip="FK_manifestarticle_article"]; - manifestarticle:tax_id:sw -> tax:in4:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_manifestarticle_tax"]; - numbers:company_id:se -> company:in4:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_numbers_company"]; - companyuser:google_auth_id:se -> google_auth:in0:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_companyuser_google_auth"]; - companyuser:company_id:sw -> company:in7:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_companyuser_company"]; - syncpos:company_id:sw -> company:in5:n [color="#c05621", fontcolor="#c05621", tooltip="FK_syncpos_company"]; - companysetting:company_id:sw -> company:in6:n [color="#319795", fontcolor="#319795", tooltip="FK_companysetting_company"]; - posterminal:company_id:e -> company:in2:n [color="#97266d", fontcolor="#97266d", tooltip="FK_posterminal_company"]; - posterminalcategory:posterminal_id:se -> posterminal:in0:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_posterminalcategory_posterminal"]; - posterminalcategory:category_id:sw -> category:in3:n [color="#276749", fontcolor="#276749", tooltip="FK_posterminalcategory_category"]; + google_auth:selected_company:w -> company:in7:n [color="#c53030", fontcolor="#c53030", tooltip="FK_google_auth_selected_company", weight=3]; + category:parent_id:e -> category:id:e [color="#2f855a", fontcolor="#2f855a", tooltip="FK_category_parent", constraint=false]; + company:root_category_id:e -> category:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_company_root_category", constraint=false, weight=1]; + company:default_language_id:e -> language:in0:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_company_default_language", constraint=false, weight=1]; + company:taxrate:w -> tax:in3:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_company_taxrate", constraint=false, weight=1]; + company:reducedtaxrate:w -> tax:in2:n [color="#c05621", fontcolor="#c05621", tooltip="FK_company_reducedtaxrate", weight=3]; + company:taxfreerate:w -> tax:in4:n [color="#319795", fontcolor="#319795", tooltip="FK_company_taxfreerate", constraint=false, weight=1]; + customergroup:company_id:w -> company:in1:n [color="#97266d", fontcolor="#97266d", tooltip="FK_customergroup_company", weight=3]; + company:poscustomergroup_id:w -> customergroup:in2:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_company_poscustomergroup", constraint=false, weight=1]; + entity:customergroup_id:nw -> customergroup:in1:n [color="#276749", fontcolor="#276749", tooltip="FK_entity_customergroup", weight=3]; + entityaddress:entity_id:sw -> entity:in1:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_entityaddress_entity", weight=3]; + entitycontact:entity_id:se -> entity:in2:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_entitycontact_entity", weight=3]; + entityhistory:entity_id:sw -> entity:in3:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_entityhistory_entity", weight=3]; + entityhistory:companyuser_id:se -> companyuser:in0:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_entityhistory_companyuser", constraint=false, weight=1]; + unit:company_id:e -> company:in8:n [color="#22543d", fontcolor="#22543d", tooltip="FK_unit_company", weight=3]; + unittext:unit_id:se -> unit:in1:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_unittext_unit", weight=3]; + unittext:language_id:nw -> language:in1:n [color="#702459", fontcolor="#702459", tooltip="FK_unittext_language", constraint=false, weight=1]; + brand:company_id:w -> company:in6:n [color="#744210", fontcolor="#744210", tooltip="FK_brand_company", weight=3]; + article:unit_id:e -> unit:in0:n [color="#285e61", fontcolor="#285e61", tooltip="FK_article_unit", constraint=false, weight=1]; + article:brand_id:sw -> brand:in1:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_article_brand", constraint=false, weight=1]; + article:articlelist_id:nw -> articlelist:in1:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_article_articlelist", weight=3]; + article:optionslist_id:e -> optionslist:in0:n [color="#38a169", fontcolor="#38a169", tooltip="FK_article_optionslist", constraint=false, weight=1]; + article:fatherarticle_id:e -> article:id:e [color="#3182ce", fontcolor="#3182ce", tooltip="FK_article_fatherarticle", constraint=false]; + articlehistory:article_id:sw -> article:in3:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_articlehistory_article", weight=3]; + articlehistory:companyuser_id:w -> companyuser:in1:n [color="#c53030", fontcolor="#c53030", tooltip="FK_articlehistory_companyuser", constraint=false, weight=1]; + articleean:article_id:sw -> article:in6:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_articleean_article", weight=3]; + articlepic:article_id:e -> article:in2:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_articlepic_article", weight=3]; + articlepic:pic_id:e -> pic:in1:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_articlepic_pic", constraint=false, weight=1]; + brandpic:brand_id:se -> brand:in0:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_brandpic_brand", weight=3]; + brandpic:pic_id:nw -> pic:in2:n [color="#c05621", fontcolor="#c05621", tooltip="FK_brandpic_pic", constraint=false, weight=1]; + articlelistarticle:articlelist_id:se -> articlelist:in0:n [color="#319795", fontcolor="#319795", tooltip="FK_articlelistarticle_articlelist", weight=3]; + articlelistarticle:article_id:w -> article:in5:n [color="#97266d", fontcolor="#97266d", tooltip="FK_articlelistarticle_article", constraint=false, weight=1]; + optionslistarticle:optionslist_id:se -> optionslist:in1:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_optionslistarticle_optionslist", weight=3]; + optionslistarticle:article_id:w -> article:in8:n [color="#276749", fontcolor="#276749", tooltip="FK_optionslistarticle_article", constraint=false, weight=1]; + categoryarticle:category_id:se -> category:in3:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_categoryarticle_category", weight=3]; + categoryarticle:article_id:sw -> article:in7:n [color="#0987a0", fontcolor="#0987a0", tooltip="FK_categoryarticle_article", constraint=false, weight=1]; + categorypic:category_id:w -> category:in1:n [color="#553c9a", fontcolor="#553c9a", tooltip="FK_categorypic_category", weight=3]; + categorypic:pic_id:w -> pic:in0:n [color="#742a2a", fontcolor="#742a2a", tooltip="FK_categorypic_pic", constraint=false, weight=1]; + categorytext:category_id:w -> category:in4:n [color="#22543d", fontcolor="#22543d", tooltip="FK_categorytext_category", weight=3]; + categorytext:language_id:e -> language:in3:n [color="#1a365d", fontcolor="#1a365d", tooltip="FK_categorytext_language", constraint=false, weight=1]; + articletext:article_id:e -> article:in9:n [color="#702459", fontcolor="#702459", tooltip="FK_articletext_article", weight=3]; + articletext:language_id:w -> language:in2:n [color="#744210", fontcolor="#744210", tooltip="FK_articletext_language", constraint=false, weight=1]; + price:tax_id:nw -> tax:in1:n [color="#285e61", fontcolor="#285e61", tooltip="FK_price_tax", constraint=false, weight=1]; + price:article_id:w -> article:in1:n [color="#d53f8c", fontcolor="#d53f8c", tooltip="FK_price_article", weight=3]; + price:customergroup_id:se -> customergroup:in0:n [color="#dd6b20", fontcolor="#dd6b20", tooltip="FK_price_customergroup", constraint=false, weight=1]; + manifest:company_id:e -> company:in0:n [color="#38a169", fontcolor="#38a169", tooltip="FK_manifest_company", weight=3]; + manifest:entity_id:e -> entity:in0:n [color="#3182ce", fontcolor="#3182ce", tooltip="FK_manifest_entity", constraint=false, weight=1]; + manifestarticle:manifest_id:sw -> manifest:in0:n [color="#805ad5", fontcolor="#805ad5", tooltip="FK_manifestarticle_manifest", weight=3]; + manifestarticle:article_id:e -> article:in0:n [color="#c53030", fontcolor="#c53030", tooltip="FK_manifestarticle_article", constraint=false, weight=1]; + manifestarticle:tax_id:sw -> tax:in0:n [color="#2f855a", fontcolor="#2f855a", tooltip="FK_manifestarticle_tax", constraint=false, weight=1]; + ledger:manifestarticle_id:ne -> manifestarticle:in0:n [color="#2b6cb0", fontcolor="#2b6cb0", tooltip="FK_ledger_manifestarticle", weight=3]; + numbers:company_id:e -> company:in2:n [color="#b7791f", fontcolor="#b7791f", tooltip="FK_numbers_company", weight=3]; + companyuser:google_auth_id:e -> google_auth:in0:n [color="#6b46c1", fontcolor="#6b46c1", tooltip="FK_companyuser_google_auth", constraint=false, weight=1]; + companyuser:company_id:sw -> company:in3:n [color="#c05621", fontcolor="#c05621", tooltip="FK_companyuser_company", weight=3]; + syncpos:company_id:se -> company:in4:n [color="#319795", fontcolor="#319795", tooltip="FK_syncpos_company", weight=3]; + companysetting:company_id:sw -> company:in5:n [color="#97266d", fontcolor="#97266d", tooltip="FK_companysetting_company", weight=3]; + posterminal:company_id:w -> company:in9:n [color="#4c51bf", fontcolor="#4c51bf", tooltip="FK_posterminal_company", weight=3]; + posterminalcategory:posterminal_id:sw -> posterminal:in0:n [color="#276749", fontcolor="#276749", tooltip="FK_posterminalcategory_posterminal", weight=3]; + posterminalcategory:category_id:w -> category:in5:n [color="#9c4221", fontcolor="#9c4221", tooltip="FK_posterminalcategory_category", constraint=false, weight=1]; } \ No newline at end of file diff --git a/schema-graph.svg b/schema-graph.svg index f7e0d52..2c784ec 100644 --- a/schema-graph.svg +++ b/schema-graph.svg @@ -4,653 +4,661 @@ - - + + schema - + cluster_identity - -Identity & Company + +Identity & Company cluster_i18n - -Localization + +Localization cluster_catalog - -Catalog + +Catalog cluster_media - -Media + +Media cluster_commerce - -Pricing & Documents + +Pricing & Documents cluster_units - -Units + +Units customergroup - - - - - -customergroup - - -id -INT -PK - - -company_id -? -INT -FK - - -name -NVARCHAR(255) + + + + + +customergroup + + +id +INT +PK + + +company_id +? +INT +FK + + +name +NVARCHAR(255) - + company - - - - - - - - - - - - -company - - -id -INT -PK - - -default_language_id -? -INT -FK - - -root_category_id -? -INT -FK - - -poscustomergroup_id -INT -FK - - -taxrate -? -INT -FK - - -reducedtaxrate -? -INT -FK - - -taxfreerate -? -INT -FK - - -name -NVARCHAR(255) + + + + + + + + + + + + +company + + +id +INT +PK + + +taxrate +? +INT +FK + + +reducedtaxrate +? +INT +FK + + +taxfreerate +? +INT +FK + + +poscustomergroup_id +INT +FK + + +root_category_id +? +INT +FK + + +default_language_id +? +INT +FK + + +name +NVARCHAR(255) -customergroup:se->company:n +customergroup:w->company:n - - + + - + -entityaddress - - -entityaddress - - -id -INT -PK - - -entity_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name? -NVARCHAR(255) - - -street? -NVARCHAR(255) - - -streetnumber? -NVARCHAR(50) - - -zip? -NVARCHAR(50) - - -city? -NVARCHAR(255) - - -county? -NVARCHAR(255) - - -addresstype -NVARCHAR(50) - - - -entity - - - - - - -entity - - -id -INT -PK - - -customergroup_id -INT -FK - - -first_name? -NVARCHAR(255) - - -name -NVARCHAR(255) - - -created_at -DATETIME2 - - -updated_at -DATETIME2 - - - -entityaddress:se->entity:n - - - - - +meta + + +meta + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) numbers - - -numbers - - -id -INT -PK - - -company_id -INT -FK - - -type -NVARCHAR(100) - - -prefix -NVARCHAR(50) - - -padding -INT - - -next -INT + + +numbers + + +id +INT +PK + + +company_id +INT +FK + + +type +NVARCHAR(100) + + +prefix +NVARCHAR(50) + + +padding +INT + + +next +INT - -numbers:se->company:n - - - + +numbers:e->company:n + + + + + + + + +entityaddress + + +entityaddress + + +id +INT +PK + + +entity_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name? +NVARCHAR(255) + + +street? +NVARCHAR(255) + + +streetnumber? +NVARCHAR(50) + + +zip? +NVARCHAR(50) + + +city? +NVARCHAR(255) + + +county? +NVARCHAR(255) + + +addresstype +NVARCHAR(50) + + +sort +INT + + + +entity + + + + + + +entity + + +id +INT +PK + + +customergroup_id +INT +FK + + +first_name? +NVARCHAR(255) + + +name +NVARCHAR(255) + + +created_at +DATETIME2 + + +updated_at +DATETIME2 + + + +entityaddress:sw->entity:n + + + -entity:sw->customergroup:n +entity:nw->customergroup:n - - - - - - - -company:nw->customergroup:n - - - - - - - - -language - - - - - - -language - - -id -INT -PK - - -iso639 -NVARCHAR(10) - - - -company:w->language:n - - - - - - - - -category - - - - - - - - -category - - -id -INT -PK - - -parent_id -? -INT -FK - - -sort -INT - - - -company:w->category:n - - - - - - - - -tax - - - - - - - -tax - - -id -INT -PK - - -rate -DECIMAL(18,4) - - - -company:e->tax:n - - - - - - - - -company:e->tax:n - - - - - - - - -company:e->tax:n - - - - - - - - -syncpos - - -syncpos - - -id -INT -PK - - -company_id -INT -FK - - -paircode -NVARCHAR(50) - - -token? -NVARCHAR(255) - - - -syncpos:sw->company:n - - - - - - - - -entitycontact - - -entitycontact - - -id -INT -PK - - -entity_id -INT -FK - - -contacttype -NVARCHAR(50) - - -contact -NVARCHAR(255) - - - -entitycontact:sw->entity:n - - - - - - - - -meta - - -meta - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - - -companysetting - - -companysetting - - -id -INT -PK - - -company_id -INT -FK - - -key -NVARCHAR(255) - - -value -NVARCHAR(MAX) - - -frontend -BIT - - - -companysetting:sw->company:n - - - - - - - - -entityhistory - - -entityhistory - - -id -INT -PK - - -entity_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) - - - -entityhistory:sw->entity:n - - - + + - + companyuser - - - - -companyuser - - -id -INT -PK - - -company_id -INT -FK - - -google_auth_id -INT -FK - - - -entityhistory:se->companyuser:n - - - - - + + + + +companyuser + + +id +INT +PK + + +company_id +INT +FK + + +google_auth_id +INT +FK - + companyuser:sw->company:n - - - + + + google_auth - - - -google_auth - - -id -INT -PK - - -selected_company -? -INT -FK - - -email -NVARCHAR(320) - - -picture? -NVARCHAR(2048) - - -picturebinary? -VARBINARY(MAX) - - -sub? -NVARCHAR(255) - - -created_at -DATETIME2 + + + +google_auth + + +id +INT +PK + + +selected_company +? +INT +FK + + +email +NVARCHAR(320) + + +picture? +NVARCHAR(2048) + + +picturebinary? +VARBINARY(MAX) + + +sub? +NVARCHAR(255) + + +created_at +DATETIME2 - -companyuser:se->google_auth:n - - - + +companyuser:e->google_auth:n + + + + + + + + +entitycontact + + +entitycontact + + +id +INT +PK + + +entity_id +INT +FK + + +contacttype +NVARCHAR(50) + + +contact +NVARCHAR(255) + + +sort +INT + + + +entitycontact:se->entity:n + + + + + + + + +company:w->customergroup:n + + + + + + + + +language + + + + + + +language + + +id +INT +PK + + +iso639 +NVARCHAR(10) + + + +company:e->language:n + + + + + + + + +category + + + + + + + + +category + + +id +INT +PK + + +parent_id +? +INT +FK + + +sort +INT + + + +company:e->category:n + + + + + + + + +tax + + + + + + + +tax + + +id +INT +PK + + +rate +DECIMAL(18,4) + + + +company:w->tax:n + + + + + + + + +company:w->tax:n + + + + + + + + +company:w->tax:n + + + + + + + + +syncpos + + +syncpos + + +id +INT +PK + + +company_id +INT +FK + + +paircode +NVARCHAR(50) + + +token? +NVARCHAR(255) + + + +syncpos:se->company:n + + + + + + + + +entityhistory + + +entityhistory + + +id +INT +PK + + +entity_id +INT +FK + + +companyuser_id +? +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) + + + +entityhistory:sw->entity:n + + + + + + + + +entityhistory:se->companyuser:n + + + + + + + + +companysetting + + +companysetting + + +id +INT +PK + + +company_id +INT +FK + + +key +NVARCHAR(255) + + +value +NVARCHAR(MAX) + + +frontend +BIT + + + +companysetting:sw->company:n + + + @@ -658,349 +666,372 @@ google_auth:w->company:n - - + + - + unittext - - -unittext - - -id -INT -PK - - -language_id -INT -FK - - -unit_id -INT -FK - - -symbol -NVARCHAR(50) - - -name -NVARCHAR(255) + + +unittext + + +id +INT +PK + + +language_id +INT +FK + + +unit_id +INT +FK + + +symbol +NVARCHAR(50) + + +name +NVARCHAR(255) -unittext:se->language:n +unittext:nw->language:n - - + + - + unit - - - - -unit - - -id -INT -PK - - -company_id -INT -FK - - -si -NVARCHAR(50) - - -basesi -NVARCHAR(50) - - -basesifactor -FLOAT + + + + +unit + + +id +INT +PK + + +company_id +INT +FK + + +si +NVARCHAR(50) + + +basesi +NVARCHAR(50) + + +basesifactor +FLOAT unittext:se->unit:n - - - - - - - -categorytext - - -categorytext - - -id -INT -PK - - -language_id -INT -FK - - -category_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) - - - -categorytext:sw->language:n - - - - - - - - -categorytext:e->category:n - - - + + - + articletext - - -articletext - - -id -INT -PK - - -language_id -INT -FK - - -article_id -INT -FK - - -name -NVARCHAR(255) - - -slug -NVARCHAR(255) - - -seo? -NVARCHAR(MAX) - - -desc? -NVARCHAR(MAX) + + +articletext + + +id +INT +PK + + +article_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) -articletext:sw->language:n +articletext:w->language:n - - + + article - - - - - - - - - - - - -article - - -id -INT -PK - - -unit_id -INT -FK - - -articlelist_id -? -INT -FK - - -fatherarticle_id -? -INT -FK - - -optionslist_id -? -INT -FK - - -brand_id -? -INT -FK - - -sku? -NVARCHAR(100) - - -unitamount -FLOAT - - -weight -FLOAT - - -is_father -BIT - - -fathersort -INT - - -created_at -DATETIME2 - - -updated_at -DATETIME2 + + + + + + + + + + + + +article + + +id +INT +PK + + +brand_id +? +INT +FK + + +fatherarticle_id +? +INT +FK + + +articlelist_id +? +INT +FK + + +optionslist_id +? +INT +FK + + +unit_id +INT +FK + + +sku? +NVARCHAR(100) + + +unitamount +FLOAT + + +weight +FLOAT + + +is_father +BIT + + +fathersort +INT + + +created_at +DATETIME2 + + +updated_at +DATETIME2 articletext:e->article:n - - + + - + + +categorytext + + +categorytext + + +id +INT +PK + + +category_id +INT +FK + + +language_id +INT +FK + + +name +NVARCHAR(255) + + +slug +NVARCHAR(255) + + +seo? +NVARCHAR(MAX) + + +desc? +NVARCHAR(MAX) + + + +categorytext:e->language:n + + + + + + + + +categorytext:w->category:n + + + + + + + -articlelistarticle - - -articlelistarticle - - -id -INT -PK - - -articlelist_id -INT -FK - - -article_id -INT -FK - - -count -INT - - -sort -INT +brand + + + + +brand + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +gsprpinfo? +NVARCHAR(MAX) - + + +brand:w->company:n + + + + + + + -articlelist - - - - -articlelist - - -id -INT -PK +articlehistory + + +articlehistory + + +id +INT +PK + + +companyuser_id +? +INT +FK + + +article_id +INT +FK + + +date +DATETIME2 + + +text +NVARCHAR(MAX) - - -articlelistarticle:se->articlelist:n - - - + + +articlehistory:w->companyuser:n + + + - - -articlelistarticle:se->article:n - - - + + +articlehistory:sw->article:n + + + - - -article:sw->articlelist:n - - - + + +article:sw->brand:n + + + @@ -1008,216 +1039,93 @@ article:e->article:e - - + + + + + + + +articlelist + + + + +articlelist + + +id +INT +PK + + + +article:nw->articlelist:n + + + - + optionslist - - - - -optionslist - - -id -INT -PK + + + + +optionslist + + +id +INT +PK -article:se->optionslist:n +article:e->optionslist:n - - - - - - - -brand - - - - -brand - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -gsprpinfo? -NVARCHAR(MAX) - - - -article:e->brand:n - - - + + -article:sw->unit:n +article:e->unit:n - - + + articleean - - -articleean - - -id -INT -PK - - -article_id -INT -FK - - -ean -NVARCHAR(50) + + +articleean + + +id +INT +PK + + +article_id +INT +FK + + +ean +NVARCHAR(50) articleean:sw->article:n - - - - - - - -optionslistarticle - - -optionslistarticle - - -id -INT -PK - - -article_id -INT -FK - - -optionslist_id -INT -FK - - -count -INT - - -sort -INT - - -mandated -BIT - - - -optionslistarticle:sw->article:n - - - - - - - - -optionslistarticle:sw->optionslist:n - - - - - - - - -articlehistory - - -articlehistory - - -id -INT -PK - - -article_id -INT -FK - - -companyuser_id -? -INT -FK - - -date -DATETIME2 - - -text -NVARCHAR(MAX) - - - -articlehistory:e->companyuser:n - - - - - - - - -articlehistory:w->article:n - - - - - - - - -brand:e->company:n - - - + + @@ -1225,408 +1133,476 @@ category:e->category:e - - + + - + categoryarticle - - -categoryarticle - - -id -INT -PK - - -article_id -INT -FK - - -category_id -INT -FK + + +categoryarticle + + +id +INT +PK + + +article_id +INT +FK + + +category_id +INT +FK -categoryarticle:w->article:n +categoryarticle:sw->article:n - - + + -categoryarticle:sw->category:n +categoryarticle:se->category:n - - + + - - -brandpic - - -brandpic - - -id -INT -PK - - -brand_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +articlelistarticle + + +articlelistarticle + + +id +INT +PK + + +article_id +INT +FK + + +articlelist_id +INT +FK + + +count +INT + + +sort +INT - - -brandpic:w->brand:n - - - + + +articlelistarticle:w->article:n + + + + + + + + +articlelistarticle:se->articlelist:n + + + + + + + + +optionslistarticle + + +optionslistarticle + + +id +INT +PK + + +article_id +INT +FK + + +optionslist_id +INT +FK + + +count +INT + + +sort +INT + + +mandated +BIT + + + +optionslistarticle:w->article:n + + + + + + + + +optionslistarticle:se->optionslist:n + + + - + pic - - - - - -pic - - -id -INT -PK - - -w -INT - - -h -INT - - -size -BIGINT - - -hash -NVARCHAR(64) - - -thumbw -INT - - -thumbh -INT - - -thumbhash -NVARCHAR(64) - - -source -NVARCHAR(MAX) - - -fullbinary -VARBINARY(MAX) - - -thumbbinary -VARBINARY(MAX) - - - -brandpic:se->pic:n - - - - - + + + + + +pic + + +id +INT +PK + + +w +INT + + +h +INT + + +size +BIGINT + + +hash +NVARCHAR(64) + + +thumbw +INT + + +thumbh +INT + + +thumbhash +NVARCHAR(64) + + +source +NVARCHAR(MAX) + + +fullbinary +VARBINARY(MAX) + + +thumbbinary +VARBINARY(MAX) articlepic - - -articlepic - - -id -INT -PK - - -article_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +articlepic + + +id +INT +PK + + +pic_id +INT +FK + + +article_id +INT +FK + + +sort +INT -articlepic:w->article:n +articlepic:e->article:n - - + + -articlepic:se->pic:n +articlepic:e->pic:n - - + + + + + + + +brandpic + + +brandpic + + +id +INT +PK + + +pic_id +INT +FK + + +brand_id +INT +FK + + +sort +INT + + + +brandpic:se->brand:n + + + + + + + + +brandpic:nw->pic:n + + + categorypic - - -categorypic - - -id -INT -PK - - -category_id -INT -FK - - -pic_id -INT -FK - - -sort -INT + + +categorypic + + +id +INT +PK + + +pic_id +INT +FK + + +category_id +INT +FK + + +sort +INT -categorypic:sw->category:n +categorypic:w->category:n - - + + -categorypic:sw->pic:n +categorypic:w->pic:n - - + + - + + +ledger + + +ledger + + +id +INT +PK + + +manifestarticle_id +INT +FK + + +transaction_type +NVARCHAR(100) + + +amount +FLOAT + + +transaction_text +NVARCHAR(MAX) + + +prev_hash? +NVARCHAR(128) + + -price - - -price - - -id -INT -PK - - -article_id -INT -FK - - -customergroup_id -INT -FK - - -tax_id -INT -FK - - -minmultiplier -DECIMAL(18,4) - - -net -FLOAT +manifestarticle + + + +manifestarticle + + +id +INT +PK + + +manifest_id +INT +FK + + +tax_id +INT +FK + + +article_id +? +INT +FK + + +amount +FLOAT + + +net +FLOAT + + +comment? +NVARCHAR(MAX) + + +rebate +FLOAT - - -price:w->customergroup:n - - - + + +ledger:ne->manifestarticle:n + + + - - -price:w->article:n - - - - - - - - -price:sw->tax:n - - - + + +manifestarticle:e->article:n + + + manifest - - - -manifest - - -id -INT -PK - - -entity_id -? -INT -FK - - -company_id -INT -FK - - -number -NVARCHAR(100) - - -type -NVARCHAR(100) - - -date -DATETIME2 + + + +manifest + + +id +INT +PK + + +entity_id +? +INT +FK + + +company_id +INT +FK + + +number +NVARCHAR(100) + + +type +NVARCHAR(100) + + +date +DATETIME2 - - -manifest:w->entity:n - - - - - - - - -manifest:sw->company:n - - - - - - - - -manifestarticle - - -manifestarticle - - -id -INT -PK - - -article_id -? -INT -FK - - -tax_id -INT -FK - - -manifest_id -INT -FK - - -amount -FLOAT - - -net -FLOAT - - -transactedamount -FLOAT - - -comment? -NVARCHAR(MAX) - - -rebate -FLOAT - - - -manifestarticle:w->article:n - - - + + +manifestarticle:sw->manifest:n + + + @@ -1634,17 +1610,88 @@ manifestarticle:sw->tax:n - - + + - - -manifestarticle:sw->manifest:n - - - + + +manifest:e->entity:n + + + + + + + + +manifest:e->company:n + + + + + + + + +price + + +price + + +id +INT +PK + + +tax_id +INT +FK + + +customergroup_id +INT +FK + + +article_id +INT +FK + + +minmultiplier +DECIMAL(18,4) + + +net +FLOAT + + + +price:se->customergroup:n + + + + + + + + +price:w->article:n + + + + + + + + +price:nw->tax:n + + + @@ -1652,117 +1699,117 @@ unit:e->company:n - - - - - - - -posterminalcategory - - -posterminalcategory - - -id -INT -PK - - -category_id -INT -FK - - -posterminal_id -INT -FK - - - -posterminalcategory:sw->category:n - - - + + posterminal - - - -posterminal - - -id -INT -PK - - -company_id -INT -FK - - -name -NVARCHAR(255) - - -token? -NVARCHAR(255) - - -pairingpin -NVARCHAR(50) + + + +posterminal + + +id +INT +PK + + +company_id +INT +FK + + +name +NVARCHAR(255) + + +token? +NVARCHAR(255) + + +pairingpin +NVARCHAR(50) - + -posterminalcategory:se->posterminal:n - - - +posterminal:w->company:n + + + - - -posterminal:e->company:n - - - + + +posterminalcategory + + +posterminalcategory + + +id +INT +PK + + +category_id +INT +FK + + +posterminal_id +INT +FK + + + +posterminalcategory:w->category:n + + + + + + + + +posterminalcategory:sw->posterminal:n + + + - + __legend - - - -Legend - -PK - -primary key - -FK - -foreign key - -? - -nullable column - -— - -FOREIGN KEY constraint - -- - - -naming convention only, no constraint + + + +Legend + +PK + +primary key + +FK + +foreign key + +? + +nullable column + +— + +FOREIGN KEY constraint + +- - + +naming convention only, no constraint diff --git a/src/schema.js b/src/schema.js index 3b15d41..3daa938 100644 --- a/src/schema.js +++ b/src/schema.js @@ -178,6 +178,7 @@ BEGIN city NVARCHAR(255) NULL, county NVARCHAR(255) NULL, addresstype NVARCHAR(50) NOT NULL, + sort INT NOT NULL CONSTRAINT DF_entityaddress_sort DEFAULT 0, CONSTRAINT FK_entityaddress_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE ); END @@ -191,6 +192,7 @@ BEGIN entity_id INT NOT NULL, contacttype NVARCHAR(50) NOT NULL, contact NVARCHAR(255) NOT NULL, + sort INT NOT NULL CONSTRAINT DF_entitycontact_sort DEFAULT 0, CONSTRAINT FK_entitycontact_entity FOREIGN KEY (entity_id) REFERENCES entity(id) ON DELETE CASCADE ); END @@ -520,7 +522,6 @@ BEGIN article_id INT NULL, amount FLOAT NOT NULL, net FLOAT NOT NULL, - transactedamount FLOAT NOT NULL, comment NVARCHAR(MAX) NULL, rebate FLOAT NOT NULL CONSTRAINT DF_manifestarticle_rebate DEFAULT 0, tax_id INT NOT NULL, @@ -531,6 +532,21 @@ BEGIN END `; +export const INIT_LEDGER = ` +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'ledger') +BEGIN + CREATE TABLE ledger ( + id INT IDENTITY(1,1) PRIMARY KEY, + manifestarticle_id INT NOT NULL, + transaction_type NVARCHAR(100) NOT NULL, + amount FLOAT NOT NULL, + transaction_text NVARCHAR(MAX) NOT NULL, + prev_hash NVARCHAR(128) NULL, + CONSTRAINT FK_ledger_manifestarticle FOREIGN KEY (manifestarticle_id) REFERENCES manifestarticle(id) ON DELETE NO ACTION + ) WITH (LEDGER = ON (APPEND_ONLY = ON)); +END +`; + export const INIT_NUMBERS = ` IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'numbers') BEGIN @@ -688,6 +704,7 @@ export const SCHEMA_SCRIPTS = [ INIT_PRICE, INIT_MANIFEST, INIT_MANIFESTARTICLE, + INIT_LEDGER, INIT_NUMBERS, INIT_GET_NEXT_NUMBER_WITH_PREFIX_AND_PADDING, INIT_COMPANYUSER, diff --git a/src/sql/delivery.js b/src/sql/delivery.js index 1cdefe4..5afc8c9 100644 --- a/src/sql/delivery.js +++ b/src/sql/delivery.js @@ -33,7 +33,6 @@ SELECT a.sku, ma.amount, ma.net, - ma.transactedamount AS transactedAmount, ma.rebate, t.rate AS tax, ma.comment @@ -79,7 +78,6 @@ INSERT INTO manifestarticle ( article_id, amount, net, - transactedamount, comment, rebate, tax_id @@ -90,7 +88,6 @@ VALUES ( @articleId, @amount, @net, - @transactedAmount, @comment, @rebate, @taxId diff --git a/src/sql/entityaddress.js b/src/sql/entityaddress.js index 47eebf7..3855e99 100644 --- a/src/sql/entityaddress.js +++ b/src/sql/entityaddress.js @@ -8,7 +8,8 @@ export const ADDRESS_COLUMNS = ` zip, city, county, - addresstype AS addressType + addresstype AS addressType, + sort `; export const LIST_BY_ENTITY = ` @@ -16,7 +17,7 @@ SELECT ${ADDRESS_COLUMNS} FROM entityaddress WHERE entity_id = @entityId -ORDER BY id +ORDER BY sort ASC, id ASC `; export const GET_BY_ENTITY_AND_ID = ` @@ -26,6 +27,12 @@ FROM entityaddress WHERE id = @id AND entity_id = @entityId `; +export const GET_NEXT_SORT = ` +SELECT ISNULL(MAX(sort), -1) + 1 AS sort +FROM entityaddress +WHERE entity_id = @entityId +`; + export const CREATE = ` INSERT INTO entityaddress ( entity_id, @@ -36,7 +43,8 @@ INSERT INTO entityaddress ( zip, city, county, - addresstype + addresstype, + sort ) OUTPUT INSERTED.id, @@ -48,7 +56,8 @@ OUTPUT INSERTED.zip, INSERTED.city, INSERTED.county, - INSERTED.addresstype AS addressType + INSERTED.addresstype AS addressType, + INSERTED.sort VALUES ( @entityId, @firstName, @@ -58,7 +67,8 @@ VALUES ( @zip, @city, @county, - @addressType + @addressType, + @sort ) `; @@ -72,7 +82,8 @@ SET zip = @zip, city = @city, county = @county, - addresstype = @addressType + addresstype = @addressType, + sort = @sort OUTPUT INSERTED.id, INSERTED.entity_id AS entityId, @@ -83,7 +94,8 @@ OUTPUT INSERTED.zip, INSERTED.city, INSERTED.county, - INSERTED.addresstype AS addressType + INSERTED.addresstype AS addressType, + INSERTED.sort WHERE id = @id AND entity_id = @entityId `; diff --git a/src/sql/entitycontact.js b/src/sql/entitycontact.js index 2e7ef54..b74ff6b 100644 --- a/src/sql/entitycontact.js +++ b/src/sql/entitycontact.js @@ -2,7 +2,8 @@ export const CONTACT_COLUMNS = ` id, entity_id AS entityId, contacttype AS contactType, - contact + contact, + sort `; export const LIST_BY_ENTITY = ` @@ -10,7 +11,7 @@ SELECT ${CONTACT_COLUMNS} FROM entitycontact WHERE entity_id = @entityId -ORDER BY id +ORDER BY sort ASC, id ASC `; export const GET_BY_ENTITY_AND_ID = ` @@ -20,24 +21,32 @@ FROM entitycontact WHERE id = @id AND entity_id = @entityId `; +export const GET_NEXT_SORT = ` +SELECT ISNULL(MAX(sort), -1) + 1 AS sort +FROM entitycontact +WHERE entity_id = @entityId +`; + export const CREATE = ` -INSERT INTO entitycontact (entity_id, contacttype, contact) +INSERT INTO entitycontact (entity_id, contacttype, contact, sort) OUTPUT INSERTED.id, INSERTED.entity_id AS entityId, INSERTED.contacttype AS contactType, - INSERTED.contact -VALUES (@entityId, @contactType, @contact) + INSERTED.contact, + INSERTED.sort +VALUES (@entityId, @contactType, @contact, @sort) `; export const UPDATE = ` UPDATE entitycontact -SET contacttype = @contactType, contact = @contact +SET contacttype = @contactType, contact = @contact, sort = @sort OUTPUT INSERTED.id, INSERTED.entity_id AS entityId, INSERTED.contacttype AS contactType, - INSERTED.contact + INSERTED.contact, + INSERTED.sort WHERE id = @id AND entity_id = @entityId `; diff --git a/src/sql/purge.js b/src/sql/purge.js index e83e53d..dcfe6c8 100644 --- a/src/sql/purge.js +++ b/src/sql/purge.js @@ -6,6 +6,7 @@ export const TABLES = [ 'posterminalcategory', 'posterminal', 'numbers', + 'ledger', 'manifestarticle', 'manifest', 'price', diff --git a/src/store/delivery/helpers.js b/src/store/delivery/helpers.js index a644edf..a46fc71 100644 --- a/src/store/delivery/helpers.js +++ b/src/store/delivery/helpers.js @@ -22,7 +22,6 @@ export function shapeLine(row) { sku: row.sku ?? null, amount: row.amount, net: row.net, - transactedAmount: row.transactedAmount, rebate: row.rebate, tax: row.tax, comment: row.comment ?? null, @@ -80,10 +79,6 @@ export function normalizeLines(lines) { const rebate = entry?.rebate === undefined || entry?.rebate === null ? 0 : entry.rebate; const tax = entry?.tax; - const transactedAmount = - entry?.transactedAmount === undefined || entry?.transactedAmount === null - ? amount - : entry.transactedAmount; const comment = entry?.comment === undefined || entry?.comment === null ? null @@ -93,12 +88,11 @@ export function normalizeLines(lines) { !Number.isFinite(amount) || !Number.isFinite(net) || !Number.isFinite(rebate) || - !Number.isFinite(tax) || - !Number.isFinite(transactedAmount) + !Number.isFinite(tax) ) { throw new Error('Invalid lines'); } - return { sku, amount, net, rebate, tax, transactedAmount, comment }; + return { sku, amount, net, rebate, tax, comment }; }); } diff --git a/src/store/delivery/writeLines.js b/src/store/delivery/writeLines.js index 34d3929..a384df8 100644 --- a/src/store/delivery/writeLines.js +++ b/src/store/delivery/writeLines.js @@ -57,7 +57,6 @@ export async function insertLines( .input('articleId', articleId) .input('amount', line.amount) .input('net', line.net) - .input('transactedAmount', line.transactedAmount) .input('comment', line.comment) .input('rebate', line.rebate) .input('taxId', taxId) diff --git a/src/store/entityaddress/create.js b/src/store/entityaddress/create.js index d8bf78d..1475c94 100644 --- a/src/store/entityaddress/create.js +++ b/src/store/entityaddress/create.js @@ -1,4 +1,4 @@ -import { CREATE } from '../../sql/entityaddress.js'; +import { CREATE, GET_NEXT_SORT } from '../../sql/entityaddress.js'; import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; @@ -14,6 +14,7 @@ function shape(row) { city: row.city ?? null, county: row.county ?? null, addressType: row.addressType, + sort: row.sort, }; } @@ -38,6 +39,17 @@ export default function create(pool) { await access(googleAuthId, companyId); await entityAccess(companyId, entityId); + let sort = input.sort; + if (sort === undefined || sort === null) { + const next = await pool + .request() + .input('entityId', entityId) + .query(GET_NEXT_SORT); + sort = next.recordset[0].sort; + } else if (!Number.isFinite(sort)) { + throw new Error('Invalid sort'); + } + const result = await pool .request() .input('entityId', entityId) @@ -49,6 +61,7 @@ export default function create(pool) { .input('city', optionalString(input.city)) .input('county', optionalString(input.county)) .input('addressType', addressType) + .input('sort', sort) .query(CREATE); return shape(result.recordset[0]); diff --git a/src/store/entityaddress/list.js b/src/store/entityaddress/list.js index 4723a40..bec8d90 100644 --- a/src/store/entityaddress/list.js +++ b/src/store/entityaddress/list.js @@ -14,6 +14,7 @@ function shape(row) { city: row.city ?? null, county: row.county ?? null, addressType: row.addressType, + sort: row.sort, }; } diff --git a/src/store/entityaddress/update.js b/src/store/entityaddress/update.js index abb9d06..f0d3c0c 100644 --- a/src/store/entityaddress/update.js +++ b/src/store/entityaddress/update.js @@ -14,6 +14,7 @@ function shape(row) { city: row.city ?? null, county: row.county ?? null, addressType: row.addressType, + sort: row.sort, }; } @@ -81,6 +82,14 @@ export default function update(pool) { } } + let sort = current.sort; + if (Object.prototype.hasOwnProperty.call(patch, 'sort')) { + if (!Number.isFinite(patch.sort)) { + throw new Error('Invalid sort'); + } + sort = patch.sort; + } + const result = await pool .request() .input('id', id) @@ -93,6 +102,7 @@ export default function update(pool) { .input('city', city) .input('county', county) .input('addressType', addressType) + .input('sort', sort) .query(UPDATE); const row = result.recordset[0]; diff --git a/src/store/entitycontact/create.js b/src/store/entitycontact/create.js index e7221b5..370fcb3 100644 --- a/src/store/entitycontact/create.js +++ b/src/store/entitycontact/create.js @@ -1,4 +1,4 @@ -import { CREATE } from '../../sql/entitycontact.js'; +import { CREATE, GET_NEXT_SORT } from '../../sql/entitycontact.js'; import resolveCompanyAccess from '../companyuser/resolveCompanyAccess.js'; import resolveEntityAccess from '../entity/resolveEntityAccess.js'; @@ -8,6 +8,7 @@ function shape(row) { entityId: row.entityId, contactType: row.contactType, contact: row.contact, + sort: row.sort, }; } @@ -27,11 +28,23 @@ export default function create(pool) { await access(googleAuthId, companyId); await entityAccess(companyId, entityId); + let sort = input.sort; + if (sort === undefined || sort === null) { + const next = await pool + .request() + .input('entityId', entityId) + .query(GET_NEXT_SORT); + sort = next.recordset[0].sort; + } else if (!Number.isFinite(sort)) { + throw new Error('Invalid sort'); + } + const result = await pool .request() .input('entityId', entityId) .input('contactType', contactType) .input('contact', contact) + .input('sort', sort) .query(CREATE); return shape(result.recordset[0]); diff --git a/src/store/entitycontact/list.js b/src/store/entitycontact/list.js index ca919aa..1fc285a 100644 --- a/src/store/entitycontact/list.js +++ b/src/store/entitycontact/list.js @@ -8,6 +8,7 @@ function shape(row) { entityId: row.entityId, contactType: row.contactType, contact: row.contact, + sort: row.sort, }; } diff --git a/src/store/entitycontact/update.js b/src/store/entitycontact/update.js index 7f47474..f227e93 100644 --- a/src/store/entitycontact/update.js +++ b/src/store/entitycontact/update.js @@ -8,6 +8,7 @@ function shape(row) { entityId: row.entityId, contactType: row.contactType, contact: row.contact, + sort: row.sort, }; } @@ -50,12 +51,21 @@ export default function update(pool) { throw new Error('Contact required'); } + let sort = current.sort; + if (Object.prototype.hasOwnProperty.call(patch, 'sort')) { + if (!Number.isFinite(patch.sort)) { + throw new Error('Invalid sort'); + } + sort = patch.sort; + } + const result = await pool .request() .input('id', id) .input('entityId', entityId) .input('contactType', contactType) .input('contact', contact) + .input('sort', sort) .query(UPDATE); const row = result.recordset[0];