This commit enhances the stock item management by introducing automatic calculation of selling price and markup based on the cost price. Key changes include: - Integration of `applyDefaultMarkupToPatch` to automatically set `selling_price` and `markup` at 80% of the cost price when they are not explicitly provided during stock item creation and updates. - Updates to the `create` and `update` functions in the stock items service to ensure that existing values are respected and not overwritten if they are already set by the operator. - Tests added to verify the correct behavior of auto-markup during both creation and updates of stock items, ensuring that the functionality works as intended across various scenarios. This feature aims to streamline the pricing process for stock items, reducing manual input and potential errors.
131 lines
5.3 KiB
JavaScript
131 lines
5.3 KiB
JavaScript
// Run in the browser console on nssfasteners.co.za after login.
|
|
// Fetches all Grade 8 (101 items) + A2 (21 items) hex nut DIN934 product pages,
|
|
// parses the hidden unit prices out of the HTML, builds PosDescription strings,
|
|
// and triggers a CSV download.
|
|
|
|
(async () => {
|
|
const BASE = 'https://nssfasteners.co.za/nuts/hex-nuts/hex-nut-din934.html';
|
|
const FILTERS = [
|
|
{ grade: '8', gradeId: '771', expectedCount: 101 },
|
|
{ grade: 'A2', gradeId: '773', expectedCount: 21 },
|
|
];
|
|
|
|
// ---- helpers ----
|
|
const fetchHtml = async (url) => {
|
|
const res = await fetch(url, { credentials: 'include' });
|
|
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
return await res.text();
|
|
};
|
|
|
|
const parsePage = (html) => {
|
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
const items = Array.from(doc.querySelectorAll('.product-item'));
|
|
return items.map(item => {
|
|
const titleEl = item.querySelector('.items-center.justify-center.text-primary');
|
|
const title = titleEl ? titleEl.textContent.trim() : '';
|
|
const sku = title.includes(' - ') ? title.split(' - ').pop().trim() : '';
|
|
const linkEl = item.querySelector('a.product-item-photo, a.product.photo');
|
|
const url = linkEl ? linkEl.getAttribute('href') : '';
|
|
|
|
// pull the description block (Material / Diameter / Thread / Grade / Finish / Pack)
|
|
const block = (item.textContent || '').replace(/\s+/g, ' ');
|
|
const get = (label) => {
|
|
const m = block.match(new RegExp(label + '\\s*:?\\s*([^\\s].*?)(?=\\s+(?:Diameter|Thread|Grade|Finish|Sold|Metric|Hi-Tensile|Stainless|Mild|Brass|$))', 'i'));
|
|
return m ? m[1].trim() : '';
|
|
};
|
|
|
|
const material = /Hi-Tensile/i.test(block) ? 'Hi-Tensile'
|
|
: /Stainless Steel/i.test(block) ? 'Stainless Steel'
|
|
: /Mild Steel/i.test(block) ? 'Mild Steel'
|
|
: /Brass/i.test(block) ? 'Brass'
|
|
: '';
|
|
const diameter = get('Diameter');
|
|
const thread = get('Thread');
|
|
const grade = get('Grade');
|
|
const finish = get('Finish');
|
|
const pack = /Sold as a Single Piece/i.test(block) ? 'Single Piece'
|
|
: /Sold as a Box/i.test(block) ? 'Box'
|
|
: /Sold as a Pack/i.test(block) ? 'Pack'
|
|
: '';
|
|
|
|
// Unit price is in the HTML but hidden with display:none
|
|
const unitPriceEl = item.querySelector('.unit-price');
|
|
const priceMatch = unitPriceEl ? unitPriceEl.textContent.match(/R\s*([\d.]+)/) : null;
|
|
const unitPrice = priceMatch ? priceMatch[1] : '';
|
|
|
|
// Description is the parent product type (e.g. "Hex Nut DIN934")
|
|
const desc = title.includes(' - ') ? title.split(' - ')[0].trim() : '';
|
|
|
|
// Build PosDescription like: "M39 NUT 8 PLN"
|
|
// diameter -> "M{value}", normalised
|
|
const sizeMatch = diameter.match(/(\d+(?:\.\d+)?)\s*mm/i);
|
|
const size = sizeMatch ? 'M' + sizeMatch[1].replace(/\.0$/, '') : diameter;
|
|
const gradeShort = grade === 'Stainless' ? '' : grade; // "8" or "A2"
|
|
const posDescription = [size, 'NUT', gradeShort, finish.toUpperCase() === 'PLAIN' ? 'PLN' : finish.toUpperCase()]
|
|
.filter(Boolean).join(' ');
|
|
|
|
return {
|
|
sku,
|
|
posDescription,
|
|
description: desc,
|
|
material,
|
|
grade,
|
|
diameter,
|
|
thread,
|
|
finish,
|
|
pack,
|
|
unitPrice: unitPrice ? 'R ' + unitPrice : '',
|
|
url,
|
|
};
|
|
});
|
|
};
|
|
|
|
const allRows = [];
|
|
for (const f of FILTERS) {
|
|
// First page
|
|
const firstUrl = `${BASE}?gradematerial=${f.gradeId}`;
|
|
const firstHtml = await fetchHtml(firstUrl);
|
|
const firstRows = parsePage(firstHtml);
|
|
allRows.push(...firstRows);
|
|
|
|
// Discover total page count from toolbar
|
|
const doc = new DOMParser().parseFromString(firstHtml, 'text/html');
|
|
const m = (doc.querySelector('.toolbar-amount')?.textContent || '').match(/of\s+(\d+)/);
|
|
const total = m ? parseInt(m[1], 10) : f.expectedCount;
|
|
const pages = Math.ceil(total / 10);
|
|
console.log(`Grade ${f.grade}: ${firstRows.length} items on page 1, ${pages} pages total, expected ${total}`);
|
|
|
|
// Remaining pages
|
|
for (let p = 2; p <= pages; p++) {
|
|
const html = await fetchHtml(`${BASE}?gradematerial=${f.gradeId}&p=${p}`);
|
|
const rows = parsePage(html);
|
|
allRows.push(...rows);
|
|
console.log(`Grade ${f.grade}: page ${p}/${pages} -> ${rows.length} items`);
|
|
}
|
|
}
|
|
|
|
console.log(`TOTAL ROWS: ${allRows.length}`);
|
|
|
|
// Build CSV
|
|
const headers = ['SKU','PosDescription','Description','Material','Grade','Diameter','Thread','Finish','Pack','UnitPrice','SourceURL'];
|
|
const esc = (v) => {
|
|
const s = (v ?? '').toString().replace(/"/g, '""');
|
|
return /[",\n]/.test(s) ? `"${s}"` : s;
|
|
};
|
|
const csv = [headers.join(',')]
|
|
.concat(allRows.map(r => headers.map(h => esc(r[h.toLowerCase()])).join(',')))
|
|
.join('\n');
|
|
|
|
// Trigger download
|
|
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'nss-hex-nut-din934-grade8-a2.csv';
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
return { count: allRows.length, sample: allRows.slice(0, 3) };
|
|
})();
|