summaryrefslogtreecommitdiff
path: root/LedgerSMB/Template/ODS.pm
blob: f3a3758314a4fda47027a73309a15a3b0ffdd273 (plain)
  1. =head1 NAME
  2. LedgerSMB::Template::ODS Template support module for LedgerSMB
  3. =head1 SYNOPSIS
  4. OpenDocument Spreadsheet output.
  5. =head1 METHODS
  6. =over
  7. =item get_template ($name)
  8. Returns the appropriate template filename for this format. '.xlst' is the
  9. extension that was chosen for the templates.
  10. =item preprocess ($vars)
  11. Returns $vars.
  12. =item process ($parent, $cleanvars)
  13. Processes the template for text.
  14. =item postprocess ($parent)
  15. Returns the output filename.
  16. =back
  17. =head1 Copyright (C) 2007, The LedgerSMB core team.
  18. This work contains copyrighted information from a number of sources all used
  19. with permission.
  20. It is released under the GNU General Public License Version 2 or, at your
  21. option, any later version. See COPYRIGHT file for details. For a full list
  22. including contact information of contributors, maintainers, and copyright
  23. holders, see the CONTRIBUTORS file.
  24. =cut
  25. package LedgerSMB::Template::ODS;
  26. use strict;
  27. use warnings;
  28. use Error qw(:try);
  29. use Data::Dumper;
  30. use CGI::Simple::Standard qw(:html);
  31. use Template;
  32. use XML::Twig;
  33. use OpenOffice::OODoc;
  34. use LedgerSMB::Template::TTI18N;
  35. # SC: The ODS handlers need these vars in common
  36. my $ods;
  37. my $rowcount;
  38. my $currcol;
  39. my %celltype;
  40. # SC: The elements of the style table for regular styles and stack are
  41. # arrays where the stack name is the first element and the style
  42. # properties are the second. The name is used for setting styles,
  43. # while the properties are used in handling nested styles.
  44. my @style_stack; # stack of styles, 0 is active style
  45. my %style_table; # hash table for created styles
  46. # SC: Subtract 8 from the attribute to get the index
  47. # http://search.cpan.org/src/JMCNAMARA/Spreadsheet-WriteExcel-2.11/doc/palette.html
  48. my @colour = (odfColor(0, 0, 0), odfColor(255, 255, 255),
  49. odfColor(255, 0, 0), odfColor(0, 255, 0),
  50. odfColor(0, 0, 255), odfColor(255, 255, 0),
  51. odfColor(255, 0, 255), odfColor(0, 255, 255),
  52. odfColor(128, 0, 0), odfColor(0, 128, 0),
  53. odfColor(0, 0, 128), odfColor(128, 128, 0),
  54. odfColor(128, 0, 128), odfColor(0, 128, 128),
  55. odfColor(192, 192, 192), odfColor(128, 128, 128),
  56. odfColor(153, 153, 255), odfColor(153, 51, 102),
  57. odfColor(255, 255, 204), odfColor(204, 255, 255),
  58. odfColor(102, 0, 102), odfColor(255, 128, 128),
  59. odfColor(0, 102, 204), odfColor(204, 204, 255),
  60. odfColor(0, 0, 128), odfColor(255, 0, 255),
  61. odfColor(255, 255, 0), odfColor(0, 255, 255),
  62. odfColor(128, 0, 128), odfColor(120, 0, 0),
  63. odfColor(0, 128, 128), odfColor(0, 0, 255),
  64. odfColor(0, 204, 255), odfColor(204, 255, 255),
  65. odfColor(204, 255, 204), odfColor(255, 255, 153),
  66. odfColor(153, 204, 255), odfColor(255, 153, 204),
  67. odfColor(204, 153, 255), odfColor(192, 192, 192),
  68. odfColor(51, 102, 255), odfColor(51, 204, 204),
  69. odfColor(153, 204, 0), odfColor(255, 204, 0),
  70. odfColor(255, 153, 0), odfColor(255, 102, 0),
  71. odfColor(102, 102, 153), odfColor(150, 150, 150),
  72. odfColor(0, 51, 102), odfColor(51, 153, 102),
  73. odfColor(0, 51, 0), odfColor(51, 51, 0),
  74. odfColor(153, 51, 0), odfColor(153, 51, 102),
  75. odfColor(51, 51, 153), odfColor(51, 51, 51),
  76. );
  77. my %colour_name = ('black' => $colour[0], 'white' => $colour[1],
  78. 'red' => $colour[2], 'lime' => $colour[3],
  79. 'blue' => $colour[4], 'yellow' => $colour[5],
  80. 'magenta' => $colour[6], 'cyan' => $colour[7],
  81. 'brown' => $colour[8], 'green' => $colour[9],
  82. 'navy' => $colour[10], 'purple' => $colour[12],
  83. 'silver' => $colour[14], 'gray' => $colour[15],
  84. 'grey' => $colour[15], 'orange' => $colour[45],
  85. );
  86. my @line_width = ('none', '0.018cm solid', '0.035cm solid',
  87. '0.018cm dashed', '0.018cm dotted', '0.141cm solid',
  88. '0.039cm double', '0.002cm solid'
  89. );
  90. sub _worksheet_handler {
  91. $rowcount = -1;
  92. $currcol = 0;
  93. my $rows = $_->{att}->{rows};
  94. my $columns = $_->{att}->{columns};
  95. $rows ||= 1000;
  96. $columns ||= 52;
  97. my $sheet;
  98. if ($_->is_first_child) {
  99. $sheet = $ods->getTable(0, $rows, $columns);
  100. $ods->renameTable($sheet, $_->{att}->{name});
  101. } else {
  102. $sheet = $ods->appendTable($_->{att}->{name}, $rows, $columns);
  103. }
  104. }
  105. sub _row_handler {
  106. $rowcount++;
  107. $currcol = 0;
  108. }
  109. sub _cell_handler {
  110. my $cell = $ods->getCell(-1, $rowcount, $currcol);
  111. if (@style_stack and $celltype{$style_stack[0][0]}) {
  112. $ods->cellValueType($cell, $celltype{$style_stack[0][0]}[0]);
  113. } elsif ($_->{att}->{type}) {
  114. my $type = $_->{att}->{type};
  115. if ($type =~ /^(string|blank|url)$/i) {
  116. $ods->cellValueType($cell, 'string');
  117. } elsif ($type =~ /^(number|formula)$/i) {
  118. $ods->cellValueType($cell, 'float');
  119. }
  120. }
  121. $ods->cellValue($cell, $_->{att}->{text});
  122. if (@style_stack) {
  123. $ods->cellStyle($cell, $style_stack[0][0]);
  124. }
  125. $currcol++;
  126. }
  127. sub _formula_handler {
  128. my $cell = $ods->getCell(-1, $rowcount, $currcol);
  129. if (@style_stack and $celltype{$style_stack[0][0]}) {
  130. $ods->cellValueType($cell, $celltype{$style_stack[0][0]}[0]);
  131. } elsif ($_->{att}->{type}) {
  132. my $type = $_->{att}->{type};
  133. if ($type =~ /^(string|blank|url)$/i) {
  134. $ods->cellValueType($cell, 'string');
  135. } elsif ($type =~ /^(number|formula)$/i) {
  136. $ods->cellValueType($cell, 'float');
  137. }
  138. }
  139. $ods->cellFormula($cell, "oooc:=$_->{att}->{text}");
  140. if (@style_stack) {
  141. $ods->cellStyle($cell, $style_stack[0][0]);
  142. }
  143. $currcol++;
  144. }
  145. sub _border_set {
  146. my ($format, $properties, $border) = @_;
  147. my $edge = $border;
  148. $edge =~ s/^border-?//;
  149. my $val;
  150. if ($edge) {
  151. $val = $format->{att}{$edge};
  152. } else {
  153. $val = $format->{att}{'border'};
  154. }
  155. if ($properties->{cell}{"fo:$border"}){
  156. $properties->{cell}{"fo:$border"} =~ s/^.* (\#......)$/$val $1/;
  157. } else {
  158. $properties->{cell}{"fo:$border"} = "$line_width[$val] #000000";
  159. }
  160. if ($edge and $format->{att}->{"${edge}_color"}) {
  161. my $colour = $format->{att}->{"${edge}_color"};
  162. if ($colour =~ /^\d+$/) {
  163. $colour = $colour[$colour];
  164. } elsif ($colour !~ /^\#......$/) {
  165. $colour = $colour_name{$colour};
  166. }
  167. $properties->{cell}{"fo:$border"} =~ s/^(.*) \#......$/$1 $colour/;
  168. } elsif ($format->{att}->{border_color}) {
  169. my $colour = $format->{att}->{"${edge}_color"};
  170. if ($colour =~ /^\d+$/) {
  171. $colour = $colour[$colour];
  172. } elsif ($colour !~ /^\#......$/) {
  173. $colour = $colour_name{$colour};
  174. }
  175. $properties->{cell}{"fo:$border"} =~ s/^(.*) \#......$/$1 $colour/;
  176. }
  177. }
  178. sub _prepare_float {
  179. my ($style) = shift;
  180. my %properties;
  181. my @sides = split /\./, $style;
  182. if ($#sides == 1) { # decimal places
  183. $properties{'number:decimal-places'} = length $sides[1];
  184. } else {
  185. $properties{'number:decimal-places'} = 0;
  186. }
  187. $properties{'number:min-integer-digits'} = length($sides[0] =~ /0+$/);
  188. $properties{'number:grouping'} = 'true' if $sides[0] =~ /.,...$/;
  189. \%properties;
  190. }
  191. sub _prepare_fraction {
  192. my ($style) = shift;
  193. my %properties;
  194. my @sides = split /[ \/]/, $style;
  195. $properties{'number:min-integer-digits'} = length($sides[0] =~ /0+$/);
  196. $properties{'number:min-numerator-digits'} = length($sides[1]);
  197. $properties{'number:min-denominator-digits'} = length($sides[2]);
  198. \%properties;
  199. }
  200. sub _create_positive_style {
  201. my ($name, $type, $base) = @_;
  202. my $pstyle = $ods->createStyle(
  203. $name,
  204. namespace => 'number',
  205. type => $type,
  206. properties => $base,
  207. references => {
  208. 'style:volatile' => 'true',
  209. },
  210. );
  211. $pstyle->insert_new_elt('last-child',
  212. 'number:text', {}, ' ');
  213. }
  214. sub _format_handler {
  215. my ($t, $format) = @_;
  216. my $style = sprintf "ce%d", (scalar (keys %style_table) + 1);
  217. my @extras;
  218. # SC: There are multiple types of properties that can be associated
  219. # with a style. However, the OO::OOD style creation code appears
  220. # to only allow for a single type to be added to the style at a
  221. # time. As a result, %properties is split into property groupings
  222. # to allow for each group to get the correct type.
  223. my %properties;
  224. if (@style_stack) {
  225. %properties = %{$style_stack[0][1]};
  226. if ($celltype{$style_stack[0][0]}) {
  227. $celltype{$style} = $celltype{$style_stack[0][0]};
  228. @extras = ('references', {
  229. 'style:data-style-name' => $celltype{$style}[1]
  230. });
  231. }
  232. }
  233. &_border_set(\%properties, $format, 'border') if $format->{att}->{border};
  234. while (my ($attr, $val) = each %{$format->{att}}) {
  235. if ($attr eq 'bottom') {
  236. &_border_set($format, \%properties, 'border-bottom');
  237. } elsif ($attr eq 'top') {
  238. &_border_set($format, \%properties, 'border-top');
  239. } elsif ($attr eq 'left') {
  240. &_border_set($format, \%properties, 'border-left');
  241. } elsif ($attr eq 'right') {
  242. &_border_set($format, \%properties, 'border-right');
  243. } elsif ($attr eq 'bg_color' or $attr eq 'bg_colour') {
  244. if ($val =~ /^\d+$/) {
  245. $properties{cell}{'fo:background-color'} =
  246. $colour[$val - 8];
  247. } elsif ($val =~ /^\#[0-9A-Fa-f]{6}$/) {
  248. $properties{cell}{'fo:background-color'} = $val;
  249. } else {
  250. $properties{cell}{'fo:background-color'} =
  251. $colour_name{$val};
  252. }
  253. } elsif ($attr eq 'color' or $attr eq 'colour') {
  254. if ($val =~ /^\d+$/) {
  255. $properties{text}{'fo:color'} =
  256. $colour[$val - 8];
  257. } elsif ($val =~ /^\#[0-9A-Fa-f]{6}$/) {
  258. $properties{text}{'fo:color'} = $val;
  259. } else {
  260. $properties{text}{'fo:color'} =
  261. $colour_name{$val};
  262. }
  263. } elsif ($attr eq 'align') {
  264. if (lc $val eq 'right') {
  265. $properties{paragraph}{'fo:text-align'} = 'end';
  266. } elsif (lc $val eq 'left') {
  267. $properties{paragraph}{'fo:text-align'} = 'start';
  268. } else {
  269. $properties{paragraph}{'fo:text-align'} = $val;
  270. }
  271. } elsif ($attr eq 'valign') {
  272. # takes top, vcenter, bottom, or vjustify
  273. # needs top, middle, or bottom
  274. if ($val =~ /^v/i) {
  275. $properties{paragraph}{'style:vertical-align'} = 'middle';
  276. } else {
  277. $properties{paragraph}{'style:vertical-align'} = $val;
  278. }
  279. } elsif ($attr eq 'hidden') {
  280. if ($properties{cell}{'style:cell-protect'} and !$val) {
  281. delete $properties{cell}{'style:cell-protect'};
  282. } elsif ($val) {
  283. $properties{cell}{'style:cell-protect'} = 'formula-hidden';
  284. }
  285. } elsif ($attr eq 'font') {
  286. $properties{text}{'style:font-name'} = $val;
  287. } elsif ($attr eq 'size') {
  288. $properties{text}{'fo:font-size'} = "${val}pt";
  289. } elsif ($attr eq 'bold') {
  290. if ($properties{text}{'fo:font-weight'} and !$val) {
  291. delete $properties{text}{'fo:font-weight'};
  292. } elsif ($val) {
  293. $properties{text}{'fo:font-weight'} = 'bold';
  294. }
  295. } elsif ($attr eq 'italic') {
  296. if ($properties{text}{'fo:font-style'} and !$val) {
  297. delete $properties{text}{'fo:font-style'};
  298. } elsif ($val) {
  299. $properties{text}{'fo:font-style'} = 'italic';
  300. }
  301. } elsif ($attr eq 'font_strikeout') {
  302. if (!$val) {
  303. $properties{text}{'style:text-line-through-type'} = 'none';
  304. } elsif ($val) {
  305. $properties{text}{'style:text-line-through-type'} = 'single';
  306. }
  307. } elsif ($attr eq 'font_shadow') {
  308. if ($properties{text}{'fo:text-shadow'} and !$val) {
  309. delete $properties{text}{'fo:text-shadow'};
  310. } elsif ($val) {
  311. $properties{text}{'fo:text-shadow'} = '2pt';
  312. }
  313. } elsif ($attr eq 'font_outline') {
  314. if ($properties{text}{'style:text-outline'} and !$val) {
  315. delete $properties{text}{'style:text-outline'};
  316. } elsif ($val) {
  317. $properties{text}{'style:text-outline'} = 'true';
  318. }
  319. } elsif ($attr eq 'shrink') {
  320. if ($properties{cell}{'style:shrink-to-fit'} and !$val) {
  321. delete $properties{cell}{'style:shrink-to-fit'};
  322. } elsif ($val) {
  323. $properties{cell}{'style:shrink-to-fit'} = 'true';
  324. }
  325. } elsif ($attr eq 'text_wrap') {
  326. if (!$val) {
  327. $properties{cell}{'style:wrap-option'} = 'no-wrap';
  328. } else {
  329. $properties{text}{'style:wrap-option'} = 'wrap';
  330. }
  331. } elsif ($attr eq 'text_justlast') {
  332. if ($properties{paragraph}{'fo:text-align-last'} and !$val) {
  333. delete $properties{paragraph}{'fo:text-align-last'};
  334. } elsif ($val) {
  335. $properties{paragraph}{'fo:text-align-last'} = 'justify';
  336. }
  337. } elsif ($attr eq 'num_format') {
  338. #SC: Number formatting is when I hit the point of,
  339. # "Screw the OO::OOD API, XML::Twig is simpler".
  340. # @children's elements are passed right into the
  341. # style via XML::Twig::Elt's insert_new_elt. The
  342. # OO:OOD API, while decent enough for the text
  343. # styles, is not so pleasant with complex number
  344. # styles.
  345. my @children;
  346. my %nproperties;
  347. my @nextras;
  348. my $nstyle;
  349. my $fval = sprintf 'N%02d', $val;
  350. @extras = ('references', {'style:data-style-name' => $fval});
  351. if ($style_table{$fval}) {
  352. # pass through
  353. } elsif ($val == 0) {
  354. $celltype{$style} = 'float';
  355. } elsif ($val == 1) {
  356. $celltype{$style} = ['float', 'N01'];
  357. $nstyle = 'number-style';
  358. %nproperties = %{&_prepare_float('0')}
  359. } elsif ($val == 2) {
  360. $celltype{$style} = ['float', 'N02'];
  361. $nstyle = 'number-style';
  362. %nproperties = %{&_prepare_float('0.00')}
  363. } elsif ($val == 3) {
  364. $celltype{$style} = ['float', 'N03'];
  365. $nstyle = 'number-style';
  366. %nproperties = %{&_prepare_float('#,##0')}
  367. } elsif ($val == 4) {
  368. $celltype{$style} = ['float', "N04"];
  369. $nstyle = 'number-style';
  370. %nproperties = %{&_prepare_float('#,##0.00')}
  371. } elsif ($val == 5) { ## ($#,##0_);($#,##0)
  372. } elsif ($val == 6) {
  373. $celltype{$style} = 'currency';
  374. } elsif ($val == 7) {
  375. $celltype{$style} = 'currency';
  376. } elsif ($val == 8) {
  377. $celltype{$style} = 'currency';
  378. } elsif ($val == 9) { ## 0%
  379. $celltype{$style} = ['percentage', "N09"];
  380. $nstyle = 'percentage-style';
  381. %nproperties = %{&_prepare_float('0')};
  382. push @children, ['number:text', {}, '%'];
  383. } elsif ($val == 10) { ## 0.00%
  384. $celltype{$style} = ['percentage', "N10"];
  385. $nstyle = 'percentage-style';
  386. %nproperties = %{&_prepare_float('0.00')};
  387. push @children, ['number:text', {}, '%'];
  388. } elsif ($val == 11) { ## 0.00E+00
  389. $celltype{$style} = ['float', "N11"];
  390. $nstyle = 'number-style';
  391. push @children, ['number:scientific-number', {
  392. %{&_prepare_float('0.00')},
  393. 'number:min-exponent-digits' => 2
  394. }];
  395. } elsif ($val == 12) { ## # ?/?
  396. $celltype{$style} = ['float', "N12"];
  397. $nstyle = 'number-style';
  398. push @children, ['number:fraction',
  399. %{&_prepare_fraction('# ?/?')}];
  400. } elsif ($val == 13) { ## # ??/??
  401. $celltype{$style} = ['float', "N13"];
  402. $nstyle = 'number-style';
  403. push @children, ['number:fraction',
  404. %{&_prepare_fraction('# ??/??')}];
  405. } elsif ($val == 14) { ## m/d/yy
  406. $celltype{$style} = ['date', "N14"];
  407. $nstyle = 'date-style';
  408. @nextras = ('references' => {
  409. 'number:automatic-order' => 'true'});
  410. @children = (
  411. ['number:month'],
  412. ['number:text', {}, '/'],
  413. ['number:day'],
  414. ['number:text', {}, '/'],
  415. ['number:year'],
  416. );
  417. } elsif ($val == 15) { ## d-mmm-yy
  418. $celltype{$style} = ['date', "N15"];
  419. $nstyle = 'date-style';
  420. @nextras = ('references' => {
  421. 'number:automatic-order' => 'true'});
  422. @children = (
  423. ['number:day'],
  424. ['number:text', {}, '-'],
  425. ['number:month', {
  426. 'number:textual' => 'true'}],
  427. ['number:text', {}, '-'],
  428. ['number:year'],
  429. );
  430. } elsif ($val == 16) { ## d-mmm
  431. $celltype{$style} = ['date', "N16"];
  432. $nstyle = 'date-style';
  433. @nextras = ('references' => {
  434. 'number:automatic-order' => 'true'});
  435. @children = (
  436. ['number:day'],
  437. ['number:text', {}, '-'],
  438. ['number:month', {
  439. 'number:textual' => 'true'}],
  440. );
  441. } elsif ($val == 17) { ## mmm-yy
  442. $celltype{$style} = ['date', "N17"];
  443. $nstyle = 'date-style';
  444. @nextras = ('references' => {
  445. 'number:automatic-order' => 'true'});
  446. @children = (
  447. ['number:month', {
  448. 'number:textual' => 'true'}],
  449. ['number:text', {}, '-'],
  450. ['number:year'],
  451. );
  452. } elsif ($val == 18) { ## h:mm AM/PM
  453. $celltype{$style} = ['time', "N18"];
  454. $nstyle = 'time-style';
  455. @nextras = ('references' => {
  456. 'number:automatic-order' => 'true'});
  457. @children = (
  458. ['number:hours'],
  459. ['number:text', {}, ':'],
  460. ['number:minutes',
  461. {'number:style' => 'long'}],
  462. ['number:text', {}, ' '],
  463. ['number:am-pm']
  464. );
  465. } elsif ($val == 19) { ## h:mm:ss AM/PM
  466. $celltype{$style} = ['time', "N19"];
  467. $nstyle = 'time-style';
  468. @nextras = ('references' => {
  469. 'number:automatic-order' => 'true'});
  470. @children = (
  471. ['number:hours'],
  472. ['number:text', {}, ':'],
  473. ['number:minutes',
  474. {'number:style' => 'long'}],
  475. ['number:text', {}, ':'],
  476. ['number:seconds',
  477. {'number:style' => 'long'}],
  478. ['number:text', {}, ' '],
  479. ['number:am-pm']
  480. );
  481. } elsif ($val == 20) { ## h:mm
  482. $celltype{$style} = ['time', "N20"];
  483. $nstyle = 'time-style';
  484. @nextras = ('references' => {
  485. 'number:automatic-order' => 'true'});
  486. @children = (
  487. ['number:hours'],
  488. ['number:text', {}, ':'],
  489. ['number:minutes',
  490. {'number:style' => 'long'}],
  491. );
  492. } elsif ($val == 21) { ## h:mm:ss
  493. $celltype{$style} = ['time', "N21"];
  494. $nstyle = 'time-style';
  495. @nextras = ('references' => {
  496. 'number:automatic-order' => 'true'});
  497. @children = (
  498. ['number:hours'],
  499. ['number:text', {}, ':'],
  500. ['number:minutes',
  501. {'number:style' => 'long'}],
  502. ['number:text', {}, ':'],
  503. ['number:seconds',
  504. {'number:style' => 'long'}],
  505. );
  506. } elsif ($val == 22) { ## m/d/yy h:mm
  507. $celltype{$style} = ['date', "N22"];
  508. $nstyle = 'date-style';
  509. @nextras = ('references' => {
  510. 'number:automatic-order' => 'true'});
  511. @children = (
  512. ['number:month'],
  513. ['number:text', {}, '/'],
  514. ['number:day'],
  515. ['number:text', {}, '/'],
  516. ['number:year'],
  517. ['number:text', {}, ' '],
  518. ['number:hours'],
  519. ['number:text', {}, ':'],
  520. ['number:minutes',
  521. {'number:style' => 'long'}],
  522. );
  523. } elsif ($val == 37) { ## (#,##0_);(#,##0)
  524. $celltype{$style} = ['float', "N37"];
  525. $nstyle = 'number-style';
  526. my %base = (
  527. 'number:min-integer-digits' => 1,
  528. 'number:grouping' => 'true',
  529. );
  530. @children = (
  531. ['number:text', {}, '('],
  532. ['number:number', \%base],
  533. ['number:text', {}, ')'],
  534. ['style:map', {
  535. 'style:condition' => 'value()>=0',
  536. 'style:apply-style-name' => "NP37",
  537. }],
  538. );
  539. &_create_positive_style("NP37",
  540. $nstyle, \%base);
  541. } elsif ($val == 38) { ## (#,##0_);[Red](#,##0)
  542. $celltype{$style} = ['float', "N38"];
  543. $nstyle = 'number-style';
  544. my %base = %{&_prepare_float('#,##0')};
  545. @children = (
  546. ['style:text-properties',
  547. {'fo:color' => '#ff0000'}],
  548. ['number:text', {}, '('],
  549. ['number:number', \%base],
  550. ['number:text', {}, ')'],
  551. ['style:map',{
  552. 'style:condition' => 'value()>=0',
  553. 'style:apply-style-name' => "NP38",
  554. }]
  555. );
  556. &_create_positive_style("NP38",
  557. $nstyle, \%base);
  558. } elsif ($val == 39) { ## (#,##0.00_);(#,##0.00)
  559. $celltype{$style} = ['float', "N39"];
  560. $nstyle = 'number-style';
  561. my %base = %{&_prepare_float('#,##0.00')};
  562. @children = (
  563. ['number:text', {}, '('],
  564. ['number:number', \%base],
  565. ['number:text', {}, ')'],
  566. ['style:map',{
  567. 'style:condition' => 'value()>=0',
  568. 'style:apply-style-name' => "NP39",
  569. }]
  570. );
  571. &_create_positive_style("NP39",
  572. $nstyle, \%base);
  573. } elsif ($val == 40) { ## (#,##0.00_);[Red](#,##0.00)
  574. $celltype{$style} = ['float', "N40"];
  575. $nstyle = 'number-style';
  576. my %base = %{&_prepare_float('#,##0.00')};
  577. @children = (
  578. ['style:text-properties',
  579. {'fo:color' => '#ff0000'}],
  580. ['number:text', {}, '('],
  581. ['number:number', \%base],
  582. ['number:text', {}, ')'],
  583. ['style:map', {
  584. 'style:condition' => 'value()>=0',
  585. 'style:apply-style-name' => "NP40",
  586. }],
  587. );
  588. &_create_positive_style("NP40",
  589. $nstyle, \%base);
  590. } elsif ($val == 41) {
  591. $celltype{$style} = 'float';
  592. } elsif ($val == 42) {
  593. $celltype{$style} = 'currency';
  594. } elsif ($val == 43) {
  595. $celltype{$style} = 'float';
  596. } elsif ($val == 44) {
  597. $celltype{$style} = 'currency';
  598. } elsif ($val == 45) { ## mm:ss
  599. $celltype{$style} = ['time', "N45"];
  600. $nstyle = 'time-style';
  601. @nextras = ('references' => {
  602. 'number:automatic-order' => 'true'});
  603. @children = (
  604. ['number:minutes',
  605. {'number:style' => 'long'}],
  606. ['number:text', {}, ':'],
  607. ['number:seconds',
  608. {'number:style' => 'long'}],
  609. );
  610. } elsif ($val == 46) { ## [h]:mm:ss
  611. $celltype{$style} = ['time', "N46"];
  612. $nstyle = 'time-style';
  613. @nextras = ('references' => {
  614. 'number:automatic-order' => 'true',
  615. 'number:truncate-on-overflow' => 'false'});
  616. @children = (
  617. ['number:hours', {}],
  618. ['number:text', {}, ':'],
  619. ['number:minutes',
  620. {'number:style' => 'long'}],
  621. ['number:text', {}, ':'],
  622. ['number:seconds',
  623. {'number:style' => 'long'}],
  624. );
  625. } elsif ($val == 47) { ## mm:ss.0
  626. $celltype{$style} = ['time', "N47"];
  627. $nstyle = 'time-style';
  628. @nextras = ('references' => {
  629. 'number:automatic-order' => 'true'});
  630. @children = (
  631. ['number:minutes',
  632. {'number:style' => 'long'}],
  633. ['number:text', {}, ':'],
  634. ['number:seconds',
  635. {'number:style' => 'long',
  636. 'number:decimal-places' => 1}],
  637. );
  638. } elsif ($val == 48) { ## ##0.0E+0
  639. $celltype{$style} = ['float', "N48"];
  640. $nstyle = 'number-style';
  641. %nproperties = ();
  642. push @children, ['number:scientific-number',
  643. {%{&_prepare_float(0.0)},
  644. 'number:min-exponent-digits' => 1
  645. }];
  646. } elsif ($val == 49) {
  647. $celltype{$style} = 'string';
  648. }
  649. # $nstyle is set on new styles
  650. if ($nstyle) {
  651. my $cstyle = $ods->createStyle(
  652. $celltype{$style}[1],
  653. namespace => 'number',
  654. type => $nstyle,
  655. properties => \%nproperties,
  656. @nextras,
  657. );
  658. for my $child (@children) {
  659. $cstyle->insert_new_elt('last_child',
  660. @$child);
  661. }
  662. $style_table{$fval} = 1;
  663. }
  664. }
  665. }
  666. # Maintain a hash table to keep the final style list size down
  667. $Data::Dumper::Sortkeys = 1;
  668. my $mystyle = Digest::MD5::md5_hex(Dumper(\%properties, \@extras));
  669. if (!$style_table{$mystyle}) {
  670. $ods->createStyle(
  671. $style,
  672. family => 'table-cell',
  673. properties => $properties{cell},
  674. @extras,
  675. );
  676. $ods->updateStyle(
  677. $style,
  678. properties => {
  679. -area => 'text',
  680. %{$properties{text}}
  681. }
  682. ) if $properties{text};
  683. $ods->updateStyle(
  684. $style,
  685. properties => {
  686. -area => 'paragraph',
  687. %{$properties{paragraph}}
  688. }
  689. ) if $properties{paragraph};
  690. $style_table{$mystyle} = [$style, \%properties];
  691. }
  692. unshift @style_stack, $style_table{$mystyle};
  693. }
  694. sub _named_format {
  695. my ($name, $t, $format) = @_;
  696. $format->{att}{$name} = 1;
  697. &_format_handler($t, $format);
  698. }
  699. sub _format_cleanup_handler {
  700. my ($t, $format) = @_;
  701. shift @style_stack;
  702. }
  703. sub _ods_process {
  704. my ($filename, $template) = @_;
  705. $ods = ooDocument(file => $filename, create => 'spreadsheet');
  706. my $parser = XML::Twig->new(
  707. start_tag_handlers => {
  708. worksheet => \&_worksheet_handler,
  709. row => \&_row_handler,
  710. cell => \&_cell_handler,
  711. formula => \&_formula_handler,
  712. format => \&_format_handler,
  713. bold => sub { &_named_format('bold', @_) },
  714. hidden => sub { &_named_format('hidden', @_) },
  715. italic => sub { &_named_format('italic', @_) },
  716. shadow => sub { &_named_format('shadow', @_) },
  717. strikeout => sub { &_named_format('strikeout', @_) },
  718. },
  719. twig_handlers => {
  720. format => \&_format_cleanup_handler,
  721. bold => \&_format_cleanup_handler,
  722. hidden => \&_format_cleanup_handler,
  723. italic => \&_format_cleanup_handler,
  724. shadow => \&_format_cleanup_handler,
  725. strikeout => \&_format_cleanup_handler,
  726. }
  727. );
  728. $parser->parse($template);
  729. $parser->purge;
  730. $ods->save;
  731. }
  732. sub get_template {
  733. my $name = shift;
  734. return "${name}.odst";
  735. }
  736. sub preprocess {
  737. my $rawvars = shift;
  738. my $vars;
  739. my $type = ref $rawvars;
  740. #XXX fix escaping function
  741. return $rawvars if $type =~ /^LedgerSMB::Locale/;
  742. return unless defined $rawvars;
  743. if ( $type eq 'ARRAY' ) {
  744. for (@{$rawvars}) {
  745. push @{$vars}, preprocess( $_ );
  746. }
  747. } elsif (!$type) {
  748. return escapeHTML($rawvars);
  749. } elsif ($type eq 'SCALAR') {
  750. return escapeHTML($$rawvars);
  751. } else { # Hashes and objects
  752. for ( keys %{$rawvars} ) {
  753. $vars->{preprocess($_)} = preprocess( $rawvars->{$_} );
  754. }
  755. }
  756. return $vars;
  757. }
  758. sub process {
  759. my $parent = shift;
  760. my $cleanvars = shift;
  761. my $template;
  762. my $source;
  763. my $tempdir = ${LedgerSMB::Sysconfig::tempdir};
  764. my $output = '';
  765. $parent->{outputfile} ||= "$tempdir/$parent->{template}-output-$$";
  766. if (ref $parent->{template} eq 'SCALAR') {
  767. $source = $parent->{template};
  768. } elsif (ref $parent->{template} eq 'ARRAY') {
  769. $source = join "\n", @{$parent->{template}};
  770. } else {
  771. $source = get_template($parent->{template});
  772. }
  773. $template = Template->new({
  774. INCLUDE_PATH => $parent->{include_path},
  775. START_TAG => quotemeta('<?lsmb'),
  776. END_TAG => quotemeta('?>'),
  777. DELIMITER => ';',
  778. DEBUG => ($parent->{debug})? 'dirs': undef,
  779. DEBUG_FORMAT => '',
  780. }) || throw Error::Simple Template->error();
  781. if (not $template->process(
  782. $source,
  783. {%$cleanvars, %$LedgerSMB::Template::TTI18N::ttfuncs,
  784. 'escape' => \&preprocess},
  785. \$output, binmode => ':utf8')) {
  786. throw Error::Simple $template->error();
  787. }
  788. &_ods_process("$parent->{outputfile}.ods", $output);
  789. $parent->{mimetype} = 'application/vnd.oasis.opendocument.spreadsheet';
  790. }
  791. sub postprocess {
  792. my $parent = shift;
  793. $parent->{rendered} = "$parent->{outputfile}.ods";
  794. return $parent->{rendered};
  795. }
  796. 1;