components.typ 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. // ==========================================
  2. // PRESENTATION: ПЕРЕИСПОЛЬЗУЕМЫЕ КОМПОНЕНТЫ
  3. // ==========================================
  4. #import "../components.typ": corp_table, formula
  5. #import "foundation.typ": render-media-slot
  6. #import "../shared/numbering.typ": attachment_numbering
  7. #let letter-accent = rgb("fbb20d")
  8. #let footer-icon(name, color) = {
  9. box(width: 20pt, height: 20pt, fill: color, radius: 2pt)[
  10. #align(center + horizon)[
  11. #image(
  12. path("../assets/icons/" + name + ".png"),
  13. width: 15pt,
  14. height: 15pt,
  15. fit: "contain",
  16. )
  17. ]
  18. ]
  19. }
  20. #let company-logo(company, width: 4cm, height: 2.5cm, policy: "placeholder") = {
  21. render-media-slot(
  22. company.resources.logo,
  23. policy: policy,
  24. width: width,
  25. height: height,
  26. label: "ЛОГОТИП",
  27. )
  28. }
  29. #let signing-media(stamp, signature, policy, show_stamp: true, offset: -1cm) = {
  30. let has-missing-media = signature == none or (show_stamp and stamp == none)
  31. let stamp-resource = if stamp == none and policy == "placeholder" {
  32. path("../assets/placeholders/stamp.svg")
  33. } else {
  34. stamp
  35. }
  36. let signature-resource = if signature == none and policy == "placeholder" {
  37. path("../assets/placeholders/signature.svg")
  38. } else {
  39. signature
  40. }
  41. if policy == "placeholder" and has-missing-media {
  42. if show_stamp {
  43. place(center, dx: -1.6cm, dy: offset)[
  44. #render-media-slot(
  45. stamp-resource,
  46. policy: policy,
  47. width: 2.8cm,
  48. height: 2.8cm,
  49. label: "ПЕЧАТЬ",
  50. )
  51. ]
  52. }
  53. place(center, dx: if show_stamp { 1.6cm } else { 0cm }, dy: offset)[
  54. #render-media-slot(
  55. signature-resource,
  56. policy: policy,
  57. width: 2.8cm,
  58. height: 1.4cm,
  59. label: "ПОДПИСЬ",
  60. )
  61. ]
  62. } else {
  63. if show_stamp {
  64. place(center, dx: -0.5cm, dy: offset)[
  65. #render-media-slot(
  66. stamp-resource,
  67. policy: policy,
  68. width: 3.5cm,
  69. height: 3.5cm,
  70. label: "МЕСТО ПЕЧАТИ",
  71. )
  72. ]
  73. }
  74. place(center, dx: if show_stamp { 1.5cm } else { 0cm }, dy: offset)[
  75. #render-media-slot(
  76. signature-resource,
  77. policy: policy,
  78. width: 3.5cm,
  79. height: 2.5cm,
  80. label: "МЕСТО ПОДПИСИ",
  81. )
  82. ]
  83. }
  84. }
  85. #let signature-block(
  86. signer,
  87. company: none,
  88. media_policy: "placeholder",
  89. show_stamp: true,
  90. note: none,
  91. ) = {
  92. let signature = signer.at(
  93. "signature",
  94. default: if company == none { none } else { company.resources.signature },
  95. )
  96. let stamp = signer.at(
  97. "stamp",
  98. default: if company == none { none } else { company.resources.stamp },
  99. )
  100. set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
  101. let left-content = [
  102. #signer.title
  103. #if company != none { [\ #company.legal.name] }
  104. ]
  105. let right-content = [#signer.name]
  106. let media-content = signing-media(
  107. stamp,
  108. signature,
  109. media_policy,
  110. show_stamp: show_stamp,
  111. )
  112. v(2cm)
  113. layout(size => {
  114. let left-width = measure(left-content).width
  115. let right-width = measure(right-content).width
  116. let roomy = left-width + right-width + 5.5cm <= size.width
  117. if roomy {
  118. grid(
  119. columns: (auto, 1fr, auto),
  120. align: (left + bottom, center + bottom, right + bottom),
  121. left-content,
  122. media-content,
  123. right-content,
  124. )
  125. } else {
  126. grid(
  127. columns: (1fr, 6.5cm, 1fr),
  128. column-gutter: 8pt,
  129. align: (left + bottom, center + bottom, right + bottom),
  130. left-content,
  131. media-content,
  132. right-content,
  133. )
  134. }
  135. })
  136. if note != none {
  137. text(font: "Arial", size: 7.5pt, fill: luma(90))[#note]
  138. }
  139. }
  140. #let approval-block(approval, company, media_policy: "placeholder") = {
  141. let signer = approval.signer
  142. let signature = signer.at("signature", default: company.resources.signature)
  143. let signature-resource = if signature == none and media_policy == "placeholder" {
  144. path("../assets/placeholders/signature.svg")
  145. } else {
  146. signature
  147. }
  148. set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
  149. box(width: 8cm)[
  150. #align(right)[
  151. #text(weight: "bold")[#approval.label] \
  152. #signer.title \
  153. #company.legal.name \
  154. #v(0.2cm)
  155. #render-media-slot(
  156. signature-resource,
  157. policy: media_policy,
  158. width: 3cm,
  159. height: 1.2cm,
  160. label: "ПОДПИСЬ",
  161. ) \
  162. #signer.name \
  163. #if approval.date != none { approval.date }
  164. ]
  165. ]
  166. }
  167. #let company-footer(company) = {
  168. let contacts = company.contacts
  169. let banking = company.banking
  170. let contact-lines = ()
  171. if contacts.phone != none { contact-lines.push(contacts.phone) }
  172. if contacts.email != none { contact-lines.push(contacts.email) }
  173. if contacts.website != none { contact-lines.push(contacts.website) }
  174. let id-lines = ()
  175. for (key, label) in (
  176. ("inn", "ИНН"),
  177. ("kpp", "КПП"),
  178. ("ogrn", "ОГРН"),
  179. ("bin", "БИН"),
  180. ("kbe", "КБЕ"),
  181. ) {
  182. let value = company.legal.at(key, default: none)
  183. if value != none { id-lines.push([#label #value]) }
  184. }
  185. let banking-lines = ()
  186. if banking.bank != none { banking-lines.push([Банк: #banking.bank]) }
  187. if banking.at("correspondent-account", default: none) != none {
  188. banking-lines.push([Кор. счёт: #banking.at("correspondent-account")])
  189. }
  190. if banking.bik != none { banking-lines.push([БИК: #banking.bik]) }
  191. if contacts.at("address-en", default: none) != none {
  192. banking-lines.push(contacts.at("address-en"))
  193. }
  194. set text(font: "Arial", size: 7.5pt, fill: luma(85))
  195. set par(first-line-indent: 0pt, justify: false, leading: 0.2em)
  196. grid(
  197. columns: (1fr, 1.2fr),
  198. column-gutter: 15pt,
  199. [
  200. #grid(
  201. columns: (20pt, 1fr),
  202. rows: (auto, auto),
  203. column-gutter: 8pt,
  204. row-gutter: 8pt,
  205. if contact-lines.len() == 0 { none } else { footer-icon("email", company.brand.color) },
  206. contact-lines.join([\ ]),
  207. if contacts.address == none { none } else { footer-icon("location", company.brand.color) },
  208. if contacts.address == none { none } else { contacts.address },
  209. )
  210. ],
  211. grid(
  212. columns: (1fr, 20pt),
  213. rows: (auto, auto),
  214. column-gutter: 8pt,
  215. row-gutter: 8pt,
  216. align: (right, center + horizon),
  217. align(right, id-lines.join([\ ])),
  218. if id-lines.len() == 0 { none } else { footer-icon("card", company.brand.color) },
  219. align(right, banking-lines.join([\ ])),
  220. if banking-lines.len() == 0 { none } else { footer-icon("bank", company.brand.color) },
  221. ),
  222. )
  223. }
  224. #let letter-header(
  225. company,
  226. recipient,
  227. date: none,
  228. reference: none,
  229. title: none,
  230. media_policy: "placeholder",
  231. ) = {
  232. set par(first-line-indent: 0pt, justify: false, leading: 0.65em)
  233. place(top + left, dx: 10cm, dy: -1.5cm)[
  234. #rect(
  235. width: 6.5cm,
  236. height: 2cm,
  237. fill: letter-accent,
  238. radius: (bottom: 2pt),
  239. )
  240. ]
  241. v(30pt)
  242. box(width: 4cm, height: 2.5cm)[
  243. #company-logo(company, width: 4cm, height: 2.5cm, policy: media_policy)
  244. ]
  245. v(-50pt)
  246. v(2.5cm)
  247. grid(
  248. columns: (1.4fr, 1fr),
  249. [
  250. #if reference != none { [Исх. № #reference] }
  251. #if date != none { [ от #date] }
  252. ],
  253. [
  254. #if recipient.company != none { [#recipient.company\ ] }
  255. #if recipient.title != none { [#recipient.title\ ] }
  256. #if recipient.name != none { recipient.name }
  257. #if recipient.address != none { [\ #recipient.address] }
  258. ],
  259. )
  260. v(0.7cm)
  261. if title != none {
  262. align(center)[#text(weight: "bold")[#upper(title)]]
  263. v(0.2cm)
  264. }
  265. }
  266. #let attachment-list(attachments) = {
  267. let items = if type(attachments) == dictionary { attachments.items } else { attachments }
  268. if items.len() > 0 {
  269. set par(first-line-indent: 0pt, justify: false)
  270. for (index, item) in items.enumerate() {
  271. let number = if item.number == auto { index + 1 } else { item.number }
  272. [Приложение #number: #item.title]
  273. if index < items.len() - 1 { linebreak() }
  274. }
  275. }
  276. }
  277. #let requisites-table(parties) = {
  278. let field-labels = (
  279. inn: "ИНН",
  280. kpp: "КПП",
  281. ogrn: "ОГРН",
  282. bin: "БИН",
  283. kbe: "КБе",
  284. address: "Юридический адрес",
  285. legal-address: "Юридический адрес",
  286. phone: "Телефон",
  287. email: "E-mail",
  288. website: "Сайт",
  289. bank: "Банк",
  290. account: "Расчётный счёт",
  291. settlement-account: "Расчётный счёт",
  292. correspondent-account: "Корреспондентский счёт",
  293. ks: "Корреспондентский счёт",
  294. bik: "БИК",
  295. iban: "IBAN",
  296. )
  297. let hidden-fields = (
  298. "name",
  299. "short-name",
  300. "jurisdiction",
  301. "summary",
  302. "address-en",
  303. "city",
  304. )
  305. let render-fields(fields) = {
  306. for (key, value) in fields {
  307. if value != none and key not in hidden-fields {
  308. let label = field-labels.at(key, default: key)
  309. [#label: #value\ ]
  310. }
  311. }
  312. }
  313. let columns = range(parties.len()).map(_ => 1fr)
  314. table(
  315. columns: columns,
  316. stroke: 0.5pt,
  317. inset: 6pt,
  318. ..parties.map(item => [
  319. #set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
  320. #text(weight: "bold")[#item.role] \
  321. #item.name \
  322. #render-fields(item.legal)
  323. #render-fields(item.contacts)
  324. #render-fields(item.banking)
  325. ]),
  326. )
  327. }
  328. #let multi-party-signing(parties, media_policy: "reserve-space") = {
  329. let columns = range(parties.len()).map(_ => 1fr)
  330. grid(
  331. columns: columns,
  332. column-gutter: 16pt,
  333. ..parties.map(item => {
  334. let person = item.representative
  335. [
  336. #set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
  337. #if person != none {
  338. grid(
  339. columns: (1fr,),
  340. rows: (auto, 0.35cm, 3.5cm, 0.25cm, auto),
  341. [#text(weight: "bold")[#item.role] \ #person.title],
  342. [],
  343. block(width: 100%, height: 3.5cm)[
  344. #signing-media(
  345. person.stamp,
  346. person.signature,
  347. media_policy,
  348. show_stamp: true,
  349. offset: 0cm,
  350. )
  351. ],
  352. [],
  353. [#person.name],
  354. )
  355. } else {
  356. text(weight: "bold")[#item.role]
  357. }
  358. ]
  359. }),
  360. )
  361. }
  362. #let appendix-heading(item, display-number) = {
  363. set par(first-line-indent: 0pt, justify: false, leading: 0.75em)
  364. align(right)[Приложение #display-number]
  365. v(0.2cm)
  366. align(center)[
  367. #text(weight: "bold")[#upper(item.title)]
  368. #if item.subtitle != none { [\ #text(weight: "bold")[#item.subtitle]] }
  369. ]
  370. }