Statements.pm 11 KB
Newer Older
1
# Statement functions
2
# Copyright (C) 2009-2020, AllWorldIT
Nigel Kukard's avatar
Nigel Kukard committed
3
# Copyright (C) 2008, LinuxRulz
Nigel Kukard's avatar
Nigel Kukard committed
4
# Copyright (C) 2007 Nigel Kukard  <nkukard@lbsd.net>
5
#
6
7
8
9
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
10
#
11
12
13
14
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
15
#
16
17
18
19
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

20
21
22



23
package wiaflos::server::core::Statements;
24
25

use strict;
Nigel Kukard's avatar
Nigel Kukard committed
26
27
use warnings;

28
29

use wiaflos::version;
30
use wiaflos::constants;
31
32
33
34
35
use wiaflos::server::core::config;
use awitpt::db::dblayer;
use wiaflos::server::core::templating;
use wiaflos::server::core::Clients;
use wiaflos::server::core::GL;
36
37
38
39
40
41
42
43
44
45
46
47
48
49

use Math::BigFloat;




# Our current error message
my $error = "";

# Set current error message
# Args: error_message
sub setError
{
	my $err = shift;
50
51
	my ($package,$filename,$line) = caller;
	my (undef,undef,undef,$subroutine) = caller(1);
52
53

	# Set error
54
	$error = "$subroutine($line): $err";
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
}

# Return current error message
# Args: none
sub Error
{
	my $err = $error;

	# Reset error
	$error = "";

	# Return error
	return $err;
}


Nigel Kukard's avatar
Nigel Kukard committed
71
## @fn getStatement
72
# Function to return a client account statement
Nigel Kukard's avatar
Nigel Kukard committed
73
74
75
76
77
78
79
80
81
82
83
84
85
#
# @param detail Parameter hash ref
# @li ClientCode Client code
# @li StartDate Optional statement start date
# @li EndDate Optional statement end date
#
# @returns Hash ref of account transactions
# @li ID Entry ID
# @li GLTransactionID GL account transaction ID
# @li Reference Entry reference
# @li Amount Amount
# @li TransactionDate Transaction date
# @li TransactionReference Transaction reference
86
87
88
89
sub getStatement
{
	my ($detail) = @_;
	my @entries = ();
90
91


92
	# Verify client
93
94
95
	if (!defined($detail->{'ClientCode'}) || $detail->{'ClientCode'} eq "") {
		setError("No client code provided");
		return ERR_PARAM;
96
97
98
99
	}

	# Check if client exists & pull
	my $tmp;
100
	$tmp->{'Code'} = $detail->{'ClientCode'};
101
	my $client = wiaflos::server::core::Clients::getClient($tmp);
102
	if (ref $client ne "HASH") {
103
		setError(wiaflos::server::core::Clients::Error());
104
105
106
107
		return $client;
	}

	$tmp = undef;
108
	$tmp->{'AccountID'} = $client->{'GLAccountID'};
109
	$tmp->{'StartDate'} = $detail->{'StartDate'};
Nigel Kukard's avatar
Nigel Kukard committed
110
111
	$tmp->{'EndDate'} = $detail->{'EndDate'};
	$tmp->{'BalanceBroughtForward'} = 1;
112
	my $res = wiaflos::server::core::GL::getGLAccountEntries($tmp);
113

114
115
	# Fetch items
	foreach my $item (@{$res}) {
Nigel Kukard's avatar
Nigel Kukard committed
116
		my $entry;
117

Nigel Kukard's avatar
Nigel Kukard committed
118
119
120
121
		$entry->{'ID'} = $item->{'ID'};
		$entry->{'GLTransactionID'} = $item->{'GLTransactionID'};
		$entry->{'Reference'} = $item->{'Reference'};
		$entry->{'Amount'} = $item->{'Amount'};
122

Nigel Kukard's avatar
Nigel Kukard committed
123
		$entry->{'TransactionDate'} = $item->{'TransactionDate'};
124
		$entry->{'TransactionReference'} = $item->{'TransactionReference'};
125

Nigel Kukard's avatar
Nigel Kukard committed
126
		push(@entries,$entry);
127
128
	}

Nigel Kukard's avatar
Nigel Kukard committed
129
130
131
	# Sort results
	@entries = sort { $a->{'TransactionDate'} cmp $b->{'TransactionDate'} } @entries;

132
133
134
135
136
137
138
	return \@entries;
}



# Send statement
# Parameters:
139
#		ClientCode	- Client code
140
#		StartDate	- Start date
141
#		EndDate		- End date
142
#		SendTo		- Send to,  email:  ,  file:  , return
143
144
# Optional:
#		Subject		- Subject of the statement
145
146
147
148
149
150
151
152
sub sendStatement
{
	my ($detail) = @_;


	# Verify SendTo
	if (!defined($detail->{'SendTo'}) || $detail->{'SendTo'} eq "") {
		setError("SendTo was not provided");
153
		return ERR_PARAM;
154
155
156
157
	}

	# Grab items
	my $data;
158
	$data->{'ClientCode'} = $detail->{'ClientCode'};
159
	$data->{'StartDate'} = $detail->{'StartDate'};
160
	$data->{'EndDate'} = $detail->{'EndDate'};
161
162
	my $entries = getStatement($data);
	if (ref $entries ne "ARRAY") {
163
		setError(Error());
164
165
166
		return $entries;
	}

167
	# Pull in config
168
	my $config = wiaflos::server::core::config::getConfig();
169

170
171
	# Pull client
	$data = undef;
172
	$data->{'Code'} = $detail->{'ClientCode'};
173
	my $client = wiaflos::server::core::Clients::getClient($data);
174
	if (ref $client ne "HASH") {
175
		setError(wiaflos::server::core::Clients::Error());
176
177
		return $client;
	}
178

179
	# Check if subject was overridden
180
	my $subject = (defined($detail->{'Subject'}) && $detail->{'Subject'} ne "") ? $detail->{'Subject'} : "Statement: ".$client->{'Code'};
181

182
	# Setup request data
183
	$data = undef;
184
185
186
	$data->{'ID'} = $client->{'ID'};

	# Pull in addresses
187
	my $addresses = wiaflos::server::core::Clients::getClientAddresses($data);
188
	if (ref $addresses ne "ARRAY") {
189
		setError(wiaflos::server::core::Clients::Error());
190
191
192
		return $addresses;
	}
	# Pull in email addresses
193
	my $emailAddies = wiaflos::server::core::Clients::getClientEmailAddresses($data);
194
	if (ref $emailAddies ne "ARRAY") {
195
		setError(wiaflos::server::core::Clients::Error());
196
197
198
199
200
		return $emailAddies;
	}

	# Parse in addresses
	my $billAddr;
201
	my $shipAddr = "";
202
203
204
205
206
207
208
209
210
211
212
	# Look for billing address
	foreach my $address (@{$addresses}) {
		if ($address->{'Type'} eq "billing") {
			$billAddr = $address->{'Address'};
		} elsif ($address->{'Type'} eq "shipping") {
			$shipAddr = $address->{'Address'};
		}
	}
	# If no billing address, use shipping address
	$billAddr = defined($billAddr) ? $billAddr : $shipAddr;
	$billAddr =~ s/\|/<br \/>/g;
213
	# Setup shipping address
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
	$shipAddr =~ s/\|/<br \/>/g;

	# Parse in email addresses
	my $billEmailAddr;
	my @billEmailAddrs;
	my @genEmailAddrs;
	# Look for accounts address
	foreach my $address (@{$emailAddies}) {
		if ($address->{'Type'} eq "accounts") {
			push(@billEmailAddrs,$address->{'Address'});
		} elsif ($address->{'Type'} eq "general") {
			push(@genEmailAddrs,$address->{'Address'});
		}
	}
	# If no accounts address, use general address
	$billEmailAddr = @billEmailAddrs > 0 ? join(',',@billEmailAddrs) : join(',',@genEmailAddrs);

231
232
233
	# Build array of stuff we can use
	my $vars = {
		'WiaflosString' => $GENSTRING,
234

235
236
237
238
239
240
241
242
243
244
245
		# Client
		'ClientName' => $client->{'Name'},
		'ClientCode' => $client->{'Code'},
		'ClientBillingAddress' => $billAddr,
		'ClientShippingAddress' => $shipAddr,

		# Statement
		'StatementDate' => DateTime->from_epoch( epoch => time() )->ymd(),
		# FIXME - not implemented yet
		'StatementNote' => "",
	};
246

247
	# Tally up a running balance
248
	my $runningBalance = Math::BigFloat->new(0);
249
	$runningBalance->precision(-2);
250

251
252
253
254
255
256
	# Load invoice line items
	foreach my $item (@{$entries}) {
		my $titem;

		# Keep balance
		$runningBalance->badd($item->{'Amount'});
257

258
		# Fix some stuff up
259
		$titem->{'TransactionDescription'} = defined($item->{'Reference'}) ? $item->{'Reference'} : $item->{'TransactionReference'};
260
261
262
263
264
265
266
		$titem->{'TransactionDate'} = $item->{'TransactionDate'};
		$titem->{'TransactionAmount'} = sprintf('%.2f',$item->{'Amount'});
		$titem->{'StatementBalance'} = sprintf('%.2f',$runningBalance->bstr());

		# Various fixups & amount sanitizations
		push(@{$vars->{'LineItems'}},$titem);
	}
267

268
	# Get statement balance
269
	my $statementBalance = Math::BigFloat->new(0);
270
271
272
273
	$statementBalance->precision(-2);
	foreach my $item (@{$entries}) {
		$statementBalance->badd($item->{'Amount'});
	}
274
	$vars->{'StatementBalance'} = $statementBalance->bstr();
275

276
277
	# Get statement template file
	my $template = defined($config->{'statements'}{'email_template'}) ? $config->{'statements'}{'email_template'} : "statements/statement1.tt2";
278
279
280
281
282

	# Check where statement must go
	if ($detail->{'SendTo'} =~ /^file:(\S+)/i) {
		my $filename = $1;

283
284
285
		# Load template
		my $res = loadTemplate($template,$vars,$filename);
		if (!$res) {
286
			setError("Failed to load template '$template': ".wiaflos::server::core::templating::Error());
287
			return ERR_SRVTEMPLATE;
288
289
290
291
		}

	# Write out using email
	} elsif ($detail->{'SendTo'} =~ /^email(?:\:(\S+))?/i) {
292
293
		# Pull email address user specified if its defined and not blank, or use billing email address
		my $emailAddy = (defined($1) && $1 ne "") ? $1 : $billEmailAddr;
294
295
296
297

		# Check if we have a email addy
		if ($emailAddy eq "") {
			setError("No email address defined to send statement to");
298
			return ERR_PARAM;
299
300
		}

Nigel Kukard's avatar
Nigel Kukard committed
301
		# Statement filename
302
		(my $statementFilename = "statement.html") =~ s,/,-,g;
Nigel Kukard's avatar
Nigel Kukard committed
303
		# Statement signature filename
304
305
306
307
		my $stmtSignFilename = $statementFilename . ".asc";


		# If we must, pull in email body
308
		my $message_template = $config->{'statements'}{'email_message_template'};
309
		my $emailBody = "";
310
		if (defined($message_template) && $message_template ne "") {
311
312
313
314
315
316
			# Variables for our template
			my $vars2 = {
				'StatementFilename' => $statementFilename,
				'StatementSignatureFilename' => $stmtSignFilename,
			};
			# Load template
Nigel Kukard's avatar
Nigel Kukard committed
317
318
			my $res = loadTemplate($message_template,$vars2,\$emailBody);
			if (!$res) {
319
				setError("Failed to load template '$message_template': ".wiaflos::server::core::templating::Error());
320
				return ERR_SRVTEMPLATE;
321
322
			}

323
			$emailBody =~ s/(?<!\r)\n/\r\n/sg; # Sanitize eol for crypt-gpg
324
325
326
		}

		# This is our entire statement
327
		my $statementData = "";
Nigel Kukard's avatar
Nigel Kukard committed
328
329
		my $res = loadTemplate($template,$vars,\$statementData);
		if (!$res) {
330
			setError("Failed to load template '$template': ".wiaflos::server::core::templating::Error());
331
			return ERR_SRVTEMPLATE;
332
		}
333
334
335
		$statementData =~ s/(?<!\r)\n/\r\n/sg; # Sanitize eol, needed to fix bug in crypt-gpg where it mangles \n

		# See if we must use GPG
336
		my $use_gpg_key = $config->{'statements'}{'use_gpg_key'};
337
		my $sign;
338
		if (defined($use_gpg_key) && $use_gpg_key ne "") {
339
340
341
			# Setup GnuPG
			my $gpg = new Crypt::GPG;
			$gpg->gpgbin('/usr/bin/gpg');
342
			$gpg->secretkey($use_gpg_key);
343
344
345
346
			$gpg->armor(1);
			# Sign statement
			$sign = $gpg->sign($statementData);
			if (!defined($sign)) {
Nigel Kukard's avatar
Nigel Kukard committed
347
				setError("Failed to sign statement");
348
				return ERR_SRVEXEC;
349
350
351
352
353
			}
		}

		# Create message
		my $msg = MIME::Lite->new(
354
				From	=> $config->{'statements'}{'email_from'},
355
				To		=> $emailAddy,
356
				Bcc		=> $config->{'statements'}{'email_bcc'},
357
				'Reply-To'		=> $config->{'statements'}{'email_reply_to'},
358
				Subject	=> $subject,
359
360
361
362
363
364
				Type	=> 'multipart/mixed'
		);

		# Attach body
		$msg->attach(
				Type	=> 'TEXT',
365
				Encoding 	=> '8bit',
366
367
368
369
370
371
				Data	=> $emailBody,
		);

		# Attach statement
		$msg->attach(
				Type	=> 'text/html',
372
				Encoding	=> 'base64',
373
374
375
376
377
378
379
380
381
382
				Data	=> $statementData,
				Disposition	=> 'attachment',
				Filename	=> $statementFilename
		);

		# If we have signature, sign statement
		if (defined($sign)) {
			# Attach signature
			$msg->attach(
					Type	=> 'text/plain',
383
					Encoding	=> 'base64',
384
385
386
387
388
389
390
					Data	=> $sign,
					Disposition	=> 'attachment',
					Filename	=> $stmtSignFilename
			);
		}

		# Send email
Nigel Kukard's avatar
Nigel Kukard committed
391
392
393
394
395
396
397
398
399
400
401
402
403
		my $server = $config->{'mail'}{'server'};
		if (defined($server) && $server ne "") {
			# Send email via SMTP
			if (!(my $res = $msg->send("smtp",$server))) {
				setError("Failed to send statement via email server '$server'");
				return ERR_SRVEXEC;
			}
		} else {
			# Send email via Sendmail
			if (!(my $res = $msg->send("sendmail"))) {
				setError("Failed to send statement via sendmail");
				return ERR_SRVEXEC;
			}
404
		}
405

406
407
	} else {
		setError("Invalid SendTo method provided");
408
		return ERR_PARAM;
409
410
411
	}


412
	return RES_OK;
413
414
415
416
417
418
419
420
}





1;
# vim: ts=4