feat: накидка по строке в таблице «Цена продажи» + минимальный CSV
Sale-таблица показывает LOT/Описание/Кол-во/Накидка,%/Цена вместо Estimate/ Склад/Конкуренты/Ручная цена; итоговая цена строки = база (raw estimate или доля от общей «Ручная цена») × (Аплифт к estimate + своя Накидка%). Экспорт CSV этой таблицы теперь выводит только LOT;Описание;Кол-во;Цена. Buy-таблица и массовый экспорт по проекту не затронуты — новые поля запроса опциональны и включаются только кнопкой «Экспорт CSV» у Sale-таблицы. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
fb412a4227
commit
7263dd4572
+177
-79
@@ -267,7 +267,6 @@
|
||||
<h3 class="text-base font-semibold text-gray-800">Цена продажи</h3>
|
||||
<span class="text-xs text-gray-400">Цены указаны за 1 шт.</span>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mb-3">Склад и Конкуренты умножаются на 1,3</p>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm border-collapse">
|
||||
<thead class="bg-gray-50 text-gray-700">
|
||||
@@ -277,22 +276,17 @@
|
||||
<th class="px-2 py-2 text-left border-b">Описание</th>
|
||||
<th class="px-2 py-2 text-left border-b">LOT</th>
|
||||
<th class="px-2 py-2 text-right border-b">Кол-во</th>
|
||||
<th class="px-2 py-2 text-right border-b">Estimate</th>
|
||||
<th class="px-2 py-2 text-right border-b stock-price-col">Склад</th>
|
||||
<th class="px-2 py-2 text-right border-b">Конкуренты</th>
|
||||
<th class="px-2 py-2 text-right border-b">Ручная цена</th>
|
||||
<th class="px-2 py-2 text-right border-b w-20">Накидка, %</th>
|
||||
<th class="px-2 py-2 text-right border-b">Цена</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="pricing-body-sale">
|
||||
<tr><td colspan="{{if eq .IndicatorMode "accessible"}}9{{else}}8{{end}}" class="px-2 py-8 text-center text-gray-400">Загрузите BOM во вкладке «BOM»</td></tr>
|
||||
<tr><td colspan="{{if eq .IndicatorMode "accessible"}}7{{else}}6{{end}}" class="px-2 py-8 text-center text-gray-400">Загрузите BOM во вкладке «BOM»</td></tr>
|
||||
</tbody>
|
||||
<tfoot id="pricing-foot-sale" class="hidden bg-gray-50 font-semibold">
|
||||
<tr>
|
||||
<td colspan="{{if eq .IndicatorMode "accessible"}}5{{else}}4{{end}}" class="px-2 py-2 text-right">Итого:</td>
|
||||
<td class="px-2 py-2 text-right" id="pricing-total-sale-estimate">—</td>
|
||||
<td class="px-2 py-2 text-right stock-price-col" id="pricing-total-sale-warehouse">—</td>
|
||||
<td class="px-2 py-2 text-right" id="pricing-total-sale-competitor">—</td>
|
||||
<td class="px-2 py-2 text-right font-bold" id="pricing-total-sale-vendor">—</td>
|
||||
<td colspan="{{if eq .IndicatorMode "accessible"}}6{{else}}5{{end}}" class="px-2 py-2 text-right">Итого:</td>
|
||||
<td class="px-2 py-2 text-right font-bold" id="pricing-total-sale-price">—</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
@@ -612,11 +606,9 @@ let componentPricesCache = {}; // { lot_name: price } - caches prices loaded via
|
||||
let componentPricesCacheLoading = new Map(); // { category: Promise } - tracks ongoing price loads
|
||||
|
||||
// ─── Sale (DDP) pricing markup — single source of truth for this file ───
|
||||
// Mirrors internal/services/export.go (saleMarkupFactor / stockCompetitorMarkupFactor).
|
||||
// Keep both sides in sync: Estimate scales by the user's uplift, Stock/Competitor by a fixed factor.
|
||||
// Mirrors internal/services/export.go effectiveSaleMarkupFactor/defaultSaleMarkup.
|
||||
const PricingMarkup = {
|
||||
DEFAULT_SALE_UPLIFT: 1.3,
|
||||
STOCK_COMPETITOR_FIXED: 1.3,
|
||||
// Reads the "Аплифт к estimate" input; falls back to DEFAULT_SALE_UPLIFT when empty/invalid.
|
||||
getSaleUplift() {
|
||||
const v = parseDecimalInput(document.getElementById('pricing-uplift-sale')?.value || '');
|
||||
@@ -624,6 +616,22 @@ const PricingMarkup = {
|
||||
},
|
||||
};
|
||||
|
||||
// Per-row "Накидка" (%) for the Sale table, keyed by _saleRowKey(vendorPN, lot).
|
||||
// Persisted in Configuration.Notes.pricing_ui.sale_row_markups (see buildPricingState/
|
||||
// restorePricingStateFromNotes); restored before the table exists, so renderPricingTab()
|
||||
// reads it live rather than relying on per-render input state.
|
||||
let saleRowMarkups = {};
|
||||
|
||||
// Amber marker for world-fallback cells — must stay outside gray/green/red/blue
|
||||
// families since applyCustomPrice() strips those on vendor-price cells via regex.
|
||||
// Shared by renderPricingTab() (initial paint) and recomputeSalePrices() (re-paint on
|
||||
// input change), so both module-level functions rather than a closure in either one.
|
||||
const WORLD_CLS = 'bg-amber-50 text-amber-700';
|
||||
const worldCls = (w) => (window.QF_INDICATOR_MODE !== 'accessible' && w) ? WORLD_CLS : '';
|
||||
const worldMark = (w) => (window.QF_INDICATOR_MODE === 'accessible' && w)
|
||||
? '<sup class="text-gray-500 font-semibold" title="Цена-заглушка из прайслиста WORLD">W</sup>'
|
||||
: '';
|
||||
|
||||
// Autocomplete state
|
||||
let autocompleteInput = null;
|
||||
let autocompleteCategory = null;
|
||||
@@ -2580,10 +2588,18 @@ function buildPricingState() {
|
||||
const saleUplift = parseDecimalInput(document.getElementById('pricing-uplift-sale')?.value || '');
|
||||
const saleCustom = parseDecimalInput(document.getElementById('pricing-custom-price-sale')?.value || '');
|
||||
|
||||
// Only non-zero entries are kept, so an untouched config serializes exactly like
|
||||
// before this feature (additive key — old clients simply ignore it).
|
||||
const rowMarkups = {};
|
||||
Object.entries(saleRowMarkups).forEach(([key, pct]) => {
|
||||
if (pct > 0) rowMarkups[key] = pct;
|
||||
});
|
||||
|
||||
return {
|
||||
buy_custom_price: buyCustom > 0 ? buyCustom : null,
|
||||
sale_uplift: saleUplift > 0 ? saleUplift : null,
|
||||
sale_custom_price: saleCustom > 0 ? saleCustom : null,
|
||||
sale_row_markups: Object.keys(rowMarkups).length ? rowMarkups : null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2625,6 +2641,17 @@ function restorePricingStateFromNotes(notesRaw) {
|
||||
? pricing.sale_custom_price.toFixed(2)
|
||||
: '';
|
||||
}
|
||||
|
||||
// Sale-table row is not rendered yet at this point — renderPricingTab() reads
|
||||
// saleRowMarkups live when it builds each row's Накидка input.
|
||||
saleRowMarkups = {};
|
||||
if (pricing.sale_row_markups && typeof pricing.sale_row_markups === 'object') {
|
||||
Object.entries(pricing.sale_row_markups).forEach(([key, pct]) => {
|
||||
if (typeof pct === 'number' && Number.isFinite(pct) && pct > 0) {
|
||||
saleRowMarkups[key] = pct;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getAutosaveStorageKey() {
|
||||
@@ -4517,9 +4544,6 @@ async function renderPricingTab() {
|
||||
} catch(e) { /* silent */ }
|
||||
}
|
||||
|
||||
// Sale uplift applied to estimate; Stock/Competitor use the fixed factor. See PricingMarkup.
|
||||
const saleUplift = PricingMarkup.getSaleUplift();
|
||||
const SALE_FIXED_MULT = PricingMarkup.STOCK_COMPETITOR_FIXED;
|
||||
|
||||
// Helper: returns unit prices from pricelist for a single LOT
|
||||
const _getUnitPrices = (pl) => ({
|
||||
@@ -4531,17 +4555,9 @@ async function renderPricingTab() {
|
||||
compWorld: !!(pl && pl.competitor_from_world),
|
||||
});
|
||||
|
||||
// Amber marker for world-fallback cells — must stay outside gray/green/red/blue
|
||||
// families since applyCustomPrice() strips those on vendor-price cells via regex.
|
||||
const WORLD_CLS = 'bg-amber-50 text-amber-700';
|
||||
|
||||
// Colour-blind-safe mode: leading signal-meter column, no row tint, and the
|
||||
// amber world-fallback cell tint is replaced by a text marker.
|
||||
const ACCESSIBLE_MODE = window.QF_INDICATOR_MODE === 'accessible';
|
||||
const worldCls = (w) => (!ACCESSIBLE_MODE && w) ? WORLD_CLS : '';
|
||||
const worldMark = (w) => (ACCESSIBLE_MODE && w)
|
||||
? '<sup class="text-gray-500 font-semibold" title="Цена-заглушка из прайслиста WORLD">W</sup>'
|
||||
: '';
|
||||
const meterCell = (q, bt) => ACCESSIBLE_MODE
|
||||
? `<td class="pricing-quality-meter px-2 py-1.5 align-middle ${bt || ''}">${qualityMeterHtml(q)}</td>`
|
||||
: '';
|
||||
@@ -4729,15 +4745,15 @@ async function renderPricingTab() {
|
||||
}
|
||||
|
||||
// ─── Populate Sale table ─────────────────────────────────────────────────
|
||||
// Sale rows show one final "Цена" per row: rawEstimate (or a proportional share
|
||||
// of the global "Ручная цена" when set) multiplied by (Аплифт к estimate + row's
|
||||
// own Накидка%). See recomputeSalePrices() / _saleRowPrice() for the shared math,
|
||||
// re-run on every input change and after every render.
|
||||
tbodySale.innerHTML = '';
|
||||
if (!rowData.length) {
|
||||
tbodySale.innerHTML = `<tr><td colspan="${ACCESSIBLE_MODE ? 9 : 8}" class="px-2 py-8 text-center text-gray-400">Нет данных для отображения</td></tr>`;
|
||||
tbodySale.innerHTML = `<tr><td colspan="${ACCESSIBLE_MODE ? 7 : 6}" class="px-2 py-8 text-center text-gray-400">Нет данных для отображения</td></tr>`;
|
||||
tfootSale.classList.add('hidden');
|
||||
} else {
|
||||
let totEst = 0, totWh = 0, totComp = 0;
|
||||
let hasEst = false, hasWh = false, hasComp = false;
|
||||
let cntWh = 0, cntComp = 0;
|
||||
let worldEst = 0, worldWh = 0, worldComp = 0;
|
||||
rowData.forEach(r => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.classList.add('pricing-row-sale');
|
||||
@@ -4746,21 +4762,16 @@ async function renderPricingTab() {
|
||||
} else if (r.isEstOnly) {
|
||||
tr.classList.add('bg-blue-50');
|
||||
}
|
||||
const saleEstUnit = r.estUnit > 0 ? r.estUnit * saleUplift : 0;
|
||||
const saleWhUnit = r.warehouseUnit != null ? r.warehouseUnit * SALE_FIXED_MULT : null;
|
||||
const saleCompUnit = r.competitorUnit != null ? r.competitorUnit * SALE_FIXED_MULT : null;
|
||||
const saleEstTotal = saleEstUnit * r.qty;
|
||||
const saleWhTotal = saleWhUnit != null ? saleWhUnit * r.qty : null;
|
||||
const saleCompTotal = saleCompUnit != null ? saleCompUnit * r.qty : null;
|
||||
tr.dataset.estSale = saleEstTotal;
|
||||
const rowKey = _saleRowKey(r.vendorPN, r.lotText);
|
||||
const markupPct = saleRowMarkups[rowKey] || 0;
|
||||
tr.dataset.rowKey = rowKey;
|
||||
tr.dataset.rawEst = r.est;
|
||||
tr.dataset.qty = r.qty;
|
||||
tr.dataset.groupStart = r.groupStart ? 'true' : 'false';
|
||||
tr.dataset.vendorPn = r.vendorPN || '';
|
||||
tr.dataset.desc = r.desc;
|
||||
tr.dataset.lot = r.lotText;
|
||||
if (saleEstTotal > 0) { totEst += saleEstTotal; hasEst = true; if (r.estWorld) worldEst += saleEstTotal; }
|
||||
if (saleWhTotal != null) { totWh += saleWhTotal; hasWh = true; cntWh++; if (r.whWorld) worldWh += saleWhTotal; }
|
||||
if (saleCompTotal != null) { totComp += saleCompTotal; hasComp = true; cntComp++; if (r.compWorld) worldComp += saleCompTotal; }
|
||||
tr.dataset.estWorld = r.estWorld ? 'true' : 'false';
|
||||
const borderTop = r.groupStart ? 'border-t border-gray-200' : '';
|
||||
const pnDescHtml = r.groupStart ? (() => {
|
||||
const rs = r.groupSize > 1 ? ` rowspan="${r.groupSize}"` : '';
|
||||
@@ -4772,23 +4783,100 @@ async function renderPricingTab() {
|
||||
${pnDescHtml}
|
||||
<td class="px-2 py-1.5 text-xs ${borderTop}">${r.lotCell}</td>
|
||||
<td class="px-2 py-1.5 text-right text-xs ${borderTop}">${r.qty}</td>
|
||||
<td class="px-2 py-1.5 text-right text-xs ${borderTop} ${worldCls(r.estWorld)}">${saleEstUnit > 0 ? formatCurrency(saleEstUnit) : '—'}${worldMark(r.estWorld)}</td>
|
||||
<td class="px-2 py-1.5 text-right text-xs stock-price-col ${borderTop} ${worldCls(r.whWorld)}">${saleWhUnit != null ? formatCurrency(saleWhUnit) : '—'}${worldMark(r.whWorld)}</td>
|
||||
<td class="px-2 py-1.5 text-right text-xs ${borderTop} ${worldCls(r.compWorld)}">${saleCompUnit != null ? formatCurrency(saleCompUnit) : '—'}${worldMark(r.compWorld)}</td>
|
||||
<td class="px-2 py-1.5 text-right text-xs pricing-vendor-price-sale ${borderTop} text-gray-400">—</td>
|
||||
<td class="px-2 py-1.5 text-right ${borderTop}">
|
||||
<input type="text" inputmode="decimal" class="pricing-row-markup w-16 px-1 py-0.5 border rounded text-right text-xs"
|
||||
value="${formatMarkupInput(markupPct)}"
|
||||
placeholder="0" oninput="onSaleRowMarkupInput(this)">
|
||||
</td>
|
||||
<td class="px-2 py-1.5 text-right text-xs pricing-sale-price ${borderTop} ${worldCls(r.estWorld)}">—${worldMark(r.estWorld)}</td>
|
||||
`;
|
||||
tbodySale.appendChild(tr);
|
||||
});
|
||||
document.getElementById('pricing-total-sale-vendor').textContent = '—';
|
||||
_setPricingTotal('pricing-total-sale-estimate', hasEst, totEst, worldEst, rowData.length, rowData.length);
|
||||
_setPricingTotal('pricing-total-sale-warehouse', hasWh, totWh, worldWh, cntWh, rowData.length);
|
||||
_setPricingTotal('pricing-total-sale-competitor', hasComp, totComp, worldComp, cntComp, rowData.length);
|
||||
tfootSale.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Restore custom prices after re-render
|
||||
applyCustomPrice('buy');
|
||||
applyCustomPrice('sale');
|
||||
recomputeSalePrices();
|
||||
}
|
||||
|
||||
// Composite key for a Sale-table row's per-row "Накидка": the same LOT can appear
|
||||
// under more than one vendor PN group (see bible-local/02-architecture.md "Per-LOT
|
||||
// row expansion rules"), so lot_name alone is not a safe map key.
|
||||
function _saleRowKey(vendorPN, lot) {
|
||||
const U = s => (s || '').toUpperCase();
|
||||
return `${U(vendorPN) || 'NONE'}::${U(lot)}`;
|
||||
}
|
||||
|
||||
// Single source of truth for a Sale row's final price: its base (raw estimate, or a
|
||||
// proportional share of the global "Ручная цена" when set) times (global uplift + this
|
||||
// row's own Накидка%).
|
||||
function _saleRowPrice(base, markupPct, saleUplift) {
|
||||
const factor = saleUplift + ((markupPct || 0) / 100);
|
||||
return base * factor;
|
||||
}
|
||||
|
||||
// Re-derives every Sale row's displayed price from its raw estimate, the global
|
||||
// uplift/manual-price inputs, and its own Накидка input. Called after render and on
|
||||
// every uplift/manual-price/row-markup change — the only place this math happens.
|
||||
function recomputeSalePrices() {
|
||||
const saleUplift = PricingMarkup.getSaleUplift();
|
||||
const customPrice = parseDecimalInput(document.getElementById('pricing-custom-price-sale')?.value || '');
|
||||
const rows = Array.from(document.querySelectorAll('#pricing-body-sale tr.pricing-row-sale'));
|
||||
|
||||
let estimateTotalRaw = 0;
|
||||
let lastPricedIdx = -1;
|
||||
rows.forEach((tr, i) => {
|
||||
const rawEst = parseFloat(tr.dataset.rawEst) || 0;
|
||||
if (rawEst > 0) { estimateTotalRaw += rawEst; lastPricedIdx = i; }
|
||||
});
|
||||
|
||||
// Same last-row-absorbs-remainder distribution as applyCustomPrice()/
|
||||
// distributeManualPrice() so the rows' bases sum to customPrice exactly.
|
||||
let assigned = 0;
|
||||
let total = 0, worldTotal = 0, hasAny = false, pricedCount = 0;
|
||||
rows.forEach((tr, i) => {
|
||||
const rawEst = parseFloat(tr.dataset.rawEst) || 0;
|
||||
const cell = tr.querySelector('.pricing-sale-price');
|
||||
if (!cell) return;
|
||||
if (rawEst <= 0) {
|
||||
cell.innerHTML = '—';
|
||||
return;
|
||||
}
|
||||
let base;
|
||||
if (customPrice > 0 && estimateTotalRaw > 0) {
|
||||
if (i === lastPricedIdx) {
|
||||
base = Math.round((customPrice - assigned) * 100) / 100;
|
||||
} else {
|
||||
base = Math.round((rawEst / estimateTotalRaw) * customPrice * 100) / 100;
|
||||
assigned += base;
|
||||
}
|
||||
} else {
|
||||
base = rawEst;
|
||||
}
|
||||
const markupPct = saleRowMarkups[tr.dataset.rowKey] || 0;
|
||||
const isWorld = tr.dataset.estWorld === 'true';
|
||||
const price = _saleRowPrice(base, markupPct, saleUplift);
|
||||
cell.innerHTML = formatCurrency(price) + worldMark(isWorld);
|
||||
total += price;
|
||||
hasAny = true;
|
||||
pricedCount++;
|
||||
if (isWorld) worldTotal += price;
|
||||
});
|
||||
|
||||
_setPricingTotal('pricing-total-sale-price', hasAny, total, worldTotal, pricedCount, rows.length);
|
||||
}
|
||||
|
||||
function onSaleRowMarkupInput(inputEl) {
|
||||
const key = inputEl.dataset.rowKey;
|
||||
const value = parseDecimalInput(inputEl.value || '');
|
||||
if (value > 0) {
|
||||
saleRowMarkups[key] = value;
|
||||
} else {
|
||||
delete saleRowMarkups[key];
|
||||
}
|
||||
recomputeSalePrices();
|
||||
triggerAutoSave();
|
||||
}
|
||||
|
||||
// ─── Pricing helpers ─────────────────────────────────────────────────────────
|
||||
@@ -4849,25 +4937,27 @@ function formatUpliftInput(value) {
|
||||
return value.toFixed(4).replace('.', ',');
|
||||
}
|
||||
|
||||
// One or two decimals, comma separator, no trailing zeros — for the per-row Накидка input.
|
||||
function formatMarkupInput(value) {
|
||||
if (!Number.isFinite(value) || value <= 0) return '';
|
||||
return String(Math.round(value * 100) / 100).replace('.', ',');
|
||||
}
|
||||
|
||||
function _getPricingEstimateTotal(table) {
|
||||
const attr = table === 'sale' ? 'estSale' : 'est';
|
||||
const cls = table === 'sale' ? 'pricing-row-sale' : 'pricing-row-buy';
|
||||
let total = 0;
|
||||
document.querySelectorAll(`#pricing-body-${table} tr.${cls}`).forEach(tr => {
|
||||
total += parseFloat(tr.dataset[attr]) || 0;
|
||||
document.querySelectorAll(`#pricing-body-${table} tr.pricing-row-${table}`).forEach(tr => {
|
||||
total += parseFloat(tr.dataset.est) || 0;
|
||||
});
|
||||
return total;
|
||||
}
|
||||
|
||||
// Apply custom (own) price proportionally to Ручная цена column.
|
||||
// table: 'buy' | 'sale'
|
||||
// Apply custom (own) price proportionally to the Buy table's Ручная цена column.
|
||||
// The Sale table's own manual-price handling lives in recomputeSalePrices().
|
||||
function applyCustomPrice(table) {
|
||||
const inputId = `pricing-custom-price-${table}`;
|
||||
const totalElId = `pricing-total-${table}-vendor`;
|
||||
const rowClass = `pricing-row-${table}`;
|
||||
const cellClass = `.pricing-vendor-price-${table}`;
|
||||
const estAttr = table === 'sale' ? 'estSale' : 'est';
|
||||
const origAttr = table === 'buy' ? 'vendorOrig' : null;
|
||||
|
||||
const customPrice = parseFloat(document.getElementById(inputId)?.value) || 0;
|
||||
const estimateTotal = _getPricingEstimateTotal(table);
|
||||
@@ -4886,7 +4976,7 @@ function applyCustomPrice(table) {
|
||||
if (customPrice > 0 && estimateTotal > 0) {
|
||||
let assigned = 0;
|
||||
rows.forEach((tr, i) => {
|
||||
const rowEst = parseFloat(tr.dataset[estAttr]) || 0;
|
||||
const rowEst = parseFloat(tr.dataset.est) || 0;
|
||||
const qty = Math.max(1, parseFloat(tr.dataset.qty) || 1);
|
||||
const cell = vendorCells[i];
|
||||
if (!cell) return;
|
||||
@@ -4911,22 +5001,17 @@ function applyCustomPrice(table) {
|
||||
const cell = vendorCells[i];
|
||||
if (!cell) return;
|
||||
cell.className = cell.className.replace(/\btext-(?:gray|green|red|blue)-\d+\b/g, '').trim();
|
||||
if (origAttr && tr.dataset.vendorOrigUnit !== '') {
|
||||
if (tr.dataset.vendorOrigUnit !== '') {
|
||||
cell.textContent = formatCurrency(parseFloat(tr.dataset.vendorOrigUnit));
|
||||
} else {
|
||||
cell.textContent = '—';
|
||||
cell.classList.add('text-gray-400');
|
||||
}
|
||||
});
|
||||
// Recompute total from originals (buy) or clear (sale)
|
||||
if (origAttr) {
|
||||
let origTotal = 0; let hasOrig = false;
|
||||
rows.forEach(tr => { if (tr.dataset[origAttr] !== '') { origTotal += parseFloat(tr.dataset[origAttr]) || 0; hasOrig = true; } });
|
||||
totalVendorEl.textContent = hasOrig ? formatCurrency(origTotal) : '—';
|
||||
} else {
|
||||
// sale: reset to — already handled above
|
||||
totalVendorEl.textContent = '—';
|
||||
}
|
||||
// Recompute total from originals
|
||||
let origTotal = 0; let hasOrig = false;
|
||||
rows.forEach(tr => { if (tr.dataset.vendorOrig !== '') { origTotal += parseFloat(tr.dataset.vendorOrig) || 0; hasOrig = true; } });
|
||||
totalVendorEl.textContent = hasOrig ? formatCurrency(origTotal) : '—';
|
||||
totalVendorEl.className = totalVendorEl.className.replace(/\btext-(?:green|red)-\d+\b/g, '').trim();
|
||||
}
|
||||
}
|
||||
@@ -4937,7 +5022,7 @@ function onBuyCustomPriceInput() {
|
||||
}
|
||||
|
||||
function onSaleCustomPriceInput() {
|
||||
applyCustomPrice('sale');
|
||||
recomputeSalePrices();
|
||||
triggerAutoSave();
|
||||
}
|
||||
|
||||
@@ -4988,20 +5073,33 @@ async function exportPricingCSV(table) {
|
||||
const manualInputId = table === 'sale' ? 'pricing-custom-price-sale' : 'pricing-custom-price-buy';
|
||||
const manualPrice = parseDecimalInput(document.getElementById(manualInputId)?.value || '');
|
||||
const saleUplift = table === 'sale' ? PricingMarkup.getSaleUplift() : 0;
|
||||
// Sale export drops Estimate/Stock/Competitor and collapses to LOT/Описание/
|
||||
// Кол-во/Цена — see internal/services/export.go MinimalSaleColumns. Buy export is
|
||||
// untouched.
|
||||
const requestBody = table === 'sale'
|
||||
? {
|
||||
include_lot: true,
|
||||
basis: basis,
|
||||
sale_markup: saleUplift > 0 ? saleUplift : null,
|
||||
manual_price: manualPrice > 0 ? manualPrice : null,
|
||||
minimal_sale_columns: true,
|
||||
sale_row_markups: saleRowMarkups,
|
||||
}
|
||||
: {
|
||||
include_lot: true,
|
||||
include_bom: true,
|
||||
include_estimate: true,
|
||||
include_stock: !!showStockPrices,
|
||||
include_competitor: true,
|
||||
basis: basis,
|
||||
sale_markup: null,
|
||||
manual_price: manualPrice > 0 ? manualPrice : null,
|
||||
};
|
||||
try {
|
||||
const resp = await fetch(`/api/configs/${configUUID}/export/pricing`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
include_lot: true,
|
||||
include_bom: true,
|
||||
include_estimate: true,
|
||||
include_stock: !!showStockPrices,
|
||||
include_competitor: true,
|
||||
basis: basis,
|
||||
sale_markup: saleUplift > 0 ? saleUplift : null,
|
||||
manual_price: manualPrice > 0 ? manualPrice : null,
|
||||
}),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
if (!resp.ok) { showToast('Ошибка экспорта', 'error'); return; }
|
||||
const blob = await resp.blob();
|
||||
|
||||
Reference in New Issue
Block a user