commercial-offer.typ 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. // ==========================================
  2. // PROFILE: ТЕХНИКО-КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ
  3. // ==========================================
  4. #import "../../domain/document.typ": document-profile, fail
  5. #import "../../domain/attachments.typ": attachment-set
  6. #import "../../domain/parties.typ": signer
  7. #import "../foundation.typ": apply-foundation
  8. #import "../components.typ": attachment-list, appendix-heading, company-footer, letter-header, signature-block
  9. #let commercial-terms(
  10. amount: none,
  11. currency: "RUB",
  12. tax_note: none,
  13. delivery_term: none,
  14. validity: none,
  15. payment_terms: none,
  16. ) = {
  17. (
  18. amount: amount,
  19. currency: currency,
  20. tax-note: tax_note,
  21. delivery-term: delivery_term,
  22. validity: validity,
  23. payment-terms: payment_terms,
  24. )
  25. }
  26. #let normalize-offer(metadata) = {
  27. if type(metadata.attachments) == array {
  28. metadata.insert("attachments", attachment-set(items: metadata.attachments))
  29. }
  30. metadata
  31. }
  32. #let validate-offer(metadata) = {
  33. if metadata.recipient == none or type(metadata.recipient) != dictionary or metadata.recipient.at("kind", default: none) != "recipient" {
  34. fail("commercial-offer.recipient", "ожидался recipient")
  35. }
  36. if type(metadata.subject) != str or metadata.subject.trim() == "" {
  37. fail("commercial-offer.subject", "предмет предложения обязателен")
  38. }
  39. if type(metadata.terms) != dictionary {
  40. fail("commercial-offer.terms", "ожидался commercial-terms")
  41. }
  42. if metadata.terms.amount == none {
  43. fail("commercial-offer.terms.amount", "стоимость обязательна")
  44. }
  45. if type(metadata.attachments) != dictionary or metadata.attachments.at("kind", default: none) != "attachment-set" {
  46. fail("commercial-offer.attachments", "ожидался attachment-set")
  47. }
  48. metadata
  49. }
  50. #let effective-offer-signer(data, company, include-media: true) = {
  51. let source = if data.signer == none {
  52. signer(
  53. company.director.name,
  54. company.director.title,
  55. signature: company.resources.signature,
  56. stamp: company.resources.stamp,
  57. )
  58. } else {
  59. data.signer
  60. }
  61. signer(
  62. source.name,
  63. source.title,
  64. basis: source.basis,
  65. signature: if include-media { source.signature } else { none },
  66. stamp: if include-media { source.stamp } else { none },
  67. )
  68. }
  69. #let offer-summary(data) = {
  70. let terms = data.terms
  71. set par(first-line-indent: 0pt, justify: false, leading: 0.7em)
  72. table(
  73. columns: (4cm, 1fr),
  74. stroke: 0.5pt + luma(170),
  75. inset: 6pt,
  76. fill: (x, _) => if x == 0 { luma(245) } else { none },
  77. [*Предмет*], [#data.subject],
  78. [*Стоимость*], [#terms.amount #terms.currency],
  79. ..if terms.at("tax-note") != none { ([*Налоги*], [#terms.at("tax-note")]) } else { () },
  80. ..if terms.at("delivery-term") != none { ([*Срок выполнения*], [#terms.at("delivery-term")]) } else { () },
  81. ..if terms.validity != none { ([*Срок действия*], [#terms.validity]) } else { () },
  82. ..if terms.at("payment-terms") != none { ([*Порядок оплаты*], [#terms.at("payment-terms")]) } else { () },
  83. )
  84. }
  85. #let render-offer-attachments(attachments, appendix-state) = {
  86. for (index, item) in attachments.items.enumerate() {
  87. pagebreak()
  88. appendix-state.update(true)
  89. let number = if item.number == auto { index + attachments.start } else { item.number }
  90. appendix-heading(item, str(number))
  91. v(0.5cm)
  92. if type(item.body) == function { (item.body)() } else { item.body }
  93. }
  94. }
  95. #let render-commercial-offer(body, ctx) = {
  96. let company = ctx.company
  97. let data = ctx.metadata
  98. let include-media = ctx.options.mode == "final"
  99. let person = effective-offer-signer(data, company, include-media: include-media)
  100. let media-policy = if include-media { ctx.options.at("media-policy") } else { "reserve-space" }
  101. let appendix-state = state("scientia-offer-in-appendix", false)
  102. let footer = context {
  103. if not appendix-state.get() { company-footer(company) }
  104. }
  105. let composed = [
  106. #letter-header(
  107. company,
  108. data.recipient,
  109. date: data.date,
  110. reference: data.reference,
  111. title: data.title,
  112. media_policy: ctx.options.at("media-policy"),
  113. )
  114. #v(0.6cm)
  115. #offer-summary(data)
  116. #v(0.7cm)
  117. #body
  118. #if data.attachments.items.len() > 0 {
  119. v(0.8cm)
  120. attachment-list(data.attachments)
  121. }
  122. #signature-block(
  123. person,
  124. company: company,
  125. media_policy: media-policy,
  126. show_stamp: data.at("show-stamp"),
  127. note: data.note,
  128. )
  129. #if data.at("render-attachments") and data.attachments.items.len() > 0 {
  130. render-offer-attachments(data.attachments, appendix-state)
  131. }
  132. ]
  133. apply-foundation(
  134. composed,
  135. ctx: ctx,
  136. tokens: (
  137. text-font: "Arial",
  138. text-size: 10pt,
  139. line-height: 0.65em,
  140. page-margin: (top: 1.5cm, bottom: 4cm, left: 1.5cm, right: 1.5cm),
  141. ),
  142. page-options: (
  143. footer: footer,
  144. footer-descent: 20%,
  145. ),
  146. )
  147. }
  148. #let commercial-offer-profile(
  149. recipient: none,
  150. subject: "",
  151. amount: none,
  152. currency: "RUB",
  153. tax_note: none,
  154. delivery_term: none,
  155. validity: none,
  156. payment_terms: none,
  157. date: none,
  158. reference: none,
  159. title: "Технико-коммерческое предложение",
  160. signer: none,
  161. note: none,
  162. show_stamp: true,
  163. attachments: attachment-set(),
  164. render_attachments: true,
  165. ) = {
  166. document-profile(
  167. "commercial-offer",
  168. render-commercial-offer,
  169. metadata: (
  170. recipient: recipient,
  171. subject: subject,
  172. terms: commercial-terms(
  173. amount: amount,
  174. currency: currency,
  175. tax_note: tax_note,
  176. delivery_term: delivery_term,
  177. validity: validity,
  178. payment_terms: payment_terms,
  179. ),
  180. date: date,
  181. reference: reference,
  182. title: title,
  183. signer: signer,
  184. note: note,
  185. show-stamp: show_stamp,
  186. parties: (),
  187. attachments: attachments,
  188. bibliographies: (),
  189. render-attachments: render_attachments,
  190. ),
  191. normalize: normalize-offer,
  192. validate: validate-offer,
  193. )
  194. }