diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx
index 8e79463d..ecf8d657 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.ar.resx
@@ -348,4 +348,179 @@
نعم
+ استخدام المخزون
+ يغيّر حفظ الاستخدام المخزون فورًا. تبقى هذه التغييرات إذا تم التخلي عن مسودة أو تعديل للتقرير. اعكس الاستخدام غير الصحيح صراحةً، ثم سجّل البديل. افتح تعديلًا قبل تغيير الاستخدام في تقرير مكتمل.
+ يتطلب استهلاك الأصناف الخاضعة للرقابة وعكسه شاهدًا مستقلاً في المخزون. أكمل هذا الإجراء أولاً، ثم اربط معرّف المعاملة الناتجة هنا.
+ الاستخدام المسجّل
+ نوع الاستخدام
+ مستخدم
+ مستهلك على مريض
+ متروك في موقع الحادث
+ تالف
+ قيد السجل
+ وقت التسجيل (UTC)
+ يعكس الاستخدام
+ تم العكس
+ عكس الاستخدام
+ يسجّل هذا الإجراء عكسًا كاملاً ويحفظ الاستخدام الأصلي في السجل التاريخي. لتصحيح كمية أو صنف، اعكس الاستخدام وسجّل البديل. إذا كان شاهد قد أكد العكس بالفعل في المخزون، فأدخل معرّف المعاملة لربطها دون تغيير المخزون مرة أخرى.
+ سبب التصحيح
+ معرّف معاملة عكس موجودة أكدها شاهد (اختياري)
+ تسجّل الكميات الموجبة الاستخدام، وتعكسه الكميات السالبة. افتح قيد السجل لعرض تفاصيل الحركة وفق صلاحيات وصولك الحالية إلى المخزون.
+ معرّف معاملة استهلاك موجودة (اختياري)
+ أدخل معاملة استهلاك مكتملة في المخزون لربط صنفها وكميتها وموقعها وملاحظتها. تُتجاهل حقول الاستهلاك الجديد أدناه، ولا يُستهلك المخزون مرة أخرى. حدد نوع الاستخدام أعلاه.
+ استهلاك جديد
+ استخدام وحدة التقرير أو محطته
+ اتركه فارغًا لاستخدام الوحدة المستجيبة في التقرير، مع استخدام المحطة كخيار بديل. حدد المصدر صراحةً إذا كان التقرير يتضمن عدة مواقع محتملة.
+ حفظ الاستخدام
+ العودة إلى التقرير
+ تحديث الأدلة من الاستخدام المسجّل
+ تم حفظ الاستخدام وتوثيق الأدلة الداعمة.
+ تم حفظ عملية المخزون. تعذّر توثيق الأدلة؛ استخدم «تحديث الأدلة» بعد حل مشكلات الوصول أو تغييرات المسودة. لا ترسل عملية المخزون مرة أخرى.
+ تغيّرت المسودة. تحقق من الاستخدام المسجّل قبل إرسال عملية مخزون أخرى.
+ أكمل إجراء الشاهد المستقل في المخزون، ثم اربط معرّف المعاملة الناتجة هنا. لم تسجّل هذه العملية أي حركة مخزون.
+ تم توثيق الأدلة الداعمة.
+ لم تُربط معاملة عكس في السجل بهذا التقرير. اربطها كتصحيح قبل اعتماد التقرير نهائيًا.
+ المشتريات
+ الموردون
+ أوامر الشراء
+ تقييم المخزون
+ يستخدم الموردون سجلات الشركات في «جهات الاتصال». أنشئ بيانات الشركة أو حدّثها في «جهات الاتصال»، ثم اربط الشركة هنا.
+ رقم الحساب لدى المورد
+ الشركة في جهات الاتصال
+ إضافة مورد
+ رقم أمر الشراء
+ المورد
+ العملة
+ عرض أمر الشراء
+ مسودة
+ تم الطلب
+ تم الاستلام جزئيًا
+ تم الاستلام
+ ملغى
+ الكمية المطلوبة
+ الكمية المستلمة
+ الكمية المتبقية
+ يؤدي وضع علامة «تم الطلب» على الأمر إلى قفل بنوده. يمنع إلغاء الأمر أي استلام إضافي ويحافظ على الكميات التي تم استلامها بالفعل.
+ وضع علامة «تم الطلب»
+ إلغاء الأمر
+ استلام الأصناف
+ استلم الطلب كاملًا أو جزءًا منه. أزل البنود غير المشمولة في هذه الشحنة. إذا كان وجود شاهد مستقل مطلوبًا، فلن تتغير أرصدة المخزون والكميات المستلمة إلا بعد موافقته في «المخزون».
+ استخدم صفًا مستقلًا لكل أصل له رقم تسلسلي، بكمية 1 ورقمه التسلسلي الخاص. أضف صفوفًا لاستلام أصول إضافية أو توزيع الكميات على مواقع أو دفعات مختلفة.
+ الكمية المراد استلامها
+ إزالة البند
+ عمليات الاستلام
+ أمر شراء جديد
+ يشمل التقييم المخزون والمواقع التي يحق لك عرضها فقط. تُعرض الإجماليات لكل عملة على حدة. تُحسب الصفوف ذات التكلفة غير المعروفة بشكل منفصل وتُستبعد من إجماليات القيمة المعروفة.
+ اعتبارًا من (UTC)
+ بعض أرصدة المخزون الظاهرة سالبة. راجعها قبل الاعتماد على هذا التقييم.
+ القيمة المعروفة
+ صفوف بلا تكلفة
+ العملة غير محددة
+ التكلفة غير معروفة
+ متوسط تكلفة الوحدة
+ المورد المفضل
+ استخدم عملة واحدة لكل صنف. يظل المخزون الحالي الذي لم تُحدد له عملة منفصلًا في التقييم؛ فتعيين عملة لا يحوّل التكاليف.
+ تم استلام أمر شراء للمخزون
+ الجرد والتنبيهات والتقارير
+ جرد المخزون
+ تنبيهات المخزون
+ تقارير المخزون
+ ابدأ بلقطة تضم ما يصل إلى 100 بند مخزون. سجّل كل كمية، ثم احفظ وأكمل الجرد. إذا تغيرت بيانات المخزون أو دليل الأصناف، فألغِ الجرد وابدأ جردًا جديدًا. سجّل المعدات ذات الأرقام التسلسلية التي تُكتشف لأول مرة من خلال «الاستلام».
+ جميع المواقع
+ بدء الجرد
+ إكمال الجرد
+ إلغاء الجرد
+ وقت اللقطة (UTC)
+ الكمية المتوقعة
+ الكمية المجرودة
+ الفرق
+ البنود التي تحتوي على فروق
+ مسودة
+ بانتظار الشاهد
+ مكتمل
+ ملغى
+ يجب أن يوافق شاهد مستقل على العملية المعلقة من سجل المخزون. ويجب أن تظل اللقطة مطابقة للحالة الحالية.
+ تُظهر التنبيهات انخفاض المخزون، والأصناف التي تنتهي صلاحيتها خلال 30 يومًا، والمخزون منتهي الصلاحية، والإرجاعات المتأخرة. حدّث التنبيهات مرة واحدة بعد إطلاق الميزة لتهيئة تنبيهات انخفاض المخزون، وكذلك بعد استيراد المخزون القديم. تُحدّثها التعديلات اللاحقة على المخزون والأصناف تلقائيًا.
+ تحديث التنبيهات
+ مخزون منخفض
+ تنتهي الصلاحية قريبًا
+ منتهي الصلاحية
+ إرجاع متأخر
+ مفتوح
+ تمت المعالجة
+ موعد الاستحقاق (UTC)
+ اختر تقريرًا وأي عوامل تصفية اختيارية. تستخدم التواريخ توقيت UTC ولا تشمل تاريخ النهاية. تعتمد تقارير اللقطة على المخزون الحالي. نزّل ملف PDF لعرضه أو طباعته.
+ من تاريخ (UTC، شامل)
+ إلى تاريخ (UTC، غير شامل)
+ تنزيل ملف PDF قابل للطباعة
+ جدولة إرسال التقارير
+ تنبيهات المخزون
+ المخزون يحتاج إلى مراجعة. سجّل الدخول للاطلاع على التنبيهات الحالية.
+ المخزون المتوفر
+ استخدام المخزون
+ انتهاء صلاحية المخزون
+ انخفاض المخزون
+ سجل نقل المخزون
+ سجل صرف المخزون
+ تقييم المخزون
+ سجل المواد الخاضعة للرقابة
+ الأيام المتبقية حتى انتهاء الصلاحية
+ الكمية الناقصة عن نقطة إعادة الطلب
+ كمية إعادة الطلب المقترحة
+ معرّف عملية الصرف
+ معرّف الشخص المستلم
+ معرّف الوحدة المستلمة
+ وقت الصرف (UTC)
+ الكمية المُعادة
+ الكمية التي لم تُعد بعد
+ وقت الإرجاع (UTC)
+ منفّذ العملية (معرّف المستخدم)
+ يعكس المعاملة
+ مصدر الاستخدام
+ معرّف السجل
+ معرّف البلاغ
+ معرّف النسخة المعدّلة
+ تصنيف إدارة مكافحة المخدرات الأمريكية (DEA)
+ رصيد المصدر قبل العملية
+ رصيد المصدر بعد العملية
+ رصيد الوجهة قبل العملية
+ رصيد الوجهة بعد العملية
+ الشاهد (معرّف المستخدم)
+ وقت تأكيد الشاهد (UTC)
+ يحتوي المخزون على بيانات محمية. افتح رابط التقرير الذي يتطلب تسجيل الدخول وتحقق من هويتك لعرض النتائج الحالية أو طباعتها. لا يتضمن ملف PDF المجدول هذا أي قيم للمخزون.
+ يتضمن هذا التقرير المخزون والمواقع التي يحق لك عرضها حاليًا فقط. تصف الكميات والقيم اللقطة المحددة، وتقتصر النتائج على 5,000 صف.
+ النطاق الزمني بتوقيت UTC (يشمل البداية ولا يشمل النهاية)
+ تظهر كل حركة مخزون مرة واحدة. تسجّل الكميات الموجبة الاستخدام الصادر، وتعكسه الكميات السالبة. يتطلب عرض تفاصيل السجل صلاحية الوصول إلى المصدر. تحتفظ الحركات الصادرة المستوردة بهويتها القديمة ولا تنشئ رابطًا بسجل عمليات أو مريض.
+ تُدرج الأصناف النشطة عندما يكون المخزون الظاهر المتتبع بالكميات أو الأصول ذات الأرقام التسلسلية عند نقطة إعادة الطلب أو دونها، مع استخدام الحد الأدنى للمخزون عند عدم تحديد تلك النقطة. تشمل الأصول ذات الأرقام التسلسلية الأصول قيد الخدمة والمصروفة والتالفة وقيد الإصلاح. تُستبعد المواقع المخفية.
+ تعرض الصفوف عمليات الصرف التي أُنشئت ضمن النطاق الزمني المحدد وحالة إرجاعها الحالية. لعرض الكميات التي لم تُعد من عمليات صرف أقدم، اختر تاريخ بداية أسبق.
+ تعرض الصفوف أرصدة المخزون الموجبة الحالية والأصول النشطة ذات الأرقام التسلسلية التي يقع تاريخ انتهاء صلاحيتها ضمن النطاق المحدد. يشمل النطاق الافتراضي المخزون المنتهي الصلاحية بالفعل والمخزون الذي تنتهي صلاحيته خلال الأيام الثلاثين القادمة.
+ توقيع منفّذ العملية
+ توقيع الشاهد
+ غير مسجّل
+ حركة مخزون دون رابط إلى سجل يمكن الوصول إليه
+ السجلات
+ حركة مخزون قديمة
+ أدخل تفاصيل جرد وكميات صحيحة.
+ اختر نطاقًا أصغر. يدعم الجرد ما يصل إلى 100 بند مخزون.
+ لا توجد بنود مخزون نشطة لجردها ضمن هذا النطاق.
+ هذا الجرد موجود بالفعل. افتحه من قائمة عمليات الجرد.
+ تغير الجرد أو لم تُسجّل جميع كمياته. أعد تحميله واحفظ جميع الكميات قبل إكماله.
+ تغير المخزون بعد التقاط هذه اللقطة. ألغِ هذا الجرد وابدأ جردًا جديدًا.
+ تتبع تسويات الجرد عملية الجرد المكتملة الخاصة بها. ابدأ جردًا جديدًا لتصحيح المخزون.
+ رحّل فروق الجرد بإكمال جرد المخزون.
+ اختر نطاق تواريخ صحيحًا بتوقيت UTC لا يتجاوز 366 يومًا.
+ اختر عوامل تصفية صحيحة للتقرير.
+ تغير التقرير أثناء إنشاء ملف PDF. أعد إنشاءه.
+ تعذر إنشاء التقرير. حاول مرة أخرى.
+ انخفاض مخزون المواد
+ اقتراب انتهاء صلاحية المخزون
+ اكتمال جرد المخزون
+ تأخر إعادة العهدة
+ هذا المورد مرتبط بسجل المخزون ويجب الاحتفاظ به.
+ تم تعديل المخزون
+ اكتمل نقل المخزون
+ تم صرف المخزون
+ تم إرجاع المخزون
+ تغيرت حالة أصل المخزون
+ تم تسجيل مادة خاضعة للرقابة
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx
index 6cae42f8..7b8ed13c 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.de.resx
@@ -472,5 +472,179 @@
Ja
+ Bestandsverbrauch
+ Das Speichern des Verbrauchs ändert den Bestand sofort. Diese Änderungen bleiben erhalten, wenn ein Entwurf oder eine Berichtsänderung verworfen wird. Stornieren Sie falschen Verbrauch ausdrücklich und erfassen Sie anschließend den Ersatz. Öffnen Sie eine Berichtsänderung, bevor Sie den Verbrauch eines abgeschlossenen Berichts ändern.
+ Verbrauch und Stornierungen kontrollierter Artikel erfordern einen unabhängigen Zeugen in Inventar. Schließen Sie diesen Vorgang zuerst ab und verknüpfen Sie anschließend die entstandene Transaktions-ID hier.
+ Erfasster Verbrauch
+ Verwendungsart
+ Verwendet
+ Am Patienten verbraucht
+ Am Einsatzort belassen
+ Beschädigt
+ Buchung
+ Erfasst (UTC)
+ Storniert Verbrauch
+ Storniert
+ Verbrauch stornieren
+ Dies erfasst eine vollständige Stornierung und erhält den ursprünglichen Verbrauch im Verlauf. Um eine Menge oder einen Artikel zu korrigieren, stornieren Sie den Verbrauch und erfassen Sie den Ersatz. Wurde die Stornierung bereits in Inventar von einem Zeugen bestätigt, geben Sie ihre Transaktions-ID ein, um sie ohne erneute Bestandsänderung zu verknüpfen.
+ Korrekturgrund
+ ID einer vorhandenen, bezeugten Stornotransaktion (optional)
+ Positive Mengen erfassen Verbrauch; negative Mengen stornieren ihn. Öffnen Sie die Buchung, um Bewegungsdetails gemäß Ihren aktuellen Inventarzugriffsrechten anzuzeigen.
+ ID einer vorhandenen Verbrauchstransaktion (optional)
+ Geben Sie eine abgeschlossene Verbrauchstransaktion aus Inventar ein, um deren Artikel, Menge, Lagerort und Notiz zu verknüpfen. Die Felder für neuen Verbrauch unten werden ignoriert, und der Bestand wird nicht erneut verbraucht. Wählen Sie oben die Verwendungsart.
+ Neuer Verbrauch
+ Einheit oder Wache des Berichts verwenden
+ Leer lassen, um die Einsatzeinheit des Berichts zu verwenden, ersatzweise deren Wache. Wählen Sie die Quelle ausdrücklich, wenn der Bericht mehrere mögliche Lagerorte hat.
+ Verbrauch speichern
+ Zum Bericht zurück
+ Nachweise aus erfasstem Verbrauch aktualisieren
+ Verbrauch gespeichert und Nachweise erfasst.
+ Der Inventarvorgang wurde gespeichert. Die Nachweise konnten nicht erfasst werden. Verwenden Sie Nachweise aktualisieren, nachdem Zugriffsprobleme oder Entwurfsänderungen geklärt sind. Senden Sie den Inventarvorgang nicht erneut.
+ Der Entwurf wurde geändert. Prüfen Sie den erfassten Verbrauch, bevor Sie einen weiteren Inventarvorgang senden.
+ Schließen Sie das Verfahren mit einem unabhängigen Zeugen in Inventar ab und verknüpfen Sie hier die entstandene Transaktions-ID. Durch diese Aktion wurde keine Bestandsbewegung erfasst.
+ Nachweise erfasst.
+ Eine Stornobuchung wurde noch nicht mit diesem Bericht verknüpft. Verknüpfen Sie sie vor dem Abschließen als Korrektur.
+ Einkauf
+ Lieferanten
+ Bestellungen
+ Bestandsbewertung
+ Lieferanten verwenden Firmeneinträge aus dem Bereich „Kontakte“. Erstellen oder aktualisieren Sie dort die Firmendaten und verknüpfen Sie die Firma anschließend hier.
+ Kundennummer beim Lieferanten
+ Firma aus den Kontakten
+ Lieferanten hinzufügen
+ Bestellnummer
+ Lieferant
+ Währung
+ Bestellung anzeigen
+ Entwurf
+ Bestellt
+ Teilweise eingegangen
+ Eingegangen
+ Storniert
+ Bestellte Menge
+ Eingegangene Menge
+ Restmenge
+ Wenn eine Bestellung als bestellt markiert wird, werden ihre Positionen gesperrt. Das Stornieren verhindert weitere Wareneingänge; bereits eingegangene Mengen bleiben erhalten.
+ Als bestellt markieren
+ Bestellung stornieren
+ Artikel entgegennehmen
+ Erfassen Sie den Eingang der gesamten Bestellung oder einer Teillieferung. Entfernen Sie Positionen, die nicht zu dieser Lieferung gehören. Ist ein unabhängiger Zeuge erforderlich, ändern sich Bestand und eingegangene Mengen erst nach dessen Bestätigung in der Bestandsverwaltung.
+ Verwenden Sie für jeden Vermögensgegenstand mit Seriennummer eine eigene Zeile mit Menge 1 und seiner Seriennummer. Fügen Sie Zeilen hinzu, um weitere Gegenstände zu erfassen oder Mengen auf Lagerorte oder Chargen aufzuteilen.
+ Zu empfangende Menge
+ Position entfernen
+ Wareneingänge
+ Neue Bestellung
+ Die Bewertung umfasst nur Bestände und Lagerorte, die Sie einsehen dürfen. Summen werden für jede Währung getrennt ausgewiesen. Zeilen mit unbekannten Kosten werden separat gezählt und nicht in die Summen der bekannten Werte einbezogen.
+ Stand (UTC)
+ Einige sichtbare Lagerbestände sind negativ. Prüfen Sie diese, bevor Sie die Bewertung als Grundlage verwenden.
+ Bekannter Wert
+ Zeilen ohne Kostenangabe
+ Währung nicht angegeben
+ Kosten unbekannt
+ Durchschnittliche Stückkosten
+ Bevorzugter Lieferant
+ Verwenden Sie pro Artikel eine Währung. Bestehende Bestände ohne Währungsangabe bleiben bei der Bewertung getrennt; das Zuweisen einer Währung rechnet die Kosten nicht um.
+ Bestellung für den Lagerbestand eingegangen
+ Bestandszählungen, Warnungen und Berichte
+ Bestandszählungen
+ Inventarwarnungen
+ Inventarberichte
+ Erstellen Sie eine Momentaufnahme von bis zu 100 Bestandspositionen. Erfassen Sie jede Menge, speichern Sie die Zählung und schließen Sie sie dann ab. Wenn sich Bestands- oder Katalogdaten ändern, brechen Sie die Zählung ab und beginnen Sie eine neue. Erfassen Sie neu aufgefundene Ausrüstung mit Seriennummer über „Einlagern“.
+ Alle Lagerorte
+ Zählung starten
+ Zählung abschließen
+ Zählung abbrechen
+ Momentaufnahme vom (UTC)
+ Sollbestand
+ Gezählter Bestand
+ Abweichung
+ Positionen mit einer Abweichung
+ Entwurf
+ Wartet auf Zeugenbestätigung
+ Abgeschlossen
+ Abgebrochen
+ Ein unabhängiger Zeuge muss den ausstehenden Vorgang im Inventarverlauf bestätigen. Die Momentaufnahme muss weiterhin aktuell sein.
+ Warnungen zeigen niedrige Bestände, in den nächsten 30 Tagen ablaufendes Inventar, abgelaufenes Inventar und überfällige Rückgaben. Aktualisieren Sie die Warnungen einmal nach der Einführung, um Bestandswarnungen zu initialisieren, sowie nach dem Import von Altbeständen. Spätere Bestands- und Artikeländerungen aktualisieren sie automatisch.
+ Warnungen aktualisieren
+ Niedriger Bestand
+ Läuft bald ab
+ Abgelaufen
+ Überfällige Rückgabe
+ Offen
+ Erledigt
+ Fällig am (UTC)
+ Wählen Sie einen Bericht und bei Bedarf Filter aus. Datumsangaben verwenden UTC; das Enddatum ist ausgeschlossen. Momentaufnahmeberichte verwenden die aktuellen Bestände. Laden Sie die PDF-Datei herunter, um sie anzuzeigen oder zu drucken.
+ Startdatum (UTC, einschließlich)
+ Enddatum (UTC, ausschließlich)
+ Druckbare PDF herunterladen
+ Berichtsversand planen
+ Inventarwarnungen
+ Das Inventar erfordert Ihre Aufmerksamkeit. Melden Sie sich an, um die aktuellen Warnungen zu prüfen.
+ Aktueller Inventarbestand
+ Inventarverbrauch
+ Ablaufdaten des Inventars
+ Niedrige Inventarbestände
+ Umlagerungsverlauf des Inventars
+ Ausgabeverlauf des Inventars
+ Bestandsbewertung
+ Protokoll kontrollierter Substanzen
+ Tage bis zum Ablauf
+ Fehlmenge zum Meldebestand
+ Vorgeschlagene Nachbestellmenge
+ Ausgabe-ID
+ ID der empfangenden Person
+ ID der empfangenden Einheit
+ Ausgegeben am (UTC)
+ Zurückgegebene Menge
+ Ausstehende Menge
+ Zurückgegeben am (UTC)
+ Durchgeführt von (Benutzer-ID)
+ Storniert Transaktion
+ Verbrauchsquelle
+ Datensatz-ID
+ Einsatz-ID
+ ID der geänderten Revision
+ DEA-Einstufung
+ Quellbestand vorher
+ Quellbestand nachher
+ Zielbestand vorher
+ Zielbestand nachher
+ Zeuge (Benutzer-ID)
+ Bestätigt am (UTC)
+ Das Inventar enthält geschützte Daten. Öffnen Sie den Berichtslink mit Anmeldung und bestätigen Sie Ihre Identität, um aktuelle Ergebnisse anzuzeigen oder zu drucken. Diese planmäßig erstellte PDF-Datei enthält keine Inventarwerte.
+ Dieser Bericht enthält nur Inventar und Lagerorte, für die Sie aktuell eine Anzeigeberechtigung haben. Mengen und Werte beziehen sich auf die angegebene Momentaufnahme. Die Ergebnisse sind auf 5.000 Zeilen begrenzt.
+ UTC-Zeitraum (Start einschließlich, Ende ausschließlich)
+ Jede Bestandsbewegung erscheint einmal. Positive Mengen erfassen ausgehenden Verbrauch; negative Mengen machen ihn rückgängig. Für Datensatzdetails ist Zugriff auf die Quelle erforderlich. Importierte ausgehende Bewegungen behalten ihre ursprüngliche Kennung und stellen keine Verknüpfung mit einem Protokoll oder Patienten her.
+ Aktive Artikel werden aufgeführt, wenn der sichtbare Mengenbestand oder Bestand mit Seriennummer den Meldebestand erreicht oder unterschreitet. Ist kein Meldebestand festgelegt, gilt der Mindestbestand. Bestände mit Seriennummer umfassen einsatzbereite, ausgegebene, beschädigte und in Reparatur befindliche Ausrüstung. Ausgeblendete Lagerorte werden ausgeschlossen.
+ Die Zeilen zeigen Ausgaben, die im ausgewählten Zeitraum erstellt wurden, und ihren aktuellen Rückgabestatus. Für ausstehende Mengen älterer Ausgaben muss ein früheres Startdatum gewählt werden.
+ Die Zeilen zeigen aktuelle positive Bestände und aktive Bestände mit Seriennummer, deren Ablaufdatum im ausgewählten Zeitraum liegt. Standardmäßig werden bereits abgelaufenes Inventar und Inventar, das in den nächsten 30 Tagen abläuft, angezeigt.
+ Unterschrift der ausführenden Person
+ Unterschrift des Zeugen
+ Nicht erfasst
+ Inventarbewegung ohne zugängliche Datensatzverknüpfung
+ Datensätze
+ Übernommene Inventarbewegung
+ Geben Sie gültige Zählungsdaten und Mengen ein.
+ Wählen Sie einen kleineren Bereich aus. Zählungen unterstützen bis zu 100 Bestandspositionen.
+ In diesem Bereich gibt es keine aktiven Bestandspositionen zu zählen.
+ Diese Zählung existiert bereits. Öffnen Sie sie über die Liste der Zählungen.
+ Die Zählung wurde geändert oder enthält unvollständige Erfassungen. Laden Sie sie erneut und speichern Sie vor dem Abschluss alle Mengen.
+ Das Inventar hat sich seit dieser Momentaufnahme geändert. Brechen Sie diese Zählung ab und beginnen Sie eine neue.
+ Zählungskorrekturen gehören zur abgeschlossenen Zählung. Beginnen Sie eine neue Zählung, um den Bestand zu korrigieren.
+ Buchen Sie Zählungsabweichungen, indem Sie die Bestandszählung abschließen.
+ Wählen Sie einen gültigen UTC-Zeitraum von höchstens 366 Tagen.
+ Wählen Sie gültige Berichtsfilter aus.
+ Der Bericht hat sich während der PDF-Erstellung geändert. Erstellen Sie ihn erneut.
+ Der Bericht konnte nicht erstellt werden. Versuchen Sie es erneut.
+ Niedriger Lagerbestand
+ Inventar läuft ab
+ Inventurzählung abgeschlossen
+ Inventarrückgabe überfällig
+ Diese Ressource wird im Bestandsverlauf referenziert und muss erhalten bleiben.
+ Bestand angepasst
+ Bestandsübertragung abgeschlossen
+ Bestand ausgegeben
+ Bestand zurückgegeben
+ Status des Inventargegenstands geändert
+ Kontrollierte Substanz erfasst
-
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.el.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.el.resx
index b9ca8679..535a9d5c 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.el.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.el.resx
@@ -531,5 +531,179 @@
Ναι
+ Χρήση αποθεμάτων
+ Η αποθήκευση της χρήσης αλλάζει αμέσως το απόθεμα. Οι αλλαγές παραμένουν αν εγκαταλειφθεί ένα προσχέδιο ή μια τροποποίηση αναφοράς. Αντιλογίστε ρητά τη λανθασμένη χρήση και καταγράψτε την αντικατάστασή της. Ανοίξτε τροποποίηση πριν αλλάξετε τη χρήση σε ολοκληρωμένη αναφορά.
+ Η κατανάλωση και οι αντιλογισμοί ελεγχόμενων ειδών απαιτούν ανεξάρτητο μάρτυρα στα Αποθέματα. Ολοκληρώστε πρώτα αυτή τη διαδικασία και συνδέστε εδώ το αναγνωριστικό της συναλλαγής που προέκυψε.
+ Καταγεγραμμένη χρήση
+ Τύπος χρήσης
+ Χρησιμοποιήθηκε
+ Καταναλώθηκε σε ασθενή
+ Αφέθηκε στον τόπο του συμβάντος
+ Κατεστραμμένο
+ Εγγραφή στο καθολικό
+ Καταγράφηκε (UTC)
+ Αντιλογίζει τη χρήση
+ Αντιλογίστηκε
+ Αντιλογισμός χρήσης
+ Αυτή η ενέργεια καταγράφει πλήρη αντιλογισμό και διατηρεί την αρχική χρήση στο ιστορικό. Για να διορθώσετε ποσότητα ή είδος, αντιλογίστε τη χρήση και καταγράψτε την αντικατάστασή της. Αν ο αντιλογισμός έχει ήδη επιβεβαιωθεί από μάρτυρα στα Αποθέματα, εισαγάγετε το αναγνωριστικό της συναλλαγής του για να τον συνδέσετε χωρίς νέα αλλαγή αποθέματος.
+ Αιτία διόρθωσης
+ Αναγνωριστικό υπάρχοντος αντιλογισμού με επιβεβαίωση μάρτυρα (προαιρετικό)
+ Οι θετικές ποσότητες καταγράφουν χρήση και οι αρνητικές την αντιλογίζουν. Ανοίξτε την εγγραφή στο καθολικό για τα στοιχεία κίνησης σύμφωνα με την τρέχουσα πρόσβασή σας στα αποθέματα.
+ Αναγνωριστικό υπάρχουσας συναλλαγής κατανάλωσης (προαιρετικό)
+ Εισαγάγετε μια ολοκληρωμένη συναλλαγή κατανάλωσης από τα Αποθέματα για να συνδέσετε το είδος, την ποσότητα, την τοποθεσία και τη σημείωσή της. Τα παρακάτω πεδία νέας κατανάλωσης αγνοούνται και το απόθεμα δεν καταναλώνεται ξανά. Επιλέξτε τον τύπο χρήσης παραπάνω.
+ Νέα κατανάλωση
+ Χρήση μονάδας ή σταθμού της αναφοράς
+ Αφήστε κενό για να χρησιμοποιηθεί η μονάδα απόκρισης της αναφοράς, με τον σταθμό ως εναλλακτική. Επιλέξτε ρητά πηγή όταν η αναφορά έχει πολλές πιθανές τοποθεσίες.
+ Αποθήκευση χρήσης
+ Επιστροφή στην αναφορά
+ Ανανέωση τεκμηρίων από καταγεγραμμένη χρήση
+ Η χρήση αποθηκεύτηκε και καταγράφηκαν τα υποστηρικτικά τεκμήρια.
+ Η ενέργεια αποθέματος αποθηκεύτηκε. Δεν ήταν δυνατή η καταγραφή τεκμηρίων. Χρησιμοποιήστε την Ανανέωση τεκμηρίων αφού επιλύσετε τα προβλήματα πρόσβασης ή τις αλλαγές του προσχεδίου. Μην υποβάλετε ξανά την ενέργεια αποθέματος.
+ Το προσχέδιο άλλαξε. Ελέγξτε την καταγεγραμμένη χρήση πριν υποβάλετε άλλη ενέργεια αποθέματος.
+ Ολοκληρώστε τη διαδικασία με ανεξάρτητο μάρτυρα στα Αποθέματα και συνδέστε εδώ το αναγνωριστικό της συναλλαγής που προέκυψε. Αυτή η ενέργεια δεν κατέγραψε καμία κίνηση αποθέματος.
+ Τα υποστηρικτικά τεκμήρια καταγράφηκαν.
+ Ένας αντιλογισμός στο καθολικό δεν έχει συνδεθεί με αυτή την αναφορά. Συνδέστε τον ως διόρθωση πριν την οριστικοποίηση.
+ Αγορές
+ Προμηθευτές
+ Εντολές αγοράς
+ Αποτίμηση αποθέματος
+ Οι προμηθευτές χρησιμοποιούν εγγραφές εταιρειών από τις Επαφές. Δημιουργήστε ή ενημερώστε τα στοιχεία της εταιρείας στις Επαφές και συνδέστε την εδώ.
+ Αριθμός λογαριασμού στον προμηθευτή
+ Εταιρεία στις Επαφές
+ Προσθήκη προμηθευτή
+ Αριθμός εντολής αγοράς
+ Προμηθευτής
+ Νόμισμα
+ Προβολή εντολής αγοράς
+ Πρόχειρη
+ Έχει παραγγελθεί
+ Έχει παραληφθεί εν μέρει
+ Έχει παραληφθεί
+ Ακυρωμένη
+ Παραγγελθείσα ποσότητα
+ Παραληφθείσα ποσότητα
+ Υπόλοιπη ποσότητα
+ Η σήμανση μιας εντολής ως παραγγελθείσας κλειδώνει τις γραμμές της. Η ακύρωση εμποδίζει περαιτέρω παραλαβές και διατηρεί τις ποσότητες που έχουν ήδη παραληφθεί.
+ Σήμανση ως παραγγελθείσα
+ Ακύρωση εντολής
+ Παραλαβή ειδών
+ Παραλάβετε ολόκληρη την εντολή ή μέρος της. Αφαιρέστε τις γραμμές που δεν περιλαμβάνονται σε αυτή την παράδοση. Αν απαιτείται ανεξάρτητος μάρτυρας, το απόθεμα και οι παραληφθείσες ποσότητες αλλάζουν μόνο μετά την έγκρισή του στην Αποθήκη.
+ Χρησιμοποιήστε μία γραμμή για κάθε πάγιο με σειριακό αριθμό, με ποσότητα 1 και τον δικό του σειριακό αριθμό. Προσθέστε γραμμές για να παραλάβετε περισσότερα πάγια ή να κατανείμετε ποσότητες σε διαφορετικές τοποθεσίες ή παρτίδες.
+ Ποσότητα προς παραλαβή
+ Αφαίρεση γραμμής
+ Παραλαβές
+ Νέα εντολή αγοράς
+ Η αποτίμηση καλύπτει μόνο το απόθεμα και τις τοποθεσίες που έχετε δικαίωμα να βλέπετε. Τα σύνολα εμφανίζονται ξεχωριστά για κάθε νόμισμα. Οι γραμμές με άγνωστο κόστος καταμετρώνται ξεχωριστά και εξαιρούνται από τα σύνολα γνωστής αξίας.
+ Κατά την (UTC)
+ Ορισμένα ορατά υπόλοιπα αποθέματος είναι αρνητικά. Ελέγξτε τα πριν βασιστείτε σε αυτή την αποτίμηση.
+ Γνωστή αξία
+ Γραμμές χωρίς κόστος
+ Δεν έχει οριστεί νόμισμα
+ Άγνωστο κόστος
+ Μέσο κόστος μονάδας
+ Προτιμώμενος προμηθευτής
+ Χρησιμοποιήστε ένα νόμισμα ανά είδος. Το υπάρχον απόθεμα χωρίς νόμισμα παραμένει ξεχωριστό στην αποτίμηση· η αντιστοίχιση νομίσματος δεν μετατρέπει τα κόστη.
+ Παραλήφθηκε εντολή αγοράς αποθέματος
+ Καταμετρήσεις, ειδοποιήσεις και αναφορές
+ Καταμετρήσεις αποθέματος
+ Ειδοποιήσεις αποθέματος
+ Αναφορές αποθέματος
+ Ξεκινήστε ένα στιγμιότυπο με έως 100 θέσεις αποθέματος. Καταγράψτε κάθε ποσότητα, αποθηκεύστε και έπειτα ολοκληρώστε την καταμέτρηση. Αν αλλάξουν το απόθεμα ή τα στοιχεία του καταλόγου, ακυρώστε και ξεκινήστε νέα καταμέτρηση. Καταγράψτε εξοπλισμό με σειριακό αριθμό που εντοπίζεται για πρώτη φορά μέσω της Παραλαβής.
+ Όλες οι τοποθεσίες
+ Έναρξη καταμέτρησης
+ Ολοκλήρωση καταμέτρησης
+ Ακύρωση καταμέτρησης
+ Χρόνος στιγμιοτύπου (UTC)
+ Αναμενόμενη ποσότητα
+ Καταμετρημένη ποσότητα
+ Απόκλιση
+ Γραμμές με απόκλιση
+ Προσχέδιο
+ Αναμονή μάρτυρα
+ Ολοκληρώθηκε
+ Ακυρώθηκε
+ Ένας ανεξάρτητος μάρτυρας πρέπει να εγκρίνει την εκκρεμή ενέργεια από το ιστορικό Αποθεμάτων. Το στιγμιότυπο πρέπει να εξακολουθεί να ανταποκρίνεται στην τρέχουσα κατάσταση.
+ Οι ειδοποιήσεις εμφανίζουν χαμηλό απόθεμα, λήξη εντός 30 ημερών, ληγμένο απόθεμα και εκπρόθεσμες επιστροφές. Κάντε μία ανανέωση μετά την ενεργοποίηση της λειτουργίας για να αρχικοποιηθούν οι ειδοποιήσεις χαμηλού αποθέματος, καθώς και μετά την εισαγωγή παλαιού αποθέματος. Οι επόμενες αλλαγές στο απόθεμα και στα είδη τις ενημερώνουν αυτόματα.
+ Ανανέωση ειδοποιήσεων
+ Χαμηλό απόθεμα
+ Λήγει σύντομα
+ Έχει λήξει
+ Εκπρόθεσμη επιστροφή
+ Ανοιχτή
+ Επιλύθηκε
+ Προθεσμία (UTC)
+ Επιλέξτε αναφορά και προαιρετικά φίλτρα. Οι ημερομηνίες χρησιμοποιούν UTC και δεν περιλαμβάνουν την ημερομηνία λήξης του διαστήματος. Οι αναφορές στιγμιοτύπου χρησιμοποιούν τα τρέχοντα αποθέματα. Κατεβάστε το PDF για προβολή ή εκτύπωση.
+ Από ημερομηνία (UTC, περιλαμβάνεται)
+ Έως ημερομηνία (UTC, δεν περιλαμβάνεται)
+ Λήψη εκτυπώσιμου PDF
+ Προγραμματισμός αποστολής αναφορών
+ Ειδοποιήσεις αποθέματος
+ Το απόθεμα χρειάζεται έλεγχο. Συνδεθείτε για να δείτε τις τρέχουσες ειδοποιήσεις.
+ Διαθέσιμο απόθεμα
+ Χρήση αποθέματος
+ Λήξη αποθέματος
+ Χαμηλό απόθεμα
+ Ιστορικό μεταφορών αποθέματος
+ Ιστορικό χορηγήσεων αποθέματος
+ Αποτίμηση αποθέματος
+ Μητρώο ελεγχόμενων ουσιών
+ Ημέρες μέχρι τη λήξη
+ Ποσότητα που υπολείπεται του σημείου αναπαραγγελίας
+ Προτεινόμενη ποσότητα αναπαραγγελίας
+ Αναγνωριστικό χορήγησης
+ Αναγνωριστικό παραλήπτη
+ Αναγνωριστικό μονάδας παραλήπτη
+ Χρόνος χορήγησης (UTC)
+ Επιστραφείσα ποσότητα
+ Ποσότητα που δεν έχει επιστραφεί
+ Χρόνος επιστροφής (UTC)
+ Εκτελέστηκε από (αναγνωριστικό χρήστη)
+ Αντιλογίζει τη συναλλαγή
+ Πηγή χρήσης
+ Αναγνωριστικό εγγραφής
+ Αναγνωριστικό κλήσης
+ Αναγνωριστικό τροποποιημένης αναθεώρησης
+ Κατηγορία ελέγχου DEA
+ Υπόλοιπο προέλευσης πριν
+ Υπόλοιπο προέλευσης μετά
+ Υπόλοιπο προορισμού πριν
+ Υπόλοιπο προορισμού μετά
+ Μάρτυρας (αναγνωριστικό χρήστη)
+ Χρόνος επιβεβαίωσης μάρτυρα (UTC)
+ Το απόθεμα περιέχει προστατευμένα δεδομένα. Ανοίξτε τον σύνδεσμο της αναφοράς που απαιτεί σύνδεση και επαληθεύστε την ταυτότητά σας για να δείτε ή να εκτυπώσετε τα τρέχοντα αποτελέσματα. Αυτό το προγραμματισμένο PDF δεν περιέχει τιμές αποθέματος.
+ Αυτή η αναφορά περιλαμβάνει μόνο το απόθεμα και τις τοποθεσίες που έχετε δικαίωμα να βλέπετε τώρα. Οι ποσότητες και οι αξίες περιγράφουν το συγκεκριμένο στιγμιότυπο. Τα αποτελέσματα περιορίζονται σε 5.000 γραμμές.
+ Διάστημα UTC (περιλαμβάνεται η αρχή, εξαιρείται το τέλος)
+ Κάθε κίνηση αποθέματος εμφανίζεται μία φορά. Οι θετικές ποσότητες καταγράφουν εξερχόμενη χρήση και οι αρνητικές την αντιλογίζουν. Τα στοιχεία της εγγραφής απαιτούν πρόσβαση στην πηγή. Οι εισαγόμενες εξερχόμενες κινήσεις διατηρούν την παλαιά ταυτότητά τους και δεν δημιουργούν σύνδεση με ημερολόγιο συμβάντων ή ασθενή.
+ Τα ενεργά είδη εμφανίζονται όταν το ορατό απόθεμα που παρακολουθείται κατά ποσότητα ή τα πάγια με σειριακό αριθμό βρίσκονται στο σημείο αναπαραγγελίας ή κάτω από αυτό. Αν δεν έχει οριστεί σημείο αναπαραγγελίας, χρησιμοποιείται το ελάχιστο απόθεμα. Στα πάγια με σειριακό αριθμό περιλαμβάνονται όσα είναι σε υπηρεσία, έχουν χορηγηθεί, έχουν υποστεί ζημιά ή βρίσκονται σε επισκευή. Οι κρυφές τοποθεσίες εξαιρούνται.
+ Οι γραμμές εμφανίζουν τις χορηγήσεις που δημιουργήθηκαν στο επιλεγμένο χρονικό διάστημα και την τρέχουσα κατάσταση επιστροφής τους. Για ποσότητες που δεν έχουν επιστραφεί από παλαιότερες χορηγήσεις, επιλέξτε προγενέστερη ημερομηνία έναρξης.
+ Οι γραμμές εμφανίζουν το τρέχον θετικό απόθεμα και τα ενεργά πάγια με σειριακό αριθμό των οποίων η ημερομηνία λήξης βρίσκεται στο επιλεγμένο διάστημα. Η προεπιλογή περιλαμβάνει το ήδη ληγμένο απόθεμα και όσα λήγουν τις επόμενες 30 ημέρες.
+ Υπογραφή εκτελούντος
+ Υπογραφή μάρτυρα
+ Δεν καταγράφηκε
+ Κίνηση αποθέματος χωρίς προσβάσιμο σύνδεσμο εγγραφής
+ Εγγραφές
+ Παλαιά κίνηση αποθέματος
+ Εισαγάγετε έγκυρα στοιχεία καταμέτρησης και ποσότητες.
+ Επιλέξτε μικρότερο εύρος. Οι καταμετρήσεις υποστηρίζουν έως 100 θέσεις αποθέματος.
+ Δεν υπάρχουν ενεργές θέσεις αποθέματος προς καταμέτρηση σε αυτό το εύρος.
+ Αυτή η καταμέτρηση υπάρχει ήδη. Ανοίξτε την από τη λίστα καταμετρήσεων.
+ Η καταμέτρηση άλλαξε ή οι παρατηρήσεις της είναι ελλιπείς. Φορτώστε την ξανά και αποθηκεύστε όλες τις ποσότητες πριν την ολοκλήρωση.
+ Το απόθεμα άλλαξε μετά από αυτό το στιγμιότυπο. Ακυρώστε αυτή την καταμέτρηση και ξεκινήστε νέα.
+ Οι προσαρμογές καταμέτρησης ανήκουν στην αντίστοιχη ολοκληρωμένη καταμέτρηση. Ξεκινήστε νέα καταμέτρηση για να διορθώσετε το απόθεμα.
+ Καταχωρίστε τις αποκλίσεις ολοκληρώνοντας την καταμέτρηση αποθέματος.
+ Επιλέξτε έγκυρο διάστημα ημερομηνιών UTC που δεν υπερβαίνει τις 366 ημέρες.
+ Επιλέξτε έγκυρα φίλτρα αναφοράς.
+ Η αναφορά άλλαξε κατά τη δημιουργία του PDF. Δημιουργήστε την ξανά.
+ Δεν ήταν δυνατή η δημιουργία της αναφοράς. Δοκιμάστε ξανά.
+ Χαμηλό απόθεμα
+ Λήξη αποθέματος σύντομα
+ Ολοκλήρωση απογραφής
+ Εκπρόθεσμη επιστροφή εξοπλισμού
+ Αυτός ο πόρος αναφέρεται στο ιστορικό αποθέματος και πρέπει να διατηρηθεί.
+ Το απόθεμα προσαρμόστηκε
+ Η μεταφορά αποθέματος ολοκληρώθηκε
+ Το απόθεμα χορηγήθηκε
+ Το απόθεμα επιστράφηκε
+ Η κατάσταση του εξοπλισμού άλλαξε
+ Η ελεγχόμενη ουσία καταγράφηκε
-
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx
index f5df0ed4..70991670 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.en.resx
@@ -531,5 +531,179 @@
Yes
+ Inventory usage
+ Saving usage changes inventory immediately. These stock changes remain if a draft or amendment is abandoned. Reverse incorrect usage explicitly, then record the replacement. Open an amendment before changing usage on a completed report.
+ Controlled-item consumption and reversals require an independent witness in Inventory. Complete that process first, then attach the resulting transaction ID here.
+ Recorded usage
+ Usage type
+ Used
+ Consumed on patient
+ Left at scene
+ Damaged
+ Ledger entry
+ Recorded at (UTC)
+ Reverses usage
+ Reversed
+ Reverse usage
+ This records a full reversal and keeps the original usage in the history. To correct an amount or item, reverse it and record the replacement. If the reversal was already witnessed in Inventory, enter its transaction ID to attach it without changing stock again.
+ Reason for correction
+ Existing witnessed reversal transaction ID (optional)
+ Positive quantities record usage; negative quantities reverse it. Open the ledger entry for the movement details under your current inventory access.
+ Existing consumption transaction ID (optional)
+ Enter a completed Inventory consumption transaction to attach its item, quantity, location and note. The new-consumption fields below are ignored, and stock is not consumed again. Select the usage type above.
+ New consumption
+ Use report unit or station
+ Leave blank to use the report's responding unit, with the station as fallback. Choose a source explicitly when the report has multiple possible locations.
+ Save usage
+ Return to report
+ Refresh evidence from recorded usage
+ Usage saved and supporting evidence captured.
+ The inventory action was saved. Evidence could not be captured; use Refresh evidence after resolving access or draft changes. Do not submit the inventory action again.
+ The draft changed. Check the recorded usage before submitting another inventory action.
+ Complete the independent witness process in Inventory, then attach the resulting transaction ID here. No stock movement was recorded by this action.
+ Supporting evidence captured.
+ A ledger reversal has not been linked to this report. Attach it as a correction before finalizing.
+ Purchasing
+ Suppliers
+ Purchase orders
+ Inventory valuation
+ Suppliers use company records from Contacts. Create or update company details in Contacts, then link the company here.
+ Supplier account number
+ Company in Contacts
+ Add supplier
+ Purchase order number
+ Supplier
+ Currency
+ View purchase order
+ Draft
+ Ordered
+ Partially received
+ Received
+ Cancelled
+ Quantity ordered
+ Quantity received
+ Remaining quantity
+ Marking an order as ordered locks its lines. Cancelling an order prevents further receipts and keeps quantities already received.
+ Mark as ordered
+ Cancel order
+ Receive items
+ Receive all or part of an order. Remove lines not included in this delivery. If an independent witness is required, stock and received quantities change only after approval in Inventory.
+ Use one row for each serialized asset, with quantity 1 and its own serial number. Add rows to receive more assets or split quantities across locations or lots.
+ Quantity to receive
+ Remove line
+ Receipts
+ New purchase order
+ Valuation covers only the inventory and locations you are authorized to view. Totals are separate for each currency. Rows with an unknown cost are counted separately and excluded from known value totals.
+ As of (UTC)
+ Some visible stock balances are negative. Review them before relying on this valuation.
+ Known value
+ Rows without a cost
+ Currency not specified
+ Unknown cost
+ Average unit cost
+ Preferred vendor
+ Use one currency per item. Existing inventory without a currency stays separate in valuation; assigning a currency does not convert costs.
+ Inventory purchase order received
+ Counts, alerts and reports
+ Stock counts
+ Inventory alerts
+ Inventory reports
+ Start a snapshot of up to 100 positions. Record every quantity, save, then complete. If stock or catalog data changes, cancel and start a fresh count. Record newly discovered serialized equipment through Receive.
+ All locations
+ Start count
+ Complete count
+ Cancel count
+ Snapshot at (UTC)
+ Expected
+ Counted
+ Variance
+ Lines with a variance
+ Draft
+ Awaiting witness
+ Completed
+ Cancelled
+ An independent witness must approve the pending operation from Inventory history. The snapshot must still be current.
+ Alerts show low stock, expiration within 30 days, expired inventory and overdue returns. Refresh once after rollout to initialize low-stock alerts, and after importing legacy inventory. Later stock and item edits update them automatically.
+ Refresh alerts
+ Low stock
+ Expiring soon
+ Expired
+ Overdue return
+ Open
+ Resolved
+ Due at (UTC)
+ Select a report and optional filters. Dates use UTC with an exclusive end date. Snapshot reports use current holdings. Download the PDF to view or print it.
+ From date (UTC, inclusive)
+ Until date (UTC, exclusive)
+ Download printable PDF
+ Schedule report delivery
+ Inventory alerts
+ Inventory needs attention. Sign in to review current alerts.
+ Inventory on hand
+ Inventory usage
+ Inventory expiration
+ Low inventory stock
+ Inventory transfer history
+ Inventory issuance history
+ Inventory valuation
+ Controlled substance log
+ Days until expiration
+ Quantity below reorder point
+ Suggested reorder quantity
+ Issuance ID
+ Recipient person ID
+ Recipient unit ID
+ Issued at (UTC)
+ Returned quantity
+ Outstanding quantity
+ Returned at (UTC)
+ Performed by (user ID)
+ Reverses transaction
+ Usage source
+ Record ID
+ Call ID
+ Amended revision ID
+ DEA schedule
+ Source balance before
+ Source balance after
+ Destination balance before
+ Destination balance after
+ Witness (user ID)
+ Witnessed at (UTC)
+ Inventory contains protected data. Open the authenticated report link and verify your identity to view or print current results. This scheduled PDF contains no inventory values.
+ This report includes only inventory and locations you are currently authorized to view. Quantities and values describe the stated snapshot; results are limited to 5,000 rows.
+ UTC range (inclusive start, exclusive end)
+ Each stock movement appears once. Positive quantities record outward usage; negative quantities reverse it. Record details require access to the source. Imported outward movements retain their legacy identity and do not establish a link to a Log or patient.
+ Active items are listed when visible bulk stock or serialized holdings are at or below the reorder point, falling back to minimum stock. Serialized holdings include in-service, issued, damaged and repair assets. Hidden locations are excluded.
+ Rows show issuances created within the selected date range and their current return status. Outstanding quantities on older issuances require an earlier start date.
+ Rows show current positive stock and active serialized holdings with an expiration date in the selected range. The default includes already expired inventory and inventory expiring in the next 30 days.
+ Performer signature
+ Witness signature
+ Not recorded
+ Inventory movement without an accessible Record link
+ Records
+ Legacy inventory movement
+ Enter valid count details and quantities.
+ Select a smaller scope. Counts support up to 100 positions.
+ There are no active inventory positions to count in this scope.
+ This count already exists. Open it from the counts list.
+ The count changed or has incomplete observations. Reload it and save all quantities before completion.
+ Inventory changed after this snapshot. Cancel this count and start a fresh one.
+ Count adjustments belong to their completed count. Start a new count to correct stock.
+ Post count variances by completing the stock count.
+ Select a valid UTC date range of no more than 366 days.
+ Select valid report filters.
+ The report changed while generating the PDF. Generate it again.
+ The report could not be generated. Try again.
+ Inventory low stock
+ Inventory expiring
+ Inventory count completed
+ Inventory return overdue
+ This resource is referenced by inventory history and must be retained.
+ Inventory adjusted
+ Inventory transfer completed
+ Inventory issued
+ Inventory returned
+ Inventory asset status changed
+ Controlled substance recorded
-
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx
index 7ed02997..98ce8ab4 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.es.resx
@@ -472,5 +472,179 @@
Sí
+ Uso de inventario
+ Guardar el uso modifica el inventario de inmediato. Estos cambios en las existencias permanecen si se abandona un borrador o una enmienda. Revierta explícitamente el uso incorrecto y luego registre el reemplazo. Abra una enmienda antes de cambiar el uso de un informe finalizado.
+ El consumo y las reversiones de artículos controlados requieren un testigo independiente en Inventario. Complete primero ese proceso y luego vincule aquí el ID de la transacción resultante.
+ Uso registrado
+ Tipo de uso
+ Utilizado
+ Consumido en un paciente
+ Dejado en el lugar
+ Dañado
+ Asiento del libro de movimientos
+ Registrado (UTC)
+ Revierte el uso
+ Revertido
+ Revertir uso
+ Esto registra una reversión completa y conserva el uso original en el historial. Para corregir una cantidad o un artículo, reviértalo y registre el reemplazo. Si la reversión ya fue validada por un testigo en Inventario, ingrese su ID de transacción para vincularla sin modificar las existencias otra vez.
+ Motivo de la corrección
+ ID de reversión existente validada por un testigo (opcional)
+ Las cantidades positivas registran uso; las negativas lo revierten. Abra el asiento para ver los detalles del movimiento según sus permisos actuales de inventario.
+ ID de la transacción de consumo existente (opcional)
+ Ingrese una transacción de consumo completada en Inventario para vincular su artículo, cantidad, ubicación y nota. Los campos de nuevo consumo que aparecen abajo se ignoran y las existencias no se consumen otra vez. Seleccione el tipo de uso arriba.
+ Nuevo consumo
+ Usar unidad o estación del informe
+ Deje en blanco para usar la unidad que respondió según el informe, con la estación como alternativa. Elija un origen explícitamente si el informe tiene varias ubicaciones posibles.
+ Guardar uso
+ Volver al informe
+ Actualizar evidencia del uso registrado
+ Uso guardado y evidencia de respaldo capturada.
+ La acción de inventario se guardó. No se pudo capturar la evidencia; use Actualizar evidencia después de resolver los problemas de acceso o los cambios del borrador. No vuelva a enviar la acción de inventario.
+ El borrador cambió. Revise el uso registrado antes de enviar otra acción de inventario.
+ Complete el proceso de testigo independiente en Inventario y luego vincule aquí el ID de la transacción resultante. Esta acción no registró ningún movimiento de existencias.
+ Evidencia de respaldo capturada.
+ Hay una reversión en el libro de movimientos sin vincular a este informe. Vincúlela como corrección antes de finalizar.
+ Compras
+ Proveedores
+ Órdenes de compra
+ Valoración del inventario
+ Los proveedores utilizan los registros de empresas de Contactos. Cree o actualice los datos de la empresa en Contactos y luego vincúlela aquí.
+ Número de cuenta con el proveedor
+ Empresa en Contactos
+ Agregar proveedor
+ Número de orden de compra
+ Proveedor
+ Moneda
+ Ver orden de compra
+ Borrador
+ Ordenada
+ Recibida parcialmente
+ Recibida
+ Cancelada
+ Cantidad ordenada
+ Cantidad recibida
+ Cantidad pendiente
+ Al marcar una orden como ordenada, sus partidas quedan bloqueadas. Cancelar una orden impide nuevas recepciones y conserva las cantidades ya recibidas.
+ Marcar como ordenada
+ Cancelar orden
+ Recibir artículos
+ Reciba la orden completa o una parte. Quite las partidas que no se incluyen en esta entrega. Si se requiere un testigo independiente, las existencias y las cantidades recibidas solo cambiarán después de su aprobación en Inventario.
+ Use una fila para cada activo con número de serie, con cantidad 1 y su propio número de serie. Agregue filas para recibir más activos o distribuir cantidades entre ubicaciones o lotes.
+ Cantidad por recibir
+ Quitar partida
+ Recepciones
+ Nueva orden de compra
+ La valoración incluye únicamente el inventario y las ubicaciones que tiene autorización para ver. Los totales se presentan por separado para cada moneda. Las filas con costo desconocido se cuentan por separado y se excluyen de los totales de valor conocido.
+ Al (UTC)
+ Algunas existencias visibles son negativas. Revíselas antes de basarse en esta valoración.
+ Valor conocido
+ Filas sin costo
+ Moneda no especificada
+ Costo desconocido
+ Costo unitario promedio
+ Proveedor preferido
+ Use una sola moneda por artículo. El inventario existente sin moneda se mantiene separado en la valoración; asignar una moneda no convierte los costos.
+ Orden de compra de inventario recibida
+ Recuentos, alertas e informes
+ Recuentos de existencias
+ Alertas de inventario
+ Informes de inventario
+ Inicie un recuento del estado actual de hasta 100 posiciones de stock. Anote todas las cantidades, guarde y complete el recuento. Si cambian las existencias o los datos del catálogo, cancele e inicie un recuento nuevo. Registre los equipos con número de serie recién encontrados mediante Recibir artículos.
+ Todas las ubicaciones
+ Iniciar recuento
+ Completar recuento
+ Cancelar recuento
+ Estado capturado el (UTC)
+ Cantidad prevista
+ Cantidad contada
+ Diferencia
+ Líneas con diferencias
+ Borrador
+ Pendiente de testigo
+ Completado
+ Cancelado
+ Un testigo independiente debe aprobar la operación pendiente desde el historial de inventario. El estado capturado debe seguir vigente.
+ Las alertas muestran existencias bajas, artículos que caducan en los próximos 30 días, artículos caducados y devoluciones vencidas. Actualícelas una vez tras la puesta en marcha para inicializar las alertas de existencias bajas y después de importar el inventario anterior. Los cambios posteriores en las existencias y los artículos las actualizan automáticamente.
+ Actualizar alertas
+ Existencias bajas
+ Próxima caducidad
+ Caducado
+ Devolución vencida
+ Abierta
+ Resuelta
+ Vencimiento (UTC)
+ Seleccione un informe y los filtros que desee. Las fechas se expresan en UTC y la fecha final no se incluye. Los informes del estado actual utilizan las existencias actuales. Descargue el PDF para verlo o imprimirlo.
+ Fecha inicial (UTC, incluida)
+ Fecha final (UTC, no incluida)
+ Descargar PDF para imprimir
+ Programar el envío de informes
+ Alertas de inventario
+ El inventario requiere atención. Inicie sesión para revisar las alertas actuales.
+ Existencias de inventario
+ Uso de inventario
+ Caducidad del inventario
+ Existencias bajas de inventario
+ Historial de transferencias de inventario
+ Historial de entregas de inventario
+ Valoración del inventario
+ Registro de sustancias controladas
+ Días hasta la caducidad
+ Cantidad que falta para el punto de reposición
+ Cantidad de reposición sugerida
+ ID de entrega
+ ID de la persona destinataria
+ ID de la unidad destinataria
+ Entregado el (UTC)
+ Cantidad devuelta
+ Cantidad pendiente de devolución
+ Devuelto el (UTC)
+ Realizado por (ID de usuario)
+ Revierte la transacción
+ Origen del uso
+ ID del registro
+ ID de llamada
+ ID de la revisión modificada
+ Clasificación de la DEA
+ Saldo de origen anterior
+ Saldo de origen posterior
+ Saldo de destino anterior
+ Saldo de destino posterior
+ Testigo (ID de usuario)
+ Atestiguado el (UTC)
+ El inventario contiene datos protegidos. Abra el enlace del informe que requiere autenticación y verifique su identidad para ver o imprimir los resultados actuales. Este PDF programado no contiene valores del inventario.
+ Este informe solo incluye el inventario y las ubicaciones que tiene autorización para consultar en este momento. Las cantidades y los valores reflejan el estado capturado en el momento indicado; los resultados se limitan a 5.000 filas.
+ Intervalo UTC (inicio incluido, fin no incluido)
+ Cada movimiento de existencias aparece una sola vez. Las cantidades positivas registran usos de salida; las negativas los revierten. Los detalles del registro requieren acceso al origen. Los movimientos de salida importados conservan su identidad anterior y no establecen un vínculo con un registro de actividad ni con un paciente.
+ Se muestran los artículos activos cuando las existencias visibles controladas por cantidad o por número de serie están en el punto de reposición o por debajo de él; si no se ha definido ese punto, se utiliza el stock mínimo. Las existencias con número de serie incluyen equipos en servicio, entregados, dañados y en reparación. Se excluyen las ubicaciones ocultas.
+ Las filas muestran las entregas creadas en el intervalo de fechas seleccionado y su estado de devolución actual. Para consultar cantidades pendientes de entregas anteriores, seleccione una fecha inicial más antigua.
+ Las filas muestran las existencias actuales con cantidad positiva y los equipos activos con número de serie cuya fecha de caducidad está en el intervalo seleccionado. De forma predeterminada, se incluyen los artículos ya caducados y los que caducan en los próximos 30 días.
+ Firma de quien realizó la operación
+ Firma del testigo
+ No registrado
+ Movimiento de inventario sin vínculo accesible a un registro
+ Registros
+ Movimiento del inventario anterior
+ Introduzca datos y cantidades válidos para el recuento.
+ Seleccione un ámbito más pequeño. Los recuentos admiten hasta 100 posiciones de stock.
+ No hay posiciones de inventario activas que contar en este ámbito.
+ Este recuento ya existe. Ábralo desde la lista de recuentos.
+ El recuento ha cambiado o contiene observaciones incompletas. Vuelva a cargarlo y guarde todas las cantidades antes de completarlo.
+ El inventario ha cambiado desde que se capturó este estado. Cancele el recuento e inicie uno nuevo.
+ Los ajustes del recuento pertenecen al recuento completado. Inicie un recuento nuevo para corregir las existencias.
+ Complete el recuento de existencias para registrar las diferencias.
+ Seleccione un intervalo de fechas UTC válido de no más de 366 días.
+ Seleccione filtros válidos para el informe.
+ El informe cambió durante la generación del PDF. Genérelo de nuevo.
+ No se pudo generar el informe. Inténtelo de nuevo.
+ Existencias bajas
+ Inventario próximo a vencer
+ Recuento de inventario completado
+ Devolución de inventario atrasada
+ Este recurso está vinculado al historial de inventario y debe conservarse.
+ Inventario ajustado
+ Transferencia de inventario completada
+ Inventario entregado
+ Inventario devuelto
+ Estado del activo de inventario actualizado
+ Sustancia controlada registrada
-
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx
index 754fc388..cc15c2db 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.fr.resx
@@ -472,5 +472,179 @@
Oui
+ Utilisation du stock
+ Enregistrer l'utilisation modifie immédiatement le stock. Ces changements persistent si un brouillon ou un amendement est abandonné. Annulez explicitement l'utilisation incorrecte, puis enregistrez son remplacement. Ouvrez un amendement avant de modifier l'utilisation dans un rapport finalisé.
+ La consommation et l'annulation d'articles réglementés nécessitent un témoin indépendant dans Inventaire. Terminez d'abord cette procédure, puis associez ici l'identifiant de la transaction obtenue.
+ Utilisation enregistrée
+ Type d'utilisation
+ Utilisé
+ Consommé pour un patient
+ Laissé sur les lieux
+ Endommagé
+ Écriture du registre
+ Enregistré (UTC)
+ Annule l'utilisation
+ Annulée
+ Annuler l'utilisation
+ Cette action enregistre une annulation complète et conserve l'utilisation d'origine dans l'historique. Pour corriger une quantité ou un article, annulez l'utilisation puis enregistrez son remplacement. Si l'annulation a déjà été validée par un témoin dans Inventaire, saisissez son identifiant de transaction pour l'associer sans modifier le stock à nouveau.
+ Motif de la correction
+ Identifiant d'annulation existante validée par un témoin (facultatif)
+ Les quantités positives enregistrent l'utilisation ; les quantités négatives l'annulent. Ouvrez l'écriture du registre pour consulter le mouvement selon vos droits d'accès actuels au stock.
+ Identifiant de la transaction de consommation existante (facultatif)
+ Saisissez une transaction de consommation terminée dans Inventaire pour associer son article, sa quantité, son emplacement et sa note. Les champs de nouvelle consommation ci-dessous sont ignorés et le stock n'est pas consommé une seconde fois. Sélectionnez le type d'utilisation ci-dessus.
+ Nouvelle consommation
+ Utiliser l'unité ou la caserne du rapport
+ Laissez vide pour utiliser l'unité intervenante du rapport, avec sa caserne comme solution de repli. Choisissez explicitement une source si le rapport présente plusieurs emplacements possibles.
+ Enregistrer l'utilisation
+ Revenir au rapport
+ Actualiser les justificatifs de l'utilisation enregistrée
+ Utilisation enregistrée et justificatifs capturés.
+ L'opération de stock a été enregistrée. Les justificatifs n'ont pas pu être capturés ; utilisez Actualiser les justificatifs après avoir résolu les problèmes d'accès ou les changements du brouillon. Ne soumettez pas à nouveau l'opération de stock.
+ Le brouillon a changé. Vérifiez l'utilisation enregistrée avant de soumettre une autre opération de stock.
+ Terminez la procédure avec un témoin indépendant dans Inventaire, puis associez ici l'identifiant de la transaction obtenue. Cette action n'a enregistré aucun mouvement de stock.
+ Justificatifs capturés.
+ Une écriture d'annulation du registre n'est pas liée à ce rapport. Associez-la en tant que correction avant de finaliser.
+ Achats
+ Fournisseurs
+ Bons de commande
+ Valorisation du stock
+ Les fournisseurs utilisent les fiches d’entreprise de Contacts. Créez ou mettez à jour les coordonnées de l’entreprise dans Contacts, puis associez-la ici.
+ Numéro de compte chez le fournisseur
+ Entreprise dans Contacts
+ Ajouter un fournisseur
+ Numéro du bon de commande
+ Fournisseur
+ Devise
+ Voir le bon de commande
+ Brouillon
+ Commandé
+ Partiellement réceptionné
+ Réceptionné
+ Annulé
+ Quantité commandée
+ Quantité réceptionnée
+ Quantité restante
+ Marquer une commande comme commandée verrouille ses lignes. L’annulation empêche toute nouvelle réception et conserve les quantités déjà réceptionnées.
+ Marquer comme commandé
+ Annuler la commande
+ Réceptionner les articles
+ Réceptionnez tout ou partie d’une commande. Retirez les lignes qui ne font pas partie de cette livraison. Si un témoin indépendant est requis, le stock et les quantités réceptionnées ne changent qu’après son approbation dans Inventaire.
+ Utilisez une ligne par équipement suivi par numéro de série, avec une quantité de 1 et son propre numéro de série. Ajoutez des lignes pour réceptionner d’autres équipements ou répartir les quantités entre plusieurs emplacements ou lots.
+ Quantité à réceptionner
+ Retirer la ligne
+ Réceptions
+ Nouveau bon de commande
+ La valorisation porte uniquement sur le stock et les emplacements que vous êtes autorisé à consulter. Les totaux sont séparés par devise. Les lignes dont le coût est inconnu sont comptées séparément et exclues des totaux de valeur connue.
+ Au (UTC)
+ Certains soldes de stock visibles sont négatifs. Vérifiez-les avant de vous appuyer sur cette valorisation.
+ Valeur connue
+ Lignes sans coût
+ Devise non précisée
+ Coût inconnu
+ Coût unitaire moyen
+ Fournisseur privilégié
+ Utilisez une seule devise par article. Le stock existant sans devise reste séparé dans la valorisation ; attribuer une devise ne convertit pas les coûts.
+ Bon de commande de stock réceptionné
+ Comptages, alertes et rapports
+ Comptages de stock
+ Alertes de stock
+ Rapports de stock
+ Relevez l’état actuel de 100 positions de stock au maximum. Saisissez toutes les quantités, enregistrez, puis terminez le comptage. Si le stock ou les données du catalogue changent, annulez et lancez un nouveau comptage. Enregistrez les équipements avec numéro de série nouvellement trouvés via Réceptionner les articles.
+ Tous les emplacements
+ Lancer le comptage
+ Terminer le comptage
+ Annuler le comptage
+ État relevé le (UTC)
+ Quantité attendue
+ Quantité comptée
+ Écart
+ Lignes présentant un écart
+ Brouillon
+ En attente d’un témoin
+ Terminé
+ Annulé
+ Un témoin indépendant doit approuver l’opération en attente depuis l’historique du stock. L’état relevé doit toujours être à jour.
+ Les alertes signalent les stocks bas, les péremptions dans les 30 prochains jours, les articles périmés et les retours en retard. Actualisez-les une fois après la mise en service pour initialiser les alertes de stock bas, ainsi qu’après l’importation de l’ancien inventaire. Les modifications ultérieures du stock et des articles les mettent à jour automatiquement.
+ Actualiser les alertes
+ Stock bas
+ Péremption prochaine
+ Périmé
+ Retour en retard
+ Ouverte
+ Résolue
+ Échéance (UTC)
+ Sélectionnez un rapport et, si nécessaire, des filtres. Les dates sont exprimées en UTC et la date de fin est exclue. Les rapports d’état utilisent le stock actuel. Téléchargez le PDF pour le consulter ou l’imprimer.
+ Date de début (UTC, incluse)
+ Date de fin (UTC, exclue)
+ Télécharger le PDF imprimable
+ Programmer l’envoi de rapports
+ Alertes de stock
+ Le stock nécessite votre attention. Connectez-vous pour consulter les alertes actuelles.
+ Stock disponible
+ Utilisation du stock
+ Péremption du stock
+ Articles en stock bas
+ Historique des transferts de stock
+ Historique des dotations
+ Valorisation du stock
+ Registre des substances contrôlées
+ Jours avant péremption
+ Quantité manquante pour atteindre le seuil de réapprovisionnement
+ Quantité de réapprovisionnement suggérée
+ ID de la dotation
+ ID de la personne destinataire
+ ID de l’unité destinataire
+ Attribué le (UTC)
+ Quantité retournée
+ Quantité restant à retourner
+ Retourné le (UTC)
+ Effectué par (ID utilisateur)
+ Annule le mouvement
+ Origine de l’utilisation
+ ID du dossier
+ ID de l’appel
+ ID de la révision modifiée
+ Classification DEA
+ Solde d’origine avant
+ Solde d’origine après
+ Solde de destination avant
+ Solde de destination après
+ Témoin (ID utilisateur)
+ Attesté le (UTC)
+ Le stock contient des données protégées. Ouvrez le lien du rapport nécessitant une authentification et vérifiez votre identité pour consulter ou imprimer les résultats actuels. Ce PDF programmé ne contient aucune valeur du stock.
+ Ce rapport inclut uniquement le stock et les emplacements que vous êtes actuellement autorisé à consulter. Les quantités et les valeurs correspondent à l’état relevé à la date indiquée ; les résultats sont limités à 5 000 lignes.
+ Intervalle UTC (début inclus, fin exclue)
+ Chaque mouvement de stock apparaît une seule fois. Les quantités positives enregistrent les sorties liées à une utilisation ; les quantités négatives les annulent. Les détails du dossier nécessitent un accès à la source. Les sorties importées conservent leur identité dans l’ancien système et ne créent aucun lien avec un journal d’activité ou un patient.
+ Les articles actifs sont affichés lorsque le stock visible suivi par quantité ou par numéro de série est inférieur ou égal au seuil de réapprovisionnement ; à défaut, le stock minimum est utilisé. Le stock suivi par numéro de série inclut les équipements en service, attribués, endommagés et en réparation. Les emplacements masqués sont exclus.
+ Les lignes présentent les dotations créées dans l’intervalle de dates sélectionné et leur état de retour actuel. Pour consulter les quantités restant à retourner sur des dotations plus anciennes, choisissez une date de début antérieure.
+ Les lignes présentent le stock actuel de quantité positive et les équipements actifs avec numéro de série dont la date de péremption se situe dans l’intervalle sélectionné. Par défaut, les articles déjà périmés et ceux qui périment dans les 30 prochains jours sont inclus.
+ Signature de l’auteur de l’opération
+ Signature du témoin
+ Non renseigné
+ Mouvement de stock sans lien accessible vers un dossier
+ Dossiers
+ Mouvement de l’ancien inventaire
+ Saisissez des informations et des quantités de comptage valides.
+ Sélectionnez un périmètre plus restreint. Les comptages acceptent jusqu’à 100 positions de stock.
+ Aucune position de stock active n’est à compter dans ce périmètre.
+ Ce comptage existe déjà. Ouvrez-le depuis la liste des comptages.
+ Le comptage a changé ou comporte des observations incomplètes. Rechargez-le et enregistrez toutes les quantités avant de le terminer.
+ Le stock a changé depuis le relevé de cet état. Annulez ce comptage et lancez-en un nouveau.
+ Les ajustements de comptage appartiennent au comptage terminé. Lancez un nouveau comptage pour corriger le stock.
+ Terminez le comptage de stock pour enregistrer les écarts.
+ Sélectionnez un intervalle de dates UTC valide de 366 jours au maximum.
+ Sélectionnez des filtres de rapport valides.
+ Le rapport a changé pendant la génération du PDF. Générez-le à nouveau.
+ Le rapport n’a pas pu être généré. Réessayez.
+ Stock faible
+ Expiration prochaine du stock
+ Inventaire terminé
+ Retour de matériel en retard
+ Cette ressource est référencée dans l’historique du stock et doit être conservée.
+ Stock ajusté
+ Transfert de stock terminé
+ Stock attribué
+ Stock retourné
+ Statut du bien en stock modifié
+ Substance contrôlée enregistrée
-
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx
index d8cb7d6c..8e5289d8 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.it.resx
@@ -472,5 +472,179 @@
Sì
+ Utilizzo delle scorte
+ Salvare l'utilizzo modifica subito le scorte. Queste modifiche rimangono se si abbandona una bozza o una modifica al rapporto. Stornare esplicitamente l'utilizzo errato, quindi registrare quello sostitutivo. Aprire una modifica prima di cambiare l'utilizzo in un rapporto finalizzato.
+ Il consumo e gli storni di articoli soggetti a controllo richiedono un testimone indipendente in Inventario. Completare prima questa procedura, quindi collegare qui l'ID della transazione risultante.
+ Utilizzo registrato
+ Tipo di utilizzo
+ Utilizzato
+ Consumato su un paziente
+ Lasciato sul posto
+ Danneggiato
+ Registrazione contabile
+ Registrato (UTC)
+ Storna l'utilizzo
+ Stornato
+ Storna utilizzo
+ Questa azione registra uno storno completo e conserva l'utilizzo originale nello storico. Per correggere una quantità o un articolo, stornare l'utilizzo e registrare quello sostitutivo. Se lo storno è già stato convalidato da un testimone in Inventario, inserire il suo ID di transazione per collegarlo senza modificare nuovamente le scorte.
+ Motivo della correzione
+ ID dello storno esistente convalidato da un testimone (facoltativo)
+ Le quantità positive registrano l'utilizzo; quelle negative lo stornano. Aprire la registrazione per visualizzare i dettagli del movimento in base agli attuali permessi di inventario.
+ ID della transazione di consumo esistente (facoltativo)
+ Inserire una transazione di consumo completata in Inventario per collegare articolo, quantità, posizione e nota. I campi di nuovo consumo sottostanti vengono ignorati e le scorte non vengono consumate di nuovo. Selezionare il tipo di utilizzo sopra.
+ Nuovo consumo
+ Usa unità o sede del rapporto
+ Lasciare vuoto per usare l'unità intervenuta indicata nel rapporto, con la sede come alternativa. Scegliere esplicitamente un'origine se il rapporto presenta più posizioni possibili.
+ Salva utilizzo
+ Torna al rapporto
+ Aggiorna documentazione dell'utilizzo registrato
+ Utilizzo salvato e documentazione di supporto acquisita.
+ L'operazione di inventario è stata salvata. Non è stato possibile acquisire la documentazione; usare Aggiorna documentazione dopo aver risolto i problemi di accesso o le modifiche alla bozza. Non inviare nuovamente l'operazione di inventario.
+ La bozza è cambiata. Controllare l'utilizzo registrato prima di inviare un'altra operazione di inventario.
+ Completare la procedura con un testimone indipendente in Inventario, quindi collegare qui l'ID della transazione risultante. Questa azione non ha registrato alcun movimento di magazzino.
+ Documentazione di supporto acquisita.
+ Uno storno nel registro non è stato collegato a questo rapporto. Collegarlo come correzione prima della finalizzazione.
+ Acquisti
+ Fornitori
+ Ordini di acquisto
+ Valutazione delle scorte
+ I fornitori utilizzano le schede aziendali di Contatti. Crea o aggiorna i dati dell’azienda in Contatti, quindi collegala qui.
+ Numero di conto presso il fornitore
+ Azienda in Contatti
+ Aggiungi fornitore
+ Numero dell’ordine di acquisto
+ Fornitore
+ Valuta
+ Visualizza ordine di acquisto
+ Bozza
+ Ordinato
+ Ricevuto parzialmente
+ Ricevuto
+ Annullato
+ Quantità ordinata
+ Quantità ricevuta
+ Quantità rimanente
+ Contrassegnare un ordine come ordinato ne blocca le righe. L’annullamento impedisce ulteriori ricezioni e mantiene le quantità già ricevute.
+ Contrassegna come ordinato
+ Annulla ordine
+ Ricevi articoli
+ Ricevi un ordine intero o una parte. Rimuovi le righe non incluse in questa consegna. Se è richiesto un testimone indipendente, le scorte e le quantità ricevute cambiano solo dopo la sua approvazione in Inventario.
+ Usa una riga per ogni bene con numero di serie, con quantità 1 e il suo numero di serie. Aggiungi righe per ricevere altri beni o suddividere le quantità tra ubicazioni o lotti.
+ Quantità da ricevere
+ Rimuovi riga
+ Ricezioni
+ Nuovo ordine di acquisto
+ La valutazione comprende solo le scorte e le ubicazioni che sei autorizzato a visualizzare. I totali sono separati per valuta. Le righe con costo sconosciuto vengono conteggiate separatamente ed escluse dai totali del valore noto.
+ Alla data (UTC)
+ Alcune giacenze visibili sono negative. Verificale prima di basarti su questa valutazione.
+ Valore noto
+ Righe senza costo
+ Valuta non specificata
+ Costo sconosciuto
+ Costo unitario medio
+ Fornitore preferito
+ Usa una sola valuta per articolo. Le scorte esistenti senza valuta rimangono separate nella valutazione; assegnare una valuta non converte i costi.
+ Ordine di acquisto per le scorte ricevuto
+ Conteggi, avvisi e report
+ Conteggi delle scorte
+ Avvisi dell’inventario
+ Report dell’inventario
+ Crea un’istantanea di un massimo di 100 posizioni di magazzino. Registra ogni quantità, salva e poi completa il conteggio. Se le scorte o i dati del catalogo cambiano, annulla il conteggio e avviane uno nuovo. Registra le nuove attrezzature con numero di serie rinvenute tramite Ricevi.
+ Tutte le ubicazioni
+ Avvia conteggio
+ Completa conteggio
+ Annulla conteggio
+ Istantanea del (UTC)
+ Quantità prevista
+ Quantità conteggiata
+ Scostamento
+ Righe con uno scostamento
+ Bozza
+ In attesa di attestazione
+ Completato
+ Annullato
+ Un testimone indipendente deve approvare l’operazione in sospeso dalla cronologia dell’inventario. L’istantanea deve essere ancora attuale.
+ Gli avvisi segnalano scorte basse, scadenze entro 30 giorni, scorte scadute e restituzioni in ritardo. Aggiorna gli avvisi una volta dopo l’introduzione della funzione per inizializzare quelli sulle scorte basse e dopo l’importazione dell’inventario preesistente. Le successive modifiche alle scorte e agli articoli li aggiornano automaticamente.
+ Aggiorna avvisi
+ Scorte basse
+ In scadenza
+ Scaduto
+ Restituzione in ritardo
+ Aperto
+ Risolto
+ Scadenza (UTC)
+ Seleziona un report ed eventuali filtri. Le date sono espresse in UTC e la data finale è esclusa. I report istantanei utilizzano le giacenze attuali. Scarica il PDF per visualizzarlo o stamparlo.
+ Data iniziale (UTC, inclusa)
+ Data finale (UTC, esclusa)
+ Scarica PDF stampabile
+ Programma l’invio dei report
+ Avvisi dell’inventario
+ L’inventario richiede attenzione. Accedi per esaminare gli avvisi attuali.
+ Giacenze dell’inventario
+ Utilizzo delle scorte
+ Scadenze dell’inventario
+ Scorte basse dell’inventario
+ Cronologia dei trasferimenti dell’inventario
+ Cronologia delle assegnazioni dell’inventario
+ Valutazione delle scorte
+ Registro delle sostanze controllate
+ Giorni alla scadenza
+ Quantità mancante al punto di riordino
+ Quantità di riordino suggerita
+ ID assegnazione
+ ID della persona destinataria
+ ID dell’unità destinataria
+ Assegnato il (UTC)
+ Quantità restituita
+ Quantità da restituire
+ Restituito il (UTC)
+ Eseguito da (ID utente)
+ Storna la transazione
+ Origine dell’utilizzo
+ ID del record
+ ID intervento
+ ID della revisione modificata
+ Classificazione DEA
+ Saldo di origine precedente
+ Saldo di origine successivo
+ Saldo di destinazione precedente
+ Saldo di destinazione successivo
+ Testimone (ID utente)
+ Attestato il (UTC)
+ L’inventario contiene dati protetti. Apri il link al report con autenticazione e verifica la tua identità per visualizzare o stampare i risultati attuali. Questo PDF programmato non contiene valori dell’inventario.
+ Questo report include solo le scorte e le ubicazioni che sei attualmente autorizzato a visualizzare. Quantità e valori si riferiscono all’istantanea indicata; i risultati sono limitati a 5.000 righe.
+ Intervallo UTC (inizio incluso, fine esclusa)
+ Ogni movimento di scorte compare una sola volta. Le quantità positive registrano l’utilizzo in uscita; quelle negative lo stornano. Per visualizzare i dettagli dei record è necessario l’accesso all’origine. I movimenti in uscita importati mantengono il proprio identificativo preesistente e non creano un collegamento a un registro o a un paziente.
+ Gli articoli attivi sono elencati quando le scorte visibili gestite per quantità o con numero di serie sono pari o inferiori al punto di riordino oppure, se non impostato, alla scorta minima. Le giacenze con numero di serie includono attrezzature in servizio, assegnate, danneggiate e in riparazione. Le ubicazioni nascoste sono escluse.
+ Le righe mostrano le assegnazioni create nell’intervallo di date selezionato e il relativo stato attuale di restituzione. Per le quantità ancora da restituire relative ad assegnazioni precedenti, seleziona una data iniziale anteriore.
+ Le righe mostrano le scorte positive attuali e le giacenze attive con numero di serie la cui scadenza rientra nell’intervallo selezionato. Per impostazione predefinita sono incluse le scorte già scadute e quelle in scadenza nei prossimi 30 giorni.
+ Firma dell’esecutore
+ Firma del testimone
+ Non registrato
+ Movimento di inventario senza collegamento accessibile a un record
+ Record
+ Movimento di inventario preesistente
+ Inserisci dettagli del conteggio e quantità validi.
+ Seleziona un ambito più ristretto. I conteggi supportano fino a 100 posizioni di magazzino.
+ In questo ambito non sono presenti posizioni di magazzino attive da conteggiare.
+ Questo conteggio esiste già. Aprilo dall’elenco dei conteggi.
+ Il conteggio è cambiato o contiene rilevazioni incomplete. Ricaricalo e salva tutte le quantità prima di completarlo.
+ L’inventario è cambiato dopo questa istantanea. Annulla questo conteggio e avviane uno nuovo.
+ Le rettifiche del conteggio appartengono al conteggio completato. Avvia un nuovo conteggio per correggere le scorte.
+ Registra gli scostamenti completando il conteggio delle scorte.
+ Seleziona un intervallo di date UTC valido di massimo 366 giorni.
+ Seleziona filtri validi per il report.
+ Il report è cambiato durante la generazione del PDF. Generalo di nuovo.
+ Impossibile generare il report. Riprova.
+ Scorte insufficienti
+ Inventario in scadenza
+ Conteggio inventario completato
+ Restituzione inventario scaduta
+ Questa risorsa è citata nello storico dell’inventario e deve essere conservata.
+ Inventario rettificato
+ Trasferimento di inventario completato
+ Inventario assegnato
+ Inventario restituito
+ Stato del bene inventariato modificato
+ Sostanza controllata registrata
-
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx
index e087ebe2..93831026 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.pl.resx
@@ -472,5 +472,179 @@
Tak
+ Zużycie zapasów
+ Zapisanie zużycia natychmiast zmienia stan zapasów. Zmiany te pozostają po porzuceniu wersji roboczej lub zmiany raportu. Jawnie cofnij nieprawidłowe zużycie, a następnie zapisz zastępujące je dane. Przed zmianą zużycia w ukończonym raporcie otwórz jego zmianę.
+ Zużycie i cofnięcia dotyczące artykułów kontrolowanych wymagają niezależnego świadka w Magazynie. Najpierw zakończ ten proces, a następnie powiąż tutaj identyfikator utworzonej transakcji.
+ Zapisane zużycie
+ Rodzaj wykorzystania
+ Wykorzystano
+ Zużyto u pacjenta
+ Pozostawiono na miejscu
+ Uszkodzono
+ Zapis w ewidencji
+ Zapisano (UTC)
+ Cofa zużycie
+ Cofnięto
+ Cofnij zużycie
+ Ta czynność zapisuje pełne cofnięcie i zachowuje pierwotne zużycie w historii. Aby poprawić ilość lub artykuł, cofnij zużycie i zapisz dane zastępcze. Jeśli cofnięcie zostało już potwierdzone przez świadka w Magazynie, podaj jego identyfikator transakcji, aby je powiązać bez ponownej zmiany zapasów.
+ Powód korekty
+ Identyfikator istniejącego cofnięcia potwierdzonego przez świadka (opcjonalnie)
+ Ilości dodatnie zapisują zużycie, a ujemne je cofają. Otwórz zapis w ewidencji, aby zobaczyć szczegóły ruchu zgodnie z aktualnymi uprawnieniami do magazynu.
+ Identyfikator istniejącej transakcji zużycia (opcjonalnie)
+ Podaj zakończoną transakcję zużycia z Magazynu, aby powiązać jej artykuł, ilość, lokalizację i notatkę. Poniższe pola nowego zużycia są ignorowane, a zapasy nie są zużywane ponownie. Wybierz rodzaj wykorzystania powyżej.
+ Nowe zużycie
+ Użyj jednostki lub stacji z raportu
+ Pozostaw puste, aby użyć jednostki interweniującej z raportu, a w razie potrzeby jej stacji. Wybierz źródło jawnie, jeśli raport ma kilka możliwych lokalizacji.
+ Zapisz zużycie
+ Wróć do raportu
+ Odśwież dokumentację zapisanego zużycia
+ Zużycie zapisano i utrwalono dokumentację potwierdzającą.
+ Operacja magazynowa została zapisana. Nie udało się utrwalić dokumentacji. Po rozwiązaniu problemów z dostępem lub zmianami wersji roboczej użyj opcji Odśwież dokumentację. Nie przesyłaj ponownie operacji magazynowej.
+ Wersja robocza uległa zmianie. Sprawdź zapisane zużycie przed przesłaniem kolejnej operacji magazynowej.
+ Zakończ proces z niezależnym świadkiem w Magazynie, a następnie powiąż tutaj identyfikator utworzonej transakcji. Ta czynność nie zarejestrowała żadnego ruchu magazynowego.
+ Utrwalono dokumentację potwierdzającą.
+ Cofnięcie w ewidencji nie zostało powiązane z tym raportem. Powiąż je jako korektę przed finalizacją.
+ Zakupy
+ Dostawcy
+ Zamówienia zakupu
+ Wycena zapasów
+ Dostawcy korzystają z wpisów firm w Kontaktach. Utwórz lub zaktualizuj dane firmy w Kontaktach, a następnie powiąż ją tutaj.
+ Numer konta u dostawcy
+ Firma z Kontaktów
+ Dodaj dostawcę
+ Numer zamówienia zakupu
+ Dostawca
+ Waluta
+ Wyświetl zamówienie zakupu
+ Wersja robocza
+ Zamówione
+ Częściowo odebrane
+ Odebrane
+ Anulowane
+ Zamówiona ilość
+ Odebrana ilość
+ Pozostała ilość
+ Oznaczenie zamówienia jako zamówionego blokuje jego pozycje. Anulowanie uniemożliwia kolejne przyjęcia i zachowuje ilości już odebrane.
+ Oznacz jako zamówione
+ Anuluj zamówienie
+ Przyjmij artykuły
+ Przyjmij całość lub część zamówienia. Usuń pozycje, których nie ma w tej dostawie. Jeśli wymagany jest niezależny świadek, stany magazynowe i odebrane ilości zmienią się dopiero po jego zatwierdzeniu w Magazynie.
+ Użyj osobnego wiersza dla każdego zasobu z numerem seryjnym, z ilością 1 i jego własnym numerem seryjnym. Dodaj wiersze, aby przyjąć więcej zasobów lub rozdzielić ilości między lokalizacje albo partie.
+ Ilość do przyjęcia
+ Usuń pozycję
+ Przyjęcia
+ Nowe zamówienie zakupu
+ Wycena obejmuje tylko zapasy i lokalizacje, do których masz uprawnienia do wglądu. Sumy są podawane oddzielnie dla każdej waluty. Wiersze z nieznanym kosztem są liczone osobno i pomijane w sumach znanej wartości.
+ Stan na (UTC)
+ Niektóre widoczne stany magazynowe są ujemne. Sprawdź je, zanim oprzesz się na tej wycenie.
+ Znana wartość
+ Wiersze bez kosztu
+ Nie określono waluty
+ Nieznany koszt
+ Średni koszt jednostkowy
+ Preferowany dostawca
+ Używaj jednej waluty dla każdego artykułu. Istniejące zapasy bez waluty pozostają osobno w wycenie; przypisanie waluty nie przelicza kosztów.
+ Odebrano zamówienie zakupu zapasów
+ Inwentaryzacje, alerty i raporty
+ Inwentaryzacje
+ Alerty inwentarza
+ Raporty inwentarza
+ Utwórz migawkę maksymalnie 100 pozycji magazynowych. Zapisz każdą ilość, zapisz inwentaryzację, a następnie ją zakończ. Jeśli zmienią się zapasy lub dane katalogowe, anuluj inwentaryzację i rozpocznij nową. Nowo odnaleziony sprzęt z numerem seryjnym zarejestruj za pomocą opcji Przyjmij.
+ Wszystkie lokalizacje
+ Rozpocznij inwentaryzację
+ Zakończ inwentaryzację
+ Anuluj inwentaryzację
+ Migawka z dnia (UTC)
+ Ilość oczekiwana
+ Ilość zliczona
+ Różnica
+ Pozycje z różnicą
+ Wersja robocza
+ Oczekuje na potwierdzenie świadka
+ Zakończona
+ Anulowana
+ Niezależny świadek musi zatwierdzić oczekującą operację w historii inwentarza. Migawka musi być nadal aktualna.
+ Alerty wskazują niskie stany magazynowe, zapasy tracące ważność w ciągu 30 dni, zapasy przeterminowane i zaległe zwroty. Odśwież alerty raz po wdrożeniu, aby zainicjować alerty niskiego stanu, oraz po zaimportowaniu wcześniejszego inwentarza. Późniejsze zmiany zapasów i artykułów aktualizują je automatycznie.
+ Odśwież alerty
+ Niski stan magazynowy
+ Wkrótce traci ważność
+ Przeterminowane
+ Zaległy zwrot
+ Otwarty
+ Rozwiązany
+ Termin (UTC)
+ Wybierz raport i opcjonalne filtry. Daty są podawane w UTC, a data końcowa jest wyłączona z zakresu. Raporty migawkowe korzystają z bieżących stanów. Pobierz plik PDF, aby go wyświetlić lub wydrukować.
+ Data początkowa (UTC, włącznie)
+ Data końcowa (UTC, wyłącznie)
+ Pobierz PDF do druku
+ Zaplanuj wysyłkę raportów
+ Alerty inwentarza
+ Inwentarz wymaga uwagi. Zaloguj się, aby sprawdzić aktualne alerty.
+ Bieżący stan inwentarza
+ Zużycie zapasów
+ Terminy ważności inwentarza
+ Niskie stany magazynowe
+ Historia przesunięć inwentarza
+ Historia wydań inwentarza
+ Wycena zapasów
+ Rejestr substancji kontrolowanych
+ Dni do upływu ważności
+ Brakująca ilość do progu ponownego zamówienia
+ Sugerowana ilość do ponownego zamówienia
+ Identyfikator wydania
+ Identyfikator osoby odbierającej
+ Identyfikator jednostki odbierającej
+ Wydano (UTC)
+ Zwrócona ilość
+ Ilość do zwrotu
+ Zwrócono (UTC)
+ Wykonawca (identyfikator użytkownika)
+ Odwraca transakcję
+ Źródło zużycia
+ Identyfikator rekordu
+ Identyfikator zdarzenia
+ Identyfikator zmienionej wersji
+ Klasyfikacja DEA
+ Stan źródłowy przed
+ Stan źródłowy po
+ Stan docelowy przed
+ Stan docelowy po
+ Świadek (identyfikator użytkownika)
+ Potwierdzono (UTC)
+ Inwentarz zawiera chronione dane. Otwórz link do raportu wymagający uwierzytelnienia i potwierdź swoją tożsamość, aby wyświetlić lub wydrukować bieżące wyniki. Ten zaplanowany plik PDF nie zawiera wartości inwentarza.
+ Ten raport obejmuje tylko inwentarz i lokalizacje, do których wyświetlania masz obecnie uprawnienia. Ilości i wartości odnoszą się do wskazanej migawki; wyniki są ograniczone do 5 000 wierszy.
+ Zakres UTC (początek włącznie, koniec wyłącznie)
+ Każdy ruch magazynowy pojawia się raz. Dodatnie ilości oznaczają zużycie wychodzące, a ujemne je odwracają. Szczegóły rekordu wymagają dostępu do źródła. Zaimportowane ruchy wychodzące zachowują swoje wcześniejsze identyfikatory i nie tworzą powiązania z dziennikiem ani pacjentem.
+ Aktywne artykuły są wyświetlane, gdy widoczne zapasy rozliczane ilościowo lub według numerów seryjnych są równe progowi ponownego zamówienia albo niższe od niego. Jeśli progu nie określono, używany jest stan minimalny. Zapasy z numerami seryjnymi obejmują sprzęt w użyciu, wydany, uszkodzony i w naprawie. Ukryte lokalizacje są pomijane.
+ Wiersze pokazują wydania utworzone w wybranym zakresie dat oraz ich bieżący status zwrotu. Aby uwzględnić niezwrócone ilości z wcześniejszych wydań, wybierz wcześniejszą datę początkową.
+ Wiersze pokazują bieżące dodatnie stany magazynowe oraz aktywne zapasy z numerami seryjnymi, których termin ważności mieści się w wybranym zakresie. Domyślnie uwzględniane są zapasy już przeterminowane i te, które stracą ważność w ciągu najbliższych 30 dni.
+ Podpis wykonawcy
+ Podpis świadka
+ Nie zarejestrowano
+ Ruch magazynowy bez dostępnego powiązania z rekordem
+ Rekordy
+ Wcześniejszy ruch magazynowy
+ Wprowadź prawidłowe dane inwentaryzacji i ilości.
+ Wybierz mniejszy zakres. Inwentaryzacja obsługuje maksymalnie 100 pozycji.
+ W tym zakresie nie ma aktywnych pozycji magazynowych do zliczenia.
+ Ta inwentaryzacja już istnieje. Otwórz ją z listy inwentaryzacji.
+ Inwentaryzacja uległa zmianie lub zawiera niepełne odczyty. Wczytaj ją ponownie i zapisz wszystkie ilości przed zakończeniem.
+ Inwentarz zmienił się od utworzenia tej migawki. Anuluj tę inwentaryzację i rozpocznij nową.
+ Korekty inwentaryzacyjne należą do zakończonej inwentaryzacji. Rozpocznij nową inwentaryzację, aby skorygować zapasy.
+ Zaksięguj różnice, kończąc inwentaryzację.
+ Wybierz prawidłowy zakres dat UTC nieprzekraczający 366 dni.
+ Wybierz prawidłowe filtry raportu.
+ Raport zmienił się podczas generowania pliku PDF. Wygeneruj go ponownie.
+ Nie udało się wygenerować raportu. Spróbuj ponownie.
+ Niski stan magazynowy
+ Zbliżający się termin ważności zapasów
+ Zakończono inwentaryzację
+ Zaległy zwrot wyposażenia
+ Ten zasób jest powiązany z historią magazynu i musi zostać zachowany.
+ Skorygowano stan magazynowy
+ Zakończono przesunięcie magazynowe
+ Wydano zapasy
+ Zwrócono zapasy
+ Zmieniono status składnika wyposażenia
+ Zarejestrowano substancję kontrolowaną
-
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx
index ee6772b8..20d2ce2f 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.sv.resx
@@ -472,5 +472,179 @@
Ja
+ Lageranvändning
+ När användningen sparas ändras lagersaldot omedelbart. Ändringarna kvarstår om ett utkast eller en rapportändring överges. Återför felaktig användning uttryckligen och registrera sedan ersättningen. Öppna en rapportändring innan du ändrar användningen i en slutförd rapport.
+ Förbrukning och återföring av kontrollerade artiklar kräver ett oberoende vittne i Lager. Slutför först den processen och koppla sedan det skapade transaktions-ID:t här.
+ Registrerad användning
+ Användningstyp
+ Använt
+ Förbrukat på patient
+ Lämnat på plats
+ Skadat
+ Lagerpost
+ Registrerat (UTC)
+ Återför användning
+ Återfört
+ Återför användning
+ Detta registrerar en fullständig återföring och behåller den ursprungliga användningen i historiken. För att korrigera en mängd eller artikel återför du användningen och registrerar ersättningen. Om återföringen redan har bekräftats av ett vittne i Lager anger du dess transaktions-ID för att koppla den utan att ändra lagret igen.
+ Orsak till korrigering
+ ID för befintlig återföring bekräftad av vittne (valfritt)
+ Positiva mängder registrerar användning; negativa mängder återför den. Öppna lagerposten för rörelsedetaljer enligt dina aktuella lagerbehörigheter.
+ ID för befintlig förbrukningstransaktion (valfritt)
+ Ange en slutförd förbrukningstransaktion från Lager för att koppla dess artikel, mängd, plats och anteckning. Fälten för ny förbrukning nedan ignoreras och lagret förbrukas inte igen. Välj användningstyp ovan.
+ Ny förbrukning
+ Använd rapportens enhet eller station
+ Lämna tomt för att använda rapportens insatsenhet, med stationen som reserv. Välj en källa uttryckligen om rapporten har flera möjliga platser.
+ Spara användning
+ Tillbaka till rapporten
+ Uppdatera underlag från registrerad användning
+ Användningen har sparats och underlaget har registrerats.
+ Lageråtgärden har sparats. Underlaget kunde inte registreras. Använd Uppdatera underlag när åtkomstproblem eller ändringar i utkastet har lösts. Skicka inte lageråtgärden igen.
+ Utkastet har ändrats. Kontrollera den registrerade användningen innan du skickar en ny lageråtgärd.
+ Slutför processen med ett oberoende vittne i Lager och koppla sedan det skapade transaktions-ID:t här. Ingen lagerrörelse registrerades av denna åtgärd.
+ Underlaget har registrerats.
+ En återföring i lagerregistret har inte kopplats till denna rapport. Koppla den som en korrigering innan rapporten slutförs.
+ Inköp
+ Leverantörer
+ Inköpsordrar
+ Lagervärdering
+ Leverantörer använder företagsposter från Kontakter. Skapa eller uppdatera företagets uppgifter i Kontakter och koppla sedan företaget här.
+ Kundnummer hos leverantören
+ Företag i Kontakter
+ Lägg till leverantör
+ Inköpsordernummer
+ Leverantör
+ Valuta
+ Visa inköpsorder
+ Utkast
+ Beställd
+ Delvis mottagen
+ Mottagen
+ Avbruten
+ Beställd mängd
+ Mottagen mängd
+ Återstående mängd
+ När en order markeras som beställd låses orderraderna. Om ordern avbryts stoppas fler inleveranser, medan redan mottagna mängder behålls.
+ Markera som beställd
+ Avbryt order
+ Ta emot artiklar
+ Ta emot hela eller delar av en order. Ta bort rader som inte ingår i leveransen. Om ett oberoende vittne krävs ändras lagersaldon och mottagna mängder först efter godkännande i Lager.
+ Använd en rad per tillgång med serienummer, med antal 1 och ett eget serienummer. Lägg till rader för att ta emot fler tillgångar eller fördela mängder mellan platser eller partier.
+ Mängd att ta emot
+ Ta bort rad
+ Inleveranser
+ Ny inköpsorder
+ Värderingen omfattar endast lager och platser som du har behörighet att se. Summor redovisas separat för varje valuta. Rader med okänd kostnad räknas separat och ingår inte i summorna för känt värde.
+ Gäller per (UTC)
+ Vissa synliga lagersaldon är negativa. Kontrollera dem innan du använder värderingen som underlag.
+ Känt värde
+ Rader utan kostnad
+ Valuta saknas
+ Okänd kostnad
+ Genomsnittlig styckkostnad
+ Förvald leverantör
+ Använd en valuta per artikel. Befintligt lager utan valuta hålls separat i värderingen; att ange en valuta räknar inte om kostnaderna.
+ Inköpsorder för lager mottagen
+ Inventeringar, varningar och rapporter
+ Inventeringar
+ Lagervarningar
+ Lagerrapporter
+ Skapa en ögonblicksbild av upp till 100 lagerpositioner. Registrera varje antal, spara och slutför sedan inventeringen. Om lagret eller kataloguppgifterna ändras ska du avbryta inventeringen och starta en ny. Registrera nyfunnen utrustning med serienummer via Ta emot.
+ Alla lagerplatser
+ Starta inventering
+ Slutför inventering
+ Avbryt inventering
+ Ögonblicksbild från (UTC)
+ Förväntat antal
+ Räknat antal
+ Avvikelse
+ Rader med avvikelse
+ Utkast
+ Inväntar vittnesintyg
+ Slutförd
+ Avbruten
+ Ett oberoende vittne måste godkänna den väntande åtgärden i lagerhistoriken. Ögonblicksbilden måste fortfarande vara aktuell.
+ Varningar visar låga lagernivåer, lager som förfaller inom 30 dagar, utgånget lager och försenade återlämningar. Uppdatera varningarna en gång efter införandet för att initiera varningar om låga lagernivåer, och efter import av äldre inventarier. Senare ändringar av lager och artiklar uppdaterar dem automatiskt.
+ Uppdatera varningar
+ Låg lagernivå
+ Förfaller snart
+ Utgånget
+ Försenad återlämning
+ Öppen
+ Åtgärdad
+ Förfaller (UTC)
+ Välj en rapport och eventuella filter. Datumen anges i UTC och slutdatumet är exkluderat. Ögonblicksrapporter använder aktuella innehav. Ladda ned PDF-filen för att visa eller skriva ut den.
+ Startdatum (UTC, inkluderat)
+ Slutdatum (UTC, exkluderat)
+ Ladda ned utskrivbar PDF
+ Schemalägg rapportutskick
+ Lagervarningar
+ Lagret behöver ses över. Logga in för att granska aktuella varningar.
+ Aktuellt lagersaldo
+ Lageranvändning
+ Lagrets utgångsdatum
+ Låga lagernivåer
+ Historik över lageröverföringar
+ Historik över lagerutlämningar
+ Lagervärdering
+ Journal över kontrollerade substanser
+ Dagar till utgångsdatum
+ Antal under beställningspunkten
+ Föreslaget beställningsantal
+ Utlämnings-ID
+ Mottagande persons ID
+ Mottagande enhets ID
+ Utlämnat (UTC)
+ Återlämnat antal
+ Antal att återlämna
+ Återlämnat (UTC)
+ Utfört av (användar-ID)
+ Återför transaktion
+ Användningskälla
+ Post-ID
+ Larm-ID
+ ID för ändrad revision
+ DEA-klassificering
+ Avsändande saldo före
+ Avsändande saldo efter
+ Mottagande saldo före
+ Mottagande saldo efter
+ Vittne (användar-ID)
+ Intygat (UTC)
+ Lagret innehåller skyddade uppgifter. Öppna rapportlänken som kräver inloggning och verifiera din identitet för att visa eller skriva ut aktuella resultat. Denna schemalagda PDF innehåller inga lagervärden.
+ Den här rapporten innehåller endast inventarier och lagerplatser som du för närvarande har behörighet att visa. Antal och värden avser den angivna ögonblicksbilden. Resultaten är begränsade till 5 000 rader.
+ UTC-intervall (inkluderad start, exkluderat slut)
+ Varje lagerrörelse visas en gång. Positiva antal registrerar utgående användning och negativa antal återför den. Postuppgifter kräver åtkomst till källan. Importerade utgående rörelser behåller sina äldre identifierare och skapar ingen koppling till en logg eller patient.
+ Aktiva artiklar listas när synligt mängdhanterat lager eller innehav med serienummer ligger på eller under beställningspunkten. Om ingen beställningspunkt finns används miniminivån. Innehav med serienummer omfattar utrustning i drift, utlämnad, skadad och under reparation. Dolda lagerplatser utesluts.
+ Raderna visar utlämningar som skapats inom det valda datumintervallet och deras aktuella återlämningsstatus. För att se antal som återstår att lämna tillbaka från äldre utlämningar måste du välja ett tidigare startdatum.
+ Raderna visar aktuella positiva lagersaldon och aktiva innehav med serienummer vars utgångsdatum ligger inom det valda intervallet. Som standard ingår redan utgånget lager och lager som förfaller under de kommande 30 dagarna.
+ Utförarens underskrift
+ Vittnets underskrift
+ Inte registrerat
+ Lagerrörelse utan åtkomlig postlänk
+ Poster
+ Äldre lagerrörelse
+ Ange giltiga inventeringsuppgifter och antal.
+ Välj en mindre omfattning. Inventeringar stöder upp till 100 lagerpositioner.
+ Det finns inga aktiva lagerpositioner att inventera inom denna omfattning.
+ Den här inventeringen finns redan. Öppna den från inventeringslistan.
+ Inventeringen har ändrats eller innehåller ofullständiga observationer. Läs in den igen och spara alla antal innan du slutför den.
+ Lagret har ändrats sedan denna ögonblicksbild. Avbryt inventeringen och starta en ny.
+ Inventeringsjusteringar hör till den slutförda inventeringen. Starta en ny inventering för att korrigera lagret.
+ Bokför inventeringsavvikelser genom att slutföra inventeringen.
+ Välj ett giltigt UTC-datumintervall på högst 366 dagar.
+ Välj giltiga rapportfilter.
+ Rapporten ändrades medan PDF-filen skapades. Skapa den igen.
+ Det gick inte att skapa rapporten. Försök igen.
+ Lågt lagersaldo
+ Lager med nära utgångsdatum
+ Inventering slutförd
+ Försenad återlämning
+ Den här resursen refereras i lagerhistoriken och måste behållas.
+ Lager justerat
+ Lageröverföring slutförd
+ Lager utlämnat
+ Lager återlämnat
+ Status för inventariet ändrad
+ Kontrollerad substans registrerad
-
diff --git a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx
index 1bf44b00..f30c4e65 100644
--- a/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx
+++ b/Core/Resgrid.Localization/Areas/User/Inventory/Inventory.uk.resx
@@ -472,5 +472,179 @@
Так
+ Використання запасів
+ Збереження використання негайно змінює залишок запасів. Ці зміни зберігаються, якщо відмовитися від чернетки або зміни звіту. Явно сторнуйте помилкове використання, а потім зареєструйте заміну. Відкрийте зміну звіту, перш ніж змінювати використання в завершеному звіті.
+ Витрачання та сторнування контрольованих товарів потребують незалежного свідка в розділі «Запаси». Спочатку завершіть цю процедуру, а потім прив’яжіть тут ідентифікатор створеної транзакції.
+ Зареєстроване використання
+ Тип використання
+ Використано
+ Витрачено на пацієнта
+ Залишено на місці події
+ Пошкоджено
+ Запис у журналі руху
+ Зареєстровано (UTC)
+ Сторнує використання
+ Сторновано
+ Сторнувати використання
+ Ця дія реєструє повне сторнування та зберігає початкове використання в історії. Щоб виправити кількість або товар, сторнуйте використання й зареєструйте заміну. Якщо сторнування вже підтверджене свідком у розділі «Запаси», укажіть ідентифікатор його транзакції, щоб прив’язати його без повторної зміни запасів.
+ Причина коригування
+ Ідентифікатор наявного сторнування, підтвердженого свідком (необов’язково)
+ Додатні кількості реєструють використання, від’ємні — сторнують його. Відкрийте запис у журналі, щоб переглянути відомості про рух відповідно до поточних прав доступу до запасів.
+ Ідентифікатор наявної транзакції витрачання (необов’язково)
+ Укажіть завершену транзакцію витрачання з розділу «Запаси», щоб прив’язати її товар, кількість, місце та примітку. Поля нового витрачання нижче ігноруються, і запаси не списуються повторно. Виберіть тип використання вище.
+ Нове витрачання
+ Використати підрозділ або станцію зі звіту
+ Залиште порожнім, щоб використати залучений підрозділ зі звіту, а за потреби — його станцію. Явно виберіть джерело, якщо звіт має кілька можливих місць.
+ Зберегти використання
+ Повернутися до звіту
+ Оновити підтвердні матеріали зареєстрованого використання
+ Використання збережено, підтвердні матеріали зафіксовано.
+ Операцію із запасами збережено. Не вдалося зафіксувати підтвердні матеріали. Скористайтеся дією «Оновити підтвердні матеріали» після усунення проблем із доступом або змінами чернетки. Не надсилайте операцію із запасами повторно.
+ Чернетку змінено. Перевірте зареєстроване використання, перш ніж надсилати іншу операцію із запасами.
+ Завершіть процедуру з незалежним свідком у розділі «Запаси», а потім прив’яжіть тут ідентифікатор створеної транзакції. Ця дія не зареєструвала руху запасів.
+ Підтвердні матеріали зафіксовано.
+ Сторнування в журналі ще не прив’язане до цього звіту. Прив’яжіть його як коригування перед завершенням звіту.
+ Закупівлі
+ Постачальники
+ Замовлення на закупівлю
+ Оцінка запасів
+ Для постачальників використовуються записи компаній із розділу «Контакти». Створіть або оновіть дані компанії в розділі «Контакти», а потім пов’яжіть її тут.
+ Номер рахунку в постачальника
+ Компанія з розділу «Контакти»
+ Додати постачальника
+ Номер замовлення на закупівлю
+ Постачальник
+ Валюта
+ Переглянути замовлення на закупівлю
+ Чернетка
+ Замовлено
+ Частково отримано
+ Отримано
+ Скасовано
+ Замовлена кількість
+ Отримана кількість
+ Залишок до отримання
+ Позначення замовлення як замовленого блокує його позиції. Скасування унеможливлює подальші надходження та зберігає вже отримані кількості.
+ Позначити як замовлене
+ Скасувати замовлення
+ Прийняти товари
+ Прийміть усе замовлення або його частину. Видаліть позиції, яких немає в цій поставці. Якщо потрібен незалежний свідок, залишки запасів та отримані кількості зміняться лише після його підтвердження в розділі «Запаси».
+ Використовуйте окремий рядок для кожного активу із серійним номером, із кількістю 1 та його власним серійним номером. Додайте рядки, щоб прийняти більше активів або розподілити кількості між місцями зберігання чи партіями.
+ Кількість до приймання
+ Видалити позицію
+ Надходження
+ Нове замовлення на закупівлю
+ Оцінка охоплює лише запаси та місця зберігання, які ви маєте право переглядати. Підсумки подаються окремо для кожної валюти. Рядки з невідомою вартістю враховуються окремо та не включаються до підсумків відомої вартості.
+ Станом на (UTC)
+ Деякі видимі залишки запасів від’ємні. Перевірте їх, перш ніж покладатися на цю оцінку.
+ Відома вартість
+ Рядки без вартості
+ Валюту не вказано
+ Невідома вартість
+ Середня вартість одиниці
+ Бажаний постачальник
+ Використовуйте одну валюту для кожного товару. Наявні запаси без валюти залишаються окремо в оцінці; призначення валюти не перераховує вартість.
+ Отримано замовлення на закупівлю запасів
+ Інвентаризації, сповіщення та звіти
+ Інвентаризації запасів
+ Сповіщення про запаси
+ Звіти про запаси
+ Створіть знімок стану не більш ніж 100 позицій запасів. Укажіть кожну кількість, збережіть і завершіть інвентаризацію. Якщо запаси або дані каталогу зміняться, скасуйте її та почніть нову. Обладнання із серійними номерами, виявлене вперше, реєструйте через «Отримання».
+ Усі місця зберігання
+ Почати інвентаризацію
+ Завершити інвентаризацію
+ Скасувати інвентаризацію
+ Час знімка стану (UTC)
+ Очікувана кількість
+ Підрахована кількість
+ Розбіжність
+ Рядки з розбіжностями
+ Чернетка
+ Очікує на свідка
+ Завершено
+ Скасовано
+ Незалежний свідок має схвалити очікувану операцію в історії запасів. Знімок стану має залишатися актуальним.
+ Сповіщення показують низький рівень запасів, закінчення терміну придатності протягом 30 днів, прострочені запаси та затримані повернення. Оновіть їх один раз після запуску функції, щоб створити сповіщення про низький рівень запасів, а також після імпорту старих запасів. Подальші зміни запасів і товарів оновлюватимуть їх автоматично.
+ Оновити сповіщення
+ Низький рівень запасів
+ Термін придатності скоро закінчиться
+ Термін придатності закінчився
+ Повернення прострочене
+ Відкрите
+ Усунено
+ Кінцевий термін (UTC)
+ Виберіть звіт і, за потреби, фільтри. Дати вказуються в UTC; кінцева дата не включається в діапазон. Звіти зі знімком стану використовують поточні запаси. Завантажте PDF для перегляду або друку.
+ Від дати (UTC, включно)
+ До дати (UTC, не включно)
+ Завантажити PDF для друку
+ Запланувати надсилання звітів
+ Сповіщення про запаси
+ Запаси потребують уваги. Увійдіть, щоб переглянути поточні сповіщення.
+ Наявні запаси
+ Використання запасів
+ Терміни придатності запасів
+ Низький рівень запасів
+ Історія переміщень запасів
+ Історія видачі запасів
+ Оцінка запасів
+ Журнал контрольованих речовин
+ Днів до закінчення терміну придатності
+ Кількість, якої бракує до точки повторного замовлення
+ Рекомендована кількість для повторного замовлення
+ Ідентифікатор видачі
+ Ідентифікатор особи-одержувача
+ Ідентифікатор підрозділу-одержувача
+ Час видачі (UTC)
+ Повернена кількість
+ Неповернена кількість
+ Час повернення (UTC)
+ Виконавець (ідентифікатор користувача)
+ Сторнує операцію
+ Джерело використання
+ Ідентифікатор запису
+ Ідентифікатор виклику
+ Ідентифікатор зміненої редакції
+ Категорія контролю DEA
+ Залишок у джерелі до операції
+ Залишок у джерелі після операції
+ Залишок у місці призначення до операції
+ Залишок у місці призначення після операції
+ Свідок (ідентифікатор користувача)
+ Час підтвердження свідком (UTC)
+ Запаси містять захищені дані. Відкрийте посилання на звіт, яке вимагає входу, і підтвердьте свою особу, щоб переглянути або надрукувати поточні результати. Цей запланований PDF не містить значень запасів.
+ Цей звіт містить лише запаси та місця зберігання, які ви зараз маєте право переглядати. Кількості та вартості описують указаний знімок стану; результати обмежено 5 000 рядками.
+ Діапазон UTC (початок включається, кінець не включається)
+ Кожен рух запасів відображається один раз. Додатні кількості позначають вихідне використання, а від’ємні його сторнують. Для перегляду деталей запису потрібен доступ до джерела. Імпортовані вихідні рухи зберігають свою стару ідентичність і не створюють зв’язку з журналом або пацієнтом.
+ Активні товари відображаються, коли видимі запаси з кількісним обліком або активи із серійними номерами перебувають на рівні точки повторного замовлення чи нижче. Якщо її не задано, використовується мінімальний запас. До активів із серійними номерами належать активи в експлуатації, видані, пошкоджені та в ремонті. Приховані місця зберігання виключаються.
+ Рядки показують видачі, створені у вибраному діапазоні дат, і поточний стан їх повернення. Щоб побачити неповернені кількості за давнішими видачами, виберіть ранішу дату початку.
+ Рядки показують поточні додатні залишки та активні активи із серійними номерами, термін придатності яких закінчується у вибраному діапазоні. За замовчуванням включаються вже прострочені запаси та запаси, термін придатності яких закінчиться протягом наступних 30 днів.
+ Підпис виконавця
+ Підпис свідка
+ Не записано
+ Рух запасів без доступного посилання на запис
+ Записи
+ Рух запасів зі старої системи
+ Укажіть коректні дані інвентаризації та кількості.
+ Виберіть менший обсяг. Інвентаризація підтримує не більш ніж 100 позицій запасів.
+ У вибраному обсязі немає активних позицій запасів для інвентаризації.
+ Ця інвентаризація вже існує. Відкрийте її зі списку інвентаризацій.
+ Інвентаризація змінилася або містить неповні результати підрахунку. Завантажте її повторно та збережіть усі кількості перед завершенням.
+ Запаси змінилися після цього знімка стану. Скасуйте цю інвентаризацію та почніть нову.
+ Коригування інвентаризації належать відповідній завершеній інвентаризації. Почніть нову, щоб виправити запаси.
+ Проведіть розбіжності, завершивши інвентаризацію запасів.
+ Виберіть коректний діапазон дат UTC тривалістю не більш ніж 366 днів.
+ Виберіть коректні фільтри звіту.
+ Звіт змінився під час створення PDF. Створіть його ще раз.
+ Не вдалося створити звіт. Спробуйте ще раз.
+ Низький рівень запасів
+ Наближається закінчення терміну придатності запасів
+ Інвентаризацію завершено
+ Прострочене повернення майна
+ На цей ресурс є посилання в історії запасів, тому його потрібно зберегти.
+ Запаси скориговано
+ Переміщення запасів завершено
+ Запаси видано
+ Запаси повернуто
+ Стан інвентарного активу змінено
+ Контрольовану речовину зареєстровано
-
diff --git a/Core/Resgrid.Model/Checklists/ChecklistWorkflowPayload.cs b/Core/Resgrid.Model/Checklists/ChecklistWorkflowPayload.cs
index 31e51a6d..e764ddb4 100644
--- a/Core/Resgrid.Model/Checklists/ChecklistWorkflowPayload.cs
+++ b/Core/Resgrid.Model/Checklists/ChecklistWorkflowPayload.cs
@@ -15,9 +15,8 @@ public static class ChecklistWorkflowPayload
(int)WorkflowTriggerEventType.ChecklistCompleted, (int)WorkflowTriggerEventType.ChecklistFailed,
(int)WorkflowTriggerEventType.ChecklistMissed, (int)WorkflowTriggerEventType.WorkOrderCreated,
(int)WorkflowTriggerEventType.WorkOrderStatusChanged, (int)WorkflowTriggerEventType.WorkOrderAssigned,
- (int)WorkflowTriggerEventType.ChecklistScheduleChanged, (int)WorkflowTriggerEventType.ChecklistOccurrenceSkipped,
- 22, 58, 59, 60, 64, 66
- });
+ (int)WorkflowTriggerEventType.ChecklistScheduleChanged, (int)WorkflowTriggerEventType.ChecklistOccurrenceSkipped
+ }.Concat(Resgrid.Model.Inventories.InventoryWorkflowPayload.Triggers).ToArray());
private static readonly string[] Identifiers = { "CompletionId", "DefinitionId", "VersionId", "ItemId", "ScheduleId", "OccurrenceId" };
private static bool IsStructuralTarget(int type, string target) =>
type >= 0 && type <= 2 && int.TryParse(target, out var numeric) && numeric > 0
diff --git a/Core/Resgrid.Model/Inventories/InventoryContracts.cs b/Core/Resgrid.Model/Inventories/InventoryContracts.cs
index a502e688..9617f682 100644
--- a/Core/Resgrid.Model/Inventories/InventoryContracts.cs
+++ b/Core/Resgrid.Model/Inventories/InventoryContracts.cs
@@ -35,16 +35,20 @@ public sealed class InventoryPosting
public string ReferenceId { get; set; }
public string ReversesTransactionId { get; set; }
public string IssuanceId { get; set; }
+ [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string UsageId { get; set; }
+ [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public InventoryUsageType? UsageType { get; set; }
+ [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string PurchaseOrderItemId { get; set; }
+ [Newtonsoft.Json.JsonProperty(NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)] public string CountItemId { get; set; }
public string Note { get; set; }
public decimal? UnitCost { get; set; }
}
public sealed class InventoryCommand { public string RequestId { get; set; } public List Lines { get; set; } = new(); }
- public sealed class InventoryResult { public string OperationId { get; set; } public bool AwaitingWitness { get; set; } public List TransactionIds { get; set; } = new(); public List OutboxIds { get; set; } = new(); public string TransferId { get; set; } public string IssuanceId { get; set; } public string AssetId { get; set; } public List IssuanceIds { get; set; } = new(); }
+ public sealed class InventoryResult { public string OperationId { get; set; } public bool AwaitingWitness { get; set; } public List TransactionIds { get; set; } = new(); public List OutboxIds { get; set; } = new(); public string TransferId { get; set; } public string IssuanceId { get; set; } public string AssetId { get; set; } public List IssuanceIds { get; set; } = new(); public string PurchaseOrderId { get; set; } }
public sealed class InventoryIssueInput { public string RequestId { get; set; } public string ItemId { get; set; } public string AssetId { get; set; } public string LotId { get; set; } public string FromLocationId { get; set; } public decimal Quantity { get; set; } public string UserId { get; set; } public int? UnitId { get; set; } public DateTime? ExpectedReturnOn { get; set; } public InventoryReferenceType ReferenceType { get; set; } public string ReferenceId { get; set; } public string Note { get; set; } }
public sealed class InventoryReturnInput { public string RequestId { get; set; } public string IssuanceId { get; set; } public int Revision { get; set; } public string ToLocationId { get; set; } public decimal Quantity { get; set; } public InventoryAssetStatus Condition { get; set; } public string Note { get; set; } }
public sealed class InventoryEquipment { public InventoryAsset Asset { get; set; } public InventoryStock Stock { get; set; } public string ItemName { get; set; } public int? UnitId { get; set; } public int? GroupId { get; set; } public string UserId { get; set; } public List Issuances { get; set; } = new(); }
public sealed class InventoryPage { public List Items { get; set; } = new(); public bool HasMore { get; set; } }
- public sealed class InventoryOperationContent { public string Fingerprint { get; set; } public InventoryResult Result { get; set; } public InventoryCommand PendingCommand { get; set; } public string PendingKind { get; set; } public List PendingIssues { get; set; } public InventoryReturnInput PendingReturn { get; set; } public string PerformerId { get; set; } public string WitnessId { get; set; } public DateTime? WitnessedOn { get; set; } public string Attestation { get; set; } }
+ public sealed class InventoryOperationContent { public string Fingerprint { get; set; } public InventoryResult Result { get; set; } public InventoryCommand PendingCommand { get; set; } public string PendingKind { get; set; } public List PendingIssues { get; set; } public InventoryReturnInput PendingReturn { get; set; } public string PerformerId { get; set; } public string WitnessId { get; set; } public DateTime? WitnessedOn { get; set; } public string Attestation { get; set; } public InventoryPurchaseReceiptInput PendingPurchaseReceipt { get; set; } public InventoryCountComplete PendingCount { get; set; } }
public sealed class InventoryKitInput { public string Id { get; set; } public int Revision { get; set; } public string Name { get; set; } public List Lines { get; set; } = new(); }
public sealed class InventoryKitLine { public string ItemId { get; set; } public decimal Quantity { get; set; } }
public sealed class InventoryKitIssueInput { public string KitId { get; set; } public string RequestId { get; set; } public List Lines { get; set; } = new(); }
diff --git a/Core/Resgrid.Model/Inventories/InventoryModels.cs b/Core/Resgrid.Model/Inventories/InventoryModels.cs
index 81023480..06ec1e87 100644
--- a/Core/Resgrid.Model/Inventories/InventoryModels.cs
+++ b/Core/Resgrid.Model/Inventories/InventoryModels.cs
@@ -92,6 +92,8 @@ public sealed class InventoryTransaction : InventoryRow
public string ReferenceId { get; set; }
public string ReversesTransactionId { get; set; }
public string IssuanceId { get; set; }
+ public string PurchaseOrderItemId { get; set; }
+ public string CountItemId { get; set; }
public int? LegacyInventoryId { get; set; }
public DateTime OccurredOn { get; set; }
}
@@ -132,6 +134,9 @@ public sealed class InventoryItemContent
public decimal? ReorderPoint { get; set; }
public decimal? ReorderQuantity { get; set; }
public decimal? DefaultUnitCost { get; set; }
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public decimal? AverageUnitCost { get; set; }
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string CurrencyCode { get; set; }
+ [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public string PreferredVendorId { get; set; }
}
public sealed class InventoryLabel { public string Name { get; set; } public string Note { get; set; } }
public sealed class InventoryLotContent { public string LotNumber { get; set; } public decimal? UnitCost { get; set; } public string VendorId { get; set; } }
@@ -139,12 +144,19 @@ public sealed class InventoryAssetContent { public string SerialNumber { get; se
public static class InventoryTables
{
public const int CatalogVersion = 19;
+ public const int UsageCatalogVersion = 20;
+ public const int PurchasingCatalogVersion = 21;
+ public const int OperationsCatalogVersion = 22;
public static readonly IReadOnlyDictionary All = new Dictionary
{
[typeof(InventoryCategory)] = "InventoryCategories", [typeof(InventoryItem)] = "InventoryItems", [typeof(InventoryLocation)] = "InventoryLocations",
[typeof(InventoryLot)] = "InventoryLots", [typeof(InventoryStock)] = "InventoryStocks", [typeof(InventoryAsset)] = "InventoryAssets", [typeof(InventoryTransaction)] = "InventoryTransactions",
[typeof(InventoryOperation)] = "InventoryOperations", [typeof(InventoryTransfer)] = "InventoryTransfers", [typeof(InventoryTransferItem)] = "InventoryTransferItems",
- [typeof(InventoryIssuance)] = "InventoryIssuances", [typeof(InventoryKit)] = "InventoryKits", [typeof(InventoryKitItem)] = "InventoryKitItems"
+ [typeof(InventoryIssuance)] = "InventoryIssuances", [typeof(InventoryKit)] = "InventoryKits", [typeof(InventoryKitItem)] = "InventoryKitItems",
+ [typeof(RecordInventoryUsage)] = "RecordInventoryUsages",
+ [typeof(InventoryVendor)] = "InventoryVendors", [typeof(InventoryPurchaseOrder)] = "InventoryPurchaseOrders", [typeof(InventoryPurchaseOrderItem)] = "InventoryPurchaseOrderItems",
+ [typeof(InventoryCount)] = "InventoryCounts", [typeof(InventoryCountItem)] = "InventoryCountItems",
+ [typeof(InventoryAlert)] = "InventoryAlerts", [typeof(InventoryAlertDelivery)] = "InventoryAlertDeliveries"
};
public static IReadOnlyDictionary Get, Action Set)> Fields() where T : InventoryRow =>
new Dictionary, Action)> { [All[typeof(T)].ToLowerInvariant() + ".content"] = (x => x.Content, (x, v) => x.Content = v) };
diff --git a/Core/Resgrid.Model/Inventories/InventoryOperations.cs b/Core/Resgrid.Model/Inventories/InventoryOperations.cs
new file mode 100644
index 00000000..9aee1012
--- /dev/null
+++ b/Core/Resgrid.Model/Inventories/InventoryOperations.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+
+namespace Resgrid.Model.Inventories
+{
+ public enum InventoryCountStatus { Draft = 0, AwaitingWitness = 1, Completed = 2, Cancelled = 3 }
+ public enum InventoryAlertType { LowStock = 0, ExpiringSoon = 1, Expired = 2, OverdueReturn = 3 }
+ public sealed class InventoryCount : InventoryMutableRow
+ {
+ public string LocationId { get; set; }
+ public int Status { get; set; }
+ public string SnapshotFingerprint { get; set; }
+ public DateTime SnapshotOn { get; set; }
+ public DateTime? CompletedOn { get; set; }
+ public string OperationId { get; set; }
+ }
+ public sealed class InventoryCountItem : InventoryRow
+ {
+ public string CountId { get; set; }
+ public string ItemId { get; set; }
+ public string LocationId { get; set; }
+ public string LotId { get; set; }
+ public string AssetId { get; set; }
+ public decimal ExpectedQuantity { get; set; }
+ public decimal? CountedQuantity { get; set; }
+ public string TransactionId { get; set; }
+ }
+ public sealed class InventoryCountContent
+ {
+ public string Name { get; set; }
+ public string Note { get; set; }
+ public int VarianceLineCount { get; set; }
+ public List Totals { get; set; } = new();
+ }
+ public sealed class InventoryCountItemContent
+ {
+ public string ItemName { get; set; }
+ public string UnitOfMeasure { get; set; }
+ public string SerialNumber { get; set; }
+ public string LotNumber { get; set; }
+ public decimal? UnitCost { get; set; }
+ public string CurrencyCode { get; set; }
+ }
+ public sealed class InventoryCountInput { public string Id { get; set; } public string LocationId { get; set; } public string Name { get; set; } public string Note { get; set; } }
+ public sealed class InventoryCountObservation { public string Id { get; set; } public decimal Quantity { get; set; } }
+ public sealed class InventoryCountUpdate { public string CountId { get; set; } public int Revision { get; set; } public List Lines { get; set; } = new(); }
+ public sealed class InventoryCountComplete { public string CountId { get; set; } public int Revision { get; set; } public string RequestId { get; set; } }
+ public sealed class InventoryCountDetail { public InventoryCount Count { get; set; } public List Lines { get; set; } = new(); }
+ /// Content is empty: unattended evaluation uses dates/statuses and attended low-stock projections only.
+ public sealed class InventoryAlert : InventoryRow
+ {
+ public int AlertType { get; set; }
+ public string DedupKey { get; set; }
+ public string ItemId { get; set; }
+ public string LocationId { get; set; }
+ public string LotId { get; set; }
+ public string AssetId { get; set; }
+ public string IssuanceId { get; set; }
+ public int Status { get; set; }
+ public DateTime OpenedOn { get; set; }
+ public DateTime? ResolvedOn { get; set; }
+ public DateTime? DueOn { get; set; }
+ public decimal? Quantity { get; set; }
+ }
+ public sealed class InventoryAlertDelivery : InventoryRow
+ {
+ public string AlertId { get; set; }
+ public string UserId { get; set; }
+ public int State { get; set; }
+ public DateTime NextAttemptOn { get; set; }
+ public DateTime? LeaseUntil { get; set; }
+ public string ClaimToken { get; set; }
+ public int AttemptCount { get; set; }
+ public DateTime? HandedOffOn { get; set; }
+ }
+ public enum InventoryReportKind { OnHand = 0, Usage = 1, Expiration = 2, LowStock = 3, TransferHistory = 4, Issuance = 5, Valuation = 6, ControlledSubstanceLog = 7 }
+ public sealed class InventoryReportInput
+ {
+ public InventoryReportKind Kind { get; set; }
+ public DateTime? FromUtc { get; set; }
+ public DateTime? UntilUtc { get; set; }
+ public string LocationId { get; set; }
+ public string ItemId { get; set; }
+ public string UserId { get; set; }
+ public int? UnitId { get; set; }
+ }
+ public sealed class InventoryReport
+ {
+ public InventoryReportKind Kind { get; set; }
+ public DateTime GeneratedOn { get; set; }
+ public DateTime? FromUtc { get; set; }
+ public DateTime? UntilUtc { get; set; }
+ public List Columns { get; set; } = new();
+ public List> Rows { get; set; } = new();
+ public List Totals { get; set; } = new();
+ }
+}
diff --git a/Core/Resgrid.Model/Inventories/InventoryPurchasing.cs b/Core/Resgrid.Model/Inventories/InventoryPurchasing.cs
new file mode 100644
index 00000000..5c379e02
--- /dev/null
+++ b/Core/Resgrid.Model/Inventories/InventoryPurchasing.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+
+namespace Resgrid.Model.Inventories
+{
+ public enum InventoryPurchaseOrderStatus { Draft = 0, Ordered = 1, PartiallyReceived = 2, Received = 3, Cancelled = 4 }
+ public sealed class InventoryVendor : InventoryMutableRow { public string ContactId { get; set; } }
+ public sealed class InventoryVendorContent { public string AccountNumber { get; set; } public string Note { get; set; } }
+ public sealed class InventoryPurchaseOrder : InventoryMutableRow
+ {
+ public string VendorId { get; set; }
+ public int Status { get; set; }
+ public string CurrencyCode { get; set; }
+ public DateTime? OrderedOn { get; set; }
+ public DateTime? ReceivedOn { get; set; }
+ }
+ public sealed class InventoryPurchaseOrderContent { public string Number { get; set; } public string Note { get; set; } public string SupplierName { get; set; } }
+ public sealed class InventoryPurchaseOrderItem : InventoryMutableRow
+ {
+ public string PurchaseOrderId { get; set; }
+ public string ItemId { get; set; }
+ public int LineNumber { get; set; }
+ public decimal QuantityOrdered { get; set; }
+ public decimal QuantityReceived { get; set; }
+ }
+ public sealed class InventoryPurchaseOrderItemContent { public string ItemName { get; set; } public string UnitOfMeasure { get; set; } public decimal UnitCost { get; set; } public string Note { get; set; } }
+ public sealed class InventoryVendorInput { public string Id { get; set; } public int Revision { get; set; } public string ContactId { get; set; } public InventoryVendorContent Details { get; set; } = new(); }
+ public sealed class InventoryPurchaseOrderInput
+ {
+ public string Id { get; set; }
+ public int Revision { get; set; }
+ public string VendorId { get; set; }
+ public string CurrencyCode { get; set; }
+ public string Number { get; set; }
+ public string Note { get; set; }
+ public List Lines { get; set; } = new();
+ }
+ public sealed class InventoryPurchaseOrderLineInput { public string Id { get; set; } public string ItemId { get; set; } public decimal QuantityOrdered { get; set; } public decimal UnitCost { get; set; } public string Note { get; set; } }
+ public sealed class InventoryPurchaseOrderChange { public string Id { get; set; } public int Revision { get; set; } public string RequestId { get; set; } }
+ public sealed class InventoryPurchaseReceiptInput { public string PurchaseOrderId { get; set; } public int Revision { get; set; } public string RequestId { get; set; } public List Lines { get; set; } = new(); }
+ public sealed class InventoryPurchaseReceiptLine
+ {
+ public string PurchaseOrderItemId { get; set; }
+ public string LocationId { get; set; }
+ public string LotId { get; set; }
+ public decimal Quantity { get; set; }
+ public InventoryPurchaseAssetInput Asset { get; set; }
+ }
+ public sealed class InventoryPurchaseAssetInput { public string SerialNumber { get; set; } public string AssetTag { get; set; } public string Barcode { get; set; } public DateTime? ExpiresOn { get; set; } }
+ public sealed class InventoryPurchaseOrderDetail { public InventoryPurchaseOrder Order { get; set; } public List Lines { get; set; } = new(); public List Receipts { get; set; } = new(); }
+ public sealed class InventoryVendorChoice { public string Id { get; set; } public string Name { get; set; } }
+ public sealed class InventoryValuationLine
+ {
+ public string ItemId { get; set; } public string AssetId { get; set; } public string LotId { get; set; } public string LocationId { get; set; }
+ public string ItemName { get; set; } public string CurrencyCode { get; set; } public decimal Quantity { get; set; }
+ public decimal? UnitCost { get; set; } public decimal? Value { get; set; }
+ }
+ public sealed class InventoryValuationTotal { public string CurrencyCode { get; set; } public decimal KnownValue { get; set; } public int UncostedRows { get; set; } }
+ public sealed class InventoryValuation { public List Lines { get; set; } = new(); public List Totals { get; set; } = new(); public bool HasNegativeStock { get; set; } public DateTime AsOfUtc { get; set; } }
+}
diff --git a/Core/Resgrid.Model/Inventories/InventoryQuery.cs b/Core/Resgrid.Model/Inventories/InventoryQuery.cs
index 4a2f7df6..f41646e2 100644
--- a/Core/Resgrid.Model/Inventories/InventoryQuery.cs
+++ b/Core/Resgrid.Model/Inventories/InventoryQuery.cs
@@ -1,5 +1,12 @@
namespace Resgrid.Model.Inventories
{
+ public sealed class InventoryStockQuantity
+ {
+ public string ItemId { get; set; }
+ public string LocationId { get; set; }
+ public decimal Quantity { get; set; }
+ }
+
/// Structural filters applied before paging; authored content is never queried outside its protected read boundary.
public sealed class InventoryQuery
{
@@ -8,5 +15,8 @@ public sealed class InventoryQuery
public string AssetId { get; set; }
public string IssuedToUserId { get; set; }
public string KitId { get; set; }
+ public string SourceId { get; set; }
+ public int? SourceType { get; set; }
+ public int? RecordKind { get; set; }
}
}
diff --git a/Core/Resgrid.Model/Inventories/InventoryWorkflowPayload.cs b/Core/Resgrid.Model/Inventories/InventoryWorkflowPayload.cs
index 3beae500..c8cf6eb0 100644
--- a/Core/Resgrid.Model/Inventories/InventoryWorkflowPayload.cs
+++ b/Core/Resgrid.Model/Inventories/InventoryWorkflowPayload.cs
@@ -12,11 +12,16 @@ namespace Resgrid.Model.Inventories
/// Reviewed inventory routing and quantities; personnel and authored content never cross the Workflow boundary.
public static class InventoryWorkflowPayload
{
- public const int CatalogVersion = 19;
- public static readonly IReadOnlyList Triggers = Array.AsReadOnly(new[] { 22, 58, 59, 60, 64, 66 });
+ public const int CatalogVersion = 22;
+ public static readonly IReadOnlyList Triggers = Array.AsReadOnly(new[] { 22, 58, 59, 60, 61, 62, 63, 64, 65, 66, 166 });
public static readonly (string Variable, string Property)[] Variables =
{
("transaction_id", "TransactionId"), ("item_id", "ItemId"), ("asset_id", "AssetId"), ("lot_id", "LotId"),
+ ("usage_id", "UsageId"), ("usage_type", "UsageType"),
+ ("count_id", "CountId"), ("count_item_id", "CountItemId"), ("variance_line_count", "VarianceLineCount"), ("variance_value", "VarianceValue"),
+ ("alert_id", "AlertId"), ("alert_type", "AlertType"), ("location_id", "LocationId"), ("due_on", "DueOn"),
+ ("purchase_order_id", "PurchaseOrderId"), ("purchase_order_item_id", "PurchaseOrderItemId"), ("vendor_id", "VendorId"),
+ ("receipt_id", "ReceiptId"), ("purchase_order_status", "PurchaseOrderStatus"), ("line_count", "LineCount"), ("currency_code", "CurrencyCode"),
("transfer_id", "TransferId"), ("issuance_id", "IssuanceId"), ("transaction_type", "TransactionType"), ("quantity", "Quantity"),
("from_location_id", "FromLocationId"), ("to_location_id", "ToLocationId"),
("from_quantity_before", "FromQuantityBefore"), ("from_quantity_after", "FromQuantityAfter"),
@@ -24,8 +29,8 @@ public static readonly (string Variable, string Property)[] Variables =
("previous_status", "OldStatus"), ("status", "NewStatus"), ("reference_type", "ReferenceType"), ("reference_id", "ReferenceId"),
("reverses_transaction_id", "ReversesTransactionId"), ("occurred_on", "OccurredOn"), ("item_name", "ItemName")
};
- private static readonly string[] GuidFields = { "TransactionId", "ItemId", "AssetId", "LotId", "TransferId", "IssuanceId", "FromLocationId", "ToLocationId", "ReversesTransactionId" };
- private static readonly string[] WithheldFields = { "ItemName", "Note", "SerialNumber", "WitnessUserId" };
+ private static readonly string[] GuidFields = { "TransactionId", "ItemId", "AssetId", "LotId", "TransferId", "IssuanceId", "FromLocationId", "ToLocationId", "ReversesTransactionId", "UsageId", "PurchaseOrderId", "PurchaseOrderItemId", "VendorId", "ReceiptId", "CountId", "CountItemId", "AlertId", "LocationId" };
+ private static readonly string[] WithheldFields = { "ItemName", "Note", "SerialNumber", "WitnessUserId", "VarianceValue" };
public static bool IsInventory(JObject payload) => payload?["InventoryEvent"]?.Type == JTokenType.Boolean && payload["InventoryEvent"].Value();
public static bool IsInventory(int trigger) => Triggers.Contains(trigger);
@@ -69,6 +74,10 @@ public static string Routing(JObject payload)
var safe = new JObject { ["InventoryEvent"] = true };
foreach (var name in GuidFields) CopyGuid(payload, safe, name);
foreach (var name in new[] { "TransactionType", "OldStatus", "NewStatus", "ReferenceType" }) CopyInteger(payload, safe, name);
+ CopyInteger(payload, safe, "UsageType", 3);
+ CopyInteger(payload, safe, "PurchaseOrderStatus", 4); CopyInteger(payload, safe, "LineCount", 100);
+ CopyInteger(payload, safe, "VarianceLineCount", 100); CopyInteger(payload, safe, "AlertType", 3); CopyTimestamp(payload, safe, "DueOn");
+ if (payload["CurrencyCode"]?.Type == JTokenType.String && payload.Value("CurrencyCode") is { Length: 3 } currency && currency.All(c => c >= 'A' && c <= 'Z')) safe["CurrencyCode"] = currency;
foreach (var name in new[] { "Quantity", "FromQuantityBefore", "FromQuantityAfter", "ToQuantityBefore", "ToQuantityAfter" })
{
var token = payload[name];
@@ -96,7 +105,7 @@ private static JObject Envelope(JObject source)
var eventName = source["EventName"];
if (eventName?.Type == JTokenType.String && Enum.TryParse(eventName.Value(), out var trigger) && IsInventory((int)trigger) && Enum.IsDefined(typeof(WorkflowTriggerEventType), trigger)) safe["EventName"] = trigger.ToString();
var aggregate = source["AggregateType"]?.Type == JTokenType.String ? source["AggregateType"].Value() : null;
- if (aggregate is "InventoryTransaction" or "InventoryItem" or "InventoryAsset" or "InventoryTransfer" or "InventoryIssuance") safe["AggregateType"] = aggregate;
+ if (aggregate is "InventoryTransaction" or "InventoryItem" or "InventoryAsset" or "InventoryTransfer" or "InventoryIssuance" or "InventoryPurchaseOrder" or "InventoryCount" or "InventoryAlert") safe["AggregateType"] = aggregate;
var origin = source["OriginClient"];
if (origin?.Type == JTokenType.String && Enum.TryParse(origin.Value(), out var client) && Enum.IsDefined(typeof(RmsOriginClient), client)) safe["OriginClient"] = client.ToString();
if (source["IsReplay"]?.Type == JTokenType.Boolean) safe["IsReplay"] = source["IsReplay"].DeepClone();
diff --git a/Core/Resgrid.Model/Inventories/RecordInventoryUsage.cs b/Core/Resgrid.Model/Inventories/RecordInventoryUsage.cs
new file mode 100644
index 00000000..3bf1264b
--- /dev/null
+++ b/Core/Resgrid.Model/Inventories/RecordInventoryUsage.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+
+namespace Resgrid.Model.Inventories
+{
+ public enum InventoryUsageSourceType { LegacyLog = 0, RmsRecord = 1 }
+ public enum InventoryUsageType { Used = 0, ConsumedOnPatient = 1, LeftAtScene = 2, Damaged = 3 }
+
+ /// Immutable source-owned usage. Corrections are separate rows and ledger entries; Records freezes references in its revision evidence.
+ public sealed class RecordInventoryUsage : InventoryRow
+ {
+ public int SourceType { get; set; }
+ public string SourceId { get; set; }
+ public int? RecordKind { get; set; }
+ public int? CallId { get; set; }
+ /// The revision being amended when posted; null for an initial draft. Final revision membership is frozen by Records evidence.
+ public string RmsRevisionId { get; set; }
+ public string ItemId { get; set; }
+ public string AssetId { get; set; }
+ public string LotId { get; set; }
+ public string SourceLocationId { get; set; }
+ public decimal Quantity { get; set; }
+ public int UsageType { get; set; }
+ public string TransactionId { get; set; }
+ public string ReversesUsageId { get; set; }
+ }
+
+ public sealed class RecordInventoryUsageLine
+ {
+ public string ItemId { get; set; }
+ public string AssetId { get; set; }
+ public string LotId { get; set; }
+ public string LocationId { get; set; }
+ public decimal Quantity { get; set; }
+ public int? ExpectedAssetRevision { get; set; }
+ public InventoryUsageType UsageType { get; set; }
+ public string Note { get; set; }
+ /// Attach an already witnessed, unclaimed Inventory consumption without posting it again.
+ public string ExistingTransactionId { get; set; }
+ }
+ public sealed class RecordInventoryUsageRequest
+ {
+ public string RequestId { get; set; }
+ public List Lines { get; set; } = new();
+ }
+ public sealed class RecordInventoryUsageCorrection
+ {
+ public string RequestId { get; set; }
+ public string UsageId { get; set; }
+ public string Reason { get; set; }
+ /// For controlled stock, attach the independently witnessed reversal from Inventory.
+ public string ExistingTransactionId { get; set; }
+ }
+}
diff --git a/Core/Resgrid.Model/ReportTypes.cs b/Core/Resgrid.Model/ReportTypes.cs
index 227cbc84..ca0d049c 100644
--- a/Core/Resgrid.Model/ReportTypes.cs
+++ b/Core/Resgrid.Model/ReportTypes.cs
@@ -7,6 +7,14 @@ public enum ReportTypes
Certifications = 2,
ShiftReadiness = 3,
ChecklistCompliance = 4,
- ChecklistMissed = 5
+ ChecklistMissed = 5,
+ InventoryOnHand = 6,
+ InventoryUsage = 7,
+ InventoryExpiration = 8,
+ InventoryLowStock = 9,
+ InventoryTransferHistory = 10,
+ InventoryIssuance = 11,
+ InventoryValuation = 12,
+ ControlledSubstanceLog = 13
}
-}
\ No newline at end of file
+}
diff --git a/Core/Resgrid.Model/Repositories/IInventoryStore.cs b/Core/Resgrid.Model/Repositories/IInventoryStore.cs
index e7e2ac45..27d63619 100644
--- a/Core/Resgrid.Model/Repositories/IInventoryStore.cs
+++ b/Core/Resgrid.Model/Repositories/IInventoryStore.cs
@@ -8,6 +8,11 @@ namespace Resgrid.Model.Repositories
public interface IInventoryStore
{
Task LockDepartmentAsync(int departmentId);
+ Task> AlertDepartmentsAsync(int afterDepartmentId);
+ Task LastEntryAsync(int departmentId);
+ Task OpenAlertAsync(int departmentId, string dedupKey);
+ Task AlertDeliveryAsync(int departmentId, string alertId, string userId);
+ Task> OpenAlertsAsync(int departmentId, int skip = 0);
Task GetAsync(int departmentId, string id) where T : InventoryRow;
Task> ListAsync(int departmentId, int skip = 0) where T : InventoryRow;
Task> QueryAsync(int departmentId, InventoryQuery filter, int skip = 0) where T : InventoryRow;
@@ -19,6 +24,8 @@ public interface IInventoryStore
Task LegacyItemAsync(int departmentId, int typeId);
Task LegacyTransactionAsync(int departmentId, int inventoryId);
Task RebuildStocksAsync(int departmentId);
+ Task> StockQuantitiesAsync(int departmentId, IReadOnlyCollection itemIds);
+ Task> AssetHistoryAsync(int departmentId, IReadOnlyCollection assetIds, System.DateTime at, bool after, int skip = 0);
Task HasLegacyMigrationAsync(int departmentId);
}
}
diff --git a/Core/Resgrid.Model/Services/IInventoryModernizationService.cs b/Core/Resgrid.Model/Services/IInventoryModernizationService.cs
index 3ac1da37..8f2e30e8 100644
--- a/Core/Resgrid.Model/Services/IInventoryModernizationService.cs
+++ b/Core/Resgrid.Model/Services/IInventoryModernizationService.cs
@@ -14,14 +14,18 @@ public interface IInventoryCatalogService
Task SaveLotAsync(InventoryActor actor, InventoryLot lot, InventoryLotContent details);
Task ArchiveAsync(InventoryActor actor, string id, int revision) where T : InventoryMutableRow;
Task GetAsync(InventoryActor actor, string id) where T : InventoryRow;
+ Task GetLegacyTransactionAsync(InventoryActor actor, int inventoryId);
Task> ListAsync(InventoryActor actor, int page = 0) where T : InventoryRow;
Task> QueryAsync(InventoryActor actor, InventoryQuery filter, int page = 0) where T : InventoryRow;
}
public interface IInventoryStockService
{
+ Task> GetVisibleQuantitiesAsync(InventoryActor actor, IReadOnlyCollection itemIds);
Task PostTransactionAsync(InventoryActor actor, InventoryCommand command, CancellationToken ct = default);
/// The caller owns the active transaction and dispatches returned OutboxIds only after its commit.
Task PostWithinTransactionAsync(InventoryActor actor, InventoryCommand command, CancellationToken ct = default);
+ /// The authorized source adapter owns the active transaction and source lifecycle checks. Does not post stock again.
+ Task RecordUsageWithinTransactionAsync(InventoryActor actor, RecordInventoryUsage usage);
Task WitnessAsync(InventoryActor actor, string requestId, string attestation);
Task RebuildStocksAsync(InventoryActor actor);
Task> GetByReferenceAsync(InventoryActor actor, InventoryReferenceType type, string id);
diff --git a/Core/Resgrid.Model/Services/IInventoryOperationsService.cs b/Core/Resgrid.Model/Services/IInventoryOperationsService.cs
new file mode 100644
index 00000000..6c713662
--- /dev/null
+++ b/Core/Resgrid.Model/Services/IInventoryOperationsService.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Resgrid.Model.Inventories;
+
+namespace Resgrid.Model.Services
+{
+ public interface IInventoryOperationsService
+ {
+ Task StartCountAsync(InventoryActor actor, InventoryCountInput input);
+ Task GetCountAsync(InventoryActor actor, string id);
+ Task SaveCountAsync(InventoryActor actor, InventoryCountUpdate input);
+ Task CompleteCountAsync(InventoryActor actor, InventoryCountComplete input);
+ Task CancelCountAsync(InventoryActor actor, string id, int revision);
+ Task RefreshAlertsAsync(InventoryActor actor);
+ Task BuildReportAsync(InventoryActor actor, InventoryReportInput input);
+ }
+ public interface IInventoryAlertService
+ {
+ Task> AlertDepartmentsAsync(int afterDepartmentId);
+ Task SweepAlertsAsync(int departmentId);
+ Task ClaimAlertAsync(int departmentId, string userId);
+ Task CanReceiveAlertAsync(int departmentId, string userId, string alertId);
+ Task FinishAlertAsync(int departmentId, string deliveryId, string claimToken, bool handedOff);
+ }
+}
diff --git a/Core/Resgrid.Model/Services/IInventoryPurchasingService.cs b/Core/Resgrid.Model/Services/IInventoryPurchasingService.cs
new file mode 100644
index 00000000..89a88c5f
--- /dev/null
+++ b/Core/Resgrid.Model/Services/IInventoryPurchasingService.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Resgrid.Model.Inventories;
+
+namespace Resgrid.Model.Services
+{
+ public interface IInventoryPurchasingService
+ {
+ Task> GetVendorContactsAsync(InventoryActor actor);
+ Task SaveVendorAsync(InventoryActor actor, InventoryVendorInput input);
+ Task GetPurchaseOrderAsync(InventoryActor actor, string id);
+ Task SavePurchaseOrderAsync(InventoryActor actor, InventoryPurchaseOrderInput input);
+ Task ChangePurchaseOrderStatusAsync(InventoryActor actor, InventoryPurchaseOrderChange input, InventoryPurchaseOrderStatus status);
+ Task ReceivePurchaseOrderAsync(InventoryActor actor, InventoryPurchaseReceiptInput input);
+ Task GetValuationAsync(InventoryActor actor, string locationId = null);
+ }
+}
diff --git a/Core/Resgrid.Model/Services/IInventoryScheduledReportService.cs b/Core/Resgrid.Model/Services/IInventoryScheduledReportService.cs
new file mode 100644
index 00000000..fbd57862
--- /dev/null
+++ b/Core/Resgrid.Model/Services/IInventoryScheduledReportService.cs
@@ -0,0 +1,11 @@
+using System.Threading.Tasks;
+
+namespace Resgrid.Model.Services
+{
+ public interface IInventoryScheduledReportService
+ {
+ Task BuildAsync(ScheduledTask queuedTask);
+ /// Revalidates the successfully built delivery for this same queued task immediately before email handoff.
+ Task ValidateAsync(ScheduledTask queuedTask);
+ }
+}
diff --git a/Core/Resgrid.Model/Services/IRmsInventoryUsageAdapter.cs b/Core/Resgrid.Model/Services/IRmsInventoryUsageAdapter.cs
index 2e360baf..23fab8ed 100644
--- a/Core/Resgrid.Model/Services/IRmsInventoryUsageAdapter.cs
+++ b/Core/Resgrid.Model/Services/IRmsInventoryUsageAdapter.cs
@@ -20,6 +20,14 @@ public class RmsInventoryUsage
public int InventoryId { get; set; }
public string TransactionId { get; set; }
public string ItemId { get; set; }
+ public string UsageId { get; set; }
+ public string AssetId { get; set; }
+ public string LotId { get; set; }
+ public string LocationId { get; set; }
+ public InventoryUsageType UsageType { get; set; }
+ public string ReversesUsageId { get; set; }
+ public bool IsReversed { get; set; }
+ public string PendingReversalTransactionId { get; set; }
public decimal Quantity { get; set; }
public string Note { get; set; }
public string ItemName { get; set; }
@@ -42,6 +50,11 @@ public interface IRmsInventoryUsageAdapter
{
Task ConsumeAsync(int departmentId, string userId, string recordId, RmsRecordKind kind, long expectedRowVersion, int typeId, int groupId, int? unitId, decimal quantity, string note, CancellationToken cancellationToken = default, string grantToken = null);
Task ConsumeModernAsync(InventoryActor actor, string recordId, RmsRecordKind kind, long expectedRowVersion, InventoryCommand command, CancellationToken cancellationToken = default);
+ Task> RecordModernUsageAsync(InventoryActor actor, string recordId, RmsRecordKind kind, long expectedRowVersion, RecordInventoryUsageRequest request, CancellationToken cancellationToken = default);
+ Task ReverseModernUsageAsync(InventoryActor actor, string recordId, RmsRecordKind kind, long expectedRowVersion, RecordInventoryUsageCorrection correction, CancellationToken cancellationToken = default);
+ Task> GetAuthorizedUsageAsync(InventoryActor actor, string recordId, RmsRecordKind kind);
+ /// Checks the live source and fences its version in Inventory's active transaction before a new linked stock correction.
+ Task RequireUsageCorrectionAccessAsync(InventoryActor actor, string usageId);
Task> GetUsageForRecordAsync(int departmentId, string recordId);
Task> GetUsageForLegacyLogAsync(int departmentId, int logId);
diff --git a/Core/Resgrid.Model/WorkOrders/WorkOrderWorkflowPayload.cs b/Core/Resgrid.Model/WorkOrders/WorkOrderWorkflowPayload.cs
index 13cdb3e2..9ddf6a8b 100644
--- a/Core/Resgrid.Model/WorkOrders/WorkOrderWorkflowPayload.cs
+++ b/Core/Resgrid.Model/WorkOrders/WorkOrderWorkflowPayload.cs
@@ -17,7 +17,13 @@ public static string Routing(JObject payload)
foreach (var field in new[] { "WorkOrderId", "Revision", "Status", "Priority", "TargetUnitId", "TargetGroupId", "AssignedToRoleId" })
if (payload[field]?.Type == JTokenType.Integer && payload[field].Value() >= 0 && payload[field].Value() <= int.MaxValue) safe[field] = payload[field].DeepClone();
if (payload["InventoryAssetId"]?.Type == JTokenType.String && Guid.TryParseExact(payload["InventoryAssetId"].Value(), "D", out var asset)) safe["InventoryAssetId"] = asset.ToString("D");
- if (payload["DueOn"]?.Type == JTokenType.Date) safe["DueOn"] = ((DateTimeOffset)payload["DueOn"]).UtcDateTime.ToString("O");
+ if (payload["DueOn"] is JValue { Type: JTokenType.Date } date)
+ {
+ var utc = date.Value is DateTimeOffset offset ? offset.UtcDateTime
+ : date.Value is DateTime value && value.Kind == DateTimeKind.Unspecified ? DateTime.SpecifyKind(value, DateTimeKind.Utc)
+ : ((DateTime)date.Value).ToUniversalTime();
+ safe["DueOn"] = utc.ToString("O");
+ }
else if (payload["DueOn"]?.Type == JTokenType.String && DateTimeOffset.TryParse(payload["DueOn"].Value(), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal, out var due)) safe["DueOn"] = due.UtcDateTime.ToString("O");
safe["Title"] = ProtectedDataEnvelope.RedactionValue; safe["is_redacted"] = true; safe["redacted_fields"] = new JArray("Title"); safe["catalog_version"] = WorkOrderTables.CatalogVersion;
return safe.ToString(Formatting.None);
diff --git a/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs b/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs
index 002d4e65..ac372acb 100644
--- a/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs
+++ b/Core/Resgrid.Model/WorkflowTemplateVariableCatalog.cs
@@ -771,17 +771,22 @@ public static IReadOnlyList GetVariableCatalog(Workf
case WorkflowTriggerEventType.InventoryIssued:
case WorkflowTriggerEventType.InventoryReturned:
case WorkflowTriggerEventType.InventoryAssetStatusChanged:
+ case WorkflowTriggerEventType.InventoryPurchaseOrderReceived:
+ case WorkflowTriggerEventType.InventoryLowStock:
+ case WorkflowTriggerEventType.InventoryExpiring:
+ case WorkflowTriggerEventType.InventoryCountCompleted:
+ case WorkflowTriggerEventType.InventoryReturnOverdue:
case WorkflowTriggerEventType.ControlledSubstanceRecorded:
foreach (var pair in Inventories.InventoryWorkflowPayload.Variables)
{
var type = pair.Variable switch
{
- "transaction_type" or "previous_status" or "status" or "reference_type" => "int",
+ "transaction_type" or "previous_status" or "status" or "reference_type" or "usage_type" or "purchase_order_status" or "line_count" or "variance_line_count" or "alert_type" => "int",
"quantity" or "from_quantity_before" or "from_quantity_after" or "to_quantity_before" or "to_quantity_after" => "decimal",
- "occurred_on" => "datetime",
+ "occurred_on" or "due_on" => "datetime",
_ => "string"
};
- list.Add(new TemplateVariableDescriptor("inventory." + pair.Variable, pair.Property + (pair.Variable == "item_name" ? "; always REDACTED" : string.Empty), type, false));
+ list.Add(new TemplateVariableDescriptor("inventory." + pair.Variable, pair.Property + (pair.Variable is "item_name" or "variance_value" ? "; always REDACTED" : string.Empty), type, false));
}
list.AddRange(new[]
{
diff --git a/Core/Resgrid.Model/WorkflowTriggerEventType.cs b/Core/Resgrid.Model/WorkflowTriggerEventType.cs
index a8cc0f3c..1f962201 100644
--- a/Core/Resgrid.Model/WorkflowTriggerEventType.cs
+++ b/Core/Resgrid.Model/WorkflowTriggerEventType.cs
@@ -176,7 +176,12 @@ public enum WorkflowTriggerEventType
InventoryTransferCompleted = 58,
InventoryIssued = 59,
InventoryReturned = 60,
+ InventoryLowStock = 61,
+ InventoryExpiring = 62,
+ InventoryCountCompleted = 63,
+ InventoryReturnOverdue = 166,
InventoryAssetStatusChanged = 64,
+ InventoryPurchaseOrderReceived = 65,
ControlledSubstanceRecorded = 66
}
diff --git a/Core/Resgrid.Services/GdprDataExportService.cs b/Core/Resgrid.Services/GdprDataExportService.cs
index d45e3fe6..df557ca8 100644
--- a/Core/Resgrid.Services/GdprDataExportService.cs
+++ b/Core/Resgrid.Services/GdprDataExportService.cs
@@ -53,11 +53,11 @@ public GdprDataExportService(
ICertificationService certificationService,
ITrainingService trainingService,
IShiftsService shiftsService,
- IEmailService emailService, IChecklistRepository checklists, Lazy checklistProtection, IChecklistReminderRepository checklistReminders, IWorkOrderRepository workOrders, IInventoryStore inventoryStore = null)
+ IEmailService emailService, IChecklistRepository checklists, Lazy checklistProtection, IChecklistReminderRepository checklistReminders, IWorkOrderRepository workOrders, IInventoryStore inventoryStore)
{
_repository = repository;
_workOrders = workOrders ?? throw new ArgumentNullException(nameof(workOrders));
- _inventoryStore = inventoryStore;
+ _inventoryStore = inventoryStore ?? throw new ArgumentNullException(nameof(inventoryStore));
_checklistReminders = checklistReminders ?? throw new ArgumentNullException(nameof(checklistReminders));
_userProfileService = userProfileService;
_memberSensitiveDataService = memberSensitiveDataService;
diff --git a/Core/Resgrid.Services/InventoryAlertNotifications.cs b/Core/Resgrid.Services/InventoryAlertNotifications.cs
new file mode 100644
index 00000000..bdd9ffb3
--- /dev/null
+++ b/Core/Resgrid.Services/InventoryAlertNotifications.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Resources;
+using System.Threading;
+using System.Threading.Tasks;
+using Resgrid.Localization;
+using Resgrid.Model.Services;
+
+namespace Resgrid.Services
+{
+ /// Hands off generic notices using routing metadata, without reading Inventory content or obtaining a grant.
+ public sealed class InventoryAlertNotifications
+ {
+ private readonly IInventoryAlertService _alerts;
+ private readonly IDepartmentsService _departments;
+ private readonly IUserProfileService _profiles;
+ private readonly IDepartmentSettingsService _settings;
+ private readonly ICommunicationService _communication;
+ private readonly TimeProvider _clock;
+ private static readonly ResourceManager Strings = new ResourceManager("Resgrid.Localization.Areas.User.Inventory.Inventory", typeof(SupportedLocales).Assembly);
+
+ public InventoryAlertNotifications(IInventoryAlertService alerts, IDepartmentsService departments, IUserProfileService profiles,
+ IDepartmentSettingsService settings, ICommunicationService communication, TimeProvider clock = null)
+ { _alerts = alerts; _departments = departments; _profiles = profiles; _settings = settings; _communication = communication; _clock = clock ?? TimeProvider.System; }
+
+ public async Task ProcessDepartmentAsync(int departmentId, CancellationToken ct)
+ {
+ ct.ThrowIfCancellationRequested();
+ var members = await _departments.GetAllMembersForDepartmentUnlimitedAsync(departmentId, true);
+ var users = members.Where(m => m.DepartmentId == departmentId && !m.IsDeleted && m.IsDisabled != true && !string.IsNullOrWhiteSpace(m.UserId))
+ .Select(m => m.UserId).Distinct(StringComparer.Ordinal).OrderBy(id => id, StringComparer.Ordinal).ToList();
+ var handedOff = 0; var failed = false;
+ foreach (var user in users)
+ {
+ // Core claim/access checks apply current Inventory permissions, including delegated managers.
+ for (var batch = 0; batch < 100; batch++)
+ {
+ ct.ThrowIfCancellationRequested();
+ var delivery = await _alerts.ClaimAlertAsync(departmentId, user);
+ if (delivery == null) break;
+ if (delivery.DepartmentId != departmentId || delivery.UserId != user || string.IsNullOrEmpty(delivery.Id)
+ || string.IsNullOrEmpty(delivery.AlertId) || string.IsNullOrEmpty(delivery.ClaimToken))
+ throw new InvalidOperationException("Inventory alert claim metadata is invalid.");
+ try
+ {
+ ct.ThrowIfCancellationRequested();
+ var profile = await _profiles.GetProfileByUserIdAsync(user, true);
+ ct.ThrowIfCancellationRequested();
+ var department = await _departments.GetDepartmentByIdAsync(departmentId, true);
+ var number = await _settings.GetTextToCallNumberForDepartmentAsync(departmentId);
+ var culture = Culture(profile?.Language);
+ var title = Strings.GetString("M5AlertNotificationTitle", culture) ?? "Inventory alerts";
+ var message = (Strings.GetString("M5AlertNotificationMessage", culture) ?? "Inventory needs attention. Sign in to review current alerts.")
+ + " " + (Config.SystemBehaviorConfig.ResgridBaseUrl ?? "").TrimEnd('/') + "/User/Inventory/Operations?tab=Alerts";
+ ct.ThrowIfCancellationRequested();
+ // Slow routing/profile lookups precede the final current-state and permission gate.
+ var allowed = department?.DepartmentId == departmentId && await _alerts.CanReceiveAlertAsync(departmentId, user, delivery.AlertId);
+ ct.ThrowIfCancellationRequested();
+ if (allowed && (!delivery.LeaseUntil.HasValue || delivery.LeaseUntil.Value <= _clock.GetUtcNow().UtcDateTime.AddSeconds(30)))
+ throw new InvalidOperationException("Inventory alert notification lease is expiring.");
+ var sent = allowed && await _communication.SendNotificationAsync(user, departmentId, message, number, department, title, profile);
+ // Persist an accepted handoff even if cancellation arrived during the provider call.
+ await _alerts.FinishAlertAsync(departmentId, delivery.Id, delivery.ClaimToken, sent);
+ if (sent) handedOff++;
+ }
+ catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
+ catch (Exception ex)
+ {
+ failed = true;
+ Resgrid.Framework.Logging.LogError($"Inventory alert notification failed for department {departmentId}: {ex.GetType().FullName}.");
+ // Completion is claim-token fenced. A completed or superseded handoff cannot be reset.
+ try { await _alerts.FinishAlertAsync(departmentId, delivery.Id, delivery.ClaimToken, false); }
+ catch { /* The lease recovers a failed release without storing exception or message content. */ }
+ }
+ }
+ }
+ ct.ThrowIfCancellationRequested();
+ if (failed) throw new InvalidOperationException("Inventory alert notifications require a retry.");
+ return handedOff;
+ }
+
+ private static CultureInfo Culture(string language)
+ {
+ try
+ {
+ var culture = CultureInfo.GetCultureInfo(language ?? "en");
+ if (SupportedLocales.GetSupportedCultures().Contains(culture.TwoLetterISOLanguageName)) return culture;
+ }
+ catch (CultureNotFoundException) { }
+ return CultureInfo.GetCultureInfo("en");
+ }
+ }
+}
diff --git a/Core/Resgrid.Services/InventoryAlerts.cs b/Core/Resgrid.Services/InventoryAlerts.cs
new file mode 100644
index 00000000..f50b71ab
--- /dev/null
+++ b/Core/Resgrid.Services/InventoryAlerts.cs
@@ -0,0 +1,225 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Resgrid.Model;
+using Resgrid.Model.Inventories;
+
+namespace Resgrid.Services
+{
+ public sealed partial class InventoryModernizationService
+ {
+ public Task> AlertDepartmentsAsync(int afterDepartmentId) => _store.AlertDepartmentsAsync(afterDepartmentId);
+ private async Task AlertTransactionAsync(int departmentId, Func, Task> work)
+ {
+ if (_uow.Transaction != null) throw new InvalidOperationException("Alert processing owns its transaction.");
+ var events = new List(); T result;
+ try
+ {
+ await _uow.CreateOrGetConnectionAsync(CancellationToken.None); await _store.LockDepartmentAsync(departmentId);
+ if (!await _auth.IsEnabledAsync(departmentId) || !await _store.HasLegacyMigrationAsync(departmentId)) { _uow.CommitChanges(); return default; }
+ result = await work(events); _uow.CommitChanges();
+ }
+ catch { _uow.DiscardChanges(); throw; }
+ await _outbox.DispatchAfterCommitAsync(events); return result;
+ }
+ private async Task UpdateAlertMetadataAsync(T row) where T : InventoryRow
+ {
+ if (row.Content != null) throw new InvalidOperationException("Alert metadata cannot contain protected content.");
+ row.ModifiedOn = Now; var revision = row.Revision++; await _store.UpdateAsync(row, revision);
+ }
+ private async Task RequireAlertAccessAsync(InventoryActor actor, InventoryAlert alert)
+ {
+ if (alert == null || alert.DepartmentId != actor.DepartmentId) throw new InventoryException(404, "Unavailable");
+ if (alert.LocationId == null)
+ {
+ await _auth.RequireAsync(actor, false, PermissionTypes.AdjustInventory);
+ // Department totals are disclosed only when every contributing holder is visible.
+ var holders = (await _store.RelatedAsync(actor.DepartmentId, "ItemId", alert.ItemId)).Where(s => !s.IsDeleted && s.Quantity != 0).Select(s => s.LocationId)
+ .Concat((await _store.RelatedAsync(actor.DepartmentId, "ItemId", alert.ItemId)).Where(a => !a.IsDeleted && a.Status is 0 or 1 or 2 or 3).Select(a => a.CurrentLocationId)).Distinct();
+ foreach (var holder in holders) if (await LiveAlertLocationAsync(actor.DepartmentId, holder)) await LocationAsync(actor, holder);
+ }
+ else await LocationAsync(actor, alert.LocationId, false, PermissionTypes.AdjustInventory);
+ var item = await _store.GetAsync(actor.DepartmentId, alert.ItemId);
+ if (item == null) throw new InventoryException(404, "Unavailable");
+ if (item.IsControlledSubstance) await _auth.RequireAsync(actor, false, PermissionTypes.ManageControlledSubstances);
+ }
+ private async Task SetAlertAsync(int departmentId, InventoryAlertType type, string itemId, string locationId, string lotId, string assetId, string issuanceId, bool active, DateTime? due, decimal? quantity, List events)
+ {
+ var key = Fingerprint(new { type, itemId, locationId, lotId, assetId, issuanceId });
+ var current = await _store.OpenAlertAsync(departmentId, key);
+ if (!active)
+ {
+ if (current != null) { current.Status = 1; current.ResolvedOn = Now; await UpdateAlertMetadataAsync(current); }
+ return;
+ }
+ if (current != null)
+ {
+ if (current.Quantity != quantity || current.DueOn != due) { current.Quantity = quantity; current.DueOn = due; await UpdateAlertMetadataAsync(current); }
+ return;
+ }
+ var row = New(new InventoryActor { DepartmentId = departmentId });
+ row.AlertType = (int)type; row.DedupKey = key; row.ItemId = itemId; row.LocationId = locationId; row.LotId = lotId; row.AssetId = assetId; row.IssuanceId = issuanceId; row.OpenedOn = Now; row.DueOn = due; row.Quantity = quantity;
+ await _store.InsertAsync(row);
+ var trigger = type == InventoryAlertType.LowStock ? WorkflowTriggerEventType.InventoryLowStock : type == InventoryAlertType.OverdueReturn ? WorkflowTriggerEventType.InventoryReturnOverdue : WorkflowTriggerEventType.InventoryExpiring;
+ var entry = await _outbox.EnqueueAsync(departmentId, "Inventory", new DomainEventEnvelope { EventName = trigger.ToString(), Trigger = trigger, SchemaVersion = 1,
+ AggregateType = "InventoryAlert", AggregateId = row.Id, OccurredOn = Now,
+ Payload = new { InventoryEvent = true, AlertId = row.Id, row.AlertType, row.ItemId, row.LocationId, row.LotId, row.AssetId, row.IssuanceId, row.Quantity, row.DueOn, OccurredOn = Now } });
+ events.Add(entry.DomainEventOutboxId);
+ }
+ private async Task RefreshLowStockAsync(InventoryActor actor, string itemId, List events)
+ {
+ var item = await GetAsync(actor, itemId); var content = Decode(item);
+ decimal quantity = 0;
+ if (item.TrackingMode == 0)
+ {
+ foreach (var stock in await _store.RelatedAsync(actor.DepartmentId, "ItemId", itemId))
+ if (!stock.IsDeleted && await LiveAlertLocationAsync(actor.DepartmentId, stock.LocationId)) quantity += stock.Quantity;
+ }
+ else foreach (var asset in await _store.RelatedAsync(actor.DepartmentId, "ItemId", itemId))
+ if (!asset.IsDeleted && asset.Status is 0 or 1 or 2 or 3 && await LiveAlertLocationAsync(actor.DepartmentId, asset.CurrentLocationId)) quantity++;
+ var threshold = content.ReorderPoint ?? content.MinLevel;
+ await SetAlertAsync(actor.DepartmentId, InventoryAlertType.LowStock, itemId, null, null, null, null,
+ !item.IsDeleted && item.IsActive && threshold.HasValue && quantity <= threshold.Value, null, quantity, events);
+ }
+ private async Task RefreshPostingAlertsAsync(InventoryActor actor, InventoryCommand command, List events)
+ {
+ var items = command.Lines.Select(l => l.ItemId).ToHashSet();
+ var moved = command.Lines.Where(l => l.AssetId != null).Select(l => l.AssetId).ToHashSet();
+ if (moved.Count > 0)
+ {
+ var locations = (await AllAsync(actor.DepartmentId)).ToDictionary(l => l.Id);
+ if (locations.Values.Any(l => l.ContainerAssetId != null && moved.Contains(l.ContainerAssetId)))
+ {
+ var assets = (await AllAsync(actor.DepartmentId)).ToDictionary(a => a.Id);
+ var affected = new HashSet();
+ foreach (var location in locations.Values)
+ {
+ var current = location; var seen = new HashSet();
+ while (current != null && seen.Add(current.Id) && seen.Count <= 32)
+ {
+ if (current.ContainerAssetId != null && moved.Contains(current.ContainerAssetId)) { affected.Add(location.Id); break; }
+ var parent = current.ContainerAssetId != null && assets.TryGetValue(current.ContainerAssetId, out var holder) ? holder.CurrentLocationId : current.ParentLocationId;
+ current = parent != null && locations.TryGetValue(parent, out var next) ? next : null;
+ }
+ }
+ foreach (var stock in await AllAsync(actor.DepartmentId)) if (affected.Contains(stock.LocationId)) items.Add(stock.ItemId);
+ foreach (var asset in assets.Values) if (asset.CurrentLocationId != null && affected.Contains(asset.CurrentLocationId)) items.Add(asset.ItemId);
+ }
+ }
+ foreach (var item in items) await RefreshLowStockAsync(actor, item, events);
+ }
+ public Task RefreshAlertsAsync(InventoryActor actor) => TransactionAsync(actor, async events =>
+ {
+ await _auth.RequireAsync(actor, false, PermissionTypes.AdjustInventory);
+ foreach (var item in await AllAsync(actor.DepartmentId)) await RefreshLowStockAsync(actor, item.Id, events);
+ await EvaluateDatedAlertsAsync(actor.DepartmentId, events); return true;
+ });
+ public Task SweepAlertsAsync(int departmentId) => AlertTransactionAsync(departmentId, async events => { await EvaluateDatedAlertsAsync(departmentId, events); return true; });
+ private async Task LiveAlertLocationAsync(int departmentId, string id)
+ {
+ if (id == null) return false;
+ var location = await _store.GetAsync(departmentId, id);
+ if (location == null || location.IsDeleted) return false;
+ try { return !(await EffectiveLocationAsync(departmentId, location)).IsDeleted; }
+ catch (InventoryException ex) when (ex.Code == "LocationUnavailable") { return false; }
+ }
+ private async Task EvaluateDatedAlertsAsync(int departmentId, List events)
+ {
+ var desired = new HashSet();
+ var items = (await AllAsync(departmentId)).Where(i => !i.IsDeleted && i.IsActive).Select(i => i.Id).ToHashSet();
+ var lots = (await AllAsync(departmentId)).ToDictionary(l => l.Id);
+ async Task Add(InventoryAlertType type, string item, string location, string lot, string asset, string issuance, DateTime due, decimal quantity)
+ {
+ if (!items.Contains(item)) return;
+ if (location != null)
+ {
+ var holder = await _store.GetAsync(departmentId, location);
+ if (holder == null || holder.IsDeleted) return;
+ try { await EffectiveLocationAsync(departmentId, holder); } catch (InventoryException ex) when (ex.Code == "LocationUnavailable") { return; }
+ }
+ desired.Add(Fingerprint(new { type, itemId = item, locationId = location, lotId = lot, assetId = asset, issuanceId = issuance }));
+ await SetAlertAsync(departmentId, type, item, location, lot, asset, issuance, true, due, quantity, events);
+ }
+ foreach (var stock in await AllAsync(departmentId))
+ if (!stock.IsDeleted && stock.Quantity > 0 && stock.LotId != null && lots.TryGetValue(stock.LotId, out var lot) && !lot.IsDeleted && lot.ExpiresOn <= Now.AddDays(30))
+ await Add(lot.ExpiresOn <= Now ? InventoryAlertType.Expired : InventoryAlertType.ExpiringSoon, stock.ItemId, stock.LocationId, stock.LotId, null, null, lot.ExpiresOn.Value, stock.Quantity);
+ foreach (var asset in await AllAsync(departmentId))
+ {
+ var expiry = asset.ExpiresOn;
+ if (asset.LotId != null && lots.TryGetValue(asset.LotId, out var lot) && lot.ExpiresOn.HasValue && (!expiry.HasValue || lot.ExpiresOn < expiry)) expiry = lot.ExpiresOn;
+ if (!asset.IsDeleted && asset.Status is 0 or 1 or 2 or 3 && asset.CurrentLocationId != null && expiry <= Now.AddDays(30))
+ await Add(expiry <= Now ? InventoryAlertType.Expired : InventoryAlertType.ExpiringSoon, asset.ItemId, asset.CurrentLocationId, asset.LotId, asset.Id, null, expiry.Value, 1);
+ }
+ foreach (var issuance in await AllAsync(departmentId))
+ if (!issuance.IsDeleted && issuance.Status is 0 or 2 && issuance.Quantity > issuance.ReturnedQuantity && issuance.ExpectedReturnOn < Now)
+ await Add(InventoryAlertType.OverdueReturn, issuance.ItemId, issuance.LocationId, issuance.LotId, issuance.AssetId, issuance.Id, issuance.ExpectedReturnOn.Value, issuance.Quantity - issuance.ReturnedQuantity);
+ foreach (var alert in await AllOpenAlertsAsync(departmentId))
+ if (alert.Status == 0 && (!items.Contains(alert.ItemId) || alert.AlertType != 0 && !desired.Contains(alert.DedupKey))) { alert.Status = 1; alert.ResolvedOn = Now; await UpdateAlertMetadataAsync(alert); }
+ }
+ public async Task CanReceiveAlertAsync(int departmentId, string userId, string alertId)
+ {
+ if (!await _auth.IsEnabledAsync(departmentId) || !await _store.HasLegacyMigrationAsync(departmentId)) return false;
+ var alert = await _store.GetAsync(departmentId, alertId);
+ if (alert == null || alert.Status != 0) return false;
+ var item = await _store.GetAsync(departmentId, alert.ItemId);
+ if (item == null || item.IsDeleted || !item.IsActive) return false;
+ if (alert.AlertType == (int)InventoryAlertType.OverdueReturn)
+ {
+ var issuance = await _store.GetAsync(departmentId, alert.IssuanceId);
+ if (issuance == null || issuance.IsDeleted || issuance.Status is not (0 or 2) || issuance.Quantity <= issuance.ReturnedQuantity || !issuance.ExpectedReturnOn.HasValue || issuance.ExpectedReturnOn >= Now) return false;
+ }
+ else if (alert.AlertType is 1 or 2)
+ {
+ DateTime? expiry = null;
+ if (alert.LotId != null) { var lot = await _store.GetAsync(departmentId, alert.LotId); if (lot == null || lot.IsDeleted) return false; expiry = lot.ExpiresOn; }
+ if (alert.AssetId != null)
+ {
+ var asset = await _store.GetAsync(departmentId, alert.AssetId);
+ if (asset == null || asset.IsDeleted || asset.Status is not (0 or 1 or 2 or 3) || asset.CurrentLocationId != alert.LocationId) return false;
+ if (asset.ExpiresOn.HasValue && (!expiry.HasValue || asset.ExpiresOn < expiry)) expiry = asset.ExpiresOn;
+ }
+ else if (!(await _store.RelatedAsync(departmentId, "ItemId", alert.ItemId)).Any(s => !s.IsDeleted && s.LotId == alert.LotId && s.LocationId == alert.LocationId && s.Quantity > 0)) return false;
+ if (!expiry.HasValue || (alert.AlertType == 2 ? expiry > Now : expiry <= Now || expiry > Now.AddDays(30))) return false;
+ }
+ try { await RequireAlertAccessAsync(new InventoryActor { DepartmentId = departmentId, UserId = userId }, alert); return true; }
+ catch (InventoryException ex) when (ex.StatusCode is 403 or 404 || ex.Code == "LocationUnavailable") { return false; }
+ }
+ public Task ClaimAlertAsync(int departmentId, string userId) => AlertTransactionAsync(departmentId, async events =>
+ {
+ foreach (var alert in await AllOpenAlertsAsync(departmentId))
+ {
+ var delivery = await _store.AlertDeliveryAsync(departmentId, alert.Id, userId);
+ if (delivery != null && (delivery.State is 2 or 3 || delivery.NextAttemptOn > Now || delivery.LeaseUntil > Now)) continue;
+ if (!await CanReceiveAlertAsync(departmentId, userId, alert.Id)) continue;
+ if (delivery == null)
+ {
+ delivery = New(new InventoryActor { DepartmentId = departmentId }); delivery.AlertId = alert.Id; delivery.UserId = userId; delivery.NextAttemptOn = Now;
+ await _store.InsertAsync(delivery);
+ }
+ delivery.State = 1; delivery.ClaimToken = Guid.NewGuid().ToString("D"); delivery.LeaseUntil = Now.AddMinutes(30); delivery.AttemptCount++;
+ await UpdateAlertMetadataAsync(delivery); return delivery;
+ }
+ return null;
+ });
+ private async Task> AllOpenAlertsAsync(int departmentId)
+ {
+ var result = new List();
+ for (var skip = 0; skip <= 100000; skip += 500)
+ {
+ var page = await _store.OpenAlertsAsync(departmentId, skip); result.AddRange(page.Take(500));
+ if (page.Count <= 500) return result;
+ }
+ throw new InventoryException(409, "InventoryTooLarge");
+ }
+ public Task FinishAlertAsync(int departmentId, string deliveryId, string claimToken, bool handedOff) => AlertTransactionAsync(departmentId, async events =>
+ {
+ var row = await _store.GetAsync(departmentId, deliveryId);
+ if (row == null || row.State != 1 || row.ClaimToken != claimToken || row.LeaseUntil <= Now) return false;
+ row.State = handedOff ? 2 : 0; row.HandedOffOn = handedOff ? Now : null; row.LeaseUntil = null; row.ClaimToken = null;
+ row.NextAttemptOn = Now.AddMinutes(Math.Min(1440, 5 * Math.Pow(2, Math.Min(row.AttemptCount, 8))));
+ await UpdateAlertMetadataAsync(row); return true;
+ });
+ }
+}
diff --git a/Core/Resgrid.Services/InventoryCatalog.cs b/Core/Resgrid.Services/InventoryCatalog.cs
index a9ab1e68..0ee7bb30 100644
--- a/Core/Resgrid.Services/InventoryCatalog.cs
+++ b/Core/Resgrid.Services/InventoryCatalog.cs
@@ -22,9 +22,16 @@ public Task SaveItemAsync(InventoryActor actor, InventoryItemInpu
if (input.CategoryId != null && (await GetAsync(actor, input.CategoryId)).IsDeleted) throw new InventoryException(404, "Unavailable");
if (input.IsControlledSubstance) await _auth.RequireAsync(actor, true, PermissionTypes.ManageControlledSubstances);
var row = input.Id == null ? New(actor) : await GetAsync(actor, input.Id);
+ Cost(input.Details.DefaultUnitCost);
+ var existingDetails = Decode(row);
+ input.Details.CurrencyCode = Currency(input.Details.CurrencyCode) ?? existingDetails.CurrencyCode;
+ if (existingDetails.CurrencyCode != null && existingDetails.CurrencyCode != input.Details.CurrencyCode && ((await _store.RelatedAsync(actor.DepartmentId, "ItemId", row.Id)).Count != 0 || await HasOpenPurchaseLineAsync(actor, row.Id)))
+ throw new InventoryException(409, "ItemCurrencyLocked");
+ input.Details.AverageUnitCost = existingDetails.AverageUnitCost;
+ if (input.Details.PreferredVendorId != null && input.Details.PreferredVendorId != existingDetails.PreferredVendorId && (await GetAsync(actor, input.Details.PreferredVendorId)).IsDeleted) throw new InventoryException(409, "VendorUnavailable");
if (input.Id != null && row.Revision != input.Revision) throw new InventoryException(409, "RevisionConflict");
if (input.Id != null && (row.TrackingMode != (int)input.TrackingMode || row.IsKit != input.IsKit || row.RequiresLotTracking != input.RequiresLotTracking || row.RequiresExpiration != input.RequiresExpiration || row.IsControlledSubstance != input.IsControlledSubstance)
- && (await _store.RelatedAsync(actor.DepartmentId, "ItemId", row.Id)).Count > 0) throw new InventoryException(409, "ItemTrackingLocked");
+ && ((await _store.RelatedAsync(actor.DepartmentId, "ItemId", row.Id)).Count > 0 || await HasOpenPurchaseLineAsync(actor, row.Id))) throw new InventoryException(409, "ItemTrackingLocked");
foreach (var other in (await AllAsync(actor.DepartmentId)).Where(x => !x.IsDeleted && x.Id != row.Id))
{
var details = Decode(await RevealAsync(actor, other));
@@ -34,7 +41,7 @@ public Task SaveItemAsync(InventoryActor actor, InventoryItemInpu
row.CategoryId = input.CategoryId; row.TrackingMode = (int)input.TrackingMode; row.IsKit = input.IsKit; row.RequiresLotTracking = input.RequiresLotTracking;
row.RequiresExpiration = input.RequiresExpiration; row.IsControlledSubstance = input.IsControlledSubstance; row.IsActive = input.IsActive;
input.Details.Name = input.Details.Name.Trim(); row.Content = JsonConvert.SerializeObject(input.Details);
- await SaveAsync(actor, row, input.Id == null); await AuditAsync(actor, row, "InventoryItemSaved"); return await RevealAsync(actor, row);
+ await SaveAsync(actor, row, input.Id == null); await AuditAsync(actor, row, "InventoryItemSaved"); await RefreshLowStockAsync(actor, row.Id, events); return await RevealAsync(actor, row);
});
public Task SaveCategoryAsync(InventoryActor actor, string id, int revision, string name, string parentId) => TransactionAsync(actor, async events =>
{
@@ -84,6 +91,8 @@ public Task SaveLotAsync(InventoryActor actor, InventoryLot lot, I
await _auth.RequireAsync(actor, true); if (lot == null || details == null) throw new InventoryException(400, "LotRequired"); Text(details.LotNumber);
var item = await GetAsync(actor, lot.ItemId);
if (item.IsDeleted || !item.IsActive || item.RequiresExpiration && !lot.ExpiresOn.HasValue || details.UnitCost < 0) throw new InventoryException(400, "InvalidLot");
+ Cost(details.UnitCost);
+ if (details.VendorId != null && (await GetAsync(actor, details.VendorId)).IsDeleted) throw new InventoryException(409, "VendorUnavailable");
foreach (var other in await _store.RelatedAsync(actor.DepartmentId, "ItemId", item.Id))
if (Decode(await RevealAsync(actor, other)).LotNumber == details.LotNumber) throw new InventoryException(409, "DuplicateLot");
var row = New(actor); row.ItemId = item.Id; row.ExpiresOn = lot.ExpiresOn?.ToUniversalTime(); row.ReceivedOn = Now; row.Content = JsonConvert.SerializeObject(details);
@@ -92,13 +101,18 @@ public Task SaveLotAsync(InventoryActor actor, InventoryLot lot, I
public Task ArchiveAsync(InventoryActor actor, string id, int revision) where T : InventoryMutableRow => TransactionAsync(actor, async events =>
{
await _auth.RequireAsync(actor, true); var row = await GetAsync(actor, id); if (row.Revision != revision) throw new InventoryException(409, "RevisionConflict");
- if (row is not (InventoryItem or InventoryCategory or InventoryLocation or InventoryKit)) throw new InventoryException(400, "ArchiveNotSupported");
+ if (row is not (InventoryItem or InventoryCategory or InventoryLocation or InventoryKit or InventoryVendor)) throw new InventoryException(400, "ArchiveNotSupported");
+ if (row is InventoryVendor && (await _store.RelatedAsync(actor.DepartmentId, "VendorId", id)).Any(p => !p.IsDeleted && p.Status is 0 or 1 or 2)) throw new InventoryException(409, "VendorHasOpenOrders");
if (row is InventoryItem item && ((await _store.RelatedAsync(actor.DepartmentId, "ItemId", id)).Any(x => x.Quantity != 0) || (await _store.RelatedAsync(actor.DepartmentId, "ItemId", id)).Any(x => !x.IsDeleted && x.Status is not (4 or 5 or 6)))) throw new InventoryException(409, "StockRemains");
if (row is InventoryLocation location && (location.IsDefault || (await _store.RelatedAsync(actor.DepartmentId, "LocationId", id)).Any(x => x.Quantity != 0) || (await _store.RelatedAsync(actor.DepartmentId, "CurrentLocationId", id)).Any() || (await _store.RelatedAsync(actor.DepartmentId, "ParentLocationId", id)).Any(x => !x.IsDeleted))) throw new InventoryException(409, "LocationInUse");
if (row is InventoryCategory && ((await _store.RelatedAsync(actor.DepartmentId, "CategoryId", id)).Any(x => !x.IsDeleted) || (await _store.RelatedAsync(actor.DepartmentId, "ParentCategoryId", id)).Any(x => !x.IsDeleted))) throw new InventoryException(409, "CategoryInUse");
row.IsDeleted = true; await SaveAsync(actor, row, false); await AuditAsync(actor, row, "InventoryArchived"); return true;
});
- public Task RebuildStocksAsync(InventoryActor actor) => TransactionAsync(actor, async events => { await _auth.RequireAsync(actor, true); await _store.RebuildStocksAsync(actor.DepartmentId); return true; });
+ public Task RebuildStocksAsync(InventoryActor actor) => TransactionAsync(actor, async events => {
+ await _auth.RequireAsync(actor, true); await _store.RebuildStocksAsync(actor.DepartmentId);
+ foreach (var item in await AllAsync(actor.DepartmentId)) await RefreshLowStockAsync(actor, item.Id, events);
+ return true;
+ });
public async Task> GetByReferenceAsync(InventoryActor actor, InventoryReferenceType type, string id)
{
await _auth.RequireAsync(actor); var result = new System.Collections.Generic.List();
diff --git a/Core/Resgrid.Services/InventoryChecklistAssets.cs b/Core/Resgrid.Services/InventoryChecklistAssets.cs
index ca8ae281..2f2a95c2 100644
--- a/Core/Resgrid.Services/InventoryChecklistAssets.cs
+++ b/Core/Resgrid.Services/InventoryChecklistAssets.cs
@@ -114,13 +114,17 @@ public async Task> AtCallAsync(ChecklistActor actor
var units = unitIds.ToHashSet(); var result = new List(); if (units.Count == 0) return result;
var assets = (await AllAsync(actor.DepartmentId)).Where(a => a.DepartmentId == actor.DepartmentId).ToDictionary(a => a.Id, StringComparer.Ordinal);
var locations = (await AllAsync(actor.DepartmentId)).Where(l => l.DepartmentId == actor.DepartmentId).ToDictionary(l => l.Id, StringComparer.Ordinal);
- var ledger = (await AllAsync(actor.DepartmentId)).Where(t => t.DepartmentId == actor.DepartmentId && t.AssetId != null)
- .OrderBy(t => t.OccurredOn).ThenBy(t => t.EntryId).ToList();
+ var ledger = await ChecklistAssetHistoryAsync(actor.DepartmentId, assets.Keys, callUtc, false);
var histories = ledger.GroupBy(t => t.AssetId).ToDictionary(g => g.Key, g => g.ToList(), StringComparer.Ordinal);
- foreach (var asset in assets.Values)
+ var candidates = assets.Values.Select(asset => (Asset: asset, State: HistoricalChecklistPosition(asset.Id, callUtc, long.MaxValue, assets, locations, histories)))
+ .Where(candidate => candidate.State?.Location?.UnitId != null && units.Contains(candidate.State.Location.UnitId.Value)).ToList();
+ // Later container changes are needed to prove the first departure, including moves into a different bag on the same unit.
+ var departureAssets = candidates.Select(c => c.Asset.Id).Concat(locations.Values.Select(l => l.ContainerAssetId).Where(id => id != null)).Distinct();
+ if (candidates.Count > 0) ledger.AddRange(await ChecklistAssetHistoryAsync(actor.DepartmentId, departureAssets, callUtc, true));
+ histories = ledger.GroupBy(t => t.AssetId).ToDictionary(g => g.Key, g => g.OrderBy(t => t.OccurredOn).ThenBy(t => t.EntryId).ToList(), StringComparer.Ordinal);
+ foreach (var candidate in candidates)
{
- var state = HistoricalChecklistPosition(asset.Id, callUtc, long.MaxValue, assets, locations, histories);
- if (state?.Location?.UnitId == null || !units.Contains(state.Location.UnitId.Value)) continue;
+ var asset = candidate.Asset; var state = candidate.State;
if (!await _auth.CanLocationAsync(inventoryActor, state.Location)) continue;
var unit = await _units.GetUnitByIdAsync(state.Location.UnitId.Value); if (unit?.DepartmentId != actor.DepartmentId) continue;
string name;
@@ -145,6 +149,20 @@ public async Task> AtCallAsync(ChecklistActor actor
return result;
}
+ private async Task> ChecklistAssetHistoryAsync(int departmentId, IEnumerable assetIds, DateTime at, bool after)
+ {
+ var result = new List();
+ foreach (var batch in assetIds.Distinct().Chunk(500))
+ for (var skip = 0; ; skip += 500)
+ {
+ var rows = await _store.AssetHistoryAsync(departmentId, batch, at, after, skip);
+ result.AddRange(rows.Take(500));
+ if (result.Count > 100000) throw new ChecklistException(400, "ReportTooLarge");
+ if (rows.Count <= 500) break;
+ }
+ return result.OrderBy(t => t.OccurredOn).ThenBy(t => t.EntryId).ToList();
+ }
+
private sealed class HistoricalChecklistAssetPosition
{
public InventoryLocation Location { get; set; }
diff --git a/Core/Resgrid.Services/InventoryCosting.cs b/Core/Resgrid.Services/InventoryCosting.cs
new file mode 100644
index 00000000..2d3480a6
--- /dev/null
+++ b/Core/Resgrid.Services/InventoryCosting.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Resgrid.Model;
+using Resgrid.Model.Inventories;
+
+namespace Resgrid.Services
+{
+ public sealed partial class InventoryModernizationService
+ {
+ private static void Cost(decimal? value)
+ { if (value < 0 || value > 100000000m || value.HasValue && decimal.Round(value.Value, 6) != value) throw new InventoryException(400, "InvalidCost"); }
+ private static decimal Money(decimal value) => decimal.Round(value, 6, MidpointRounding.ToEven);
+ private static string Currency(string value)
+ {
+ if (value == null) return null;
+ value = value.Trim().ToUpperInvariant();
+ if (value.Length != 3 || value.Any(c => c < 'A' || c > 'Z')) throw new InventoryException(400, "InvalidCurrency");
+ return value;
+ }
+ private async Task CurrentUnitCostAsync(InventoryActor actor, InventoryItem item, string lotId, string assetId)
+ {
+ var lotCost = lotId == null ? null : Decode(await GetAsync(actor, lotId)).UnitCost;
+ var assetCost = assetId == null ? null : Decode(await GetAsync(actor, assetId)).AcquisitionCost;
+ var details = Decode(item);
+ return lotCost ?? assetCost ?? details.AverageUnitCost ?? details.DefaultUnitCost;
+ }
+ private sealed class PostingCost
+ {
+ public decimal? UnitCost; public string CurrencyCode; public decimal? AverageAfter; public bool UpdateAverage;
+ }
+ private async Task CostForPostingAsync(InventoryActor actor, InventoryItem item, InventoryPosting line)
+ {
+ Cost(line.UnitCost);
+ var details = Decode(item);
+ var result = new PostingCost { CurrencyCode = details.CurrencyCode };
+ InventoryTransaction original = null;
+ if (line.ReversesTransactionId != null) original = await _store.GetAsync(actor.DepartmentId, line.ReversesTransactionId);
+ else if (line.Type == InventoryTransactionType.Return && line.IssuanceId != null)
+ original = (await _store.RelatedAsync(actor.DepartmentId, "IssuanceId", line.IssuanceId)).SingleOrDefault(t => t.TransactionType == (int)InventoryTransactionType.Issue);
+ if (original != null)
+ {
+ original = await GetAsync(actor, original.Id);
+ var frozen = JObject.Parse(original.Content ?? "{}");
+ result.UnitCost = frozen.Value("UnitCost"); result.CurrencyCode = frozen.Value("CurrencyCode");
+ if (line.UnitCost.HasValue && line.UnitCost != result.UnitCost) throw new InventoryException(409, "ReversalCostMismatch");
+ }
+ else
+ {
+ if (line.UnitCost.HasValue && line.Type != InventoryTransactionType.Receive) throw new InventoryException(400, "CostOverrideUnsupported");
+ if (line.Type == InventoryTransactionType.Receive)
+ {
+ var lotCost = line.LotId == null ? null : Decode(await GetAsync(actor, line.LotId)).UnitCost;
+ var assetCost = line.AssetId == null ? null : Decode(await GetAsync(actor, line.AssetId)).AcquisitionCost;
+ if (lotCost.HasValue && (line.UnitCost.HasValue && line.UnitCost != lotCost || assetCost.HasValue && assetCost != lotCost)) throw new InventoryException(409, "LotCostMismatch");
+ if (assetCost.HasValue && line.UnitCost.HasValue && line.UnitCost != assetCost) throw new InventoryException(409, "AssetCostMismatch");
+ }
+ result.UnitCost = line.UnitCost ?? await CurrentUnitCostAsync(actor, item, line.LotId, line.AssetId);
+ }
+ Cost(result.UnitCost);
+ var restoresStock = line.ReversesTransactionId != null && original?.FromLocationId != null && original.ToLocationId == null;
+ var removesStock = line.ReversesTransactionId != null && original?.FromLocationId == null && original?.ToLocationId != null;
+ if (line.Type != InventoryTransactionType.Receive && !restoresStock && !removesStock) return result;
+ decimal before;
+ if (item.TrackingMode == (int)InventoryTrackingMode.Bulk)
+ {
+ // Fixed-price lots carry their own value. Mixing them into the fallback average
+ // would price untracked stock twice: once in this average and again by lot cost.
+ var pricedLots = new System.Collections.Generic.HashSet();
+ foreach (var lot in await _store.RelatedAsync(actor.DepartmentId, "ItemId", item.Id))
+ if (Decode(await RevealAsync(actor, lot)).UnitCost.HasValue) pricedLots.Add(lot.Id);
+ if (line.LotId != null && pricedLots.Contains(line.LotId)) return result;
+ before = (await _store.RelatedAsync(actor.DepartmentId, "ItemId", item.Id)).Where(s => s.LotId == null || !pricedLots.Contains(s.LotId)).Sum(s => s.Quantity);
+ }
+ else before = (await _store.RelatedAsync(actor.DepartmentId, "ItemId", item.Id)).Count(a => !a.IsDeleted && a.CurrentLocationId != null && a.Status is 0 or 1 or 2 or 3);
+ var incoming = line.Type == InventoryTransactionType.Receive || restoresStock;
+ if (before < 0 && !incoming) throw new InventoryException(409, "CostReconciliationRequired");
+ before = Math.Max(0, before);
+ var oldCost = details.AverageUnitCost ?? details.DefaultUnitCost;
+ var after = before + (incoming ? line.Quantity : -line.Quantity);
+ if (after < 0) throw new InventoryException(409, "CostReconciliationRequired");
+ result.UpdateAverage = true;
+ if (!result.UnitCost.HasValue || before > 0 && !oldCost.HasValue)
+ result.AverageAfter = null; // Unknown opening value is not silently priced as zero.
+ else if (after == 0) result.AverageAfter = null;
+ else
+ {
+ var value = before * (oldCost ?? 0) + (incoming ? 1 : -1) * line.Quantity * result.UnitCost.Value;
+ if (value < 0) throw new InventoryException(409, "CostReconciliationRequired");
+ result.AverageAfter = Money(value / after); Cost(result.AverageAfter);
+ }
+ return result;
+ }
+ private async Task ApplyAverageCostAsync(InventoryActor actor, InventoryItem item, PostingCost cost)
+ {
+ if (!cost.UpdateAverage) return;
+ var details = Decode(item); details.AverageUnitCost = cost.AverageAfter;
+ item.Content = JsonConvert.SerializeObject(details); await SaveAsync(actor, item, false);
+ }
+ public async Task GetValuationAsync(InventoryActor actor, string locationId = null)
+ {
+ await _auth.RequireAsync(actor, false, PermissionTypes.AdjustInventory);
+ if (_uow.Transaction != null) throw new InvalidOperationException("Inventory valuation owns its read transaction.");
+ try
+ {
+ await _uow.CreateOrGetConnectionAsync(System.Threading.CancellationToken.None);
+ // Use the posting lock so quantities and protected average costs describe the same instant.
+ await _store.LockDepartmentAsync(actor.DepartmentId);
+ var result = await ReadValuationAsync(actor, locationId); _uow.CommitChanges(); return result;
+ }
+ catch { _uow.DiscardChanges(); throw; }
+ }
+ private async Task ReadValuationAsync(InventoryActor actor, string locationId)
+ {
+ await _auth.RequireAsync(actor, false, PermissionTypes.AdjustInventory);
+ if (locationId != null) await LocationAsync(actor, locationId);
+ var result = new InventoryValuation { AsOfUtc = Now };
+ var items = (await AllAsync(actor.DepartmentId)).ToDictionary(i => i.Id);
+ async Task Add(string itemId, string holderId, string lotId, string assetId, decimal quantity)
+ {
+ if (holderId == null || locationId != null && locationId != holderId || !items.TryGetValue(itemId, out var stored)) return;
+ try { await LocationAsync(actor, holderId); }
+ catch (InventoryException e) when (e.StatusCode is 403 or 404 || e.Code == "LocationUnavailable") { return; }
+ var item = await RevealAsync(actor, JsonConvert.DeserializeObject(JsonConvert.SerializeObject(stored)));
+ var details = Decode(item);
+ // Serialized equipment is valued once, at its own acquisition cost when known.
+ var cost = assetId == null ? await CurrentUnitCostAsync(actor, item, lotId, null)
+ : Decode(await GetAsync(actor, assetId)).AcquisitionCost ?? await CurrentUnitCostAsync(actor, item, lotId, assetId);
+ result.Lines.Add(new InventoryValuationLine { ItemId = itemId, AssetId = assetId, LotId = lotId, LocationId = holderId, ItemName = details.Name,
+ CurrencyCode = details.CurrencyCode, Quantity = quantity, UnitCost = cost, Value = cost.HasValue ? Money(cost.Value * quantity) : null });
+ if (result.Lines.Count > 5000) throw new InventoryException(409, "InventoryTooLarge");
+ }
+ foreach (var stock in await AllAsync(actor.DepartmentId)) if (stock.Quantity != 0) await Add(stock.ItemId, stock.LocationId, stock.LotId, null, stock.Quantity);
+ foreach (var asset in await AllAsync(actor.DepartmentId)) if (!asset.IsDeleted && asset.Status is 0 or 1 or 2 or 3) await Add(asset.ItemId, asset.CurrentLocationId, asset.LotId, asset.Id, 1);
+ result.HasNegativeStock = result.Lines.Any(l => l.Quantity < 0);
+ result.Totals = result.Lines.GroupBy(l => l.CurrencyCode).Select(g => new InventoryValuationTotal { CurrencyCode = g.Key,
+ KnownValue = g.Sum(l => l.Value ?? 0), UncostedRows = g.Count(l => !l.Value.HasValue) }).OrderBy(t => t.CurrencyCode).ToList();
+ await _auth.RequireAsync(actor, false, PermissionTypes.AdjustInventory); return result;
+ }
+ }
+}
diff --git a/Core/Resgrid.Services/InventoryCounts.cs b/Core/Resgrid.Services/InventoryCounts.cs
new file mode 100644
index 00000000..b4928a7c
--- /dev/null
+++ b/Core/Resgrid.Services/InventoryCounts.cs
@@ -0,0 +1,149 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Newtonsoft.Json;
+using Resgrid.Model;
+using Resgrid.Model.Inventories;
+
+namespace Resgrid.Services
+{
+ public sealed partial class InventoryModernizationService
+ {
+ private async Task RequireCountAccessAsync(InventoryActor actor, InventoryCount count, bool historical = true)
+ {
+ if (count == null || count.DepartmentId != actor.DepartmentId || count.IsDeleted) throw new InventoryException(404, "Unavailable");
+ if (count.LocationId == null) await _auth.RequireAsync(actor, false, PermissionTypes.AdjustInventory);
+ else await LocationAsync(actor, count.LocationId, false, PermissionTypes.AdjustInventory, historical: historical);
+ foreach (var line in await _store.RelatedAsync(actor.DepartmentId, "CountId", count.Id))
+ await LocationAsync(actor, line.LocationId, false, PermissionTypes.AdjustInventory, historical: historical);
+ }
+ // Conservative department fence also catches new positions, catalogue edits and rebuilds.
+ // Counts and observations themselves are excluded so independent counts do not invalidate each other.
+ private async Task CountFingerprintAsync(int departmentId) => Fingerprint(new {
+ Items = (await AllAsync(departmentId)).OrderBy(x => x.Id).Select(x => new { x.Id, x.Revision }),
+ Stocks = (await AllAsync(departmentId)).OrderBy(x => x.Id).Select(x => new { x.Id, x.Revision, x.Quantity }),
+ Assets = (await AllAsync(departmentId)).OrderBy(x => x.Id).Select(x => new { x.Id, x.Revision }),
+ Lots = (await AllAsync(departmentId)).OrderBy(x => x.Id).Select(x => new { x.Id, x.Revision }),
+ Locations = (await AllAsync(departmentId)).OrderBy(x => x.Id).Select(x => new { x.Id, x.Revision }),
+ Ledger = await _store.LastEntryAsync(departmentId)
+ });
+ public Task StartCountAsync(InventoryActor actor, InventoryCountInput input) => TransactionAsync(actor, async events =>
+ {
+ if (input == null) throw new InventoryException(400, "InvalidCount"); Id(input.Id); Text(input.Name);
+ if (input.Note?.Length > 16000) throw new InventoryException(400, "InvalidText");
+ var count = New(actor); count.Id = input.Id; count.LocationId = input.LocationId;
+ await RequireCountAccessAsync(actor, count, false);
+ if (await _store.GetAsync(actor.DepartmentId, count.Id) != null) throw new InventoryException(409, "CountAlreadyExists");
+ count.SnapshotOn = Now; count.SnapshotFingerprint = await CountFingerprintAsync(actor.DepartmentId);
+ count.Content = JsonConvert.SerializeObject(new InventoryCountContent { Name = input.Name.Trim(), Note = input.Note });
+ var lines = new List();
+ async Task Add(string itemId, string locationId, string lotId, string assetId, decimal quantity)
+ {
+ if (locationId == null || input.LocationId != null && locationId != input.LocationId) return;
+ if (!await LiveAlertLocationAsync(actor.DepartmentId, locationId)) return;
+ await LocationAsync(actor, locationId, false, PermissionTypes.AdjustInventory);
+ var item = await GetAsync(actor, itemId);
+ if (item.IsDeleted || !item.IsActive) return;
+ var details = Decode(item);
+ var row = New(actor); row.CountId = count.Id; row.ItemId = itemId; row.LocationId = locationId; row.LotId = lotId; row.AssetId = assetId; row.ExpectedQuantity = quantity;
+ row.Content = JsonConvert.SerializeObject(new InventoryCountItemContent { ItemName = details.Name, UnitOfMeasure = details.UnitOfMeasure, CurrencyCode = details.CurrencyCode,
+ UnitCost = await CurrentUnitCostAsync(actor, item, lotId, assetId),
+ SerialNumber = assetId == null ? null : Decode(await GetAsync(actor, assetId)).SerialNumber,
+ LotNumber = lotId == null ? null : Decode(await GetAsync(actor, lotId)).LotNumber });
+ lines.Add(row); if (lines.Count > 100) throw new InventoryException(409, "CountScopeTooLarge");
+ }
+ foreach (var stock in await AllAsync(actor.DepartmentId)) if (!stock.IsDeleted) await Add(stock.ItemId, stock.LocationId, stock.LotId, null, stock.Quantity);
+ foreach (var asset in await AllAsync(actor.DepartmentId)) if (!asset.IsDeleted && asset.Status is 0 or 1 or 2 or 3) await Add(asset.ItemId, asset.CurrentLocationId, asset.LotId, asset.Id, 1);
+ if (input.LocationId != null) foreach (var item in await AllAsync(actor.DepartmentId))
+ if (!item.IsDeleted && item.IsActive && item.TrackingMode == 0 && !item.RequiresLotTracking && !lines.Any(l => l.ItemId == item.Id)) await Add(item.Id, input.LocationId, null, null, 0);
+ if (lines.Count == 0) throw new InventoryException(409, "CountEmpty");
+ await SaveAsync(actor, count); foreach (var line in lines) await SaveAsync(actor, line);
+ await AuditAsync(actor, count, "InventoryCountStarted"); return await GetCountAsync(actor, count.Id);
+ });
+ public async Task GetCountAsync(InventoryActor actor, string id)
+ {
+ var count = await GetAsync(actor, id);
+ var result = new InventoryCountDetail { Count = count };
+ foreach (var line in await _store.RelatedAsync(actor.DepartmentId, "CountId", id)) result.Lines.Add(await RevealAsync(actor, line));
+ return result;
+ }
+ public Task SaveCountAsync(InventoryActor actor, InventoryCountUpdate input) => TransactionAsync(actor, async events =>
+ {
+ if (input?.Lines == null || input.Lines.Count is < 1 or > 100 || input.Lines.Any(l => l == null) || input.Lines.Select(l => l.Id).Distinct().Count() != input.Lines.Count) throw new InventoryException(400, "InvalidCount");
+ var detail = await GetCountAsync(actor, input.CountId);
+ if (detail.Count.Status != 0 || detail.Count.Revision != input.Revision) throw new InventoryException(409, "CountStateConflict");
+ foreach (var observation in input.Lines)
+ {
+ Quantity(observation.Quantity, true);
+ var line = detail.Lines.SingleOrDefault(l => l.Id == observation.Id) ?? throw new InventoryException(404, "Unavailable");
+ if (line.AssetId != null && observation.Quantity is not (0 or 1)) throw new InventoryException(400, "SerializedQuantity");
+ line.CountedQuantity = observation.Quantity; await SaveAsync(actor, line, false);
+ }
+ await SaveAsync(actor, detail.Count, false); await AuditAsync(actor, detail.Count, "InventoryCountSaved"); return await GetCountAsync(actor, detail.Count.Id);
+ });
+ public Task CancelCountAsync(InventoryActor actor, string id, int revision) => TransactionAsync(actor, async events =>
+ {
+ var count = await GetAsync(actor, id);
+ if (count.Revision != revision || count.Status is not (0 or 1)) throw new InventoryException(409, "CountStateConflict");
+ count.Status = 3; await SaveAsync(actor, count, false); await AuditAsync(actor, count, "InventoryCountCancelled"); return true;
+ });
+ private async Task CountCommandAsync(InventoryActor actor, InventoryCountDetail detail, InventoryCountComplete input, bool witnessed)
+ {
+ if (detail.Count.Status != (witnessed ? 1 : 0) || detail.Count.Revision != input.Revision || detail.Lines.Any(l => !l.CountedQuantity.HasValue)) throw new InventoryException(409, "CountStateConflict");
+ if (detail.Count.SnapshotFingerprint != await CountFingerprintAsync(actor.DepartmentId)) throw new InventoryException(409, "CountSnapshotChanged");
+ var command = new InventoryCommand { RequestId = input.RequestId };
+ foreach (var row in detail.Lines.Where(l => l.CountedQuantity != l.ExpectedQuantity))
+ {
+ var variance = row.CountedQuantity.Value - row.ExpectedQuantity;
+ if (row.AssetId != null && (variance != -1 || (await _store.RelatedAsync