diff --git a/.travis.yml b/.travis.yml index ae1674f..41016ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,6 +22,7 @@ addons: - python-simplejson - python-serial - python-yaml + - unixodbc-dev env: global: diff --git a/clouder_metric/README.md b/clouder_metric/README.md new file mode 100644 index 0000000..04b16bf --- /dev/null +++ b/clouder_metric/README.md @@ -0,0 +1,28 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +============== +Clouder Metric +============== + +This module provides models to ingest and store usage metrics from ELK about + running instances. + +Installation +============ + +* Install module as normal + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. + +Contributors +------------ + +* Dave Lasley +* Ted Salmon diff --git a/clouder_metric/__init__.py b/clouder_metric/__init__.py new file mode 100644 index 0000000..04e65cf --- /dev/null +++ b/clouder_metric/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/clouder_metric/__manifest__.py b/clouder_metric/__manifest__.py new file mode 100644 index 0000000..3a6dfc0 --- /dev/null +++ b/clouder_metric/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Clouder - Metrics", + "summary": "Provides Usage Metric Interface for Clouder", + "version": "10.0.1.0.0", + "category": "Clouder", + "website": "https://github.com/clouder-community/clouder", + "author": "LasLabs", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + 'base_external_dbsource', + "clouder", + "contract_variable_quantity", + "sale", + ], + "data": [ + "security/ir.model.access.csv", + ], +} diff --git a/clouder_metric/models/__init__.py b/clouder_metric/models/__init__.py new file mode 100644 index 0000000..1b070df --- /dev/null +++ b/clouder_metric/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import clouder_metric_type +from . import clouder_metric_interface +from . import clouder_metric_value diff --git a/clouder_metric/models/clouder_metric_interface.py b/clouder_metric/models/clouder_metric_interface.py new file mode 100644 index 0000000..2e225fc --- /dev/null +++ b/clouder_metric/models/clouder_metric_interface.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ClouderMetricInterface(models.Model): + """ It provides a common interface for Clouder Usage metrics. + + This object receives all attributes from ``clouder.metric.type``. + """ + + _name = 'clouder.metric.interface' + _description = 'Clouder Metric Interfaces' + _inherits = {'clouder.metric.type': 'type_id'} + + type_id = fields.Many2one( + string='Metric Type', + comodel_name='clouder.metric.type', + required=True, + ondelete='restrict', + ) + metric_value_ids = fields.One2many( + string='Metric Values', + comodel_name='clouder.metric.value', + inverse_name='interface_id', + ) + metric_model = fields.Selection( + related='type_id.metric_model', + ) + metric_ref = fields.Integer( + required=True, + ) + source_id = fields.Many2one( + string='Metric Source', + comodel_name='base.external.dbsource', + domain="[('connector', '=', type_id.connector_type)]", + required=True, + ) + cron_id = fields.Many2one( + string='Scheduled Task', + comodel_name='ir.cron', + domain="[('model', '=', _name)]", + context="""{ + 'default_model': _name, + 'default_name': '[Clouder Metric] %s' % display_name, + }""", + ) + interval_number = fields.Integer( + related='cron_id.interval_number', + ) + interval_type = fields.Selection( + related='cron_id.interval_type', + ) + query_code = fields.Text() + + @property + @api.multi + def metric_id(self): + self.ensure_one() + return self.env[self.metric_model].browse( + self.metric_ref, + ) + + @api.multi + def name_get(self): + return [ + (r.id, '%s - %s' % (r.type_id.name, r.metric_id.id)) for r in self + ] diff --git a/clouder_metric/models/clouder_metric_type.py b/clouder_metric/models/clouder_metric_type.py new file mode 100644 index 0000000..dd0805d --- /dev/null +++ b/clouder_metric/models/clouder_metric_type.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.tools import safe_eval +from odoo.exceptions import ValidationError, UserError + + +class ClouderMetricType(models.Model): + """ It provides context for usage metric types """ + + _name = 'clouder.metric.type' + _description = 'Clouder Metric Types' + + name = fields.Char() + code = fields.Char() + metric_model = fields.Selection( + selection=lambda s: s._get_metric_models(), + required=True, + help='Clouder entity type that this metric is related to.', + ) + uom_id = fields.Many2one( + string='Unit of Measure', + comodel_name='product.uom', + required=True, + ) + connector_type = fields.Selection( + selection=lambda s: s.env['base.external.dbsource'].CONNECTORS, + ) + metric_code = fields.Text( + required=True, + default=lambda s: s._default_query_code(), + help='Python code to use as query for metric.' + ) + + @api.model + def _default_query_code(self): + return _("# Python code. \n" + "Use `value = my_value` to specify the final calculated " + " metric value. This is required. \n" + "Optionally use ``uom = product_uom_record`` to change the " + "units that the metric is being measured in. \n" + "You should also add `date_start` and `date_end`, which " + "are `datetime` values to signify the date of occurrence of " + "the metric value in question. \n" + "# You can use the following variables: \n" + "# - self: browse_record of the current ID Category \n" + "# - interface: browse_record of the Metrics Interface. \n" + "# - metric_model: Name of the metric model type. \n") + + @api.model + def _get_metric_models(self): + """ Returns a selection of available metric models + Returns: + list: Additional metric models + """ + return [ + ('clouder.base', 'Base'), + ('clouder.service', 'Service'), + ] + + @api.multi + def _get_query_code_context(self, interface): + """ Returns a query context for use + Args: + interface (clouder.metric.interface): The interface to use + Returns: + dict: Dict with the context for the given iface and model + """ + self.ensure_one() + return { + 'interface': interface, + 'metric_model': self.metric_model, + 'self': self, + } + + @api.model + def save_metric_value(self, metric_interfaces): + """ Saves a metric value from the given interface + Args: + metric_interfaces (clouder.metric.interface): The interface to use + Returns: + None + """ + for iface in metric_interfaces: + eval_context = iface.type_id._get_query_code_context(iface) + try: + safe_eval( + iface.query_code, + eval_context, + mode='exec', + nocopy=True, + ) + except Exception as e: + raise UserError(_( + 'Error while evaluating metrics query:' + '\n %s \n %s' % (iface.name, e), + )) + if eval_context.get('value') is None: + raise ValidationError(_( + 'Metrics query did not set the `value` variable, which ' + 'is used to indicate the value that should be saved for ' + 'the query.', + )) + uom = eval_context.get('uom') or iface.uom_id + iface.write({ + 'metric_value_ids': [(0, 0, { + 'value': eval_context['value'], + 'date_start': eval_context.get('date_start'), + 'date_end': eval_context.get('date_end'), + 'uom_id': uom.id, + })], + }) diff --git a/clouder_metric/models/clouder_metric_value.py b/clouder_metric/models/clouder_metric_value.py new file mode 100644 index 0000000..133fc72 --- /dev/null +++ b/clouder_metric/models/clouder_metric_value.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ClouderMetricValue(models.Model): + """ It provides a record of metric values used in billing. """ + + _name = 'clouder.metric.value' + _description = 'Clouder Metric Values' + + interface_id = fields.Many2one( + string='Interface', + comodel_name='clouder.metric.interface', + required=True, + ) + value = fields.Float( + required=True, + ) + uom_id = fields.Many2one( + string='Unit of Measure', + comodel_name='product.uom', + ) + date_start = fields.Datetime( + string='Metric Start', + ) + date_end = fields.Datetime( + string='Metric End', + ) + date_create = fields.Datetime( + string='Creation Time', + default=lambda s: fields.Datetime.now(), + ) diff --git a/clouder_metric/security/ir.model.access.csv b/clouder_metric/security/ir.model.access.csv new file mode 100644 index 0000000..f091693 --- /dev/null +++ b/clouder_metric/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_clouder_metric_type,access_clouder_metric_type,model_clouder_metric_type,clouder.group_clouder_user,1,0,0,0 +access_clouder_metric_interface,access_clouder_metric_interface,model_clouder_metric_interface,clouder.group_clouder_user,1,0,0,0 +access_clouder_metric_value,access_clouder_metric_value,model_clouder_metric_value,clouder.group_clouder_user,1,0,0,0 diff --git a/clouder_metric/tests/__init__.py b/clouder_metric/tests/__init__.py new file mode 100644 index 0000000..1611e3f --- /dev/null +++ b/clouder_metric/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_clouder_metric_interface +from . import test_clouder_metric_type diff --git a/clouder_metric/tests/common.py b/clouder_metric/tests/common.py new file mode 100644 index 0000000..407a58b --- /dev/null +++ b/clouder_metric/tests/common.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestCommon(TransactionCase): + + def setUp(self): + super(TestCommon, self).setUp() + self.metric_type = self.env['clouder.metric.type'].create({ + 'name': 'Test Metric', + 'code': 'TEST', + 'metric_model': 'clouder.base', + 'uom_id': self.env.ref('product.uom_categ_wtime').id, + }) + self.metric_interface = self.env['clouder.metric.interface'].create({ + 'type_id': self.metric_type.id, + 'metric_ref': 7, + 'source_id': self.env.ref( + 'base_external_dbsource.demo_postgre').id, + 'query_code': 'print True', + }) diff --git a/clouder_metric/tests/test_clouder_metric_interface.py b/clouder_metric/tests/test_clouder_metric_interface.py new file mode 100644 index 0000000..4f06017 --- /dev/null +++ b/clouder_metric/tests/test_clouder_metric_interface.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from .common import TestCommon + + +class TestClouderMetricInterface(TestCommon): + + def test_metric_id(self): + """ It should test to see that at least one metric_id is returned """ + self.assertTrue(len(self.metric_interface.metric_id) == 1) + + def test_name_get(self): + """ It should return the right name """ + exp = [ + (self.metric_interface.id, 'Test Metric - 7') + ] + self.assertEqual(exp, self.metric_interface.name_get()) diff --git a/clouder_metric/tests/test_clouder_metric_type.py b/clouder_metric/tests/test_clouder_metric_type.py new file mode 100644 index 0000000..a93ce3b --- /dev/null +++ b/clouder_metric/tests/test_clouder_metric_type.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.exceptions import UserError, ValidationError + +from .common import TestCommon + + +class TestClouderMetricType(TestCommon): + + def test_get_metric_models(self): + """ It should have the correct metric model types """ + exp = [ + ('clouder.base', 'Base'), + ('clouder.service', 'Service'), + ] + self.assertEquals(exp, self.metric_type._get_metric_models()) + + def test_save_metric_value_usererror(self): + """ It should raise UserError when a bad query is supplied """ + with self.assertRaises(UserError): + self.metric_type.save_metric_value(self.metric_interface) + + def test_save_metric_value_validationerror(self): + """ It should raise ValidationError when no value is supplied """ + self.metric_interface.query_code = 'test = 0' + with self.assertRaises(ValidationError): + self.metric_type.save_metric_value(self.metric_interface) + + def test_save_metric_value(self): + """ It should verify that the right metric values are saved """ + self.metric_interface.query_code = 'value = 100' + self.metric_type.save_metric_value(self.metric_interface) + self.assertTrue( + self.metric_interface.metric_value_ids.mapped('value') == [100.0] + ) diff --git a/oca_dependencies.txt b/oca_dependencies.txt index 5428a2d..83d9107 100644 --- a/oca_dependencies.txt +++ b/oca_dependencies.txt @@ -1 +1,4 @@ connector +contract +# Pending https://github.com/OCA/server-tools/pull/660 +server-tools https://github.com/laslabs/server-tools feature/10.0/base_external_dbsource_elasticsearch-v10 diff --git a/sale_clouder/README.md b/sale_clouder/README.md new file mode 100644 index 0000000..70647a9 --- /dev/null +++ b/sale_clouder/README.md @@ -0,0 +1,28 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +============ +Sale Clouder +============ + +This module extends contracts to provide Clouder-specific functionality such + as allowing usage and threshold based contracts. + +Installation +============ + +* Install module as normal + + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. + +Contributors +------------ + +* Dave Lasley +* Ted Salmon diff --git a/sale_clouder/__init__.py b/sale_clouder/__init__.py new file mode 100644 index 0000000..04e65cf --- /dev/null +++ b/sale_clouder/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import models diff --git a/sale_clouder/__manifest__.py b/sale_clouder/__manifest__.py new file mode 100644 index 0000000..ef06210 --- /dev/null +++ b/sale_clouder/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "Sale - Clouder", + "summary": "Provides the ability to sell Clouder instances.", + "version": "10.0.1.0.0", + "category": "Clouder", + "website": "https://github.com/clouder-community/clouder", + "author": "LasLabs", + "license": "LGPL-3", + "application": False, + "installable": True, + "depends": [ + "clouder", + "contract", + "sale", + "clouder_metric", + ], + "data": [ + "data/sale_clouder.xml", # Must be created before formula + "data/contract_line_qty_formula.xml", + "security/ir.model.access.csv", + ], +} diff --git a/sale_clouder/data/contract_line_qty_formula.xml b/sale_clouder/data/contract_line_qty_formula.xml new file mode 100644 index 0000000..82df7dd --- /dev/null +++ b/sale_clouder/data/contract_line_qty_formula.xml @@ -0,0 +1,14 @@ + + + + + + + Clouder Billing + result = env['clouder.contract'].get_invoice_line_quantity(contract, line, invoice) + + + diff --git a/sale_clouder/data/sale_clouder.xml b/sale_clouder/data/sale_clouder.xml new file mode 100644 index 0000000..aaefd26 --- /dev/null +++ b/sale_clouder/data/sale_clouder.xml @@ -0,0 +1,14 @@ + + + + + + + Clouder Contract + clouder.contract + CLOUD + 8 + + + diff --git a/sale_clouder/models/__init__.py b/sale_clouder/models/__init__.py new file mode 100644 index 0000000..25040b4 --- /dev/null +++ b/sale_clouder/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import clouder_contract +from . import clouder_contract_line +from . import product_template diff --git a/sale_clouder/models/clouder_contract.py b/sale_clouder/models/clouder_contract.py new file mode 100644 index 0000000..e421071 --- /dev/null +++ b/sale_clouder/models/clouder_contract.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import api, fields, models + + +_logger = logging.getLogger(__name__) + + +class ClouderContract(models.Model): + """ It provides formulas specific to billing Clouder contracts. """ + + _name = 'clouder.contract' + _description = 'Clouder Contracts' + _inherits = {'account.analytic.account': 'ref_contract_id'} + + ref_contract_id = fields.Many2one( + string='Related Contract', + comodel_name='account.analytic.account', + index=True, + require=True, + ondelete='cascade', + ) + + _sql_constraints = [ + ('ref_contract_id_unique', 'UNIQUE(ref_contract_id)', + 'Cannot assign two ClouderContracts to the same Analytic Account.'), + ] + + @property + @api.model + def invoice_policy_map(self): + """ It returns a mapping of invoice policies to processing methods. + + Returns: + dict: Mapping keyed by invoice policy type, pointing to the + method that should determine the quantity to use for + invoicing. + The method will receive the recurring contract line and the + invoice as arguments. + See one of the methods that are already mapped for examples. + """ + return { + 'threshold': self._get_quantity_threshold, + 'usage': self._get_quantity_usage, + 'cost': self._get_quantity_usage, + 'order': self._get_quantity_flat, + 'delivery': self._get_quantity_usage, + } + + @api.model + def get_invoice_line_quantity(self, account, account_line, invoice): + """ It returns the Qty to be used for contract billing formula. + + This method should be called from ``contract.line.qty.formula`` by + adding the following into the ``code`` field: + + .. code-block:: python + + result = env['clouder.contract'].get_invoice_line_quantity( + contract, line, invoice, + ) + + Args: + account (AccountAnalyticAccount): Contract that recurring + invoice line belongs to. This is called + account_line (AccountAnalyticInvoiceLine): Recurring invoice + line being referenced. + invoice (AccountInvoice): Invoice that is being created. + Returns: + int: Quantity to use on invoice line in the UOM defined on the + ``contract_line``. + """ + invoice_policy = account_line.product_id.invoice_policy + invoice_policy_map = self.invoice_policy_map + try: + method = invoice_policy_map[invoice_policy] + except KeyError: + _logger.info( + 'No calculation method found for invoice policy "%s". ' + 'Defaulting to Flat Rate instead.', invoice_policy + ) + method = invoice_policy_map['order'] + return method(account_line, invoice) + + @api.model + def _create_default_vals(self, account): + """ It returns default values to create and link new ClouderContracts. + + Args: + account (AccountAnalyticAccount): Account that ClouderContract + will reference. + Returns: + dict: Values fed to ``create`` in ``_get_contract_by_account``. + """ + number = self.env['ir.sequence'].next_by_code('clouder.contract') + # Default to the current users company if not set + company_id = (account.company_id and account.company_id.id or + self.env.user.company_id.id) + return { + 'name': number, + 'company_id': company_id, + 'ref_contract_id': account.id, + } + + @api.model + def _get_contract_by_account(self, account, create=False): + """ It returns the ClouderContract or possibly creates a new one. + + Args: + account: (AccountAnalyticAccount) Contract to search by. + create: (bool) True will create a new ClouderContract if one does + not already exist. + Returns: + clouder.contract: Clouder contract associated with ``account``. + """ + contract = self.search([('ref_contract_id', '=', account.id)]) + if create and not contract: + contract = self.create(self._create_default_vals(account)) + return contract + + @api.multi + def _get_quantity_flat(self, account_line, invoice): + """ It returns the base quantity with no calculations + Args: + account_line (AccountAnalyticInvoiceLine): Recurring invoice + line being referenced. + invoice (AccountInvoice): Invoice that is being created. + Returns: + float: Quantity with no calculations performed + """ + return account_line.quantity + + @api.multi + def _get_quantity_threshold(self, account_line, invoice): + """ It functions like flat rate for the most part + + Args: + account_line (AccountAnalyticInvoiceLine): Recurring invoice + line being referenced. + invoice (AccountInvoice): Invoice that is being created. + Returns: + float: Quantity to use on invoice line in the UOM defined on the + ``contract_line``. + """ + return account_line.quantity + + @api.multi + def _get_quantity_usage(self, account_line, invoice): + """ It provides a quantity based on unbilled and used metrics + Args: + account_line (AccountAnalyticInvoiceLine): Recurring invoice + line being referenced. + invoice (AccountInvoice): Invoice that is being created. + Returns: + float: Quantity to use on invoice line in the UOM defined on the + ``contract_line``. + """ + vals = account_line.metric_interface_id.metric_value_ids + inv_date = fields.Datetime.from_string(invoice.date_invoice) + inv_delta = self.ref_contract_id.get_relative_delta( + self.recurring_rule_type, self.recurring_interval + ) + start_date = inv_date - inv_delta + + # Filter out metrics that fall within the billing period + def filter(rec): + if not all((rec.date_start, rec.date_end)): + return False + start = fields.Datetime.from_string(rec.date_start) + end = fields.Datetime.from_string(rec.date_end) + return start >= start_date and end <= inv_date + + return sum(vals.filtered(filter).mapped('value')) or 0.0 diff --git a/sale_clouder/models/clouder_contract_line.py b/sale_clouder/models/clouder_contract_line.py new file mode 100644 index 0000000..d06b71a --- /dev/null +++ b/sale_clouder/models/clouder_contract_line.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ClouderContractLine(models.Model): + """ It provides the link between billing and Clouder Services. """ + + _name = 'clouder.contract.line' + _description = 'Clouder Contract Lines' + _inherits = {'account.analytic.invoice.line': 'contract_line_id'} + + contract_line_id = fields.Many2one( + string='Recurring Line', + comodel_name='account.analytic.invoice.line', + index=True, + required=True, + ondelete='restrict', + ) + metric_interface_id = fields.Many2one( + string='Metric Interface', + comodel_name='clouder.metric.interface', + required=True, + ) diff --git a/sale_clouder/models/product_template.py b/sale_clouder/models/product_template.py new file mode 100644 index 0000000..3130ad6 --- /dev/null +++ b/sale_clouder/models/product_template.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + invoice_policy = fields.Selection( + selection_add=[ + ('threshold', 'Invoice and Enforce a Threshold'), + ('usage', 'Invoice Based on Usage'), + ], + ) diff --git a/sale_clouder/security/ir.model.access.csv b/sale_clouder/security/ir.model.access.csv new file mode 100644 index 0000000..df8be25 --- /dev/null +++ b/sale_clouder/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_clouder_contract,access_clouder_contract,model_clouder_contract,clouder.group_clouder_user,1,1,1,1 +access_clouder_contract_line,access_clouder_contract_line,model_clouder_contract_line,clouder.group_clouder_user,1,1,1,1 diff --git a/sale_clouder/tests/__init__.py b/sale_clouder/tests/__init__.py new file mode 100644 index 0000000..66f9c8b --- /dev/null +++ b/sale_clouder/tests/__init__.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_clouder_contract +from . import test_product_template diff --git a/sale_clouder/tests/test_clouder_contract.py b/sale_clouder/tests/test_clouder_contract.py new file mode 100644 index 0000000..c4d867b --- /dev/null +++ b/sale_clouder/tests/test_clouder_contract.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestClouderContract(TransactionCase): + + def _scaffold_test(self, billing_type): + """ It creates a contract with usage based on the ``billing_type`` + Args: + billing_type (str): 'usage', 'threshold', 'order' + Returns: + None + """ + self.partner = self.env.ref('base.res_partner_2') + self.product = self.env.ref('product.product_product_2') + self.product.taxes_id += self.env['account.tax'].search( + [('type_tax_use', '=', 'sale')], limit=1) + self.product.description_sale = 'Test Description' + self.product.invoice_policy = billing_type + self.metric_type = self.env['clouder.metric.type'].create({ + 'name': 'Test Metric', + 'code': 'TEST', + 'metric_model': 'clouder.base', + 'uom_id': self.env.ref('product.uom_categ_wtime').id, + }) + self.metric_interface = self.env['clouder.metric.interface'].create({ + 'type_id': self.metric_type.id, + 'metric_ref': 7, + 'source_id': self.env.ref( + 'base_external_dbsource.demo_postgre').id, + 'query_code': "value = 100\n" + "date_start = '2017-01-02'\n" + "date_end = '2016-01-03'\n", + }) + self.contract = self.env['clouder.contract'].create({ + 'name': 'Test Contract', + 'partner_id': self.partner.id, + 'pricelist_id': self.partner.property_product_pricelist.id, + 'recurring_invoices': True, + 'date_start': '2017-01-01', + 'recurring_next_date': '2017-02-01', + 'recurring_rule_type': 'monthly', + 'recurring_interval': 1, + }) + self.contract_line = self.env['clouder.contract.line'].create({ + 'analytic_account_id': self.contract.ref_contract_id.id, + 'product_id': self.product.id, + 'name': 'Clouder Instance Test', + 'quantity': 1, + 'uom_id': self.product.uom_id.id, + 'price_unit': 100, + 'discount': 50, + 'metric_interface_id': self.metric_interface.id, + }) + + def test_invoice_policy_map(self): + """ It should contain the expected keys """ + self._scaffold_test('usage') + policy_map = self.contract.invoice_policy_map + exp = ('threshold', 'usage', 'cost', 'order', 'delivery') + for k in exp: + self.assertTrue(k in policy_map) + + def test_get_invoice_line_quantity_usage_based(self): + """ It should return the expected usage quantity """ + self._scaffold_test('usage') + self.metric_type.save_metric_value(self.metric_interface) + invoice = self.contract.ref_contract_id._create_invoice() + usage = self.contract.get_invoice_line_quantity( + self.contract.ref_contract_id, self.contract_line, invoice + ) + self.assertTrue(usage == 100.0) + + def test_get_invoice_line_quantity_usage_based_no_metric_dates(self): + """ It should return the expected usage quantity + but not have metrics with dates, so as to trigger a conditional """ + self._scaffold_test('usage') + self.metric_interface.query_code = 'value = 100' + self.metric_type.save_metric_value(self.metric_interface) + invoice = self.contract.ref_contract_id._create_invoice() + usage = self.contract.get_invoice_line_quantity( + self.contract.ref_contract_id, self.contract_line, invoice + ) + self.assertTrue(usage == 0.0) + + def test_get_invoice_line_quantity_threshold_based(self): + """ It should return the expected threshold quantity """ + self._scaffold_test('threshold') + invoice = self.contract.ref_contract_id._create_invoice() + usage = self.contract.get_invoice_line_quantity( + self.contract.ref_contract_id, self.contract_line, invoice + ) + self.assertTrue(usage == 1.0) + + def test_get_invoice_line_quantity_flat_fee_based(self): + """ It should return the expected flat fee quantity """ + self._scaffold_test('order') + invoice = self.contract.ref_contract_id._create_invoice() + usage = self.contract.get_invoice_line_quantity( + self.contract.ref_contract_id, self.contract_line, invoice + ) + self.assertTrue(usage == 1.0) + + def test_get_contract_by_account(self): + """ It should return the existing contract """ + self._scaffold_test('order') + contract = self.env['clouder.contract']._get_contract_by_account( + self.contract.ref_contract_id + ) + self.assertEquals(contract.id, self.contract.id) + + def test_get_contract_by_account_new(self): + """ It should create a new contract based on the account """ + self._scaffold_test('order') + account = self.env['account.analytic.account'].create({ + 'name': 'Test Contract', + 'partner_id': self.partner.id, + 'pricelist_id': self.partner.property_product_pricelist.id, + 'recurring_invoices': True, + 'date_start': '2017-01-01', + 'recurring_next_date': '2017-02-01', + 'recurring_rule_type': 'monthly', + 'recurring_interval': 1, + }) + contract = self.env['clouder.contract']._get_contract_by_account( + account, True + ) + self.assertTrue('CLOUD' in contract.name) + self.assertEquals( + contract.company_id.id, self.env.user.company_id.id + ) diff --git a/sale_clouder/tests/test_product_template.py b/sale_clouder/tests/test_product_template.py new file mode 100644 index 0000000..c47ae36 --- /dev/null +++ b/sale_clouder/tests/test_product_template.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 LasLabs Inc. +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tests.common import TransactionCase + + +class TestProductTemplate(TransactionCase): + + def test_invoice_policy(self): + """ It should ensure the right options exist for invoice policy """ + policy = self.env['product.template']._fields['invoice_policy'] + exp = ('usage', 'threshold') + res = [] + for item in policy.selection: + res.append(item[0]) + for e in exp: + self.assertTrue(e in res)