1 #ident "Kalopa: $Id: postinvoice.c,v 1.33 2008/10/31 15:17:13 dtynan Exp $"
2 
3 /*
4  * $Id: postinvoice.c,v 1.33 2008/10/31 15:17:13 dtynan Exp $
5  *
6  * Copyright (c) 2003, Kalopa Media Limited.  All rights reserved.
7  * Copyright (c) 2005, Kalopa Research Limited.  All rights reserved.
8  * This is free software; you can redistribute it and/or modify it
9  * under the terms of the GNU General Public License as published
10  * by the Free Software Foundation; either version 2, or (at your
11  * option) any later version.
12  *
13  * It is distributed in the hope that it will be useful, but WITHOUT
14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
16  * License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this product; see the file COPYING.  If not, write to
20  * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139,
21  * USA.
22  *
23  * THIS SOFTWARE IS PROVIDED BY KALOPA RESEARCH LIMITED "AS IS" AND
24  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
25  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
26  * PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL KALOPA
27  * RESEARCH LIMITED BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
28  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
29  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
30  * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31  * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
32  * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
34  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35  *
36  * ABSTRACT
37  *
38  * $Log: postinvoice.c,v $
39  * Revision 1.33  2008/10/31 15:17:13  dtynan
40  * Extensive changes for version 0.8.2.
41  *
42  * Revision 1.32  2008/10/20 17:53:25  dtynan
43  * Added a document for installation (quite basic at this point), removed
44  * some debug output from tpadjust(), and fixed a lingering issue with the
45  * way invoice due-dates were calculated.
46  *
47  * Revision 1.31  2008/10/15 15:11:14  dtynan
48  * Mammoth change to convert from old-style database configuration to new
49  * schema where an RC file defines the database access.
50  *
51  * Revision 1.30  2008/10/13 20:59:38  dtynan
52  * Extensive changes (part 1) for change to database configuration.
53  *
54  * Revision 1.29  2008/07/18 11:07:38  dtynan
55  * Fixed issue with isholiday/skipholiday where it wouldn't actually detect
56  * bank holidays (only weekends).  It now interrogates the database.
57  *
58  * Revision 1.28  2008/07/08 17:10:18  dtynan
59  * Extensive changes.
60  *
61  * Revision 1.27  2008/01/23 16:29:21  dtynan
62  * Added the ability to run the apps without effect (-n) and fixed some
63  * effective-date issues with invoice postings.
64  *
65  * Revision 1.26  2008/01/15 18:10:44  dtynan
66  * Changed to use new company name and copyright, also fixed a few old
67  * files with the wrong license.
68  *
69  * Revision 1.25  2007/08/07 07:48:13  dtynan
70  * When posting a credit note, don't add the invoice description to the
71  * Sales Ledger, add "Credit Note" instead.
72  *
73  * Revision 1.24  2006/10/01 10:32:33  dtynan
74  * Major changes for VAT.
75  *
76  * Revision 1.23  2006/07/31 10:17:52  dtynan
77  * Massive changeover to a more Ruby/Rails primary key naming convention.
78  *
79  * Revision 1.22  2005/12/19 22:17:43  dtynan
80  * Added rudimentary support for VAT - still need to sort out some
81  * multicurrency issues.
82  *
83  * Revision 1.21  2005/10/17 15:41:47  dtynan
84  * Tweaked the roundoff routine.
85  *
86  * Revision 1.20  2005/10/17 15:12:42  dtynan
87  * Use the new roundoff function for accurate discount calculations.
88  *
89  * Revision 1.19  2005/10/17 13:35:27  dtynan
90  * First pass at a multicurrency invoicing system.
91  *
92  * Revision 1.18  2005/09/21 18:10:28  dtynan
93  * Extensive changes prior to first public release, including an
94  * import/export mechanism.
95  *
96  * Revision 1.17  2005/08/12 18:57:50  dtynan
97  * Changed to use a slightly different format - now an invoice is
98  * sent with a statement the first time but only a statement is
99  * sent as a reminder.
100  *
101  * Revision 1.16  2005/04/11 13:52:04  dtynan
102  * Added code to post invoice data to sales ledger.
103  *
104  * Revision 1.15  2005/03/22 09:41:25  dtynan
105  * Extensive upgrade to make sure the copyright block was
106  * consistent and GPL'ed.
107  *
108  * Revision 1.14  2005/03/09 11:22:13  dtynan
109  * Removed custcode from the customer table.  Now replaced with ccode
110  * from the contact table.
111  *
112  * Revision 1.13  2005/02/17 17:25:28  dtynan
113  * Fixed some compiler warnings.
114  *
115  * Revision 1.12  2005/02/17 16:23:22  dtynan
116  * Extensive changes to support new customer/contact database
117  * format and to add user IDs to journal entries.
118  *
119  * Revision 1.11  2005/01/13 18:19:22  dtynan
120  * Added code to correctly handle prepayments.
121  *
122  * Revision 1.10  2005/01/13 17:50:31  dtynan
123  * Debit the customer record by the invoice amount.
124  *
125  * Revision 1.9  2004/12/09 20:33:57  dtynan
126  * Fixed bug in include files.
127  *
128  * Revision 1.8  2004/12/09 20:29:16  dtynan
129  * Use new mechanism for determining organisation name.
130  *
131  * Revision 1.7  2004/12/08 14:30:37  dtynan
132  * Fixed a bug.
133  *
134  * Revision 1.6  2004/12/08 14:29:42  dtynan
135  * Now adds the invoice # to the journal folio and calculates the invoice
136  * total.
137  *
138  * Revision 1.5  2004/09/07 12:00:24  dtynan
139  * Fixed issue where geninvoice was being called without specifying
140  * the company number (which defaults to 1).
141  *
142  * Revision 1.4  2004/08/26 17:24:10  dtynan
143  * Minor changes and tweaks - nothing serious.
144  *
145  * Revision 1.3  2004/08/05 12:51:53  dtynan
146  * Added code to post an invoice to the journal.
147  *
148  * Revision 1.2  2004/08/04 13:15:24  dtynan
149  * Make sure the company number is handled properly.
150  *
151  * Revision 1.1  2004/07/16 19:23:33  dtynan
152  * Extensive changes to split the invoicing into posting and generating.
153  * Also fixed some bugs in the site billing code, added a default nominal
154  * account (5060) and fixed some bugs in the invoice emailing code.  Added
155  * makepath() calls to generate the correct paths.
156  */
157 #include <stdio.h>
158 #include <stdlib.h>
159 #include <unistd.h>
160 #include <string.h>
161 #include <time.h>
162 
163 #include "beanie.h"
164 #include "hacks.h"
165 
166 struct	jitem	{
167 	int	acctno;
168 	double	amount;
169 } jitem[20];
170 
171 int		noeffect;
172 int		verbose;
173 struct db_company *comp;
174 struct	vatamt	*vat;
175 
176 extern	int	optind, opterr;
177 extern	char	*optarg;
178 
179 void	postinvoice(struct db_invoice *);
180 void	usage();
181 
182 /*
183  *
184  */
185 int
main(int argc,char * argv[])186 main(int argc, char *argv[])
187 {
188 	int i;
189 	char *cname;
190 	dbow_conn *dbp;
191 	struct db_invoice *invp;
192 
193 	verbose = noeffect = 0;
194 	cname = NULL;
195 	while ((i = getopt(argc, argv, "c:nv")) != EOF) {
196 		switch (i) {
197 		case 'c':
198 			cname = optarg;
199 			break;
200 
201 		case 'n':
202 			noeffect = 1;
203 			break;
204 
205 		case 'v':
206 			verbose = 1;
207 			break;
208 
209 		default:
210 			usage();
211 			break;
212 		}
213 	}
214 	/*
215 	 * Find the company that we're processing...
216 	 */
217 	loadconfig(cname);
218 	dbp = getdbase();
219 	if ((comp = db_findcompanyfirst(dbp)) == NULL)
220 		berror("postinvoice", "cannot find company");
221 	vat = vat_init(dbp);
222 	/*
223 	 * Post all the invoices that aren't complete.
224 	 */
225 	invp = db_findinvoicebystate(dbp, INVOICE_STATE_NEW);
226 	while (invp != NULL) {
227 		postinvoice(invp);
228 		invp = db_findinvoicenext(dbp, invp);
229 	}
230 	exit(0);
231 }
232 
233 /*
234  *
235  */
236 void
postinvoice(struct db_invoice * invp)237 postinvoice(struct db_invoice *invp)
238 {
239 	int i, njitem, curr;
240 	struct jitem *xp;
241 	struct db_journal *jp;
242 	struct db_journal_item *jip;
243 	struct db_invoice *pip;
244 	struct db_invoice_item *iip;
245 	struct db_customer *cusp;
246 	struct db_contact *cp;
247 	struct db_sledger *slp;
248 	char descr[256], invid[64];
249 	double hval, aval, ttl;
250 	dbow_conn *dbp = getdbase();
251 
252 	if (verbose)
253 		printf("Posting invoice/crnote #%d to journal...\n", invp->id);
254 	vat_clear(vat);
255 	/*
256 	 * Find the name of the customer and get their default currency.
257 	 */
258 	if (invp->customer_id == 0) {
259 		/*
260 		 * Cash sale.
261 		 */
262 		cusp = NULL;
263 	} else if ((cusp = db_findcustomerbyid(dbp, invp->customer_id)) == NULL)
264 		berror("postinvoice", "cannot find customer record");
265 	if (cusp != NULL && cusp->currency_id > 0)
266 		curr = cusp->currency_id;
267 	else
268 		curr = HOME_CURRENCY;
269 	if (invp->currency_id == 0)
270 		invp->currency_id = curr;
271 	/*
272 	 * Update the invoice fields, including the 'due date' and the
273 	 * invoice state (via the dunning system).
274 	 */
275 	if (invp->idate == 0)
276 		time((time_t *)&invp->idate);
277 	dunning(invp);
278 	invp->vatttl = 0.0;
279 	invp->shipping = 0.0;
280 	if (verbose)
281 		printf("Due date: %s", ctime((time_t *)&invp->ddate));
282 	/*
283 	 * Generate the journal entry title
284 	 */
285 	if (cusp != NULL) {
286 		cp = contact_findbyid(dbp, cusp->contact_id);
287 		sprintf(descr, "%s for %s", invp->cninv_id != 0 ?
288 				"Credit Note" : "Invoice", orgname(cp));
289 	} else {
290 		sprintf(descr, "Cash %s", invp->cninv_id != 0 ?
291 				"Credit Note" : "Invoice");
292 	}
293 	sprintf(invid, "%d", invp->id);
294 	if (verbose)
295 		printf("Posting title: [%s] (%s)\n", descr, invid);
296 	/*
297 	 * Allocate (and stuff) a journal entry.
298 	 */
299 	if ((jp = db_journalalloc()) == NULL)
300 		berror("postinvoice", "cannot create journal entry");
301 	jp->jdate = invp->idate;
302 	jp->folio = strdup(invid);
303 	jp->descr = strdup(descr);
304 	jp->user_id = getuid();
305 	jp->state = 0;
306 	if (!noeffect && db_insertjournal(dbp, jp) < 0)
307 		berror("postinvoice", "cannot create journal entry");
308 	/*
309 	 * Find out how many different nominal accounts are needed.
310 	 * The first is for accounts-receivable which is debited.
311 	 * 'aval' is the actual value of the invoice item in the
312 	 * customer currency, and 'hval' is the home value of the
313 	 * invoice in our local currency.  Note that the customer
314 	 * will be invoiced in their currency, but the posting will
315 	 * be in our currency.  When payment is made (in their
316 	 * currency), we use N/A "9400; currency gains and losses"
317 	 * to account for any discrepancy.
318 	 */
319 	njitem = 1;
320 	xp = jitem;
321 	xp->acctno = (cusp == NULL) ? NA_CASHSALES : NA_DEBTORSCA;
322 	xp->amount = ttl = 0.0;
323 #ifdef VAT_CALCULATE
324 	xp++;
325 	njitem++;
326 	xp->acctno = NA_VATACCOUNT;
327 	xp->amount = 0.0;
328 #endif
329 	iip = db_findinvoice_itembyinvoice_id(dbp, invp->id);
330 	while (iip != NULL) {
331 		invoice_total(invp, iip, &aval, &hval);
332 		ttl += aval;
333 		for (i = 0, xp = jitem; i < njitem; i++, xp++)
334 			if (xp->acctno == iip->nomacct_id)
335 				break;
336 		if (i == njitem && ++njitem < 20) {
337 			jitem[i].acctno = iip->nomacct_id;
338 			jitem[i].amount = 0.0;
339 		}
340 		jitem[i].amount += hval;
341 		jitem[0].amount += hval;
342 		vat_lineitem(vat, hval, iip->vcode, jp->id, VATDATA_SALES);
343 		iip = db_findinvoice_itemnext(dbp, iip);
344 	}
345 	/*
346 	 * Set the invoice total.  This is done here because we can
347 	 * add multiple invoice line items to an invoice over the
348 	 * course of a business day so we don't need to keep
349 	 * recomputing the invoice total until we post the invoice.
350 	 * Also remember what the home value of the invoice is, for
351 	 * computing currency gains and losses later on.
352 	 *
353 	 * HACK: We need to add a journal line item for shipping
354 	 * charges.  They get totalled in the first line item but
355 	 * don't appear anywhere else.
356 	 */
357 #ifdef VAT_CALCULATE
358 	invp->vatttl = vat_total(vat);
359 	jitem[1].amount = currconv(invp->vatttl, invp->currency_id);
360 	jitem[0].amount += jitem[1].amount;
361 #endif
362 	jitem[0].amount += currconv(invp->shipping, invp->currency_id);
363 	invp->total = ttl + invp->vatttl + invp->shipping;
364 	invp->htotal = jitem[0].amount;
365 	/*
366 	 * Now allocate (and stuff) each journal item entry.
367 	 */
368 	jip = db_journal_itemalloc();
369 	jip->journal_id = jp->id;
370 	db_journalfree(jp);
371 	for (i = 0, xp = jitem; i < njitem; i++, xp++) {
372 		if (verbose)
373 			printf("%d - %.2f\n", xp->acctno, xp->amount);
374 		jip->item_id = i + 1;
375 		jip->nomacct_id = xp->acctno;
376 		if (xp->amount < 0.0) {
377 			jip->drcrf = (i == 0) ? 1 : 0;
378 			jip->amount = -xp->amount;
379 		} else {
380 			jip->drcrf = (i == 0) ? 0 : 1;
381 			jip->amount = xp->amount;
382 		}
383 		if (!noeffect && db_insertjournal_item(dbp, jip) < 0)
384 			berror("postinvoice", "cannot create journal item");
385 	}
386 	db_journal_itemfree(jip);
387 	/*
388 	 * Update the customer balance.  If they are still in credit,
389 	 * then show the prepayment amount as going away.
390 	 */
391 	if (cusp!=NULL && invp->total>=0.0 && cusp->balance>=invp->total) {
392 		/*
393 		 * Allocate a journal entry for recognising the prepayment.
394 		 */
395 		sprintf(descr, "Prepayment on invoice #%d", invp->id);
396 		if (!noeffect) {
397 			journal_entry(&invp->idate, NULL, descr, 0,
398 					NA_PREPAYMENTS, NA_DEBTORSCA,
399 					invp->htotal);
400 		}
401 		invp->state = INVOICE_STATE_PAID;
402 	}
403 	if (cusp != NULL) {
404 		cusp->balance -= invp->total;
405 		if (verbose)
406 			printf("New customer balance: %.2f\n", cusp->balance);
407 	}
408 	/*
409 	 * If this is a credit note and it covers the full amount of the
410 	 * credited invoice, close them both out now.
411 	 */
412 	if (invp->cninv_id != 0) {
413 		if ((pip = db_findinvoicebyid(dbp, invp->cninv_id)) == NULL)
414 			berror("postinvoice", "cannot find parent invoice");
415 		if (invp->total <= -pip->total) {
416 			pip->state = INVOICE_STATE_PAID;
417 			invp->state = INVOICE_STATE_PAID;
418 		}
419 	}
420 	/*
421 	 * Update the database with the invoice changes.
422 	 */
423 	if (!noeffect) {
424 		if (db_updateinvoicebyid(dbp, invp, invp->id) < 0)
425 			berror("postinvoice", "cannot update invoice");
426 		if (cusp!=NULL && db_updatecustomerbyid(dbp, cusp, cusp->id)<0)
427 			berror("postinvoice", "cannot update customer");
428 	}
429 	/*
430 	 * Add a record to the sales ledger.
431 	 */
432 	if ((slp = db_sledgeralloc()) == NULL)
433 		berror("postinvoice", "cannot generate sales ledger entry");
434 	slp->customer_id = invp->customer_id;
435 	slp->sldate = invp->idate;
436 	slp->folio = strdup(invid);
437 	if (invp->cninv_id != 0)
438 		slp->descr = strdup("Credit Note");
439 	else
440 		slp->descr = strdup(invp->descr);
441 	slp->amount = -invp->total;
442 	if (!noeffect && db_insertsledger(dbp, slp) < 0)
443 		berror("postinvoice", "cannot create sales ledger entry");
444 	db_sledgerfree(slp);
445 	/*
446 	 * Send a copy of the invoice via the most appropriate method.
447 	 */
448 	if (!noeffect && cusp != NULL) {
449 		if (invp->cninv_id != 0)
450 			invoicemail(comp, cusp, invp, "crnote");
451 		else
452 			invoicemail(comp, cusp, invp, "invoice");
453 	}
454 }
455 
456 /*
457  *
458  */
459 void
usage()460 usage()
461 {
462 	fprintf(stderr, "Usage: postinvoice [-c <comp>][-nv]\n");
463 	exit(2);
464 }
465