Bosanska lokalizacija Odoo: PDV 0% primljeni avansi i mapiranje u fiskalnoj poziciji ZPDV CLAN 15 (l10n_ba_data, l10n_ba_fbih_data)
Problem
Fiskalna pozicija ZPDV CLAN 15 se koristi kada izdajemo račune stranim licima za usluge oslobođene PDV-a po članu 15. Zakona o PDV-u (npr. usluge iz inostranstva). Mehanizam je standardan Odoo flow: faktura ima porez PDV 17%, fiskalna pozicija na partneru mapira ga u PDV 0% C15, i porez se obračuna kao 0%.
Pravilo je radilo za dva od tri tipa prodajnih poreza:
| Izvorni porez | Destinacijski porez |
|---|---|
| PDV 17% | PDV 0% C15 |
| PDV 17% KP | PDV 0% C15 |
| PDV 17% primljeni avansi | (nije postojalo) ❌ |
Treći slučaj — primljeni avans od stranog kupca — nije imao pratećeg 0% poreza za destinaciju. Što znači da kad knjigovođa primi avans od stranog kupca i koristi fiskalnu poziciju ZPDV CLAN 15, sistem nije imao šta da mapira — porez PDV 17% primljeni avansi bi ostao kao 17% i avans bi bio neispravno tretiran u PDV obračunu.
Slika ispod prikazuje konačno stanje: kartica “Mapiranje PDV-a” za fiskalnu poziciju ZPDV CLAN 15 sa sva tri potrebna mapiranja, treći (uokvireno narandžasto) je tema ovog posta:

Rješenje
Dvije izmjene u dva modula:
l10n_ba_data 16.0.1.3.8 → 16.0.1.3.9
Novi porez i novi mapping zapis u XML data fajlovima:
<!-- account_taxes.xml -->
<record id="ba_sale_vat_0_avansi" model="account.tax">
<field name="name">PDV 0% primljeni avansi</field>
<field name="description">0%</field>
<field name="amount">0</field>
<field name="amount_type">percent</field>
<field name="type_tax_use">sale</field>
<field name="country_id" ref="base.ba"/>
<field name="tax_group_id" ref="tax_group_vat_0"/>
</record>
<!-- account_fiscal_positions.xml — unutar ZPDV CLAN 15 -->
<record id="fiscal_position_zpdv_clan_15_tax_sale_17avansi_to_0avansi"
model="account.fiscal.position.tax">
<field name="position_id" ref="fiscal_position_zpdv_clan_15"/>
<field name="tax_src_id" ref="ba_sale_vat_17_avansi"/>
<field name="tax_dest_id" ref="ba_sale_vat_0_avansi"/>
</record>
l10n_ba_fbih_data 16.0.1.5.9 (post_init_hook + post-migrate)
Novi porez ulazi u dvije postojeće liste u post_init_hook:
# Tagovi na repartition liniji (K_BASE na bazi, K na porezu) —
# isti obrazac kao postojeća prod konfiguracija
('ba_sale_vat_0_avansi', 'base', 100, ['tax_tag_K_base']),
('ba_sale_vat_0_avansi', 'tax', 100, ['tax_tag_K']),
# Konto 4710 na tax liniji — isti kao kod 17% primljenih avansa,
# jer su to ista logika obračuna (samo druga stopa)
('PDV 0% primljeni avansi', '4710'),
post_init_hook se okida samo na svježu instalaciju, pa za postojeće tenante migracija post-migrate.py ponovo aplicira tagove i konto kroz _set_repartition_line_tags (idempotentno: DELETE + INSERT u tag M2M tabelama).
Izazov: već smo imali ručno kreirane zapise u produkciji
Prije nego što smo ovo kodificirali u modul, ručno smo kreirali porez PDV 0% primljeni avansi i mapiranje u fiskalnoj poziciji direktno u Odoo UI na produkciji. To je sasvim valjano za hitne intervencije, ali kreira problem za sljedeći deploy:
- Ručno kreirani zapisi nemaju
ir.model.dataxml_id (nisu vezani za modul) - Kad data loader pokuša učitati novi
<record id="ba_sale_vat_0_avansi">, vidi da xml_id ne postoji u bazi i pokušava INSERT novog zapisa - Ali postoji business-key constraint
account_fiscal_position_tax_tax_src_dest_uniqkoji odbija duplikat → deploy crash - Ili još gore: tax record nema unique constraint na (name, type_tax_use, country_id), pa bi se dva poreza istog imena kreirala paralelno
Rješenje: pre-migrate.py koji teče prije data load-a i adoptira ručne zapise tako što insertuje ir.model.data red koji veže postojeći res_id za novi xml_id. Data loader onda nailazi na pravu vezu i radi UPDATE umjesto INSERT.
def _adopt_account_tax(env, xml_id, search_domain):
existing = env["account.tax"].search(search_domain, limit=1)
if not existing:
return
# Ako nema xml_id, kreiraj vezu
env["ir.model.data"].create({
"module": "l10n_ba_data",
"name": "ba_sale_vat_0_avansi",
"model": "account.tax",
"res_id": existing.id,
"noupdate": False,
})
Idempotentno: ako xml_id već postoji i pokazuje na isti red, skripta ništa ne radi. Ako pokazuje na obrisani red (stale ir.model.data), repointuje ga.
Bonus: data load popravi i pogrešnu konfiguraciju
Postojeći ručno kreirani porez u produkciji imao je tax_group_id = 95 (“PDV 17%” grupa) — što je očito pogrešno za 0% porez. Niko nije primijetio jer Odoo PDV grupa utiče samo na grupisanje u tax summary, ne na samu računicu.
Nakon adoption u pre-migrate, data load radi UPDATE prema XML definiciji (<field name="tax_group_id" ref="tax_group_vat_0"/>) — i automatski popravlja pogrešnu grupu na ispravnu PDV 0% grupu. To je lijep side-effect kodifikacije kroz modul: jednom kad zapis postane “module-owned”, on dobiva i svu validaciju koja u UI ručnom unosu nije postojala.
Verifikacija na produkciji
Prije deploya:
id | name | tax_group_id | group_name | module | xml_id
-----+-------------------------+--------------+--------------+--------+--------
203 | PDV 0% primljeni avansi | 95 | PDV 17% | | ← orphan + pogrešna grupa
id | position | src_tax | dst_tax | module | xml_id
-----+--------------+--------------------------+-------------------------+--------+--------
156 | ZPDV CLAN 15 | PDV 17% primljeni avansi | PDV 0% primljeni avansi | | ← orphan
Nakon deploya (upgrade_production_nix_service.py --modules l10n_ba_data,l10n_ba_fbih_data):
id | name | tax_group_id | group_name | module | xml_id
-----+-------------------------+--------------+--------------+--------------+----------------------
203 | PDV 0% primljeni avansi | 97 | PDV 0% | l10n_ba_data | ba_sale_vat_0_avansi ✓
id | position | src_tax | dst_tax | module | xml_id
-----+--------------+--------------------------+-------------------------+--------------+-----------------------------------------------------------
156 | ZPDV CLAN 15 | PDV 17% primljeni avansi | PDV 0% primljeni avansi | l10n_ba_data | fiscal_position_zpdv_clan_15_tax_sale_17avansi_to_0avansi ✓
id | repartition_type | factor_percent | account | tags
-----+------------------+----------------+---------+--------
530 | base | 100 | | K_BASE
531 | tax | 100 | 4710 | K
532 | base | 100 | | K_BASE
533 | tax | 100 | 4710 | K
Bez duplikata, sa očuvanim tagovima i kontom 4710 na tax liniji, sa ispravljenom tax grupom, i sa stabilnim modul vezivanjem za buduće upgrade-ove.
Lekcija
Ručne intervencije u produkciji su prihvatljive za hitne fikseve — ali kad dođe vrijeme da se popravak kodificira u modul, pre-migrate je obavezan. Bez njega data loader ide na sukob sa već postojećim podacima. Sa njim, deploy je idempotentan i bezbjedno može da se ponavlja.
Naš l10n_ba_data modul već je imao isti obrazac za fiscal position mapiranja (vidi migrations/16.0.1.3.8/pre-migrate.py) — ovaj deploy je samo proširio listu adoptiranih zapisa za jedan tax i jedan fpt red.
Napomena
Generisano od strane Claude 🤖
Ernad Husremović, hernad@bring.out.ba