Commit 1b0f6d8e4da4c60f63a8bfb6aef19d6794d33419
1 parent
f7ec1479
feat(production): shop-floor dashboard snapshot endpoint
Adds GET /api/v1/production/work-orders/shop-floor — a pure read
that returns every IN_PROGRESS work order with its current
operation and planned/actual time totals. Designed to feed a
future shop-floor dashboard (web SPA, mobile, or an external
reporting tool) without any follow-up round trips.
**Service method.** `WorkOrderService.shopFloorSnapshot()` is a
@Transactional(readOnly = true) query that:
1. Pulls every IN_PROGRESS work order via the existing
`WorkOrderJpaRepository.findByStatus`.
2. Sorts by WO code ascending so a dashboard poll gets a stable
row order.
3. For each WO picks the "current operation" = first op in
IN_PROGRESS status, or, if none, first PENDING op. This
captures both live states: "operator is running step N right
now" and "operator just finished step N and hasn't picked up
step N+1 yet".
4. Computes `totalStandardMinutes` (sum across every op) +
`totalActualMinutes` (sum of completed ops' `actualMinutes`
only, treating null as zero).
5. Counts completed vs total operations for a "step 2 of 5"
badge.
6. Returns a list of `ShopFloorEntry` DTOs — flat structure, one
row per WO, nullable `current*` fields when a WO has no
routing at all (v2-compat path).
**HTTP surface.**
- `GET /api/v1/production/work-orders/shop-floor`
- New permission `production.shop-floor.read`
- Response is `List<ShopFloorEntryResponse>` — flat so a SPA can
render a table without joining across nested JSON. Fields are
1:1 with the service-side `ShopFloorEntry`.
**Design choices.**
- Mounted under `/work-orders/shop-floor` rather than a top-level
`/production/shop-floor` so every production read stays under
the same permission/audit/OpenAPI root.
- Read-only, zero events published, zero ledger writes. Pure
projection over existing state.
- Returns empty list when no WO is in-progress — the dashboard
renders "no jobs running" without a special case.
- Sorted by code so polling is deterministic. A future chunk
might add sort-by-work-center if a dashboard needs a
by-station view.
**Why not a top-level "shop-floor" PBC.** A shop-floor dashboard
doesn't own any state — every field it displays is projected from
pbc-production. A new PBC would duplicate the data model and
create a reaction loop on work order events. Keeping the read in
pbc-production matches the CLAUDE.md guardrail "grow the PBC when
real consumers appear, not on speculation".
**Nullable `current*` fields.** A WO with an empty operations list
(the v2-compat path — auto-spawned from SalesOrderConfirmedSubscriber
before v3 routings) has all four `current*` fields set to null.
The dashboard UI renders "no routing" or similar without any
downstream round trip.
**Tests (5 new).** empty snapshot when no IN_PROGRESS WOs; one
entry per IN_PROGRESS WO with stable sort; current-op picks
IN_PROGRESS over PENDING; current-op picks first PENDING when no
op is IN_PROGRESS (between-operations state); v2-compat WO with
no operations shows null current-op fields and zero time sums.
**Smoke-tested end-to-end against real Postgres:**
1. Empty shop-floor initially (no IN_PROGRESS WOs)
2. Started plugin-printing-shop-quote-to-work-order BPMN with
quoteCode=Q-DASH-1, quantity=500
3. Started the resulting WO — shop-floor showed
currentOperationLineNo=1 (CUT @ PRINTING-CUT-01) status=PENDING,
0/4 completed, totalStandardMinutes=75, totalActualMinutes=0
4. Started op 1 — currentOperationStatus flipped to IN_PROGRESS
5. Completed op 1 with actualMinutes=17 — current op rolled
forward to line 2 (PRINT @ PRINTING-PRESS-A) status=PENDING,
operationsCompleted=1/4, totalActualMinutes=17
24 modules, 355 unit tests (+5), all green.
Showing
4 changed files
with
264 additions
and
0 deletions
pbc/pbc-production/src/main/kotlin/org/vibeerp/pbc/production/application/WorkOrderService.kt
| @@ -93,6 +93,70 @@ class WorkOrderService( | @@ -93,6 +93,70 @@ class WorkOrderService( | ||
| 93 | orders.findBySourceSalesOrderCode(salesOrderCode) | 93 | orders.findBySourceSalesOrderCode(salesOrderCode) |
| 94 | 94 | ||
| 95 | /** | 95 | /** |
| 96 | + * Build a shop-floor snapshot: every IN_PROGRESS work order | ||
| 97 | + * paired with its current operation and planned/actual time | ||
| 98 | + * totals. This is a pure read — no state changes, no events | ||
| 99 | + * published — designed to feed a future shop-floor dashboard | ||
| 100 | + * (web SPA, mobile app, or an external reporting tool via | ||
| 101 | + * the REST API). | ||
| 102 | + * | ||
| 103 | + * **Current-operation rule.** For a WO with a non-empty routing | ||
| 104 | + * the "current operation" is the first IN_PROGRESS op in | ||
| 105 | + * `lineNo` order, or, if none is IN_PROGRESS, the first PENDING | ||
| 106 | + * op. This handles the two live states a WO can be in on the | ||
| 107 | + * shop floor: "operator is running step N right now" and | ||
| 108 | + * "operator just finished step N and hasn't picked up step N+1 | ||
| 109 | + * yet". A WO with no operations at all (the v2-compat path) | ||
| 110 | + * returns `currentOperationLineNo = null` — the dashboard UI | ||
| 111 | + * shows it as "no routing". | ||
| 112 | + * | ||
| 113 | + * **Time sums**: | ||
| 114 | + * - `totalStandardMinutes` — sum of `standardMinutes` across | ||
| 115 | + * every operation (the planned runtime of the whole WO) | ||
| 116 | + * - `totalActualMinutes` — sum of `actualMinutes` across the | ||
| 117 | + * COMPLETED operations only (null on pending/in-progress | ||
| 118 | + * ops is treated as zero) | ||
| 119 | + * - `operationsCompleted` / `operationsTotal` — for an | ||
| 120 | + * at-a-glance "step 2 of 5" display | ||
| 121 | + * | ||
| 122 | + * Sorted deterministically by the work order's business code | ||
| 123 | + * so a dashboard poll returns a stable order. | ||
| 124 | + */ | ||
| 125 | + @Transactional(readOnly = true) | ||
| 126 | + fun shopFloorSnapshot(): List<ShopFloorEntry> { | ||
| 127 | + val inProgressOrders = orders.findByStatus(WorkOrderStatus.IN_PROGRESS) | ||
| 128 | + .sortedBy { it.code } | ||
| 129 | + | ||
| 130 | + return inProgressOrders.map { order -> | ||
| 131 | + val ops = order.operations | ||
| 132 | + val current = ops.firstOrNull { it.status == WorkOrderOperationStatus.IN_PROGRESS } | ||
| 133 | + ?: ops.firstOrNull { it.status == WorkOrderOperationStatus.PENDING } | ||
| 134 | + | ||
| 135 | + val totalStd = ops.fold(BigDecimal.ZERO) { acc, op -> acc + op.standardMinutes } | ||
| 136 | + val totalAct = ops.fold(BigDecimal.ZERO) { acc, op -> | ||
| 137 | + acc + (op.actualMinutes ?: BigDecimal.ZERO) | ||
| 138 | + } | ||
| 139 | + val completedCount = ops.count { it.status == WorkOrderOperationStatus.COMPLETED } | ||
| 140 | + | ||
| 141 | + ShopFloorEntry( | ||
| 142 | + workOrderId = order.id, | ||
| 143 | + workOrderCode = order.code, | ||
| 144 | + outputItemCode = order.outputItemCode, | ||
| 145 | + outputQuantity = order.outputQuantity, | ||
| 146 | + sourceSalesOrderCode = order.sourceSalesOrderCode, | ||
| 147 | + currentOperationLineNo = current?.lineNo, | ||
| 148 | + currentOperationCode = current?.operationCode, | ||
| 149 | + currentWorkCenter = current?.workCenter, | ||
| 150 | + currentOperationStatus = current?.status, | ||
| 151 | + operationsCompleted = completedCount, | ||
| 152 | + operationsTotal = ops.size, | ||
| 153 | + totalStandardMinutes = totalStd, | ||
| 154 | + totalActualMinutes = totalAct, | ||
| 155 | + ) | ||
| 156 | + } | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + /** | ||
| 96 | * Convenience passthrough for response mappers — delegates to | 160 | * Convenience passthrough for response mappers — delegates to |
| 97 | * [ExtJsonValidator.parseExt]. Returns an empty map on an empty | 161 | * [ExtJsonValidator.parseExt]. Returns an empty map on an empty |
| 98 | * or unparseable column so response rendering never 500s. | 162 | * or unparseable column so response rendering never 500s. |
| @@ -605,3 +669,41 @@ data class WorkOrderOperationCommand( | @@ -605,3 +669,41 @@ data class WorkOrderOperationCommand( | ||
| 605 | val workCenter: String, | 669 | val workCenter: String, |
| 606 | val standardMinutes: BigDecimal, | 670 | val standardMinutes: BigDecimal, |
| 607 | ) | 671 | ) |
| 672 | + | ||
| 673 | +/** | ||
| 674 | + * One row in the shop-floor dashboard snapshot returned by | ||
| 675 | + * [WorkOrderService.shopFloorSnapshot]. Carries enough info for a | ||
| 676 | + * dashboard UI to render "WO X is running step N of M at work | ||
| 677 | + * center Y" without any follow-up round trips. | ||
| 678 | + * | ||
| 679 | + * **Nullable current-op fields.** A WO with no operations at all | ||
| 680 | + * (the v2-compat path — auto-spawned by SalesOrderConfirmedSubscriber | ||
| 681 | + * before the v3 routings era) has all four `current*` fields set | ||
| 682 | + * to `null`. The dashboard should render these as "no routing" or | ||
| 683 | + * similar. | ||
| 684 | + * | ||
| 685 | + * **`operationsCompleted` vs `operationsTotal`** gives a dashboard | ||
| 686 | + * the raw numbers for a "step 2 of 5" badge without requiring it | ||
| 687 | + * to scan the full operations list. | ||
| 688 | + * | ||
| 689 | + * **`totalActualMinutes`** counts only COMPLETED operations — | ||
| 690 | + * PENDING or IN_PROGRESS ops contribute zero because nobody has | ||
| 691 | + * entered the actual runtime yet. Compared against | ||
| 692 | + * `totalStandardMinutes` this gives an early-warning variance | ||
| 693 | + * signal ("we're 10 minutes into the planned 30-minute run"). | ||
| 694 | + */ | ||
| 695 | +data class ShopFloorEntry( | ||
| 696 | + val workOrderId: UUID, | ||
| 697 | + val workOrderCode: String, | ||
| 698 | + val outputItemCode: String, | ||
| 699 | + val outputQuantity: BigDecimal, | ||
| 700 | + val sourceSalesOrderCode: String?, | ||
| 701 | + val currentOperationLineNo: Int?, | ||
| 702 | + val currentOperationCode: String?, | ||
| 703 | + val currentWorkCenter: String?, | ||
| 704 | + val currentOperationStatus: WorkOrderOperationStatus?, | ||
| 705 | + val operationsCompleted: Int, | ||
| 706 | + val operationsTotal: Int, | ||
| 707 | + val totalStandardMinutes: BigDecimal, | ||
| 708 | + val totalActualMinutes: BigDecimal, | ||
| 709 | +) |
pbc/pbc-production/src/main/kotlin/org/vibeerp/pbc/production/http/WorkOrderController.kt
| @@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping | @@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RequestMapping | ||
| 16 | import org.springframework.web.bind.annotation.ResponseStatus | 16 | import org.springframework.web.bind.annotation.ResponseStatus |
| 17 | import org.springframework.web.bind.annotation.RestController | 17 | import org.springframework.web.bind.annotation.RestController |
| 18 | import org.vibeerp.pbc.production.application.CreateWorkOrderCommand | 18 | import org.vibeerp.pbc.production.application.CreateWorkOrderCommand |
| 19 | +import org.vibeerp.pbc.production.application.ShopFloorEntry | ||
| 19 | import org.vibeerp.pbc.production.application.WorkOrderInputCommand | 20 | import org.vibeerp.pbc.production.application.WorkOrderInputCommand |
| 20 | import org.vibeerp.pbc.production.application.WorkOrderOperationCommand | 21 | import org.vibeerp.pbc.production.application.WorkOrderOperationCommand |
| 21 | import org.vibeerp.pbc.production.application.WorkOrderService | 22 | import org.vibeerp.pbc.production.application.WorkOrderService |
| @@ -48,6 +49,29 @@ class WorkOrderController( | @@ -48,6 +49,29 @@ class WorkOrderController( | ||
| 48 | fun list(): List<WorkOrderResponse> = | 49 | fun list(): List<WorkOrderResponse> = |
| 49 | workOrderService.list().map { it.toResponse(workOrderService) } | 50 | workOrderService.list().map { it.toResponse(workOrderService) } |
| 50 | 51 | ||
| 52 | + /** | ||
| 53 | + * Shop-floor dashboard snapshot — every IN_PROGRESS work order | ||
| 54 | + * with its current operation and planned/actual time totals. | ||
| 55 | + * A pure read; no state changes. Designed to feed a dashboard | ||
| 56 | + * poll ("which WOs are running right now and how far along are | ||
| 57 | + * they?"). A WO with no routing at all returns `null` for the | ||
| 58 | + * four `current*` fields so the UI can render it as "no routing" | ||
| 59 | + * without any downstream round trips. | ||
| 60 | + * | ||
| 61 | + * **Sort order**: by work order code ascending, so a poll | ||
| 62 | + * returns a stable order. | ||
| 63 | + * | ||
| 64 | + * Mounted as a sub-path of `/work-orders` rather than a | ||
| 65 | + * top-level `/shop-floor` because this is the existing PBC's | ||
| 66 | + * own read endpoint and keeping every production read under | ||
| 67 | + * the `/production/work-orders` root makes the | ||
| 68 | + * permission/audit/OpenAPI story trivially consistent. | ||
| 69 | + */ | ||
| 70 | + @GetMapping("/shop-floor") | ||
| 71 | + @RequirePermission("production.shop-floor.read") | ||
| 72 | + fun shopFloor(): List<ShopFloorEntryResponse> = | ||
| 73 | + workOrderService.shopFloorSnapshot().map { it.toResponse() } | ||
| 74 | + | ||
| 51 | @GetMapping("/{id}") | 75 | @GetMapping("/{id}") |
| 52 | @RequirePermission("production.work-order.read") | 76 | @RequirePermission("production.work-order.read") |
| 53 | fun get(@PathVariable id: UUID): ResponseEntity<WorkOrderResponse> { | 77 | fun get(@PathVariable id: UUID): ResponseEntity<WorkOrderResponse> { |
| @@ -327,3 +351,41 @@ private fun WorkOrderOperation.toResponse(): WorkOrderOperationResponse = | @@ -327,3 +351,41 @@ private fun WorkOrderOperation.toResponse(): WorkOrderOperationResponse = | ||
| 327 | startedAt = startedAt, | 351 | startedAt = startedAt, |
| 328 | completedAt = completedAt, | 352 | completedAt = completedAt, |
| 329 | ) | 353 | ) |
| 354 | + | ||
| 355 | +/** | ||
| 356 | + * One row in the shop-floor dashboard. Flat by design so a web | ||
| 357 | + * SPA can render a table without joining across nested JSON. See | ||
| 358 | + * [ShopFloorEntry] for the per-field rationale. | ||
| 359 | + */ | ||
| 360 | +data class ShopFloorEntryResponse( | ||
| 361 | + val workOrderId: UUID, | ||
| 362 | + val workOrderCode: String, | ||
| 363 | + val outputItemCode: String, | ||
| 364 | + val outputQuantity: BigDecimal, | ||
| 365 | + val sourceSalesOrderCode: String?, | ||
| 366 | + val currentOperationLineNo: Int?, | ||
| 367 | + val currentOperationCode: String?, | ||
| 368 | + val currentWorkCenter: String?, | ||
| 369 | + val currentOperationStatus: WorkOrderOperationStatus?, | ||
| 370 | + val operationsCompleted: Int, | ||
| 371 | + val operationsTotal: Int, | ||
| 372 | + val totalStandardMinutes: BigDecimal, | ||
| 373 | + val totalActualMinutes: BigDecimal, | ||
| 374 | +) | ||
| 375 | + | ||
| 376 | +private fun ShopFloorEntry.toResponse(): ShopFloorEntryResponse = | ||
| 377 | + ShopFloorEntryResponse( | ||
| 378 | + workOrderId = workOrderId, | ||
| 379 | + workOrderCode = workOrderCode, | ||
| 380 | + outputItemCode = outputItemCode, | ||
| 381 | + outputQuantity = outputQuantity, | ||
| 382 | + sourceSalesOrderCode = sourceSalesOrderCode, | ||
| 383 | + currentOperationLineNo = currentOperationLineNo, | ||
| 384 | + currentOperationCode = currentOperationCode, | ||
| 385 | + currentWorkCenter = currentWorkCenter, | ||
| 386 | + currentOperationStatus = currentOperationStatus, | ||
| 387 | + operationsCompleted = operationsCompleted, | ||
| 388 | + operationsTotal = operationsTotal, | ||
| 389 | + totalStandardMinutes = totalStandardMinutes, | ||
| 390 | + totalActualMinutes = totalActualMinutes, | ||
| 391 | + ) |
pbc/pbc-production/src/main/resources/META-INF/vibe-erp/metadata/production.yml
| @@ -26,6 +26,8 @@ permissions: | @@ -26,6 +26,8 @@ permissions: | ||
| 26 | description: Start a routing operation (PENDING → IN_PROGRESS). Parent work order must be IN_PROGRESS and the operation must come next in sequence. | 26 | description: Start a routing operation (PENDING → IN_PROGRESS). Parent work order must be IN_PROGRESS and the operation must come next in sequence. |
| 27 | - key: production.work-order.operation.complete | 27 | - key: production.work-order.operation.complete |
| 28 | description: Complete a routing operation (IN_PROGRESS → COMPLETED). Records the operator-entered actual_minutes. | 28 | description: Complete a routing operation (IN_PROGRESS → COMPLETED). Records the operator-entered actual_minutes. |
| 29 | + - key: production.shop-floor.read | ||
| 30 | + description: Read the shop-floor dashboard snapshot (in-progress work orders with their current operation and time totals). | ||
| 29 | 31 | ||
| 30 | customFields: | 32 | customFields: |
| 31 | - key: production_priority | 33 | - key: production_priority |
pbc/pbc-production/src/test/kotlin/org/vibeerp/pbc/production/application/WorkOrderServiceTest.kt
| @@ -809,4 +809,102 @@ class WorkOrderServiceTest { | @@ -809,4 +809,102 @@ class WorkOrderServiceTest { | ||
| 809 | .isInstanceOf(IllegalArgumentException::class) | 809 | .isInstanceOf(IllegalArgumentException::class) |
| 810 | .messageContains("refusing to overwrite") | 810 | .messageContains("refusing to overwrite") |
| 811 | } | 811 | } |
| 812 | + | ||
| 813 | + // ─── shop-floor snapshot ───────────────────────────────────────── | ||
| 814 | + | ||
| 815 | + @Test | ||
| 816 | + fun `shopFloorSnapshot is empty when no WO is IN_PROGRESS`() { | ||
| 817 | + every { orders.findByStatus(WorkOrderStatus.IN_PROGRESS) } returns emptyList() | ||
| 818 | + | ||
| 819 | + val snapshot = service.shopFloorSnapshot() | ||
| 820 | + | ||
| 821 | + assertThat(snapshot).hasSize(0) | ||
| 822 | + } | ||
| 823 | + | ||
| 824 | + @Test | ||
| 825 | + fun `shopFloorSnapshot returns one entry per IN_PROGRESS WO`() { | ||
| 826 | + val wo1 = inProgressOrderWithOps( | ||
| 827 | + id = UUID.randomUUID(), | ||
| 828 | + code = "WO-AAA", | ||
| 829 | + itemCode = "FG-A", | ||
| 830 | + qty = "10", | ||
| 831 | + ) | ||
| 832 | + val wo2 = inProgressOrderWithOps( | ||
| 833 | + id = UUID.randomUUID(), | ||
| 834 | + code = "WO-BBB", | ||
| 835 | + itemCode = "FG-B", | ||
| 836 | + qty = "20", | ||
| 837 | + ) | ||
| 838 | + every { orders.findByStatus(WorkOrderStatus.IN_PROGRESS) } returns listOf(wo2, wo1) | ||
| 839 | + | ||
| 840 | + val snapshot = service.shopFloorSnapshot() | ||
| 841 | + | ||
| 842 | + assertThat(snapshot).hasSize(2) | ||
| 843 | + // Sorted by code ascending regardless of repository order. | ||
| 844 | + assertThat(snapshot[0].workOrderCode).isEqualTo("WO-AAA") | ||
| 845 | + assertThat(snapshot[1].workOrderCode).isEqualTo("WO-BBB") | ||
| 846 | + } | ||
| 847 | + | ||
| 848 | + @Test | ||
| 849 | + fun `shopFloorSnapshot picks IN_PROGRESS op as current over PENDING`() { | ||
| 850 | + val wo = inProgressOrderWithOps( | ||
| 851 | + id = UUID.randomUUID(), | ||
| 852 | + code = "WO-CURR", | ||
| 853 | + ops = listOf( | ||
| 854 | + op(1, WorkOrderOperationStatus.COMPLETED).also { | ||
| 855 | + it.actualMinutes = BigDecimal("12") | ||
| 856 | + }, | ||
| 857 | + op(2, WorkOrderOperationStatus.IN_PROGRESS), | ||
| 858 | + op(3, WorkOrderOperationStatus.PENDING), | ||
| 859 | + ), | ||
| 860 | + ) | ||
| 861 | + every { orders.findByStatus(WorkOrderStatus.IN_PROGRESS) } returns listOf(wo) | ||
| 862 | + | ||
| 863 | + val entry = service.shopFloorSnapshot().single() | ||
| 864 | + | ||
| 865 | + assertThat(entry.currentOperationLineNo).isEqualTo(2) | ||
| 866 | + assertThat(entry.currentOperationStatus).isEqualTo(WorkOrderOperationStatus.IN_PROGRESS) | ||
| 867 | + assertThat(entry.operationsCompleted).isEqualTo(1) | ||
| 868 | + assertThat(entry.operationsTotal).isEqualTo(3) | ||
| 869 | + assertThat(entry.totalStandardMinutes).isEqualTo(BigDecimal("30")) | ||
| 870 | + assertThat(entry.totalActualMinutes).isEqualTo(BigDecimal("12")) | ||
| 871 | + } | ||
| 872 | + | ||
| 873 | + @Test | ||
| 874 | + fun `shopFloorSnapshot picks first PENDING op when none is IN_PROGRESS`() { | ||
| 875 | + val wo = inProgressOrderWithOps( | ||
| 876 | + id = UUID.randomUUID(), | ||
| 877 | + code = "WO-BETWEEN", | ||
| 878 | + ops = listOf( | ||
| 879 | + op(1, WorkOrderOperationStatus.COMPLETED).also { | ||
| 880 | + it.actualMinutes = BigDecimal("9") | ||
| 881 | + }, | ||
| 882 | + op(2, WorkOrderOperationStatus.PENDING), | ||
| 883 | + op(3, WorkOrderOperationStatus.PENDING), | ||
| 884 | + ), | ||
| 885 | + ) | ||
| 886 | + every { orders.findByStatus(WorkOrderStatus.IN_PROGRESS) } returns listOf(wo) | ||
| 887 | + | ||
| 888 | + val entry = service.shopFloorSnapshot().single() | ||
| 889 | + | ||
| 890 | + assertThat(entry.currentOperationLineNo).isEqualTo(2) | ||
| 891 | + assertThat(entry.currentOperationStatus).isEqualTo(WorkOrderOperationStatus.PENDING) | ||
| 892 | + } | ||
| 893 | + | ||
| 894 | + @Test | ||
| 895 | + fun `shopFloorSnapshot handles a v2-compat WO with no operations`() { | ||
| 896 | + val wo = inProgressOrderWithOps(id = UUID.randomUUID(), code = "WO-NO-OPS") | ||
| 897 | + every { orders.findByStatus(WorkOrderStatus.IN_PROGRESS) } returns listOf(wo) | ||
| 898 | + | ||
| 899 | + val entry = service.shopFloorSnapshot().single() | ||
| 900 | + | ||
| 901 | + assertThat(entry.currentOperationLineNo).isEqualTo(null) | ||
| 902 | + assertThat(entry.currentOperationCode).isEqualTo(null) | ||
| 903 | + assertThat(entry.currentWorkCenter).isEqualTo(null) | ||
| 904 | + assertThat(entry.currentOperationStatus).isEqualTo(null) | ||
| 905 | + assertThat(entry.operationsTotal).isEqualTo(0) | ||
| 906 | + assertThat(entry.operationsCompleted).isEqualTo(0) | ||
| 907 | + assertThat(entry.totalStandardMinutes).isEqualTo(BigDecimal.ZERO) | ||
| 908 | + assertThat(entry.totalActualMinutes).isEqualTo(BigDecimal.ZERO) | ||
| 909 | + } | ||
| 812 | } | 910 | } |