Ozon Price Calculator

A new extension

Size

16.3 KB

Version

1.0.3

Created

Apr 1, 2026

Updated

14 days ago

1// ==UserScript==
2// @name		Ozon Price Calculator
3// @description		A new extension
4// @version		1.0.3
5// @match		https://*.calculator.ozon.ru/*
6// @icon		https://ir.ozone.ru/s3/fe-favicon/favicon-media.svg
7// ==/UserScript==
8(function () {
9    'use strict';
10
11    // ── helpers ──────────────────────────────────────────────────────────────
12    function parseRub(text) {
13    // "−52 ₽" or "-52 ₽" → 52  (absolute value)
14        const m = text.replace(//g, '-').match(/-?([\d\s]+)/);
15        return m ? parseFloat(m[1].replace(/\s/g, '')) : 0;
16    }
17
18    function parsePct(text) {
19    // "20%" → 0.20
20        const m = text.match(/([\d.]+)%/);
21        return m ? parseFloat(m[1]) / 100 : 0;
22    }
23
24    // ── read commissions from Ozon table (FBO column) ─────────────────────
25    function readOzonTable() {
26        const t = document.querySelector('table');
27        if (!t) return null;
28        const rows = Array.from(t.rows);
29
30        // Find a row by keyword in its text
31        function findRow(keyword) {
32            return rows.find(r => r.innerText.includes(keyword)) || null;
33        }
34
35        // Extract first rub value from a row's FBO cell (3rd td or innerText)
36        function extractRub(row) {
37            if (!row) return 0;
38            const text = row.innerText.replace(//g, '-');
39            const m = text.match(/-?([\d\s]+)\s*₽/);
40            return m ? parseFloat(m[1].replace(/\s/g, '')) : 0;
41        }
42
43        // Extract first percent value from a row's FBO cell
44        function extractPct(row) {
45            if (!row) return 0;
46            const m = row.innerText.match(/([\d.]+)%/);
47            return m ? parseFloat(m[1]) / 100 : 0;
48        }
49
50        const priceRow    = findRow('Цена товара');
51        const commRow     = findRow('Вознаграждение');
52        const acqRow      = findRow('Эквайринг');
53        const delivRow    = findRow('Обработка');
54        const taxRow      = findRow('Налог на прибыль');
55
56        const price       = extractRub(priceRow);
57        const commPct     = extractPct(commRow);
58        const acqPct      = extractPct(acqRow);
59        const deliveryRub = extractRub(delivRow);
60        const taxRub      = extractRub(taxRow);
61        const taxPct      = price > 0 ? taxRub / price : 0;
62
63        console.log('[RM Calc] Таблица Ozon:', { price, commPct, acqPct, deliveryRub, taxRub, taxPct });
64
65        return { price, commPct, acqPct, deliveryRub, taxPct, taxRub };
66    }
67
68    // ── formula ──────────────────────────────────────────────────────────────
69    // Let P = цена поручения
70    // Цена реализации = P * (1 - ozonDiscount)
71    // Вознаграждение  = P * commPct
72    // Эквайринг       = P * acqPct
73    // Доставка        = deliveryRub  (fixed)
74    // Налог           = P * taxPct
75    // Реклама         = P * advPct
76    // Себестоимость на 1 выкуп = cost / (buyoutPct/100)
77    // Прибыль = P - P*commPct - P*acqPct - deliveryRub - P*taxPct - P*advPct - cost/(buyoutPct/100)
78    //         = P*(1 - commPct - acqPct - taxPct - advPct) - deliveryRub - costPerBuyout
79    // Маржинальность = Прибыль / P = targetMargin
80    // → P*(1 - commPct - acqPct - taxPct - advPct) - deliveryRub - costPerBuyout = targetMargin * P
81    // → P*(1 - commPct - acqPct - taxPct - advPct - targetMargin) = deliveryRub + costPerBuyout
82    // → P = (deliveryRub + costPerBuyout) / (1 - commPct - acqPct - taxPct - advPct - targetMargin)
83
84    function calculate(inputs, ozon) {
85        const { cost, buyoutPct, ozonDiscount, advPct, targetMargin } = inputs;
86        const { commPct, acqPct, deliveryRub, taxPct } = ozon;
87
88        const costPerBuyout = cost / (buyoutPct / 100);
89        const divisor = 1 - commPct - acqPct - taxPct - advPct / 100 - targetMargin / 100;
90
91        if (divisor <= 0) return null; // невозможно рассчитать
92
93        const P = (deliveryRub + costPerBuyout) / divisor;
94
95        const commRub    = P * commPct;
96        const acqRub     = P * acqPct;
97        const taxRub     = P * taxPct;
98        const advRub     = P * (advPct / 100);
99        const salePrice  = P * (1 - ozonDiscount / 100);
100        const profit     = P - commRub - acqRub - deliveryRub - taxRub - advRub - costPerBuyout;
101        const margin     = profit / P * 100;
102        const rom        = profit / cost * 100;
103        const costPct    = costPerBuyout / P * 100;
104
105        return { P, salePrice, commRub, commPct: commPct * 100, acqRub, acqPct: acqPct * 100, deliveryRub, taxRub, taxPct: taxPct * 100, advRub, costPerBuyout, profit, margin, rom, costPct };
106    }
107
108    // ── styles ────────────────────────────────────────────────────────────────
109    function injectStyles() {
110        const style = document.createElement('style');
111        style.textContent = `
112      #rm-calc {
113        background: #fff;
114        border: 1.5px solid #e5e7eb;
115        border-radius: 16px;
116        padding: 28px 32px 24px;
117        margin: 32px 0;
118        font-family: inherit;
119        max-width: 900px;
120      }
121      #rm-calc h2 {
122        font-size: 20px;
123        font-weight: 700;
124        color: #001a34;
125        margin: 0 0 20px 0;
126      }
127      .rm-grid {
128        display: grid;
129        grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
130        gap: 14px;
131        margin-bottom: 20px;
132      }
133      .rm-field label {
134        display: block;
135        font-size: 12px;
136        color: #6b7280;
137        margin-bottom: 4px;
138        font-weight: 500;
139      }
140      .rm-field input {
141        width: 100%;
142        box-sizing: border-box;
143        border: 1.5px solid #d1d5db;
144        border-radius: 8px;
145        padding: 8px 10px;
146        font-size: 15px;
147        color: #001a34;
148        outline: none;
149        transition: border-color .15s;
150        background: #f9fafb;
151      }
152      .rm-field input:focus {
153        border-color: #005bff;
154        background: #fff;
155      }
156      .rm-btn {
157        background: #005bff;
158        color: #fff;
159        border: none;
160        border-radius: 8px;
161        padding: 10px 28px;
162        font-size: 15px;
163        font-weight: 600;
164        cursor: pointer;
165        transition: background .15s;
166      }
167      .rm-btn:hover { background: #0047cc; }
168      .rm-result {
169        margin-top: 22px;
170        background: #f0f5ff;
171        border-radius: 12px;
172        padding: 20px 24px;
173        display: none;
174      }
175      .rm-result.visible { display: block; }
176      .rm-result-main {
177        font-size: 22px;
178        font-weight: 700;
179        color: #005bff;
180        margin-bottom: 16px;
181      }
182      .rm-result-main span {
183        font-size: 14px;
184        font-weight: 400;
185        color: #6b7280;
186        margin-left: 6px;
187      }
188      .rm-result-grid {
189        display: grid;
190        grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
191        gap: 10px;
192      }
193      .rm-result-item {
194        background: #fff;
195        border-radius: 8px;
196        padding: 10px 14px;
197        border: 1px solid #e5e7eb;
198      }
199      .rm-result-item .rm-ri-label {
200        font-size: 11px;
201        color: #9ca3af;
202        font-weight: 500;
203        text-transform: uppercase;
204        letter-spacing: .4px;
205        margin-bottom: 3px;
206      }
207      .rm-result-item .rm-ri-value {
208        font-size: 16px;
209        font-weight: 700;
210        color: #001a34;
211      }
212      .rm-result-item .rm-ri-value.green { color: #16a34a; }
213      .rm-result-item .rm-ri-value.red   { color: #dc2626; }
214      .rm-error {
215        color: #dc2626;
216        font-size: 13px;
217        margin-top: 10px;
218        display: none;
219      }
220      .rm-error.visible { display: block; }
221      .rm-ozon-note {
222        font-size: 12px;
223        color: #9ca3af;
224        margin-top: 14px;
225      }
226      .rm-section-title {
227        font-size: 13px;
228        font-weight: 600;
229        color: #6b7280;
230        margin: 0 0 8px 0;
231        text-transform: uppercase;
232        letter-spacing: .5px;
233      }
234    `;
235        document.head.appendChild(style);
236    }
237
238    // ── build UI ──────────────────────────────────────────────────────────────
239    function buildCalc() {
240        const wrap = document.createElement('div');
241        wrap.id = 'rm-calc';
242        wrap.innerHTML = `
243      <h2>Калькулятор цены поручения</h2>
244      <div class="rm-grid">
245        <div class="rm-field">
246          <label>Себестоимость, ₽</label>
247          <input id="rm-cost" type="number" min="0" placeholder="100" value="">
248        </div>
249        <div class="rm-field">
250          <label>% выкупа</label>
251          <input id="rm-buyout" type="number" min="1" max="100" placeholder="80" value="">
252        </div>
253        <div class="rm-field">
254          <label>Скидка от Озона, %</label>
255          <input id="rm-ozon-discount" type="number" min="0" max="100" placeholder="0" value="">
256        </div>
257        <div class="rm-field">
258          <label>% рекламных расходов</label>
259          <input id="rm-adv" type="number" min="0" placeholder="5" value="">
260        </div>
261        <div class="rm-field">
262          <label>Желаемая маржинальность, %</label>
263          <input id="rm-margin" type="number" min="0" placeholder="20" value="">
264        </div>
265      </div>
266      <button class="rm-btn" id="rm-calc-btn">Рассчитать</button>
267      <div class="rm-error" id="rm-error">Невозможно рассчитать: сумма комиссий и маржи превышает 100%. Уменьшите желаемую маржинальность или % рекламы.</div>
268      <div class="rm-result" id="rm-result">
269        <div class="rm-result-main" id="rm-price-out"></div>
270        <div class="rm-result-grid" id="rm-result-grid"></div>
271        <div class="rm-ozon-note" id="rm-ozon-note"></div>
272      </div>
273    `;
274        return wrap;
275    }
276
277    function fmt(n, decimals = 0) {
278        return n.toLocaleString('ru-RU', { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
279    }
280
281    function renderResult(res, ozon) {
282        const resultDiv = document.getElementById('rm-result');
283        const priceOut  = document.getElementById('rm-price-out');
284        const grid      = document.getElementById('rm-result-grid');
285        const note      = document.getElementById('rm-ozon-note');
286
287        priceOut.innerHTML = `Цена поручения: <b>${fmt(res.P, 2)}</b> <span>при маржинальности ${fmt(res.margin, 1)}%</span>`;
288
289        const items = [
290            { label: 'Цена реализации',          value: `${fmt(res.salePrice, 2)}`,        cls: '' },
291            { label: 'Прибыль за шт',            value: `${fmt(res.profit, 2)}`,            cls: res.profit >= 0 ? 'green' : 'red' },
292            { label: 'Маржинальность',           value: `${fmt(res.margin, 1)} %`,            cls: res.margin >= 0 ? 'green' : 'red' },
293            { label: 'ROM (прибыль/себест.)',     value: `${fmt(res.rom, 1)} %`,               cls: res.rom >= 0 ? 'green' : 'red' },
294            { label: '% себестоимости в цене',   value: `${fmt(res.costPct, 1)} %`,           cls: '' },
295            { label: 'Вознаграждение Ozon',      value: `${fmt(res.commRub, 2)} ₽ (${fmt(res.commPct, 1)}%)`, cls: 'red' },
296            { label: 'Эквайринг',               value: `${fmt(res.acqRub, 2)} ₽ (${fmt(res.acqPct, 1)}%)`,  cls: 'red' },
297            { label: 'Доставка (FBO)',           value: `${fmt(res.deliveryRub, 2)}`,       cls: 'red' },
298            { label: 'Налог',                    value: `${fmt(res.taxRub, 2)} ₽ (${fmt(res.taxPct, 1)}%)`,  cls: 'red' },
299            { label: 'Реклама',                  value: `${fmt(res.advRub, 2)}`,            cls: 'red' },
300            { label: 'Себест. на 1 выкуп',       value: `${fmt(res.costPerBuyout, 2)}`,     cls: '' },
301        ];
302
303        grid.innerHTML = items.map(it => `
304      <div class="rm-result-item">
305        <div class="rm-ri-label">${it.label}</div>
306        <div class="rm-ri-value ${it.cls}">${it.value}</div>
307      </div>
308    `).join('');
309
310        note.textContent = `Данные комиссий из таблицы Ozon: вознаграждение ${fmt(ozon.commPct * 100, 1)}%, эквайринг ${fmt(ozon.acqPct * 100, 1)}%, доставка ${fmt(ozon.deliveryRub, 0)} ₽, налог ${fmt(ozon.taxPct * 100, 2)}%`;
311
312        resultDiv.classList.add('visible');
313    }
314
315    // ── init ──────────────────────────────────────────────────────────────────
316    function init() {
317        injectStyles();
318
319        const calcSection = document.querySelector('._calculation_vz0jj_18');
320        if (!calcSection) {
321            console.error('[RM Calc] Не найдена секция калькулятора Ozon');
322            return;
323        }
324
325        const calcEl = buildCalc();
326        calcSection.parentNode.insertBefore(calcEl, calcSection.nextSibling);
327        console.log('[RM Calc] Калькулятор добавлен на страницу');
328
329        document.getElementById('rm-calc-btn').addEventListener('click', function () {
330            const cost         = parseFloat(document.getElementById('rm-cost').value);
331            const buyoutPct    = parseFloat(document.getElementById('rm-buyout').value);
332            const ozonDiscount = parseFloat(document.getElementById('rm-ozon-discount').value) || 0;
333            const advPct       = parseFloat(document.getElementById('rm-adv').value) || 0;
334            const targetMargin = parseFloat(document.getElementById('rm-margin').value);
335
336            const errorDiv  = document.getElementById('rm-error');
337            const resultDiv = document.getElementById('rm-result');
338
339            errorDiv.classList.remove('visible');
340            resultDiv.classList.remove('visible');
341
342            if (isNaN(cost) || isNaN(buyoutPct) || isNaN(targetMargin)) {
343                errorDiv.textContent = 'Заполните обязательные поля: себестоимость, % выкупа, желаемая маржинальность.';
344                errorDiv.classList.add('visible');
345                return;
346            }
347
348            const ozon = readOzonTable();
349            if (!ozon) {
350                errorDiv.textContent = 'Не удалось прочитать данные из таблицы Ozon. Убедитесь, что таблица загружена.';
351                errorDiv.classList.add('visible');
352                return;
353            }
354
355            console.log('[RM Calc] Данные Ozon:', ozon);
356            console.log('[RM Calc] Входные данные:', { cost, buyoutPct, ozonDiscount, advPct, targetMargin });
357
358            const res = calculate({ cost, buyoutPct, ozonDiscount, advPct, targetMargin }, ozon);
359
360            if (!res) {
361                errorDiv.textContent = 'Невозможно рассчитать: сумма комиссий и маржи превышает 100%. Уменьшите желаемую маржинальность или % рекламы.';
362                errorDiv.classList.add('visible');
363                return;
364            }
365
366            console.log('[RM Calc] Результат:', res);
367            renderResult(res, ozon);
368        });
369    }
370
371    // Wait for the Ozon table to be rendered
372    function waitForTable() {
373        const t = document.querySelector('table.ozi__table__table__HAe8A');
374        if (t && !document.getElementById('rm-calc')) {
375            init();
376        } else if (!document.getElementById('rm-calc')) {
377            setTimeout(waitForTable, 1000);
378        }
379    }
380
381    if (document.readyState === 'loading') {
382        document.addEventListener('DOMContentLoaded', waitForTable);
383    } else {
384        waitForTable();
385    }
386
387})();