@alizadefaig/pos-server 1.2.32 → 1.2.33

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.
@@ -366,6 +366,7 @@ class PrintService {
366
366
  const printerTerminal = await this.printers.findTerminal(printer.terminal_id);
367
367
  jobs.push({
368
368
  workshopId,
369
+ printerId: printer.id,
369
370
  printData: {
370
371
  data: printData.data,
371
372
  terminal_id: printer.terminal_id,
@@ -376,15 +377,28 @@ class PrintService {
376
377
  }
377
378
  }
378
379
 
380
+ // Два принтера цеха на одном терминале с одним именем («ESC/POS») дали бы один ключ —
381
+ // терминал принял бы второй бегунок за повтор первого и не напечатал. Таким ключ
382
+ // различаем id принтера; с уникальными именами ключ прежний (как в Laravel).
383
+ const nameKeyOf = (workshopId, printData) => `${workshopId}|${printData.terminal_id}|${str(printData.printer.name)}`;
384
+ const nameCounts = new Map();
385
+ for (const {workshopId, printData} of jobs) {
386
+ const nameKey = nameKeyOf(workshopId, printData);
387
+ nameCounts.set(nameKey, (nameCounts.get(nameKey) || 0) + 1);
388
+ }
389
+
379
390
  const receipts = jobs.length ? await this.receiptsOf(team) : null;
380
- const requests = jobs.map(({workshopId, printData}) => {
391
+ const requests = jobs.map(({workshopId, printerId, printData}) => {
381
392
  // Стабильный per-ticket ключ: одинаков при повторе того же состояния — терминал
382
393
  // распознаёт дубль и не печатает второй раз. request_id в ключ не входит.
383
394
  const ticketSig = (syncOrderProducts.get(workshopId) || [])
384
395
  .map((opId) => `${opId}:${qtyById.has(opId) ? qtyById.get(opId) : 0}`)
385
396
  .sort()
386
397
  .join('|');
387
- const printIdempotencyKey = `${id}:${workshopId}:${printData.terminal_id}:${str(printData.printer.name)}:${md5(ticketSig)}`;
398
+ const printerKeyPart = nameCounts.get(nameKeyOf(workshopId, printData)) > 1
399
+ ? `${str(printData.printer.name)}#${printerId}`
400
+ : str(printData.printer.name);
401
+ const printIdempotencyKey = `${id}:${workshopId}:${printData.terminal_id}:${printerKeyPart}:${md5(ticketSig)}`;
388
402
 
389
403
  return {
390
404
  workshopId,
@@ -202,7 +202,7 @@ class StorageService {
202
202
  }
203
203
 
204
204
  /** Позиция, её товар, склад и единицы; null — склада нет, двигать некуда. */
205
- async prepare(orderProductOrId) {
205
+ async prepare(orderProductOrId, sign = -1) {
206
206
  const orderProductId = typeof orderProductOrId === 'object' && orderProductOrId
207
207
  ? orderProductOrId.id
208
208
  : orderProductOrId;
@@ -217,14 +217,15 @@ class StorageService {
217
217
  throw new Error('позиция не найдена');
218
218
  }
219
219
  const product = await this.db.get(
220
- 'SELECT id, type, unit_id FROM products WHERE id = ?',
220
+ `SELECT id, type, unit_id, product_workshop_id, product_category_id, ignore_workshop
221
+ FROM products WHERE id = ?`,
221
222
  [orderProduct.product_id]
222
223
  );
223
224
  if (!product) {
224
225
  throw new Error('товар не найден');
225
226
  }
226
227
 
227
- const storageId = await this.storageId();
228
+ const storageId = await this.storageId(product, orderProduct.id, sign);
228
229
  if (!storageId) {
229
230
  return null;
230
231
  }
@@ -234,7 +235,7 @@ class StorageService {
234
235
 
235
236
  /** Только модификаторы из списка — тех. карту блюда и сам товар не трогает. */
236
237
  async applyModifiers(orderProductOrId, modifiers, quantity, type, sign) {
237
- const target = await this.prepare(orderProductOrId);
238
+ const target = await this.prepare(orderProductOrId, sign);
238
239
  if (!target || target.product.type !== TYPE_DISH) {
239
240
  return;
240
241
  }
@@ -242,7 +243,7 @@ class StorageService {
242
243
  }
243
244
 
244
245
  async apply(orderProductOrId, quantity, type, sign) {
245
- const target = await this.prepare(orderProductOrId);
246
+ const target = await this.prepare(orderProductOrId, sign);
246
247
  if (!target) {
247
248
  return;
248
249
  }
@@ -333,10 +334,31 @@ class StorageService {
333
334
  return unit;
334
335
  }
335
336
 
336
- /** Склад заведения (`company_storage`), иначе первый склад команды. */
337
- async storageId() {
337
+ /**
338
+ * Склад списания позиции — порт Platform\StorageService::storageId.
339
+ * Возврат ($sign > 0) — на склад, с которого позицию списали. Продажа — по правилу заведения
340
+ * `companies.storage_withdraw_type` (склад цеха; by_terminal_and_workshop — пары «терминал +
341
+ * цех», нет пары — любое правило цеха), иначе склад заведения, иначе первый склад команды.
342
+ */
343
+ async storageId(product = null, orderProductId = null, sign = -1) {
338
344
  const {teamId, companyId} = this.context;
345
+ if (sign > 0 && orderProductId) {
346
+ const sold = await this.db.value(
347
+ `SELECT storage_id FROM storage_stock_transactions
348
+ WHERE external_id = ? AND type = 'sale'
349
+ ORDER BY datetime DESC LIMIT 1`,
350
+ [orderProductId]
351
+ );
352
+ if (sold) {
353
+ return sold;
354
+ }
355
+ }
339
356
  if (companyId) {
357
+ // Схема без правил (старая локальная БД) — как раньше, склад заведения.
358
+ const byRule = await this.ruleStorageId(companyId, product).catch(() => null);
359
+ if (byRule) {
360
+ return byRule;
361
+ }
340
362
  const storageId = await this.db.value(
341
363
  'SELECT storage_id FROM company_storage WHERE company_id = ? LIMIT 1',
342
364
  [companyId]
@@ -354,6 +376,49 @@ class StorageService {
354
376
  );
355
377
  }
356
378
 
379
+ /** Склад по правилу «склад цеха»; null — правила нет. */
380
+ async ruleStorageId(companyId, product) {
381
+ const workshopId = await this.workshopId(product);
382
+ if (!workshopId) {
383
+ return null;
384
+ }
385
+ const type = await this.db.value('SELECT storage_withdraw_type FROM companies WHERE id = ?', [companyId]);
386
+ if (type !== 'by_workshop' && type !== 'by_terminal_and_workshop') {
387
+ return null;
388
+ }
389
+ const base = `SELECT r.storage_id FROM company_storage_withdraw_rules r
390
+ JOIN storages s ON s.id = r.storage_id AND s.deleted_at IS NULL
391
+ WHERE r.deleted_at IS NULL AND r.company_id = ? AND r.workshop_id = ?`;
392
+ const terminalId = this.context.terminalId;
393
+ if (type === 'by_terminal_and_workshop' && terminalId) {
394
+ const own = await this.db.value(
395
+ `${base} AND r.terminal_id = ? ORDER BY r.created_at, r.id LIMIT 1`,
396
+ [companyId, workshopId, terminalId]
397
+ );
398
+ if (own) {
399
+ return own;
400
+ }
401
+ }
402
+ return await this.db.value(`${base} ORDER BY r.created_at, r.id LIMIT 1`, [companyId, workshopId]);
403
+ }
404
+
405
+ /** Мутатор Product::productWorkshopId: ignore_workshop гасит цех, пустой — у категории. */
406
+ async workshopId(product) {
407
+ if (!product || toBool(product.ignore_workshop)) {
408
+ return null;
409
+ }
410
+ if (product.product_workshop_id) {
411
+ return product.product_workshop_id;
412
+ }
413
+ if (!product.product_category_id) {
414
+ return null;
415
+ }
416
+ return await this.db.value(
417
+ 'SELECT product_workshop_id FROM product_categories WHERE id = ?',
418
+ [product.product_category_id]
419
+ ) || null;
420
+ }
421
+
357
422
  /** StockService::register без суммы (продажа себестоимость не меняет) + applyToStock. */
358
423
  async move(orderProduct, product, type, quantity, storageId, units) {
359
424
  // Нулевое движение не пишется (как в PHP — до проверки единицы).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alizadefaig/pos-server",
3
- "version": "1.2.32",
3
+ "version": "1.2.33",
4
4
  "description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
5
5
  "homepage": "https://gitlab.com/faigalizade/pos-server#readme",
6
6
  "bugs": {