Added initial semi-reference OpenERP plugin(incomplete) for Invoices. Tweaks around how Invoice interaction has to work. Added INVOICES.md document to cover how interacting with invoices will take place in the Artifice API.

This commit is contained in:
Aurynn Shaw 2013-08-07 12:20:37 +12:00
parent ae70d7126f
commit 5f6ecaf638
4 changed files with 198 additions and 34 deletions

83
INVOICES.md Normal file
View File

@ -0,0 +1,83 @@
# Invoice API
This document details how to add a new Invoice adapter to Artifice, enabling connection with arbitrary ERP systems.
## What Does Artifice Do
Artifice manages the connection from OpenStack Ceilometer to the ERP/billing system, managing data from Ceilometer in terms of a block (usually a month) of "billed time."
Artifice maintains its own storage to handle time that has been billed.
Artifice does not hold opinions on which ERP system should be used; OpenERP acts only as a reference implementation, detailing how an invoice could be billed.
Artifice makes the following assumptions:
* An invoice must be creatable
* An invoice must allow Sections to be declared
* An invoice must allow line items to be added
* An invoice must respond to cost(datacenter, name), returning a cost value
* An invoice must be commitable, saving it to the underlying ERP storage
## Implementation
Implementing an Artifice invoice object is intended to be simple:
from artifice.invoice import Invoice
class MyInvoice(Invoice):
def __init__(self, tenant, config):
pass
def add_line(self, item):
"""item is a triple of datacenter, name, value"""
def add_section(self, name):
"""Expected to create a subsection in the Invoice. Used for datacenter locations.
"""
def commit(self):
"""Closes this invoice. Expects to save to the underlying ERP storage system.
"""
def cost(self, datacenter, name):
"""
Taking a datacenter and a meter name, expected to return a fixed value for the given month.
:param datacenter
:param name
:returns float
"""
## Configuration
Configuration of the Invoice object is passed from the main Artifice configuration during Invoice initiation.
Configuration will be passed as-is from the main configuration to the Invoice system, allowing for arbitrary items to be passed through.
For example:
[invoice:object]
database_name="somedb"
or
[invoice:object]
url = "https://localhost:4567"
or
[invoice:object]
csv_directory = "/path/to/my/file"
## Usage and Declaration
Usage of the custom Invoice is controlled via the Artifice configuration file.
Under
[main]
add the line:
[main]
invoice:object="path.to.my.object:MyInvoice"
similar to *paste*-style configuration items.

View File

@ -1,15 +1,68 @@
import oerplib
from collections import defaultdict
from artifice import invoice
class connection(object):
pass
def __init__(self, config):
self.config = config
self.oerp = oerplib()
self.sections = []
self._data = defaultdict(list)
self.this_section = None
class openerp(invoice.Invoice):
def create(self):
pass
def save(self):
def section(self, title):
if title not in self.sections:
self.sections.append(title)
self.this_section = title
def add_line(self, text, value):
self._data[self.this_section].append((text, value))
def cost(self, datacenter, meter):
"""Returns the cost of a given resource in a given datacenter."""
return costs[meter][datacenter]
class OpenERP(invoice.Invoice):
def __init__(self, tenant, config):
super(OpenERP, self).__init__(tenant)
# Conn is expected to be a dict with:
#
self.conn = connection(self.config)
self.id = self.conn.create()
def add_section(self, name):
self.conn.section(name)
def add_line(self, item):
"""
Performs the save action against the OpenERP API
Doesn't do much more than create a thing and
"""
datacenter, name, value = item
self.conn.section( datacenter )
cost = self.conn.cost(datacenter, name)
self.conn.add_line( name, cost * value )
def commit(self):
self.conn.bill(self.id)
return self.id
def close(self):
"""
Makes this invoice no longer writable - it's closed and registered as
a closed invoice in OpenERP; sent out for payment, etc.
"""
pass
def cost(self, datacenter, name):
return self.conn.cost(datacenter, name)

View File

@ -411,14 +411,16 @@ class Artifact(object):
self.start,
self.end,
)
self.db.add(usage)
session.add(usage)
session.commit() # Persist to our backend
def volume(self):
"""
Default billable number for this volume
"""
return self.usage[-1]["counter_volume"]
return sum([x["counter_volume"] for x in self.usage])
class Cumulative(Artifact):
@ -428,12 +430,11 @@ class Cumulative(Artifact):
total_usage = measurements[-1]["counter_volume"] - measurements[0]["counter_volume"]
return total_usage
class Gauge(Artifact):
# def volume(self):
# pass
# Gauge and Delta have very little to do: They are expected only to
# exist as "not a cumulative" sort of artifact.
class Gauge(Artifact):
pass
class Delta(Artifact):
pass

View File

@ -10,50 +10,77 @@ an Invoice interface consists of:
"""
class IntegrityViolation(BaseException): pass
class BillingOverlap(BaseException): pass
class NoSuchType(KeyError): pass
class NoSuchLocation(KeyError): pass
costs = {
"cpu_util" : { "local": "1"}
"cpu_util" : { "nova": "1" }
}
class Costs(object):
def cost(self, location, name):
""""""
try:
locations = costs[name]
except KeyError:
raise NoSuchType(name)
try:
return locations[location]
except KeyError:
raise NoSuchLocation(location)
required = ["add_line", "close"]
def requirements(name, parents, attrs):
for attr_name in required:
try:
assert attr_name in attrs
except AssertionError:
raise RuntimeError("%s not in %s" % (attr_name, name))
try:
assert callable(attr_name)
except AssertionError:
raise RuntimeError("%s is not callable" % (attr_name))
return type(name, parents, attrs)
class Invoice(object):
def __init__(self, tenant):
# __metaclass__ = requirements
def __init__(self, tenant, config):
self.tenant = tenant
self.config = config
def close(self):
raise NotImplementedError("Not implemented in base class")
def bill(self, usage):
"""
Expects a list of dicts of datacenters
Each DC is expected to have a list of Types: VM, Network, Storage
Each Type is expected have to a list of Meters
Each Meter is expected to have a Usage method that takes our start
and end values.
Each Meter will be entered as a line on the Invoice.
"""
for dc in usage:
# DC is the name of the DC/region. Or the internal code. W/E.
# print datacenter
self.subheading(dc["name"])
for section in dc["sections"]: # will be vm, network, storage
self.subheading( section )
self.add_section( section )
meters = dc["sections"][section]
for usage in meters:
cost = self.cost( dc["name"], meter["name"] )
self.line( "%s per unit " % cost, usage.volume, cost * usage.volume )
self.add_line( "%s per unit " % cost, usage.volume, cost * usage.volume )
self.commit() # Writes to OpenERP? Closes the invoice? Something.
def commit(self):
pass
def close(self):
"""
Makes this invoice no longer writable - it's closed and registered as
a closed invoice in OpenERP; sent out for payment, etc.
"""
pass
def add_line(self, item):
raise NotImplementedError("Not implemented in base class")
def cost(self, datacenter, meter):
"""Returns the cost of a given resource in a given datacenter."""
return costs[meter][datacenter]
def add_section(self, title):
raise NotImplementedError("Not implemented in base class")