Cornerstone Report: system values, not display names
Reworked against the load spec and the 27.09. test file. The values I had guessed were the German display names, which is the first entry on the list of errors from earlier loads: Cornerstone answers "ungültiger Wert" and rejects the whole row, followed by "Alle abhängigen Felder müssen gültig sein" as a follow-on. Status Aktiv/Inaktiv -> Active/Inactive Employment Status Arbeitend/... -> Working/On Leave/Terminated User Type Mitarbeiter -> Employee Time Zone CET -> empty The time zone is the second entry on that list: only a portal time zone id is valid, an abbreviation gives "Zeitzonencode nicht eindeutig". Empty means the portal or the OU decides. Division ID is the GUID from the test file, not a name. Termination fields and Leave Reason are filled only when the employment status carries them -- a reason without a termination is an invalid state for the load, not extra information. Four fields now stay empty on purpose, because filling them would mean inventing an identifier that belongs to the target system: Location ID (locations has id/name/country and no Cornerstone id), Position ID (our S-0001 is not a Cornerstone position), Months of Service (Cornerstone derives it) and Rehired Employee (the value for "yes" is unconfirmed, and an unconfirmed value costs the whole row). Retention Rules and Organisationsstufe stay empty because the load ignores them. Header and row now match the test file byte for byte, except User ID (no TEST- prefix outside a test load), Required Training Approvals and Location ID. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import type { Kostenstelle } from "./cost-centers";
|
||||
import type { CsvForm, ExportColumn } from "./export";
|
||||
import { unitOf, type OrgMaps } from "./org";
|
||||
import type { EmploymentStatus } from "./types";
|
||||
|
||||
// Der Export für Cornerstone (CSOD) — 57 Spalten in fester Reihenfolge.
|
||||
@@ -40,12 +39,36 @@ import type { EmploymentStatus } from "./types";
|
||||
export const CORNERSTONE_CSV: CsvForm = { trenner: ",", bom: false };
|
||||
|
||||
export const CORNERSTONE_SPRACHE = "de-DE";
|
||||
export const CORNERSTONE_ZEITZONE = "CET";
|
||||
export const CORNERSTONE_WAEHRUNG = "EUR";
|
||||
export const CORNERSTONE_USER_TYPE = "Mitarbeiter";
|
||||
|
||||
/** Platzhalter, solange das führende System die Kennung nicht liefert. */
|
||||
export const DIVISION_PLATZHALTER = "DIVISION-ID-HIER";
|
||||
/**
|
||||
* Der Benutzertyp als **Systemwert**, nicht als Anzeigename.
|
||||
*
|
||||
* Cornerstone zeigt „Mitarbeiter" und erwartet beim Einlesen „Employee".
|
||||
* Der Anzeigename führt zu „User Type ungültiger Wert" und danach zu
|
||||
* „Alle abhängigen Felder müssen gültig sein" — einem Folgefehler, der
|
||||
* verschwindet, sobald der erste behoben ist.
|
||||
*/
|
||||
export const CORNERSTONE_USER_TYPE = "Employee";
|
||||
|
||||
/**
|
||||
* Die Zeitzone bleibt leer.
|
||||
*
|
||||
* Gültig ist allein eine Zeitzonen-ID aus dem Portal; ein Kürzel wie „CET"
|
||||
* quittiert der Load mit „Zeitzonencode nicht eindeutig". Leer heisst:
|
||||
* Vererbung vom Portal beziehungsweise von der Organisationseinheit.
|
||||
*/
|
||||
export const CORNERSTONE_ZEITZONE = "";
|
||||
|
||||
/**
|
||||
* Die Division als GUID, übernommen aus dem Testload vom 27.09.2026.
|
||||
*
|
||||
* Cornerstone verlangt hier die GUID, nicht den Namen. Sie steht in keiner
|
||||
* Alpenwerk-Tabelle — sie gehört dem Zielsystem. Vor einem Echtlauf gegen
|
||||
* das Portal prüfen.
|
||||
*/
|
||||
export const DIVISION_GUID = "1f4c00b3-f0e7-4b53-b84e-e1f8d6088262";
|
||||
|
||||
export const DOXIS_PRAEFIX = "DOX-";
|
||||
export const LGV_PRAEFIX = "LGV-";
|
||||
|
||||
@@ -75,16 +98,6 @@ export function csDatum(iso: string | null | undefined): string {
|
||||
return j && m && t ? `${t}.${m}.${j}` : "";
|
||||
}
|
||||
|
||||
/** Volle Monate zwischen Eintritt und Stichtag. */
|
||||
export function dienstmonate(eintritt: string | null | undefined, stichtag: string): number | "" {
|
||||
if (!eintritt) return "";
|
||||
const [ej, em, et] = eintritt.slice(0, 10).split("-").map(Number);
|
||||
const [sj, sm, st] = stichtag.slice(0, 10).split("-").map(Number);
|
||||
if (!ej || !sj) return "";
|
||||
const monate = (sj - ej) * 12 + (sm - em) - (st < et ? 1 : 0);
|
||||
return Math.max(0, monate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Anmeldename: vorname.nachname, klein, ohne Umlaute und Leerzeichen.
|
||||
*
|
||||
@@ -115,14 +128,21 @@ export function benutzername(vorname: string, nachname: string): string {
|
||||
* dem System oder sie gälte als arbeitend.
|
||||
*/
|
||||
export function kontoStatus(status: EmploymentStatus): string {
|
||||
return status === "Ausgetreten" ? "Inaktiv" : "Aktiv";
|
||||
return status === "Ausgetreten" ? "Inactive" : "Active";
|
||||
}
|
||||
|
||||
/**
|
||||
* Working, On Leave oder Terminated — die Werte des Zielsystems.
|
||||
*
|
||||
* „Geplant" kennt Cornerstone nicht. Eine Person, deren Eintritt noch
|
||||
* bevorsteht, wird als Working geladen: sie bekommt ein Konto, und das
|
||||
* Eintrittsdatum steht ohnehin in der Zeile. Alles andere hiesse, sie
|
||||
* entweder wegzulassen oder einen Wert zu erfinden.
|
||||
*/
|
||||
export function beschaeftigungsStatus(status: EmploymentStatus): string {
|
||||
if (status === "Karenz") return "Abwesend";
|
||||
if (status === "Ausgetreten") return "Ausgetreten";
|
||||
if (status === "Geplant") return "Eintritt geplant";
|
||||
return "Arbeitend";
|
||||
if (status === "Karenz") return "On Leave";
|
||||
if (status === "Ausgetreten") return "Terminated";
|
||||
return "Working";
|
||||
}
|
||||
|
||||
export type CornerstoneZeile = Record<string, string | number>;
|
||||
@@ -149,31 +169,24 @@ export type CornerstoneQuelle = {
|
||||
status: EmploymentStatus;
|
||||
paygrade: string;
|
||||
location_id: string;
|
||||
org_unit_id: string | null;
|
||||
position_id: string | null;
|
||||
position_number: string | null;
|
||||
manager_id: string | null;
|
||||
};
|
||||
|
||||
export type CornerstoneKontext = {
|
||||
orgMaps: OrgMaps;
|
||||
standortName: Map<string, string>;
|
||||
/** Personalnummer je Mitarbeiterkennung — Cornerstone verweist über die User ID. */
|
||||
managerNummer: Map<string, number>;
|
||||
/** Kostenstelle je Planstelle zum Stichtag. */
|
||||
kostenstelle: Map<string, Kostenstelle>;
|
||||
/** Wer schon einmal wiedereingetreten ist. */
|
||||
wiedereingestellt: Set<string>;
|
||||
stichtag: string;
|
||||
};
|
||||
|
||||
/** Eine Zeile — die Zuordnung selbst, ohne Datenbank und ohne Dateiformat. */
|
||||
export function baueCornerstoneZeile(p: CornerstoneQuelle, k: CornerstoneKontext): CornerstoneZeile {
|
||||
const nummer = p.personnel_number;
|
||||
const name = benutzername(p.first_name, p.last_name);
|
||||
const einheit = unitOf(k.orgMaps, p.org_unit_id);
|
||||
const kst = p.position_id ? k.kostenstelle.get(p.position_id) : undefined;
|
||||
const chef = p.manager_id ? k.managerNummer.get(p.manager_id) : undefined;
|
||||
const ausgetreten = p.status === "Ausgetreten";
|
||||
|
||||
return {
|
||||
"User ID": String(nummer),
|
||||
@@ -193,7 +206,9 @@ export function baueCornerstoneZeile(p: CornerstoneQuelle, k: CornerstoneKontext
|
||||
Gender: geschlecht(p.gender),
|
||||
Exempt: "",
|
||||
Ethnicity: "",
|
||||
"Months of Service": dienstmonate(p.entry_date, k.stichtag),
|
||||
// Leer: Cornerstone rechnet die Dienstzeit aus den Eintrittsdaten selbst.
|
||||
// Ein mitgeladener Wert wäre ab dem Tag nach dem Einlesen veraltet.
|
||||
"Months of Service": "",
|
||||
"Compensation Currency": CORNERSTONE_WAEHRUNG,
|
||||
"Address Line 1": p.address ?? "",
|
||||
"Address Line 2": "",
|
||||
@@ -217,24 +232,35 @@ export function baueCornerstoneZeile(p: CornerstoneQuelle, k: CornerstoneKontext
|
||||
"Time Zone": CORNERSTONE_ZEITZONE,
|
||||
"User Type": CORNERSTONE_USER_TYPE,
|
||||
"Employment Status": beschaeftigungsStatus(p.status),
|
||||
"Leave Reason": p.status === "Karenz" ? (p.absence_type ?? "") : "",
|
||||
"Termination Type": p.austrittsart ?? "",
|
||||
"Termination Reason": p.exit_reason ?? "",
|
||||
"Termination Date": csDatum(p.exit_date),
|
||||
// Die vier Austrittsfelder und der Abwesenheitsgrund nur, wenn der
|
||||
// Beschäftigungsstatus sie trägt. Ein Grund ohne Austritt ist für den
|
||||
// Load ein ungültiger Zustand, nicht eine Zusatzangabe.
|
||||
"Leave Reason": ausgetreten ? "" : p.status === "Karenz" ? (p.absence_type ?? "") : "",
|
||||
"Termination Type": ausgetreten ? (p.austrittsart ?? "") : "",
|
||||
"Termination Reason": ausgetreten ? (p.exit_reason ?? "") : "",
|
||||
"Termination Date": ausgetreten ? csDatum(p.exit_date) : "",
|
||||
// Wiedereinstellbarkeit wird in Alpenwerk nicht erhoben.
|
||||
"Eligible For Rehire": "",
|
||||
// „Ja" nur, wenn die Historie einen Wiedereintritt trägt. Leer heisst
|
||||
// „nie ausgetreten und zurückgekommen", nicht „unbekannt".
|
||||
"Rehired Employee": k.wiedereingestellt.has(p.id) ? "Ja" : "",
|
||||
"Division ID": DIVISION_PLATZHALTER,
|
||||
"Position ID": p.position_number ?? "",
|
||||
// „Nein" gehört laut Vorgabe nicht in die Datei, und der Wert für „Ja"
|
||||
// ist nicht bestätigt. Ein nicht bestätigter Wert liesse den Load die
|
||||
// **ganze** Zeile abweisen, nicht nur das Feld — deshalb bleibt sie leer.
|
||||
"Rehired Employee": "",
|
||||
"Division ID": DIVISION_GUID,
|
||||
// Leer: die Planstellennummer aus Alpenwerk (S-0001) ist keine
|
||||
// Cornerstone-Position-ID. Sie hier einzutragen hiesse, eine Kennung des
|
||||
// Zielsystems zu erfinden.
|
||||
"Position ID": "",
|
||||
"Grade ID": p.paygrade,
|
||||
"Cost Center ID": kst?.code ?? "",
|
||||
"Location ID": k.standortName.get(p.location_id) ?? "",
|
||||
// Leer: Cornerstone erwartet seine eigene Standortkennung („01"), und
|
||||
// die Tabelle locations führt nur id, name und country. Der Name wäre
|
||||
// hier keine Kennung, sondern eine, die es im Zielsystem nicht gibt.
|
||||
// Solange dafür keine Spalte existiert, bleibt das Feld leer.
|
||||
"Location ID": "",
|
||||
// Beide Spalten wertet der Load derzeit nicht aus. Sie stehen in der
|
||||
// Kopfzeile, weil die Vorlage sie führt.
|
||||
"Retention Rules": "",
|
||||
// Die Ebene, auf der die Person hängt — Gesellschaft, Bereich, Abteilung
|
||||
// oder Team. Nicht der Name der Einheit: der steht im Organigramm, und
|
||||
// Cornerstone gliedert nach der Stufe.
|
||||
Organisationsstufe: einheit?.unit_type ?? "",
|
||||
Organisationsstufe: "",
|
||||
"Customfield ID AD": name,
|
||||
"Customfield ID Doxis": `${DOXIS_PRAEFIX}${nummer}`,
|
||||
"Customfield ID Interflex": String(nummer),
|
||||
|
||||
Reference in New Issue
Block a user