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
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)