@opencxh/domain 1.245.0 → 1.247.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1,67 @@
1
+ import { PostalAddress } from '../organization/types';
2
+ /**
3
+ * One set of books with one letterhead: a BV, a VOF, a foreign branch.
4
+ *
5
+ * Two of these in one workspace is the Odoo/Exact "divisions" model — one inbox, one CRM, one
6
+ * team, two sets of books. It is deliberately **not** a second tenant (that is
7
+ * `Organization.parentId`, and it splits the inbox and the CRM too) and **not** a rung of
8
+ * {@link OwnerScope}: an entity says which party issues a document and which books it lands in,
9
+ * never who may read it.
10
+ *
11
+ * Absent is the normal case. An organisation with one entity is an organisation that never sees
12
+ * the concept, and that path must keep working — which is why no field here is required beyond
13
+ * the name, and why no existing row anywhere needs a write.
14
+ */
15
+ export interface BusinessEntity {
16
+ id: string;
17
+ organizationId: string;
18
+ /** The legal name, as it goes on the paper. */
19
+ name: string;
20
+ /** Short code for the badge and, from the numbering phase, the default number prefix. */
21
+ code?: string;
22
+ address?: PostalAddress;
23
+ /**
24
+ * The three identifications that belong together and today live on two different objects —
25
+ * `billing.vatNumber` / `billing.kvkNumber` on the organisation, `houseStyle.iban` beside it.
26
+ * The reason they were split ("a second copy is the one that goes stale") falls away here.
27
+ */
28
+ vatNumber?: string;
29
+ kvkNumber?: string;
30
+ iban?: string;
31
+ /** Inline data-URI, same rule as `Organization.logo`. */
32
+ logo?: string;
33
+ accentColor?: string;
34
+ footerText?: string;
35
+ /**
36
+ * The language this entity writes in.
37
+ *
38
+ * The floor of `recipientLocale`, which never had one: `Organization` carries no locale, so the
39
+ * whole ladder was dead at its one call site. It belongs here and not on the tenant — a Dutch
40
+ * BV and its Belgian sister write different default languages under one workspace.
41
+ */
42
+ locale?: string;
43
+ /** One per entity: an administration is set up per country and per currency. */
44
+ currency?: string;
45
+ /** The one a new document lands on. Exactly one row carries it. */
46
+ isDefault?: boolean;
47
+ /** Archived, never deleted: an issued quote names the entity it went out as. */
48
+ archived?: boolean;
49
+ order?: number;
50
+ createdBy: string;
51
+ createdAt?: number;
52
+ updatedAt?: number;
53
+ }
54
+ /** What {@link businessEntitySender} reads. Structural, so the document layer keeps pointing here. */
55
+ export type BusinessEntityLike = Pick<BusinessEntity, "name" | "address" | "vatNumber" | "kvkNumber" | "iban" | "logo" | "footerText">;
56
+ /**
57
+ * Does this row belong to this entity?
58
+ *
59
+ * Master data is owned by **exactly one** entity — two administrations with the same offering
60
+ * keep two rows. An unset owner means the **default** entity, and that is what makes this cost no
61
+ * backfill: every product written before entities existed keeps showing up where it always did,
62
+ * and the first save stamps it for real.
63
+ *
64
+ * The check runs in memory because the rows are loaded whole anyway, and because "the field is
65
+ * unset" is not expressible as a filter on this platform — there is no `$exists`.
66
+ */
67
+ export declare function belongsToEntity(rowEntityId: string | undefined, entityId: string, isDefaultEntity: boolean): boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -50,6 +50,14 @@ export interface Product {
50
50
  costPriceCents?: Cents;
51
51
  /** Who we buy it from. A `Company` in crm, so free — and all a later purchase order needs. */
52
52
  supplierCompanyId?: string;
53
+ /**
54
+ * The administration this belongs to. Unset = the default one — see `belongsToEntity`.
55
+ *
56
+ * Exactly one, not a list: two administrations that both sell this keep two rows, with their
57
+ * own sku, their own price and their own ledger category. That is the price of a hard split and
58
+ * it is what was chosen.
59
+ */
60
+ businessEntityId?: string;
53
61
  archived?: boolean;
54
62
  order?: number;
55
63
  createdBy: string;
@@ -91,6 +99,8 @@ export interface PriceRule {
91
99
  * contract price is not a second pricing mechanism, it is a price rule with an owner and a term.
92
100
  */
93
101
  contractId?: string;
102
+ /** The administration this rule belongs to. Unset = the default one. */
103
+ businessEntityId?: string;
94
104
  createdBy: string;
95
105
  createdAt?: number;
96
106
  }
@@ -23,6 +23,14 @@ export declare function conditionHolds(condition: TemplateCondition | undefined,
23
23
  export interface FillDocumentResult {
24
24
  blocks: DocumentBlock[];
25
25
  unresolved: string[];
26
+ /**
27
+ * Slots the template had no placeholder for.
28
+ *
29
+ * A slot the caller computed and the template cannot place is **dropped**, and dropping a
30
+ * quote's totals without a word is how a document goes out missing its total. Reported here so
31
+ * the preview can say it and the issue gate can refuse it.
32
+ */
33
+ unusedSlots: string[];
26
34
  }
27
35
  /**
28
36
  * Blocks the caller computes, keyed by the `block_id` of the template block they replace.
@@ -1,4 +1,5 @@
1
1
  import { DocumentBlock } from '../../platform/document-blocks';
2
+ import { BusinessEntityLike } from '../business-entity/types';
2
3
  import { PostalAddress } from '../organization/types';
3
4
  /**
4
5
  * A document template: the blocks a generated document is built from.
@@ -104,11 +105,19 @@ export interface DocumentSender {
104
105
  logo?: string;
105
106
  }
106
107
  /**
107
- * An organisation's details as a letterhead.
108
+ * A sending entity's details as a letterhead.
108
109
  *
109
- * Every field is optional on `Organization`, so every line here is conditional: a half-filled
110
- * organisation gets a short letterhead, never `undefined, undefined`. The billing name wins over
111
- * the display name what is on a quote is the legal entity.
110
+ * Every field is optional, so every line here is conditional: a half-filled entity gets a short
111
+ * letterhead, never `undefined, undefined`. **Nothing is inherited from the organisation** — a
112
+ * second BV that has not filled in its own KvK number prints no number rather than the first
113
+ * BV's, because this is a legal document. The issue gate catches the hole instead.
114
+ */
115
+ export declare function businessEntitySender(entity: BusinessEntityLike | null | undefined): DocumentSender;
116
+ /**
117
+ * The organisation as a letterhead. One caller left: the seed that gives an organisation its
118
+ * first `BusinessEntity`. An adapter and not a second line-builder, so the two can never drift.
119
+ *
120
+ * The billing name wins over the display name — what is on a quote is the legal entity.
112
121
  */
113
122
  export declare function organizationSender(organization: OrganizationLike | null | undefined): DocumentSender;
114
123
  /**
@@ -7,6 +7,15 @@
7
7
  */
8
8
  export interface TaxRate {
9
9
  key: string;
10
+ /**
11
+ * Which sending entity this rate belongs to. Empty = every entity.
12
+ *
13
+ * A scalar and **not** a list, unlike the rest of the master data: a Belgian sister entity does
14
+ * not need the `low` rate *hidden*, it needs a different percentage for the same key. That is an
15
+ * override, and a membership list cannot express one. An entity's own row shadows the shared row
16
+ * with the same key.
17
+ */
18
+ businessEntityId?: string;
10
19
  /** i18n key, not text — the same rule as `FIXED_TYPES` in work. */
11
20
  label: string;
12
21
  /** Whole percent as written on the invoice: `21`, not `0.21`. */
@@ -25,6 +25,13 @@ export interface Transaction {
25
25
  /** Version within the same quote. A sent quote is revised, never edited. */
26
26
  version: number;
27
27
  previousVersionId?: string;
28
+ /**
29
+ * Which sending entity issues this — whose letterhead, whose books, whose number series.
30
+ *
31
+ * Empty means the organisation's default. Frozen at issue: it is not in
32
+ * {@link FROZEN_WRITABLE_FIELDS}, so changing it on a sent document is already refused.
33
+ */
34
+ businessEntityId?: string;
28
35
  companyId: string;
29
36
  contactId?: string;
30
37
  /** The work item (the deal) this belongs to. Optional: not every quote has a deal. */