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})();