This is Invoice.m in view mode; [Download] [Up]
/*
* For legal stuff see the file COPYRIGHT
*/
#import <stdio.h>
#import <time.h>
#import <bsd/libc.h>
#import <sys/file.h>
#import <sys/types.h>
#import <sys/stat.h>
#import <objc/NXBundle.h>
#import <appkit/appkit.h>
#import "Controller.h"
#import "Invoice.h"
#import "Session.h"
#import "Expense.h"
#import "Preferences.h"
#import "createPath.h"
#define EXPENSE "Expense"
#define DETAIL "Detail"
#define INVOICE "Invoice"
#define TEMPLATE_TYPE "rtf"
#define MONTH_SWITCH_DAY 20
@interface Invoice(PRIVATE)
- (void)copyTemplateIfNeeded:(const char *)template;
- (FILE *)openTemplate:(const char *)template;
- (FILE *)openOutput:(const char *)path;
- (void)filterExpenses:(FILE *)in to:(FILE *)out;
- (void)filterDetail:(FILE *)in to:(FILE *)out;
- (void)filter:(FILE *)in to:(FILE *)out;
- (const char *)translate:(const char *)token;
- (BOOL)filterBuffer:(char *)ptr to:(FILE *)out
match:(const char *)target;
- (const char *)templatePath:(const char *)template;
- (const char *)createFilename:(const char *)type;
- (int)nextInvoiceNumber;
- generateInvoice;
- generateDetail;
- generateExpenses;
- (int)likelyMonth:(struct tm *)tm;
- (const char *)number; /* invoice number */
- (const char *)today; /* today's date */
- (const char *)thisMonth; /* the likely current invoicing month */
- (const char *)contact; /* person at client */
- (const char *)client; /* client name */
- (const char *)street; /* street address */
- (const char *)city;
- (const char *)state;
- (const char *)zip;
- (const char *)enddate; /* end of invoice period */
- (const char *)startdate; /* start of invoice period */
- (const char *)billings; /* total hourly billings */
- (const char *)expenses; /* total expenses */
- (const char *)total; /* billings + expenses */
- (const char *)hours; /* total hours worked for invoice period */
- (const char *)rate; /* hourly rate */
- (const char *)date; /* date of a session or expense */
- (const char *)length; /* length of a session (in hours) */
- (const char *)amount; /* expense amount */
- (const char *)description; /* description of session task or expense */
- (const char *)myName; /* consultant's personal information, from prefs */
- (const char *)myCompany;
- (const char *)myStreet;
- (const char *)myCity;
- (const char *)myState;
- (const char *)myZip;
- (const char *)myPhone;
- (const char *)myFax;
- (const char *)myEmail;
@end
@implementation Invoice
- initTemplateDir:(const char *)dir
{
[super init];
templateDir = NXCopyStringBuffer(dir);
if ( createPath( templateDir, DIRMODE ) != PathCreationOk ) {
NXRunAlertPanel( [NXApp name], "Cannot create path `%s'",
"Harumph.", NULL, NULL, templateDir );
return nil;
}
/*
* If the templates are not found in the templateDir,
* copy them there from the app wrapper.
*/
[self copyTemplateIfNeeded:EXPENSE];
[self copyTemplateIfNeeded:INVOICE];
[self copyTemplateIfNeeded:DETAIL];
return self;
}
- free
{
if ( templateDir ) {
free(templateDir);
templateDir = NULL;
}
return [super free];
}
- (BOOL)editTemplate:(const char *)template
{
const char *path = [self templatePath:template];
return [[Application workspace] openFile:path];
}
- (void)generate:(List *)clientList
{
int i, count = [clientList count];
int sessionCount, expenseCount;
for ( i = 0; i < count; i++ ) {
info = [clientList objectAt:i];
sessionCount = [info sessionCount];
expenseCount = [info expenseCount];
/* Don't generate an invoice if no work was done */
if ( sessionCount == 0 && expenseCount == 0 )
continue;
/* get (and increment) the invoice number */
number = [self nextInvoiceNumber];
[self generateInvoice];
if ( sessionCount > 0 )
[self generateDetail];
if ( expenseCount > 0 )
[self generateExpenses];
}
}
@end
@implementation Invoice(PRIVATE)
static char *Tokens[] = {
"number",
"today",
"contact",
"client",
"street",
"city",
"state",
"zip",
"startdate",
"enddate",
"billings",
"expenses",
"total",
"hours",
"rate",
"date",
"length",
"amount",
"description",
"myName",
"myCompany",
"myStreet",
"myCity",
"myState",
"myZip",
"myPhone",
"myFax",
"myEmail",
0,
};
#define START '['
#define END ']'
#define MAX_LINE 500
#define MAX_TOKEN 20
/*
* Determine the value of the token, or return NULL if
* not a known token.
*/
- (const char *)translate:(const char *)token
{
char **ptr;
for ( ptr = Tokens; *ptr; ptr++ ) {
if ( strcmp( *ptr, token ) == 0 ) {
SEL sel = sel_getUid(token);
if ( [self respondsTo:sel] )
return (const char *)[self perform:sel];
else
return NULL; /* found string, but don't respond */
}
}
return NULL;
}
/*
* Translate the line pointed to by 'ptr', writing the result to the
* stream 'out'. Return 1 if the given token occurred in this line.
*/
- (BOOL)filterBuffer:(char *)ptr to:(FILE *)output
match:(const char *)target
{
char c, *tok, token[MAX_TOKEN + 1];
const char *value;
int count;
BOOL matched = NO;
for ( ; *ptr ; ptr++ ) {
if ( *ptr == START ) {
for (count = 0, tok = token;
count < MAX_TOKEN && (c = *++ptr) && NXIsAlpha(c);
count++ )
*tok++ = c;
*tok = '\0';
if ( c == END ) { /* got a token */
if ( (value = [self translate:token]) == NULL ) /* it was unknown */
fprintf(output, "%c%s%c", START, token, END); /* pass it thru */
else {
fprintf(output, "%s", value); /* use its value */
if ( target && strcmp( token, target ) == 0 )
matched = YES;
}
} else if ( !NXIsAlpha(c) ) /* bad character */
fprintf(output, "%c%s%c", START, token, c); /* pass it thru */
else /* it was too long */
fprintf(output, "%c%s", START, token); /* pass it thru */
} else
fputc( *ptr, output );
}
return matched;
}
/*
* Straight translation - used to generate invoices.
*/
- (void)filter:(FILE *)input to:(FILE *)output
{
char buf[MAX_LINE + 1];
while ( fgets( buf, MAX_LINE, input ) )
[self filterBuffer:buf to:output match:NULL];
}
/*
* Expense generator - loops over Expense records when it
* finds the {description} token in a line.
*/
- (void)filterExpenses:(FILE *)input to:(FILE *)output
{
char buf[MAX_LINE + 1];
while ( fgets( buf, MAX_LINE, input ) ) {
/*
* When we find {description}, treat the current buf as
* the template for all expenses.
*/
if ( [self filterBuffer:buf to:output match:"description"] ) {
int i, count = [info expenseCount];
for ( i = 0; i < count; i++ ) {
expense = (Expense *)[info expenseAt:i];
[self filterBuffer:buf to:output match:NULL];
}
expense = nil;
}
}
}
/*
* Detail generator - loops over Session records when it
* finds the {description} token in a line.
*/
- (void)filterDetail:(FILE *)input to:(FILE *)output
{
char buf[MAX_LINE + 1];
while ( fgets( buf, MAX_LINE, input ) ) {
/*
* When we find {description}, treat the current buf as
* the template for all sessions.
*/
if ( [self filterBuffer:buf to:output match:"description"] ) {
int i, count = [info sessionCount];
for ( i = 0; i < count; i++ ) {
session = (Session *)[info sessionAt:i];
[self filterBuffer:buf to:output match:NULL];
}
session = nil;
}
}
}
/*
* Intuit the most likely month that this invoice is for. If it is before
* the 20th day of the month, assume it is for the prior month, otherwise
* assume it is for the current month.
*/
- (int)likelyMonth:(struct tm *)tm
{
int month = tm->tm_mon;
if ( tm->tm_mday < MONTH_SWITCH_DAY )
month = ( month == 0 ? 11 : month - 1 ); /* special for January */
return month;
}
/*
* Return the (likely) current invoicing month.
*/
- (const char *)thisMonth
{
time_t now;
struct tm *tm;
static char *months[] = {
"jan", "feb", "mar", "apr",
"may", "jun", "jul", "aug", "sep",
"oct", "nov", "dec"
};
time(&now);
tm = localtime(&now);
return months[[self likelyMonth:tm]];
}
/***********************************************************************
* Token Translation Methods
***********************************************************************/
- (const char *)number
{
static char buf[20];
sprintf( buf, "%d", number );
return buf;
}
/*
* Return today's date as a string
*/
- (const char *)today
{
time_t now;
struct tm *tm;
static char *months[] = {
"January", "February", "March", "April",
"May", "June", "July", "August", "September",
"October", "November", "December"
};
static char buf[80];
time(&now);
tm = localtime(&now);
sprintf(buf, "%s %d, 19%d",
months[tm->tm_mon], tm->tm_mday, tm->tm_year );
return buf;
}
- (const char *)contact
{
return info ? [info contactName] : "";
}
- (const char *)client
{
return info ? [info clientName] : "";
}
- (const char *)street
{
return info ? [info street] : "";
}
- (const char *)city
{
return info ? [info city] : "";
}
- (const char *)state
{
return info ? [info addrState] : "";
}
- (const char *)zip
{
return info ? [info zipCode] : "";
}
- (const char *)enddate
{
int month;
time_t now;
struct tm *tm;
static char buf[80];
static int lengths[] = { /* number of days per month */
31, 28, 31, 30, /* gloss over leap years... */
31, 30, 31, 31,
30, 31, 30, 31
};
time(&now);
tm = localtime(&now);
month = [self likelyMonth:tm];
sprintf( buf, "%02d/%02d/%02d", month + 1, lengths[month], tm->tm_year );
return buf;
}
/*
* Intuit the most likely start date for this invoice. If it is before
* the 20th day of the month, assume it is for the prior month, otherwise
* assume it is for the current month.
*/
- (const char *)startdate
{
int month;
time_t now;
struct tm *tm;
static char buf[80];
time(&now);
tm = localtime(&now);
month = [self likelyMonth:tm];
sprintf( buf, "%02d/01/%02d", month + 1, tm->tm_year );
return buf;
}
- (const char *)billings
{
static char buf[20];
if ( info ) {
commafyDouble( [info totalBillable], buf );
return buf;
}
return "";
}
- (const char *)expenses
{
static char buf[20];
if ( info ) {
commafyDouble( [info totalExpenses], buf );
return buf;
}
return "";
}
- (const char *)total
{
static char buf[20];
if ( info ) {
commafyDouble( [info totalBillable] + [info totalExpenses], buf );
return buf;
}
return "";
}
- (const char *)hours
{
static char buf[20];
if ( info ) {
sprintf( buf, "%1.2f", [info totalHours] );
return buf;
}
return "";
}
- (const char *)rate
{
static char buf[20];
if ( info ) {
sprintf( buf, "%1.2f", [info hourlyRate] );
return buf;
}
return "";
}
- (const char *)date
{
if ( session )
return [session startDateString];
if ( expense )
return [expense dateString];
return "";
}
- (const char *)amount
{
static char buf[20];
if ( !expense )
return "";
commafyDouble( [expense amount], buf );
return buf;
}
- (const char *)length
{
static char buf[20];
if ( !session )
return "";
sprintf( buf, "%1.2f", [session hours] );
return buf;
}
- (const char *)description
{
if ( expense )
return [expense description];
if ( session )
return [session description];
return "";
}
/*
* Consultant information taken from Preferences
*/
- (const char *)myName
{
return [[Preferences new] myName];
}
- (const char *)myCompany
{
return [[Preferences new] myCompany];
}
- (const char *)myStreet
{
return [[Preferences new] myStreet];
}
- (const char *)myCity
{
return [[Preferences new] myCity];
}
- (const char *)myState
{
return [[Preferences new] myState];
}
- (const char *)myZip
{
return [[Preferences new] myZip];
}
- (const char *)myPhone
{
return [[Preferences new] myPhone];
}
- (const char *)myFax
{
return [[Preferences new] myFax];
}
- (const char *)myEmail
{
return [[Preferences new] myEmail];
}
- (const char *)templatePath:(const char *)template
{
static char path[FILENAME_MAX + 1];
sprintf( path, "%s/%sTemplate.%s", templateDir, template, TEMPLATE_TYPE );
return path;
}
/*
* If the named template file is not found in the templateDir,
* copy the one from the app wrapper over there.
*/
- (void)copyTemplateIfNeeded:(const char *)template
{
FILE *fp;
const char *templatePath;
char templateSrc[FILENAME_MAX + 1];
char name[FILENAME_MAX + 1];
templatePath = [self templatePath:template];
/* assume it's ok if we can open it for reading */
if ( (fp = fopen( templatePath, "r" )) != NULL ) {
fclose(fp);
return; /* the file is already there */
}
sprintf( name, "%sTemplate", template );
if ( ! [[NXBundle mainBundle] getPath:templateSrc
forResource:name ofType:TEMPLATE_TYPE] ) {
NXRunAlertPanel( [NXApp name], "Can't find %s.%s in app wrapper",
"What Bogosity!", NULL, NULL, name, TEMPLATE_TYPE );
return;
}
copyFile( templateSrc, templatePath );
}
/*
* Open a template file for reading.
*/
- (FILE *)openTemplate:(const char *)template
{
FILE *fp;
const char *path = [self templatePath:template];
if ( ! (fp = fopen( path, "r" )) )
NXRunAlertPanel( [NXApp name], "Can't open `%s' for reading",
"Dang!", NULL, NULL, path );
return fp;
}
/*
* Take out blanks and punctuation characters to create a reasonable
* file name. NB: Returns a pointer to a local static buffer
*/
- (const char *)createFilename:(const char *)type
{
char c, *ptr;
const char *dir = GET_DEFAULT( INVOICE_DIR );
const char *src = [info shortName];
static char buf[MAXPATHLEN];
sprintf( buf, "%s/", dir ) ;
ptr = buf + strlen(buf);
while ( (c = *src++) != '\0' )
if ( NXIsAlpha(c) ) /* don't copy punctuation */
*ptr++ = ( NXIsUpper(c) ? NXToLower(c) : c );
sprintf( ptr, ".%d.%s.%s.rtf", number, [self thisMonth], type );
return buf;
}
/*
* Open a file of the given type for writing
*/
- (FILE *)openOutput:(const char *)type
{
FILE *fp;
const char *path = [self createFilename:type];
if ( ! (fp = fopen( path, "w" )) )
NXRunAlertPanel( [NXApp name], "Can't open `%s' for writing",
"Drat.", NULL, NULL, path );
return fp;
}
/*
* Read the current "next invoice number" from the defaults database
* increment and return the number, after storing the value back.
*/
- (int)nextInvoiceNumber
{
char num[10];
int value;
value = atoi(GET_DEFAULT( INVOICE_NUM ));
sprintf( num, "%d", ++value );
PUT_DEFAULT( INVOICE_NUM, num );
return value; /* return the incremented value */
}
/*
* Generate an expense report data file for any customer that had
* expenses for this period.
*/
- generateExpenses
{
FILE *in, *out;
if ( !( in = [self openTemplate:EXPENSE]) || !( out = [self openOutput:"exp"] ))
return nil;
[self filterExpenses:in to:out];
fclose( in );
fclose( out );
return self;
}
- generateDetail
{
FILE *in, *out;
if ( !( in = [self openTemplate:DETAIL]) || !( out = [self openOutput:"dtl"] ))
return nil;
[self filterDetail:in to:out];
fclose( in );
fclose( out );
return self;
}
- generateInvoice
{
FILE *in, *out;
if ( !( in = [self openTemplate:INVOICE]) || !( out = [self openOutput:"inv"] ))
return nil;
[self filter:in to:out];
fclose( in );
fclose( out );
return self;
}
@end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.