Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions frontend/src/routes/settings/firma/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,17 @@
class="mt-1 w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm text-primary focus:border-accent focus:ring-1 focus:ring-accent/50 focus:outline-none"
/>
</div>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div>
<label for="title" class="block text-sm font-medium text-secondary">Titul</label>
<input
id="title"
type="text"
value={field('title')}
oninput={(e) => setField('title', (e.target as HTMLInputElement).value)}
class="mt-1 w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm text-primary focus:border-accent focus:ring-1 focus:ring-accent/50 focus:outline-none"
/>
</div>
<div>
<label for="first_name" class="block text-sm font-medium text-secondary">Jméno</label>
<input
Expand Down Expand Up @@ -284,6 +294,18 @@
class="mt-1 w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm text-primary focus:border-accent focus:ring-1 focus:ring-accent/50 focus:outline-none"
/>
</div>
<div>
<label for="databox_id" class="block text-sm font-medium text-secondary"
>Datová schránka</label
>
<input
id="databox_id"
type="text"
value={field('databox_id')}
oninput={(e) => setField('databox_id', (e.target as HTMLInputElement).value)}
class="mt-1 w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm text-primary focus:border-accent focus:ring-1 focus:ring-accent/50 focus:outline-none"
/>
</div>
</div>
</Card>

Expand Down Expand Up @@ -445,7 +467,20 @@
value={field('cssz_code')}
oninput={(e) => setField('cssz_code', (e.target as HTMLInputElement).value)}
class="mt-1 w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm text-primary focus:border-accent focus:ring-1 focus:ring-accent/50 focus:outline-none"
placeholder="např. prehledosvc"
placeholder="např. 775"
/>
</div>
<div>
<label for="cssz_variable_symbol" class="block text-sm font-medium text-secondary"
>VS pro ČSSZ</label
>
<input
id="cssz_variable_symbol"
type="text"
value={field('cssz_variable_symbol')}
oninput={(e) =>
setField('cssz_variable_symbol', (e.target as HTMLInputElement).value)}
class="mt-1 w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm text-primary focus:border-accent focus:ring-1 focus:ring-accent/50 focus:outline-none"
/>
</div>
<div>
Expand Down
10 changes: 9 additions & 1 deletion frontend/src/routes/settings/firma/page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function jsonResponse(data: unknown, status = 200) {

const sampleSettings = {
company_name: 'Test OSVC',
title: 'Ing.',
first_name: 'Jan',
last_name: 'Novak',
ico: '12345678',
Expand All @@ -26,13 +27,16 @@ const sampleSettings = {
zip: '10000',
email: 'test@example.com',
phone: '+420123456789',
databox_id: '7rgmm99',
bank_account: '123456789',
bank_code: '0100',
iban: 'CZ1234567890',
swift: 'KOMBCZPP',
c_ufo: '464',
c_pracufo: '3305',
c_okec: '582900'
c_okec: '582900',
cssz_code: '775',
cssz_variable_symbol: '75660248'
};

beforeEach(() => {
Expand Down Expand Up @@ -64,6 +68,7 @@ describe('Settings Firma Page', () => {
});
expect(document.querySelector('#first_name')).toBeInTheDocument();
expect(document.querySelector('#last_name')).toBeInTheDocument();
expect(document.querySelector('#title')).toBeInTheDocument();
expect(document.querySelector('#ico')).toBeInTheDocument();
expect(document.querySelector('#dic')).toBeInTheDocument();
expect(document.querySelector('#vat_registered')).toBeInTheDocument();
Expand All @@ -73,13 +78,16 @@ describe('Settings Firma Page', () => {
expect(document.querySelector('#zip')).toBeInTheDocument();
expect(document.querySelector('#email')).toBeInTheDocument();
expect(document.querySelector('#phone')).toBeInTheDocument();
expect(document.querySelector('#databox_id')).toBeInTheDocument();
expect(document.querySelector('#bank_account')).toBeInTheDocument();
expect(document.querySelector('#bank_code')).toBeInTheDocument();
expect(document.querySelector('#iban')).toBeInTheDocument();
expect(document.querySelector('#swift')).toBeInTheDocument();
expect(document.querySelector('#c_ufo')).toBeInTheDocument();
expect(document.querySelector('#c_pracufo')).toBeInTheDocument();
expect(document.querySelector('#c_okec')).toBeInTheDocument();
expect(document.querySelector('#cssz_code')).toBeInTheDocument();
expect(document.querySelector('#cssz_variable_symbol')).toBeInTheDocument();
});

it('shows settings values in inputs', async () => {
Expand Down
140 changes: 102 additions & 38 deletions internal/annualtaxxml/social_insurance_gen.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,38 @@ package annualtaxxml
import (
"encoding/xml"
"fmt"
"regexp"
"strconv"
"strings"

"github.com/zajca/zfaktury/internal/domain"
)

var csszDepCodePattern = regexp.MustCompile(`^\d{3}$`)

// GenerateSocialInsuranceXML produces CSSZ OSVC overview XML from a SocialInsuranceOverview
// and application settings containing taxpayer information.
func GenerateSocialInsuranceXML(sio *domain.SocialInsuranceOverview, settings map[string]string) ([]byte, error) {
if sio == nil {
return nil, fmt.Errorf("social insurance overview is nil: %w", domain.ErrInvalidInput)
}
if err := validateSocialInsuranceXMLInput(settings); err != nil {
return nil, err
}

rok := strconv.Itoa(sio.Year)

assessmentBase := ToWholeCZK(sio.AssessmentBase)
minAssessmentBase := ToWholeCZK(sio.MinAssessmentBase)
finalAssessmentBase := ToWholeCZK(sio.FinalAssessmentBase)
totalInsurance := ToWholeCZK(sio.TotalInsurance)
prepayments := ToWholeCZK(sio.Prepayments)
difference := ToWholeCZK(sio.Difference)
totalRevenue := ToWholeCZK(sio.TotalRevenue)
totalExpenses := ToWholeCZK(sio.TotalExpenses)
newMonthlyPrepay := ToWholeCZK(sio.NewMonthlyPrepay)
monthlyAssessmentBase := divideRoundedUp(finalAssessmentBase, 12)

// Determine flat-rate flag for zal section.
pauFlag := "N"
if settings["flat_rate_expenses"] == "true" {
pauFlag = "A"
priznElektr := "A"
if v := strings.TrimSpace(settings["tax_return_filed_electronically"]); v != "" {
priznElektr = v
}

doc := &OSVC{
Expand All @@ -43,21 +47,21 @@ func GenerateSocialInsuranceXML(sio *domain.SocialInsuranceOverview, settings ma
Sender: Sender{
EmailNotifikace: "",
ISDSreport: "3",
VerzeProtokolu: "1",
VerzeProtokolu: "1.0",
},
Prehled: PrehledOSVC{
For: settings["cssz_code"],
Dep: "122",
For: "prehledosvc",
Dep: strings.TrimSpace(settings["cssz_code"]),
Rok: rok,
Typ: CSSZFilingTypeCode(sio.FilingType),
VSDP: "",
VSDP: strings.TrimSpace(settings["cssz_variable_symbol"]),
Dat: "",

Client: Client{
Name: ClientName{
Fir: settings["taxpayer_first_name"],
Sur: settings["taxpayer_last_name"],
Tit: "",
Tit: settings["taxpayer_title"],
},
Birth: ClientBirth{
Bno: settings["taxpayer_birth_number"],
Expand All @@ -70,43 +74,46 @@ func GenerateSocialInsuranceXML(sio *domain.SocialInsuranceOverview, settings ma
Cit: settings["taxpayer_city"],
Cnt: "CZ",
},
Druc: "H",
IDDS: settings["taxpayer_databox_id"],
Email: settings["taxpayer_email"],
Tel: settings["taxpayer_phone"],
Druc: "H",
Hlavc: Hlavc{
MonthFlags: MonthFlags{M1: "A"},
MonthFlags: MonthFlags{M13: "A"},
},
Vedc: Vedc{},
Sleva: MonthFlags{M1: "n"},
Sleva: MonthFlags{},
},

PVV: PVV{
Pri: "1",
Mesc: HVPair{H: "1", V: ""},
Mesv: HVPair{H: "1", V: ""},
Mesp: "1",
Rdza: HVPair{
H: strconv.FormatInt(totalRevenue, 10),
V: strconv.FormatInt(totalExpenses, 10),
},
Pri: formatAmount(sio.TaxBase),
Mesc: HVPair{H: "12", V: ""},
Mesv: HVPair{H: "12", V: ""},
Mesp: formatMonthlyAverage(sio.TaxBase, 12),
Rdza: HVPair{H: "0", V: "0"},
VVZ: HVPair{
H: strconv.FormatInt(assessmentBase, 10),
V: "0",
},
DVZ: HVPair{H: "0", V: "0"},
MVZ: strconv.FormatInt(minAssessmentBase, 10),
UVZ: strconv.FormatInt(finalAssessmentBase, 10),
Vzsu: strconv.FormatInt(finalAssessmentBase, 10),
Vzsvc: strconv.FormatInt(finalAssessmentBase, 10),
Poj: strconv.FormatInt(totalInsurance, 10),
Zal: strconv.FormatInt(prepayments, 10),
Ned: strconv.FormatInt(difference, 10),
DVZ: HVPair{H: "0", V: "0"},
MVZ: strconv.FormatInt(finalAssessmentBase, 10),
UVZ: strconv.FormatInt(finalAssessmentBase, 10),
Vzsu: strconv.FormatInt(finalAssessmentBase, 10),
Vzsvc: strconv.FormatInt(finalAssessmentBase, 10),
Poj: strconv.FormatInt(totalInsurance, 10),
Slev: "0",
Pojposlev: strconv.FormatInt(totalInsurance, 10),
Zal: strconv.FormatInt(prepayments, 10),
Ned: strconv.FormatInt(difference, 10),
},

Zal: Zal{
Pau: pauFlag,
VZ: strconv.FormatInt(finalAssessmentBase, 10),
Ved: "H",
Pau: settings["flat_tax_regime"],
VZ: strconv.FormatInt(monthlyAssessmentBase, 10),
DP: strconv.FormatInt(newMonthlyPrepay, 10),
NP: "0",
Duch: "",
NP: strings.TrimSpace(settings["sickness_insurance_monthly"]),
Duch: strings.TrimSpace(settings["working_pensioner_discount"]),
},

Pre: Pre{
Expand All @@ -115,15 +122,20 @@ func GenerateSocialInsuranceXML(sio *domain.SocialInsuranceOverview, settings ma
},

Prizn: Prizn{
Pau: "H",
Pau: settings["flat_tax_regime_reason"],
Pov: "A",
Elektr: "N",
Por: "N",
Elektr: priznElektr,
Por: settings["tax_advisor_after_april"],
},

Spo: Spo{
Name: SpoName{},
},
Prilo: Prilo{
Coun: "0",
Plnamoc: "N",
Jina: "N",
},
},
}

Expand All @@ -139,3 +151,55 @@ func GenerateSocialInsuranceXML(sio *domain.SocialInsuranceOverview, settings ma

return result, nil
}

func validateSocialInsuranceXMLInput(settings map[string]string) error {
required := []struct {
label string
key string
}{
{"kód OSSZ", "cssz_code"},
{"jméno", "taxpayer_first_name"},
{"příjmení", "taxpayer_last_name"},
{"rodné číslo / EČP", "taxpayer_birth_number"},
{"datum narození", "taxpayer_birth_date"},
{"číslo domu", "taxpayer_house_number"},
{"PSČ", "taxpayer_postal_code"},
{"obec", "taxpayer_city"},
}

var problems []string
for _, req := range required {
if strings.TrimSpace(settings[req.key]) == "" {
problems = append(problems, "chybí "+req.label)
}
}
if dep := strings.TrimSpace(settings["cssz_code"]); dep != "" && !csszDepCodePattern.MatchString(dep) {
problems = append(problems, "kód OSSZ musí mít přesně 3 číslice")
}
if len(problems) == 0 {
return nil
}
return fmt.Errorf("nelze vygenerovat XML přehledu ČSSZ: %s: %w", strings.Join(problems, "; "), domain.ErrInvalidInput)
}

func formatAmount(amount domain.Amount) string {
return amount.String()
}

func formatMonthlyAverage(amount domain.Amount, months int64) string {
if months <= 0 {
return "0.00"
}
return domain.Amount(int64(amount) / months).String()
}

func divideRoundedUp(value int64, divisor int64) int64 {
if divisor <= 0 {
return 0
}
result := value / divisor
if value%divisor != 0 {
result++
}
return result
}
Loading
Loading