Example #1
0
	def createInvoice(self, invoiceid, template=None, outfile=None):
		"""Create an invoice from the parsed Gnucash data.

		Arguments:
			invoiceid -- Id of the invoice to extract from Gnucash. A string
						 or an integer.
			template -- Name of the invoice template file, or list of lines.
			outfile -- File name for the generated invoice, default is stdout.
		Options from self.options used by this method:
			quantities_uselocale -- Format quantity values using the locale
				setting.
			currency_uselocale -- Format currency values using the locale
				setting.
			quantities_precision -- Used decimal precision for quantities.
			currency_precision -- Used decimal precision for currencies.
			quantities_dashsymb -- Replace a zero fractional part of quantity
				values with this symbol, but only if not None, and if
				uselocale. Example: '12.00' -> '12.-'.
			currency_dashsymb -- As quantities_dashsymb for currency values.
			qformat -- Function to format quantity values, overrides
				quantities_*, should take a Decimal as argument and return an
				unicode string.
			cformat -- Function to format currency values, overrides
				currency_*, should take a Decimal as argument and return an
				unicode string.
			templates -- Dictionary of invoice template file names; keys are
				the 'owner' values of the invoice, or 'default'.
			outfile -- Name of the file to write the invoice out.
			regex_rex -- Expression regex used by the template engine.
			regex_rbe -- Begin statement regex.
			regex_ren -- End statement regex.
			regex_rco -- Continuation statement regex.

		"""
		invoiceid = Convert.readint(invoiceid)
		try:
			invoice = self.invoices[invoiceid]
		except KeyError:
			self.logger.error("No invoice found for invoiceid [%s]" % 
					invoiceid)
			raise GcinvoiceError("No invoice found for invoiceid [%s]" % 
					invoiceid)
		invc = copy.deepcopy(invoice)
		if invc.get('_warndiscount', False):
			self.logger.warn("The invoice contains POSTTAX discounts, which "
					"are calculated differenty in gcinvoice and Gnucash")
		invc['amountNet'] = sum(x['amountNet'] for x in invc['entries'])
		invc['amountGross'] = sum(x['amountGross'] for x in invc['entries'])
		invc['amountTaxes'] = sum(x['amountTaxes'] for x in invc['entries'])

		uselocale_qty = getattr(self.options, 'quantities_uselocale', True)
		precision_qty = getattr(self.options, 'quantities_precision', None)
		dashsymb_qty = getattr(self.options, 'quantities_dashsymb', None)
		qformat = getattr(self.options, 'qformat', None)
		uselocale_curr = getattr(self.options, 'currency_uselocale', True)
		precision_curr = getattr(self.options, 'currency_precision', None)
		dashsymb_curr = getattr(self.options, 'currency_dashsymb', None)
		cformat = getattr(self.options, 'cformat', None)
		invc['currencyformatting'] = Format.currencyformatting
		invc['quantityformatting'] = Format.quantityformatting
		cformat = invc['cformat'] = cformat or functools.partial(
				Format.currencyformatting, uselocale=uselocale_curr,
				precision=precision_curr, dashsymb=dashsymb_curr)
		qformat = invc['qformat'] = qformat or functools.partial(
				Format.quantityformatting, uselocale=uselocale_qty,
				precision=precision_qty, dashsymb=dashsymb_qty)
		invc['Decimal'] = Decimal
		for x in ['amountNet', 'amountGross', 'amountTaxes']:
			invc["%sInt" % x] = invc[x]
			invc[x] = cformat(invc[x])
		for e in invc['entries']:
			for x in ['price', 'amountRaw', 'amountNet', 'amountGross',
					'amountTaxes', 'amountDiscount']:
				e["%sInt" % x] = e[x]
				e[x] = cformat(e[x])
			for x in ['qty']:
				e["%sInt" % x] = e[x]
				e[x] = qformat(e[x])
			if e['discount'] is not None:
				e['discountInt'] = e['discount']
				if e['discountType'] == 'PERCENT':
					e['discount'] = cformat(e['discount'])
				else:
					e['discount'] = qformat(e['discount'])

		rex = re.compile(getattr(self.options, 'regex_rex', None) or
				'@\\{([^}]+)\\}')
		rbe = re.compile(getattr(self.options, 'regex_rbe', None) or '%\\+')
		ren = re.compile(getattr(self.options, 'regex_ren', None) or '%-')
		rco = re.compile(getattr(self.options, 'regex_rco', None) or '%= ')
		try:
			ownername = invc['owner']['name']
		except Exception:
			ownername = None

		template = template or \
				self.options.templates.get(ownername, None) or \
				self.options.templates.get('default', None)
		if template is None:
			self.logger.error("No template given.")
			raise GcinvoiceError("No template given.")
		readfromfile = True
		if isinstance(template, basestring):
			# The name of the template file is itself a template in order to
			# select different templates depending on the invoice.
			templ_ = StringIO.StringIO()
			cop = copier(rex, invc, rbe, ren, rco, ouf=templ_,
				encoding=self._gcfile_encoding)
			cop.copy([template])
			templ = templ_.getvalue()
			templ_.close()
			try:
				templ = file(templ)
				self.logger.info("Using file [%s] as template" % templ)
			except Exception:
				self.logger.info("The given template [%s] is not readable, "
						"trying to use it directly as string..." % templ,
						exc_info=True)
				try:
					templ = [(line + '\n') for line in template.split('\n')]
					readfromfile = False
				except Exception:
					self.logger.error("The given template [%s] is neither a "
							"readable file, nor a readable string" % template,
							exc_info=True)
					raise GcinvoiceError("The template is neither a file nor a"
							" string")
		else:
			templ = template
		if readfromfile:
			self.logger.info("Using [%s] as file object" % templ)
			try:
				templ = [line.decode(self._gcfile_encoding)
						for line in templ.readlines()]
			except UnicodeDecodeError:
				self.logger.error("The template file [%s] cannot be "
						"decoded using the encoding [%s] of Gnucash data files"
						% (template, self._gcfile_encoding), exc_info=True)
				raise GcinvoiceError("The given template cannot be decoded")

		outfile = outfile or \
				self.options.outfiles.get(ownername, None) or \
				self.options.outfiles.get('default', None)
		if isinstance(outfile, basestring):
			# The name of the outfile is itself a template in order to
			# select different outfiles depending on the invoice.
			outf_ = StringIO.StringIO()
			cop = copier(rex, invc, rbe, ren, rco, ouf=outf_,
					encoding=self._gcfile_encoding)
			cop.copy([outfile])
			outfile = outf_.getvalue()
			outf_.close()
			try:
				outf = file(outfile, "w")
			except Exception:
				self.logger.error("Cannot open [%s] for writing" % outfile,
						exc_info=True)
				raise
			self.logger.info("Using [%s] as outfile" % outfile)
		elif not outfile:
			outf = sys.stdout
			self.logger.info("Using stdout as outfile")
		else:
			outf = outfile
			self.logger.info("Using [%s] directly as outfile object")
		# now the very templating
		def handle(expr):
			self.logger.warn("Cannot do template for expression [%s]" % expr,
					exc_info=True)
			return expr
		cop = copier(rex, invc, rbe, ren, rco, ouf=outf, handle=handle,
				encoding=self._gcfile_encoding)
		try:
			cop.copy(templ)
			outf.close()
		except Exception:
			self.logger.error("Error in template", exc_info=True)
			raise
Example #2
0
	def parse(self, gcfile=None):
		"""Parse a Gnucash file.

		Currently only the data useful for invoices is extracted.

		Arguments:
			gcfile -- the file containing Gnucash data.
		Options from self.options used by this method:
			gcfile -- the file containing Gnucash data.

		"""

		if not gcfile:
			gcfile = getattr(self.options, 'gcfile', None)
		if not gcfile:
			self.logger.error("No gcfile given.")
			raise GcinvoiceError("No gcfile given.")
		try:
			self.gctree = ET.parse(gcfile)
		except SyntaxError:
			try:
				gcfile_ = gzip.open(gcfile)
				self.gctree = ET.parse(gcfile_)
				gcfile_.close()
			except Exception:
				self.logger.error("Could not parse file [%s]." % gcfile)
				raise

		ns = self._xmlns_qualify
		book = self.gctree.find(ns('gnc:book'))

		self.customers = {}
		for cust in book.findall(ns('gnc:GncCustomer')):
			try:
				custdict = dict(address=[])
				custdict['guid'] = cust.findtext(ns('cust:guid'))
				custdict['name'] = cust.findtext(ns('cust:name'))
				custdict['id'] = Convert.readint(cust.findtext(ns('cust:id')))
				for a in cust.findall(ns('cust:addr/*')):
					if a.tag == ns('addr:email'):
						custdict['email'] = a.text
					elif a.tag == ns('addr:name'):
						custdict['fullName'] = a.text
					elif a.tag.startswith(ns('addr:addr')):
						custdict['address'].append((a.tag, a.text))
				custdict['address'].sort(key=get0)
				custdict['address'] = [x[1] for x in custdict['address']]
				self.customers[custdict['guid']] = custdict
			except Exception:
				self.logger.warn("Problem parsing GncCustomer [%s]" % 
						ET.tostring(cust), exc_info=True)
				continue

		self.vendors = {}
		for vendor in book.findall(ns('gnc:GncVendor')):
			try:
				vendordict = dict(address=[])
				vendordict['guid'] = vendor.findtext(ns('vendor:guid'))
				vendordict['name'] = vendor.findtext(ns('vendor:name'))
				vendordict['id'] = Convert.readint(vendor.findtext(ns('vendor:id')))
				for a in vendor.findall(ns('vendor:addr/*')):
					if a.tag == ns('addr:email'):
						vendordict['email'] = a.text
					elif a.tag == ns('addr:name'):
						vendordict['fullName'] = a.text
					elif a.tag.startswith(ns('addr:addr')):
						vendordict['address'].append((a.tag, a.text))
				vendordict['address'].sort(key=get0)
				vendordict['address'] = [x[1] for x in vendordict['address']]
				self.vendors[vendordict['guid']] = vendordict
			except Exception:
				self.logger.warn("Problem parsing GncVendor [%s]" % 
						ET.tostring(vendor), exc_info=True)
				continue

		self.terms = {}
		for term in book.findall(ns('gnc:GncBillTerm')):
			try:
				termdict = dict()
				termdict['guid'] = term.findtext(ns('billterm:guid'))
				termdict['name'] = term.findtext(ns('billterm:name'))
				termdict['desc'] = term.findtext(ns('billterm:desc'))
				termdict['due-days'] = term.findtext(
						ns('billterm:days/bt-days:due-days'))
				termdict['disc-days'] = term.findtext(
						ns('billterm:days/bt-days:disc-days'))
				discount = term.findtext(ns('billterm:days/bt-days:discount'))
				if discount is not None:
					discount = Convert.readnumber(discount)
				termdict['discount'] = discount
				self.terms[termdict['guid']] = termdict
			except Exception:
				self.logger.warn("Problem parsing GncBillTerm [%s]" % 
						ET.tostring(term), exc_info=True)
				continue

		self.taxtables = {}
		for tax in book.findall(ns('gnc:GncTaxTable')):
			try:
				taxdict = dict(entries=[])
				taxdict['guid'] = tax.findtext(ns('taxtable:guid'))
				taxdict['name'] = tax.findtext(ns('taxtable:name'))
				taxdict['percentSum'] = Decimal(0)
				taxdict['valueSum'] = Decimal(0)
				for te in tax.findall(
						ns('taxtable:entries/gnc:GncTaxTableEntry')):
					tedict = dict()
					try:
						tedict['type'] = te.findtext(ns('tte:type'))
						tedict['amount'] = Convert.readnumber(
								te.findtext(ns('tte:amount')))
						if tedict['type'] == 'PERCENT':
							taxdict['percentSum'] += tedict['amount']
						elif tedict['type'] == 'VALUE':
							taxdict['valueSum'] += tedict['amount']
						else:
							self.logger.warn("Invalid tte:type [%s]" % 
									tedict['type'])
							raise GcinvoiceError("Invalid tte:type")
					except Exception:
						self.logger.warn(
								"Problem parsing GncTaxTableEntry [%s]" % 
								ET.tostring(te), exc_info=True)
						raise
					taxdict['entries'].append(tedict)
			except Exception:
				self.logger.warn("Problem parsing GncTaxTable [%s]" % 
						ET.tostring(tax), exc_info=True)
				continue
			self.taxtables[taxdict['guid']] = taxdict

		self.jobs = {}
		for job in book.findall(ns('gnc:GncJob')):
			try:
				jobdict = dict()
				jobdict['guid'] = job.findtext(ns('job:guid'))
				jobdict['name'] = job.findtext(ns('job:name'))
				jobdict['id'] = Convert.readint(job.findtext(ns('job:id')))
				jobdict['reference'] = job.findtext(ns('job:reference'))
				ownerguid = job.findtext(ns('job:owner/owner:id'))
				ownertype = job.findtext(ns('job:owner/owner:type'))
				if ownertype == 'gncVendor':
					jobdict['owner'] = self.vendors.get(ownerguid, None)
				elif ownertype == 'gncCustomer':
					jobdict['owner'] = self.customers.get(ownerguid, None)
				self.jobs[jobdict['guid']] = jobdict
			except Exception:
				self.logger.warn("Problem parsing Gncjob [%s]" % 
						ET.tostring(job), exc_info=True)
				continue

		self.invoices = {}
		self.invoices_ = {}
		for invc in book.findall(ns('gnc:GncInvoice')):
			invcdict = dict()
			try:
				invcdict['guid'] = invc.findtext(ns('invoice:guid'))
				invcdict['id'] = Convert.readint(invc.findtext(ns('invoice:id')))
				invcdict['billing_id'] = invc.findtext(ns('invc:billing_id'))
				invcdict['job'] = None
				ownerguid = invc.findtext(ns('invoice:owner/owner:id'))
				ownertype = invc.findtext(ns('invoice:owner/owner:type'))
				if ownertype == 'gncVendor':
					invcdict['owner'] = self.vendors.get(ownerguid, None)
				elif ownertype == 'gncCustomer':
					invcdict['owner'] = self.customers.get(ownerguid, None)
				elif ownertype == 'gncJob':
					invcdict['job'] = self.jobs.get(ownerguid, None)
					if invcdict['job']:
						invcdict['owner'] = invcdict['job'].get('owner', None)
				invcdict['dateOpened'] = Convert.readdate(invc.findtext(
					ns('invoice:opened/ts:date')))
				invcdict['datePosted'] = Convert.readdate(invc.findtext(
					ns('invoice:posted/ts:date')))
				termsguid = invc.findtext(ns('invoice:terms'))
				invcdict['terms'] = self.terms.get(termsguid, None)
				invcdict['notes'] = invc.findtext(ns('invoice:notes'))
				invcdict['currency'] = invc.findtext(
						ns('invoice:currency/cmdty:id'))
			except Exception:
				self.logger.warn("Problem parsing GncInvoice [%s]" % 
						ET.tostring(invc), exc_info=True)
				continue
			invcdict['entries'] = []   # to be filled later parsing entries
			self.invoices[invcdict['id']] = invcdict
			self.invoices_[invcdict['guid']] = invcdict

		self.entries = {}
		# do this until the XPath 'tag[subtag]' expression works (ET >= 1.3).
		for entry in book.findall(ns('gnc:GncEntry')):
			try:
				invoiceguid = entry.findtext(ns('entry:invoice'))
				if not invoiceguid:
					continue
				try:
					invoiceentries = self.invoices_[invoiceguid]['entries']
				except KeyError:
					self.logger.warn("Cannot find GncInvoice for guid [%s]"
							"refered in GncEntry [%s]" % (invoiceguid,
							ET.tostring(entry)), exc_info=True)
					continue
				entrydict = dict()
				entrydict['guid'] = entry.findtext(ns('entry:guid'))
				entrydict['date'] = Convert.readdate(entry.findtext(
					ns('entry:date/ts:date')))
				entrydict['entered'] = Convert.readdatetime(entry.findtext(
					ns('entry:entered/ts:date')))
				entrydict['description'] = entry.findtext(
						ns('entry:description'))
				entrydict['action'] = entry.findtext(ns('entry:action'))
				entrydict['qty'] = Convert.readnumber(entry.findtext(
						ns('entry:qty')))
				entrydict['price'] = Convert.readnumber(entry.findtext(
						ns('entry:i-price')))
				entrydict['discount'] = entry.findtext(ns('entry:i-discount'))
				if entrydict['discount'] is not None:
					entrydict['discount'] = Convert.readnumber(entrydict['discount'])
					entrydict['discountType'] = entry.findtext(
							ns('entry:i-disc-type'))
					entrydict['discount_how'] = entry.findtext(
							ns('entry:i-disc-how'))
				entrydict['taxable'] = int(entry.findtext(
							ns('entry:i-taxable')))
				if entrydict['taxable']:
					entrydict['taxincluded'] = int(entry.findtext(
							ns('entry:i-taxincluded')))
					taxtable = entry.findtext(ns('entry:i-taxtable'))
					if taxtable:
						try:
							entrydict['taxtable'] = self.taxtables[taxtable]
						except KeyError:
							self.logger.warn("Cannot find GncTaxTable for guid"
								" [%s] refered in GncEntry [%s]" % (taxtable,
								ET.tostring(entry)), exc_info=True)
							continue
			except Exception:
				self.logger.warn("Problem parsing GncEntry [%s]" % 
						ET.tostring(entry), exc_info=True)
				continue
			try:
				self._calcTaxDiscount(entrydict)
			except GcinvoiceError, msg:
				self.logger.error(msg)
				continue
			if entrydict.get('_warndiscount', False):
				del entrydict['_warndiscount']
				self.invoices_[invoiceguid]['_warndiscount'] = True
			self.entries[entrydict['guid']] = entrydict
			invoiceentries.append(entrydict)