@mostajs/stocks 0.1.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.
- package/README.md +22 -0
- package/llms.txt +38 -0
- package/package.json +30 -0
- package/src/index.js +123 -0
package/README.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @mostajs/stocks
|
|
2
|
+
|
|
3
|
+
**Auteur** : Dr Hamid MADANI <drmdh@msn.com> · **Licence** : AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
Gestion des **stocks / inventaire** (couche logique) — articles, mouvements
|
|
6
|
+
(entrées/sorties/ajustements), **niveaux** calculés, **lots & péremption**, **seuils & alertes**,
|
|
7
|
+
**valorisation PMP**. **Compose** des repositories injectés (DEVRULES §10) ; DB-agnostique.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { createStocks, ArticleSchema, StockMovementSchema } from '@mostajs/stocks';
|
|
11
|
+
const s = createStocks({ repositories: { articles, movements } }); // repos ORM/data-plug
|
|
12
|
+
const a = await s.articles.create({ name: 'Lait', unit: 'L', minThreshold: 10 });
|
|
13
|
+
await s.movements.in(a.id, { qty: 50, unitCost: 90, lot: 'L1', expiry: '2026-08-01' });
|
|
14
|
+
await s.movements.out(a.id, { qty: 45, beneficiaryId: 'ben-1' }); // refuse le stock négatif
|
|
15
|
+
await s.levels.of(a.id); // { qty: 5, byLot }
|
|
16
|
+
await s.alerts.lowStock(); // articles ≤ seuil
|
|
17
|
+
await s.alerts.expiringSoon({ days: 30 });
|
|
18
|
+
await s.valorize(); // { total, method: 'pmp' }
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Tests & exemple (§11.1 / §12) : `npm install && npm test`. Conception : `DESIGN.md`. Proposition &
|
|
22
|
+
livrables : `docs/`. Fiche LLM : `llms.txt`.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @mostajs/stocks — fiche LLM
|
|
2
|
+
> Gestion des stocks / inventaire : articles, mouvements, niveaux, lots & péremption, seuils & alertes, valorisation PMP.
|
|
3
|
+
|
|
4
|
+
- Version: 0.1.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
|
|
5
|
+
- Stack: mosta-gestion-stack
|
|
6
|
+
|
|
7
|
+
## RÔLE
|
|
8
|
+
Couche LOGIQUE d'inventaire, DB-agnostique (repositories injectés, §10). Suit des ARTICLES et leurs
|
|
9
|
+
MOUVEMENTS (entrées/sorties/ajustements), calcule les NIVEAUX, alerte sur seuils & péremption, valorise
|
|
10
|
+
en PMP. Ne réimplémente ni persistance ni numérotation.
|
|
11
|
+
|
|
12
|
+
## EXPORTS
|
|
13
|
+
- createStocks({ repositories, now? }) -> { articles, movements, levels, alerts, valorize }
|
|
14
|
+
- ArticleSchema, StockMovementSchema (EntitySchema @mostajs/orm)
|
|
15
|
+
|
|
16
|
+
## API
|
|
17
|
+
- articles: create(data), update(id,patch), get(id), list(), deactivate(id)
|
|
18
|
+
- movements:
|
|
19
|
+
- in(articleId, { qty, lot?, expiry?, unitCost?, ref?, by? }) entrée (don/achat)
|
|
20
|
+
- out(articleId, { qty, ref?, beneficiaryId?, by? }) sortie (refuse stock négatif)
|
|
21
|
+
- adjust(articleId, { qty, reason?, by? }) ajustement signé (casse/inventaire)
|
|
22
|
+
- list({ articleId? })
|
|
23
|
+
- levels: of(articleId) -> { articleId, qty, byLot }, all() -> { article, qty, byLot }[]
|
|
24
|
+
- alerts: lowStock() -> articles ≤ seuil, expiringSoon({ days }) -> lots proches péremption
|
|
25
|
+
- valorize() -> { total, method:'pmp' } (prix moyen pondéré des entrées)
|
|
26
|
+
|
|
27
|
+
## REPOSITORIES (contrat injecté)
|
|
28
|
+
- articles, movements : chacun { create, findById, update, find(predicate) }
|
|
29
|
+
(l'app registre ArticleSchema / StockMovementSchema sur son ORM/data-plug)
|
|
30
|
+
|
|
31
|
+
## ROADMAP
|
|
32
|
+
FEFO strict par lot (dépletion), multi-emplacements & transferts, valorisation FIFO, sortie = aide en
|
|
33
|
+
nature (lien @mostajs/aid-grants) + pièce @mostajs/ged, inventaires tournants.
|
|
34
|
+
|
|
35
|
+
## PIÈGES
|
|
36
|
+
- `out` refuse de passer le stock négatif (contrôle de disponibilité).
|
|
37
|
+
- `adjust` prend une qté SIGNÉE (négative = casse/perte).
|
|
38
|
+
- valorize = PMP simple sur les entrées (FIFO = roadmap).
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mostajs/stocks",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Gestion des stocks / inventaire — articles, mouvements (entrées/sorties/ajustements), niveaux, lots & péremption, seuils & alertes, valorisation PMP. DB-agnostique (repositories injectés).",
|
|
5
|
+
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
|
+
"license": "AGPL-3.0-or-later",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src",
|
|
14
|
+
"README.md",
|
|
15
|
+
"llms.txt"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"stock",
|
|
19
|
+
"inventaire",
|
|
20
|
+
"inventory",
|
|
21
|
+
"warehouse",
|
|
22
|
+
"mostajs"
|
|
23
|
+
],
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@mostajs/mjs-unit": "^0.3.0"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node test-scripts/unit/stocks.test.mjs && node examples/run.mjs"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// @mostajs/stocks — gestion des stocks / inventaire (couche logique). MVP.
|
|
2
|
+
// Articles + mouvements (entrées/sorties/ajustements), niveaux calculés, lots & péremption,
|
|
3
|
+
// seuils & alertes, valorisation PMP. COMPOSE par INJECTION (DEVRULES §10) : repositories
|
|
4
|
+
// injectés (DB-agnostique) ; ne réimplémente ni la persistance ni la numérotation.
|
|
5
|
+
// @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
6
|
+
|
|
7
|
+
/** Article de stock (EntitySchema @mostajs/orm). */
|
|
8
|
+
export const ArticleSchema = {
|
|
9
|
+
name: 'Article', collection: 'stock_articles', timestamps: true,
|
|
10
|
+
fields: {
|
|
11
|
+
name: { type: 'string', required: true }, sku: { type: 'string', default: null },
|
|
12
|
+
unit: { type: 'string', default: 'u' }, category: { type: 'string', default: null },
|
|
13
|
+
minThreshold: { type: 'number', default: 0 }, perishable: { type: 'boolean', default: false },
|
|
14
|
+
active: { type: 'boolean', default: true },
|
|
15
|
+
},
|
|
16
|
+
indexes: [{ fields: { category: 'asc' } }],
|
|
17
|
+
};
|
|
18
|
+
/** Mouvement de stock (EntitySchema @mostajs/orm). */
|
|
19
|
+
export const StockMovementSchema = {
|
|
20
|
+
name: 'StockMovement', collection: 'stock_movements', timestamps: true,
|
|
21
|
+
fields: {
|
|
22
|
+
articleId: { type: 'string', required: true }, kind: { type: 'string', required: true }, // in|out|adjust
|
|
23
|
+
qty: { type: 'number', default: 0 }, lot: { type: 'string', default: null }, expiry: { type: 'date', default: null },
|
|
24
|
+
unitCost: { type: 'number', default: 0 }, ref: { type: 'string', default: null },
|
|
25
|
+
beneficiaryId: { type: 'string', default: null }, reason: { type: 'string', default: null },
|
|
26
|
+
by: { type: 'string', default: null }, at: { type: 'date' },
|
|
27
|
+
},
|
|
28
|
+
indexes: [{ fields: { articleId: 'asc' } }, { fields: { kind: 'asc' } }],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {object} opts
|
|
35
|
+
* @param {object} opts.repositories { articles, movements } — chacun { create, findById, update, find(predicate) }
|
|
36
|
+
* @param {() => Date} [opts.now]
|
|
37
|
+
*/
|
|
38
|
+
export function createStocks({ repositories = {}, now = () => new Date() } = {}) {
|
|
39
|
+
const articlesRepo = repositories.articles;
|
|
40
|
+
const movementsRepo = repositories.movements;
|
|
41
|
+
if (!articlesRepo || !movementsRepo) throw new Error('createStocks: repositories.articles et .movements requis');
|
|
42
|
+
|
|
43
|
+
const articles = {
|
|
44
|
+
create: (data) => articlesRepo.create({ unit: 'u', minThreshold: 0, perishable: false, active: true, ...data }),
|
|
45
|
+
update: (id, patch) => articlesRepo.update(id, patch),
|
|
46
|
+
get: (id) => articlesRepo.findById(id),
|
|
47
|
+
list: () => articlesRepo.find((a) => a.active !== false),
|
|
48
|
+
deactivate: (id) => articlesRepo.update(id, { active: false }),
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const movementsOf = (articleId) => movementsRepo.find((m) => m.articleId === articleId);
|
|
52
|
+
|
|
53
|
+
const levels = {
|
|
54
|
+
/** Niveau courant d'un article : qté, valeur (cumul des entrées), lots. */
|
|
55
|
+
async of(articleId) {
|
|
56
|
+
const ms = await movementsOf(articleId);
|
|
57
|
+
let qty = 0; const lots = {};
|
|
58
|
+
for (const m of ms) {
|
|
59
|
+
if (m.kind === 'in') { qty += num(m.qty); if (m.lot) lots[m.lot] = (lots[m.lot] || 0) + num(m.qty); }
|
|
60
|
+
else if (m.kind === 'out') qty -= num(m.qty);
|
|
61
|
+
else if (m.kind === 'adjust') qty += num(m.qty); // signé
|
|
62
|
+
}
|
|
63
|
+
return { articleId, qty, byLot: lots };
|
|
64
|
+
},
|
|
65
|
+
async all() {
|
|
66
|
+
const arts = await articles.list();
|
|
67
|
+
return Promise.all(arts.map(async (a) => ({ article: a, ...(await levels.of(a.id)) })));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const movements = {
|
|
72
|
+
/** Entrée (don/achat). */
|
|
73
|
+
async in(articleId, { qty, lot = null, expiry = null, unitCost = 0, ref = null, by = null } = {}) {
|
|
74
|
+
if (!(num(qty) > 0)) throw new Error('stocks.in: quantité invalide');
|
|
75
|
+
return movementsRepo.create({ articleId, kind: 'in', qty: num(qty), lot, expiry, unitCost: num(unitCost), ref, beneficiaryId: null, reason: null, by, at: now() });
|
|
76
|
+
},
|
|
77
|
+
/** Sortie (distribution) — refuse de passer le stock négatif. */
|
|
78
|
+
async out(articleId, { qty, ref = null, beneficiaryId = null, by = null } = {}) {
|
|
79
|
+
if (!(num(qty) > 0)) throw new Error('stocks.out: quantité invalide');
|
|
80
|
+
const lvl = await levels.of(articleId);
|
|
81
|
+
if (num(qty) > lvl.qty) throw new Error(`stocks.out: stock insuffisant (disponible ${lvl.qty}, demandé ${num(qty)})`);
|
|
82
|
+
return movementsRepo.create({ articleId, kind: 'out', qty: num(qty), lot: null, expiry: null, unitCost: 0, ref, beneficiaryId, reason: null, by, at: now() });
|
|
83
|
+
},
|
|
84
|
+
/** Ajustement d'inventaire (qté signée : casse, péremption, recomptage). */
|
|
85
|
+
async adjust(articleId, { qty, reason = 'inventaire', by = null } = {}) {
|
|
86
|
+
return movementsRepo.create({ articleId, kind: 'adjust', qty: num(qty), lot: null, expiry: null, unitCost: 0, ref: null, beneficiaryId: null, reason, by, at: now() });
|
|
87
|
+
},
|
|
88
|
+
list: ({ articleId } = {}) => (articleId ? movementsOf(articleId) : movementsRepo.find(() => true)),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const alerts = {
|
|
92
|
+
/** Articles dont le stock est ≤ seuil minimal. */
|
|
93
|
+
async lowStock() {
|
|
94
|
+
const all = await levels.all();
|
|
95
|
+
return all.filter((x) => x.qty <= num(x.article.minThreshold)).map((x) => ({ article: x.article, qty: x.qty, minThreshold: num(x.article.minThreshold) }));
|
|
96
|
+
},
|
|
97
|
+
/** Lots dont la péremption tombe dans `days` jours (entrées avec date). */
|
|
98
|
+
async expiringSoon({ days = 30 } = {}) {
|
|
99
|
+
const ms = (await movementsRepo.find((m) => m.kind === 'in' && m.expiry));
|
|
100
|
+
const limit = new Date(now().getTime() + days * 86400000).getTime();
|
|
101
|
+
return ms.filter((m) => new Date(m.expiry).getTime() <= limit)
|
|
102
|
+
.map((m) => ({ articleId: m.articleId, lot: m.lot, expiry: m.expiry, qty: num(m.qty) }));
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Valorisation du stock (PMP — prix moyen pondéré des entrées). */
|
|
107
|
+
async function valorize() {
|
|
108
|
+
const all = await levels.all();
|
|
109
|
+
let total = 0;
|
|
110
|
+
for (const x of all) {
|
|
111
|
+
const ins = (await movementsOf(x.article.id)).filter((m) => m.kind === 'in');
|
|
112
|
+
const inQty = ins.reduce((s, m) => s + num(m.qty), 0);
|
|
113
|
+
const inVal = ins.reduce((s, m) => s + num(m.qty) * num(m.unitCost), 0);
|
|
114
|
+
const pmp = inQty ? inVal / inQty : 0;
|
|
115
|
+
total += x.qty * pmp;
|
|
116
|
+
}
|
|
117
|
+
return { total: Math.round(total * 100) / 100, method: 'pmp' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { now, articles, movements, levels, alerts, valorize };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export default createStocks;
|