fix(import): Sammelbuchungen und nicht abrechenbare Projekte anlegen
Beim Nachziehen der Harvest-Historie sind zwei Regeln im Weg gestanden,
die fuer eine Uebernahme nicht passen:
- Beim Anlegen verlangte die API auch fuer nicht abrechenbare Projekte
einen Satz, obwohl validateProject ihn direkt danach verwirft. Nur
Abruf rechnet ueber den Satz am Projekt ab, also gilt die Pflicht auch
nur dort.
- Der Importweg liess hoechstens 24 Stunden je Tag zu. In Harvest stehen
Sammelbuchungen ("Nachbuchung DORA September und Oktober", 80 Stunden
auf einem Tag). Sie abzulehnen verliert die Stunden und verfaelscht das
Restbudget, sie zu verteilen erfindet Leistungsdaten - deshalb kommen
sie so herein, wie sie gebucht wurden. Eine Buchung darf jetzt bis zu
31 Tage umfassen, damit Zahlendreher weiter auffallen. Im
Buchungsdialog bleibt die Tagesgrenze bestehen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e962be41ea
commit
2710baa615
@@ -16,11 +16,21 @@ import { isBillable, resolvePot } from '../lib/projects.js';
|
||||
*
|
||||
* Wiedererkennung ueber harvest_id: ein zweiter Lauf legt nichts doppelt an,
|
||||
* sondern meldet die Buchung als "schon vorhanden".
|
||||
*
|
||||
* Eine Ausnahme von den Fachregeln: die Grenze von 24 Stunden je Tag gilt hier
|
||||
* nicht. In Harvest stehen Sammelbuchungen ("Nachbuchung DORA September und
|
||||
* Oktober", 80 Stunden auf einem Tag). Wer sie ablehnt, verliert die Stunden
|
||||
* und damit das Restbudget des Projekts; wer sie auf Tage verteilt, erfindet
|
||||
* Leistungsdaten. Sie kommen deshalb so herein, wie sie gebucht wurden. Im
|
||||
* Buchungsdialog bleibt die Tagesgrenze bestehen.
|
||||
*/
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const MAX_BATCH = 1000;
|
||||
// Sammelbuchungen duerfen mehr als einen Tag umfassen - hoechstens aber 31 Tage,
|
||||
// damit Zahlendreher nicht unbemerkt durchgehen.
|
||||
const MAX_MINUTES = 31 * 24 * 60;
|
||||
|
||||
router.post('/entries', async (req, res, next) => {
|
||||
const body = req.body || {};
|
||||
@@ -47,7 +57,9 @@ router.post('/entries', async (req, res, next) => {
|
||||
const note = row.note == null ? null : String(row.note).trim().slice(0, 1000) || null;
|
||||
|
||||
if (!parseDate(date)) throw new Error('Datum fehlt oder ist unguelltig.');
|
||||
if (!Number.isInteger(minutes) || minutes < 1 || minutes > 24 * 60) throw new Error('Minuten muessen zwischen 1 und 1440 liegen.');
|
||||
if (!Number.isInteger(minutes) || minutes < 1 || minutes > MAX_MINUTES) {
|
||||
throw new Error(`Minuten muessen zwischen 1 und ${MAX_MINUTES} liegen.`);
|
||||
}
|
||||
|
||||
// Schon importiert? Dann nicht erneut anlegen.
|
||||
if (harvestId) {
|
||||
@@ -77,12 +89,6 @@ router.post('/entries', async (req, res, next) => {
|
||||
task = found;
|
||||
}
|
||||
|
||||
const [[day]] = await conn.query(
|
||||
'SELECT COALESCE(SUM(minutes), 0) AS total FROM pulse_time_entries WHERE user_id = ? AND entry_date = ?',
|
||||
[userId, date]
|
||||
);
|
||||
if (Number(day.total) + minutes > 24 * 60) throw new Error('An diesem Tag waeren es mehr als 24 Stunden.');
|
||||
|
||||
const billable = isBillable(project, task);
|
||||
// Historische Uebernahme: abgelaufene Toepfe nehmen ihre eigenen Zeiten noch an.
|
||||
const pot = await resolvePot(conn, project, date, minutes, null, billable, { historic: true });
|
||||
|
||||
@@ -195,7 +195,9 @@ router.post('/', requireRole('lead'), async (req, res, next) => {
|
||||
errors.push(...result.errors);
|
||||
pots.push(result.pot);
|
||||
}
|
||||
} else if (!out.rate) {
|
||||
} else if (out.type === 'abruf' && !out.rate) {
|
||||
// Nur Abruf rechnet ueber den Satz am Projekt ab; nicht abrechenbare
|
||||
// Projekte brauchen keinen - er wird gleich darunter ohnehin verworfen.
|
||||
errors.push('Abruf: Stundensatz oder Tagessatz fehlt.');
|
||||
}
|
||||
if (out.type !== 'abruf') out.rate = null;
|
||||
|
||||
Reference in New Issue
Block a user