Knjigovodstvo: storno prodaje/nabavke više ne briše KIR/KPR/KPO
Storno je fizički brisao vezani KIR/KPR/KPO zapis — zakon o računovodstvu ne dozvoljava brisanje poslovnih knjiga (KPO dodatno ima redni broj bez prekida). Sad se original čuva, a storno se dograđuje kao poništavajuća stavka (negirani iznosi, referenca na original) — model.KirStorno/KprStorno/KpoStorno. Usput ispravljen i pravi uzrok prijavljenog bug-a: kirKandidatiProdaje je filtrirala samo internu mapu, ne i listu naloga koju obilazi KirBackfillProdaje — stornirani nalozi su zbog toga dobijali ponovni (pogrešan) KIR upis.
This commit is contained in:
@@ -66,8 +66,9 @@ type PdvKirRepository interface {
|
|||||||
DohvatiID(ctx context.Context, id int64) (*model.PdvKir, error)
|
DohvatiID(ctx context.Context, id int64) (*model.PdvKir, error)
|
||||||
Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error)
|
Kreiraj(ctx context.Context, k *model.PdvKir) (int64, error)
|
||||||
Obrisi(ctx context.Context, id int64) error
|
Obrisi(ctx context.Context, id int64) error
|
||||||
// ObrisiPoIzvoru briše zapise vezane za dati izvor (npr. pri stornu prodaje)
|
// DohvatiPoIzvoru vraća zapise vezane za dati izvor (npr. za storno prodaje —
|
||||||
ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error
|
// original se ne briše, storno stavka se dograđuje na osnovu ovih zapisa)
|
||||||
|
DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKir, error)
|
||||||
// PostojiZaIzvor vraća true ako postoji bar jedan zapis za dati izvor i izvorID
|
// PostojiZaIzvor vraća true ako postoji bar jedan zapis za dati izvor i izvorID
|
||||||
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error)
|
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error)
|
||||||
// PostojiPoBrojuDokumenta vraća true ako postoji zapis sa datim brojem dokumenta
|
// PostojiPoBrojuDokumenta vraća true ako postoji zapis sa datim brojem dokumenta
|
||||||
@@ -83,8 +84,9 @@ type PdvKprRepository interface {
|
|||||||
DohvatiID(ctx context.Context, id int64) (*model.PdvKpr, error)
|
DohvatiID(ctx context.Context, id int64) (*model.PdvKpr, error)
|
||||||
Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error)
|
Kreiraj(ctx context.Context, k *model.PdvKpr) (int64, error)
|
||||||
Obrisi(ctx context.Context, id int64) error
|
Obrisi(ctx context.Context, id int64) error
|
||||||
// ObrisiPoIzvoru briše zapise vezane za dati izvor (npr. pri brisanju nabavke)
|
// DohvatiPoIzvoru vraća zapise vezane za dati izvor (npr. za storno nabavke —
|
||||||
ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error
|
// original se ne briše, storno stavka se dograđuje na osnovu ovih zapisa)
|
||||||
|
DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKpr, error)
|
||||||
// PostojiZaIzvor vraća true ako postoji bar jedan zapis za dati izvor i izvorID
|
// PostojiZaIzvor vraća true ako postoji bar jedan zapis za dati izvor i izvorID
|
||||||
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error)
|
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error)
|
||||||
}
|
}
|
||||||
@@ -413,5 +415,7 @@ type KpoRepository interface {
|
|||||||
Kreiraj(ctx context.Context, z *model.KpoZapis) (int64, error)
|
Kreiraj(ctx context.Context, z *model.KpoZapis) (int64, error)
|
||||||
Obrisi(ctx context.Context, id int64) error
|
Obrisi(ctx context.Context, id int64) error
|
||||||
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error)
|
PostojiZaIzvor(ctx context.Context, izvor string, izvorID int64) (bool, error)
|
||||||
ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error
|
// DohvatiPoIzvoru vraća zapise vezane za dati izvor (npr. za storno prodaje —
|
||||||
|
// original se ne briše, storno stavka se dograđuje na osnovu ovih zapisa)
|
||||||
|
DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.KpoZapis, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,10 +115,41 @@ func (r *KpoRepo) PostojiZaIzvor(ctx context.Context, izvor string, izvorID int6
|
|||||||
return n > 0, nil
|
return n > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObrisiPoIzvoru briše KPO zapise vezane za dati izvor (npr. pri stornu prodaje).
|
// DohvatiPoIzvoru vraća sve KPO zapise vezane za dati izvor (npr. za storno prodaje).
|
||||||
func (r *KpoRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error {
|
func (r *KpoRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.KpoZapis, error) {
|
||||||
if _, err := r.db.ExecContext(ctx, "DELETE FROM kpo_unosi WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil {
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
return fmt.Errorf("ntech: KpoRepo.ObrisiPoIzvoru: %w", err)
|
SELECT id, datum_prometa, redni_broj, broj_dokumenta, opis, prihod,
|
||||||
|
nacin_placanja, napomena, izvor, izvor_id, datum_unosa
|
||||||
|
FROM kpo_unosi WHERE izvor = ? AND izvor_id = ?`, izvor, izvorID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: KpoRepo.DohvatiPoIzvoru: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
defer redovi.Close()
|
||||||
|
|
||||||
|
var rezultat []model.KpoZapis
|
||||||
|
for redovi.Next() {
|
||||||
|
var z model.KpoZapis
|
||||||
|
var opis, nacinPlacanja, napomena sql.NullString
|
||||||
|
var redniBroj sql.NullInt64
|
||||||
|
var izvorIDCol sql.NullInt64
|
||||||
|
|
||||||
|
if err := redovi.Scan(
|
||||||
|
&z.ID, &z.DatumPrometa, &redniBroj, &z.BrojDokumenta, &opis, &z.Prihod,
|
||||||
|
&nacinPlacanja, &napomena, &z.Izvor, &izvorIDCol, &z.DatumUnosa,
|
||||||
|
); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: KpoRepo.DohvatiPoIzvoru: scan: %w", err)
|
||||||
|
}
|
||||||
|
if redniBroj.Valid {
|
||||||
|
n := int(redniBroj.Int64)
|
||||||
|
z.RedniBroj = &n
|
||||||
|
}
|
||||||
|
z.Opis = opis.String
|
||||||
|
z.NacinPlacanja = nacinPlacanja.String
|
||||||
|
z.Napomena = napomena.String
|
||||||
|
if izvorIDCol.Valid {
|
||||||
|
z.IzvorID = &izvorIDCol.Int64
|
||||||
|
}
|
||||||
|
rezultat = append(rezultat, z)
|
||||||
|
}
|
||||||
|
return rezultat, redovi.Err()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,12 +128,32 @@ func (r *PdvKirRepo) Obrisi(ctx context.Context, id int64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObrisiPoIzvoru briše KIR zapise vezane za dati izvor (npr. pri stornu/brisanju prodaje)
|
// DohvatiPoIzvoru vraća sve KIR zapise vezane za dati izvor (npr. za storno prodaje)
|
||||||
func (r *PdvKirRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error {
|
func (r *PdvKirRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKir, error) {
|
||||||
if _, err := r.db.ExecContext(ctx, "DELETE FROM pdv_kir WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil {
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
return fmt.Errorf("ntech: PdvKirRepo.ObrisiPoIzvoru: %w", err)
|
SELECT id, datum_prometa, datum_knjizenja, broj_dokumenta,
|
||||||
|
kupac_naziv, COALESCE(kupac_pib, ''), COALESCE(kupac_mesto, ''),
|
||||||
|
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
|
||||||
|
osloboden_sa_pravom, osloboden_bez_prava, ukupno,
|
||||||
|
COALESCE(napomena, ''), izvor, izvor_id, datum_unosa
|
||||||
|
FROM pdv_kir WHERE izvor = ? AND izvor_id = ?`, izvor, izvorID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiPoIzvoru: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
defer redovi.Close()
|
||||||
|
|
||||||
|
var rezultat []model.PdvKir
|
||||||
|
for redovi.Next() {
|
||||||
|
k, err := skenirajKir(redovi.Scan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiPoIzvoru: scan: %w", err)
|
||||||
|
}
|
||||||
|
rezultat = append(rezultat, k)
|
||||||
|
}
|
||||||
|
if err := redovi.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: PdvKirRepo.DohvatiPoIzvoru: %w", err)
|
||||||
|
}
|
||||||
|
return rezultat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostojiZaIzvor vraća true ako postoji bar jedan KIR zapis za dati izvor i izvorID
|
// PostojiZaIzvor vraća true ako postoji bar jedan KIR zapis za dati izvor i izvorID
|
||||||
@@ -318,12 +338,32 @@ func (r *PdvKprRepo) Obrisi(ctx context.Context, id int64) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObrisiPoIzvoru briše KPR zapise vezane za dati izvor (npr. pri brisanju nabavke)
|
// DohvatiPoIzvoru vraća sve KPR zapise vezane za dati izvor (npr. za storno nabavke)
|
||||||
func (r *PdvKprRepo) ObrisiPoIzvoru(ctx context.Context, izvor string, izvorID int64) error {
|
func (r *PdvKprRepo) DohvatiPoIzvoru(ctx context.Context, izvor string, izvorID int64) ([]model.PdvKpr, error) {
|
||||||
if _, err := r.db.ExecContext(ctx, "DELETE FROM pdv_kpr WHERE izvor = ? AND izvor_id = ?", izvor, izvorID); err != nil {
|
redovi, err := r.db.QueryContext(ctx, `
|
||||||
return fmt.Errorf("ntech: PdvKprRepo.ObrisiPoIzvoru: %w", err)
|
SELECT id, datum_prometa, datum_knjizenja, datum_placanja, broj_dokumenta,
|
||||||
|
dobavljac_naziv, COALESCE(dobavljac_pib, ''), COALESCE(dobavljac_mesto, ''),
|
||||||
|
osnovica_opsta, pdv_opsta, osnovica_posebna, pdv_posebna,
|
||||||
|
pdv_bez_odbitka, osloboden_nabavka, ukupno,
|
||||||
|
COALESCE(napomena, ''), izvor, izvor_id, uvoz, datum_unosa
|
||||||
|
FROM pdv_kpr WHERE izvor = ? AND izvor_id = ?`, izvor, izvorID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: PdvKprRepo.DohvatiPoIzvoru: %w", err)
|
||||||
}
|
}
|
||||||
return nil
|
defer redovi.Close()
|
||||||
|
|
||||||
|
var rezultat []model.PdvKpr
|
||||||
|
for redovi.Next() {
|
||||||
|
k, err := skenirajKpr(redovi.Scan)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: PdvKprRepo.DohvatiPoIzvoru: scan: %w", err)
|
||||||
|
}
|
||||||
|
rezultat = append(rezultat, k)
|
||||||
|
}
|
||||||
|
if err := redovi.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("ntech: PdvKprRepo.DohvatiPoIzvoru: %w", err)
|
||||||
|
}
|
||||||
|
return rezultat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostojiZaIzvor vraća true ako postoji bar jedan KPR zapis za dati izvor i izvorID
|
// PostojiZaIzvor vraća true ako postoji bar jedan KPR zapis za dati izvor i izvorID
|
||||||
|
|||||||
@@ -162,12 +162,12 @@ func TestPdvKirIzvor(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ObrisiPoIzvoru briše samo vezani auto zapis, ručni ostaje
|
// DohvatiPoIzvoru vraća samo vezani auto zapis
|
||||||
if err := repo.ObrisiPoIzvoru(ctx, "prodaja", 7); err != nil {
|
poIzvoru, err := repo.DohvatiPoIzvoru(ctx, "prodaja", 7)
|
||||||
t.Fatalf("ObrisiPoIzvoru: %v", err)
|
if err != nil {
|
||||||
|
t.Fatalf("DohvatiPoIzvoru: %v", err)
|
||||||
}
|
}
|
||||||
preostali, _ := repo.Lista(ctx, time.Time{}, time.Time{})
|
if len(poIzvoru) != 1 || poIzvoru[0].BrojDokumenta != "P-1" {
|
||||||
if len(preostali) != 1 || preostali[0].BrojDokumenta != "R-1" {
|
t.Errorf("očekivan samo auto zapis P-1, dobijeno %d", len(poIzvoru))
|
||||||
t.Errorf("posle ObrisiPoIzvoru očekivan samo ručni zapis, dobijeno %d", len(preostali))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,9 +377,17 @@ func (h *Handler) StornoNabavke(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?greska=storno", http.StatusSeeOther)
|
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?greska=storno", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// stornirana nabavka ne ulazi u PDV — ukloni vezani auto-KPR zapis
|
// stornirana nabavka se ne briše iz KPR (zakon o računovodstvu ne dozvoljava
|
||||||
if err := h.PdvKprRepo.ObrisiPoIzvoru(r.Context(), "nabavka", id); err != nil {
|
// brisanje poslovnih knjiga) — dograđuje se storno stavka koja poništava original
|
||||||
slog.Error("brisanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err)
|
if zapisi, err := h.PdvKprRepo.DohvatiPoIzvoru(r.Context(), "nabavka", id); err != nil {
|
||||||
|
slog.Error("čitanje vezanog KPR zapisa nije uspelo", "nabavka_id", id, "error", err)
|
||||||
|
} else {
|
||||||
|
for _, z := range zapisi {
|
||||||
|
storno := model.KprStorno(z, razlog, time.Now())
|
||||||
|
if _, e := h.PdvKprRepo.Kreiraj(r.Context(), &storno); e != nil {
|
||||||
|
slog.Error("upis storno KPR zapisa nije uspeo", "nabavka_id", id, "error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
http.Redirect(w, r, "/nabavke/"+strconv.FormatInt(id, 10)+"?sacuvano=1", http.StatusSeeOther)
|
||||||
|
|||||||
@@ -687,9 +687,17 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// stornirana prodaja ne ulazi u PDV — ukloni vezani auto-KIR zapis
|
// stornirana prodaja se ne briše iz KIR (zakon o računovodstvu ne dozvoljava
|
||||||
if err := h.PdvKirRepo.ObrisiPoIzvoru(ctx, "prodaja", id); err != nil {
|
// brisanje poslovnih knjiga) — dograđuje se storno stavka koja poništava original
|
||||||
slog.Error("brisanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
|
if zapisi, err := h.PdvKirRepo.DohvatiPoIzvoru(ctx, "prodaja", id); err != nil {
|
||||||
|
slog.Error("čitanje vezanog KIR zapisa nije uspelo", "prodaja_id", id, "error", err)
|
||||||
|
} else {
|
||||||
|
for _, z := range zapisi {
|
||||||
|
storno := model.KirStorno(z, razlog, time.Now())
|
||||||
|
if _, e := h.PdvKirRepo.Kreiraj(ctx, &storno); e != nil {
|
||||||
|
slog.Error("upis storno KIR zapisa nije uspeo", "prodaja_id", id, "error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fiskalni refund — best-effort
|
// fiskalni refund — best-effort
|
||||||
@@ -744,10 +752,18 @@ func (h *Handler) stornirajProdaju(ctx context.Context, id int64, razlog string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// brisanje KPO zapisa na storno
|
// stornirana prodaja se ne briše iz KPO (redni broj mora ostati bez prekida) —
|
||||||
|
// dograđuje se storno stavka koja poništava original
|
||||||
if h.modulUkljucen(ctx, "kpo") {
|
if h.modulUkljucen(ctx, "kpo") {
|
||||||
if e := h.KpoRepo.ObrisiPoIzvoru(ctx, "prodaja", id); e != nil {
|
if zapisi, e := h.KpoRepo.DohvatiPoIzvoru(ctx, "prodaja", id); e != nil {
|
||||||
slog.Error("brisanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e)
|
slog.Error("čitanje vezanog KPO zapisa nije uspelo", "prodaja_id", id, "error", e)
|
||||||
|
} else {
|
||||||
|
for _, z := range zapisi {
|
||||||
|
storno := model.KpoStorno(z, razlog, time.Now())
|
||||||
|
if _, e := h.KpoRepo.Kreiraj(ctx, &storno); e != nil {
|
||||||
|
slog.Error("upis storno KPO zapisa nije uspeo", "prodaja_id", id, "error", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,10 +59,18 @@ func sumnjiviDuplikati(svi []kandidatUskladjivanja) map[int64]bool {
|
|||||||
// KIR upis) sa mapom postojećih upisa i kandidatima za detekciju duplikata. Koristi se
|
// KIR upis) sa mapom postojećih upisa i kandidatima za detekciju duplikata. Koristi se
|
||||||
// i za backfill (KirBackfillProdaje) i za brojanje praznina na dashboard-u.
|
// i za backfill (KirBackfillProdaje) i za brojanje praznina na dashboard-u.
|
||||||
func (h *Handler) kirKandidatiProdaje(ctx context.Context) ([]model.ProdajniNalogSaDetaljem, map[int64]bool, []kandidatUskladjivanja, error) {
|
func (h *Handler) kirKandidatiProdaje(ctx context.Context) ([]model.ProdajniNalogSaDetaljem, map[int64]bool, []kandidatUskladjivanja, error) {
|
||||||
nalozi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
|
svi, err := h.ProdajaRepo.Lista(ctx, appdb.ProdajaFilter{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
// stornirani nalozi se nikad ne razmatraju za KIR upis — ni za backfill ni za
|
||||||
|
// brojanje praznina — jer stornirana prodaja ne predstavlja oporeziv promet
|
||||||
|
var nalozi []model.ProdajniNalogSaDetaljem
|
||||||
|
for _, nd := range svi {
|
||||||
|
if !nd.Stornirano {
|
||||||
|
nalozi = append(nalozi, nd)
|
||||||
|
}
|
||||||
|
}
|
||||||
postojiMap := make(map[int64]bool, len(nalozi))
|
postojiMap := make(map[int64]bool, len(nalozi))
|
||||||
var kandidati []kandidatUskladjivanja
|
var kandidati []kandidatUskladjivanja
|
||||||
for _, nd := range nalozi {
|
for _, nd := range nalozi {
|
||||||
|
|||||||
@@ -22,3 +22,24 @@ type KpoSume struct {
|
|||||||
Prihod float64
|
Prihod float64
|
||||||
Broj int
|
Broj int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KpoStorno gradi storno zapis za dati KPO red: negiran prihod, upisan pod istim
|
||||||
|
// izvorom/izvor_id kao original. Knjiga o ostvarenom prometu paušalaca vodi redni
|
||||||
|
// broj bez prekida (Pravilnik) — original se nikad ne briše, storno se dodaje kao
|
||||||
|
// poništavajuća stavka koja upućuje na original.
|
||||||
|
func KpoStorno(original KpoZapis, razlog string, datumStorna time.Time) KpoZapis {
|
||||||
|
napomena := "Storno naloga " + original.BrojDokumenta
|
||||||
|
if razlog != "" {
|
||||||
|
napomena += ": " + razlog
|
||||||
|
}
|
||||||
|
return KpoZapis{
|
||||||
|
DatumPrometa: datumStorna,
|
||||||
|
BrojDokumenta: "STORNO-" + original.BrojDokumenta,
|
||||||
|
Opis: original.Opis,
|
||||||
|
Prihod: -original.Prihod,
|
||||||
|
NacinPlacanja: original.NacinPlacanja,
|
||||||
|
Napomena: napomena,
|
||||||
|
Izvor: original.Izvor,
|
||||||
|
IzvorID: original.IzvorID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -129,6 +129,36 @@ func KirIzProdaje(nalog ProdajniNalog, stavke []StavkaProdaje, kupacNaziv, kupac
|
|||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KirStorno gradi storno (knjižno odobrenje) zapis za dati KIR red: iznosi su
|
||||||
|
// negirani original, upisan pod istim izvorom/izvor_id tako da PostojiZaIzvor
|
||||||
|
// ostane tačan i backfill ne pokuša ponovni upis. Zakon o računovodstvu ne
|
||||||
|
// dozvoljava brisanje knjigovodstvenih evidencija — storno se knjiži kao
|
||||||
|
// poništavajuća stavka koja upućuje na original, a ne kao brisanje reda.
|
||||||
|
func KirStorno(original PdvKir, razlog string, datumStorna time.Time) PdvKir {
|
||||||
|
napomena := "Storno računa " + original.BrojDokumenta
|
||||||
|
if razlog != "" {
|
||||||
|
napomena += ": " + razlog
|
||||||
|
}
|
||||||
|
return PdvKir{
|
||||||
|
DatumPrometa: datumStorna,
|
||||||
|
DatumKnjizenja: datumStorna,
|
||||||
|
BrojDokumenta: "STORNO-" + original.BrojDokumenta,
|
||||||
|
KupacNaziv: original.KupacNaziv,
|
||||||
|
KupacPib: original.KupacPib,
|
||||||
|
KupacMesto: original.KupacMesto,
|
||||||
|
OsnovicaOpsta: -original.OsnovicaOpsta,
|
||||||
|
PdvOpsta: -original.PdvOpsta,
|
||||||
|
OsnovicaPosebna: -original.OsnovicaPosebna,
|
||||||
|
PdvPosebna: -original.PdvPosebna,
|
||||||
|
OslobodenSaPravom: -original.OslobodenSaPravom,
|
||||||
|
OslobodenBezPrava: -original.OslobodenBezPrava,
|
||||||
|
Ukupno: -original.Ukupno,
|
||||||
|
Napomena: napomena,
|
||||||
|
Izvor: original.Izvor,
|
||||||
|
IzvorID: original.IzvorID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// PdvKpr je jedan zapis u knjizi primljenih računa (ulazni PDV).
|
// PdvKpr je jedan zapis u knjizi primljenih računa (ulazni PDV).
|
||||||
type PdvKpr struct {
|
type PdvKpr struct {
|
||||||
ID int64
|
ID int64
|
||||||
@@ -153,6 +183,33 @@ type PdvKpr struct {
|
|||||||
DatumUnosa time.Time
|
DatumUnosa time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KprStorno gradi storno zapis za dati KPR red — vidi KirStorno.
|
||||||
|
func KprStorno(original PdvKpr, razlog string, datumStorna time.Time) PdvKpr {
|
||||||
|
napomena := "Storno dokumenta " + original.BrojDokumenta
|
||||||
|
if razlog != "" {
|
||||||
|
napomena += ": " + razlog
|
||||||
|
}
|
||||||
|
return PdvKpr{
|
||||||
|
DatumPrometa: datumStorna,
|
||||||
|
DatumKnjizenja: datumStorna,
|
||||||
|
BrojDokumenta: "STORNO-" + original.BrojDokumenta,
|
||||||
|
DobavljacNaziv: original.DobavljacNaziv,
|
||||||
|
DobavljacPib: original.DobavljacPib,
|
||||||
|
DobavljacMesto: original.DobavljacMesto,
|
||||||
|
OsnovicaOpsta: -original.OsnovicaOpsta,
|
||||||
|
PdvOpsta: -original.PdvOpsta,
|
||||||
|
OsnovicaPosebna: -original.OsnovicaPosebna,
|
||||||
|
PdvPosebna: -original.PdvPosebna,
|
||||||
|
PdvBezOdbitka: -original.PdvBezOdbitka,
|
||||||
|
OslobodenNabavka: -original.OslobodenNabavka,
|
||||||
|
Ukupno: -original.Ukupno,
|
||||||
|
Napomena: napomena,
|
||||||
|
Izvor: original.Izvor,
|
||||||
|
IzvorID: original.IzvorID,
|
||||||
|
Uvoz: original.Uvoz,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// OznakaPoreskogBroja vraća „JMBG" za 13-cifreni broj, inače „PIB" (dobavljači su obično firme).
|
// OznakaPoreskogBroja vraća „JMBG" za 13-cifreni broj, inače „PIB" (dobavljači su obično firme).
|
||||||
func (k PdvKpr) OznakaPoreskogBroja() string {
|
func (k PdvKpr) OznakaPoreskogBroja() string {
|
||||||
cifre := 0
|
cifre := 0
|
||||||
|
|||||||
@@ -70,6 +70,38 @@ func TestPdvKirDodajNeto(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestKirStorno: storno stavka negira sve kolone originala, čuva izvor/izvor_id
|
||||||
|
// (da PostojiZaIzvor ostane tačan i backfill ne pokuša ponovni upis) i original
|
||||||
|
// se ne dira — storno se samo dograđuje kao novi red.
|
||||||
|
func TestKirStorno(t *testing.T) {
|
||||||
|
izvorID := int64(18)
|
||||||
|
original := PdvKir{
|
||||||
|
BrojDokumenta: "PR-2607-0013", KupacNaziv: "Kupac doo", KupacPib: "123456789",
|
||||||
|
OsnovicaOpsta: 1000, PdvOpsta: 200, OsnovicaPosebna: 100, PdvPosebna: 10,
|
||||||
|
OslobodenSaPravom: 50, OslobodenBezPrava: 20, Ukupno: 1380,
|
||||||
|
Izvor: "prodaja", IzvorID: &izvorID,
|
||||||
|
}
|
||||||
|
datumStorna := time.Date(2026, 7, 2, 0, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
s := KirStorno(original, "kupac odustao", datumStorna)
|
||||||
|
|
||||||
|
if s.BrojDokumenta != "STORNO-PR-2607-0013" {
|
||||||
|
t.Errorf("broj_dokumenta=%q, očekivano STORNO-PR-2607-0013", s.BrojDokumenta)
|
||||||
|
}
|
||||||
|
if s.Izvor != "prodaja" || s.IzvorID == nil || *s.IzvorID != 18 {
|
||||||
|
t.Errorf("izvor=%q izvor_id=%v, očekivano prodaja/18 (isto kao original)", s.Izvor, s.IzvorID)
|
||||||
|
}
|
||||||
|
if !blizu(s.OsnovicaOpsta, -1000) || !blizu(s.PdvOpsta, -200) {
|
||||||
|
t.Errorf("opšta: osnovica=%v pdv=%v, očekivano -1000/-200", s.OsnovicaOpsta, s.PdvOpsta)
|
||||||
|
}
|
||||||
|
if !blizu(s.Ukupno, -1380) {
|
||||||
|
t.Errorf("ukupno=%v, očekivano -1380", s.Ukupno)
|
||||||
|
}
|
||||||
|
if original.Ukupno != 1380 {
|
||||||
|
t.Errorf("original izmenjen: ukupno=%v, očekivano da ostane 1380", original.Ukupno)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestKprIzNabavke(t *testing.T) {
|
func TestKprIzNabavke(t *testing.T) {
|
||||||
nabavka := Nabavka{ID: 3, Napomena: "test", Datum: time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)}
|
nabavka := Nabavka{ID: 3, Napomena: "test", Datum: time.Date(2026, 6, 2, 0, 0, 0, 0, time.UTC)}
|
||||||
stavke := []NabavkaStavkaPdv{
|
stavke := []NabavkaStavkaPdv{
|
||||||
|
|||||||
Reference in New Issue
Block a user