u
This commit is contained in:
@@ -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 },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -344,9 +344,9 @@ class ManifestHeader extends PureComponent {
|
||||
<SingleLineTextEdit
|
||||
size="small"
|
||||
label="Nummer"
|
||||
value={number}
|
||||
disabled={saving || pdfUploading || useExternalNumber}
|
||||
onChange={onNumberChange}
|
||||
value={number || ''}
|
||||
disabled
|
||||
placeholder="–"
|
||||
sx={{
|
||||
flex: { xs: '1 1 100%', sm: '1 1 120px' },
|
||||
minWidth: { sm: 100 },
|
||||
|
||||
@@ -475,6 +475,7 @@ export class OrderList extends PureComponent {
|
||||
this.state = {
|
||||
items: orderAutomat.getItems(),
|
||||
unfilteredTotal: orderAutomat.state.unfilteredTotal ?? 0,
|
||||
filterKey: orderAutomat.getFilterKey?.() ?? '',
|
||||
partiesById: {},
|
||||
busyId: null,
|
||||
};
|
||||
@@ -494,6 +495,7 @@ export class OrderList extends PureComponent {
|
||||
this.unsub = orderAutomat.subscribe(this, (state) => ({
|
||||
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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
112
src/utils/documentNumber.js
Normal file
112
src/utils/documentNumber.js
Normal file
@@ -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<string>}
|
||||
*/
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user