[19.0][BUG] l10n_ro_stock_account: stiva FIFO per locație ignoră transferurile interne între gestiuni valorizate → COGS greșit pe ieșiri
Modul
l10n_ro_stock_account 19.0.0.24.0 / 19.0.0.25.0 (funcționalitatea fifo_per_location, models/fifo/)
Mediu
- Odoo 19.0 Enterprise (Odoo Online / SaaS), bază de producție migrată de la 18.0 pe 09.07.2026
- Companie românească →
company.fifo_per_location = True (default calculat pentru country_code == "RO")
- Categorie de produs cu
property_cost_method = "fifo", fără valorizare pe loturi
Fluxul de business care declanșează bugul
Depozitul folosește un flux intern în doi pași, uzual pentru marfă care necesită
procesare/etichetare înainte de vânzare:
Furnizori ──(recepție, is_in=True)──▶ WH/Stock/Neprocesate ──(transfer intern)──▶ WH/Stock/Procesate ──(livrare)──▶ Clienți
Toate recepțiile de la furnizori intră în Neprocesate. Marfa trece în Procesate printr-un
transfer intern simplu, iar livrările către clienți pleacă din Procesate.
Pași de reproducere (minim)
- Companie RO, produs FIFO, două locații interne (valorizate)
A și B.
- Cu ani în urmă: o ajustare de inventar a creat 1 bucată direct în
B, la un cost istoric mare
(ex. 176,79). Bucata a fost vândută ulterior — la nivel de companie stratul e complet consumat.
- Se recepționează 2 bucăți @ ~54,00 de la furnizor în
A.
- Transfer intern
A → B cu cele 2 bucăți.
- Se livrează 1 bucată din
B către client.
Comportament observat
Mișcarea de ieșire este valorizată la 176,79 — costul ajustării de inventar de la pasul 2,
un strat consumat fizic demult, fără nicio legătură cu stocul aflat pe mână.
Pe baza noastră de producție au fost afectate 382 de produse (COGS / valoare de stoc
supraevaluate cu ~12.400 RON), toate produse care trec prin fluxul în doi pași
Neprocesate → Procesate. Problema a fost sesizată prima dată de client ca o marjă negativă
inexplicabilă (preț de vânzare 132,52, cost 176,79) la un produs al cărui cost curent e ~53.
Comportament așteptat
Ieșirea ar trebui valorizată din costul purtat efectiv de bucățile aflate pe mână în locația
sursă — ~54,00 (recepția recentă de la furnizor, adusă de transferul intern).
Analiza cauzei
product.product._run_fifo_get_stack() (models/fifo/product.py), când primește location,
construiește stiva per locație cu domeniul:
moves_domain &= Domain([("location_dest_id", "=", location.id)])
...
moves_domain &= Domain([("is_in", "=", True)])
is_in este câmpul stocat din core Odoo care marchează mișcările ce aduc valoare în
companie (recepții de la furnizor, retururi de la client…). Un transfer simplu între două
locații interne valorizate are is_in = False stocat, deci:
- Pentru locația
B (locația de livrare), transferul intern A → B este invizibil pentru
constructorul stivei.
- Singura mișcare
is_in=True care a țintit vreodată direct B este vechea ajustare de
inventar, deci stiva lui B conține doar acel strat vechi, iar fiecare livrare din B îl
consumă — la nesfârșit (qty_available la B e pozitiv, iar parcurgerea stivei refolosește
singura mișcare pe care o găsește, indiferent de câte ori i-a fost deja consumată cantitatea la
nivel de companie).
Doi factori agravanți:
-
Inconsistență între is_in stocat și _is_in() runtime. Override-ul RO din modulul de
bază (models/stock_move.py::_get_in_move_lines) extinde liniile de intrare la transferurile
intern→intern pentru înregistrările RO, deci move._is_in() întoarce True la runtime pentru
transfer — dar coloana stocată is_in (calculată de core, store=True) este False
(pe această bază, probabil calculată în timpul migrării 18→19, înainte ca override-ul RO să fie
în registry). Căutarea stivei folosește coloana stocată, deci transferul rămâne invizibil, deși
modulul „intenționa" să-l trateze ca intrare. Exemplu din producție:
| mișcare |
referință |
is_in stocat |
_is_in() runtime |
| transfer intern A→B |
nepr_proce05751 |
False |
True |
-
Transferul intern însuși nu poartă o valoare FIFO fiabilă. Chiar acolo unde transferul e
vizibil, value nu este costul FIFO consumat din locația sursă (în datele noastre conținea un
fragment rezidual de landed cost: value = 10,06 pentru 2 bucăți a căror recepție a costat
107,71), deci stiva destinației tot ar valoriza greșit marfa.
De ce FIFO la nivel de companie dă rezultatul corect
Cu fifo_per_location = False totul trece prin _run_fifo_get_stack din core, stiva este pe
toată compania, recepțiile sunt mereu vizibile, iar revalorizarea ieșirilor afectate produce exact
costurile așteptate (verificat pe o copie a bazei de producție: 176,79 → 53,86/53,01). Acesta e
workaround-ul pe care l-am aplicat.
Direcții de fix sugerate
Pentru ca stiva per locație să fie corectă, e nevoie de ambele jumătăți:
-
În _run_fifo_get_stack, când se primește location (internă, valorizată), să fie acceptate
ca intrări în stivă nu doar mișcările is_in = True, ci și transferurile sosite dintr-o altă
locație internă/tranzit valorizată:
moves_domain &= Domain([("is_in", "=", True)]) | Domain([
("location_id.is_valued_internal", "=", True),
("location_id", "!=", location.id),
])
-
În _set_value, transferurile interne de acest tip să primească o valoare reală: costul FIFO
consumat din stiva locației sursă (azi ele nu sunt valorizate nici pe ramura
_is_out(), nici pe is_in din override-urile fifo/, iar ramura
l10n_ro_move_type == "internal_transfer" din modulul de bază cade pe _get_value(), care
ajunge la standard_price).
Suplimentar, valorile stocate is_in/is_out pe bazele migrate nu corespund override-urilor RO
_is_in()/_is_out() și probabil au nevoie de un recompute la migrare.
Putem furniza scripturile complete de reproducere și putem testa un fix candidat pe copia noastră
a bazei de producție afectate.
Evaluarea impactului pe cazul nostru de producție
- 11.841 produse cu stoc pe mână; 487 semnalate inițial, dintre care 382 confirmate greșite
sub FIFO per locație (celelalte 105 sunt stoc vechi autentic, valorizat corect).
- Fiecare livrare a unui produs afectat postează o notă COGS supraevaluată (
607 / 371).
- Descoperit pe: LCD Huawei P40 Pro (ref. internă 64314) — vândut pe 15.07.2026 și 20.07.2026 la
cost 176,79, în timp ce costul FIFO corect era 53,86/53,01.
[19.0][BUG] l10n_ro_stock_account: stiva FIFO per locație ignoră transferurile interne între gestiuni valorizate → COGS greșit pe ieșiri
Modul
l10n_ro_stock_account19.0.0.24.0 / 19.0.0.25.0 (funcționalitateafifo_per_location,models/fifo/)Mediu
company.fifo_per_location = True(default calculat pentrucountry_code == "RO")property_cost_method = "fifo", fără valorizare pe loturiFluxul de business care declanșează bugul
Depozitul folosește un flux intern în doi pași, uzual pentru marfă care necesită
procesare/etichetare înainte de vânzare:
Toate recepțiile de la furnizori intră în
Neprocesate. Marfa trece înProcesateprintr-untransfer intern simplu, iar livrările către clienți pleacă din
Procesate.Pași de reproducere (minim)
AșiB.B, la un cost istoric mare(ex. 176,79). Bucata a fost vândută ulterior — la nivel de companie stratul e complet consumat.
A.A → Bcu cele 2 bucăți.Bcătre client.Comportament observat
Mișcarea de ieșire este valorizată la 176,79 — costul ajustării de inventar de la pasul 2,
un strat consumat fizic demult, fără nicio legătură cu stocul aflat pe mână.
Pe baza noastră de producție au fost afectate 382 de produse (COGS / valoare de stoc
supraevaluate cu ~12.400 RON), toate produse care trec prin fluxul în doi pași
Neprocesate → Procesate. Problema a fost sesizată prima dată de client ca o marjă negativăinexplicabilă (preț de vânzare 132,52, cost 176,79) la un produs al cărui cost curent e ~53.
Comportament așteptat
Ieșirea ar trebui valorizată din costul purtat efectiv de bucățile aflate pe mână în locația
sursă — ~54,00 (recepția recentă de la furnizor, adusă de transferul intern).
Analiza cauzei
product.product._run_fifo_get_stack()(models/fifo/product.py), când primeștelocation,construiește stiva per locație cu domeniul:
is_ineste câmpul stocat din core Odoo care marchează mișcările ce aduc valoare încompanie (recepții de la furnizor, retururi de la client…). Un transfer simplu între două
locații interne valorizate are
is_in = Falsestocat, deci:B(locația de livrare), transferul internA → Beste invizibil pentruconstructorul stivei.
is_in=Truecare a țintit vreodată directBeste vechea ajustare deinventar, deci stiva lui
Bconține doar acel strat vechi, iar fiecare livrare dinBîlconsumă — la nesfârșit (
qty_availablelaBe pozitiv, iar parcurgerea stivei refoloseștesingura mișcare pe care o găsește, indiferent de câte ori i-a fost deja consumată cantitatea la
nivel de companie).
Doi factori agravanți:
Inconsistență între
is_instocat și_is_in()runtime. Override-ul RO din modulul debază (
models/stock_move.py::_get_in_move_lines) extinde liniile de intrare la transferurileintern→intern pentru înregistrările RO, deci
move._is_in()întoarceTruela runtime pentrutransfer — dar coloana stocată
is_in(calculată de core,store=True) esteFalse(pe această bază, probabil calculată în timpul migrării 18→19, înainte ca override-ul RO să fie
în registry). Căutarea stivei folosește coloana stocată, deci transferul rămâne invizibil, deși
modulul „intenționa" să-l trateze ca intrare. Exemplu din producție:
is_instocat_is_in()runtimenepr_proce05751FalseTrueTransferul intern însuși nu poartă o valoare FIFO fiabilă. Chiar acolo unde transferul e
vizibil,
valuenu este costul FIFO consumat din locația sursă (în datele noastre conținea unfragment rezidual de landed cost:
value = 10,06pentru 2 bucăți a căror recepție a costat107,71), deci stiva destinației tot ar valoriza greșit marfa.
De ce FIFO la nivel de companie dă rezultatul corect
Cu
fifo_per_location = Falsetotul trece prin_run_fifo_get_stackdin core, stiva este petoată compania, recepțiile sunt mereu vizibile, iar revalorizarea ieșirilor afectate produce exact
costurile așteptate (verificat pe o copie a bazei de producție: 176,79 → 53,86/53,01). Acesta e
workaround-ul pe care l-am aplicat.
Direcții de fix sugerate
Pentru ca stiva per locație să fie corectă, e nevoie de ambele jumătăți:
În
_run_fifo_get_stack, când se primeștelocation(internă, valorizată), să fie acceptateca intrări în stivă nu doar mișcările
is_in = True, ci și transferurile sosite dintr-o altălocație internă/tranzit valorizată:
În
_set_value, transferurile interne de acest tip să primească o valoare reală: costul FIFOconsumat din stiva locației sursă (azi ele nu sunt valorizate nici pe ramura
_is_out(), nici peis_indin override-urile fifo/, iar ramural10n_ro_move_type == "internal_transfer"din modulul de bază cade pe_get_value(), careajunge la
standard_price).Suplimentar, valorile stocate
is_in/is_outpe bazele migrate nu corespund override-urilor RO_is_in()/_is_out()și probabil au nevoie de un recompute la migrare.Putem furniza scripturile complete de reproducere și putem testa un fix candidat pe copia noastră
a bazei de producție afectate.
Evaluarea impactului pe cazul nostru de producție
sub FIFO per locație (celelalte 105 sunt stoc vechi autentic, valorizat corect).
607/371).cost 176,79, în timp ce costul FIFO corect era 53,86/53,01.