diff --git a/routes/operations.js b/routes/operations.js index 7566c4a..a91f6a4 100644 --- a/routes/operations.js +++ b/routes/operations.js @@ -749,25 +749,27 @@ module.exports = function(pool) { }); } - // Cloning from a named segment is how a period with no baseline gets a - // shape: pick the ledger line -- prior year, plan -- and copy its mix - // forward. That means reading rows exclude_iters normally keeps - // operations away from, so the entry is named explicitly and replaces - // the exclusion rather than widening it. The rows written are ordinary - // clone rows either way, so they are adjustable afterwards. - let excludeClause; + // exclude_iters deliberately does not apply here. It exists to stop + // operations *modifying* reference rows: scale would attribute forecast + // movement to prior-year rows by distributing across them, and recode + // writes negative rows that zero the original out. Clone does neither -- + // it reads rows and writes new pf_iter = 'clone' rows, leaving the source + // untouched. Copying a plan or a prior year out of reference and into + // adjustments is the operation working as intended, and excluding them + // meant a visible, deliberate selection silently produced nothing. + // + // from_logid narrows instead: a selection spanning AOP and Prior Year + // where only one is wanted. + let excludeClause = ''; if (from_logid != null) { const srcLog = await pool.query( - `SELECT id, operation, coalesce(nullif(tag, ''), note) AS label - FROM pf.log WHERE id = $1 AND version_id = $2`, + `SELECT id FROM pf.log WHERE id = $1 AND version_id = $2`, [parseInt(from_logid), ctx.version.id] ); if (!srcLog.rows.length) { return res.status(400).json({ error: `No log entry ${from_logid} on this version` }); } excludeClause = `AND pf_logid = ${parseInt(from_logid)}`; - } else { - excludeClause = buildExcludeClause(ctx.version.exclude_iters); } // Period dimensions come from the calendar against the shifted date, not diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index f1a4635..900ce45 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -1067,11 +1067,18 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio flash('Enter at least one new dimension value', 'error') return } + // A clone that changes nothing is only pointless when the source and the + // destination are the same band. Copying a plan or a prior year out of + // reference and into adjustments changes what the rows *are*, even at factor 1 + // with no shift -- that is the whole operation. Only warn when the selection + // is already adjustable. if (op === 'clone') { const shifts = body.date_offset && body.date_offset !== '0 days' const scales = body.scale != null && body.scale !== 1 const changes = Object.keys(body.set || {}).length > 0 - if (!shifts && !scales && !changes) { + const fromRef = !!body.from_logid || + (currentTotals?.excluded?.rows > 0 && !(currentTotals?.total?.value || currentTotals?.total?.units)) + if (!shifts && !scales && !changes && !fromRef) { flash('A clone with no override, no date shift and a factor of 1 would just duplicate the rows', 'error') return }