Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
c2cc3d0
feat(cmdb-import): read the archive sheet and let module and usage be…
WilcoLouwerse Oct 12, 2026
33585a1
feat(cmdb-import): archive or soft-delete applications that left the …
WilcoLouwerse Oct 12, 2026
7184a4a
feat(cmdb-import): offer the missing-records choice and an Archived q…
WilcoLouwerse Oct 12, 2026
388181a
docs(openspec): name the renamed module-version test in the cmdb-expo…
WilcoLouwerse Oct 12, 2026
07d533d
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-1-reader…
WilcoLouwerse Oct 12, 2026
d3601b3
fix(cmdb-import): do not cache a trash listing that failed to load
WilcoLouwerse Oct 12, 2026
f196420
docs(cmdb-import): point the two new @spec tags at the archived cmdb-…
WilcoLouwerse Oct 12, 2026
c4d2b33
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-2-reconc…
WilcoLouwerse Oct 12, 2026
f6ae270
fix(cmdb-import): stop telling the admin missing applications stay an…
WilcoLouwerse Oct 12, 2026
97c4e99
docs(cmdb-import): say what a split import with Keep leaves undone
WilcoLouwerse Oct 12, 2026
3671df0
fix(cmdb-import): stop counting rows as applications in the reconcili…
WilcoLouwerse Oct 12, 2026
bbf4209
fix(cmdb-import): say applications, not only rows, in the cancel texts
WilcoLouwerse Oct 12, 2026
16337d9
fix(cmdb-import): report archive-sheet APPIDs that have no cached value
WilcoLouwerse Oct 12, 2026
11d9a23
test(cmdb-import): pin every bound the archive sheet is read under
WilcoLouwerse Oct 12, 2026
8360c6a
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-1-reader…
WilcoLouwerse Oct 12, 2026
ac3305a
fix(cmdb-import): name archived, unarchived, deleted and restored in …
WilcoLouwerse Oct 12, 2026
08acba6
fix(cmdb-import): never soft-delete on an archive sheet with unreadab…
WilcoLouwerse Oct 12, 2026
f99c063
fix(cmdb-import): let the Archived tab of the Applications page ignor…
WilcoLouwerse Oct 12, 2026
b30cd7f
fix(cmdb-import): reconcile one application per import key
WilcoLouwerse Oct 12, 2026
b1d249a
test(cmdb-import): make the missing-applications choice and the delet…
WilcoLouwerse Oct 12, 2026
8a1562a
fix(cmdb-import): name the archive sheet where the section says which…
WilcoLouwerse Oct 12, 2026
a4570e7
test(cmdb-import): pin the totality promises of the reconciliation
WilcoLouwerse Oct 12, 2026
cdbec3d
docs(cmdb-import): say the archive sheet is read, and bounded, in eve…
WilcoLouwerse Oct 12, 2026
2795770
style(cmdb-import): bring the two reconciliation fixes within phpcs
WilcoLouwerse Oct 12, 2026
226b76c
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-2-reconc…
WilcoLouwerse Oct 12, 2026
aa3b664
docs(openspec): state the archive-only rule in every copy
WilcoLouwerse Oct 12, 2026
70d6f93
docs(openspec): the Archived tab ignores the facet narrowing
WilcoLouwerse Oct 12, 2026
2e89a1e
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-1-reader…
WilcoLouwerse Oct 12, 2026
9fb2dfd
docs(cmdb-import): state all three cases where an import archives and…
WilcoLouwerse Oct 12, 2026
e8772a7
docs(cmdb-import): state the full archive-only rule in the service do…
WilcoLouwerse Oct 12, 2026
2d288ac
docs(cmdb-import): correct the Modules page note on how quick filters…
WilcoLouwerse Oct 12, 2026
16de647
fix(cmdb-import): count the rows in the uncached-archive-key warning
WilcoLouwerse Oct 12, 2026
d805c5b
fix(cmdb-import): bring every usage of a leaving key to the same state
WilcoLouwerse Oct 12, 2026
9a5ffff
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-2-reconc…
WilcoLouwerse Oct 12, 2026
323d761
docs(cmdb-import): quote both forms of the uncached-APPID warning
WilcoLouwerse Oct 12, 2026
ae31ac3
docs: scope the archive-reconciliation change to the reader slice and…
WilcoLouwerse Oct 13, 2026
ddcffcc
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-1-reader…
WilcoLouwerse Oct 13, 2026
8dfa03a
feat(cmdb-import): shorten the help of the missing-applications choice
WilcoLouwerse Oct 13, 2026
dca8211
fix(cmdb-import): never soft-delete when a CMDB-sheet APPID is unread…
WilcoLouwerse Oct 13, 2026
18bd222
docs(openspec): tick the reconciliation tasks that this PR implements
WilcoLouwerse Oct 13, 2026
3d172db
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-2-reconc…
WilcoLouwerse Oct 13, 2026
27a2110
fix(cmdb-import): see a fully empty CMDB row with an uncached APPID; …
WilcoLouwerse Oct 13, 2026
c55a96d
test(cmdb-import): wait on a rendered list, not networkidle, and fail…
WilcoLouwerse Oct 13, 2026
4b70494
fix(applications): say on the Archived tab that facets and search are…
WilcoLouwerse Oct 13, 2026
3c17b47
docs(cmdb-import): tick the UI tasks and document the archive tab not…
WilcoLouwerse Oct 13, 2026
f1ca071
Merge branch 'feat/woo587-cmdb-import-archive-reconciliation-2-reconc…
WilcoLouwerse Oct 13, 2026
1719b19
fix(applications): keep the Archived-tab note in step with the list's…
WilcoLouwerse Oct 13, 2026
a1cca04
Merge remote-tracking branch 'origin/feat/woo587-cmdb-import-archive-…
WilcoLouwerse Oct 13, 2026
3fbc2f9
Merge pull request #1287 from ConductionNL/feat/woo587-cmdb-import-ar…
WilcoLouwerse Oct 15, 2026
0e4bcd4
Merge pull request #1286 from ConductionNL/feat/woo587-cmdb-import-ar…
WilcoLouwerse Oct 15, 2026
7d5aa91
refactor(cmdb-import): choose a missing application's target and reas…
WilcoLouwerse Oct 15, 2026
363c470
docs(applications): tag the quick-filter tab tracking with the archiv…
WilcoLouwerse Oct 15, 2026
ca176a5
Merge remote-tracking branch 'origin/development' into feat/woo587-cm…
WilcoLouwerse Oct 15, 2026
e0aba3d
style(test): prettier on the archive quick filter spec
WilcoLouwerse Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
sheets becomes or updates a module, its vendor, a usage for the municipality
and a contact person for its owner; a repeat import matches on APPID, so it
updates instead of duplicating. See `docs/features/cmdb-import.md`.
- CMDB import, repeat imports: applications that left the CMDB sheets are
archived when the sheet "Gearchiveerde Applicaties" lists them and moved to
OpenRegister's trash when no sheet does; one that returns is unarchived or
restored, never duplicated. The new option `missingRecords` (`archive`, the
default, or `keep`) is a choice in the CMDB import section, and the report
counts archived, unarchived, deleted and restored applications. The module
(0.3.9) and usage (1.5.7) schemas opt into OpenRegister's archive state, and
the Applications and Applications in use pages get an "Archived" quick
filter. (cmdb-import-archive-reconciliation)

### Changed
- EOL feed: the register and schema slugs the feature reads from are corrected.
Expand Down
2 changes: 1 addition & 1 deletion docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ The ArchiMate integration maps GEMMA Softwarecatalogus objects to ArchiMate appl

## CMDB Import

Import a TOPdesk CMDB export (`.xlsx`) for one municipality from the **CMDB import** section of the admin settings. Every application row of the two CMDB sheets ("Onbeh Applicaties CMDB" and "Beheerde Applicaties CMDB") becomes or updates a module, its vendor as a Supplier organisation, a usage that links it to the municipality and records whether maintenance is arranged, and a contact person for its owner (identity in Nextcloud Contacts, never public). A repeat import matches on APPID per municipality, so it updates instead of duplicating, and leaves applications missing from the newer export as they are. The column mapping is declarative JSON executed by OpenRegister's mapping engine.
Import a TOPdesk CMDB export (`.xlsx`) for one municipality from the **CMDB import** section of the admin settings. Every application row of the two CMDB sheets ("Onbeh Applicaties CMDB" and "Beheerde Applicaties CMDB") becomes or updates a module, its vendor as a Supplier organisation, a usage that links it to the municipality and records whether maintenance is arranged, and a contact person for its owner (identity in Nextcloud Contacts, never public). A repeat import matches on APPID per municipality, so it updates instead of duplicating. By default it archives the applications that moved to the sheet "Gearchiveerde Applicaties" and moves those on no sheet any more to OpenRegister's trash, and brings either back when it returns; the admin can choose to keep them instead. The Applications and Applications in use pages list the archive under the quick filter "Archived". The column mapping is declarative JSON executed by OpenRegister's mapping engine.

See [CMDB import](cmdb-import.md) for the steps, the expected file structure, the error codes and how to adjust the mapping.

Expand Down
119 changes: 98 additions & 21 deletions docs/features/cmdb-import.md

Large diffs are not rendered by default.

43 changes: 33 additions & 10 deletions l10n/en.js

Large diffs are not rendered by default.

43 changes: 33 additions & 10 deletions l10n/en.json

Large diffs are not rendered by default.

43 changes: 33 additions & 10 deletions l10n/nl.js

Large diffs are not rendered by default.

43 changes: 33 additions & 10 deletions l10n/nl.json

Large diffs are not rendered by default.

22 changes: 17 additions & 5 deletions lib/Controller/CmdbImportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,16 @@ public function __construct(
* Import a TOPdesk CMDB export for one municipality.
*
* Multipart fields: `cmdbFile`, `municipalityUuid` or `municipalityName`,
* `updateExisting` (default true), `publish` (default true), `missingRecords` (only `keep`) and
* `updateExisting` (default true), `publish` (default true), `missingRecords` (`keep` or `archive`,
* default `archive`) and
* `operationId` (pattern `cmdb-` plus 8 to 64 letters, digits or hyphens).
*
* @return JSONResponse The report (200), or an error envelope with the contract code.
*
* @auth admin-only the import writes with RBAC and multitenancy off, across tenants, so only a full Nextcloud admin may run it.
*
* @spec openspec/changes/cmdb-export-import/tasks.md#task-8
* @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-records-missing-from-a-newer-export-shall-be-archived-by-default-or-kept-on-request-req-cmdb-012
*/
public function import(): JSONResponse {
try {
Expand Down Expand Up @@ -247,15 +249,21 @@ protected function iniBytes(string $name): ?int {
* @return array{path: string, options: array<string, mixed>}|JSONResponse The import input, or the first error.
*
* @spec openspec/changes/cmdb-export-import/tasks.md#task-8
* @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-records-missing-from-a-newer-export-shall-be-archived-by-default-or-kept-on-request-req-cmdb-012
*/
private function readOptions(string $path, string $fileName): array|JSONResponse {
$missingRecords = $this->stringParam(name: 'missingRecords', default: 'keep');
$missingRecords = $this->stringParam(name: 'missingRecords', default: CmdbExportImportService::DEFAULT_MISSING_RECORDS);
if ($missingRecords === null) {
return $this->invalidField(field: 'missingRecords');
}

if ($this->importService->supportsMissingRecords(mode: $missingRecords) === false) {
return $this->error(code: 'MISSING_RECORDS_UNSUPPORTED', status: Http::STATUS_UNPROCESSABLE_ENTITY, details: ['accepted' => ['keep']]);
$accepted = $this->importService->missingRecordsModes();
if (in_array($missingRecords, $accepted, true) === false) {
return $this->error(
code: 'MISSING_RECORDS_UNSUPPORTED',
status: Http::STATUS_UNPROCESSABLE_ENTITY,
details: ['accepted' => $accepted]
);
}

// An unrecognised value is refused rather than read as true: true is the mode that overwrites.
Expand Down Expand Up @@ -293,6 +301,7 @@ private function readOptions(string $path, string $fileName): array|JSONResponse
'municipalityName' => $municipalityName,
'updateExisting' => $updateExisting,
'publish' => $publish,
'missingRecords' => $missingRecords,
'operationId' => $this->request->getParam('operationId'),
'fileName' => basename(str_replace('\\', '/', $fileName)),
],
Expand Down Expand Up @@ -394,7 +403,7 @@ private function message(string $code, array $details): string {
'NO_FILE_UPLOADED' => $this->l10n->t('No file was uploaded.'),
'NOT_XLSX' => $this->l10n->t('The file is not an Excel workbook (.xlsx).'),
'FILE_TOO_LARGE' => $this->l10n->t('The file is larger than the maximum of %s MB.', [$megabytes]),
'MISSING_RECORDS_UNSUPPORTED' => $this->l10n->t('Only keeping records that are missing from the export is supported.'),
'MISSING_RECORDS_UNSUPPORTED' => $this->l10n->t('Applications missing from the export can only be archived or kept.'),
'FIELD_INVALID' => $this->fieldMessage(field: (string)($details['field'] ?? ''), accepted: $accepted),
'MUNICIPALITY_REQUIRED' => $this->l10n->t('Choose a municipality or enter the name of a new one.'),
'MUNICIPALITY_INVALID' => $this->l10n->t('The chosen organisation is not a municipality.'),
Expand All @@ -410,6 +419,9 @@ private function message(string $code, array $details): string {
'SCHEMA_OUTDATED' => $this->l10n->t('The stackiq register is out of date; import its configuration again.'),
'IMPORT_IN_PROGRESS' => $this->l10n->t('Another CMDB import is running; try again when it has finished.'),
'MUNICIPALITY_AMBIGUOUS' => $this->l10n->t('Several municipalities have this name; choose one from the list.'),
'ARCHIVE_UNAVAILABLE' => $this->l10n->t(
'Applications missing from the export cannot be archived: OpenRegister is missing or too old. Choose to keep them, or update OpenRegister.'
),
default => $this->l10n->t('The import failed. The details are in the Nextcloud log.'),
};
}//end message()
Expand Down
2 changes: 2 additions & 0 deletions lib/Exception/CmdbImportException.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ class CmdbImportException extends RuntimeException {
public const SCHEMA_OUTDATED = 'SCHEMA_OUTDATED';
public const IMPORT_IN_PROGRESS = 'IMPORT_IN_PROGRESS';
public const MUNICIPALITY_AMBIGUOUS = 'MUNICIPALITY_AMBIGUOUS';
public const ARCHIVE_UNAVAILABLE = 'ARCHIVE_UNAVAILABLE';

/**
* HTTP status per error code.
Expand All @@ -70,6 +71,7 @@ class CmdbImportException extends RuntimeException {
self::SCHEMA_OUTDATED => 503,
self::IMPORT_IN_PROGRESS => 409,
self::MUNICIPALITY_AMBIGUOUS => 422,
self::ARCHIVE_UNAVAILABLE => 503,
];

/**
Expand Down
55 changes: 50 additions & 5 deletions lib/Service/Cmdb/CmdbImportProfile.php
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,42 @@ public function sheetNames(): array {
return array_column($this->sheets(), 'name');
}//end sheetNames()

/**
* The name of the archive sheet, or null when the profile names none.
*
* The archive sheet lists the applications the municipality archived; the
* import reads only its key column (`archiveSheet.name` in the profile).
*
* @return string|null
*
* @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-an-application-missing-from-the-cmdb-sheets-shall-be-archived-when-the-archive-sheet-lists-it-and-soft-deleted-when-no-sheet-does-req-cmdb-015
*/
public function archiveSheetName(): ?string {
$name = $this->profile()['archiveSheet']['name'] ?? null;
if (is_string($name) === false || $name === '' || in_array($name, $this->sheetNames(), true) === true) {
return null;
}

return $name;
}//end archiveSheetName()

/**
* Every sheet the reader may parse: the source sheets, then the archive sheet.
*
* @return array<int, string>
*
* @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-an-application-missing-from-the-cmdb-sheets-shall-be-archived-when-the-archive-sheet-lists-it-and-soft-deleted-when-no-sheet-does-req-cmdb-015
*/
public function readSheetNames(): array {
$names = $this->sheetNames();
$archive = $this->archiveSheetName();
if ($archive !== null) {
$names[] = $archive;
}

return $names;
}//end readSheetNames()

/**
* The rank of a sheet when an APPID is on more than one: lower wins.
*
Expand Down Expand Up @@ -574,19 +610,28 @@ public function neverWrittenOnUpdate(string $target): array {
}//end neverWrittenOnUpdate()

/**
* The accepted values of the missingRecords option.
* The accepted values of the missingRecords option, readable without validating the packs.
*
* The controller checks the option before the import runs, so, like
* maxFileBytes(), this must not depend on OpenRegister being available.
*
* @return array<int, string>
*
* @spec openspec/changes/cmdb-export-import/tasks.md#task-8
* @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-records-missing-from-a-newer-export-shall-be-archived-by-default-or-kept-on-request-req-cmdb-012
*/
public function missingRecordsModes(): array {
$modes = $this->stringList(key: 'missingRecords');
if ($modes === []) {
try {
$profile = $this->profile ?? $this->decodeFile(fileName: $this->profileFile);
} catch (CmdbImportException $e) {
return ['keep'];
}

$modes = $profile['missingRecords'] ?? [];
if (is_array($modes) === false || $modes === []) {
return ['keep'];
}

return $modes;
return array_values(array_map('strval', $modes));
}//end missingRecordsModes()

/**
Expand Down
32 changes: 27 additions & 5 deletions lib/Service/Cmdb/CmdbImportReport.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ class CmdbImportReport {
public const UNCHANGED = 'unchanged';
public const SKIPPED = 'skipped';
public const FAILED = 'failed';
public const ARCHIVED = 'archived';
public const UNARCHIVED = 'unarchived';
public const DELETED = 'deleted';
public const RESTORED = 'restored';

/**
* The row entries, in processing order.
Expand Down Expand Up @@ -90,7 +94,7 @@ public function __construct(
* Add one row outcome.
*
* @param string $sheet The sheet name.
* @param int $row The 1-based sheet row number.
* @param int $row The 1-based sheet row number, or 0 for an application the reconciliation archived or deleted.
* @param string $appId The APPID ('' when missing).
* @param string $name The application name ('' when missing).
* @param string $outcome One of the outcome constants.
Expand Down Expand Up @@ -180,36 +184,54 @@ public function markCancelled(): void {
}//end markCancelled()

/**
* The number of rows processed so far.
* Whether the run stopped on a cancel.
*
* @return bool
*
* @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-an-application-missing-from-the-cmdb-sheets-shall-be-archived-when-the-archive-sheet-lists-it-and-soft-deleted-when-no-sheet-does-req-cmdb-015
*/
public function isCancelled(): bool {
return $this->cancelled;
}//end isCancelled()

/**
* The number of sheet rows processed so far; reconciliation entries (row 0) are not sheet rows.
*
* @return int
*
* @spec openspec/changes/cmdb-export-import/tasks.md#task-7
*/
public function processed(): int {
return count($this->rows);
return count(array_filter($this->rows, static fn (array $row): bool => $row['row'] > 0));
}//end processed()

/**
* The summary counts.
*
* `unpublished` counts the modules created without a publication date;
* a row whose module was created but whose usage then failed counts too.
* `processed` counts sheet rows only; every outcome, also of a
* reconciliation entry, is counted under its own key.
*
* @return array{rowsRead: int, processed: int, created: int, updated: int, unchanged: int, skipped: int, failed: int,
* warnings: int, unpublished: int}
* archived: int, unarchived: int, deleted: int, restored: int, warnings: int, unpublished: int}
*
* @spec openspec/changes/cmdb-export-import/tasks.md#task-7
* @spec openspec/changes/cmdb-import-archive-reconciliation/specs/cmdb-export-import/spec.md#requirement-an-application-missing-from-the-cmdb-sheets-shall-be-archived-when-the-archive-sheet-lists-it-and-soft-deleted-when-no-sheet-does-req-cmdb-015
*/
public function summary(): array {
$summary = [
'rowsRead' => $this->rowsRead,
'processed' => count($this->rows),
'processed' => $this->processed(),
self::CREATED => 0,
self::UPDATED => 0,
self::UNCHANGED => 0,
self::SKIPPED => 0,
self::FAILED => 0,
self::ARCHIVED => 0,
self::UNARCHIVED => 0,
self::DELETED => 0,
self::RESTORED => 0,
'warnings' => 0,
'unpublished' => $this->unpublished,
];
Expand Down
Loading
Loading