From 56a86fc00534558b76c6a241136123440e383315 Mon Sep 17 00:00:00 2001 From: seb Date: Wed, 14 Oct 2026 00:10:50 +0200 Subject: [PATCH] u --- src/components/CompanySetupDialog.jsx | 18 +++- src/components/manifest/ManifestForm.jsx | 9 +- src/components/manifest/ManifestHeader.jsx | 6 +- src/components/manifest/OrderList.jsx | 5 +- src/components/manifest/manifestConfig.js | 10 +- src/utils/documentNumber.js | 112 +++++++++++++++++++++ 6 files changed, 148 insertions(+), 12 deletions(-) create mode 100644 src/utils/documentNumber.js diff --git a/src/components/CompanySetupDialog.jsx b/src/components/CompanySetupDialog.jsx index 61427b0..94ad4a9 100644 --- a/src/components/CompanySetupDialog.jsx +++ b/src/components/CompanySetupDialog.jsx @@ -32,6 +32,8 @@ import PeopleIcon from '@mui/icons-material/People'; import LocalShippingIcon from '@mui/icons-material/LocalShipping'; import TuneIcon from '@mui/icons-material/Tune'; import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; +import RequestQuoteIcon from '@mui/icons-material/RequestQuote'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; import BackendError from './BackendError'; import { company, number as numberApi } from '../api'; import authAutomat, { bootstrapAuthMe } from '../automats/authAutomat'; @@ -49,18 +51,22 @@ const NUMBER_SCHEMES = [ { type: 'entity', label: 'Kundennummer', icon: PeopleIcon }, { type: 'delivery', label: 'Lieferschein', icon: LocalShippingIcon }, { type: 'adjustment', label: 'Bestandskorrektur', icon: TuneIcon }, + { type: 'quote', label: 'Angebot', icon: RequestQuoteIcon }, + { type: 'order', label: 'Bestellung', icon: ShoppingCartIcon }, ]; const PRESETS = [ { id: 'standard', label: 'Standard', - hint: 'Klassische Kürzel (ART-, KD-, LS-, KORR-)', + hint: 'Klassische Kürzel (ART-, KD-, LS-, KORR-, ANG-, AUF-)', values: { sku: { prefix: 'ART-', padding: 6, next: 1 }, entity: { prefix: 'KD-', padding: 6, next: 1001 }, delivery: { prefix: 'LS-', padding: 6, next: 1 }, adjustment: { prefix: 'KORR-', padding: 5, next: 1 }, + quote: { prefix: 'ANG-', padding: 6, next: 1 }, + order: { prefix: 'AUF-', padding: 6, next: 1 }, }, }, { @@ -72,28 +78,34 @@ const PRESETS = [ entity: { prefix: '', padding: 5, next: 20001 }, delivery: { prefix: '', padding: 5, next: 30001 }, adjustment: { prefix: '', padding: 5, next: 90001 }, + quote: { prefix: '', padding: 5, next: 40001 }, + order: { prefix: '', padding: 5, next: 50001 }, }, }, { id: 'compact', label: 'Kurzkürzel', - hint: 'Einbuchstabige Präfixe (A-, K-, L-, B-)', + hint: 'Einbuchstabige Präfixe (A-, K-, L-, B-, AG-, AU-)', values: { sku: { prefix: 'A-', padding: 5, next: 1 }, entity: { prefix: 'K-', padding: 5, next: 1001 }, delivery: { prefix: 'L-', padding: 5, next: 1 }, adjustment: { prefix: 'B-', padding: 5, next: 1 }, + quote: { prefix: 'AG-', padding: 5, next: 1 }, + order: { prefix: 'AU-', padding: 5, next: 1 }, }, }, { id: 'international', label: 'International', - hint: 'Englische Kürzel (SKU-, C-, DEL-, ADJ-)', + hint: 'Englische Kürzel (SKU-, C-, DEL-, ADJ-, Q-, ORD-)', values: { sku: { prefix: 'SKU-', padding: 5, next: 1 }, entity: { prefix: 'C-', padding: 5, next: 1001 }, delivery: { prefix: 'DEL-', padding: 5, next: 1 }, adjustment: { prefix: 'ADJ-', padding: 4, next: 1 }, + quote: { prefix: 'Q-', padding: 5, next: 1 }, + order: { prefix: 'ORD-', padding: 5, next: 1 }, }, }, ]; diff --git a/src/components/manifest/ManifestForm.jsx b/src/components/manifest/ManifestForm.jsx index 9cbb803..529fd5e 100644 --- a/src/components/manifest/ManifestForm.jsx +++ b/src/components/manifest/ManifestForm.jsx @@ -29,6 +29,7 @@ import { apiFor, parseNum, } from './manifestConfig'; +import { ensureDocumentNumber, getNextDocumentNumber } from '../../utils/documentNumber'; class ManifestForm extends PureComponent { constructor(props) { @@ -125,8 +126,11 @@ class ManifestForm extends PureComponent { } }); - if (this.props.mode === 'edit') void this.loadEdit(); - else void this.loadPartiesOnly(); + if (this.props.mode === 'edit') { + void this.loadEdit(); + } else { + void this.loadPartiesOnly(); + } } componentDidUpdate(prevProps) { @@ -664,6 +668,7 @@ class ManifestForm extends PureComponent { attachments: this.state.attachments, }, { + kind: cfg.kind, partyField: cfg.partyField, requireAtLeastOne, externalNumber: cfg.externalNumber, diff --git a/src/components/manifest/ManifestHeader.jsx b/src/components/manifest/ManifestHeader.jsx index 6170090..a47db39 100644 --- a/src/components/manifest/ManifestHeader.jsx +++ b/src/components/manifest/ManifestHeader.jsx @@ -344,9 +344,9 @@ class ManifestHeader extends PureComponent { ({ items: state.items, unfilteredTotal: state.unfilteredTotal ?? 0, + filterKey: orderAutomat.getFilterKey(state), })); void this.loadParties(); } @@ -583,7 +585,7 @@ export class OrderList extends PureComponent { } render() { - const { items, unfilteredTotal, partiesById, busyId } = this.state; + const { items, unfilteredTotal, filterKey, partiesById, busyId } = this.state; rowContext.partiesById = partiesById; rowContext.busyId = busyId; @@ -615,6 +617,7 @@ export class OrderList extends PureComponent { reserveBottom={32} minPageSize={8} minHeight={0} + resetKey={filterKey} items={items || []} totalCount={items ? items.length : 0} unfilteredTotal={unfilteredTotal} diff --git a/src/components/manifest/manifestConfig.js b/src/components/manifest/manifestConfig.js index c9d3931..92d326f 100644 --- a/src/components/manifest/manifestConfig.js +++ b/src/components/manifest/manifestConfig.js @@ -12,6 +12,7 @@ import orderAutomat from '../../automats/orderAutomat'; import { getArticle } from '../../automats/artikelAutomat'; import { selectCompanyTaxrate } from '../../automats/companyAutomat'; import { netFromGross } from '../../utils/price'; +import { ensureDocumentNumber, getNextDocumentNumber } from '../../utils/documentNumber'; import { GREEN_PANEL, ORANGE_PANEL, @@ -444,7 +445,7 @@ export function collectFilesForUpload(attachments = [], { onlyNew = false } = {} */ export async function buildManifestBody( form, - { partyField, requireAtLeastOne, externalNumber: useExternalNumber }, + { partyField, requireAtLeastOne, externalNumber: useExternalNumber, kind }, ) { const date = form.date; if (!date) throw new Error('Datum fehlt.'); @@ -460,8 +461,11 @@ export async function buildManifestBody( lines: buildLines(form.lines, { requireAtLeastOne }), }; } else { - const number = String(form.number ?? '').trim(); - if (!number) throw new Error('Nummer fehlt.'); + let number = String(form.number ?? '').trim(); + if (!number) { + number = await ensureDocumentNumber(kind || (partyField === 'entityId' ? 'quote' : 'doc')); + } + if (!number) throw new Error('Nummer konnte nicht ermittelt werden.'); body = { number, date, diff --git a/src/utils/documentNumber.js b/src/utils/documentNumber.js new file mode 100644 index 0000000..f5c3419 --- /dev/null +++ b/src/utils/documentNumber.js @@ -0,0 +1,112 @@ +import numberAutomat from '../automats/numberAutomat'; +import quoteAutomat from '../automats/quoteAutomat'; +import orderAutomat from '../automats/orderAutomat'; + +/** + * Formats a number pattern given prefix, padding width, and counter. + * + * @param {string} [prefix=''] + * @param {number} [padding=0] + * @param {number} [next=1] + * @returns {string} + */ +export function formatNumberPattern(prefix, padding, next) { + const pad = Math.max(0, Number(padding) || 0); + const num = Math.max(1, Number(next) || 1); + const numStr = pad > 0 ? String(num).padStart(pad, '0') : String(num); + return `${prefix ?? ''}${numStr}`; +} + +/** + * Returns the next document number for quote or order based on company sequences. + * + * @param {'quote'|'order'|string} kind + * @returns {string} + */ +export function getNextDocumentNumber(kind) { + const numbers = numberAutomat.state.items ?? []; + const seq = numbers.find((r) => r.type === kind); + if (seq) { + return formatNumberPattern(seq.prefix, seq.padding, seq.next); + } + + // Detect style from existing company sequences (sku, delivery, entity) + const skuSeq = numbers.find((r) => r.type === 'sku'); + const deliverySeq = numbers.find((r) => r.type === 'delivery'); + const entitySeq = numbers.find((r) => r.type === 'entity'); + const refSeq = skuSeq || deliverySeq || entitySeq; + + const pad = refSeq ? Math.max(0, Number(refSeq.padding) || 0) : 6; + const prefixStyle = refSeq?.prefix ?? ''; + + let prefix = ''; + let startNum = 1; + + if ( + prefixStyle.startsWith('ART-') || + prefixStyle.startsWith('LS-') || + prefixStyle.startsWith('KD-') || + prefixStyle.endsWith('-') + ) { + // Standard German ERP prefix style (ART-, KD-, LS-, KORR-) + prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : ''); + } else if ( + prefixStyle.startsWith('SKU-') || + prefixStyle.startsWith('DEL-') || + prefixStyle.startsWith('C-') + ) { + prefix = kind === 'quote' ? 'Q-' : (kind === 'order' ? 'ORD-' : ''); + } else if ( + prefixStyle.startsWith('A-') || + prefixStyle.startsWith('L-') || + prefixStyle.startsWith('K-') + ) { + prefix = kind === 'quote' ? 'AG-' : (kind === 'order' ? 'AU-' : ''); + } else if (!prefixStyle && refSeq?.next >= 10000) { + prefix = ''; + startNum = kind === 'quote' ? 40001 : 50001; + } else { + prefix = kind === 'quote' ? 'ANG-' : (kind === 'order' ? 'AUF-' : ''); + } + + // Check existing docs from quoteAutomat / orderAutomat to avoid collisions + const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : null); + const existingDocs = Array.isArray(listAutomat?.state?.items) ? listAutomat.state.items : []; + let maxFound = startNum - 1; + + for (const doc of existingDocs) { + if (!doc || typeof doc !== 'object') continue; + const docNum = String(doc.number ?? '').trim(); + if (!docNum) continue; + if (prefix && docNum.startsWith(prefix)) { + const rest = docNum.slice(prefix.length); + const parsed = parseInt(rest, 10); + if (!Number.isNaN(parsed) && parsed > maxFound) { + maxFound = parsed; + } + } else if (!prefix) { + const parsed = parseInt(docNum, 10); + if (!Number.isNaN(parsed) && parsed >= startNum && parsed > maxFound) { + maxFound = parsed; + } + } + } + + const nextVal = maxFound >= startNum ? maxFound + 1 : startNum; + return formatNumberPattern(prefix, pad, nextVal); +} + +/** + * Asynchronously ensures required stores are loaded, then returns the next document number. + * + * @param {'quote'|'order'|string} kind + * @returns {Promise} + */ +export async function ensureDocumentNumber(kind) { + const listAutomat = kind === 'quote' ? quoteAutomat : (kind === 'order' ? orderAutomat : null); + await Promise.all([ + numberAutomat.actions.ensureLoaded().catch(() => {}), + listAutomat?.actions?.ensureLoaded ? listAutomat.actions.ensureLoaded().catch(() => {}) : Promise.resolve(), + ]); + return getNextDocumentNumber(kind); +}