Files
2026-10-09 01:45:17 +00:00

206 lines
6.0 KiB
Typst
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ==========================================
// PROFILE: ДОГОВОР
// ==========================================
#import "../../domain/document.typ": document-profile, fail
#import "../../domain/attachments.typ": attachment-set
#import "../../domain/parties.typ": validate-parties, signer
#import "../../shared/numbering.typ": attachment_numbering
#import "../foundation.typ": apply-foundation
#import "../components.typ": appendix-heading, multi-party-signing, requisites-table
#let contract-section(
id,
title,
body,
number: auto,
level: 1,
) = {
if type(id) != str or id.trim() == "" {
fail("contract-section.id", "нужна непустая строка")
}
if type(title) not in (str, content) {
fail("contract-section.title", "ожидались str или content")
}
if type(body) not in (content, function) {
fail("contract-section.body", "ожидались content или function")
}
if type(level) != int or level < 1 {
fail("contract-section.level", "ожидалось положительное целое число")
}
(
kind: "contract-section",
id: id,
title: title,
body: body,
number: number,
level: level,
)
}
#let validate-sections(sections) = {
if type(sections) != array {
fail("contract.sections", "ожидался array")
}
let seen = ()
for section in sections {
if type(section) != dictionary or section.at("kind", default: none) != "contract-section" {
fail("contract.sections", "каждый элемент должен быть contract-section")
}
if section.id in seen { fail("contract.sections", "повторяющийся id " + repr(section.id)) }
seen.push(section.id)
}
sections
}
#let normalize-contract(metadata) = {
if type(metadata.attachments) == array {
metadata.insert("attachments", attachment-set(items: metadata.attachments))
}
metadata
}
#let validate-contract(metadata) = {
if type(metadata.title) not in (str, content) {
fail("contract.title", "ожидались str или content")
}
if metadata.number == none {
fail("contract.number", "номер договора обязателен")
}
let _ = validate-parties(metadata.parties, minimum: 2)
let _ = validate-sections(metadata.sections)
if type(metadata.attachments) != dictionary or metadata.attachments.at("kind", default: none) != "attachment-set" {
fail("contract.attachments", "ожидался attachment-set")
}
metadata
}
#let contract-preamble(data) = {
if data.preamble == auto {
[Стороны, указанные в разделе «Реквизиты и подписи сторон», заключили настоящий договор о нижеследующем.]
} else {
data.preamble
}
}
#let render-contract-sections(sections) = {
for (index, section) in sections.enumerate() {
let number = if section.number == auto { index + 1 } else { section.number }
heading(level: section.level, numbering: none)[#number. #upper(section.title)]
if type(section.body) == function { (section.body)() } else { section.body }
}
}
#let signing-parties(parties, include-media: true) = {
parties.map(item => {
if item.representative == none or include-media {
item
} else {
let person = item.representative
let clean-person = signer(
person.name,
person.title,
basis: person.basis,
signature: none,
stamp: none,
)
let clean-item = item
clean-item.insert("representative", clean-person)
clean-item
}
})
}
#let render-contract-attachments(attachments) = {
for (index, item) in attachments.items.enumerate() {
pagebreak()
let source-number = if item.number == auto { index + attachments.start } else { item.number }
let display-number = attachment_numbering(attachments.numbering, source-number)
appendix-heading(item, display-number)
v(0.5cm)
if type(item.body) == function { (item.body)() } else { item.body }
}
}
#let render-contract(body, ctx) = {
let data = ctx.metadata
let include-media = ctx.options.mode == "final"
let media-policy = if include-media { ctx.options.at("media-policy") } else { "reserve-space" }
let signing = signing-parties(data.parties, include-media: include-media)
let composed = [
#set heading(numbering: "1.1")
#show heading: it => {
set text(font: "Times New Roman", size: 11pt, weight: "bold", hyphenate: false)
set par(first-line-indent: 0pt, justify: false)
v(1em, weak: true)
block(sticky: true, width: 100%)[#it]
v(0.5em)
}
#align(center)[
#text(weight: "bold", size: 14pt)[#upper(data.title)] \
№ #data.number
]
#v(0.7cm)
#grid(
columns: (1fr, 1fr),
[#data.place],
align(right)[#data.date],
)
#v(0.8cm)
#contract-preamble(data)
#v(0.6cm)
#if data.sections.len() > 0 { render-contract-sections(data.sections) }
#body
#pagebreak(weak: true)
#heading(numbering: none)[РЕКВИЗИТЫ И ПОДПИСИ СТОРОН]
#requisites-table(data.parties)
#v(1cm)
#multi-party-signing(signing, media_policy: media-policy)
#if data.attachments.items.len() > 0 {
render-contract-attachments(data.attachments)
}
]
apply-foundation(
composed,
ctx: ctx,
tokens: (
text-font: "Times New Roman",
text-size: 11pt,
line-height: 1.15em,
page-margin: (left: 2.5cm, right: 2cm, top: 2cm, bottom: 2cm),
),
)
}
#let contract-profile(
number: none,
date: none,
place: "Екатеринбург",
title: "Договор",
parties: (),
preamble: auto,
sections: (),
attachments: attachment-set(),
) = {
document-profile(
"contract",
render-contract,
metadata: (
number: number,
date: date,
place: place,
title: title,
parties: parties,
preamble: preamble,
sections: sections,
attachments: attachments,
bibliographies: (),
),
normalize: normalize-contract,
validate: validate-contract,
)
}