Skip to content
This repository has been archived by the owner on Feb 9, 2019. It is now read-only.

Commit

Permalink
Merge branch 'release/0.4.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
opennode-jenkins committed Jun 30, 2016
2 parents e804b9e + 895579c commit ad68758
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 56 deletions.
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

os.environ['DJANGO_SETTINGS_MODULE'] = 'nodeconductor.server.test_settings'
os.environ['DJANGO_SETTINGS_MODULE'] = 'nodeconductor.server.doc_settings'
from django.conf import settings
settings.INSTALLED_APPS = [app for app in settings.INSTALLED_APPS if not app.endswith('tests')]
settings.BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
Expand Down
49 changes: 49 additions & 0 deletions packaging/nodeconductor-killbill.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Name: nodeconductor-killbill
Summary: KillBill plugin for NodeConductor
Group: Development/Libraries
Version: 0.4.0
Release: 1.el7
License: Copyright 2015 OpenNode LLC. All rights reserved.
Url: http://nodeconductor.com
Source0: %{name}-%{version}.tar.gz

Requires: nodeconductor > 0.102.2
Requires: python-lxml >= 3.2.0
Requires: python-xhtml2pdf >= 0.0.6

BuildArch: noarch
BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot

BuildRequires: python-setuptools

%description
NodeConductor KillBill allows to make invoices via KillBill.

%prep
%setup -q -n %{name}-%{version}

%build
python setup.py build

%install
rm -rf %{buildroot}
python setup.py install --single-version-externally-managed -O1 --root=%{buildroot} --record=INSTALLED_FILES

%clean
rm -rf %{buildroot}

%files -f INSTALLED_FILES
%defattr(-,root,root)

%changelog
* Thu Jun 30 2016 Jenkins <jenkins@opennodecloud.com> - 0.4.0-1.el7
- New upstream release

* Thu Apr 28 2016 Jenkins <jenkins@opennodecloud.com> - 0.3.3-1.el7
- New upstream release

* Tue Dec 8 2015 Jenkins <jenkins@opennodecloud.com> - 0.3.2-1.el7
- New upstream release

* Thu Nov 19 2015 Roman Kosenko <roman@opennodecloud.com> - 0.1.0-1.el7
- Initial version of the package
23 changes: 3 additions & 20 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env python
import sys
from setuptools import setup, find_packages


Expand All @@ -8,32 +7,16 @@
]

install_requires = [
'nodeconductor>0.91.0',
'nodeconductor>0.102.2',
'lxml>=3.2',
'xhtml2pdf>=0.0.6',
'Pillow>=2.0.0,<3.0.0',
]


# RPM installation does not need oslo, cliff and stevedore libs -
# they are required only for installation with setuptools
try:
action = sys.argv[1]
except IndexError:
pass
else:
if action in ['develop', 'install', 'test', 'bdist_egg']:
install_requires += [
'cliff==1.7.0',
'oslo.config==1.4.0',
'oslo.i18n==1.0.0',
'oslo.utils==1.0.0',
'stevedore==1.0.0',
]


setup(
name='nodeconductor-killbill',
version='0.3.3',
version='0.4.0',
author='OpenNode Team',
author_email='info@opennodecloud.com',
url='http://nodeconductor.com',
Expand Down
57 changes: 54 additions & 3 deletions src/nodeconductor_killbill/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
from django.conf.urls import patterns, url
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.utils.translation import gettext

from nodeconductor.core.admin import AdminActionsRegister
from nodeconductor.core.tasks import send_task
from nodeconductor.structure.models import PaidResource
from nodeconductor.cost_tracking.admin import DefaultPriceListItemAdmin
from nodeconductor.cost_tracking.models import PayableMixin

from .backend import KillBillBackend
from .backend import KillBillBackend, KillBillError
from .models import Invoice
from .tasks import update_today_usage_of_resource

Expand All @@ -23,7 +26,7 @@ def get_urls(self):
return my_urls + super(InvoiceAdmin, self).get_urls()

def move_date(self, request):
for model in PaidResource.get_all_models():
for model in PayableMixin.get_all_models():
for resource in model.objects.all():
try:
update_today_usage_of_resource(resource.to_string())
Expand Down Expand Up @@ -51,3 +54,51 @@ def sync(self, request):


admin.site.register(Invoice, InvoiceAdmin)


def sync(request):
send_task('killbill', 'sync_pricelist')()
messages.add_message(request, messages.INFO, "Price lists scheduled for sync")
return redirect(reverse('admin:cost_tracking_defaultpricelistitem_changelist'))


def subscribe_resources(request):
erred_resources = {}
subscribed_resources = []
existing_resources = []
for model in PayableMixin.get_all_models():
for resource in model.objects.exclude(state=model.States.ERRED):
try:
backend = KillBillBackend(resource.customer)
is_newly_subscribed = backend.subscribe(resource)
except KillBillError as e:
erred_resources[resource] = str(e)
else:
resource.last_usage_update_time = None
resource.save(update_fields=['last_usage_update_time'])
if is_newly_subscribed:
subscribed_resources.append(resource)
else:
existing_resources.append(resource)

if subscribed_resources:
message = gettext('Successfully subscribed %s resources: %s')
message = message % (len(subscribed_resources), ', '.join(r.name for r in subscribed_resources))
messages.add_message(request, messages.INFO, message)

if existing_resources:
message = gettext('%s resources were already subscribed: %s')
message = message % (len(existing_resources), ', '.join(r.name for r in existing_resources))
messages.add_message(request, messages.INFO, message)

if erred_resources:
message = gettext('Failed to subscribe resources: %(erred_resources)s')
erred_resources_message = ', '.join(['%s (error: %s)' % (r.name, e) for r, e in erred_resources.items()])
message = message % {'erred_resources': erred_resources_message}
messages.add_message(request, messages.ERROR, message)

return redirect(reverse('admin:cost_tracking_defaultpricelistitem_changelist'))


AdminActionsRegister.register(DefaultPriceListItemAdmin, sync, 'Sync price lists with backend')
AdminActionsRegister.register(DefaultPriceListItemAdmin, subscribe_resources, 'Subscribe missed resources')
18 changes: 17 additions & 1 deletion src/nodeconductor_killbill/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
from django.db.models import signals
from django_fsm.signals import post_transition

from nodeconductor.cost_tracking.models import PayableMixin


class KillBillConfig(AppConfig):
name = 'nodeconductor_killbill'
Expand All @@ -27,7 +29,7 @@ def ready(self):
dispatch_uid='nodeconductor_killbill.handlers.log_invoice_delete',
)

for index, resource in enumerate(structure_models.PaidResource.get_all_models()):
for index, resource in enumerate(PayableMixin.get_all_models()):
post_transition.connect(
handlers.subscribe,
sender=resource,
Expand Down Expand Up @@ -56,6 +58,20 @@ def ready(self):
resource.__name__, index),
)

for index, service in enumerate(structure_models.Service.get_all_models()):
signals.post_save.connect(
handlers.update_service_name,
sender=service,
dispatch_uid='nodeconductor_killbill.handlers.update_service_name_{}_{}'.format(
service.__name__, index),
)

signals.post_save.connect(
handlers.update_service_settings_name,
sender=structure_models.ServiceSettings,
dispatch_uid='nodeconductor_killbill.handlers.update_service_settings_name',
)

signals.post_save.connect(
handlers.update_project_name,
sender=structure_models.Project,
Expand Down
16 changes: 12 additions & 4 deletions src/nodeconductor_killbill/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,14 @@ def sync_invoices(self):
map(lambda i: i.delete(), cur_invoices.values())

def subscribe(self, resource):
""" Return True if resource was not subscribed before """
client_id = self.get_or_create_client()
resource.billing_backend_id = self.api.add_subscription(client_id, resource)
resource.save(update_fields=['billing_backend_id'])
billing_backend_id = self.api.add_subscription(client_id, resource)
if resource.billing_backend_id != billing_backend_id:
resource.billing_backend_id = billing_backend_id
resource.save(update_fields=['billing_backend_id'])
return True
return False

def terminate(self, resource):
self.api.del_subscription(resource.billing_backend_id)
Expand Down Expand Up @@ -170,6 +175,7 @@ def _parse_invoice_data(self, raw_invoice):
invoice['items'].append(dict(
backend_id=item['invoiceItemId'],
name=item['usageName'] or item['description'],
service=fields['service_name'],
project=fields['project_name'],
resource=fields['resource_name'],
currency=item['currency'],
Expand Down Expand Up @@ -221,7 +227,8 @@ def add_subscription(self, client_id, resource):
self.update_subscription_fields(
subscription_id,
resource_name=resource.full_name,
project_name=resource.project.full_name)
project_name=resource.project.full_name,
service_name=resource.service_project_link.service.full_name)
return subscription_id
except NotFoundKillBillError:
pass
Expand All @@ -239,7 +246,8 @@ def add_subscription(self, client_id, resource):
self.set_subscription_fields(
subscription['subscriptionId'],
resource_name=resource.full_name,
project_name=resource.project.full_name)
project_name=resource.project.full_name,
service_name=resource.service_project_link.service.full_name)

return subscription['subscriptionId']

Expand Down
67 changes: 47 additions & 20 deletions src/nodeconductor_killbill/handlers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
import logging

from nodeconductor.structure.models import PaidResource
from nodeconductor.cost_tracking.models import PayableMixin
from nodeconductor.structure import SupportedServices

from .backend import KillBillBackend, KillBillError
from .log import event_logger


logger = logging.getLogger(__name__)
paid_models = PayableMixin.get_all_models()


def update_subscription_fields(model, queryargs=None, fields=None):
backend = KillBillBackend()
for resource in model.objects.exclude(billing_backend_id=None).filter(**queryargs):
try:
args = {k: reduce(getattr, v.split('__'), resource) for k, v in fields.items()}
backend.update_subscription_fields(resource.billing_backend_id, **args)
except KillBillError as e:
logger.error(
"Failed to update KillBill fields for resource %s: %s", resource, e)


def log_invoice_save(sender, instance, created=False, **kwargs):
Expand Down Expand Up @@ -63,27 +76,41 @@ def update_resource_name(sender, instance, created=False, **kwargs):

def update_project_name(sender, instance, created=False, **kwargs):
if not created and instance.tracker.has_changed('name'):
backend = KillBillBackend()
for model in PaidResource.get_all_models():
for resource in model.objects.exclude(billing_backend_id=None).filter(project=instance):
try:
backend.update_subscription_fields(
resource.billing_backend_id, project_name=resource.project.full_name)
except KillBillError as e:
logger.error(
"Failed to update project name in KillBill for resource %s: %s",
resource, e)
for model in paid_models:
update_subscription_fields(
model,
queryargs={'project': instance},
fields={'project_name': 'project__full_name'})


def update_project_group_name(sender, instance, created=False, **kwargs):
if not created and instance.tracker.has_changed('name'):
for model in paid_models:
update_subscription_fields(
model,
queryargs={'project__project_groups': instance},
fields={'project_name': 'project__full_name'})


def update_service_name(sender, instance, created=False, **kwargs):
if not created and instance.tracker.has_changed('name'):
resources = SupportedServices.get_related_models(instance)['resources']
for model in resources:
if model in paid_models:
update_subscription_fields(
model,
queryargs={'service_project_link__service': instance},
fields={'service_name': 'service_project_link__service__full_name'})


def update_service_settings_name(sender, instance, created=False, **kwargs):
if not created and instance.tracker.has_changed('name'):
resources = SupportedServices.get_related_models(instance)['resources']
for model in resources:
if model in paid_models:
update_subscription_fields(
model,
queryargs={'service_project_link__service__settings': instance},
fields={'service_name': 'service_project_link__service__full_name'})

backend = KillBillBackend()
for model in PaidResource.get_all_models():
for resource in model.objects.exclude(billing_backend_id=None).filter(project__project_groups=instance):
try:
backend.update_subscription_fields(
resource.billing_backend_id, project_name=resource.project.full_name)
except KillBillError as e:
logger.error(
"Failed to update project group name in KillBill for resource %s: %s",
resource, e)
4 changes: 2 additions & 2 deletions src/nodeconductor_killbill/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def generate_pdf(self, invoice):
projects = {}
for item in invoice['items']:
project = item['project']
resource = item['resource']
resource = '%s (%s)' % (item['resource'], item['service'])
projects.setdefault(project, {'items': {}, 'amount': 0})
projects[project]['amount'] += item['amount']
projects[project]['items'].setdefault(resource, 0)
Expand Down Expand Up @@ -138,7 +138,7 @@ def generate_usage_pdf(self, invoice):
item['usage'] = "{:.3f} {} x {:.3f} {}".format(
usage, unit, value, item['currency'])

resource = item['resource']
resource = '%s (%s)' % (item['resource'], item['service'])
resources.setdefault(resource, {'items': [], 'amount': 0})
resources[resource]['amount'] += item['amount']
resources[resource]['items'].append(item)
Expand Down
10 changes: 5 additions & 5 deletions src/nodeconductor_killbill/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from django.utils import timezone

from nodeconductor.cost_tracking import CostTrackingRegister
from nodeconductor.cost_tracking.models import DefaultPriceListItem
from nodeconductor.structure.models import Resource, PaidResource
from nodeconductor.cost_tracking.models import DefaultPriceListItem, PayableMixin
from nodeconductor.structure.models import Resource

from .backend import KillBillBackend, KillBillError

Expand All @@ -27,7 +27,7 @@ def sync_pricelist():
@shared_task(name='nodeconductor.killbill.sync_invoices')
def sync_invoices():
customers = set()
for model in PaidResource.get_all_models():
for model in PayableMixin.get_all_models():
for resource in model.objects.exclude(billing_backend_id=''):
customers.add(resource.customer)

Expand All @@ -52,8 +52,8 @@ def update_today_usage():
2015-08-20 13:00 support-basic 1
"""

for model in PaidResource.get_all_models():
for resource in model.objects.all():
for model in PayableMixin.get_all_models():
for resource in model.objects.exclude(state=model.States.ERRED):
update_today_usage_of_resource.delay(resource.to_string())


Expand Down

0 comments on commit ad68758

Please sign in to comment.