Բոլոր դասերը
Էլեկտրոնային առևտուրԱռաջադեմ13 րոպե ընթերցումԻմ ստեկում

Պահեստ և ERP սինքրոնիզացիա

Պահեստներ, մնացորդի շարժեր, ռեզերվներ, գնման պատվերներ և խանութի համաձայնեցում հաշվապահության հետ։

  • Warehouses
  • Stock movements
  • ERP sync
  • Fiscal receipts
Քայլ 1/5
Դարակի վերջին ապրանքը։ Երկու գնորդ նույն պահին սեղմում են «գնել»։ Հաղթողը միայն մեկն է։

30 վայրկյանում

Պահեստային հաշվառումը պատասխանում է պարզ հարցի՝ քանի՞սը կարող ենք վաճառել և որտե՞ղ են դրանք, որը բարդանում է, երբ կան մի քանի պահեստ, մատակարարներ, վերադարձներ և հաշվապահական համակարգ, որը նույնպես կարծում է, թե գիտի պատասխանը։ Հուսալի մոտեցումն է՝ մնացորդի յուրաքանչյուր փոփոխություն գրանցել որպես շարժ, ռեզերվել մնացորդը պատվերների համար և սինքրոնացնել ERP-ի հետ պլանավորված, դիտարկելի job-երով՝ հստակ կանոններով, թե որ համակարգին է պատկանում որ տվյալը։

Ինչու եմ օգտագործում

  • Ես օգտագործում եմ շարժերի վրա հիմնված հաշվառում և պլանավորված sync job-եր, որովհետև կառուցել եմ մանրածախ վաճառողի պահեստային հաշվառումը և դրա սինքրոնիզացիան հաշվապահական/ERP համակարգի և ֆիսկալ կտրոնների հետ։
  • Երբ մնացորդը սխալ է, բոլորը տուժում են՝ հաճախորդները գնում են գոյություն չունեցող ապրանքներ, իսկ պահեստներում կա ապրանք, որը ոչ ոք չի տեսնում առցանց։
  • Հաշվապահական համակարգերը գների, հաճախորդների և վաճառքների իրավական գրառումն են։ Խանութը պետք է համաձայն լինի դրանց հետ, նույնիսկ երբ դրանց API-ները դանդաղ են կամ չեն աշխատում։
  • Ֆիսկալ կտրոնները շատ երկրներում իրավական պահանջ են․ դրանց ձախողումները պարզապես bug-եր չեն, այլ compliance-ի խնդիրներ։

Ինչպես է աշխատում

01Պահեստներ, մնացորդներ և շարժեր

Պահեստը ցանկացած տեղ է, որտեղ ապրանք կա՝ կենտրոնական պահեստ, խանութի մասնաճյուղ, մատակարարի dropship մնացորդ։ Մնացորդը (stock level) մեկ տարբերակի ընթացիկ քանակն է մեկ պահեստում։ Հիմնական գաղափարն այն է, որ դուք երբեք պարզապես չեք վերագրում այդ թիվը։ Յուրաքանչյուր փոփոխություն մնացորդի շարժ է՝ տող, որն ասում է «+20 ստացվել է 118 գնման պատվերից», «−2 վաճառվել է 5531 պատվերում», «−1 վնասված», «+3 հաշվառման ուղղում»։ Մնացորդը շարժերի գումարն է՝ արագության համար cache-ավորված։ Ինչպես բանկային քաղվածքը, շարժերի պատմությունը բացատրում է յուրաքանչյուր թիվ, թույլ է տալիս վերակառուցել մնացորդները bug-ից հետո և տալիս է հաշվետվություններ՝ պահեստի արժեքավորում, «մեռած» մնացորդ և sell-through։ Գույքագրումները (stocktake) համեմատում են ֆիզիկական հաշվարկը համակարգի հետ և տարբերության համար ստեղծում ուղղիչ շարժեր։

sql
CREATE TABLE stock_movements (
  id            BIGSERIAL PRIMARY KEY,
  variant_id    BIGINT NOT NULL,
  warehouse_id  BIGINT NOT NULL,
  quantity      INTEGER NOT NULL,          -- positive in, negative out
  reason        TEXT NOT NULL,             -- sale, receipt, transfer_out, count, waste
  reference     TEXT,                      -- order or purchase order id
  created_by    BIGINT,
  created_at    TIMESTAMPTZ NOT NULL DEFAULT now()
);
 
-- Rebuild or verify cached levels from history
SELECT variant_id, warehouse_id, SUM(quantity) AS on_hand
FROM stock_movements
GROUP BY variant_id, warehouse_id;

02Հասանելի մնացորդ և ռեզերվներ

«On hand»-ը այն է, ինչ ֆիզիկապես պահեստում է։ «Available»-ը այն է, ինչ դեռ կարող եք խոստանալ՝ on hand-ը հանած ռեզերվը։ Ռեզերվը ստեղծվում է, երբ պատվերը տեղադրվում և վճարվում է (կամ մինչ վճարումը pending է՝ ժամկետով), և դառնում է իրական ելքային շարժ, երբ պատվերը հավաքվում և ուղարկվում է։ Եթե պատվերը չեղարկվում է կամ վճարումը ձախողվում է, ռեզերվն ազատվում է։ Storefront-ը պետք է ցույց տա հասանելի մնացորդը՝ գումարված այն պահեստներով, որոնք կարող են սպասարկել հաճախորդին։ Fulfilment-ի կանոններն ընտրում են աղբյուրը՝ մոտակա մասնաճյուղ, գլխավոր պահեստ, մատակարար, որն ուղիղ է առաքում, կամ backorder (վաճառել հիմա, առաքել համալրումից հետո)։ Այս կանոնները դարձրեք բացահայտ, որովհետև «ո՞ր պահեստն է սա ուղարկում» հարցը ազդում է առաքման ժամկետի, արժեքի և հաշվապահության վրա։

03Տեղափոխումներ, գնման պատվերներ և մատակարարներ

Տեղափոխումը (transfer) մնացորդը պահեստների միջև շարժում է երկու քայլով՝ ելքային շարժ, երբ այն դուրս է գալիս, և մուտքային, երբ հասնում է։ Այդ ընթացքում ապրանքը «ճանապարհին է» և չի պատկանում ոչ մի դարակի․ մոդելավորեք այդ վիճակը, այլապես մնացորդը կարծես անհետանում է։ Գնման պատվերները (PO) այն միջոցն են, որով մնացորդը մուտք է գործում․ դուք պատվիրում եք մատակարարից, PO-ն ունի սպասվող քանակներ և արժեքներ, իսկ ապրանքի ընդունումները (goods receipts) գրանցում են իրականում ժամանածը, որը կարող է լինել մասնակի կամ տարբեր։ Յուրաքանչյուր ընդունում ստեղծում է մուտքային շարժեր և թարմացնում ինքնարժեքը, որը սնում է մարժայի և արժեքավորման հաշվետվությունները։ Լավ տվյալներով կարող եք գեներացնել վերապատվիրման առաջարկներ՝ միջին օրական վաճառքը բազմապատկած մատակարարի lead time-ով՝ գումարած պահուստային մնացորդը, համեմատած հասանելի և ճանապարհին գտնվող մնացորդի հետ։

  • PO-ների կարգավիճակներ՝ draft → sent → partially received → received → closed։
  • Ընդունման պահին գրանցեք միավորի արժեքը․ մարժաները կախված են դրանից։
  • Հետևեք մատակարարների արդյունավետությանը՝ ուշացած առաքումներ և քանակների տարբերություններ։

04Սինքրոնիզացիա հաշվապահության և ERP-ի հետ

ERP-ն (enterprise resource planning) կամ հաշվապահական համակարգը բիզնեսի back office-ն է՝ ապրանքներ, գներ, հաճախորդներ, հաշիվ-ապրանքագրեր և մնացորդ՝ հարկային նպատակներով։ Նախ որոշեք սեփականությունն ըստ տվյալի տեսակի։ Տարածված բաժանում՝ ERP-ին են պատկանում ապրանքների կոդերը, գները և պաշտոնական մնացորդը, խանութին՝ նկարագրությունները, նկարները, SEO-ն և առցանց պատվերները։ Հետո սինքրոնացրեք սեփականության ուղղությամբ։ ERP-ների API-ների մեծ մասը դանդաղ է կամ batch-ի վրա կողմնորոշված, ուստի սինքրոնացրեք պլանավորված job-երով՝ փոխված գներն ու մնացորդը քաշեք մի քանի րոպեն մեկ, նոր պատվերներն ու վերադարձները push արեք դրանց կատարվելուն պես՝ queue-ի միջոցով, իսկ ամեն գիշեր կատարեք ամբողջական համեմատություն։ Յուրաքանչյուր job դարձրեք idempotent՝ համընկեցնելով կայուն արտաքին ID-ներով, մշակեք batch-երով և պահեք sync log՝ քանակներով և սխալներով։ Թույլ տվեք ադմիններին կարգավորումներից միացնել և անջատել առանձին job-երը․ դա անգնահատելի է ERP-ի սպասարկման ժամանակ։

php
// Laravel scheduler: each sync is a separate, non-overlapping job
use Illuminate\Support\Facades\Schedule;
 
Schedule::job(new SyncPricesFromErp)->everyFiveMinutes()
    ->withoutOverlapping()->when(fn () => settings('erp.sync_prices'));
 
Schedule::job(new SyncStockFromErp)->everyTenMinutes()
    ->withoutOverlapping()->when(fn () => settings('erp.sync_stock'));
 
Schedule::job(new CompareCatalogueWithErp)->dailyAt('03:00')
    ->withoutOverlapping();
 
Schedule::job(new CheckErpHealth)->everyFiveMinutes(); // alerts admins on failures

05Կոնֆլիկտներ, health check-եր և անվտանգության ցանցեր

Sync-երը ձախողվում են լուռ ձևերով՝ ERP-ն չի աշխատում, ապրանքի կոդը վերանվանվել է, գների import-ը զրո է պարունակում։ Պաշտպանեք ձեզ։ Health check-երը հետևում են յուրաքանչյուր job-ի վերջին հաջող գործարկմանը և alert են ուղարկում, երբ այն չափազանց հին է կամ սխալների տոկոսը կտրուկ աճում է։ Sanity կանոնները մերժում են կասկածելի փոփոխությունները՝ գնի 90% անկում, ամբողջ մնացորդի զրոյացում, և պահում դրանք վերանայման համար։ Պահեք նախորդ արժեքները, որպեսզի ադմինը կարողանա մեկ click-ով հետ շրջել գների վատ import-ը։ Կոնֆլիկտների դեպքում հաղթում է սեփականատերը․ եթե երկու կողմն էլ փոխել են դաշտը, վերցրեք արժեքը այն համակարգից, որին այն պատկանում է, և log արեք վերագրումը։ Գիշերային համեմատության հաշվետվությունը ցուցակագրում է այն ապրանքները, որոնք կան միայն մի կողմում կամ տարբերվում են, որպեսզի մարդիկ տվյալներն ուղղեն աղբյուրում՝ խանութը patch անելու փոխարեն։

06Ֆիսկալ կտրոններ

Շատ երկրներում յուրաքանչյուր վաճառք պետք է գրանցվի հարկային մարմնում ֆիսկալ սարքի կամ cloud ծառայության միջոցով, որը վերադարձնում է ֆիսկալ կտրոնի համար (հաճախ տպված կամ ցույց տրված որպես QR կոդ)։ Վերադարձներին պետք են իրենց սեփական ֆիսկալ վերադարձի կտրոնները՝ կապված սկզբնականի հետ։ Վերաբերվեք սրան ինչպես վճարային ինտեգրացիային՝ ստեղծեք կտրոնի գրառում կարգավիճակով, ուղարկեք այն queue-ի միջոցով retry-ներով, պահեք ֆիսկալ համարը պատվերի վրա և alert ուղարկեք ձախողումների դեպքում։ Retry արեք միայն այն ժամանակ, երբ դա անվտանգ է․ եթե ծառայությունը կարող էր գրանցել կտրոնը մինչև timeout-ը, նախ ստուգեք՝ կրկնօրինակներից խուսափելու համար։ Մասնաճյուղերը կամ դրամարկղերը, որոնք կարող են աշխատել offline, պետք է ունենան offline ռեժիմ, որը կտրոնները հերթագրում է և ուղարկում կապը վերականգնվելուն պես՝ օրենքով թույլատրված ժամկետում։

Օգտագործեք, երբ

  • Մնացորդը գտնվում է մեկից ավելի տեղում, կամ առցանց և offline վաճառքները կիսում են մնացորդը։
  • Բիզնեսն արդեն օգտագործում է հաշվապահական կամ ERP համակարգ, որը պետք է մնա գների և մնացորդի ճշմարտության աղբյուրը։
  • Չունեցած ապրանքը վաճառելը թանկ է՝ արագ փչացող ապրանքներ, սահմանափակ թողարկումներ, մատակարարի dropship։
  • Տեղական օրենքը պահանջում է յուրաքանչյուր վաճառքի ֆիսկալ գրանցում։

Մի՛ օգտագործեք, երբ

  • Պատվերով պատրաստվող կամ թվային ապրանքներ, որոնց մնացորդը հետևելու կարիք չկա։
  • Մեկ փոքր պահեստ, որտեղ ատոմար update-ներով պարզ քանակի սյունակը բավարար է։
  • Երբ ERP-ն առաջարկում է պատրաստի, սպասարկվող connector ձեր պլատֆորմի համար․ օգտագործեք այն՝ նախքան սեփականը գրելը։

Այլընտրանքներ

  • Խանութի պլատֆորմի հաշվառում

    Shopify-ն և WooCommerce-ը հետևում են մնացորդին ըստ location-ի․ նորմալ է պարզ դեպքերի համար, թույլ՝ գնման պատվերների և ինքնարժեքի համար։

  • Հաշվառման / WMS առանձին գործիքներ

    Պահեստի կառավարման համակարգերը մշակում են հավաքումը, բջիջները և տեղափոխումները․ խանութը սինքրոնանում է դրանց հետ՝ մնացորդին տիրապետելու փոխարեն։

  • ERP-ն որպես մնացորդի միակ աղբյուր

    Խանութը մնացորդը կարդում է ERP-ից գրեթե իրական ժամանակում․ սեփականության ամենապարզ մոդելն է, բայց յուրաքանչյուր outage արգելափակում է վաճառքները։

Լավագույն պրակտիկաներ

  1. Մնացորդի յուրաքանչյուր փոփոխություն գրանցեք որպես շարժ՝ պատճառով և reference-ով։
  2. Առանձնացրեք on hand, ռեզերված և հասանելի մնացորդը։
  3. Որոշեք տվյալների սեփականությունն ըստ դաշտի՝ նախքան sync-ի որևէ կոդ գրելը։
  4. Sync-երն աշխատեցրեք որպես idempotent, չհամընկնող պլանավորված job-եր՝ արտաքին ID-ներով։
  5. Մոնիտորինգ արեք յուրաքանչյուր job-ի վերջին հաջողության ժամանակը և alert ուղարկեք հնացած կամ ձախողվող sync-երի դեպքում։
  6. Մերժեք կասկածելի զանգվածային փոփոխությունները և պահեք նախորդ արժեքները՝ հետ շրջելու համար։
  7. Ֆիսկալ կտրոնները հերթագրեք անվտանգ retry-ներով և պահեք դրանց համարները պատվերի վրա։

Տարածված սխալներ

  • Մնացորդի քանակներն ուղղակիորեն վերագրել՝ կորցնելով պատմությունը, թե ինչու են դրանք փոխվել։
  • Երկկողմանի sync առանց սեփականության կանոնների, որի պատճառով համակարգերը ցիկլով վերագրում են միմյանց։
  • Sync job-եր, որոնք համընկնում են և նույն batch-ը երկու անգամ մշակում։
  • Sync-ի լուռ ձախողումներ, որոնք նկատվում են միայն այն ժամանակ, երբ հաճախորդները բողոքում են գներից։
  • Տեղափոխումների ժամանակ անտեսել ճանապարհին գտնվող մնացորդը։

Խորը վերլուծություն

07Համաձայնեցվածություն շարժերի և cache արված մնացորդների միջև

Շարժերը պատմությունն են, իսկ մնացորդների մակարդակները՝ cache, ուստի դրանք երբեք չպետք է շեղվեն միմյանցից։ Գրեք երկուսն էլ նույն transaction-ում. insert արեք շարժը և թարմացրեք մակարդակը պայմանական հրամանով, որը չի կարող բացասական դառնալ, եթե backorder-ները թույլատրված չեն։ Lock-երի հերթականությունը կարևոր է, երբ մեկ գործողությունը դիպչում է մի քանի տողի, օրինակ՝ տեղափոխումը կամ բազմատող պատվերը. միշտ lock արեք տողերը նույն հերթականությամբ (օրինակ՝ ըստ variant_id-ի, ապա warehouse_id-ի), որպեսզի խուսափեք deadlock-ներից։ Պահեք գիշերային ստուգման job, որը գումարում է շարժերը յուրաքանչյուր տարբերակի և պահեստի համար և համեմատում cache արված մակարդակի հետ. ցանկացած տարբերություն bug է, որը պետք է ուսումնասիրել, այլ ոչ թե լուռ ուղղել։ Գրառումների մեծ ծավալների դեպքում շարժերը կարելի է partition անել ըստ ամիսների, իսկ հին ամիսները ամփոփել սկզբնական մնացորդների մեջ։

sql
BEGIN;
-- 1. Decrease the cached level only if enough is available
UPDATE stock_levels
SET on_hand = on_hand - 3
WHERE variant_id = 42 AND warehouse_id = 1 AND on_hand - reserved >= 3;
-- 0 rows → ROLLBACK and report out of stock
 
-- 2. Record why it changed
INSERT INTO stock_movements (variant_id, warehouse_id, quantity, reason, reference)
VALUES (42, 1, -3, 'sale', 'order:5531');
COMMIT;

08Պաշարների գնահատում և ինքնարժեքի մեթոդներ

Մնացորդը դարակների վրա դրված գումար է, ուստի ֆինանսական բաժինը հարցնում է, թե որքան արժե այն և ինչ ինքնարժեք ուներ յուրաքանչյուր վաճառք։ Դա կախված է ինքնարժեքի մեթոդից։ Weighted average cost-ը յուրաքանչյուր մուտքի ժամանակ վերահաշվարկում է միավորի ինքնարժեքը. (հին քանակ × հին ինքնարժեք + ստացված քանակ × նոր ինքնարժեք) / ընդհանուր քանակ։ Այն պարզ է և տարածված ERP-ներում։ FIFO-ն (first in, first out) ենթադրում է, որ ամենահին միավորները վաճառվում են առաջինը, ուստի յուրաքանչյուր վաճառք սպառում է ինքնարժեքի շերտերը ամենահին մուտքից. դա պահանջում է շերտեր յուրաքանչյուր մուտքի համար, բայց ճշգրիտ է արտացոլում աճող գները։ Ինչ էլ ընտրեք, այն պետք է համապատասխանի ERP-ին կամ հաշվապահական համակարգին, հակառակ դեպքում խանութում և հաշվապահությունում մարժաները չեն համընկնի։ Ինքնարժեքը գրանցեք վաճառքի շարժի վրա վաճառքի պահին, ճիշտ ինչպես գների snapshot-ները պատվերներում, որպեսզի հետագա մուտքերը չվերագրեն պատմական մարժաները։

  • Weighted average. մեկ ինքնարժեք յուրաքանչյուր տարբերակի և պահեստի համար, թարմացվում է մուտքի ժամանակ։
  • FIFO. ինքնարժեքի շերտեր յուրաքանչյուր մուտքի համար, սպառվում են ամենահիններից սկսած։
  • Վերադարձները մնացորդ են մտնում այն ինքնարժեքով, որով դուրս են եկել, այլ ոչ թե այսօրվա ինքնարժեքով։

09Ինտեգրացիայի pattern-ներ. polling, event-ներ և outbox

Մեկ այլ համակարգում փոփոխությունների մասին իմանալու երկու եղանակ կա. polling (ժամանակացույցով հարցնել «ի՞նչ է փոխվել T պահից հետո») և event-ներ (մյուս համակարգը push է անում webhook-ներ կամ message-ներ)։ Շատ ERP-ներ աջակցում են միայն polling։ Poll արեք watermark-ով՝ ձեր մշակած վերջին updated timestamp-ով կամ change ID-ով, որը պահվում է յուրաքանչյուր job-ի համար, և պատուհանը թեթևակի համընկնող դարձրեք՝ ժամացույցների շեղումը հանդուրժելու համար՝ հենվելով idempotent upsert-ների վրա, որպեսզի կրկնակի ընթերցումները անվնաս լինեն։ Ձեր ուղարկած տվյալների համար օգտագործեք transactional outbox pattern-ը. պատվերի հետ նույն database transaction-ում insert արեք տող outbox աղյուսակում, իսկ worker-ը կարդում է outbox-ը և ուղարկում ERP, կրկնելով մինչև հաստատում ստանալը։ Սա երաշխավորում է, որ commit արված պատվերը վերջիվերջո export կլինի, իսկ rollback արվածը՝ երբեք, առանց distributed transaction-ների։

sql
CREATE TABLE outbox (
  id           BIGSERIAL PRIMARY KEY,
  topic        TEXT NOT NULL,        -- erp.order_created, erp.return_created
  payload      JSONB NOT NULL,
  attempts     INT NOT NULL DEFAULT 0,
  sent_at      TIMESTAMPTZ,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);
 
-- Worker: claim a batch without blocking other workers
SELECT id, topic, payload FROM outbox
WHERE sent_at IS NULL
ORDER BY id
LIMIT 50
FOR UPDATE SKIP LOCKED;

10Բազմապահեստային բաշխում և բաժանված առաքումներ

Մի քանի պահեստի դեպքում յուրաքանչյուր պատվեր փոքր օպտիմալացման խնդիր է. ո՞ր վայրերը պետք է կատարեն որ տողերը։ Տարածված կանոններ՝ առաջնահերթության կարգով. ամեն ինչ առաքել մեկ վայրից, եթե հնարավոր է (մեկ ծանրոցն ավելի էժան է), նախընտրել հաճախորդին ամենամոտ վայրը, խուսափել մասնաճյուղի ցուցափեղկի մնացորդը սպառելուց և մատակարարի dropship-ը կամ backorder-ը օգտագործել միայն որպես վերջին միջոց։ Կանոնները գրեք որպես scoring function և որոշումը գրանցեք պատվերի վրա, որպեսզի պահեստի աշխատակիցներն ու support-ը տեսնեն, թե ինչու է ընտրվել տվյալ վայրը։ Բաժանված առաքումներին պետք են իրենց սեփական գրառումները. մեկ պատվեր, մի քանի shipment, յուրաքանչյուրը՝ տողերով, tracking-ով և կարգավիճակով, իսկ պատվերը դառնում է առաքված միայն այն ժամանակ, երբ բոլոր shipment-ները առաքված են։ Ռեզերվները պետք է արվեն յուրաքանչյուր պահեստի համար checkout-ի ժամանակ, ոչ թե ավելի ուշ, հակառակ դեպքում բաշխումը կարող է ձախողվել այն բանից հետո, երբ հաճախորդն արդեն վճարել է։

Իրական իրավիճակներ

01

Գների import-ը կատալոգի կեսի գինը դարձնում է 0.01

Խնդիր

ERP թիմը գիշերը փոխում է գնացուցակի ֆորմատը։ 5 րոպեանոց գների sync-ը import է անում հազարավոր ապրանքներ 0.01 գնով, և լուսաբացին հաճախորդները սկսում են պատվիրել դրանք։

Ինչպես լուծել

Կանգնեցրեք վնասը. անջատեք գների sync-ը admin-ի կարգավորումներից, վերականգնեք գները պահված նախորդ արժեքներից և բիզնեսի հետ որոշեք, թե ինչպես վարվել սխալ գներով կատարված պատվերների հետ՝ պայմանների համաձայն։ Ապա ավելացրեք պաշտպանություններ, որպեսզի դա չկրկնվի. մերժեք batch-ը, եթե ապրանքների կարգավորված մասնաբաժնից ավելին փոխվում է, ասենք, 50%-ից ավելի, մի ընդունեք զրոյական կամ գրեթե զրոյական գներ, եթե դա հստակ թույլատրված չէ, և մերժված batch-երը պահեք մարդու հաստատման համար՝ diff report-ով։ Վալիդացրեք payload-ի schema-ն, որպեսզի ֆորմատի փոփոխությունը բարձրաձայն ձախողվի, այլ ոչ թե parse լինի անհեթեթության։ Alert ուղարկեք հերթապահին, երբ batch-ը պահվում է։

02

Առցանց մնացորդը 5 է, դարակը՝ դատարկ

Խնդիր

Մասնաճյուղի աշխատակիցները պարբերաբար չեղարկում են առցանց pickup պատվերները, որովհետև որպես առկա ցուցադրված ապրանքները ֆիզիկապես տեղում չեն։ Հաճախորդները կորցնում են վստահությունը ցուցադրվող մնացորդի նկատմամբ։

Ինչպես լուծել

Գտեք, թե որտեղ են իրականությունն ու համակարգը շեղվում։ Սովորական պատճառները. խանութում կատարված վաճառքները գրանցվում են ERP-ում, բայց sync են արվում միայն ժամը մեկ, վնասված կամ գողացված ապրանքները երբեք չեն գրանցվում, և ցուցափեղկի մնացորդը հաշվվում է որպես վաճառելի։ Կրճատեք մնացորդի sync-ի ինտերվալը կամ խանութի վաճառքները push արեք որպես event-ներ, ավելացրեք դուրսգրման շարժի տեսակ, որը աշխատակիցները կարող են գրանցել հեռախոսից, և ներմուծեք cycle count-ներ՝ ամեն օր մի քանի ապրանք հաշվել տարեկան գույքագրման փոխարեն։ Առցանց հասանելիությունը ցույց տվեք յուրաքանչյուր մասնաճյուղի համար safety buffer-ով (հասանելին՝ հանած մեկ կամ երկու արագ վաճառվող ապրանքների համար)։ Հետևեք չեղարկման պատճառներին՝ ըստ մասնաճյուղերի. հաշվետվությունը արագ ցույց է տալիս, թե որ վայրերին է պետք գործընթացի փոփոխություն, այլ ոչ թե կոդի։

03

ERP API-ն սահմանափակված է րոպեում 60 հարցումով

Խնդիր

40,000 ապրանքների կատալոգի ամբողջական համեմատությունը տևում է ժամեր և արգելափակում է պատվերների export-ը, որն օգտագործում է նույն API quota-ն։

Ինչպես լուծել

Quota-ն օգտագործեք գիտակցաբար։ Պարզեք՝ արդյոք ERP-ն առաջարկում է bulk կամ delta endpoint-ներ, օրինակ՝ export ֆայլեր կամ «changed since» query-ներ, որոնք շատ ավելի քիչ հարցումներ են պահանջում, քան ապրանք առ ապրանք կանչերը։ ERP-ի բոլոր կանչերը անցկացրեք մեկ client-ով՝ ընդհանուր token-bucket rate limiter-ով, և պատվերների ու վերադարձների export-ներին առաջնահերթություն տվեք background համեմատության նկատմամբ, որպեսզի եկամտի համար կրիտիկական տվյալները միշտ անցնեն։ Ամբողջական համեմատությունը գործարկեք գիշերը, էջերով, վերջին մշակված էջից շարունակելու հնարավորությամբ։ Cache արեք չփոխվող reference տվյալները։ Չափեք quota-ի օգտագործումը յուրաքանչյուր job-ի համար, և եթե բյուջեն դեռ չափազանց փոքր է, դա կոնկրետ փաստարկ է ERP vendor-ի հետ ավելի բարձր սահմանի շուրջ բանակցելու համար։

Արագ ուղեցույց

Մնացորդների վերակառուցում շարժերից
SELECT variant_id, warehouse_id, SUM(quantity) FROM stock_movements GROUP BY 1, 2;
Outbox տողերի անվտանգ վերցնում
SELECT ... FROM outbox WHERE sent_at IS NULL ORDER BY id LIMIT 50 FOR UPDATE SKIP LOCKED;
Idempotent upsert ըստ արտաքին ID-ի
INSERT INTO products (erp_id, price) VALUES (:id, :price) ON CONFLICT (erp_id) DO UPDATE SET price = EXCLUDED.price;
Չհամընկնող job մեկ սերվերի վրա (Laravel)
Schedule::job(new SyncStockFromErp)->everyTenMinutes()->withoutOverlapping()->onOneServer();
Weighted average cost մուտքի ժամանակ
new_cost = (qty_old * cost_old + qty_in * cost_in) / (qty_old + qty_in)
Վերապատվիրման կետ
reorder_point = avg_daily_sales * lead_time_days + safety_stock
Հնացած sync-ի ստուգում
SELECT job FROM sync_runs GROUP BY job HAVING MAX(finished_at) < now() - interval '30 minutes';
Մեծ աղյուսակների մշակում chunk-երով (Laravel)
Product::query()->chunkById(500, fn ($products) => $this->compare($products));

Իմ իրական նախագծերում

  • Կենդանիների ապրանքների պլատֆորմում ես կառուցեցի 13 մոդելից բաղկացած հաշվառում՝ պահեստներ, մնացորդներ և շարժեր, տեղափոխումներ, գույքագրումներ, մատակարարներ, գնման պատվերներ և ընդունումներ, ինչպես նաև մատակարարների fulfilment queue՝ վերապատվիրման առաջարկների, պահեստի արժեքավորման, «մեռած» մնացորդի և մատակարարների արդյունավետության հաշվետվություններով։
  • Նույն պլատֆորմը սինքրոնանում է հաշվապահական/ERP համակարգի հետ 12 service class-ի միջոցով՝ կատալոգ, գներ, հաճախորդներ, լոյալություն, ֆիսկալ կտրոններ, պատվերների export և վերադարձներ, կատալոգի համեմատություն և գների հետ շրջում, health alert-ներ, որպես պլանավորված job-եր, որոնք միացվում և անջատվում են ադմինի կարգավորումներից։
  • Մարզասրահների ցանցի CRM-ում ինտեգրեցի ֆիսկալ կտրոնների cloud ծառայություն՝ մասնաճյուղային սարքերի pairing-ով և կարգավորված, չհամընկնող պլանավորված job-երով․ POS SaaS-ը մշակում է վաճառքի և վերադարձի կտրոններ, offline տարբերակներ և retry-ներ։

Հարցեր և պատասխաններ

Q1Ինչո՞ւ օգտագործել մնացորդի շարժեր, այլ ոչ պարզապես քանակի սյունակ։

Քանակի սյունակը ասում է միայն ընթացիկ թիվը, բայց չի կարող այն բացատրել։ Շարժերի դեպքում յուրաքանչյուր ընդունում, վաճառք, վերադարձ, տեղափոխում և ուղղում տող է՝ պատճառով, reference-ով և հեղինակով, իսկ քանակը դրանց գումարն է։ Կարող եք audit անել անհամապատասխանությունները, վերակառուցել մնացորդները bug-ից հետո, հաշվետվություններ կազմել արժեքավորման և «մեռած» մնացորդի մասին և տեսնել, թե ճիշտ երբ է ապրանքը վերջացել։ Գործնականում պահում եմ cache-ավորված մնացորդ արագ ընթերցման համար և թարմացնում այն շարժի հետ նույն transaction-ում։

Q2Ո՞րն է տարբերությունը on-hand և հասանելի մնացորդի միջև։

On-hand-ը այն է, ինչ ֆիզիկապես պահեստում է։ Հասանելին այն է, ինչ դեռ կարող եք վաճառել՝ on-hand-ը հանած այն պատվերների համար ռեզերված մնացորդը, որոնք տեղադրված են, բայց դեռ չեն ուղարկվել։ Storefront-ը պետք է ցույց տա հասանելի մնացորդը, այլապես երկու հաճախորդ կարող են գնել նույն վերջին ապրանքը։ Ռեզերվները ստեղծվում են պատվերը տեղադրելիս, առաքման ժամանակ վերածվում ելքային շարժերի և ազատվում չեղարկման կամ վճարման ձախողման դեպքում։ Գնման պատվերներից սպասվող մնացորդը կարելի է առանձին ցույց տալ backorder-ների համար։

Q3Ինչպե՞ս հուսալիորեն սինքրոնացնել խանութը ERP-ի հետ։

Սկսեք սեփականությունից՝ յուրաքանչյուր դաշտի համար որոշեք, թե որ համակարգն է ճշմարտության աղբյուրը։ Հետո սինքրոնացրեք այդ ուղղությամբ պլանավորված, idempotent job-երով՝ կայուն արտաքին ID-ներով, batch-երով մշակված և համընկնող գործարկումներից պաշտպանված։ Նոր պատվերների նման event-ները push արեք queue-ի միջոցով՝ retry-ներով։ Log արեք յուրաքանչյուր գործարկում, հետևեք յուրաքանչյուր job-ի վերջին հաջողությանը և alert ուղարկեք, երբ այն հնացած է։ Ավելացրեք sanity check-եր կասկածելի զանգվածային փոփոխությունների համար, պահեք նախորդ արժեքները հետ շրջելու համար և ամեն գիշեր աշխատեցրեք համեմատության հաշվետվություն՝ շեղումները բռնելու համար։

Q4Ի՞նչ է լինում, երբ ERP-ն չի աշխատում։

Խանութը պետք է շարունակի վաճառել։ Գներն ու մնացորդն արդեն տեղում cache-ավորված են վերջին հաջող sync-ից, այնպես որ storefront-ն աշխատում է։ Դուրս գնացող տվյալները, օրինակ՝ պատվերներն ու վերադարձները, սպասում են queue-ում և retry են արվում backoff-ով, մինչև ERP-ն վերադառնա։ Health check-երը alert են ուղարկում ադմիններին, և ադմինը կարող է պլանավորված սպասարկման ժամանակ դադարեցնել job-ը։ Եթե մնացորդի ճշգրտությունը կրիտիկական է, կարող եք ավելացնել պահուստային buffer՝ ցույց տալ մի փոքր ավելի քիչ հասանելի մնացորդ, քանի դեռ sync-ը հնացած է։

Q5Ինչպե՞ս եք խուսափում ֆիսկալ կտրոնների կրկնօրինակներից։

Յուրաքանչյուր պատվեր ստանում է մեկ ֆիսկալ կտրոնի գրառում՝ կարգավիճակով և եզակի reference-ով։ Ուղարկումը կատարվում է queue-ի job-ում։ Եթե կանչը հստակ ձախողվում է մինչև ծառայությանը հասնելը, retry անելն անվտանգ է։ Եթե այն timeout է տալիս, կտրոնը կարող է արդեն գրանցված լինել, ուստի job-ը նախ ծառայությունից հարցնում է՝ կա՞ արդյոք այդ reference-ով կտրոն։ Միայն դրանից հետո է retry անում։ Ֆիսկալ համարը պահվում է պատվերի վրա, իսկ վերադարձները ստեղծում են առանձին վերադարձի կտրոններ՝ կապված սկզբնականի հետ։

Սենիոր մակարդակի հարցեր

S1Մնացորդի ճշմարտության աղբյուրը պետք է լինի խանո՞ւթը, թե՞ ERP-ն։

Կախված է նրանից, թե որտեղ են տեղի ունենում մնացորդի փոփոխությունները և որքան հուսալի է ERP-ն։ Եթե շարժերի մեծ մասը տեղի է ունենում ERP-ում՝ պահեստային մուտքեր, խանութի վաճառքներ, տեղափոխումներ, ERP-ն պետք է տիրապետի մնացորդին, իսկ խանութը պահում է գրեթե real-time cache՝ վրան ավելացված ռեզերվներով։ Եթե վաճառքների մեծ մասը խանութից է, իսկ ERP-ն դանդաղ է կամ batch-ային, խանութը կարող է տիրապետել օպերացիոն մնացորդին և շարժերը push անել ERP՝ հաշվապահության համար։ Ամենավատ տարբերակը համատեղ սեփականությունն է առանց կանոնների։ Որ կողմն էլ տիրապետի, մյուսը պետք է հանդուրժի հնացածությունը. ցույց տալ safety buffer, նորից ստուգել checkout-ում և ամեն գիշեր reconcile անել։ Ես որոշում եմ հաճախորդի հետ՝ թվարկելով մնացորդը փոխող յուրաքանչյուր event և որտեղից է այն ծագում։

S2Ինչպե՞ս եք երաշխավորում, որ յուրաքանչյուր պատվեր ERP հասնի ճիշտ մեկ անգամ։

Network-ով exactly-once առաքումն ընդհանուր դեպքում հնարավոր չէ, ուստի ես կառուցում եմ at-least-once առաքում գումարած idempotent մշակում, ինչը տալիս է exactly-once էֆեկտ։ Պատվերը և outbox-ի տողը commit են արվում նույն transaction-ում, ուստի ոչինչ չի կորչում։ Worker-ը ուղարկում է outbox-ի տողերը և դրանք նշում ուղարկված միայն ERP-ի հաստատումից հետո։ Եթե հաստատումը կորչում է, տողը նորից է ուղարկվում, և ERP-ի կողմը կրկնությունները հեռացնում է մեր պատվերի reference-ով՝ եզակի արտաքին ID դաշտի միջոցով կամ ստեղծելուց առաջ reference-ը query անելով։ Եթե ERP-ն չի կարող կրկնությունները հեռացնել, worker-ը timeout-ից հետո ցանկացած retry-ի ժամանակ պետք է նախ query անի։ Մոնիտորինգը alert է տալիս outbox-ի տարիքի վրա, ուստի խրված export-ները տեսանելի են րոպեների ընթացքում։

S3Ինչպե՞ս կscale անեիք պաշարների գրառումները մեծ ծավալի վաճառքի համար։

Խցանումը մի քանի թեժ տողերի վրա contention-ն է. նույն հանրաճանաչ տարբերակը նույն պահեստում։ Տարբերակներ՝ բարդության աճման կարգով. transaction-ները պահել շատ կարճ և տողերը lock անել նույն հերթականությամբ, ոչ կրիտիկական աշխատանքը, օրինակ՝ analytics-ի և ERP export-ի շարժերը, թեժ transaction-ից տեղափոխել outbox, թեժ ապրանքների մնացորդը նախապես բաժանել մի քանի bucket-ի, որոնք նվազեցվում են անկախ և գումարվում ցուցադրման համար, կամ թեժ ապրանքների առջև դնել ատոմար Redis counter, որը տրամադրում է ռեզերվներ՝ database-ի հետ պարբերական reconciliation-ով։ Յուրաքանչյուր քայլ պարզությունը փոխանակում է throughput-ի հետ, իսկ Redis մոտեցումը ավելացնում է ճշմարտության երկրորդ աղբյուր, որը պահանջում է զգույշ recovery։ Ես նախ չափում եմ lock wait-երը, որովհետև խանութների մեծ մասին երբեք պետք չի լինում առաջին երկուսից ավելին։

Բառարան

On hand
Միավորներ, որոնք հենց հիմա ֆիզիկապես առկա են պահեստում։
Available
Միավորներ, որոնք դեռ կարելի է վաճառել. on hand՝ հանած ռեզերվվածը։
Stock movement
Մնացորդի մեկ փոփոխության գրառում՝ քանակով, պատճառով, reference-ով և հեղինակով։
Cycle count
Ապրանքների փոքր, պտտվող խմբի պարբերական հաշվում՝ ամբողջ պահեստը միանգամից հաշվելու փոխարեն։
Weighted average cost
Միավորի ինքնարժեք, որը յուրաքանչյուր մուտքի ժամանակ վերահաշվարկվում է որպես հին և նոր մնացորդի ինքնարժեքների միջին։
FIFO
First in, first out. ամենահին մնացորդը և դրա ինքնարժեքը սպառվում են առաջինը։
Transactional outbox
Ելքային message-ների գրառում աղյուսակում տվյալների հետ նույն transaction-ում, ապա դրանց ասինխրոն ուղարկում։
Watermark
Վերջին մշակված timestamp-ը կամ change ID-ն, որպեսզի հաջորդ poll-ը վերցնի միայն ավելի նոր փոփոխությունները։
Safety stock
Լրացուցիչ մնացորդ, որը պահվում է պահանջարկի թռիչքները և մատակարարների ուշացումները ծածկելու համար։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։