summaryrefslogtreecommitdiff
path: root/LedgerSMB/Form.pm
blob: 9c0626060440252a91ef8c72b89d6d04fbcf553b (plain)
  1. =head1 NAME
  2. Form
  3. =head1 SYNOPSIS
  4. This module provides general legacy support functions and the central object
  5. =head1 STATUS
  6. Deprecated
  7. =head1 COPYRIGHT
  8. #====================================================================
  9. # LedgerSMB
  10. # Small Medium Business Accounting software
  11. # http://www.ledgersmb.org/
  12. #
  13. # Copyright (C) 2006
  14. # This work contains copyrighted information from a number of sources
  15. # all used with permission.
  16. #
  17. # This file contains source code included with or based on SQL-Ledger
  18. # which is Copyright Dieter Simader and DWS Systems Inc. 2000-2005
  19. # and licensed under the GNU General Public License version 2 or, at
  20. # your option, any later version. For a full list including contact
  21. # information of contributors, maintainers, and copyright holders,
  22. # see the CONTRIBUTORS file.
  23. #
  24. # Original Copyright Notice from SQL-Ledger 2.6.17 (before the fork):
  25. # Copyright (C) 2000
  26. #
  27. # Author: DWS Systems Inc.
  28. # Web: http://www.sql-ledger.org
  29. #
  30. # Contributors: Thomas Bayen <bayen@gmx.de>
  31. # Antti Kaihola <akaihola@siba.fi>
  32. # Moritz Bunkus (tex)
  33. # Jim Rawlings <jim@your-dba.com> (DB2)
  34. #====================================================================
  35. #
  36. # This file has undergone whitespace cleanup.
  37. #
  38. #====================================================================
  39. #
  40. # main package
  41. #
  42. #====================================================================
  43. =head1 METHODS
  44. =over
  45. =cut
  46. #inline documentation
  47. use strict;
  48. use Math::BigFloat lib => 'GMP';
  49. use LedgerSMB::Sysconfig;
  50. use List::Util qw(first);
  51. use Time::Local;
  52. use Cwd;
  53. use File::Copy;
  54. use charnames ':full';
  55. use open ':utf8';
  56. package Form;
  57. =item new Form([$argstr])
  58. Returns a reference to new Form object. The initial set of attributes is
  59. obtained from $argstr, a CGI query string, or $ARGV[0]. All the values are
  60. run through unescape to undo any URI encoding.
  61. The version and dbversion attributes are set to hardcoded values; action,
  62. nextsub, path, script, and login are filtered to remove some dangerous values.
  63. Both menubar and lynx are set if path matches lynx.
  64. $form->error may be called to deny access on some attribute values.
  65. =cut
  66. sub new {
  67. my $type = shift;
  68. my $argstr = shift;
  69. if ($ENV{CONTENT_LENGTH} > $LedgerSMB::Sysconfig::max_post_size) {
  70. print "Status: 413\n Request entity too large\n\n";
  71. die "Error: Request entity too large\n";
  72. }
  73. read( STDIN, $_, $ENV{CONTENT_LENGTH} );
  74. if ($argstr) {
  75. $_ = $argstr;
  76. }
  77. elsif ( $ENV{QUERY_STRING} ) {
  78. $_ = $ENV{QUERY_STRING};
  79. }
  80. elsif ( $ARGV[0] ) {
  81. $_ = $ARGV[0];
  82. }
  83. my $self = {};
  84. %$self = split /[&=]/;
  85. for ( keys %$self ) { $self->{$_} = unescape( "", $self->{$_} ) }
  86. if ( substr( $self->{action}, 0, 1 ) !~ /( |\.)/ ) {
  87. $self->{action} = lc $self->{action};
  88. $self->{action} =~ s/( |-|,|\#|\/|\.$)/_/g;
  89. $self->{nextsub} = lc $self->{nextsub};
  90. $self->{nextsub} =~ s/( |-|,|\#|\/|\.$)/_/g;
  91. }
  92. $self->{login} =~ s/[^a-zA-Z0-9._+\@'-]//g;
  93. if (!$self->{company} && $ENV{HTTP_COOKIE}){
  94. $ENV{HTTP_COOKIE} =~ s/;\s*/;/g;
  95. my %cookie;
  96. my @cookies = split /;/, $ENV{HTTP_COOKIE};
  97. foreach (@cookies) {
  98. my ( $name, $value ) = split /=/, $_, 2;
  99. $cookie{$name} = $value;
  100. }
  101. my $ccookie = $cookie{${LedgerSMB::Sysconfig::cookie_name}};
  102. $ccookie =~ s/.*:([^:]*)$/$1/;
  103. $self->{company} = $ccookie;
  104. }
  105. $self->{menubar} = 1 if $self->{path} =~ /lynx/i;
  106. #menubar will be deprecated, replaced with below
  107. $self->{lynx} = 1 if $self->{path} =~ /lynx/i;
  108. $self->{version} = "1.2.99";
  109. $self->{dbversion} = "1.2.0";
  110. bless $self, $type;
  111. if ( $self->{path} ne 'bin/lynx' ) { $self->{path} = 'bin/mozilla'; }
  112. if ( ( $self->{script} )
  113. and not List::Util::first { $_ eq $self->{script} }
  114. @{LedgerSMB::Sysconfig::scripts} )
  115. {
  116. $self->error( 'Access Denied', __LINE__, __FILE__ );
  117. }
  118. if ( ( $self->{action} =~ /(:|')/ ) || ( $self->{nextsub} =~ /(:|')/ ) ) {
  119. $self->error( "Access Denied", __LINE__, __FILE__ );
  120. }
  121. for ( keys %$self ) { $self->{$_} =~ s/\N{NULL}//g }
  122. if ( ($self->{action} eq 'redirect') || ($self->{nextsub} eq 'redirect') ) {
  123. $self->error( "Access Denied", __LINE__, __FILE__ );
  124. }
  125. $self;
  126. }
  127. =item $form->debug([$file]);
  128. Outputs the sorted contents of $form. If a filename is specified, log to it,
  129. otherwise output to STDOUT.
  130. =cut
  131. sub debug {
  132. my ( $self, $file ) = @_;
  133. if ($file) {
  134. open( FH, '>', "$file" ) or die $!;
  135. for ( sort keys %$self ) { print FH "$_ = $self->{$_}\n" }
  136. close(FH);
  137. }
  138. else {
  139. print "\n";
  140. for ( sort keys %$self ) { print "$_ = $self->{$_}\n" }
  141. }
  142. }
  143. =item $form->encode_all();
  144. Does nothing and is unused. Contains merely the comment # TODO;
  145. =cut
  146. sub encode_all {
  147. # TODO;
  148. }
  149. =item $form->decode_all();
  150. Does nothing and is unused. Contains merely the comment # TODO
  151. =cut
  152. sub decode_all {
  153. # TODO
  154. }
  155. =item $form->escape($str[, $beenthere]);
  156. Returns the URI-encoded $str. $beenthere is a boolean that when true forces a
  157. single encoding run. When false, it escapes the string twice if it detects
  158. that it is running on a version of Apache 2.0 earlier than 2.0.44.
  159. Note that recurring transaction support depends on this function escaping ','.
  160. =cut
  161. sub escape {
  162. my ( $self, $str, $beenthere ) = @_;
  163. # for Apache 2 we escape strings twice
  164. if ( ( $ENV{SERVER_SIGNATURE} =~ /Apache\/2\.(\d+)\.(\d+)/ )
  165. && !$beenthere )
  166. {
  167. $str = $self->escape( $str, 1 ) if $1 == 0 && $2 < 44;
  168. }
  169. utf8::encode($str);
  170. # SC: Adding commas to the ignore list will break recurring transactions
  171. $str =~ s/([^a-zA-Z0-9_.-])/sprintf("%%%02x", ord($1))/ge;
  172. $str;
  173. }
  174. =item $form->unescape($str);
  175. Returns the unencoded form of the URI-encoded $str.
  176. =cut
  177. sub unescape {
  178. my ( $self, $str ) = @_;
  179. $str =~ tr/+/ /;
  180. $str =~ s/\\$//;
  181. utf8::encode($str) if utf8::is_utf8($str);
  182. $str =~ s/%([0-9a-fA-Z]{2})/pack("c",hex($1))/eg;
  183. utf8::decode($str);
  184. $str =~ s/\r?\n/\n/g;
  185. $str;
  186. }
  187. =item $form->quote($str);
  188. Replaces all double quotes in $str with '&quot;'. Does nothing if $str is a
  189. reference.
  190. =cut
  191. sub quote {
  192. my ( $self, $str ) = @_;
  193. if ( $str && !ref($str) ) {
  194. $str =~ s/"/&quot;/g;
  195. }
  196. $str;
  197. }
  198. =item $form->unquote($str);
  199. Replaces all '&quot;' in $str with double quotes. Does nothing if $str is a
  200. reference.
  201. =cut
  202. sub unquote {
  203. my ( $self, $str ) = @_;
  204. if ( $str && !ref($str) ) {
  205. $str =~ s/&quot;/"/g;
  206. }
  207. $str;
  208. }
  209. =item $form->hide_form([...]);
  210. Outputs hidden HTML form fields to STDOUT. If values are passed into this
  211. function, only those $form values are output. If no values are passed in, all
  212. $form values are output as well as deleting $form->{header}. Values from the
  213. $form object are run through $form->quote, whereas keys/names are not.
  214. Sample output:
  215. <input type="hidden" name="login" value="testuser" />
  216. =cut
  217. sub hide_form {
  218. my $self = shift;
  219. if (@_) {
  220. for (@_) {
  221. print qq|<input type="hidden" name="$_" value="|
  222. . $self->quote( $self->{$_} )
  223. . qq|" />\n|;
  224. }
  225. }
  226. else {
  227. delete $self->{header};
  228. for ( sort keys %$self ) {
  229. print qq|<input type="hidden" name="$_" value="|
  230. . $self->quote( $self->{$_} )
  231. . qq|" />\n|;
  232. }
  233. }
  234. }
  235. =item $form->error($msg);
  236. Output an error message, $msg. If a CGI environment is detected, this outputs
  237. an HTTP and HTML header section if required, and displays the message after
  238. running it through $form->format_string. If it is not a CGI environment and
  239. $ENV{error_function} is set, call the specified function with $msg as the sole
  240. argument. Otherwise, this function simply dies with $msg.
  241. This function does not return. Execution is terminated at the end of the
  242. appropriate path.
  243. =cut
  244. sub error {
  245. my ( $self, $msg ) = @_;
  246. if ( $ENV{GATEWAY_INTERFACE} ) {
  247. $self->{msg} = $msg;
  248. $self->{format} = "html";
  249. $self->format_string('msg');
  250. delete $self->{pre};
  251. if ( !$self->{header} ) {
  252. $self->header;
  253. }
  254. print
  255. qq|<body><h2 class="error">Error!</h2> <p><b>$self->{msg}</b></body>|;
  256. exit;
  257. }
  258. else {
  259. if ( $ENV{error_function} ) {
  260. __PACKAGE__->can($ENV{error_function})->($msg);
  261. }
  262. die "Error: $msg\n";
  263. }
  264. }
  265. =item $form->info($msg);
  266. Output an informational message, $msg. If a CGI environment is detected, this
  267. outputs an HTTP and HTML header section if required, and displays the message
  268. in bold tags without escaping. If it is not a CGI environment and
  269. $ENV{info_function} is set, call the specified function with $msg as the sole
  270. argument. Otherwise, this function simply prints $msg to STDOUT.
  271. =cut
  272. sub info {
  273. my ( $self, $msg ) = @_;
  274. if ( $ENV{GATEWAY_INTERFACE} ) {
  275. $msg =~ s/\n/<br>/g;
  276. delete $self->{pre};
  277. if ( !$self->{header} ) {
  278. $self->header;
  279. print qq| <body>|;
  280. $self->{header} = 1;
  281. }
  282. print "<b>$msg</b>";
  283. }
  284. else {
  285. if ( $ENV{info_function} ) {
  286. __PACKAGE__->can($ENV{info_function})->($msg);
  287. }
  288. else {
  289. print "$msg\n";
  290. }
  291. }
  292. }
  293. =item $form->numtextrows($str, $cols[, $maxrows]);
  294. Returns the number of rows of $cols columns can be formed by $str. If $maxrows
  295. is set and the number of rows is greater than $maxrows, this returns $maxrows.
  296. In the determination of rowcount, newline characters, "\n", are taken into
  297. account while spaces are not.
  298. =cut
  299. sub numtextrows {
  300. my ( $self, $str, $cols, $maxrows ) = @_;
  301. my $rows = 0;
  302. for ( split /\n/, $str ) {
  303. $rows += int( ( (length) - 2 ) / $cols ) + 1;
  304. }
  305. $maxrows = $rows unless defined $maxrows;
  306. return ( $rows > $maxrows ) ? $maxrows : $rows;
  307. }
  308. =item $form->dberror($msg);
  309. Outputs a message as in $form->error but with $DBI::errstr automatically
  310. appended to $msg.
  311. =cut
  312. sub dberror {
  313. my ( $self, $msg ) = @_;
  314. $self->error( "$msg\n" . $DBI::errstr );
  315. }
  316. =item $form->isblank($name, $msg);
  317. Calls $form->error($msg) if the value of $form->{$name} matches /^\s*$/.
  318. =cut
  319. sub isblank {
  320. my ( $self, $name, $msg ) = @_;
  321. $self->error($msg) if $self->{$name} =~ /^\s*$/;
  322. }
  323. =item $form->header([$init, $headeradd]);
  324. Outputs HTML and HTTP headers and sets $form->{header} to indicate that headers
  325. have been output. If called with $form->{header} set or in a non-CGI
  326. environment, does not output anything. $init is ignored. $headeradd is data
  327. to be added to the <head> portion of the output headers. $form->{stylesheet},
  328. $form->{title}, $form->{titlebar}, and $form->{pre} all affect the output of
  329. this function.
  330. If the stylesheet indicated by $form->{stylesheet} exists, output a link tag
  331. to reference it. If $form->{title} is false, the title text is the value of
  332. $form->{titlebar}. If $form->{title} is true, the title text takes the form of
  333. "$form->{title} - $form->{titlebar}". The value of $form->{pre} is output
  334. immediately after the closing of <head>.
  335. =cut
  336. sub header {
  337. my ( $self, $init, $headeradd ) = @_;
  338. return if $self->{header};
  339. my ( $stylesheet, $favicon, $charset );
  340. if ( $ENV{GATEWAY_INTERFACE} ) {
  341. if ( $self->{stylesheet} && ( -f "css/$self->{stylesheet}" ) ) {
  342. $stylesheet =
  343. qq|<link rel="stylesheet" href="css/$self->{stylesheet}" type="text/css" title="LedgerSMB stylesheet" />\n|;
  344. }
  345. $self->{charset} ||= "utf-8";
  346. $charset =
  347. qq|<meta http-equiv="content-type" content="text/html; charset=$self->{charset}" />\n|;
  348. $self->{titlebar} =
  349. ( $self->{title} )
  350. ? "$self->{title} - $self->{titlebar}"
  351. : $self->{titlebar};
  352. print qq|Content-Type: text/html; charset=utf-8\n\n
  353. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  354. "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  355. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  356. <head>
  357. <title>$self->{titlebar}</title>
  358. <meta http-equiv="Pragma" content="no-cache" />
  359. <meta http-equiv="Expires" content="-1" />
  360. <link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
  361. $stylesheet
  362. $charset
  363. <meta name="robots" content="noindex,nofollow" />
  364. $headeradd
  365. </head>
  366. $self->{pre} \n|;
  367. }
  368. $self->{header} = 1;
  369. }
  370. =item $form->redirect([$msg]);
  371. If $form->{callback} is set or $msg is not set, call the redirect function in
  372. common.pl. If main::redirect returns, exit.
  373. Otherwise, output $msg as an informational message with $form->info($msg).
  374. =cut
  375. sub redirect {
  376. my ( $self, $msg ) = @_;
  377. if ( $self->{callback} || !$msg ) {
  378. print STDERR "Full redirect\n";
  379. main::redirect();
  380. exit;
  381. }
  382. else {
  383. $self->info($msg);
  384. }
  385. }
  386. =item $form->sort_columns(@columns);
  387. Sorts the list @columns. If $form->{sort} is unset, do nothing. If the value
  388. of $form->{sort} does not exist in @columns, returns the list formed by the
  389. value of $form->{sort} followed by the values of @columns. If the value of
  390. $form->{sort} is in @columns, return the list formed by @columns with the value
  391. of $form->{sort} moved to the head of the list.
  392. =cut
  393. sub sort_columns {
  394. my ( $self, @columns ) = @_;
  395. if ( $self->{sort} ) {
  396. $self->{sort} =~ s/^"*(.*?)"*$/$1/;
  397. if (@columns) {
  398. @columns = grep !/^$self->{sort}$/, @columns;
  399. if ($self->{sort} !~ /^\w*$/){
  400. $self->{sort} = $self->{dbh}->quote_identifier($self->{sort});
  401. }
  402. splice @columns, 0, 0, $self->{sort};
  403. }
  404. }
  405. @columns;
  406. }
  407. =item $form->sort_order($columns[, $ordinal]);
  408. Returns a string that contains ordering details for the columns in SQL form.
  409. $columns is a reference to a list of columns, $ordinal is a reference to a hash
  410. that maps column names to ordinal positions. This function depends upon the
  411. values of $form->{direction}, $form->{sort}, and $form->{oldsort}.
  412. If $form->{direction} is false, it becomes 'ASC'. If $form->{direction} is true
  413. and $form->{sort} and $form->{oldsort} are equal, reverse the order specified by
  414. $form->{direction}. $form->{oldsort} is set to the same value as $form->{sort}
  415. The actual sorting of $columns happens as in $form->sort_columns(@$columns).
  416. If $ordinal is set, the positions given by it are substituted for the names of
  417. columns returned.
  418. =cut
  419. sub sort_order {
  420. my ( $self, $columns, $ordinal ) = @_;
  421. # setup direction
  422. if ( $self->{direction} ) {
  423. if ( $self->{sort} eq $self->{oldsort} ) {
  424. if ( $self->{direction} eq 'ASC' ) {
  425. $self->{direction} = "DESC";
  426. }
  427. else {
  428. $self->{direction} = "ASC";
  429. }
  430. }
  431. }
  432. else {
  433. $self->{direction} = "ASC";
  434. }
  435. $self->{oldsort} = $self->{sort};
  436. my @a = $self->sort_columns( @{$columns} );
  437. if (ref $ordinal eq 'HASH') {
  438. $a[0] =
  439. ( $ordinal->{ $a[$_] } )
  440. ? "$ordinal->{$a[0]} $self->{direction}"
  441. : "$a[0] $self->{direction}";
  442. for ( 1 .. $#a ) {
  443. $a[$_] = $ordinal->{ $a[$_] } if $ordinal->{ $a[$_] };
  444. }
  445. }
  446. else {
  447. $a[0] .= " $self->{direction}";
  448. }
  449. my $sortorder = join ',', @a;
  450. $sortorder;
  451. }
  452. =item $form->format_amount($myconfig, $amount, $places, $dash);
  453. Returns $amount as formatted in the form specified by $form->{numberformat}.
  454. $places is the number of decimal places to have in the output. $dash indicates
  455. how to represent conditions surrounding values.
  456. +-------+----------+---------+------+
  457. | $dash | -1.00 | 1.00 | 0.00 |
  458. +-------+----------+---------+------+
  459. | - | (1.00) | 1.00 | - |
  460. | DRCR | 1.00 DR | 1.00 CR | DRCR |
  461. | 0 | -1.00 | 1.00 | 0.00 |
  462. | x | -1.00 | 1.00 | x |
  463. | undef | -1.00 | 1.00 | |
  464. +-------+----------+---------+------+
  465. Sample behaviour of the formatted output of various numbers for select $dash
  466. values.
  467. =cut
  468. sub format_amount {
  469. my ( $self, $myconfig, $amount, $places, $dash ) = @_;
  470. my $negative;
  471. if ($amount) {
  472. $amount = $self->parse_amount( $myconfig, $amount );
  473. $negative = ( $amount < 0 );
  474. $amount =~ s/-//;
  475. }
  476. if ( $places =~ /\d+/ ) {
  477. #$places = 4 if $places == 2;
  478. $amount = $self->round_amount( $amount, $places );
  479. }
  480. # is the amount negative
  481. # Parse $myconfig->{numberformat}
  482. my ( $ts, $ds ) = ( $1, $2 );
  483. if ($amount) {
  484. if ( $myconfig->{numberformat} ) {
  485. my ( $whole, $dec ) = split /\./, "$amount";
  486. $amount = join '', reverse split //, $whole;
  487. if ($places) {
  488. $dec .= "0" x $places;
  489. $dec = substr( $dec, 0, $places );
  490. }
  491. if ( $myconfig->{numberformat} eq '1,000.00' ) {
  492. $amount =~ s/\d{3,}?/$&,/g;
  493. $amount =~ s/,$//;
  494. $amount = join '', reverse split //, $amount;
  495. $amount .= "\.$dec" if ( $dec ne "" );
  496. }
  497. elsif ( $myconfig->{numberformat} eq '1 000.00' ) {
  498. $amount =~ s/\d{3,}?/$& /g;
  499. $amount =~ s/\s$//;
  500. $amount = join '', reverse split //, $amount;
  501. $amount .= "\.$dec" if ( $dec ne "" );
  502. }
  503. elsif ( $myconfig->{numberformat} eq "1'000.00" ) {
  504. $amount =~ s/\d{3,}?/$&'/g;
  505. $amount =~ s/'$//;
  506. $amount = join '', reverse split //, $amount;
  507. $amount .= "\.$dec" if ( $dec ne "" );
  508. }
  509. elsif ( $myconfig->{numberformat} eq '1.000,00' ) {
  510. $amount =~ s/\d{3,}?/$&./g;
  511. $amount =~ s/\.$//;
  512. $amount = join '', reverse split //, $amount;
  513. $amount .= ",$dec" if ( $dec ne "" );
  514. }
  515. elsif ( $myconfig->{numberformat} eq '1000,00' ) {
  516. $amount = "$whole";
  517. $amount .= ",$dec" if ( $dec ne "" );
  518. }
  519. elsif ( $myconfig->{numberformat} eq '1000.00' ) {
  520. $amount = "$whole";
  521. $amount .= ".$dec" if ( $dec ne "" );
  522. }
  523. if ( $dash =~ /-/ ) {
  524. $amount = ($negative) ? "($amount)" : "$amount";
  525. }
  526. elsif ( $dash =~ /DRCR/ ) {
  527. $amount = ($negative) ? "$amount DR" : "$amount CR";
  528. }
  529. else {
  530. $amount = ($negative) ? "-$amount" : "$amount";
  531. }
  532. }
  533. }
  534. else {
  535. if ( $dash eq "0" && $places ) {
  536. if ( $myconfig->{numberformat} =~ /0,00$/ ) {
  537. $amount = "0" . "," . "0" x $places;
  538. }
  539. else {
  540. $amount = "0" . "." . "0" x $places;
  541. }
  542. }
  543. else {
  544. $amount = ( $dash ne "" ) ? "$dash" : "";
  545. }
  546. }
  547. $amount;
  548. }
  549. =item $form->parse_amount($myconfig, $amount);
  550. Return a Math::BigFloat containing the value of $amount where $amount is
  551. formatted as $myconfig->{numberformat}. If $amount is '' or undefined, it is
  552. treated as zero. DRCR and parenthesis notation is accepted in addition to
  553. negative sign notation.
  554. Calls $form->error if the value is NaN.
  555. =cut
  556. sub parse_amount {
  557. my ( $self, $myconfig, $amount ) = @_;
  558. if ( ( $amount eq '' ) or ( ! defined $amount ) ) {
  559. $amount = 0;
  560. }
  561. if ( UNIVERSAL::isa( $amount, 'Math::BigFloat' ) )
  562. { # Amount may not be an object
  563. return $amount;
  564. }
  565. my $numberformat = $myconfig->{numberformat};
  566. if ( ( $numberformat eq '1.000,00' )
  567. || ( $numberformat eq '1000,00' ) )
  568. {
  569. $amount =~ s/\.//g;
  570. $amount =~ s/,/./;
  571. }
  572. elsif ( $numberformat eq '1 000.00' ) {
  573. $amount =~ s/\s//g;
  574. }
  575. elsif ( $numberformat eq "1'000.00" ) {
  576. $amount =~ s/'//g;
  577. }
  578. $amount =~ s/,//g;
  579. if ( $amount =~ s/\((\d*\.?\d*)\)/$1/ ) {
  580. $amount = $1 * -1;
  581. }
  582. elsif ( $amount =~ s/(\d*\.?\d*)\s?DR/$1/ ) {
  583. $amount = $1 * -1;
  584. }
  585. $amount =~ s/\s?CR//;
  586. $amount =~ /(\d*)\.(\d*)/;
  587. my $decimalplaces = length $1 + length $2;
  588. $amount = new Math::BigFloat($amount);
  589. if ($amount->is_nan){
  590. $self->error("Invalid number detected during parsing");
  591. }
  592. return ( $amount * 1 );
  593. }
  594. =item $form->round_amount($amount, $places);
  595. Rounds the provided $amount to $places decimal places.
  596. =cut
  597. sub round_amount {
  598. my ( $self, $amount, $places ) = @_;
  599. # These rounding rules follow from the previous implementation.
  600. # They should be changed to allow different rules for different accounts.
  601. Math::BigFloat->round_mode('+inf') if $amount >= 0;
  602. Math::BigFloat->round_mode('-inf') if $amount < 0;
  603. $amount = Math::BigFloat->new($amount)->ffround( -$places ) if $places >= 0;
  604. $amount = Math::BigFloat->new($amount)->ffround( -( $places - 1 ) )
  605. if $places < 0;
  606. $amount->precision(undef); #we are assuming whole cents so do not round
  607. #immediately on arithmatic. This is necessary
  608. #because Math::BigFloat is arithmatically
  609. #correct wrt accuracy and precision.
  610. return $amount;
  611. }
  612. =item $form->db_parse_numeric('sth' => $sth, ['arrayref' => $arrayref, 'hashref' => $hashref])
  613. Converts numeric values in the result set $arrayref or $hashref to
  614. Math::BigFloat using $sth to determine which fields are numeric.
  615. =cut
  616. sub db_parse_numeric {
  617. my $self = shift;
  618. my %args = @_;
  619. my ($sth, $arrayref, $hashref) = ($args{sth}, $args{arrayref},
  620. $args{hashref});
  621. my @types = @{$sth->{TYPE}};
  622. my @names = @{$sth->{NAME_lc}};
  623. for (0 .. $#names){
  624. # numeric float4/real
  625. if ($types[$_] == 3 or $types[$_] ==2) {
  626. $arrayref->[$_] = Math::BigFloat->new($arrayref->[$_])
  627. if defined $arrayref;
  628. $hashref->{$names[$_]} = Math::BigFloat->new($hashref->{$names[$_]})
  629. if defined $hashref;
  630. }
  631. }
  632. return ($hashref || $arrayref);
  633. }
  634. =item $form->get_my_emp_num($myconfig);
  635. Function to get the employee number of the user $form->{login}. $myconfig is
  636. only used to create %myconfig. $form->{emp_num} is set to the retrieved value.
  637. This function is currently (2007-08-02) only used by pos.conf.pl.
  638. =cut
  639. sub get_my_emp_num {
  640. my ( $self, $myconfig) = @_;
  641. my %myconfig = %{$myconfig};
  642. my $dbh = $self->{dbh};
  643. # we got a connection, check the version
  644. my $query = qq|
  645. SELECT employeenumber FROM employee
  646. WHERE login = ?|;
  647. my $sth = $dbh->prepare($query);
  648. $sth->execute( $self->{login} ) || $self->dberror($query);
  649. my ($id) = $sth->fetchrow_array;
  650. $sth->finish;
  651. $self->{'emp_num'} = $id;
  652. }
  653. =item $form->format_string(@fields);
  654. Escape the values of $form selected by @fields for the format specified by
  655. $form->{format}.
  656. =cut
  657. sub format_string {
  658. my ( $self, @fields ) = @_;
  659. my $format = $self->{format};
  660. if ( $self->{format} =~ /(postscript|pdf)/ ) {
  661. $format = 'tex';
  662. }
  663. my %replace = (
  664. 'order' => {
  665. html => [ '<', '>', '\n', '\r' ],
  666. txt => [ '\n', '\r' ],
  667. tex => [
  668. quotemeta('\\'), '&', '\n', '\r',
  669. '\$', '%', '_', '#',
  670. quotemeta('^'), '{', '}', '<',
  671. '>', '£'
  672. ]
  673. },
  674. html => {
  675. '<' => '&lt;',
  676. '>' => '&gt;',
  677. '\n' => '<br />',
  678. '\r' => '<br />'
  679. },
  680. txt => { '\n' => "\n", '\r' => "\r" },
  681. tex => {
  682. '&' => '\&',
  683. '$' => '\$',
  684. '%' => '\%',
  685. '_' => '\_',
  686. '#' => '\#',
  687. quotemeta('^') => '\^\\',
  688. '{' => '\{',
  689. '}' => '\}',
  690. '<' => '$<$',
  691. '>' => '$>$',
  692. '\n' => '\newline ',
  693. '\r' => '\newline ',
  694. '£' => '\pounds ',
  695. quotemeta('\\') => '/'
  696. }
  697. );
  698. my $key;
  699. foreach $key ( @{ $replace{order}{$format} } ) {
  700. for (@fields) { $self->{$_} =~ s/$key/$replace{$format}{$key}/g }
  701. }
  702. }
  703. =item $form->datetonum($myconfig, $date[, $picture]);
  704. Converts $date from the format $myconfig->{dateformat} to the format 'yyyymmdd'.
  705. If the year extracted is only two-digits, the year given is assumed to be in the
  706. range 2000-2099.
  707. If $date does not contain any non-digits, datetonum does nothing.
  708. $picture is ignored.
  709. =cut
  710. sub datetonum {
  711. my ( $self, $myconfig, $date, $picture ) = @_;
  712. if ($date =~ /^\d{4}-\d{2}-\d{2}$/){
  713. $date =~ s/-//g;
  714. return $date;
  715. }
  716. if ( $date && $date =~ /\D/ ) {
  717. my $yy;
  718. my $mm;
  719. my $dd;
  720. if ( $date =~ /^\d{4}-\d\d-\d\d$/ ) {
  721. ( $yy, $mm, $dd ) = split /\D/, $date;
  722. } if ( $myconfig->{dateformat} =~ /^yy/ ) {
  723. ( $yy, $mm, $dd ) = split /\D/, $date;
  724. } elsif ( $myconfig->{dateformat} =~ /^mm/ ) {
  725. ( $mm, $dd, $yy ) = split /\D/, $date;
  726. } elsif ( $myconfig->{dateformat} =~ /^dd/ ) {
  727. ( $dd, $mm, $yy ) = split /\D/, $date;
  728. }
  729. $dd *= 1;
  730. $mm *= 1;
  731. $yy += 2000 if length $yy == 2;
  732. $dd = substr( "0$dd", -2 );
  733. $mm = substr( "0$mm", -2 );
  734. $date = "$yy$mm$dd";
  735. }
  736. $date;
  737. }
  738. =item $form->add_date($myconfig, $date, $repeat, $unit);
  739. Returns the date $repeat $units from $date in the input format. $date can
  740. either be in $myconfig->{dateformat} or 'yyyymmdd' (four digit year required for
  741. this option). The valid values for $unit are 'days', 'weeks', 'months', and
  742. 'years'.
  743. This function is unreliable for $unit values other than 'days' or 'weeks' and
  744. can die horribly.
  745. =cut
  746. sub add_date {
  747. my ( $self, $myconfig, $date, $repeat, $unit ) = @_;
  748. my $diff = 0;
  749. my $spc = $myconfig->{dateformat};
  750. my $yy;
  751. my $mm;
  752. my $dd;
  753. $spc =~ s/\w//g;
  754. $spc = substr( $spc, 0, 1 );
  755. if ($date) {
  756. if ( $date =~ /\D/ ) {
  757. if ( $myconfig->{dateformat} =~ /^yy/ ) {
  758. ( $yy, $mm, $dd ) = split /\D/, $date;
  759. }
  760. elsif ( $myconfig->{dateformat} =~ /^mm/ ) {
  761. ( $mm, $dd, $yy ) = split /\D/, $date;
  762. }
  763. elsif ( $myconfig->{dateformat} =~ /^dd/ ) {
  764. ( $dd, $mm, $yy ) = split /\D/, $date;
  765. }
  766. }
  767. else {
  768. # ISO
  769. ( $yy, $mm, $dd ) = ($date =~ /(....)(..)(..)/);
  770. }
  771. if ( $unit eq 'days' ) {
  772. $diff = $repeat * 86400;
  773. }
  774. elsif ( $unit eq 'weeks' ) {
  775. $diff = $repeat * 604800;
  776. }
  777. elsif ( $unit eq 'months' ) {
  778. $diff = $mm + $repeat;
  779. my $whole = int( $diff / 12 );
  780. $yy += $whole;
  781. $mm = ( $diff % 12 );
  782. $mm = '12' if $mm == 0;
  783. $yy-- if $mm == 12;
  784. $diff = 0;
  785. }
  786. elsif ( $unit eq 'years' ) {
  787. $yy += $repeat;
  788. }
  789. $mm--;
  790. my @t = localtime( Time::Local::timelocal( 0, 0, 0, $dd, $mm, $yy ) + $diff );
  791. $t[4]++;
  792. $mm = substr( "0$t[4]", -2 );
  793. $dd = substr( "0$t[3]", -2 );
  794. $yy = $t[5] + 1900;
  795. if ( $date =~ /\D/ ) {
  796. if ( $myconfig->{dateformat} =~ /^yy/ ) {
  797. $date = "$yy$spc$mm$spc$dd";
  798. }
  799. elsif ( $myconfig->{dateformat} =~ /^mm/ ) {
  800. $date = "$mm$spc$dd$spc$yy";
  801. }
  802. elsif ( $myconfig->{dateformat} =~ /^dd/ ) {
  803. $date = "$dd$spc$mm$spc$yy";
  804. }
  805. }
  806. else {
  807. $date = "$yy$mm$dd";
  808. }
  809. }
  810. $date;
  811. }
  812. =item $form->print_button($button, $name);
  813. Outputs a submit button to STDOUT. $button is a hashref that contains data
  814. about buttons, $name is the key for the element in $button to output. Each
  815. value in $button is a reference to a hash of two elements, 'key' and 'value'.
  816. $name is the value of the button that gets sent to the server when clicked,
  817. $button->{$name}{key} is the accesskey, and $button->{$name}{value} is the label
  818. for the button.
  819. =cut
  820. sub print_button {
  821. my ( $self, $button, $name ) = @_;
  822. print
  823. qq|<button class="submit" type="submit" name="action" value="$name" accesskey="$button->{$name}{key}" title="$button->{$name}{value} [Alt-$button->{$name}{key}]">$button->{$name}{value}</button>\n|;
  824. }
  825. # Database routines used throughout
  826. =item $form->db_init($myconfig);
  827. Connect to the database that $myconfig is set to use and initialise the base
  828. parameters. The connection handle becomes $form->{dbh} and
  829. $form->{custom_db_fields} is populated. The connection initiated has
  830. autocommit disabled.
  831. =cut
  832. sub db_init {
  833. my ( $self, $myconfig ) = @_;
  834. # Handling of HTTP Basic Auth headers
  835. my $auth = $ENV{'HTTP_AUTHORIZATION'};
  836. $auth =~ s/Basic //i; # strip out basic authentication preface
  837. $auth = MIME::Base64::decode($auth);
  838. my ($login, $password) = split(/:/, $auth);
  839. $self->{login} = $login;
  840. if (!$self->{company}){
  841. $self->{company} = $LedgerSMB::Sysconfig::default_db;
  842. }
  843. my $dbname = $self->{company};
  844. my $dbconfig = { dbconnect => "dbi:Pg:dbname=$dbname",
  845. dbuser => $login,
  846. dbpasswd => $password
  847. };
  848. $self->{dbh} = $self->dbconnect_noauto($dbconfig) || $self->dberror();
  849. $self->{dbh}->{pg_server_prepare} = 0;
  850. my $dbh = $self->{dbh};
  851. my %date_query = (
  852. 'mm/dd/yy' => 'set DateStyle to \'SQL, US\'',
  853. 'mm-dd-yy' => 'set DateStyle to \'POSTGRES, US\'',
  854. 'dd/mm/yy' => 'set DateStyle to \'SQL, EUROPEAN\'',
  855. 'dd-mm-yy' => 'set DateStyle to \'POSTGRES, EUROPEAN\'',
  856. 'dd.mm.yy' => 'set DateStyle to \'GERMAN\''
  857. );
  858. $self->{dbh}->do( $date_query{ $myconfig->{dateformat} } );
  859. $self->{db_dateformat} = $myconfig->{dateformat}; #shim
  860. # This is the general version check
  861. my $sth = $dbh->prepare("
  862. SELECT value FROM defaults
  863. WHERE setting_key = 'version'");
  864. $sth->execute;
  865. my ($dbversion) = $sth->fetchrow_array;
  866. if ($dbversion ne $self->{dbversion}){
  867. $self->error("Database is not the expected version.");
  868. }
  869. my $query = "SELECT t.extends,
  870. coalesce (t.table_name, 'custom_' || extends)
  871. || ':' || f.field_name as field_def
  872. FROM custom_table_catalog t
  873. JOIN custom_field_catalog f USING (table_id)";
  874. my $sth = $self->{dbh}->prepare($query);
  875. $sth->execute;
  876. my $ref;
  877. while ( $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  878. push @{ $self->{custom_db_fields}{ $ref->{extends} } },
  879. $ref->{field_def};
  880. }
  881. # Roles tracking
  882. $self->{_roles} = [];
  883. $query = "select rolname from pg_roles
  884. where pg_has_role(SESSION_USER, 'USAGE')";
  885. $sth = $dbh->prepare($query);
  886. $sth->execute();
  887. while (my @roles = $sth->fetchrow_array){
  888. push @{$self->{_roles}}, $roles[0];
  889. }
  890. $sth->finish();
  891. }
  892. =item $form->run_custom_queries($tablename, $query_type[, $linenum]);
  893. Runs queries against custom fields for the specified $query_type against
  894. $tablename.
  895. Valid values for $query_type are any casing of 'SELECT', 'INSERT', and 'UPDATE'.
  896. =cut
  897. sub run_custom_queries {
  898. my ( $self, $tablename, $query_type, $linenum ) = @_;
  899. my $dbh = $self->{dbh};
  900. if ( $query_type !~ /^(select|insert|update)$/i ) {
  901. $self->error(
  902. "Passed incorrect query type to run_custom_queries."
  903. );
  904. }
  905. my @rc;
  906. my %temphash;
  907. my @templist;
  908. my @elements;
  909. my $query;
  910. my $did_insert;
  911. my $ins_values;
  912. my $sth;
  913. if ($linenum) {
  914. $linenum = "_$linenum";
  915. }
  916. $query_type = uc($query_type);
  917. for ( @{ $self->{custom_db_fields}{$tablename} } ) {
  918. @elements = split( /:/, $_ );
  919. push @{ $temphash{ $elements[0] } }, $elements[1];
  920. }
  921. for ( keys %temphash ) {
  922. my @data;
  923. my $ins_values;
  924. $query = "$query_type ";
  925. if ( $query_type eq 'UPDATE' ) {
  926. $query = "DELETE FROM $_ WHERE row_id = ?";
  927. my $sth = $dbh->prepare($query);
  928. $sth->execute( $self->{ "id" . "$linenum" } )
  929. || $self->dberror($query);
  930. }
  931. elsif ( $query_type eq 'INSERT' ) {
  932. $query .= " INTO $_ (";
  933. }
  934. my $first = 1;
  935. for ( @{ $temphash{$_} } ) {
  936. $query .= "$_";
  937. if ( $query_type eq 'UPDATE' ) {
  938. $query .= '= ?';
  939. }
  940. $ins_values .= "?, ";
  941. $query .= ", ";
  942. $first = 0;
  943. if ( $query_type eq 'UPDATE' or $query_type eq 'INSERT' ) {
  944. push @data, $self->{"$_$linenum"};
  945. }
  946. }
  947. if ( $query_type ne 'INSERT' ) {
  948. $query =~ s/, $//;
  949. }
  950. if ( $query_type eq 'SELECT' ) {
  951. $query .= " FROM $_";
  952. }
  953. if ( $query_type eq 'SELECT' or $query_type eq 'UPDATE' ) {
  954. $query .= " WHERE row_id = ?";
  955. }
  956. if ( $query_type eq 'INSERT' ) {
  957. $query .= " row_id) VALUES ($ins_values ?)";
  958. }
  959. if ( $query_type eq 'SELECT' ) {
  960. push @rc, [$query];
  961. }
  962. else {
  963. unshift( @data, $query );
  964. push @rc, [@data];
  965. }
  966. }
  967. if ( $query_type eq 'INSERT' ) {
  968. for (@rc) {
  969. $query = shift( @{$_} );
  970. $sth = $dbh->prepare($query)
  971. || $self->db_error($query);
  972. $sth->execute( @{$_}, $self->{id} )
  973. || $self->dberror($query);
  974. $sth->finish;
  975. $did_insert = 1;
  976. }
  977. }
  978. elsif ( $query_type eq 'UPDATE' ) {
  979. @rc = $self->run_custom_queries( $tablename, 'INSERT', $linenum );
  980. }
  981. elsif ( $query_type eq 'SELECT' ) {
  982. for (@rc) {
  983. my $query = shift @{$_};
  984. my $sth = $self->{dbh}->prepare($query);
  985. $sth->execute( $self->{id} );
  986. my $ref = $sth->fetchrow_hashref('NAME_lc');
  987. for ( keys %{$ref} ) {
  988. $self->{$_} = $ref->{$_};
  989. }
  990. }
  991. }
  992. @rc;
  993. }
  994. =item $form->dbconnect($myconfig);
  995. Returns an autocommit connection to the database specified in $myconfig.
  996. =cut
  997. sub dbconnect {
  998. my ( $self, $myconfig ) = @_;
  999. # connect to database
  1000. my $dbh = DBI->connect( $myconfig->{dbconnect},
  1001. $myconfig->{dbuser}, $myconfig->{dbpasswd} )
  1002. or $self->dberror;
  1003. $dbh->{pg_enable_utf8} = 1;
  1004. # set db options
  1005. if ( $myconfig->{dboptions} ) {
  1006. $dbh->do( $myconfig->{dboptions} )
  1007. || $self->dberror( $myconfig->{dboptions} );
  1008. }
  1009. $dbh;
  1010. }
  1011. =item $form->dbconnect_noauto($myconfig);
  1012. Returns a non-autocommit connection to the database specified in $myconfig.
  1013. =cut
  1014. sub dbconnect_noauto {
  1015. my ( $self, $myconfig ) = @_;
  1016. # connect to database
  1017. my $dbh = DBI->connect(
  1018. $myconfig->{dbconnect}, $myconfig->{dbuser},
  1019. $myconfig->{dbpasswd}, { AutoCommit => 0 }
  1020. ) or $self->dberror;
  1021. $dbh->{pg_enable_utf8} = 1;
  1022. # set db options
  1023. if ( $myconfig->{dboptions} ) {
  1024. $dbh->do( $myconfig->{dboptions} );
  1025. }
  1026. $dbh;
  1027. }
  1028. =item $form->dbquote($var);
  1029. If $var is an empty string, return NULL, otherwise return $var as quoted by
  1030. $form->{dbh}->quote($var).
  1031. =cut
  1032. sub dbquote {
  1033. my ( $self, $var ) = @_;
  1034. if ( $var eq '' ) {
  1035. $_ = "NULL";
  1036. }
  1037. else {
  1038. $_ = $self->{dbh}->quote($var);
  1039. }
  1040. $_;
  1041. }
  1042. =item $form->update_balance($dbh, $table, $field, $where, $value);
  1043. B<WARNING>: This is a dangerous private function. All apps calling it must be
  1044. careful to avoid SQL injection issues.
  1045. If $value is set, sets the value of $field in $table to the sum of the current
  1046. stored value and $value. In order to not annihilate the values in $table,
  1047. $where must contain a WHERE clause that limits the UPDATE to a single row.
  1048. =cut
  1049. sub update_balance {
  1050. # This is a dangerous private function. All apps calling it must
  1051. # be careful to avoid SQL injection issues
  1052. my ( $self, $dbh, $table, $field, $where, $value ) = @_;
  1053. $table = $dbh->quote_identifier($table);
  1054. $field = $dbh->quote_identifier($field);
  1055. # if we have a value, go do it
  1056. if ($value) {
  1057. # retrieve balance from table
  1058. my $query = "SELECT $field FROM $table WHERE $where FOR UPDATE";
  1059. my ($balance) = $dbh->selectrow_array($query);
  1060. $balance = $dbh->quote($balance + $value);
  1061. # update balance
  1062. $query = "UPDATE $table SET $field = $balance WHERE $where";
  1063. $dbh->do($query) || $self->dberror($query);
  1064. }
  1065. }
  1066. =item $form->update_exchangerate($dbh, $curr, $transdate, $buy, $sell);
  1067. Updates the exchange rates $buy and $sell for the given $currency on $transdate.
  1068. If there is not yet an exchange rate for $currency on $transdate, an entry is
  1069. inserted. This returns without doing anything if $curr eq ''.
  1070. $dbh is not used, favouring $self->{dbh}.
  1071. =cut
  1072. sub update_exchangerate {
  1073. my ( $self, $dbh, $curr, $transdate, $buy, $sell ) = @_;
  1074. # some sanity check for currency
  1075. return if ( $curr eq "" );
  1076. my $query = qq|
  1077. SELECT curr
  1078. FROM exchangerate
  1079. WHERE curr = ?
  1080. AND transdate = ?
  1081. FOR UPDATE|;
  1082. my $sth = $self->{dbh}->prepare($query);
  1083. $sth->execute( $curr, $transdate ) || $self->dberror($query);
  1084. my $set;
  1085. my @queryargs;
  1086. if ( $buy && $sell ) {
  1087. $set = "buy = ?, sell = ?";
  1088. @queryargs = ( $buy, $sell );
  1089. }
  1090. elsif ($buy) {
  1091. $set = "buy = ?";
  1092. @queryargs = ($buy);
  1093. }
  1094. elsif ($sell) {
  1095. $set = "sell = ?";
  1096. @queryargs = ($sell);
  1097. }
  1098. if ( !$set ) {
  1099. $self->error("Exchange rate missing!");
  1100. }
  1101. if ( $sth->fetchrow_array ) {
  1102. $query = qq|UPDATE exchangerate
  1103. SET $set
  1104. WHERE curr = ?
  1105. AND transdate = ?|;
  1106. push( @queryargs, $curr, $transdate );
  1107. }
  1108. else {
  1109. $query = qq|
  1110. INSERT INTO exchangerate (
  1111. curr, buy, sell, transdate)
  1112. VALUES (?, ?, ?, ?)|;
  1113. @queryargs = ( $curr, $buy, $sell, $transdate );
  1114. }
  1115. $sth->finish;
  1116. $sth = $self->{dbh}->prepare($query);
  1117. $sth->execute(@queryargs) || $self->dberror($query);
  1118. }
  1119. =item $form->save_exchangerate($myconfig, $currency, $transdate, $rate, $fld);
  1120. Saves the exchange rate $rate for the given $currency on $transdate for the
  1121. provided purpose in $fld. $fld can be either 'buy' or 'sell'.
  1122. $myconfig is not used. $self->update_exchangerate is used for the majority of
  1123. the work.
  1124. =cut
  1125. sub save_exchangerate {
  1126. my ( $self, $myconfig, $currency, $transdate, $rate, $fld ) = @_;
  1127. my ( $buy, $sell ) = ( 0, 0 );
  1128. $buy = $rate if $fld eq 'buy';
  1129. $sell = $rate if $fld eq 'sell';
  1130. $self->update_exchangerate( $self->{dbh}, $currency, $transdate, $buy,
  1131. $sell );
  1132. }
  1133. =item $form->get_exchangerate($dbh, $curr, $transdate, $fld);
  1134. Returns the exchange rate in relation to the default currency for $currency on
  1135. $transdate for the purpose indicated by $fld. $fld can be either 'buy' or
  1136. 'sell' to get usable results.
  1137. $dbh is not used, favouring $self->{dbh}.
  1138. =cut
  1139. sub get_exchangerate {
  1140. my ( $self, $dbh, $curr, $transdate, $fld ) = @_;
  1141. my $exchangerate = 1;
  1142. if ($transdate) {
  1143. my $query = qq|
  1144. SELECT $fld FROM exchangerate
  1145. WHERE curr = ? AND transdate = ?|;
  1146. my $sth = $self->{dbh}->prepare($query);
  1147. $sth->execute( $curr, $transdate );
  1148. ($exchangerate) = $sth->fetchrow_array;
  1149. $exchangerate = Math::BigFloat->new($exchangerate);
  1150. $sth->finish;
  1151. }
  1152. $exchangerate;
  1153. }
  1154. =item $form->check_exchangerate($myconfig, $currency, $transdate, $fld);
  1155. Returns some true value when an entry for $currency on $transdate is true for
  1156. the purpose indicated by $fld. $fld can be either 'buy' or 'sell' to get
  1157. usable results. Returns false if $transdate is not set.
  1158. $myconfig is not used.
  1159. =cut
  1160. sub check_exchangerate {
  1161. my ( $self, $myconfig, $currency, $transdate, $fld ) = @_;
  1162. return "" unless $transdate;
  1163. my $query = qq|
  1164. SELECT $fld
  1165. FROM exchangerate
  1166. WHERE curr = ? AND transdate = ?|;
  1167. my $sth = $self->{dbh}->prepare($query);
  1168. $sth->execute( $currency, $transdate );
  1169. my @array = $sth->fetchrow_array;
  1170. $self->db_parse_numeric(sth => $sth, arrayref => \@array);
  1171. my ($exchangerate) = @array;
  1172. $sth->finish;
  1173. $exchangerate;
  1174. }
  1175. =item $form->add_shipto($dbh, $id);
  1176. Inserts a new address into the table shipto if the value of any of the shipto
  1177. address components in $form differs to the regular attribute in $form. The
  1178. inserted value of trans_id is $id, the other fields correspond with the shipto
  1179. address components of $form.
  1180. $dbh is unused.
  1181. =cut
  1182. sub add_shipto {
  1183. my ( $self, $dbh, $id ) = @_;
  1184. my $shipto;
  1185. foreach my $item (
  1186. qw(name address1 address2 city state
  1187. zipcode country contact phone fax email)
  1188. )
  1189. {
  1190. if ( $self->{"shipto$item"} ne "" ) {
  1191. $shipto = 1 if ( $self->{$item} ne $self->{"shipto$item"} );
  1192. }
  1193. }
  1194. if ($shipto) {
  1195. my $query = qq|
  1196. INSERT INTO shipto
  1197. (trans_id, shiptoname, shiptoaddress1,
  1198. shiptoaddress2, shiptocity, shiptostate,
  1199. shiptozipcode, shiptocountry, shiptocontact,
  1200. shiptophone, shiptofax, shiptoemail)
  1201. VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  1202. |;
  1203. my $sth = $self->{dbh}->prepare($query) || $self->dberror($query);
  1204. $sth->execute(
  1205. $id, $self->{shiptoname},
  1206. $self->{shiptoaddress1}, $self->{shiptoaddress2},
  1207. $self->{shiptocity}, $self->{shiptostate},
  1208. $self->{shiptozipcode}, $self->{shiptocountry},
  1209. $self->{shiptocontact}, $self->{shiptophone},
  1210. $self->{shiptofax}, $self->{shiptoemail}
  1211. ) || $self->dberror($query);
  1212. $sth->finish;
  1213. }
  1214. }
  1215. =item $form->get_employee($dbh);
  1216. Returns a list containing the name and id of the employee $form->{login}. Any
  1217. portion of $form->{login} including and past '@' are ignored.
  1218. $dbh is unused.
  1219. =cut
  1220. sub get_employee {
  1221. my ( $self, $dbh ) = @_;
  1222. my $login = $self->{login};
  1223. $login =~ s/@.*//;
  1224. my $query = qq|
  1225. SELECT name, id
  1226. FROM entity WHERE id IN (select entity_id
  1227. FROM users
  1228. WHERE username = ?)|;
  1229. my $sth = $self->{dbh}->prepare($query);
  1230. $sth->execute($login);
  1231. my (@a) = $sth->fetchrow_array();
  1232. $a[1] *= 1;
  1233. $sth->finish;
  1234. @a;
  1235. }
  1236. =item $form->get_name($myconfig, $table[, $transdate])
  1237. Sets $form->{name_list} to refer to a list of customers or vendors whose names
  1238. or numbers match the value found in $form->{$table} and returns the number of
  1239. matches. $table can be 'vendor', 'customer', or 'employee'; if the optional
  1240. field $transdate is provided, the result set is further limited to $table
  1241. entries which were active on the provided date as determined by the start and
  1242. end dates. The elements of $form->{name_list} are references returned rows in
  1243. hashref form and are sorted by the name field. The fields of the hash are those
  1244. of the view $table and the table entity.
  1245. $myconfig is unused.
  1246. =cut
  1247. # this sub gets the id and name from $table
  1248. sub get_name {
  1249. my ( $self, $myconfig, $table, $transdate ) = @_;
  1250. my @queryargs;
  1251. my $where;
  1252. if ($transdate) {
  1253. $where = qq|
  1254. AND (c.startdate IS NULL OR c.startdate <= ?)
  1255. AND (c.enddate IS NULL OR c.enddate >= ?)|;
  1256. @queryargs = ( $transdate, $transdate );
  1257. }
  1258. # SC: Check for valid table/view name. Other values will cause SQL errors.
  1259. if ($table !~ /^(vendor|customer|employee)$/i) {
  1260. $self->error('Invalid name source');
  1261. }
  1262. # Company name is stored in $self->{vendor} or $self->{customer}
  1263. if ($self->{"${table}number"} eq ''){
  1264. $self->{"${table}number"} = $self->{$table};
  1265. }
  1266. my $name = $self->like( lc $self->{$table} );
  1267. # Vendor and Customer are now views into entity_credit_account.
  1268. my $query = qq/
  1269. SELECT c.*, e.name FROM entity_credit_account c
  1270. JOIN entity e ON (c.entity_id = e.id)
  1271. WHERE (lower(e.name) LIKE ?
  1272. OR c.meta_number LIKE ?)
  1273. $where
  1274. ORDER BY e.name/;
  1275. unshift( @queryargs, $name, $self->{"${table}number"} );
  1276. my $sth = $self->{dbh}->prepare($query);
  1277. $sth->execute(@queryargs) || $self->dberror($query);
  1278. my $i = 0;
  1279. @{ $self->{name_list} } = ();
  1280. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1281. push( @{ $self->{name_list} }, $ref );
  1282. $i++;
  1283. }
  1284. $sth->finish;
  1285. return $i;
  1286. }
  1287. =item $form->all_vc($myconfig, $vc, $module, $dbh, $transdate, $job);
  1288. Populates the list referred to by $form->{all_${vc}} with hashes of either
  1289. vendor or customer id and name, ordered by the name. This will be vendor
  1290. details unless $vc is set to 'customer'. This list can be limited to only
  1291. vendors or customers which are usable on a given day by specifying $transdate.
  1292. As a further restriction, $form->{all_${vc}} will not be populated if the
  1293. number of vendors or customers that would be present in that list exceeds, or
  1294. is equal to, $myconfig->{vclimit}.
  1295. In addition to the possible population of $form->{all_${vc}},
  1296. $form->{employee_id} is looked up if not already set, the list
  1297. $form->{all_language} is populated using the language table and is sorted by the
  1298. description, and $form->all_employees, $form->all_departments,
  1299. $form->all_projects, and $form->all_taxaccounts are all run.
  1300. $module and $dbh are unused.
  1301. =cut
  1302. sub all_vc {
  1303. my ( $self, $myconfig, $vc, $module, $dbh, $transdate, $job ) = @_;
  1304. my $ref;
  1305. my $table;
  1306. if ($module eq 'AR'){
  1307. $table = 'ar';
  1308. } elsif ($module eq 'AP'){
  1309. $table = 'ap';
  1310. }
  1311. $dbh = $self->{dbh};
  1312. my $sth;
  1313. if ($vc eq 'customer'){
  1314. $self->{vc_class} = 1;
  1315. } else {
  1316. $self->{vc_class} = 2;
  1317. $vc = 'vendor';
  1318. }
  1319. my $query = qq|SELECT count(*) FROM entity_credit_account ec
  1320. where ec.entity_class = ?|;
  1321. my $where;
  1322. my @queryargs2 = ($self->{vc_class});
  1323. my @queryargs;
  1324. if ($transdate) {
  1325. $query .= qq| AND (ec.startdate IS NULL OR ec.startdate <= ?)
  1326. AND (ec.enddate IS NULL OR ec.enddate >= ?)|;
  1327. $where = qq| (ec.startdate IS NULL OR ec.startdate <= ?)
  1328. AND (ec.enddate IS NULL OR ec.enddate >= ?)
  1329. AND ec.entity_class = ?|;
  1330. push (@queryargs, $transdate, $transdate, $self->{vc_class});
  1331. push (@queryargs2, $transdate, $transdate);
  1332. } else {
  1333. $where = " true";
  1334. }
  1335. $sth = $dbh->prepare($query);
  1336. $sth->execute(@queryargs2);
  1337. my ($count) = $sth->fetchrow_array;
  1338. $sth->finish;
  1339. # build selection list
  1340. if ( $count < $myconfig->{vclimit} ) {
  1341. $self->{"${vc}_id"} *= 1;
  1342. # TODO: Alter this so that it pulls up the entity_credit_account
  1343. # instead of the entity_id. --CT
  1344. $query = qq|
  1345. SELECT ec.id, e.name
  1346. FROM entity e
  1347. JOIN entity_credit_account ec ON (ec.entity_id = e.id)
  1348. WHERE ec.id = ? OR $where
  1349. ORDER BY name|;
  1350. push( @queryargs, $self->{"${vc}_id"} );
  1351. $sth = $dbh->prepare($query);
  1352. $sth->execute(@queryargs) || $self->dberror($query);
  1353. @{ $self->{"all_$vc"} } = ();
  1354. while ( $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1355. push @{ $self->{"all_$vc"} }, $ref;
  1356. }
  1357. $sth->finish;
  1358. } elsif ($self->{id}) {
  1359. $query = qq|
  1360. SELECT ec.id, e.name
  1361. FROM entity e
  1362. JOIN entity_credit_account ec ON (ec.entity_id = e.id)
  1363. WHERE ec.id = (select entity_credit_account FROM $table
  1364. WHERE id = ?)
  1365. ORDER BY name|;
  1366. $sth = $self->{dbh}->prepare($query);
  1367. $sth->execute($self->{id});
  1368. ($self->{"${vc}_id"}, $self->{$vc}) = $sth->fetchrow_array();
  1369. }
  1370. # get self
  1371. if ( !$self->{employee_id} ) {
  1372. ( $self->{employee}, $self->{employee_id} ) = split /--/,
  1373. $self->{employee};
  1374. ( $self->{employee}, $self->{employee_id} ) = $self->get_employee($dbh)
  1375. unless $self->{employee_id};
  1376. }
  1377. $self->all_employees( $myconfig, $dbh, $transdate, 1 );
  1378. $self->all_departments( $myconfig, $dbh, $vc );
  1379. $self->all_projects( $myconfig, $dbh, $transdate, $job );
  1380. # get language codes
  1381. $query = qq|SELECT *
  1382. FROM language
  1383. ORDER BY 2|;
  1384. $sth = $dbh->prepare($query);
  1385. $sth->execute || $self->dberror($query);
  1386. $self->{all_language} = ();
  1387. while ( $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1388. push @{ $self->{all_language} }, $ref;
  1389. }
  1390. $sth->finish;
  1391. $self->all_taxaccounts( $myconfig, $dbh, $transdate );
  1392. }
  1393. =item $form->all_accounts()
  1394. Sets $form->{accounts} to all accounts. Returns the list as well.
  1395. Example: my @account_list = $form->all_accounts();
  1396. =cut
  1397. sub all_accounts {
  1398. my ($self) = @_;
  1399. my $ref;
  1400. $self->{all_accounts} = [];
  1401. my $sth = $self->{dbh}->prepare('SELECT * FROM chart_list_all()');
  1402. $sth->execute || $self->dberror('SELECT * FROM chart_list_all()');
  1403. while ($ref = $sth->fetchrow_hashref('NAME_lc')){
  1404. push(@{$self->{all_accounts}}, $ref);
  1405. }
  1406. $sth->finish;
  1407. return @{$self->{all_accounts}};
  1408. }
  1409. =item $form->all_taxaccounts($myconfig, $dbh2[, $transdate]);
  1410. Get the tax rates and numbers for all the taxes in $form->{taxaccounts}. Does
  1411. nothing if $form->{taxaccounts} is false. Taxes are listed as a space seperated
  1412. list of account numbers from the chart. The retrieved values are placed within
  1413. $form->{${accno}_rate} and $form->{${accno}_taxnumber}. If $transdate is set,
  1414. then only process taxes that were valid on $transdate.
  1415. $myconfig and $dbh2 are unused.
  1416. =cut
  1417. sub all_taxaccounts {
  1418. my ( $self, $myconfig, $dbh2, $transdate ) = @_;
  1419. my $dbh = $self->{dbh};
  1420. my $sth;
  1421. my $query;
  1422. my $where;
  1423. my @queryargs = ();
  1424. if ($transdate) {
  1425. $where = qq| AND (t.validto >= ? OR t.validto IS NULL)|;
  1426. push( @queryargs, $transdate );
  1427. }
  1428. if ( $self->{taxaccounts} ) {
  1429. # rebuild tax rates
  1430. $query = qq|SELECT t.rate, t.taxnumber
  1431. FROM tax t
  1432. JOIN chart c ON (c.id = t.chart_id)
  1433. WHERE c.accno = ?
  1434. $where
  1435. ORDER BY accno, validto|;
  1436. $sth = $dbh->prepare($query) || $self->dberror($query);
  1437. foreach my $accno ( split / /, $self->{taxaccounts} ) {
  1438. $sth->execute( $accno, @queryargs );
  1439. ( $self->{"${accno}_rate"}, $self->{"${accno}_taxnumber"} ) =
  1440. $sth->fetchrow_array;
  1441. $sth->finish;
  1442. }
  1443. }
  1444. }
  1445. =item $form->all_employees($myconfig, $dbh2, $transdate, $sales);
  1446. Sets $form->{all_employee} to be a reference to an array referencing hashes of
  1447. employee information. The hashes are of the form {'id' => id, 'name' => name}.
  1448. If $transdate is set, the query is limited to employees who are active on that
  1449. day. If $sales is true, only employees with the sales flag set are added.
  1450. $dbh2 is unused.
  1451. =cut
  1452. sub all_employees {
  1453. my ( $self, $myconfig, $dbh2, $transdate, $sales ) = @_;
  1454. my $dbh = $self->{dbh};
  1455. my @whereargs = ();
  1456. # setup employees/sales contacts
  1457. my $query = qq|
  1458. SELECT id, name
  1459. FROM entity
  1460. WHERE id IN (SELECT entity_id FROM employee
  1461. WHERE|;
  1462. if ($transdate) {
  1463. $query .= qq| (startdate IS NULL OR startdate <= ?)
  1464. AND (enddate IS NULL OR enddate >= ?) AND|;
  1465. @whereargs = ( $transdate, $transdate );
  1466. }
  1467. else {
  1468. $query .= qq| enddate IS NULL AND|;
  1469. }
  1470. if ($sales) {
  1471. $query .= qq| sales = '1' AND|;
  1472. }
  1473. $query =~ s/(WHERE|AND)$//;
  1474. $query .= qq|) ORDER BY name|;
  1475. my $sth = $dbh->prepare($query);
  1476. $sth->execute(@whereargs) || $self->dberror($query);
  1477. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1478. push @{ $self->{all_employee} }, $ref;
  1479. }
  1480. $sth->finish;
  1481. }
  1482. =item $form->all_projects($myconfig, $dbh2[, $transdate, $job]);
  1483. Populates the list referred to as $form->{all_project} with hashes detailing
  1484. all projects. If $job is true, limit the projects to those whose ids are not
  1485. also present in parts with a project_id > 0. If $transdate is set, the projects
  1486. are limited to those valid on $transdate. If $form->{language_code} is set,
  1487. include the translation description in the project list and limit to
  1488. translations with a matching language_code. The result list,
  1489. $form->{all_project}, is sorted by projectnumber.
  1490. $myconfig and $dbh2 are unused. $job appears to be part of attempted job-
  1491. costing support.
  1492. =cut
  1493. sub all_projects {
  1494. my ( $self, $myconfig, $dbh2, $transdate, $job ) = @_;
  1495. my $dbh = $self->{dbh};
  1496. my @queryargs = ();
  1497. my $where = "1 = 1";
  1498. $where = qq|id NOT IN (SELECT id
  1499. FROM parts
  1500. WHERE project_id > 0)| if !$job;
  1501. my $query = qq|SELECT *
  1502. FROM project
  1503. WHERE $where|;
  1504. if ( $self->{language_code} ) {
  1505. $query = qq|
  1506. SELECT pr.*, t.description AS translation
  1507. FROM project pr
  1508. LEFT JOIN translation t ON (t.trans_id = pr.id)
  1509. WHERE t.language_code = ?|;
  1510. push( @queryargs, $self->{language_code} );
  1511. }
  1512. if ($transdate) {
  1513. $query .= qq| AND (startdate IS NULL OR startdate <= ?)
  1514. AND (enddate IS NULL OR enddate >= ?)|;
  1515. push( @queryargs, $transdate, $transdate );
  1516. }
  1517. $query .= qq| ORDER BY projectnumber|;
  1518. my $sth = $dbh->prepare($query);
  1519. $sth->execute(@queryargs) || $self->dberror($query);
  1520. @{ $self->{all_project} } = ();
  1521. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1522. push @{ $self->{all_project} }, $ref;
  1523. }
  1524. $sth->finish;
  1525. }
  1526. =item $form->all_departments($myconfig, $dbh2, $vc);
  1527. Set $form->{all_department} to be a reference to a list of hashrefs describing
  1528. departments of the form {'id' => id, 'description' => description}. If $vc
  1529. is 'customer', further limit the results to those whose role is 'P' (Profit
  1530. Center).
  1531. This procedure is internally followed by a call to $form->all_years($myconfig).
  1532. $dbh2 is not used.
  1533. =cut
  1534. sub all_departments {
  1535. my ( $self, $myconfig, $dbh2, $vc ) = @_;
  1536. my $dbh = $self->{dbh};
  1537. my $where = "1 = 1";
  1538. if ($vc) {
  1539. if ( $vc eq 'customer' ) {
  1540. $where = " role = 'P'";
  1541. }
  1542. }
  1543. my $query = qq|SELECT id, description
  1544. FROM department
  1545. WHERE $where
  1546. ORDER BY 2|;
  1547. my $sth = $dbh->prepare($query);
  1548. $sth->execute || $self->dberror($query);
  1549. @{ $self->{all_department} } = ();
  1550. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1551. push @{ $self->{all_department} }, $ref;
  1552. }
  1553. $sth->finish;
  1554. $self->all_years($myconfig);
  1555. }
  1556. =item $form->all_languages($myconfig);
  1557. Set $form->{all_language} to be a reference to a list of hashrefs describing
  1558. languages using the form {'code' => code, 'description' => description}.
  1559. =cut
  1560. sub all_languages {
  1561. my ( $self ) = @_;
  1562. my $dbh = $self->{dbh};
  1563. my $query = qq|
  1564. SELECT code, description
  1565. FROM language
  1566. ORDER BY description|;
  1567. my $sth = $dbh->prepare($query);
  1568. $sth->execute || $self->dberror($query);
  1569. $self->{all_language} = [];
  1570. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1571. push @{ $self->{all_language} }, $ref;
  1572. }
  1573. $sth->finish;
  1574. }
  1575. =item $form->all_years($myconfig[, $dbh2]);
  1576. Populates the hash $form->{all_month} with a mapping between a two-digit month
  1577. number and the English month name. Populates the list $form->{all_years} with
  1578. all years which contain transactions.
  1579. $dbh2 is unused.
  1580. =cut
  1581. sub all_years {
  1582. my ( $self, $myconfig ) = @_;
  1583. my $dbh = $self->{dbh};
  1584. $self->{all_years} = [];
  1585. # get years
  1586. my $query = qq|
  1587. SELECT extract('YEARS' FROM transdate) FROM acc_trans
  1588. GROUP BY extract('YEARS' FROM transdate) ORDER BY 1 DESC|;
  1589. my $sth = $dbh->prepare($query);
  1590. $sth->execute();
  1591. while (my ($year) = $sth->fetchrow_array()){
  1592. push @{$self->{all_years}}, $year;
  1593. }
  1594. #this should probably be changed to use locale
  1595. %{ $self->{all_month} } = (
  1596. '01' => 'January',
  1597. '02' => 'February',
  1598. '03' => 'March',
  1599. '04' => 'April',
  1600. '05' => 'May ',
  1601. '06' => 'June',
  1602. '07' => 'July',
  1603. '08' => 'August',
  1604. '09' => 'September',
  1605. '10' => 'October',
  1606. '11' => 'November',
  1607. '12' => 'December'
  1608. );
  1609. }
  1610. =item $form->create_links($module, $myconfig, $vc[, $job]);
  1611. Populates the hash referred to as $form->{${module}_links} details about
  1612. accounts that have $module in their link field. The hash is keyed upon link
  1613. elements such as 'AP_amount' and 'AR_tax' and they refer to lists of hashes
  1614. containing accno and description for the appropriate accounts. If the key does
  1615. not contain 'tax', the account number is appended to the space seperated list
  1616. $form->{accounts}. $module is typically 'AR' or 'AP' and is the base type of
  1617. the accounts looked up.
  1618. If $form->{id} is not set, check $form->{"$form->{vc}_id"}. If neither is set,
  1619. use $form->lastname_used to populate the details. If $form->{id} is set,
  1620. populate the invnumber, transdate, ${vc}_id, datepaid, duedate, ordnumber,
  1621. taxincluded, currency, notes, intnotes, ${vc}, department_id, department,
  1622. oldinvtotal, oldtotalpaid, employee_id, employee, language_code, ponumber,
  1623. reverse, printed, emailed, queued, recurring, exchangerate, and acc_trans
  1624. attributes of $form with details about the transaction $form->{id}. All of
  1625. these attributes, save for acc_trans, are scalar; $form->{acc_trans} refers to
  1626. a hash keyed by link elements whose values are lists of references to hashes
  1627. describing acc_trans table entries corresponding to the transaction $form->{id}.
  1628. The elements in the acc_trans entry hashes are accno, description, source,
  1629. amount, memo, transdate, cleared, project_id, projectnumber, and exchangerate.
  1630. The closedto, separate_duties, revtrans, and currencies $form attributes are filled with values
  1631. from the defaults table, while $form->{current_date} is populated with the
  1632. current date. If $form->{id} is not set, then $form->{transdate} also takes on
  1633. the current date.
  1634. After all this, it calls $form->all_vc to conclude.
  1635. =cut
  1636. sub create_links {
  1637. my ( $self, $module, $myconfig, $vc, $job ) = @_;
  1638. # get last customers or vendors
  1639. my ( $query, $sth );
  1640. if (!$self->{dbh}) {
  1641. $self->db_init($myconfig);
  1642. }
  1643. my $dbh = $self->{dbh};
  1644. my %xkeyref = ();
  1645. my $val;
  1646. my $ref;
  1647. my $key;
  1648. # now get the account numbers
  1649. $query = qq|SELECT accno, description, link
  1650. FROM chart
  1651. WHERE link LIKE ?
  1652. ORDER BY accno|;
  1653. $sth = $dbh->prepare($query);
  1654. $sth->execute( "%" . "$module%" ) || $self->dberror($query);
  1655. $self->{accounts} = "";
  1656. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1657. foreach my $key ( split /:/, $ref->{link} ) {
  1658. if ( $key =~ /$module/ ) {
  1659. # cross reference for keys
  1660. $xkeyref{ $ref->{accno} } = $key;
  1661. push @{ $self->{"${module}_links"}{$key} },
  1662. {
  1663. accno => $ref->{accno},
  1664. description => $ref->{description}
  1665. };
  1666. $self->{accounts} .= "$ref->{accno} "
  1667. unless $key =~ /tax/;
  1668. }
  1669. }
  1670. }
  1671. $sth->finish;
  1672. my $arap = ( $vc eq 'customer' ) ? 'ar' : 'ap';
  1673. $vc = 'vendor' unless $vc eq 'customer';
  1674. if ( $self->{id} ) {
  1675. $query = qq|
  1676. SELECT a.invnumber, a.transdate,
  1677. a.entity_credit_account AS entity_id,
  1678. a.datepaid, a.duedate, a.ordnumber,
  1679. a.taxincluded, a.curr AS currency, a.notes,
  1680. a.intnotes, ce.name AS $vc, a.department_id,
  1681. d.description AS department,
  1682. a.amount AS oldinvtotal, a.paid AS oldtotalpaid,
  1683. a.person_id, e.name AS employee,
  1684. c.language_code, a.ponumber, a.reverse,
  1685. a.approved
  1686. FROM $arap a
  1687. JOIN entity_credit_account c
  1688. ON (a.entity_credit_account = c.id)
  1689. JOIN entity ce ON (ce.id = c.entity_id)
  1690. LEFT JOIN employee er ON (er.entity_id = a.person_id)
  1691. LEFT JOIN entity e ON (er.entity_id = e.id)
  1692. LEFT JOIN department d ON (d.id = a.department_id)
  1693. WHERE a.id = ? AND c.entity_class =
  1694. (select id FROM entity_class
  1695. WHERE class ilike ?)|;
  1696. $sth = $dbh->prepare($query);
  1697. $sth->execute( $self->{id}, $self->{vc} ) || $self->dberror($query);
  1698. $ref = $sth->fetchrow_hashref('NAME_lc');
  1699. $self->db_parse_numeric(sth=>$sth, hashref=>$ref);
  1700. if (!defined $ref->{approved}){
  1701. $ref->{approved} = 0;
  1702. }
  1703. foreach $key ( keys %$ref ) {
  1704. $self->{$key} = $ref->{$key};
  1705. }
  1706. $sth->finish;
  1707. # get printed, emailed
  1708. $query = qq|
  1709. SELECT s.printed, s.emailed, s.spoolfile, s.formname
  1710. FROM status s WHERE s.trans_id = ?|;
  1711. $sth = $dbh->prepare($query);
  1712. $sth->execute( $self->{id} ) || $self->dberror($query);
  1713. while ( $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1714. $self->{printed} .= "$ref->{formname} "
  1715. if $ref->{printed};
  1716. $self->{emailed} .= "$ref->{formname} "
  1717. if $ref->{emailed};
  1718. $self->{queued} .= "$ref->{formname} " . "$ref->{spoolfile} "
  1719. if $ref->{spoolfile};
  1720. }
  1721. $sth->finish;
  1722. for (qw(printed emailed queued)) { $self->{$_} =~ s/ +$//g }
  1723. # get recurring
  1724. $self->get_recurring($dbh);
  1725. # get amounts from individual entries
  1726. $query = qq|
  1727. SELECT c.accno, c.description, a.source, a.amount,
  1728. a.memo, a.transdate, a.cleared, a.project_id,
  1729. p.projectnumber
  1730. FROM acc_trans a
  1731. JOIN chart c ON (c.id = a.chart_id)
  1732. LEFT JOIN project p ON (p.id = a.project_id)
  1733. WHERE a.trans_id = ?
  1734. AND a.fx_transaction = '0'
  1735. ORDER BY transdate|;
  1736. $sth = $dbh->prepare($query);
  1737. $sth->execute( $self->{id} ) || $self->dberror($query);
  1738. my $fld = ( $vc eq 'customer' ) ? 'buy' : 'sell';
  1739. $self->{exchangerate} =
  1740. $self->get_exchangerate( $dbh, $self->{currency}, $self->{transdate},
  1741. $fld );
  1742. # store amounts in {acc_trans}{$key} for multiple accounts
  1743. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1744. $ref->{exchangerate} =
  1745. $self->get_exchangerate( $dbh, $self->{currency},
  1746. $ref->{transdate}, $fld );
  1747. if ($self->{reverse}){
  1748. $ref->{amount} *= -1;
  1749. }
  1750. push @{ $self->{acc_trans}{ $xkeyref{ $ref->{accno} } } }, $ref;
  1751. }
  1752. $sth->finish;
  1753. }
  1754. else {
  1755. if ( !$self->{"$self->{vc}_id"} ) {
  1756. $self->lastname_used( $myconfig, $dbh, $vc, $module );
  1757. }
  1758. }
  1759. for (qw(separate_duties current_date curr closedto revtrans)) {
  1760. if ($_ eq 'closedto'){
  1761. $query = qq|
  1762. SELECT value::date FROM defaults
  1763. WHERE setting_key = '$_'|;
  1764. } elsif ($_ eq 'current_date') {
  1765. $query = qq| select $_|;
  1766. } else {
  1767. $query = qq|
  1768. SELECT value FROM defaults
  1769. WHERE setting_key = '$_'|;
  1770. }
  1771. $sth = $dbh->prepare($query);
  1772. $sth->execute || $self->dberror($query);
  1773. ($val) = $sth->fetchrow_array();
  1774. if ( $_ eq 'curr' ) {
  1775. $self->{currencies} = $val;
  1776. }
  1777. else {
  1778. $self->{$_} = $val;
  1779. }
  1780. $sth->finish;
  1781. }
  1782. if (!$self->{id} && !$self->{transdate}){
  1783. $self->{transdate} = $self->{current_date};
  1784. }
  1785. $self->all_vc( $myconfig, $vc, $module, $dbh, $self->{transdate}, $job );
  1786. }
  1787. =item $form->lastname_used($myconfig, $dbh2, $vc, $module);
  1788. Fills the name, currency, ${vc}_id, duedate, and possibly invoice_notes
  1789. attributes of $form with the last used values for the transaction type specified
  1790. by both $vc and $form->{type}. $vc can be either 'vendor' or 'customer' and if
  1791. unspecified will take on the value given in $form->{vc}, defaulting to 'vendor'.
  1792. If $form->{type} matches /_order/, the transaction type used is order, if it
  1793. matches /_quotation/, quotations are looked through. If $form->{type} does not
  1794. match either of the above, then ar or ap transactions are used.
  1795. $myconfig, $dbh2, and $module are unused.
  1796. =cut
  1797. sub lastname_used {
  1798. my ( $self, $myconfig, $dbh2, $vc, $module ) = @_;
  1799. my $dbh = $self->{dbh};
  1800. $vc ||= $self->{vc}; # add default to correct for improper passing
  1801. my $arap;
  1802. my $where;
  1803. if ($vc eq 'customer') {
  1804. $arap = 'ar';
  1805. } else {
  1806. $arap = 'ap';
  1807. $vc = 'vendor';
  1808. }
  1809. my $sth;
  1810. if ( $self->{type} =~ /_order/ ) {
  1811. $arap = 'oe';
  1812. $where = "quotation = '0'";
  1813. }
  1814. if ( $self->{type} =~ /_quotation/ ) {
  1815. $arap = 'oe';
  1816. $where = "quotation = '1'";
  1817. }
  1818. $where = "AND $where " if $where;
  1819. my $inv_notes;
  1820. $inv_notes = "ct.invoice_notes," if $vc eq 'customer';
  1821. my $query = qq|
  1822. SELECT entity.name, ct.curr AS currency, entity_id AS ${vc}_id,
  1823. current_date + ct.terms AS duedate,
  1824. $inv_notes
  1825. ct.curr AS currency
  1826. FROM $vc ct
  1827. JOIN entity ON (ct.entity_id = entity.id)
  1828. WHERE entity.id = (select entity_id from $arap
  1829. where entity_id IS NOT NULL $where
  1830. order by id DESC limit 1)|;
  1831. $sth = $self->{dbh}->prepare($query);
  1832. $sth->execute() || $self->dberror($query);
  1833. my $ref = $sth->fetchrow_hashref('NAME_lc');
  1834. for ( keys %$ref ) { $self->{$_} = $ref->{$_} }
  1835. $sth->finish;
  1836. }
  1837. =item $form->current_date($myconfig[, $thisdate, $days]);
  1838. If $thisdate is false, get the current date from the database.
  1839. If $thisdate is true, get the date $days days from $thisdate in the date
  1840. format specified by $myconfig->{dateformat} from the database.
  1841. =cut
  1842. sub current_date {
  1843. my ( $self, $myconfig, $thisdate, $days ) = @_;
  1844. my $dbh = $self->{dbh};
  1845. my $query;
  1846. my @queryargs;
  1847. $days *= 1;
  1848. if ($thisdate) {
  1849. my $dateformat = $myconfig->{dateformat};
  1850. if ( $myconfig->{dateformat} !~ /^y/ ) {
  1851. my @a = split /\D/, $thisdate;
  1852. $dateformat .= "yy" if ( length $a[2] > 2 );
  1853. }
  1854. if ( $thisdate !~ /\D/ ) {
  1855. $dateformat = 'yyyymmdd';
  1856. }
  1857. $query = qq|SELECT (to_date(?, ?)
  1858. + ?::interval)::date AS thisdate|;
  1859. @queryargs = ( $thisdate, $dateformat, sprintf('%d days', $days) );
  1860. }
  1861. else {
  1862. $query = qq|SELECT current_date AS thisdate|;
  1863. @queryargs = ();
  1864. }
  1865. my $sth = $dbh->prepare($query);
  1866. $sth->execute(@queryargs);
  1867. my ($thisdate) = $sth->fetchrow_array;
  1868. $thisdate;
  1869. }
  1870. =item $form->like($str);
  1871. Returns '%$str%'
  1872. =cut
  1873. sub like {
  1874. my ( $self, $str ) = @_;
  1875. "%$str%";
  1876. }
  1877. =item $form->redo_rows($flds, $new, $count, $numrows);
  1878. $flds refers to a list of field names and $new refers to a list of row detail
  1879. hashes with the elements of $flds as keys as well as runningnumber for an order
  1880. or another multi-row item that normally expresses elements in the form
  1881. $form->{${fieldname}_${index}}.
  1882. For every $field in @{$flds} populates $form->{${field}_$i} with an appropriate
  1883. value from a $new detail hash where $i is an index between 1 and $count. The
  1884. ordering of the details is done in terms of the runningnumber element of the
  1885. row detail hashes in $new.
  1886. All $form attributes with names of the form ${field}_$i where the index $i is
  1887. between $count + 1 and $numrows is deleted.
  1888. =cut
  1889. sub redo_rows {
  1890. my ( $self, $flds, $new, $count, $numrows ) = @_;
  1891. my @ndx = ();
  1892. for ( 1 .. $count ) {
  1893. push @ndx, { num => $new->[ $_ - 1 ]->{runningnumber}, ndx => $_ };
  1894. }
  1895. my $i = 0;
  1896. # fill rows
  1897. foreach my $item ( sort { $a->{num} <=> $b->{num} } @ndx ) {
  1898. $i++;
  1899. my $j = $item->{ndx} - 1;
  1900. for ( @{$flds} ) { $self->{"${_}_$i"} = $new->[$j]->{$_} }
  1901. }
  1902. # delete empty rows
  1903. for $i ( $count + 1 .. $numrows ) {
  1904. for ( @{$flds} ) { delete $self->{"${_}_$i"} }
  1905. }
  1906. }
  1907. =item $form->get_partsgroup($myconfig[, $p]);
  1908. Populates the list referred to as $form->{all_partsgroup}. $p refers to a hash
  1909. that describes which partsgroups to retrieve. $p->{searchitems} can be 'part',
  1910. 'service', 'assembly', 'labor', or 'nolabor' and will limit the groups to those
  1911. that contain the item type described. $p->{searchitems} and $p->{all} conflict.
  1912. If $p->{all} is set and $p->{language_code} is not, all partsgroups are
  1913. retrieved. If $p->{language_code} is set, also include the translation
  1914. description specified by $p->{language_code} for the partsgroup.
  1915. The results in $form->{all_partsgroup} are normally sorted by partsgroup name.
  1916. If a language_code is specified, the results are then sorted by the translated
  1917. description.
  1918. $myconfig is unused.
  1919. =cut
  1920. sub get_partsgroup {
  1921. my ( $self, $myconfig, $p ) = @_;
  1922. my $dbh = $self->{dbh};
  1923. my $query = qq|SELECT DISTINCT pg.id, pg.partsgroup
  1924. FROM partsgroup pg
  1925. JOIN parts p ON (p.partsgroup_id = pg.id)|;
  1926. my $where;
  1927. my $sortorder = "partsgroup";
  1928. if ( $p->{searchitems} eq 'part' ) {
  1929. $where = qq| WHERE (p.inventory_accno_id > 0
  1930. AND p.income_accno_id > 0)|;
  1931. } elsif ( $p->{searchitems} eq 'service' ) {
  1932. $where = qq| WHERE p.inventory_accno_id IS NULL|;
  1933. } elsif ( $p->{searchitems} eq 'assembly' ) {
  1934. $where = qq| WHERE p.assembly = '1'|;
  1935. } elsif ( $p->{searchitems} eq 'labor' ) {
  1936. $where =
  1937. qq| WHERE p.inventory_accno_id > 0 AND p.income_accno_id IS NULL|;
  1938. } elsif ( $p->{searchitems} eq 'nolabor' ) {
  1939. $where = qq| WHERE p.income_accno_id > 0|;
  1940. }
  1941. if ( $p->{all} ) {
  1942. $query = qq|SELECT id, partsgroup
  1943. FROM partsgroup|;
  1944. }
  1945. my @queryargs = ();
  1946. if ( $p->{language_code} ) {
  1947. $sortorder = "translation";
  1948. $query = qq|
  1949. SELECT DISTINCT pg.id, pg.partsgroup,
  1950. t.description AS translation
  1951. FROM partsgroup pg
  1952. JOIN parts p ON (p.partsgroup_id = pg.id)
  1953. LEFT JOIN translation t ON (t.trans_id = pg.id
  1954. AND t.language_code = ?)|;
  1955. @queryargs = ( $p->{language_code} );
  1956. }
  1957. $query .= qq| $where ORDER BY $sortorder|;
  1958. my $sth = $dbh->prepare($query);
  1959. $sth->execute(@queryargs) || $self->dberror($query);
  1960. $self->{all_partsgroup} = ();
  1961. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  1962. push @{ $self->{all_partsgroup} }, $ref;
  1963. }
  1964. $sth->finish;
  1965. }
  1966. =item $form->update_status($myconfig);
  1967. DELETEs all status rows which have a formname of $form->{formname} and a
  1968. trans_id of $form->{id}. INSERTs a new row into status where trans_id is
  1969. $form->{id}, formname is $form->{formname}, printed and emailed are true if
  1970. their respective $form attributes match /$form->{formname}/, and spoolfile is
  1971. the file extracted from the string $form->{queued} or NULL if there is no entry
  1972. for $form->{formname}.
  1973. $myconfig is unused.
  1974. =cut
  1975. sub update_status {
  1976. my ( $self, $myconfig ) = @_;
  1977. # no id return
  1978. return unless $self->{id};
  1979. my $dbh = $self->{dbh};
  1980. my %queued = split / +/, $self->{queued};
  1981. my $spoolfile =
  1982. ( $queued{ $self->{formname} } )
  1983. ? "'$queued{$self->{formname}}'"
  1984. : 'NULL';
  1985. my $query = qq|DELETE FROM status
  1986. WHERE formname = ?
  1987. AND trans_id = ?|;
  1988. my $sth = $dbh->prepare($query);
  1989. $sth->execute( $self->{formname}, $self->{id} ) || $self->dberror($query);
  1990. $sth->finish;
  1991. my $printed = ( $self->{printed} =~ /$self->{formname}/ ) ? "1" : "0";
  1992. my $emailed = ( $self->{emailed} =~ /$self->{formname}/ ) ? "1" : "0";
  1993. $query = qq|
  1994. INSERT INTO status
  1995. (trans_id, printed, emailed, spoolfile, formname)
  1996. VALUES (?, ?, ?, ?, ?)|;
  1997. $sth = $dbh->prepare($query);
  1998. $sth->execute( $self->{id}, $printed, $emailed, $spoolfile,
  1999. $self->{formname} );
  2000. $sth->finish;
  2001. }
  2002. =item $form->save_status();
  2003. Clears out any old status entries for $form->{id} and saves new status entries.
  2004. Queued form names are extracted from $form->{queued}. Printed and emailed form
  2005. names are extracted from $form->{printed} and $form->{emailed}. The queued,
  2006. printed, and emailed fields are space seperated lists.
  2007. =cut
  2008. sub save_status {
  2009. my ($self) = @_;
  2010. my $dbh = $self->{dbh};
  2011. my $formnames = $self->{printed};
  2012. my $emailforms = $self->{emailed};
  2013. my $query = qq|DELETE FROM status
  2014. WHERE trans_id = ?|;
  2015. my $sth = $dbh->prepare($query);
  2016. $sth->execute( $self->{id} );
  2017. $sth->finish;
  2018. my %queued;
  2019. my $formname;
  2020. my $printed;
  2021. my $emailed;
  2022. if ( $self->{queued} ) {
  2023. %queued = split / +/, $self->{queued};
  2024. foreach $formname ( keys %queued ) {
  2025. $printed = ( $self->{printed} =~ /$formname/ ) ? "1" : "0";
  2026. $emailed = ( $self->{emailed} =~ /$formname/ ) ? "1" : "0";
  2027. if ( $queued{$formname} ) {
  2028. $query = qq|
  2029. INSERT INTO status
  2030. (trans_id, printed, emailed,
  2031. spoolfile, formname)
  2032. VALUES (?, ?, ?, ?, ?)|;
  2033. $sth = $dbh->prepare($query);
  2034. $sth->execute( $self->{id}, $printed, $emailed,
  2035. $queued{$formname}, $formname )
  2036. || $self->dberror($query);
  2037. $sth->finish;
  2038. }
  2039. $formnames =~ s/$formname//;
  2040. $emailforms =~ s/$formname//;
  2041. }
  2042. }
  2043. # save printed, emailed info
  2044. $formnames =~ s/^ +//g;
  2045. $emailforms =~ s/^ +//g;
  2046. my %status = ();
  2047. for ( split / +/, $formnames ) { $status{$_}{printed} = 1 }
  2048. for ( split / +/, $emailforms ) { $status{$_}{emailed} = 1 }
  2049. foreach my $formname ( keys %status ) {
  2050. $printed = ( $formnames =~ /$self->{formname}/ ) ? "1" : "0";
  2051. $emailed = ( $emailforms =~ /$self->{formname}/ ) ? "1" : "0";
  2052. $query = qq|
  2053. INSERT INTO status (trans_id, printed, emailed,
  2054. formname)
  2055. VALUES (?, ?, ?, ?)|;
  2056. $sth = $dbh->prepare($query);
  2057. $sth->execute( $self->{id}, $printed, $emailed, $formname );
  2058. $sth->finish;
  2059. }
  2060. $dbh->commit;
  2061. }
  2062. =item $form->get_recurring();
  2063. Sets $form->{recurring} to contain info about the recurrence schedule for the
  2064. action $form->{id}. $form->{recurring} is of the same form used by
  2065. $form->save_recurring($dbh2, $myconfig).
  2066. reference,startdate,repeat,unit,howmany,payment,print,email,message
  2067. text date int text int int text text text
  2068. =cut
  2069. sub get_recurring {
  2070. my ($self) = @_;
  2071. my $dbh = $self->{dbh};
  2072. my $query = qq/
  2073. SELECT s.*, se.formname || ':' || se.format AS emaila,
  2074. se.message, sp.formname || ':' ||
  2075. sp.format || ':' || sp.printer AS printa
  2076. FROM recurring s
  2077. LEFT JOIN recurringemail se ON (s.id = se.id)
  2078. LEFT JOIN recurringprint sp ON (s.id = sp.id)
  2079. WHERE s.id = ?/;
  2080. my $sth = $dbh->prepare($query);
  2081. $sth->execute( $self->{id} ) || $self->dberror($query);
  2082. for (qw(email print)) { $self->{"recurring$_"} = "" }
  2083. while ( my $ref = $sth->fetchrow_hashref('NAME_lc') ) {
  2084. for ( keys %$ref ) { $self->{"recurring$_"} = $ref->{$_} }
  2085. $self->{recurringemail} .= "$ref->{emaila}:";
  2086. $self->{recurringprint} .= "$ref->{printa}:";
  2087. for (qw(emaila printa)) { delete $self->{"recurring$_"} }
  2088. }
  2089. $sth->finish;
  2090. chop $self->{recurringemail};
  2091. chop $self->{recurringprint};
  2092. if ( $self->{recurringstartdate} ) {
  2093. $self->{recurringreference} =
  2094. $self->escape( $self->{recurringreference}, 1 );
  2095. $self->{recurringmessage} =
  2096. $self->escape( $self->{recurringmessage}, 1 );
  2097. for (
  2098. qw(reference startdate repeat unit howmany
  2099. payment print email message)
  2100. )
  2101. {
  2102. $self->{recurring} .= qq|$self->{"recurring$_"},|;
  2103. }
  2104. chop $self->{recurring};
  2105. }
  2106. }
  2107. =item $form->save_recurring($dbh2, $myconfig);
  2108. Saves or deletes recurring transaction scheduling. $form->{id} is used to
  2109. determine the id used in the various recurring tables. A recurring transaction
  2110. schedule is deleted by having $form->{recurring} be false. For adding or
  2111. updating a schedule, $form->{recurring} is a comma seperated field with partial
  2112. subfield quoting of the form:
  2113. reference,startdate,repeat,unit,howmany,payment,print,email,message
  2114. text date int text int int text text text
  2115. =over
  2116. =item reference
  2117. A URI-encoded reference string for the recurrence set.
  2118. =item startdate
  2119. The index date for the recurrence.
  2120. =item repeat
  2121. The unitless repetition frequency.
  2122. =item unit
  2123. The interval unit used. Can be 'days', 'weeks', 'months', or 'years',
  2124. capitalisation and pluralisation ignored.
  2125. =item howmany
  2126. The number of recurrences for the transaction.
  2127. =item payment
  2128. Flag to indicate if a payment is included in the transaction.
  2129. =item print
  2130. A colon seperated list of formname:format:printer triplets.
  2131. =item email
  2132. A colon seperated list of formname:format pairs.
  2133. =item message
  2134. A URI-encoded message for the emails to be sent.
  2135. =back
  2136. Values for the nextdate and enddate columns of the recurring table are
  2137. calculated using startdate, repeat, unit, howmany, and the current database
  2138. date. All other fields of the recurring, recurringemail, and recurringprint are
  2139. obtained directly from $form->{recurring}.
  2140. B<WARNING>: This function does not check the validity of most subfields of
  2141. $form->{recurring}.
  2142. $dbh2 is not used.
  2143. =cut
  2144. sub save_recurring {
  2145. my ( $self, $dbh2, $myconfig ) = @_;
  2146. my $dbh = $self->{dbh};
  2147. my $query;
  2148. $query = qq|DELETE FROM recurring
  2149. WHERE id = ?|;
  2150. my $sth = $dbh->prepare($query) || $self->dberror($query);
  2151. $sth->execute( $self->{id} ) || $self->dberror($query);
  2152. $query = qq|DELETE FROM recurringemail
  2153. WHERE id = ?|;
  2154. $sth = $dbh->prepare($query) || $self->dberror($query);
  2155. $sth->execute( $self->{id} ) || $self->dberror($query);
  2156. $query = qq|DELETE FROM recurringprint
  2157. WHERE id = ?|;
  2158. $sth = $dbh->prepare($query) || $self->dberror($query);
  2159. $sth->execute( $self->{id} ) || $self->dberror($query);
  2160. if ( $self->{recurring} ) {
  2161. my %s = ();
  2162. (
  2163. $s{reference}, $s{startdate}, $s{repeat},
  2164. $s{unit}, $s{howmany}, $s{payment},
  2165. $s{print}, $s{email}, $s{message}
  2166. ) = split /,/, $self->{recurring};
  2167. if ($s{unit} !~ /^(day|week|month|year)s?$/i){
  2168. $dbh->rollback;
  2169. $self->error("Invalid recurrence unit");
  2170. }
  2171. if ($s{howmany} == 0){
  2172. $self->error("Cannot set to recur 0 times");
  2173. }
  2174. for (qw(reference message)) { $s{$_} = $self->unescape( $s{$_} ) }
  2175. for (qw(repeat howmany payment)) { $s{$_} *= 1 }
  2176. # calculate enddate
  2177. my $advance = $s{repeat} * ( $s{howmany} - 1 );
  2178. my %interval;
  2179. $interval{'Pg'} =
  2180. "(?::date + interval '$advance $s{unit}')";
  2181. $query = qq|SELECT $interval{$myconfig->{dbdriver}}|;
  2182. my ($enddate) = $dbh->selectrow_array($query, undef, $s{startdate});
  2183. # calculate nextdate
  2184. $query = qq|
  2185. SELECT current_date - ?::date AS a,
  2186. ?::date - current_date AS b|;
  2187. $sth = $dbh->prepare($query) || $self->dberror($query);
  2188. $sth->execute( $s{startdate}, $enddate );
  2189. my ( $a, $b ) = $sth->fetchrow_array;
  2190. if ( $a + $b ) {
  2191. $advance =
  2192. int( ( $a / ( $a + $b ) ) * ( $s{howmany} - 1 ) + 1 ) *
  2193. $s{repeat};
  2194. }
  2195. else {
  2196. $advance = 0;
  2197. }
  2198. my $nextdate = $enddate;
  2199. if ( $advance > 0 ) {
  2200. if ( $advance < ( $s{repeat} * $s{howmany} ) ) {
  2201. $query = qq|SELECT (?::date + interval '$advance $s{unit}')|;
  2202. ($nextdate) = $dbh->selectrow_array($query, undef, $s{startdate});
  2203. }
  2204. }
  2205. else {
  2206. $nextdate = $s{startdate};
  2207. }
  2208. if ( $self->{recurringnextdate} ) {
  2209. $nextdate = $self->{recurringnextdate};
  2210. $query = qq|SELECT ?::date - ?::date|;
  2211. if ( $dbh->selectrow_array($query, undef, $enddate, $nextdate) < 0 ) {
  2212. undef $nextdate;
  2213. }
  2214. }
  2215. $self->{recurringpayment} *= 1;
  2216. $query = qq|
  2217. INSERT INTO recurring
  2218. (id, reference, startdate, enddate, nextdate,
  2219. repeat, unit, howmany, payment)
  2220. VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)|;
  2221. $sth = $dbh->prepare($query);
  2222. $sth->execute(
  2223. $self->{id}, $s{reference}, $s{startdate},
  2224. $enddate, $nextdate, $s{repeat},
  2225. $s{unit}, $s{howmany}, $s{payment}
  2226. );
  2227. my @p;
  2228. my $p;
  2229. my $i;
  2230. my $sth;
  2231. if ( $s{email} ) {
  2232. # formname:format
  2233. @p = split /:/, $s{email};
  2234. $query =
  2235. qq|INSERT INTO recurringemail (id, formname, format, message)
  2236. VALUES (?, ?, ?, ?)|;
  2237. $sth = $dbh->prepare($query) || $self->dberror($query);
  2238. for ( $i = 0 ; $i <= $#p ; $i += 2 ) {
  2239. $sth->execute( $self->{id}, $p[$i], $p[ $i + 1 ], $s{message} );
  2240. }
  2241. $sth->finish;
  2242. }
  2243. if ( $s{print} ) {
  2244. # formname:format:printer
  2245. @p = split /:/, $s{print};
  2246. $query =
  2247. qq|INSERT INTO recurringprint (id, formname, format, printer)
  2248. VALUES (?, ?, ?, ?)|;
  2249. $sth = $dbh->prepare($query) || $self->dberror($query);
  2250. for ( $i = 0 ; $i <= $#p ; $i += 3 ) {
  2251. $p = ( $p[ $i + 2 ] ) ? $p[ $i + 2 ] : "";
  2252. $sth->execute( $self->{id}, $p[$i], $p[ $i + 1 ], $p );
  2253. }
  2254. $sth->finish;
  2255. }
  2256. }
  2257. $dbh->commit;
  2258. }
  2259. =item $form->save_intnotes($myconfig, $vc);
  2260. Sets the intnotes field of the entry in the table $vc that has the id
  2261. $form->{id} to the value of $form->{intnotes}.
  2262. Does nothing if $form->{id} is not set.
  2263. =cut
  2264. sub save_intnotes {
  2265. my ( $self, $myconfig, $vc ) = @_;
  2266. # no id return
  2267. return unless $self->{id};
  2268. my $dbh = $self->{dbh};
  2269. my $query = qq|UPDATE $vc SET intnotes = ? WHERE id = ?|;
  2270. my $sth = $dbh->prepare($query);
  2271. $sth->execute( $self->{intnotes}, $self->{id} ) || $self->dberror($query);
  2272. $dbh->commit;
  2273. }
  2274. =item $form->update_defaults($myconfig, $fld[, $dbh]);
  2275. Updates the defaults entry for the setting $fld following rules specified by
  2276. the existing value and returns the processed value that results. If $form is
  2277. false, such as the case when invoked as "Form::update_defaults('',...)", $dbh is
  2278. used as the handle. When $form is set, it uses $form->{dbh}, initialising the
  2279. connection if it does not yet exist. The entry $fld must exist prior to
  2280. executing this function and this update function does not handle the general
  2281. case of updating the defaults table.
  2282. B<NOTE>: rules handling is currently broken.
  2283. Rules followed by this function's processing:
  2284. =over
  2285. =item *
  2286. If digits are found in the field, increment the left-most set. This change,
  2287. unlike the others is reflected in the UPDATE.
  2288. =item *
  2289. Replace <?lsmb date ?> with the date specified in $form->{transdate} formatted
  2290. as $myconfig->{dateformat}.
  2291. =item *
  2292. Replace <?lsmb curr ?> with the value of $form->{currency}
  2293. =back
  2294. =cut
  2295. sub update_defaults {
  2296. my ( $self, $myconfig, $fld ) = @_;
  2297. if ( !$self->{dbh} && $self ) {
  2298. $self->db_init($myconfig);
  2299. }
  2300. my $dbh = $self->{dbh};
  2301. if ( !$self ) {
  2302. $dbh = $_[3];
  2303. }
  2304. my $query = qq|
  2305. SELECT value FROM defaults
  2306. WHERE setting_key = ? FOR UPDATE|;
  2307. my $sth = $dbh->prepare($query);
  2308. $sth->execute($fld);
  2309. ($_) = $sth->fetchrow_array();
  2310. $_ = "0" unless $_;
  2311. # check for and replace
  2312. # <?lsmb DATE ?>, <?lsmb YYMMDD ?>, <?lsmb YEAR ?>, <?lsmb MONTH ?>, <?lsmb DAY ?> or variations of
  2313. # <?lsmb NAME 1 1 3 ?>, <?lsmb BUSINESS ?>, <?lsmb BUSINESS 10 ?>, <?lsmb CURR... ?>
  2314. # <?lsmb DESCRIPTION 1 1 3 ?>, <?lsmb ITEM 1 1 3 ?>, <?lsmb PARTSGROUP 1 1 3 ?> only for parts
  2315. # <?lsmb PHONE ?> for customer and vendors
  2316. my $num = $_;
  2317. ($num) = $num =~ /(\d+)/;
  2318. if ( defined $num ) {
  2319. my $incnum;
  2320. # if we have leading zeros check how long it is
  2321. if ( $num =~ /^0/ ) {
  2322. my $l = length $num;
  2323. $incnum = $num + 1;
  2324. $l -= length $incnum;
  2325. # pad it out with zeros
  2326. my $padzero = "0" x $l;
  2327. $incnum = ( "0" x $l ) . $incnum;
  2328. }
  2329. else {
  2330. $incnum = $num + 1;
  2331. }
  2332. s/$num/$incnum/;
  2333. }
  2334. my $dbvar = $_;
  2335. my $var = $_;
  2336. my $str;
  2337. my $param;
  2338. if (/<\?lsmb /) {
  2339. while (/<\?lsmb /) {
  2340. s/<\?lsmb .*? \?>//;
  2341. last unless $&;
  2342. $param = $&;
  2343. $str = "";
  2344. if ( $param =~ /<\?lsmb date \?>/i ) {
  2345. $str = (
  2346. $self->split_date(
  2347. $myconfig->{dateformat},
  2348. $self->{transdate}
  2349. )
  2350. )[0];
  2351. $var =~ s/$param/$str/;
  2352. }
  2353. if ( $param =~
  2354. /<\?lsmb (name|business|description|item|partsgroup|phone|custom)/i
  2355. )
  2356. {
  2357. #SC: XXX hairy, undoc, possibly broken
  2358. my $fld = lc $&;
  2359. $fld =~ s/<\?lsmb //;
  2360. if ( $fld =~ /name/ ) {
  2361. if ( $self->{type} ) {
  2362. $fld = $self->{vc};
  2363. }
  2364. }
  2365. my $p = $param;
  2366. $p =~ s/(<|>|%)//g;
  2367. my @p = split / /, $p;
  2368. my @n = split / /, uc $self->{$fld};
  2369. if ( $#p > 0 ) {
  2370. for ( my $i = 1 ; $i <= $#p ; $i++ ) {
  2371. $str .= substr( $n[ $i - 1 ], 0, $p[$i] );
  2372. }
  2373. }
  2374. else {
  2375. ($str) = split /--/, $self->{$fld};
  2376. }
  2377. $var =~ s/$param/$str/;
  2378. $var =~ s/\W//g if $fld eq 'phone';
  2379. }
  2380. if ( $param =~ /<\?lsmb (yy|mm|dd)/i ) {
  2381. # SC: XXX Does this even work anymore?
  2382. my $p = $param;
  2383. $p =~ s/(<|>|%)//g;
  2384. my $spc = $p;
  2385. $spc =~ s/\w//g;
  2386. $spc = substr( $spc, 0, 1 );
  2387. my %d = ( yy => 1, mm => 2, dd => 3 );
  2388. my @p = ();
  2389. my @a = $self->split_date( $myconfig->{dateformat},
  2390. $self->{transdate} );
  2391. for ( sort keys %d ) { push @p, $a[ $d{$_} ] if ( $p =~ /$_/ ) }
  2392. $str = join $spc, @p;
  2393. $var =~ s/$param/$str/;
  2394. }
  2395. if ( $param =~ /<\?lsmb curr/i ) {
  2396. $var =~ s/$param/$self->{currency}/;
  2397. }
  2398. }
  2399. }
  2400. my $query = qq|
  2401. UPDATE defaults
  2402. SET value = ?
  2403. WHERE setting_key = ?|;
  2404. my $sth = $dbh->prepare($query);
  2405. $sth->execute( $dbvar, $fld ) || $self->dberror($query);
  2406. $dbh->commit;
  2407. $var;
  2408. }
  2409. =item $form->db_prepare_vars(var1, var2, ..., varI<n>)
  2410. Undefines $form->{varI<m>}, 1 <= I<m> <= I<n>, iff $form-<{varI<m> is both
  2411. false and not "0".
  2412. =cut
  2413. sub db_prepare_vars {
  2414. my $self = shift;
  2415. for (@_) {
  2416. if ( !$self->{$_} and $self->{$_} ne "0" ) {
  2417. undef $self->{$_};
  2418. }
  2419. }
  2420. }
  2421. =item $form->split_date($dateformat[, $date]);
  2422. Returns ($rv, $yy, $mm, $dd) for the provided $date, or the current date if no
  2423. date is provided. $rv is a seperator-free merging of the fields $yy, $mm, and
  2424. $dd in the ordering supplied by $dateformat. If the supplied $date does not
  2425. contain non-digit characters, $rv is $date and the other return values are
  2426. undefined.
  2427. $yy is two digits.
  2428. =cut
  2429. sub split_date {
  2430. my ( $self, $dateformat, $date ) = @_;
  2431. my $mm;
  2432. my $dd;
  2433. my $yy;
  2434. my $rv;
  2435. if ( !$date ) {
  2436. my @d = localtime;
  2437. $dd = $d[3];
  2438. $mm = ++$d[4];
  2439. $yy = substr( $d[5], -2 );
  2440. $mm = substr( "0$mm", -2 );
  2441. $dd = substr( "0$dd", -2 );
  2442. }
  2443. if ( $dateformat =~ /^yy/ ) {
  2444. if ($date) {
  2445. if ( $date =~ /\D/ ) {
  2446. ( $yy, $mm, $dd ) = split /\D/, $date;
  2447. $mm *= 1;
  2448. $dd *= 1;
  2449. $mm = substr( "0$mm", -2 );
  2450. $dd = substr( "0$dd", -2 );
  2451. $yy = substr( $yy, -2 );
  2452. $rv = "$yy$mm$dd";
  2453. }
  2454. else {
  2455. $rv = $date;
  2456. }
  2457. }
  2458. else {
  2459. $rv = "$yy$mm$dd";
  2460. }
  2461. }
  2462. elsif ( $dateformat =~ /^mm/ ) {
  2463. if ($date) {
  2464. if ( $date =~ /\D/ ) {
  2465. ( $mm, $dd, $yy ) = split /\D/, $date;
  2466. $mm *= 1;
  2467. $dd *= 1;
  2468. $mm = substr( "0$mm", -2 );
  2469. $dd = substr( "0$dd", -2 );
  2470. $yy = substr( $yy, -2 );
  2471. $rv = "$mm$dd$yy";
  2472. }
  2473. else {
  2474. $rv = $date;
  2475. }
  2476. }
  2477. else {
  2478. $rv = "$mm$dd$yy";
  2479. }
  2480. }
  2481. elsif ( $dateformat =~ /^dd/ ) {
  2482. if ($date) {
  2483. if ( $date =~ /\D/ ) {
  2484. ( $dd, $mm, $yy ) = split /\D/, $date;
  2485. $mm *= 1;
  2486. $dd *= 1;
  2487. $mm = substr( "0$mm", -2 );
  2488. $dd = substr( "0$dd", -2 );
  2489. $yy = substr( $yy, -2 );
  2490. $rv = "$dd$mm$yy";
  2491. }
  2492. else {
  2493. $rv = $date;
  2494. }
  2495. }
  2496. else {
  2497. $rv = "$dd$mm$yy";
  2498. }
  2499. }
  2500. ( $rv, $yy, $mm, $dd );
  2501. }
  2502. =item $form->format_date($date);
  2503. Returns $date converted from 'yyyy-mm-dd' format to the format specified by
  2504. $form->{db_dateformat}. If the supplied date does not match /^\d{4}\D/,
  2505. return the supplied date.
  2506. This function takes a four digit year and returns the date with a four digit
  2507. year.
  2508. =cut
  2509. sub format_date {
  2510. # takes an iso date in, and converts it to the date for printing
  2511. my ( $self, $date ) = @_;
  2512. my $datestring;
  2513. if ( $date =~ /^\d{4}\D/ ) { # is an ISO date
  2514. $datestring = $self->{db_dateformat};
  2515. my ( $yyyy, $mm, $dd ) = split( /\W/, $date );
  2516. $datestring =~ s/y+/$yyyy/;
  2517. $datestring =~ s/mm/$mm/;
  2518. $datestring =~ s/dd/$dd/;
  2519. }
  2520. else { # return date
  2521. $datestring = $date;
  2522. }
  2523. $datestring;
  2524. }
  2525. =item $form->from_to($yyyy, $mm[, $interval]);
  2526. Returns the date $yyyy-$mm-01 and the the last day of the month interval - 1
  2527. months from then in the form ($form->format_date(fromdate),
  2528. $form->format_date(later)). If $interval is false but defined, the later date
  2529. is the current date.
  2530. This function dies horribly when $mm + $interval > 24
  2531. =cut
  2532. sub from_to {
  2533. my ( $self, $yyyy, $mm, $interval ) = @_;
  2534. my @t;
  2535. my $dd = 1;
  2536. my $fromdate = "$yyyy-${mm}-01";
  2537. my $bd = 1;
  2538. if ( defined $interval ) {
  2539. if ( $interval == 12 ) {
  2540. $yyyy++;
  2541. }
  2542. else {
  2543. if ( ( $mm += $interval ) > 12 ) {
  2544. $mm -= 12;
  2545. $yyyy++;
  2546. }
  2547. if ( $interval == 0 ) {
  2548. @t = localtime(time);
  2549. $dd = $t[3];
  2550. $mm = $t[4] + 1;
  2551. $yyyy = $t[5] + 1900;
  2552. $bd = 0;
  2553. }
  2554. }
  2555. }
  2556. else {
  2557. if ( ++$mm > 12 ) {
  2558. $mm -= 12;
  2559. $yyyy++;
  2560. }
  2561. }
  2562. $mm--;
  2563. @t = localtime( Time::Local::timelocal( 0, 0, 0, $dd, $mm, $yyyy ) - $bd );
  2564. $t[4]++;
  2565. $t[4] = substr( "0$t[4]", -2 );
  2566. $t[3] = substr( "0$t[3]", -2 );
  2567. $t[5] += 1900;
  2568. return ( $self->format_date($fromdate), $self->format_date("$t[5]-$t[4]-$t[3]") );
  2569. }
  2570. =item $form->audittrail($dbh, $myconfig, $audittrail);
  2571. $audittrail is a hashref. If $audittrail->{id} is false, this function
  2572. retrieves the current time from the database and return a string of the form
  2573. "tablename|reference|formname|action|timestamp|" where all the values save
  2574. timestamp are taken directly from the $audittrail hashref.
  2575. If $audittrail->{id} is true but the value of audittrail in the defaults table
  2576. is '0', do nothing and return.
  2577. If $form->{audittrail} is true and $myconfig is false, $form->{audittrail} is
  2578. treated as a pipe seperated list (trailing pipe required) of the form:
  2579. table1|ref1|form1|action1|date1|...|tablen|refn|formn|actionn|daten|
  2580. All the entries described by $form->{audittrail} are inserted into the audit
  2581. table, taking on a transaction id of $audittrail->{id} and the employee id of
  2582. the calling user.
  2583. Irrespective of $form->{audittrail} and $myconfig status, this function will add
  2584. a record to the audittrail using the values contained within $audittrail,
  2585. substituting the current date if $audittrail->{transdate} is not set and the
  2586. employee id of the calling user.
  2587. =cut
  2588. sub audittrail {
  2589. my ( $self, $dbh, $myconfig, $audittrail ) = @_;
  2590. # table, $reference, $formname, $action, $id, $transdate) = @_;
  2591. my $query;
  2592. my $rv;
  2593. my $disconnect;
  2594. if ( !$dbh ) {
  2595. $dbh = $self->{dbh};
  2596. }
  2597. my $sth;
  2598. my $query;
  2599. # if we have an id add audittrail, otherwise get a new timestamp
  2600. my @queryargs;
  2601. if ( $audittrail->{id} ) {
  2602. $query = qq|
  2603. SELECT value FROM defaults
  2604. WHERE setting_key = 'audittrail'|;
  2605. if ( $dbh->selectrow_array($query) ) {
  2606. my ( $null, $employee_id ) = $self->get_employee($dbh);
  2607. if ( $self->{audittrail} && !$myconfig ) {
  2608. chop $self->{audittrail};
  2609. my @a = split /\|/, $self->{audittrail};
  2610. my %newtrail = ();
  2611. my $key;
  2612. my $i;
  2613. my @flds = qw(tablename reference formname action transdate);
  2614. # put into hash and remove dups
  2615. while (@a) {
  2616. $key = "$a[2]$a[3]";
  2617. $i = 0;
  2618. $newtrail{$key} = { map { $_ => $a[ $i++ ] } @flds };
  2619. splice @a, 0, 5;
  2620. }
  2621. $query = qq|
  2622. INSERT INTO audittrail
  2623. (trans_id, tablename, reference,
  2624. formname, action, transdate,
  2625. employee_id)
  2626. VALUES (?, ?, ?, ?, ?, ?, ?)|;
  2627. my $sth = $dbh->prepare($query) || $self->dberror($query);
  2628. foreach $key (
  2629. sort {
  2630. $newtrail{$a}{transdate} cmp $newtrail{$b}{transdate}
  2631. } keys %newtrail
  2632. )
  2633. {
  2634. $i = 2;
  2635. $sth->bind_param( 1, $audittrail->{id} );
  2636. for (@flds) {
  2637. $sth->bind_param( $i++, $newtrail{$key}{$_} );
  2638. }
  2639. $sth->bind_param( $i++, $employee_id );
  2640. $sth->execute() || $self->dberror($query);
  2641. $sth->finish;
  2642. }
  2643. }
  2644. if ( $audittrail->{transdate} ) {
  2645. $query = qq|
  2646. INSERT INTO audittrail (
  2647. trans_id, tablename, reference,
  2648. formname, action, employee_id,
  2649. transdate)
  2650. VALUES (?, ?, ?, ?, ?, ?, ?)|;
  2651. @queryargs = (
  2652. $audittrail->{id}, $audittrail->{tablename},
  2653. $audittrail->{reference}, $audittrail->{formname},
  2654. $audittrail->{action}, $employee_id,
  2655. $audittrail->{transdate}
  2656. );
  2657. }
  2658. else {
  2659. $query = qq|
  2660. INSERT INTO audittrail
  2661. (trans_id, tablename, reference,
  2662. formname, action, employee_id)
  2663. VALUES (?, ?, ?, ?, ?, ?)|;
  2664. @queryargs = (
  2665. $audittrail->{id}, $audittrail->{tablename},
  2666. $audittrail->{reference}, $audittrail->{formname},
  2667. $audittrail->{action}, $employee_id,
  2668. );
  2669. }
  2670. $sth = $dbh->prepare($query);
  2671. $sth->execute(@queryargs) || $self->dberror($query);
  2672. }
  2673. }
  2674. else {
  2675. $query = qq|SELECT current_timestamp|;
  2676. my ($timestamp) = $dbh->selectrow_array($query);
  2677. $rv =
  2678. "$audittrail->{tablename}|$audittrail->{reference}|$audittrail->{formname}|$audittrail->{action}|$timestamp|";
  2679. }
  2680. $rv;
  2681. }
  2682. 1;
  2683. =back