diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..96e08a3 --- /dev/null +++ b/.flake8 @@ -0,0 +1,6 @@ +[flake8] +max-line-length = 88 +extend-select = B950 +extend-ignore = E203, E501, E701 +per-file-ignores = + __manifest__.py:B018 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5d60408 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,21 @@ +name: test + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + pre-commit: + runs-on: ubuntu-22.04 + steps: + - name: Checkout branch + uses: actions/checkout@v4 + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.8" + - name: Run pre-commit hooks + uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c33ab45 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,30 @@ +--- + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks.git + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: mixed-line-ending + - id: end-of-file-fixer + - id: detect-private-key + - id: check-added-large-files + - id: check-merge-conflict + - repo: https://github.com/crate-ci/typos.git + rev: v1.21.0 + hooks: + - id: typos + - repo: https://github.com/psf/black.git + rev: 24.4.2 + hooks: + - id: black + - repo: https://github.com/PyCQA/flake8.git + rev: 7.0.0 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear==24.4.26 + - repo: https://github.com/adrienverge/yamllint.git + rev: v1.35.1 + hooks: + - id: yamllint diff --git a/README.md b/README.md new file mode 100755 index 0000000..e9499c9 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# OpenStack Integration Addon for Odoo v13 + +This is an addon to facilitate the integration between OpenStack using Adjutant and Distil with Odoo v13. + +These are models and changes based on the Catalystcloud use cases, and based on how we invoice, but likely applicable in some form for other clouds running OpenStack. + +## Altered Odoo Models + +### Partner + +We add quite a few custom fields here that link back to our models elsewhere. We also add the field `stripe_customer_id` to optional link to a customer in Stripe itself for credit card related things. + +### Sale Order + +The main change to Sale Order is the inclusion of `os_project` on both the Sale Order itself, and the lines. The idea here being that `os_project` on the Sale Order represents the root project for the given project group which the invoice is ultimately for, while `os_project` on each line allows each line to potentially come from a different project in that subgroup in the case of invoice grouping. We also add overrides for `_prepare_invoice` and `_prepare_invoice_line` to handle these fields. + +**NOTE**: We also add `os_invoice_date` to deal with a particularly annoying issue around sale order date and how it is handled. But this will be moved to a standalone addon and renamed to `invoice_date` later. + +### Account Move + +Much like Sale Order, we add `os_project` to both the Account Move (invoice) and Account Move Lines. This is for the same reasons/functionality as in Sale Order. + +We also have the function `is_openstack_invoice`, which works off is `os_project` is set or not. + +Then there is a custom categorisation function `categorised_openstack_invoice_lines` which sorts things in a way more useful for the PDF summary page. + +## Custom Models + +The bulk of this addon is custom addons, many of which don't have much logic built in on the odoo side, and most of that logic (for now) is primarily handled external to Odoo in Adjutant and Distil (including the Distil Odoo Writer). + +### Project + +The core of this addon is built mostly around the OpenStack concept of a project, and this model is meant to represent that Odoo side. A project can only have one `owner` which is the customer partner, but then through an intermediary model called `project_contacts` has relationships to other partners such as billing contacts, etc. + +Many other models link back to projects, as they are ultimately the main connection between a customer, and their OpenStack side usage. + +### Customer Group + +A customer group is a grouping of customers for the purposes of special discounting. The customer group is mostly for special case handling of volume discounts. + +While not implemented yet, a group can also be given a pricelist which would then apply to all customers in the group. + +### Voucher Code (Not used yet) + +These are unused as yet, but their intended function is to allow us to create codes that can be added to an account during signup, or later, which will confer credit, or discounts as tired to the code. They would then turn into a Credit, Grant, or add a customer to a customer group. Their purpose was to be useful for a range of things. + +### Credit + +Credits are a model representation of credit that an OpenStack Customer has against their invoices. The balance is made up from credit transactions that are attached to the model, which gives us a history of the use of that credit. + +Credits by default are not refundable, but for the purposes of prepaid or SLA credit they may have to be, and the credit_type model lets you define if a given type of credit has to be able to be refunded. + +### Grant + +A grant is like credit, except unlike credits, it doesn't have a balance, just a max value that can be applied to an invoice each month. It is reoccurring credit essentially. + +A grant type has a special case field `only_on_group_root` that handles a case where this grant cannot be applied to any project that isn't the root of a invoice project group. This is because when grouping invoices credits/grants are applied to the whole group, and something like a dev grant should never be applied to parent projects, so a dev grant can only affect the project it is applied to or sub projects. + +### Volume Discount + +These are ranged of volume discount, and they are used as part of invoicing. A customer group may have it's own unique volume discounts, but by default a customer without a customer group will use the volume discounts with a group set. + +### Term Discount + +A model representing an agreed to term discount. + +### Support Subscription (Not used, yet) + +A model (and type) to represent a support subscription that a customer has signed up for of a given type. + +### Reseller + +We have a model for representing a reseller on our cloud, as well has what tier that reseller is at. This handles their discounts, as well as other special cases. A project is optionally linked back to a reseller. + +### Trial (Not used) + +A model representing a trial signup for OpenStack, and the status of that. + +### Referral (Not used) + +A basic referral model that would allow customers who provide referrals to earn some credit themselves. diff --git a/__init__.py b/__init__.py new file mode 100755 index 0000000..de694df --- /dev/null +++ b/__init__.py @@ -0,0 +1,20 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import controllers +from . import models +from . import report + +__all__ = ["controllers", "models", "report"] diff --git a/__manifest__.py b/__manifest__.py new file mode 100755 index 0000000..18be504 --- /dev/null +++ b/__manifest__.py @@ -0,0 +1,55 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +{ + "name": "OpenStack Integration", + "category": "Tools", + "summary": """OpenStack integration for the Odoo ERP.""", + "version": "14.0.1.0.0", + "author": "Catalyst Cloud", + "website": "https://catalystcloud.nz", + "license": "Other OSI approved licence", + "depends": [ + "account", + "base", + "product", + "report_csv", + "sale", + ], + "data": [ + "report/openstack_invoice_csv.xml", + "security/openstack_security.xml", + "security/ir.model.access.csv", + "views/main_menu.xml", + "views/account_move_view.xml", + "views/credit_view.xml", + "views/customer_group_view.xml", + "views/grant_view.xml", + "views/project_view.xml", + "views/referral_view.xml", + "views/report_invoice.xml", + "views/reseller_view.xml", + "views/res_partner_view.xml", + "views/sale_order_view.xml", + "views/support_subscription_view.xml", + "views/term_discount_view.xml", + "views/trial_view.xml", + "views/volume_discount_view.xml", + "views/voucher_code_view.xml", + # load mail template after report_invoice.xml which it refers to + "data/mail_template_data.xml", + ], + "installable": True, +} diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000..02d3ec1 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,2 @@ +[default.extend-words] +datas = "datas" diff --git a/controllers/__init__.py b/controllers/__init__.py new file mode 100755 index 0000000..5dbc015 --- /dev/null +++ b/controllers/__init__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import main + +__all__ = ["main"] diff --git a/controllers/main.py b/controllers/main.py new file mode 100755 index 0000000..9f85f7f --- /dev/null +++ b/controllers/main.py @@ -0,0 +1,71 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json + +from odoo.http import content_disposition, request, route +from odoo.tools.safe_eval import safe_eval, time + +from odoo.addons.report_csv.controllers import main as report + + +class ReportController(report.ReportController): + """report_csv.ReportController that fixes a mysterious permission problem""" + + @route() + def report_routes(self, reportname, docids=None, converter=None, **data): + if converter == "csv": + report = request.env["ir.actions.report"]._get_report_from_name(reportname) + context = dict(request.env.context) + if docids: + docids = [int(i) for i in docids.split(",")] + if data.get("options"): + data.update(json.loads(data.pop("options"))) + if data.get("context"): + # Ignore 'lang' here, because the context in data is the one + # from the webclient *but* if the user explicitly wants to + # change the lang, this mechanism overwrites it. + data["context"] = json.loads(data["context"]) + if data["context"].get("lang"): + del data["context"]["lang"] + context.update(data["context"]) + # TODO: Investigate why we need to insert sudo() here + csv = report.sudo().with_context(context)._render_csv(docids, data=data)[0] + filename = "{}.{}".format(report.name, "csv") + if docids: + obj = request.env[report.model].browse(docids) + if report.print_report_name and not len(obj) > 1: + report_name = safe_eval( + report.print_report_name, + {"object": obj, "time": time, "multi": False}, + ) + filename = "{}.{}".format(report_name, "csv") + # When we print multiple records we still allow a custom + # filename. + elif report.print_report_name and len(obj) > 1: + report_name = safe_eval( + report.print_report_name, + {"objects": obj, "time": time, "multi": True}, + ) + filename = "{}.{}".format(report_name, "csv") + csvhttpheaders = [ + ("Content-Type", "text/csv"), + ("Content-Length", len(csv)), + ("Content-Disposition", content_disposition(filename)), + ] + return request.make_response(csv, headers=csvhttpheaders) + return super(ReportController, self).report_routes( + reportname, docids, converter, **data + ) diff --git a/data/mail_template_data.xml b/data/mail_template_data.xml new file mode 100755 index 0000000..37417b2 --- /dev/null +++ b/data/mail_template_data.xml @@ -0,0 +1,839 @@ + + + + + + + + Invoice: Send by email (OpenStack with CSV) + + ${(object.invoice_user_id.email_formatted or user.email_formatted) |safe} + ${object.get_email_to()} + ${ctx.subject} + + + % if ctx.email_type == 'credit_card': + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + +
+
Thank you for using our cloud
+
+
The invoice for ${ctx.bill_period} has been processed, and the balance was automatically deducted from your credit card.
+
+
A detailed breakdown of your usage is on the attached invoice.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + % if ctx.project: + + + + + % endif + +
+
Customer:
+
+
${object.partner_id.name}
+
+
Invoice ID:
+
+
${object.name}
+
+
Project name:
+
+
${ctx.project.name}
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + +
Resource usage during ${ctx.bill_period}$${"%.2f" % object.amount_untaxed}
Tax (GST 15%)$${"%.2f" % object.amount_tax}
Subtotal$${"%.2f" % object.amount_total}
Amount charged$${"%.2f" % object.amount_total}
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
OpenStack
+
+
+ +
+
+ +
+ + + % elif ctx.email_type == 'summary': + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + +
+
Thank you for using our cloud
+
+
A summary of your resource usage for ${ctx.bill_period} has been processed.
+
+
It appears that you have usage, but your project credits or discounts have covered your costs. Attached is a detailed summary of your usage.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + % if ctx.project: + + + + + % endif + +
+
Customer:
+
+
${object.partner_id.name}
+
+
Invoice ID:
+
+
${object.name}
+
+
Project name:
+
+
${ctx.project.name}
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + +
Resource usage during ${ctx.bill_period}$${"%.2f" % ctx.usage_total}
Free Resources$${"%.2f" % ctx.free_resource_total}
Discount and credits$${"%.2f" % ctx.discount_total}
Amount due$${"%.2f" % object.amount_total}
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
OpenStack
+
+
+ +
+
+ +
+ + + + % else: + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ + + +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + +
+
Thank you for using our cloud
+
+
The invoice for ${ctx.bill_period} has been processed, and is now due for payment.
+
+
A detailed breakdown of your usage is on the attached invoice, with remittance advice included.
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + % if ctx.project: + + + + + % endif + +
+
Customer:
+
+
${object.partner_id.name}
+
+
Invoice ID:
+
+
${object.name}
+
+
Project name:
+
+
${ctx.project.name}
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + +
Resource usage during ${ctx.bill_period}$${"%.2f" % object.amount_untaxed}
Tax (GST 15%)$${"%.2f" % object.amount_tax}
Subtotal$${"%.2f" % object.amount_total}
Amount due$${"%.2f" % object.amount_total}
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
OpenStack
+
+
+ +
+
+ +
+ + + % endif + + + + +
+ + Invoice_${(object.name or '').replace('/','_')}${object.state == 'draft' and '_draft' or ''} + ${object.partner_id.lang} + +
+
+
diff --git a/models/__init__.py b/models/__init__.py new file mode 100755 index 0000000..638a314 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,46 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import account_move +from . import credit +from . import customer_group +from . import grant +from . import project +from . import referral +from . import reseller +from . import res_partner +from . import sale_order +from . import support_subscription +from . import term_discount +from . import trial +from . import volume_discount +from . import voucher_codes + +__all__ = [ + "account_move", + "credit", + "customer_group", + "grant", + "project", + "referral", + "reseller", + "res_partner", + "sale_order", + "support_subscription", + "term_discount", + "trial", + "volume_discount", + "voucher_codes", +] diff --git a/models/account_move.py b/models/account_move.py new file mode 100755 index 0000000..18cf070 --- /dev/null +++ b/models/account_move.py @@ -0,0 +1,280 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import base64 +import datetime + +from odoo import fields, models + + +def credit_and_debit(input, old_credit=0, old_debit=0): + if input < 0: + return old_credit + input, old_debit + return old_credit, old_debit + input + + +class InvoiceRegionCategory: + def __init__(self): + self.lines = [] + self.count = 0 + self.credit = 0 + self.debit = 0 + self.name = "" + + def total(self): + return self.credit + self.debit + + def add_region(self, line): + self.name = line.product_id.categ_id.parent_id["name"] + self.credit, self.debit = credit_and_debit( + line.price_subtotal, self.credit, self.debit + ) + self.count += 1 + + self.lines.append(line) + + +class InvoiceProductCategory: + def __init__(self): + self.regions = {} + self.count = 0 + self.credit = 0 + self.debit = 0 + self.name = "" + self.uncategorised = [] + + def total(self): + return self.credit + self.debit + + def add_product(self, line): + product = line.product_id + self.credit, self.debit = credit_and_debit( + line.price_subtotal, self.credit, self.debit + ) + self.count += 1 + + if product and product.categ_id and product.categ_id.parent_id: + self.name = product.categ_id["name"] + region_name = product.categ_id.parent_id["name"] + try: + region_group = self.regions[region_name] + region_group.add_region(line) + except KeyError: + self.regions[region_name] = InvoiceRegionCategory() + self.regions[region_name].add_region(line) + else: + self.uncategorised.append(line) + + +class CategorisedInvoice: + def __init__(self): + self.products = {} + self.count = 0 + self.credit = 0 + self.debit = 0 + self.uncategorised = [] + + def total(self): + return self.credit + self.debit + + def add_line(self, line): + product = line.product_id + self.credit, self.debit = credit_and_debit( + line.price_subtotal, self.credit, self.debit + ) + self.count += 1 + + if product and product.categ_id: + product_name = product.categ_id["name"] + try: + product_group = self.products[product_name] + product_group.add_product(line) + except KeyError: + self.products[product_name] = InvoiceProductCategory() + self.products[product_name].add_product(line) + else: + self.uncategorised.append(line) + + +class OutstandingInvoices: + def __init__(self): + self.old_invoices = [] + self.current_invoices = [] + + def _reset(self): + # use instead of reinit OutStandingInvoices - report_invoice.xml template is + # already tracking this object. But if we re-run invoicing in quick + # succession without resetting these, the contents will be doubled up. + # It's probably cursed. + self.old_invoices = [] + self.current_invoices = [] + + def ordered_old_invoices(self): + return sorted(self.old_invoices, key=lambda x: x["invoice_date_due"]) + + def ordered_current_invoices(self): + return sorted(self.current_invoices, key=lambda x: x["invoice_date_due"]) + + def total_owing(self): + return self.old_owing() + self.current_owing() + + def old_owing(self): + sum = 0 + + for inv in self.old_invoices: + sum = sum + inv["amount_residual_signed"] + return sum + + def current_owing(self): + sum = 0 + for inv in self.current_invoices: + sum = sum + inv["amount_residual_signed"] + return sum + + def add(self, invoices): + for invoice in invoices: + # temp dict to prevent attempting deeper calls on the object when + # connection has closed just pulls the relevant values and stores them + # for recall later. + temp_invoice = { + "invoice_date_due": invoice.invoice_date_due, + "amount_residual_signed": invoice.amount_residual_signed, + "name": invoice.name, + } + + if datetime.date.today() > invoice.invoice_date_due: + self.old_invoices.append(temp_invoice) + else: + self.current_invoices.append(temp_invoice) + + +class AccountMove(models.Model): + _inherit = "account.move" + outstanding_invoices = OutstandingInvoices() + + os_project = fields.Many2one( + comodel_name="openstack.project", + string="OpenStack Project", + ) + + os_is_cloud_framework_agreement_invoice = fields.Boolean( + string="CFA invoice", + default=False, + required=False, + ) + + def is_openstack_invoice(self): + """Return whether this is an OpenStack invoice.""" + + return bool(self.os_project) + + def categorised_openstack_invoice_lines(self): + """Group invoice lines by project and categorise""" + + categorised_invoice = CategorisedInvoice() + + for line in self.invoice_line_ids: + categorised_invoice.add_line(line) + return categorised_invoice + + def get_outstanding_invoices(self, project_id, partner_id): + if not project_id: + return self.env["account.move"].search( + [ + ("amount_residual_signed", ">", 0), + ("partner_id.id", "=", partner_id["id"]), + ] + ) + + return self.env["account.move"].search( + [ + ("amount_residual_signed", ">", 0), + ("os_project.id", "=", project_id["id"]), + ] + ) + + def send_openstack_invoice_email(self, email_ctx=None): + # reset before, to ensure data is current + self.outstanding_invoices._reset() + try: + if email_ctx is None: + email_ctx = {} + template_id = self.env.ref( + "openstack_integration.email_template_openstack_invoice" + ).with_context(**email_ctx) + for invoice in self: + if invoice["is_move_sent"]: + continue + outstanding = self.get_outstanding_invoices( + invoice.os_project, invoice.partner_id + ) + for inv in outstanding: + self.outstanding_invoices.add(inv) + csv_data = self.env.ref( + "openstack_integration.openstack_invoice_csv" + )._render_csv([invoice.id], False)[0] + csv_data_encoded = base64.encodebytes(csv_data.encode("utf-8")) + invoice_name = (invoice.state == "posted") and ( + (invoice.name or "INV").replace("/", "_") + ) + filename = "Detail_{}.csv".format(invoice_name) + attachment = { + "name": filename, + "datas": csv_data_encoded, + "store_fname": filename, + "res_model": "account.move", + "type": "binary", # "binary" just means file, as opposed to "url" + "res_id": invoice.id, + "mimetype": "text/csv", + } + attachment_id = self.env["ir.attachment"].create(attachment) + # Set additional attachment, send, remove attachments again + template_id.attachment_ids = [(6, 0, [attachment_id.id])] + template_id.send_mail(invoice.id) + template_id.attachment_ids = False + + invoice.write({"is_move_sent": True}) + finally: + # Reset after - avoids manually generated Statement Of Accounts issues + self.outstanding_invoices._reset() + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + os_project = fields.Many2one( + comodel_name="openstack.project", + string="OpenStack Project", + ) + + os_region = fields.Text( + string="OS Region", + required=False, + ) + + os_resource_type = fields.Text( + string="OS Resource Type", + required=False, + ) + + os_resource_name = fields.Text( + string="OS Resource Name", + required=False, + ) + + os_resource_id = fields.Text( + string="OS Resource ID", + required=False, + ) diff --git a/models/credit.py b/models/credit.py new file mode 100755 index 0000000..74009e9 --- /dev/null +++ b/models/credit.py @@ -0,0 +1,181 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import date + +from odoo import _, api, fields, models, exceptions + + +class OpenStackCredit(models.Model): + """OpenStack credit which is attached to a project""" + + _name = "openstack.credit" + _description = "OpenStack Credit" + + voucher_code = fields.Many2one( + comodel_name="openstack.voucher_code", string="Voucher Code" + ) + + project = fields.Many2one( + comodel_name="openstack.project", + string="Project", + required=True, + ) + + @api.depends("project", "voucher_code") + def _get_name(self): + """Computed field to show in the breadcrumbs""" + for credit in self: + name = credit.project.name + if credit.voucher_code: + name = f"{credit.project.name} - {credit.voucher_code.name}" + credit.name = name + + name = fields.Char(string="Name", compute="_get_name") + + credit_type = fields.Many2one( + comodel_name="openstack.credit.type", + string="Credit Type", + required=True, + ) + + start_date = fields.Date(string="Start Date", required=True, default=date.today()) + expiry_date = fields.Date(string="Expiry Date") + + initial_balance = fields.Float(string="Initial Balance", required=True) + + current_balance = fields.Float(string="Current Balance", compute="_compute_balance") + + transactions = fields.One2many( + comodel_name="openstack.credit.transaction", + inverse_name="credit", + string="Credit Transactions", + ) + + @api.depends("initial_balance") + def _compute_balance(self): + for record in self: + transactions = self.env["openstack.credit.transaction"].search( + [("credit", "=", record.id)] + ) + if not transactions: + record.current_balance = record.initial_balance + continue + record.current_balance = record.initial_balance + sum( + [t.value for t in transactions] + ) + + @api.constrains("start_date", "expiry_date") + def _check_expiry_date(self): + """Constrain start date + + Start date must always be before the expiry date. + """ + if self.expiry_date and self.start_date > self.expiry_date: + raise exceptions.ValidationError( + _("Expiry date must be after the start date") + ) + + @property + def is_active(self): + """Return whether this credit has started and not expired""" + if date.today() < self.start_date: + return False + return self.expiry_date is None or date.today() < self.expiry_date + + @property + def available_balance(self): + """Return current balance or 0.0 if the credit is not active + + That is, if the credit has not yet started or has expired. + """ + if not self.is_active: + return 0.0 + else: + return self.current_balance + + +class OpenStackCreditTransaction(models.Model): + """Credit transaction attached to Credit instances""" + + _name = "openstack.credit.transaction" + _description = "OpenStack Credit Transaction" + + credit = fields.Many2one( + comodel_name="openstack.credit", + string="Credit", + required=True, + ) + description = fields.Char(string="Description") + value = fields.Float(string="Value", required=True) + + @api.constrains("credit", "value") + def _check_value(self): + """Constraint value + + Current balance on credit can never go below zero. + """ + transactions = self.env["openstack.credit.transaction"].search( + [("credit", "=", self.credit.id), ("id", "!=", self.id)] + ) + if not transactions: + return + + current_balance = self.credit.initial_balance + sum( + [t.value for t in transactions] + ) + if (current_balance + self.value) < 0: + if abs(current_balance + self.value) < 0.01: + # If the diff is this small, just make it zero. + self.value = -current_balance + else: + raise exceptions.ValidationError( + _("Cannot add transaction that brings balance below zero.") + ) + + +class OpenStackCreditType(models.Model): + """Credit types attached to each Credit instance""" + + _name = "openstack.credit.type" + _description = "OpenStack Credit Type" + + name = fields.Char(string="Name", required=True) + refundable = fields.Boolean(string="Refundable", default=False) + + product = fields.Many2one( + comodel_name="product.product", string="Product", required=True + ) + credits = fields.One2many( + comodel_name="openstack.credit", inverse_name="credit_type" + ) + + only_for_products = fields.Many2many( + "product.product", + string="Only for these products", + help=( + "This credit type can only apply to these products. " + "This is mutually inclusive with `only_for_product_categories`." + ), + ) + + only_for_product_categories = fields.Many2many( + "product.category", + string="Only for these product categories", + help=( + "This credit type can only apply to these product categories. " + "This is mutually inclusive with `only_for_products`." + ), + ) diff --git a/models/customer_group.py b/models/customer_group.py new file mode 100755 index 0000000..603c8e6 --- /dev/null +++ b/models/customer_group.py @@ -0,0 +1,29 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from odoo import fields, models + + +class OpenStackCustomerGroup(models.Model): + """ """ + + _name = "openstack.customer_group" + _description = "OpenStack customer groups." + + name = fields.Char(string="Customer group name", required=True) + partners = fields.One2many( + comodel_name="res.partner", inverse_name="os_customer_group" + ) + pricelist = fields.Many2one(comodel_name="product.pricelist", string="Pricelist") diff --git a/models/grant.py b/models/grant.py new file mode 100755 index 0000000..fba2871 --- /dev/null +++ b/models/grant.py @@ -0,0 +1,123 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import date + +from odoo import _, api, fields, models, exceptions + + +class OpenStackGrant(models.Model): + """OpenStack grant which is attached to a project""" + + _name = "openstack.grant" + _description = "Cloud Grant" + + voucher_code = fields.Many2one( + comodel_name="openstack.voucher_code", + string="Voucher Code", + ) + + project = fields.Many2one( + comodel_name="openstack.project", + string="Project", + required=True, + ) + + @api.depends("project", "voucher_code") + def _get_name(self): + """Computed field to show in the breadcrumbs""" + for grant in self: + name = grant.project.name + if grant.voucher_code: + name = f"{grant.project.name} - {grant.voucher_code.name}" + grant.name = name + + name = fields.Char(string="Name", compute="_get_name") + + grant_type = fields.Many2one( + comodel_name="openstack.grant.type", + string="Grant Type", + required=True, + ) + + start_date = fields.Date(string="Start Date", required=True, default=date.today()) + expiry_date = fields.Date(string="Expiry Date") + + value = fields.Float(string="Value", required=True) + + @api.constrains("start_date", "expiry_date") + def _check_expiry_date(self): + """Constrain start date + + Start date must always be before the expiry date. + """ + if self.expiry_date and self.start_date > self.expiry_date: + raise exceptions.ValidationError( + _("Expiry date must be after the start date") + ) + + @api.constrains("value") + def _check_value_is_positive(self): + """Constrain value""" + if self.value < 0.0: + raise exceptions.ValidationError(_("value cannot be negative")) + + def is_active(self, today=None): + """Return whether this grant has started and not expired""" + if today is None: + today = date.today() + if today < self.start_date: + return False + return self.expiry_date is None or today < self.expiry_date + + +class OpenStackGrantType(models.Model): + """Grant types attached to each grant instance""" + + _name = "openstack.grant.type" + _description = "OpenStack Grant Type" + + name = fields.Char(string="Name", required=True) + product = fields.Many2one( + comodel_name="product.product", string="Product", required=True + ) + grants = fields.One2many(comodel_name="openstack.grant", inverse_name="grant_type") + + only_on_group_root = fields.Boolean( + string="Only on group root", + help=( + "If true, this grant type is only allowed to be part of an invoice " + "grouping if it is on the group root project." + ), + default=False, + ) + + only_for_products = fields.Many2many( + "product.product", + string="Only for these products", + help=( + "This grant type can only apply to these products. " + "This is mutually inclusive with `only_for_product_categories`." + ), + ) + + only_for_product_categories = fields.Many2many( + "product.category", + string="Only for these product categories", + help=( + "This grant type can only apply to these product categories. " + "This is mutually inclusive with `only_for_products`." + ), + ) diff --git a/models/project.py b/models/project.py new file mode 100755 index 0000000..98d85cd --- /dev/null +++ b/models/project.py @@ -0,0 +1,124 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from odoo import api, fields, models + + +class OpenStackProject(models.Model): + """A Project, attached to partner""" + + _name = "openstack.project" + _description = "OpenStack Project" + _rec_name = "display_name" + _inherit = ["mail.thread"] + + name = fields.Char(string="Project Name", required=True) + os_id = fields.Char( + string="Project ID", + required=True, + unique=True, + ) + + @api.depends("name", "os_id") + def _get_name(self): + """Computed field to show in the breadcrumbs""" + for project in self: + project.display_name = f"{project.name} ({project.os_id})" + + display_name = fields.Char(string="Display Name", compute="_get_name", store=True) + + parent = fields.Many2one(comodel_name="openstack.project", string="Parent Project") + enabled = fields.Boolean(string="Enabled", default=True) + + billing_type = fields.Selection( + selection=[ + ("customer", "customer"), + ("internal", "internal"), + ], + default="customer", + string="Billing Type", + required=True, + ) + + group_invoices = fields.Boolean(string="Group Invoices") + + payment_method = fields.Selection( + selection=[("credit_card", "credit_card"), ("invoice", "invoice")], + default="invoice", + string="Payment Method", + required=True, + ) + stripe_card_id = fields.Char("Stripe Card ID", required=False) + po_number = fields.Char(string="PO Number") + override_po_number = fields.Boolean(string="Override PO Number") + + owner = fields.Many2one( + comodel_name="res.partner", string="Owner partner", required=True + ) + project_contacts = fields.One2many( + comodel_name="openstack.project_contact", + inverse_name="project", + string="Project Contacts", + ) + + project_credits = fields.One2many( + comodel_name="openstack.credit", + inverse_name="project", + string="Credits", + ) + project_grants = fields.One2many( + comodel_name="openstack.grant", + inverse_name="project", + string="Grants", + ) + term_discounts = fields.One2many( + comodel_name="openstack.term_discount", + inverse_name="project", + string="Term Discounts", + ) + + support_subscription = fields.Many2one( + comodel_name="openstack.support_subscription", + string="Premium Support Subscription", + ) + + +class OpenStackProjectContacts(models.Model): + """Bridge table for openstack project and a partner""" + + _name = "openstack.project_contact" + _description = "OpenStack Project Contacts bridge table" + + project = fields.Many2one(comodel_name="openstack.project", string="Project") + partner = fields.Many2one( + comodel_name="res.partner", string="Partner", required=True + ) + inherit = fields.Boolean( + string="Inherit to sub-projects", + default=False, + ) + + contact_type = fields.Selection( + selection=[ + ("primary", "primary"), + ("billing", "billing"), + ("technical", "technical"), + ("legal", "legal"), + # TODO(adriant): REMOVE RESELLER LATER: + ("reseller customer", "reseller customer"), + ], + string="Contact Type", + required=True, + ) diff --git a/models/referral.py b/models/referral.py new file mode 100755 index 0000000..48b3cd1 --- /dev/null +++ b/models/referral.py @@ -0,0 +1,80 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from odoo import api, fields, models + + +class OpenStackReferralCode(models.Model): + _name = "openstack.referral_code" + _description = "OpenStack referral code." + + partner = fields.Many2one( + comodel_name="res.partner", + string="Partner", + required=True, + ) + code = fields.Char("Code", required=True, unique=True) + + @api.depends("partner", "code") + def _get_name(self): + """Computed field to show in the breadcrumbs""" + for ref in self: + ref.name = f"{ref.partner.name} - {ref.code}" + + name = fields.Char("Name", compute="_get_name") + + referral_credit_amount = fields.Float( + string="Referral credit initial balance", + ) + referral_credit_type = fields.Many2one( + comodel_name="openstack.credit.type", + string="Referral credit Type", + required=True, + ) + referral_credit_duration = fields.Integer( + string="Referral credit duration in days", + required=True, + ) + + before_reward_usage_threshold = fields.Float( + string="Before reward usage threshold", + required=True, + ) + + reward_credit_amount = fields.Float( + string="Reward credit initial balance", + required=True, + ) + reward_credit_type = fields.Many2one( + comodel_name="openstack.credit.type", + string="Reward credit Type", + required=True, + ) + reward_credit_duration = fields.Integer( + string="Reward credit duration in days", + required=True, + ) + + allowed_uses = fields.Integer( + string="Number of allowed uses of this code. Default: -1 (infinite)", + default=-1, + required=True, + ) + + referrals = fields.One2many( + comodel_name="res.partner", + inverse_name="os_referral", + string="Referrals using this code.", + ) diff --git a/models/res_partner.py b/models/res_partner.py new file mode 100755 index 0000000..eee7534 --- /dev/null +++ b/models/res_partner.py @@ -0,0 +1,88 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from odoo import fields, models + + +class ResPartner(models.Model): + """Add some fields related to OpenStack""" + + _inherit = "res.partner" + + stripe_customer_id = fields.Char( + string="Stripe Customer ID", + copy=False, + groups="openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager", + ) + os_projects = fields.One2many( + comodel_name="openstack.project", + inverse_name="owner", + copy=False, + groups="openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager", + ) + os_project_contacts = fields.One2many( + comodel_name="openstack.project_contact", + inverse_name="partner", + copy=False, + groups="openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager", + ) + os_customer_group = fields.Many2one( + comodel_name="openstack.customer_group", + string="OpenStack Customer Group", + copy=False, + groups="openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager", + ) + os_reseller = fields.Many2one( + comodel_name="openstack.reseller", + string="OpenStack Reseller", + copy=False, + groups="openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager", + ) + os_trial = fields.Many2one( + comodel_name="openstack.trial", + string="OpenStack trial on signup.", + copy=False, + groups="openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager", + ) + os_referral = fields.Many2one( + comodel_name="openstack.referral_code", + string="OpenStack referral on signup.", + copy=False, + groups="openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager", + ) + os_referral_codes = fields.One2many( + comodel_name="openstack.referral_code", + inverse_name="partner", + string="OpenStack Referral codes", + copy=False, + groups="openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager", + ) + os_is_cloud_framework_agreement_partner = fields.Boolean( + string="CFA partner", + default=False, + required=False, + groups=( + "openstack_integration.group_openstack_user," + "openstack_integration.group_openstack_manager" + ), + ) diff --git a/models/reseller.py b/models/reseller.py new file mode 100755 index 0000000..e486d32 --- /dev/null +++ b/models/reseller.py @@ -0,0 +1,110 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from odoo import _, api, fields, models, exceptions + + +class OpenStackReseller(models.Model): + """A customer of OpenStack who has signed as a reseller.""" + + _name = "openstack.reseller" + _description = "OpenStack Reseller " + + partner = fields.Many2one( + comodel_name="res.partner", + string="Reseller partner", + required=True, + ) + + @api.depends("partner") + def _get_name(self): + """Computed field to show in the breadcrumbs""" + for reseller in self: + reseller.name = reseller.partner.name + + name = fields.Char(string="Name", compute="_get_name") + + tier = fields.Many2one( + "openstack.reseller.tier", + "Reseller tier", + required=True, + ) + hide_billing = fields.Boolean( + string="Hide billing", + default=False, + ) + alternative_billing_url = fields.Char( + string="Alternative billing url", + ) + hide_support = fields.Boolean( + string="Hide support", + default=False, + ) + alternative_support_url = fields.Char( + string="Alternative support url", + ) + + demo_project = fields.Many2one( + comodel_name="openstack.project", + string="Demo tenant", + ) + + +class OpenStackResellerTier(models.Model): + _name = "openstack.reseller.tier" + _description = "Reseller tier" + + name = fields.Char( + string="Tier name", + required=True, + ) + + min_usage_threshold = fields.Float( + string="Required usage amount for tier.", + required=True, + ) + + discount_percent = fields.Float( + string="Discount percentage for this range (0 to 100)", + required=True, + ) + discount_product = fields.Many2one( + comodel_name="product.product", + string="Discount product", + required=True, + ) + + free_support_hours = fields.Integer( + string="Free support hours per month", + required=True, + ) + + free_monthly_credit = fields.Float( + string="Free monthly demo credit", + required=True, + ) + free_monthly_credit_product = fields.Many2one( + comodel_name="product.product", + string="Free monthly credit product", + required=True, + ) + + @api.constrains("discount_percent") + def _check_discount_percent(self): + """Constrain discount to 0-100""" + if self.discount_percent < 0.0 or self.discount_percent > 100.0: + raise exceptions.ValidationError( + _("discount_percent must be between 0-100") + ) diff --git a/models/sale_order.py b/models/sale_order.py new file mode 100755 index 0000000..1f9b388 --- /dev/null +++ b/models/sale_order.py @@ -0,0 +1,115 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + os_project = fields.Many2one( + comodel_name="openstack.project", + string="OpenStack Project", + ) + + # NOTE(adriant): because odoo is stupid... + # NOTE(TODO): move this into standalone addon + os_invoice_date = fields.Date( + string="Invoice Date", + required=False, + ) + + os_invoice_due_date = fields.Date( + string="Invoice Due Date", + required=False, + ) + + os_is_cloud_framework_agreement_sale_order = fields.Boolean( + string="CFA Sale Order", + default=False, + required=False, + ) + + def create_invoices(self): + sale_order_ids = [s.id for s in self] + payment = self.env["sale.advance.payment.inv"].create( + {"advance_payment_method": "delivered"} + ) + payment.with_context(active_ids=sale_order_ids).create_invoices() + + def _prepare_invoice(self): + self.ensure_one() + invoice_vals = super(SaleOrder, self)._prepare_invoice() + invoice_vals["os_project"] = self.os_project.id + if self.os_invoice_date: + invoice_vals["invoice_date"] = self.os_invoice_date + if self.os_invoice_due_date: + invoice_vals["invoice_date_due"] = self.os_invoice_due_date + invoice_vals["os_is_cloud_framework_agreement_invoice"] = False + if self.os_is_cloud_framework_agreement_sale_order: + invoice_vals["os_is_cloud_framework_agreement_invoice"] = ( + self.os_is_cloud_framework_agreement_sale_order + ) + return invoice_vals + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + os_project = fields.Many2one( + comodel_name="openstack.project", + string="OpenStack Project", + ) + + os_region = fields.Text( + string="OS Region", + required=False, + ) + + os_resource_type = fields.Text( + string="OS Resource Type", + required=False, + ) + + os_resource_name = fields.Text( + string="OS Resource Name", + required=False, + ) + + os_resource_id = fields.Text( + string="OS Resource ID", + required=False, + ) + + def _prepare_invoice_line(self, **optional_values): + self.ensure_one() + invoice_line_vals = super(SaleOrderLine, self)._prepare_invoice_line() + invoice_line_vals["os_project"] = self.os_project.id + + invoice_line_vals["os_region"] = "NZ" + invoice_line_vals["os_resource_type"] = "" + invoice_line_vals["os_resource_name"] = "" + invoice_line_vals["os_resource_id"] = "" + + if self.os_region: + invoice_line_vals["os_region"] = self.os_region + if self.os_resource_type: + invoice_line_vals["os_resource_type"] = self.os_resource_type + if self.os_resource_name: + invoice_line_vals["os_resource_name"] = self.os_resource_name + if self.os_resource_id: + invoice_line_vals["os_resource_id"] = self.os_resource_id + + return invoice_line_vals diff --git a/models/support_subscription.py b/models/support_subscription.py new file mode 100755 index 0000000..419d73c --- /dev/null +++ b/models/support_subscription.py @@ -0,0 +1,111 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import date + +from odoo import _, api, fields, models, exceptions + + +class OpenStackSupportSubscription(models.Model): + """An openstack support subscription + + Attached to a partner or optionally a project.""" + + _name = "openstack.support_subscription" + _description = "OpenStack Support Subscription" + _rec_name = "project" + + project = fields.Many2one( + comodel_name="openstack.project", + string="Project", + ) + + partner = fields.Many2one( + "res.partner", + string="Partner", + ) + + billing_type = fields.Selection( + selection=[ + ("paid", "paid"), + ("complimentary", "complimentary"), + ], + default="paid", + string="Billing Type", + required=True, + ) + + start_date = fields.Date( + string="Start Date", + required=True, + default=date.today(), + ) + end_date = fields.Date(string="End Date") + support_subscription_type = fields.Many2one( + comodel_name="openstack.support_subscription.type", + string="Subscription Type", + required=True, + ) + + @api.constrains("project", "") + def _check_owner(self): + """Check an owner is set + + Project and/or Partner must be set + """ + if not self.project and not self.partner: + raise exceptions.ValidationError( + _("Must set at one or both Project/Partner.") + ) + + @api.constrains("start_date", "end_date") + def _check_end_date(self): + """Constrain start date + + Start date must always be before the expiry date. + """ + if self.end_date and self.start_date > self.end_date: + raise exceptions.ValidationError(_("End date must be after the start date")) + + +class OpenStackSupportSubscriptionType(models.Model): + """Support Subscription type.""" + + _name = "openstack.support_subscription.type" + _description = "OpenStack Support Subscription Type" + + name = fields.Char(string="Name", required=True) + product = fields.Many2one( + comodel_name="product.product", + string="Product", + required=True, + ) + usage_percent = fields.Float( + string="Percentage of usage to compare to price (0 to 100)", + required=True, + ) + + support_subscription = fields.One2many( + comodel_name="openstack.support_subscription", + inverse_name="support_subscription_type", + ) + + @api.constrains("discount_percent") + def _check_discount_percent(self): + """Constrain discount to 0-100""" + if self.discount_percent < 0.0 or self.discount_percent > 100.0: + raise exceptions.ValidationError( + _("discount_percent must be between 0-100") + ) diff --git a/models/term_discount.py b/models/term_discount.py new file mode 100755 index 0000000..9c2a917 --- /dev/null +++ b/models/term_discount.py @@ -0,0 +1,80 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import date + +from odoo import _, api, fields, models, exceptions + + +class OpenStackTermDiscount(models.Model): + """OpenStack Term Discount which is attached to a Partner or project.""" + + _name = "openstack.term_discount" + _description = "OpenStack Term Discount" + _rec_name = "project" + + partner = fields.Many2one( + comodel_name="res.partner", + string="Partner", + required=True, + ) + + project = fields.Many2one( + comodel_name="openstack.project", + string="Project", + ) + + start_date = fields.Date( + string="Start Date", + required=True, + default=date.today(), + ) + end_date = fields.Date( + string="Expiry Date", + required=True, + ) + early_termination_date = fields.Date(string="Early termination Date") + + min_commit = fields.Float( + string="Minimum Commitment", + required=True, + ) + + discount_percent = fields.Float( + string="Discount percentage (0 to 100)", + required=True, + ) + + superseded_by = fields.Many2one( + comodel_name="openstack.term_discount", + string="Superseded by", + ) + + @api.constrains("discount_percent") + def _check_discount_percent(self): + """Constrain discount to 0-100""" + if self.discount_percent < 0.0 or self.discount_percent > 100.0: + raise exceptions.ValidationError( + _("discount_percent must be between 0-100") + ) + + @api.constrains("start_date", "end_date") + def _check_end_date(self): + """Constrain start date + + Start date must always be before the expiry date. + """ + if self.end_date and self.start_date > self.end_date: + raise exceptions.ValidationError(_("End date must be after the start date")) diff --git a/models/trial.py b/models/trial.py new file mode 100755 index 0000000..c61952b --- /dev/null +++ b/models/trial.py @@ -0,0 +1,51 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from datetime import date + +from odoo import _, api, fields, models, exceptions + + +class OpenStackTrial(models.Model): + _name = "openstack.trial" + _description = "OpenStack Trial" + _rec_name = "partner" + + partner = fields.Many2one( + "res.partner", + string="Trial partner", + required=True, + ) + + start_date = fields.Date( + string="Start Date", + required=True, + default=date.today(), + ) + end_date = fields.Date(string="End Date", required=True) + + account_suspended_on = fields.Date(string="Account suspended date") + account_terminated_on = fields.Date(string="Account terminated date") + + account_upgraded_on = fields.Date(string="Account upgraded date") + + @api.constrains("start_date", "end_date") + def _check_end_date(self): + """Constrain start date + + Start date must always be before the expiry date. + """ + if self.end_date and self.start_date > self.end_date: + raise exceptions.ValidationError(_("End date must be after the start date")) diff --git a/models/volume_discount.py b/models/volume_discount.py new file mode 100755 index 0000000..797cc3f --- /dev/null +++ b/models/volume_discount.py @@ -0,0 +1,68 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from odoo import _, api, fields, models, exceptions + + +class OpenStackVolumeDiscountRange(models.Model): + """OpenStack Volume Discount ranges""" + + _name = "openstack.volume_discount_range" + _description = "OpenStack Volume Discount Range" + + customer_group = fields.Many2one( + "openstack.customer_group", + "Customer Group", + ) + + @api.depends("use_max", "min", "max", "customer_group") + def _get_name(self): + """Computed field to show in the breadcrumbs""" + for vol_disc in self: + if vol_disc.use_max: + name = f"{vol_disc.min} to {vol_disc.max}" + else: + name = f"{vol_disc.min} and higher" + if vol_disc.customer_group: + name += f" {vol_disc.customer_group.name}" + vol_disc.name = name + + name = fields.Char(string="Name", compute="_get_name") + + min = fields.Float(string="min end of the range", required=True) + + use_max = fields.Boolean(string="Use max", default=True) + max = fields.Float( + string="Max end of the range", + default=None, + ) + + discount_percent = fields.Float( + "Discount percentage for this range (0-100)", required=True + ) + + @api.constrains("discount_percent") + def _check_discount_percent(self): + """Constrain discount to 0-100""" + if self.discount_percent < 0.0 or self.discount_percent > 100.0: + raise exceptions.ValidationError( + _("discount_percent must be between 0-100") + ) + + @api.constrains("min", "max") + def _check_max(self): + """Min must be less than max.""" + if self.max and self.min > self.max: + raise exceptions.ValidationError(_("Min must be less than max.")) diff --git a/models/voucher_codes.py b/models/voucher_codes.py new file mode 100755 index 0000000..6a2eff4 --- /dev/null +++ b/models/voucher_codes.py @@ -0,0 +1,85 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from odoo import _, api, fields, models, exceptions + + +class OpenStackVoucherCode(models.Model): + """OpenStack voucher code.""" + + _name = "openstack.voucher_code" + _description = "OpenStack Voucher Code" + + code = fields.Char(string="Code", required=True, unique=True) + + @api.depends("code") + def _get_name(self): + """Computed field to show field 'code' in the breadcrumbs""" + for voucher in self: + voucher.name = voucher.code + + name = fields.Char(string="Name", compute="_get_name") + + claimed = fields.Boolean(string="Claimed", default=False) + multi_use = fields.Boolean(string="Multi-use code", default=False) + + expiry_date = fields.Date(string="Expiry Date") + + sales_person = fields.Many2one( + comodel_name="res.partner", + string="Sales person", + ) + + customer_group = fields.Many2one( + comodel_name="openstack.customer_group", + string="Customer Group", + ) + + credit_amount = fields.Float( + string="Credit initial balance", + ) + credit_type = fields.Many2one( + comodel_name="openstack.credit.type", + string="Credit Type", + ) + credit_duration = fields.Integer( + string="Credit duration in days", + ) + + grant_value = fields.Float( + string="Grant value", + ) + grant_type = fields.Many2one( + comodel_name="openstack.grant.type", + string="Grant Type", + ) + grant_duration = fields.Integer( + string="Grant duration in days", + ) + + quota_size = fields.Char(string="Default quota size on signup") + + tags = fields.Many2many( + "res.partner.category", + column1="openstack_voucher_id", + column2="category_id", + string="Tags", + ) + + @api.constrains("claimed", "multi_use") + def _check_claimed_if_multi_use(self): + """Can't be claimed if multi_use""" + if self.multi_use and self.claimed: + raise exceptions.ValidationError(_("Can't claim if multi_use is true.")) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..8dbe98f --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +target-version = [ + "py38", + "py39", + "py310", + "py311", + "py312", +] diff --git a/report/__init__.py b/report/__init__.py new file mode 100755 index 0000000..d3573be --- /dev/null +++ b/report/__init__.py @@ -0,0 +1,18 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import openstack_invoice_csv + +__all__ = ["openstack_invoice_csv"] diff --git a/report/openstack_invoice_csv.py b/report/openstack_invoice_csv.py new file mode 100755 index 0000000..57a1c05 --- /dev/null +++ b/report/openstack_invoice_csv.py @@ -0,0 +1,72 @@ +# Copyright (C) 2021-2024 Catalyst Cloud Limited +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import csv + +from odoo.exceptions import UserError +from odoo import models + + +class OpenStackInvoiceCSV(models.AbstractModel): + _name = "report.openstack_integration.openstack_invoice_csv" + _inherit = "report.report_csv.abstract" + _description = "OpenStack Invoice CSV" + + def generate_csv_report(self, writer, data, invoice): + if len(invoice) != 1: + # TODO: Hey why - can do for more + raise UserError("This CSV can only be created for one invoice at a time") + writer.writeheader() + # TODO: Investigate why this line is needed + invoice = invoice.with_context(allowed_company_ids=[invoice.company_id.id]) + for line in invoice.invoice_line_ids: + writer.writerow( + { + "name": line.name, + "project": line.os_project.name, + "project_id": line.os_project.os_id, + "product": line.product_id.name, + "region": line.os_region, + "resource_name": line.os_resource_name, + "resource_type": line.os_resource_type, + "resource_id": line.os_resource_id, + "unit_type": line.product_id.default_code, + "units": line.quantity, + "price_per_unit": line.price_unit, + "subtotal": round(line.price_subtotal, 6), + } + ) + + def csv_report_options(self): + res = super().csv_report_options() + fieldnames = [ + "name", + "project", + "project_id", + "product", + "region", + "resource_name", + "resource_type", + "resource_id", + "unit_type", + "units", + "price_per_unit", + "subtotal", + ] + for field in fieldnames: + res["fieldnames"].append(field) + res["delimiter"] = "," + res["quoting"] = csv.QUOTE_NONNUMERIC + return res diff --git a/report/openstack_invoice_csv.xml b/report/openstack_invoice_csv.xml new file mode 100755 index 0000000..3fcd040 --- /dev/null +++ b/report/openstack_invoice_csv.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/security/ir.model.access.csv b/security/ir.model.access.csv new file mode 100755 index 0000000..f9380b4 --- /dev/null +++ b/security/ir.model.access.csv @@ -0,0 +1,35 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_openstack_credit_user,access_openstack_credit_user,model_openstack_credit,group_openstack_user,1,0,0,0 +access_openstack_credit_manager,access_openstack_credit_manager,model_openstack_credit,group_openstack_manager,1,1,1,1 +access_openstack_credit_transaction_user,access_openstack_credit_transaction_user,model_openstack_credit_transaction,group_openstack_user,1,0,0,0 +access_openstack_credit_transaction_manager,access_openstack_credit_transaction_manager,model_openstack_credit_transaction,group_openstack_manager,1,1,1,1 +access_openstack_credit_type_user,access_openstack_credit_type_user,model_openstack_credit_type,group_openstack_user,1,0,0,0 +access_openstack_credit_type_manager,access_openstack_credit_type_manager,model_openstack_credit_type,group_openstack_manager,1,1,1,1 +access_openstack_customer_group_user,access_openstack_customer_group_user,model_openstack_customer_group,group_openstack_user,1,0,0,0 +access_openstack_customer_group_manager,access_openstack_customer_group_manager,model_openstack_customer_group,group_openstack_manager,1,1,1,1 +access_openstack_grant_user,access_openstack_grant_user,model_openstack_grant,group_openstack_user,1,0,0,0 +access_openstack_grant_manager,access_openstack_grant_manager,model_openstack_grant,group_openstack_manager,1,1,1,1 +access_openstack_grant_type_user,access_openstack_grant_type_user,model_openstack_grant_type,group_openstack_user,1,0,0,0 +access_openstack_grant_type_manager,access_openstack_grant_type_manager,model_openstack_grant_type,group_openstack_manager,1,1,1,1 +access_openstack_project_user,access_openstack_project_user,model_openstack_project,group_openstack_user,1,0,0,0 +access_openstack_project_manager,access_openstack_project_manager,model_openstack_project,group_openstack_manager,1,1,1,1 +access_openstack_project_contact_user,access_openstack_project_contact_user,model_openstack_project_contact,group_openstack_user,1,0,0,0 +access_openstack_project_contact_manager,access_openstack_project_contact_manager,model_openstack_project_contact,group_openstack_manager,1,1,1,1 +access_openstack_referral_code_user,access_openstack_referral_code_user,model_openstack_referral_code,group_openstack_user,1,0,0,0 +access_openstack_referral_code_manager,access_openstack_referral_code_manager,model_openstack_referral_code,group_openstack_manager,1,1,1,1 +access_openstack_reseller_user,access_openstack_reseller_user,model_openstack_reseller,group_openstack_user,1,0,0,0 +access_openstack_reseller_manager,access_openstack_reseller_manager,model_openstack_reseller,group_openstack_manager,1,1,1,1 +access_openstack_reseller_tier_user,access_openstack_reseller_tier_user,model_openstack_reseller_tier,group_openstack_user,1,0,0,0 +access_openstack_reseller_tier_manager,access_openstack_reseller_tier_manager,model_openstack_reseller_tier,group_openstack_manager,1,1,1,1 +access_openstack_support_subscription_user,access_openstack_support_subscription_user,model_openstack_support_subscription,group_openstack_user,1,0,0,0 +access_openstack_support_subscription_manager,access_openstack_support_subscription_manager,model_openstack_support_subscription,group_openstack_manager,1,1,1,1 +access_openstack_support_subscription_type_user,access_openstack_support_subscription_type_user,model_openstack_support_subscription_type,group_openstack_user,1,0,0,0 +access_openstack_support_subscription_type_manager,access_openstack_support_subscription_type_manager,model_openstack_support_subscription_type,group_openstack_manager,1,1,1,1 +access_openstack_term_discount_user,access_openstack_term_discount_user,model_openstack_term_discount,group_openstack_user,1,0,0,0 +access_openstack_term_discount_manager,access_openstack_term_discount_manager,model_openstack_term_discount,group_openstack_manager,1,1,1,1 +access_openstack_trial_user,access_openstack_trial_user,model_openstack_trial,group_openstack_user,1,0,0,0 +access_openstack_trial_manager,access_openstack_trial_manager,model_openstack_trial,group_openstack_manager,1,1,1,1 +access_openstack_volume_discount_range_user,access_openstack_volume_discount_range_user,model_openstack_volume_discount_range,group_openstack_user,1,0,0,0 +access_openstack_volume_discount_range_manager,access_openstack_volume_discount_range_manager,model_openstack_volume_discount_range,group_openstack_manager,1,1,1,1 +access_openstack_voucher_code_user,access_openstack_voucher_code_user,model_openstack_voucher_code,group_openstack_user,1,0,0,0 +access_openstack_voucher_code_manager,access_openstack_voucher_code_manager,model_openstack_voucher_code,group_openstack_manager,1,1,1,1 diff --git a/security/openstack_security.xml b/security/openstack_security.xml new file mode 100755 index 0000000..d7d38e7 --- /dev/null +++ b/security/openstack_security.xml @@ -0,0 +1,19 @@ + + + + + + OpenStack Services + + + + User + + + + Manager + + + + + diff --git a/static/OpenStack-Logo-Horizontal.png b/static/OpenStack-Logo-Horizontal.png new file mode 100644 index 0000000..b1946cd Binary files /dev/null and b/static/OpenStack-Logo-Horizontal.png differ diff --git a/static/OpenStack-Logo-Horizontal.svg b/static/OpenStack-Logo-Horizontal.svg new file mode 100644 index 0000000..085d529 --- /dev/null +++ b/static/OpenStack-Logo-Horizontal.svg @@ -0,0 +1 @@ +OpenStack_Logo_Horizontal diff --git a/static/OpenStack-Logo-Mark.png b/static/OpenStack-Logo-Mark.png new file mode 100644 index 0000000..8061ae5 Binary files /dev/null and b/static/OpenStack-Logo-Mark.png differ diff --git a/static/OpenStack-Logo-Mark.svg b/static/OpenStack-Logo-Mark.svg new file mode 100644 index 0000000..666ec98 --- /dev/null +++ b/static/OpenStack-Logo-Mark.svg @@ -0,0 +1 @@ +OpenStack_Logo_Mark diff --git a/views/account_move_view.xml b/views/account_move_view.xml new file mode 100755 index 0000000..076a560 --- /dev/null +++ b/views/account_move_view.xml @@ -0,0 +1,51 @@ + + + + + + account.invoice.tree (in openstack_integration) + account.move + + + + + + + + + + + + + account.move.form + account.move + form + + + + + + + + + + + + + + + + + + + + + + + diff --git a/views/credit_view.xml b/views/credit_view.xml new file mode 100755 index 0000000..d857087 --- /dev/null +++ b/views/credit_view.xml @@ -0,0 +1,143 @@ + + + + + + openstack.credit.form + openstack.credit + +
+ + + + + + + + + + + + + + + + + + + +
+
+
+ + + + openstack.credit.tree + openstack.credit + + + + + + + + + + + + + + + + openstack.credit.transaction.tree + openstack.credit.transaction + + + + + + + + + + + + + openstack.credit.select + openstack.credit + search + + + + + + + + + + + OpenStack Credits + openstack.credit + tree,form + + + + + + + + openstack.credit.type.form + openstack.credit.type + +
+ + + + + + + + + +
+
+
+ + + + openstack.credit.type.tree + openstack.credit.type + + + + + + + + + + + + + OpenStack Credit Types + openstack.credit.type + tree,form + + + + + +
+
diff --git a/views/customer_group_view.xml b/views/customer_group_view.xml new file mode 100755 index 0000000..e70f77e --- /dev/null +++ b/views/customer_group_view.xml @@ -0,0 +1,64 @@ + + + + + + + openstack.customer_group.form + openstack.customer_group + +
+ + + + + + + + + + +
+
+
+ + + + openstack.customer_group.tree + openstack.customer_group + + + + + + + + + + openstack.customer_group.select + openstack.customer_group + search + + + + + + + + + + + + OpenStack Customer Groups + openstack.customer_group + tree,form + + + + + +
+
diff --git a/views/grant_view.xml b/views/grant_view.xml new file mode 100755 index 0000000..26fc436 --- /dev/null +++ b/views/grant_view.xml @@ -0,0 +1,118 @@ + + + + + + + openstack.grant.form + openstack.grant + +
+ + + + + + + + + + + + + +
+
+
+ + + + openstack.grant.tree + openstack.grant + + + + + + + + + + + + + + + openstack.grant.select + openstack.grant + search + + + + + + + + + + + + OpenStack Grants + openstack.grant + tree,form + + + + + + + + openstack.grant.type.form + openstack.grant.type + +
+ + + + + + + + + +
+
+
+ + + + openstack.grant.type.tree + openstack.grant.type + + + + + + + + + + + + OpenStack Grant Types + openstack.grant.type + tree,form + + + + + +
+
diff --git a/views/main_menu.xml b/views/main_menu.xml new file mode 100755 index 0000000..c73f8a6 --- /dev/null +++ b/views/main_menu.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/views/project_view.xml b/views/project_view.xml new file mode 100755 index 0000000..aa84b1a --- /dev/null +++ b/views/project_view.xml @@ -0,0 +1,122 @@ + + + + + + + openstack.project.form + openstack.project + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+
+
+ + + + openstack.project.tree + openstack.project + + + + + + + + + + + + + + openstack.project.select + openstack.project + search + + + + + + + + + + + + + + + + + + OpenStack Projects + openstack.project + tree,form + {"search_default_filter_openstack_project":1} + + + + + +
+
diff --git a/views/referral_view.xml b/views/referral_view.xml new file mode 100755 index 0000000..eb37119 --- /dev/null +++ b/views/referral_view.xml @@ -0,0 +1,83 @@ + + + + + + + openstack.referral_code.form + openstack.referral_code + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + openstack.referral_code.tree + openstack.referral_code + + + + + + + + + + + openstack.referral_code.select + openstack.referral_code + search + + + + + + + + + + + OpenStack Referral Codes + openstack.referral_code + tree,form + + + + + +
+
diff --git a/views/report_invoice.xml b/views/report_invoice.xml new file mode 100755 index 0000000..0cd927e --- /dev/null +++ b/views/report_invoice.xml @@ -0,0 +1,354 @@ + + + + + + + + + + + + + diff --git a/views/res_partner_view.xml b/views/res_partner_view.xml new file mode 100755 index 0000000..f3e4db7 --- /dev/null +++ b/views/res_partner_view.xml @@ -0,0 +1,49 @@ + + + + + + + res.partner.openstack.project.form + res.partner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/views/reseller_view.xml b/views/reseller_view.xml new file mode 100755 index 0000000..e422cc1 --- /dev/null +++ b/views/reseller_view.xml @@ -0,0 +1,121 @@ + + + + + + + openstack.reseller.form + openstack.reseller + +
+ + + + + + + + + + + + + + +
+
+
+ + + + openstack.reseller.tree + openstack.reseller + + + + + + + + + + + + + openstack.reseller.select + openstack.reseller + search + + + + + + + + + + + + OpenStack Resellers + openstack.reseller + tree,form + + + + + + + + openstack.reseller.tier.form + openstack.reseller.tier + +
+ + + + + + + + + + + + + + + + +
+
+
+ + + + openstack.reseller.tier.tree + openstack.reseller.tier + + + + + + + + + + OpenStack Reseller Tiers + openstack.reseller.tier + tree,form + + + + + +
+
diff --git a/views/sale_order_view.xml b/views/sale_order_view.xml new file mode 100755 index 0000000..aac46d8 --- /dev/null +++ b/views/sale_order_view.xml @@ -0,0 +1,34 @@ + + + + + + + sale.order.form + sale.order + form + + + + + + + + + + + + + + + + + + + + + + + diff --git a/views/support_subscription_view.xml b/views/support_subscription_view.xml new file mode 100755 index 0000000..f607269 --- /dev/null +++ b/views/support_subscription_view.xml @@ -0,0 +1,117 @@ + + + + + + + openstack.support_subscription.form + openstack.support_subscription + +
+ + + + + + + + + + + + + + + +
+
+
+ + + + openstack.support_subscription.tree + openstack.support_subscription + + + + + + + + + + + + + + + openstack.support_subscription.select + openstack.support_subscription + search + + + + + + + + + + + OpenStack Support Subscriptions + openstack.support_subscription + tree,form + + + + + + + + openstack.support_subscription.type.form + openstack.support_subscription.type + +
+ + + + + + + + +
+
+
+ + + + openstack.support_subscription.type.tree + openstack.support_subscription.type + + + + + + + + + + + + OpenStack Support Subscription Types + openstack.support_subscription.type + tree,form + + + + + +
+
diff --git a/views/term_discount_view.xml b/views/term_discount_view.xml new file mode 100755 index 0000000..63a7c9b --- /dev/null +++ b/views/term_discount_view.xml @@ -0,0 +1,74 @@ + + + + + + + openstack.term_discount.form + openstack.term_discount + +
+ + + + + + + + + + + + + + + + + +
+
+
+ + + + openstack.term_discount.tree + openstack.term_discount + + + + + + + + + + + + + + + openstack.term_discount.select + openstack.term_discount + search + + + + + + + + + + OpenStack Term Discounts + openstack.term_discount + tree,form + + + + + +
+
diff --git a/views/trial_view.xml b/views/trial_view.xml new file mode 100755 index 0000000..c2034e8 --- /dev/null +++ b/views/trial_view.xml @@ -0,0 +1,70 @@ + + + + + + + openstack.trial.form + openstack.trial + +
+ + + + + + + + + + + + + +
+
+
+ + + + openstack.trial.tree + openstack.trial + + + + + + + + + + + + + + + openstack.trial.select + openstack.trial + search + + + + + + + + + + OpenStack Trials + openstack.trial + tree,form + + + + + +
+
diff --git a/views/volume_discount_view.xml b/views/volume_discount_view.xml new file mode 100755 index 0000000..e0589d0 --- /dev/null +++ b/views/volume_discount_view.xml @@ -0,0 +1,65 @@ + + + + + + + openstack.volume_discount_range.form + openstack.volume_discount_range + +
+ + + + + + + + + + +
+
+
+ + + + openstack.volume_discount_range.tree + openstack.volume_discount_range + + + + + + + + + + + + + openstack.volume_discount_range.select + openstack.volume_discount_range + search + + + + + + + + + + OpenStack Volume Discount Ranges + openstack.volume_discount_range + tree,form + + + + + +
+
diff --git a/views/voucher_code_view.xml b/views/voucher_code_view.xml new file mode 100755 index 0000000..6672ce4 --- /dev/null +++ b/views/voucher_code_view.xml @@ -0,0 +1,85 @@ + + + + + + + openstack.voucher_code.form + openstack.voucher_code + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + openstack.voucher_code.tree + openstack.voucher_code + + + + + + + + + + + + + + openstack.voucher_code.select + openstack.voucher_code + search + + + + + + + + + + + + + + OpenStack Voucher Codes + openstack.voucher_code + tree,form + + + + + +
+