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:
parent
ae70d7126f
commit
5f6ecaf638
83
INVOICES.md
Normal file
83
INVOICES.md
Normal 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.
|
||||||
|
|
@ -1,15 +1,68 @@
|
|||||||
import oerplib
|
import oerplib
|
||||||
|
from collections import defaultdict
|
||||||
from artifice import invoice
|
from artifice import invoice
|
||||||
|
|
||||||
class connection(object):
|
class connection(object):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.oerp = oerplib()
|
||||||
|
self.sections = []
|
||||||
|
self._data = defaultdict(list)
|
||||||
|
self.this_section = None
|
||||||
|
|
||||||
|
def create(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class openerp(invoice.Invoice):
|
def section(self, title):
|
||||||
|
|
||||||
def save(self):
|
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
|
pass
|
||||||
|
|
||||||
|
def cost(self, datacenter, name):
|
||||||
|
return self.conn.cost(datacenter, name)
|
@ -411,14 +411,16 @@ class Artifact(object):
|
|||||||
self.start,
|
self.start,
|
||||||
self.end,
|
self.end,
|
||||||
)
|
)
|
||||||
self.db.add(usage)
|
session.add(usage)
|
||||||
|
session.commit() # Persist to our backend
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def volume(self):
|
def volume(self):
|
||||||
"""
|
"""
|
||||||
Default billable number for this volume
|
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):
|
class Cumulative(Artifact):
|
||||||
|
|
||||||
@ -428,12 +430,11 @@ class Cumulative(Artifact):
|
|||||||
total_usage = measurements[-1]["counter_volume"] - measurements[0]["counter_volume"]
|
total_usage = measurements[-1]["counter_volume"] - measurements[0]["counter_volume"]
|
||||||
return total_usage
|
return total_usage
|
||||||
|
|
||||||
class Gauge(Artifact):
|
|
||||||
|
|
||||||
# def volume(self):
|
# Gauge and Delta have very little to do: They are expected only to
|
||||||
# pass
|
# exist as "not a cumulative" sort of artifact.
|
||||||
|
class Gauge(Artifact):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class Delta(Artifact):
|
class Delta(Artifact):
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
@ -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 = {
|
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):
|
class Invoice(object):
|
||||||
|
|
||||||
def __init__(self, tenant):
|
# __metaclass__ = requirements
|
||||||
|
|
||||||
|
def __init__(self, tenant, config):
|
||||||
self.tenant = tenant
|
self.tenant = tenant
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
raise NotImplementedError("Not implemented in base class")
|
||||||
|
|
||||||
def bill(self, usage):
|
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:
|
for dc in usage:
|
||||||
# DC is the name of the DC/region. Or the internal code. W/E.
|
# DC is the name of the DC/region. Or the internal code. W/E.
|
||||||
# print datacenter
|
# print datacenter
|
||||||
self.subheading(dc["name"])
|
self.subheading(dc["name"])
|
||||||
for section in dc["sections"]: # will be vm, network, storage
|
for section in dc["sections"]: # will be vm, network, storage
|
||||||
self.subheading( section )
|
self.add_section( section )
|
||||||
|
|
||||||
meters = dc["sections"][section]
|
meters = dc["sections"][section]
|
||||||
|
|
||||||
for usage in meters:
|
for usage in meters:
|
||||||
cost = self.cost( dc["name"], meter["name"] )
|
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.
|
self.commit() # Writes to OpenERP? Closes the invoice? Something.
|
||||||
|
|
||||||
def commit(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def close(self):
|
def add_line(self, item):
|
||||||
"""
|
raise NotImplementedError("Not implemented in base class")
|
||||||
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, meter):
|
def add_section(self, title):
|
||||||
"""Returns the cost of a given resource in a given datacenter."""
|
raise NotImplementedError("Not implemented in base class")
|
||||||
return costs[meter][datacenter]
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user