diff --git a/argocd_capacity/views/application_tag_view.xml b/argocd_capacity/views/application_tag_view.xml index 2e76633..2d94ec2 100644 --- a/argocd_capacity/views/application_tag_view.xml +++ b/argocd_capacity/views/application_tag_view.xml @@ -4,7 +4,7 @@ argocd.application.tag - + diff --git a/argocd_capacity/views/application_template_view.xml b/argocd_capacity/views/application_template_view.xml index f6fd982..5ecbfce 100644 --- a/argocd_capacity/views/application_template_view.xml +++ b/argocd_capacity/views/application_template_view.xml @@ -4,7 +4,7 @@ argocd.application.template - + diff --git a/argocd_deployer/__manifest__.py b/argocd_deployer/__manifest__.py index f733b84..20328f1 100644 --- a/argocd_deployer/__manifest__.py +++ b/argocd_deployer/__manifest__.py @@ -10,6 +10,7 @@ "data/application_namespace_prefix.xml", "data/application_set_template.xml", "data/application_set.xml", + "views/application_domain_view.xml", "views/application_template_view.xml", "views/application_set_template_view.xml", "views/application_tag_view.xml", diff --git a/argocd_deployer/data/application_set.xml b/argocd_deployer/data/application_set.xml index b36018c..efd5725 100644 --- a/argocd_deployer/data/application_set.xml +++ b/argocd_deployer/data/application_set.xml @@ -17,8 +17,6 @@ main /home/tarteo/repo instances - %(application_name)s.curq.k8s.onestein.eu - %(subdomain)s.%(application_name)s.curq.k8s.onestein.eu diff --git a/argocd_deployer/demo/application_tag_demo.xml b/argocd_deployer/demo/application_tag_demo.xml index b76c4f6..8de3dc6 100644 --- a/argocd_deployer/demo/application_tag_demo.xml +++ b/argocd_deployer/demo/application_tag_demo.xml @@ -9,6 +9,5 @@ Matomo Server matomo_server - matomo.domain diff --git a/argocd_deployer/models/__init__.py b/argocd_deployer/models/__init__.py index 29e08db..36c5909 100644 --- a/argocd_deployer/models/__init__.py +++ b/argocd_deployer/models/__init__.py @@ -3,6 +3,6 @@ from . import application_template from . import application from . import application_tag -from . import application_tag_domain_override from . import application_value from . import application_namespace_prefix +from . import application_domain diff --git a/argocd_deployer/models/application.py b/argocd_deployer/models/application.py index b7d8531..8d56cbf 100644 --- a/argocd_deployer/models/application.py +++ b/argocd_deployer/models/application.py @@ -2,9 +2,7 @@ import re import jinja2 -import yaml from git import Repo -from yaml import Loader from odoo import _, api, fields, models from odoo.exceptions import ValidationError @@ -42,6 +40,11 @@ class Application(models.Model): inverse_name="application_id", string="Values", ) + domain_ids = fields.One2many( + comodel_name="argocd.application.domain", + inverse_name="application_id", + string="Domains", + ) application_set_id = fields.Many2one( "argocd.application.set", ) @@ -61,23 +64,12 @@ def has_tag(self, key): self.ensure_one() return bool(self.tag_ids.filtered(lambda t: t.key == key)) - def format_domain(self, subdomain=None): - """ - Helper method for generating the yaml / helm values. If no domain is specified in e.g. value_ids this can be used - to make a default domain. - Uses config parameters `argocd.application_subdomain_format` and `argocd.application_domain_format` for the format. - - @param subdomain: tag key (e.g. matomo) - @return: formatted domain - """ + def create_domain(self, preferred, *alternatives, scope="global"): + """Shortcut""" self.ensure_one() - values = {"application_name": self.name} - if subdomain: - domain_format = self.application_set_id.subdomain_format or "" - values["subdomain"] = subdomain - else: - domain_format = self.application_set_id.domain_format or "" - return domain_format % values + return self.env["argocd.application.domain"].create_domain( + self, preferred, *alternatives, scope=scope + ) @api.depends("config") def _compute_description(self): @@ -132,28 +124,16 @@ def _render_description(self): raise_if_not_found=False, ) - @staticmethod - def _get_domain(helm): - return helm.get("domain") or helm.get("global", {}).get("domain") - def get_urls(self): self.ensure_one() urls = [] - if not self.config: - return urls - - config = yaml.load(self.config, Loader=Loader) - helm = yaml.load(config["helm"], Loader=Loader) - urls.append(("https://%s" % self._get_domain(helm), "Odoo")) - for tag in self.tag_ids.filtered(lambda t: bool(t.domain_yaml_path)): - yaml_path = tag.get_domain_yaml_path(self.application_set_id).split(".") - domain = helm - for p in yaml_path: - domain = domain.get(p) - if not domain: - break - else: - urls.append(("https://%s" % domain, tag.name)) + for scope in self.domain_ids.mapped("scope"): + prioritized_domain = self.domain_ids.filtered( + lambda d: d.scope == scope + ).sorted("sequence")[0] + urls.append( + ("https://%s" % prioritized_domain.name, prioritized_domain.scope) + ) return urls @api.depends("tag_ids", "tag_ids.is_odoo_module") @@ -163,20 +143,32 @@ def _compute_modules(self): application.tag_ids.filtered(lambda t: t.is_odoo_module).mapped("key") ) - _sql_constraints = [("application_name_unique", "unique(name)", "Already exists")] + _sql_constraints = [ + ( + "application_name_unique", + "unique(application_set_id, name)", + "Already exists in this application set", + ) + ] @api.model - def find_next_available_name(self, name): + def find_next_available_name(self, app_set, name): """ Find a name which is available based on name (e.g. greg2) + @param app_set: application set @param name: a name @return: first available name """ - if not self.search([("name", "=", name)], count=True): + if not self.search( + [("application_set_id", "=", app_set.id), ("name", "=", name)], count=True + ): return name i = 0 - while self.search([("name", "=", name + str(i))], count=True): + while self.search( + [("application_set_id", "=", app_set.id), ("name", "=", name + str(i))], + count=True, + ): i += 1 return name + str(i) @@ -197,7 +189,7 @@ def _get_config_render_values(self): "application": self, "has_tag": self.has_tag, "get_value": self.get_value, - "format_domain": self.format_domain, + "create_domain": self.create_domain, } def render_config(self, context=None): @@ -263,11 +255,11 @@ def immediate_destroy(self): self.ensure_one() self._apply_repository_changes(self._get_destroy_content) - def destroy(self): + def destroy(self, eta=0): self.ensure_one() delay = safe_eval( self.env["ir.config_parameter"].get_param( "argocd.application_destruction_delay", "0" ) ) - self.with_delay(eta=delay).immediate_destroy() + self.with_delay(eta=eta or delay).immediate_destroy() diff --git a/argocd_deployer/models/application_domain.py b/argocd_deployer/models/application_domain.py new file mode 100644 index 0000000..ff3c164 --- /dev/null +++ b/argocd_deployer/models/application_domain.py @@ -0,0 +1,46 @@ +from odoo import api, fields, models + + +class ApplicationDomain(models.Model): + _name = "argocd.application.domain" + _description = "ArgoCD Application Domain" + _order = "sequence" + + application_id = fields.Many2one(comodel_name="argocd.application", required=True) + scope = fields.Char(default="Application") + sequence = fields.Integer(default=10) + name = fields.Char(required=True) + + _sql_constraints = [ + ( + "application_domain_name_unique", + "unique(name)", + "Domain is already in use", + ) + ] + + @api.model + def create_domain(self, application, preferred, *alternatives, scope="Application"): + existing = application.domain_ids.filtered(lambda d: d.scope == scope).sorted( + "sequence" + ) + if existing: + return existing.name + domains = (preferred,) + alternatives + i = 0 + best_available = False + while not best_available: + i_as_str = str(i) + for domain in domains: + domain_name = domain + if i: + domain_name += i_as_str + already_exists = self.search([("name", "=", domain_name)], count=True) + if not already_exists: + best_available = domain_name + break + i += 1 + self.create( + {"application_id": application.id, "name": best_available, "scope": scope} + ) + return best_available diff --git a/argocd_deployer/models/application_set.py b/argocd_deployer/models/application_set.py index bf122a4..ea03020 100644 --- a/argocd_deployer/models/application_set.py +++ b/argocd_deployer/models/application_set.py @@ -38,16 +38,6 @@ class ApplicationSet(models.Model): deployment_directory = fields.Char( help="Folder inside the repository in which to store the application YAML files.", ) - domain_format = fields.Char( - required=True, - help="The domain format used to build the domain for the deployment.", - default="-", - ) - subdomain_format = fields.Char( - required=True, - help="The domain format used to build the domain for the deployment.", - default="-", - ) namespace_prefix_id = fields.Many2one("argocd.application.namespace.prefix") _sql_constraints = [ diff --git a/argocd_deployer/models/application_tag.py b/argocd_deployer/models/application_tag.py index b817855..e28d8f6 100644 --- a/argocd_deployer/models/application_tag.py +++ b/argocd_deployer/models/application_tag.py @@ -8,25 +8,5 @@ class ApplicationTag(models.Model): name = fields.Char(required=True) key = fields.Char(required=True, copy=False) is_odoo_module = fields.Boolean(string="Is additional Odoo Module") - domain_yaml_path = fields.Char( - help="Path where to find the domain in the yaml (e.g. nextcloud.domain)" - ) - domain_override_ids = fields.One2many( - "argocd.application.tag.domain.override", inverse_name="tag_id" - ) _sql_constraints = [("application_tag_key_unique", "unique(key)", "Already exists")] - - def get_domain_yaml_path(self, application_set=False): - self.ensure_one() - if ( - not application_set - or application_set.id not in self.domain_override_ids.application_set_id.ids - ): - return self.domain_yaml_path or "" - return ( - self.domain_override_ids.filtered( - lambda do: do.application_set_id == application_set - ).domain_yaml_path - or "" - ) diff --git a/argocd_deployer/models/application_tag_domain_override.py b/argocd_deployer/models/application_tag_domain_override.py deleted file mode 100644 index b17d967..0000000 --- a/argocd_deployer/models/application_tag_domain_override.py +++ /dev/null @@ -1,20 +0,0 @@ -from odoo import fields, models - - -class ApplicationTagDomainOverride(models.Model): - _name = "argocd.application.tag.domain.override" - _description = "Application Tag Domain Override" - - tag_id = fields.Many2one("argocd.application.tag") - application_set_id = fields.Many2one("argocd.application.set") - domain_yaml_path = fields.Char( - help="Path where to find the domain in the yaml (e.g. nextcloud.domain)" - ) - - _sql_constraints = [ - ( - "application_tag_key_unique", - "unique(tag_id, application_set_id)", - "Override already exists.", - ) - ] diff --git a/argocd_deployer/security/ir.model.access.csv b/argocd_deployer/security/ir.model.access.csv index af621aa..47f4b2c 100644 --- a/argocd_deployer/security/ir.model.access.csv +++ b/argocd_deployer/security/ir.model.access.csv @@ -5,5 +5,5 @@ application_tag_access,application_tag_access,model_argocd_application_tag,base. application_value_access,application_value_access,model_argocd_application_value,base.group_system,1,1,1,1 application_set_access,application_set_access,model_argocd_application_set,base.group_system,1,1,1,1 application_set_template_access,application_set_template_access,model_argocd_application_set_template,base.group_system,1,1,1,1 -application_tag_domain_override_access,application_tag_domain_override_access,model_argocd_application_tag_domain_override,base.group_system,1,1,1,1 application_namespace_prefix_access,application_namespace_prefix_access,model_argocd_application_namespace_prefix,base.group_system,1,1,1,1 +application_domain_access,application_domain_access,model_argocd_application_domain,base.group_system,1,1,1,1 diff --git a/argocd_deployer/tests/test_application_tag.py b/argocd_deployer/tests/test_application_tag.py deleted file mode 100644 index a6f1b86..0000000 --- a/argocd_deployer/tests/test_application_tag.py +++ /dev/null @@ -1,58 +0,0 @@ -from odoo import Command -from odoo.tests.common import TransactionCase - - -class TestApplicationTag(TransactionCase): - def test_get_domain_yaml_path(self): - """get_domain_yaml_path returns a reasonable value in all circumstances.""" - - template = self.env["argocd.application.set.template"].create( - { - "name": "test-set-template", - "yaml": """test: - me: domain""", - } - ) - application_set_1 = self.env["argocd.application.set"].create( - { - "name": "test-set", - "template_id": template.id, - "repository_url": "http://hello.com", - "branch": "hello", - "repository_directory": "hello", - "deployment_directory": "bye", - } - ) - application_set_2 = self.env["argocd.application.set"].create( - { - "name": "test-set-also", - "template_id": template.id, - "repository_url": "http://hello.com", - "branch": "hello", - "repository_directory": "hello", - "deployment_directory": "byeagain", - } - ) - tag = self.env["argocd.application.tag"].create( - {"name": "test-tag", "domain_yaml_path": False, "key": "thegoldentowerkey"} - ) - - self.assertFalse(tag.get_domain_yaml_path()) - self.assertFalse(tag.get_domain_yaml_path(application_set_1)) - - tag.domain_yaml_path = "test.me" - self.assertEqual("test.me", tag.get_domain_yaml_path()) - self.assertEqual("test.me", tag.get_domain_yaml_path(application_set_1)) - - tag.domain_override_ids = [ - Command.create( - { - "application_set_id": application_set_1.id, - "domain_yaml_path": "test.me.now", - } - ) - ] - - self.assertEqual("test.me", tag.get_domain_yaml_path()) - self.assertEqual("test.me.now", tag.get_domain_yaml_path(application_set_1)) - self.assertEqual("test.me", tag.get_domain_yaml_path(application_set_2)) diff --git a/argocd_deployer/views/application_domain_view.xml b/argocd_deployer/views/application_domain_view.xml new file mode 100644 index 0000000..ac421fc --- /dev/null +++ b/argocd_deployer/views/application_domain_view.xml @@ -0,0 +1,13 @@ + + + + argocd.application.domain + + + + + + + + + diff --git a/argocd_deployer/views/application_set_view.xml b/argocd_deployer/views/application_set_view.xml index e7fbb6a..f4c66d4 100644 --- a/argocd_deployer/views/application_set_view.xml +++ b/argocd_deployer/views/application_set_view.xml @@ -78,8 +78,6 @@ - - diff --git a/argocd_deployer/views/application_tag_view.xml b/argocd_deployer/views/application_tag_view.xml index 414bd43..894226d 100644 --- a/argocd_deployer/views/application_tag_view.xml +++ b/argocd_deployer/views/application_tag_view.xml @@ -9,15 +9,6 @@ - - - - - - - - - diff --git a/argocd_deployer/views/application_view.xml b/argocd_deployer/views/application_view.xml index 9f9384b..88c1aa7 100644 --- a/argocd_deployer/views/application_view.xml +++ b/argocd_deployer/views/application_view.xml @@ -80,6 +80,7 @@ + diff --git a/argocd_sale/__manifest__.py b/argocd_sale/__manifest__.py index 2aa2351..f192a2d 100644 --- a/argocd_sale/__manifest__.py +++ b/argocd_sale/__manifest__.py @@ -20,7 +20,8 @@ "data/ir_config_parameter_data.xml", "views/product_template.xml", "views/application_view.xml", - "views/application_template_view.xml", "views/res_config_settings_view.xml", + "views/sale_subscription_view.xml", + "views/product_attribute_view.xml", ], } diff --git a/argocd_sale/data/ir_config_parameter_data.xml b/argocd_sale/data/ir_config_parameter_data.xml index c310dda..6ccb806 100644 --- a/argocd_sale/data/ir_config_parameter_data.xml +++ b/argocd_sale/data/ir_config_parameter_data.xml @@ -12,12 +12,4 @@ argocd_sale.grace_period_tag_id - - argocd_sale.subscription_free_period - 3 - - - argocd_sale.subscription_free_period_type - months - diff --git a/argocd_sale/models/__init__.py b/argocd_sale/models/__init__.py index 7c5d432..feed92e 100644 --- a/argocd_sale/models/__init__.py +++ b/argocd_sale/models/__init__.py @@ -1,7 +1,9 @@ from . import product_template from . import account_move from . import application -from . import application_template from . import res_partner from . import subscription +from . import sale_subscription_line from . import res_config_settings +from . import product_attribute +from . import product_attribute_value diff --git a/argocd_sale/models/application.py b/argocd_sale/models/application.py index 57927f2..d1d21c6 100644 --- a/argocd_sale/models/application.py +++ b/argocd_sale/models/application.py @@ -1,5 +1,4 @@ -from odoo import _, api, fields, models -from odoo.exceptions import UserError +from odoo import api, fields, models class Application(models.Model): @@ -11,6 +10,21 @@ class Application(models.Model): store=True, readonly=False, ) + subscription_id = fields.Many2one( + comodel_name="sale.subscription", + compute="_compute_subscription_id", + store=True, + readonly=False, + ) + subscription_line_id = fields.Many2one(comodel_name="sale.subscription.line") + + _sql_constraints = [ + ( + "application_subscription_line_id_unique", + "unique(subscription_line_id)", + "Only one application can be linked to a subscription line", + ) + ] def is_created_by_reseller(self): self.ensure_one() @@ -18,27 +32,25 @@ def is_created_by_reseller(self): self.partner_id.parent_id and self.partner_id.parent_id.is_reseller ) - subscription_id = fields.Many2one(comodel_name="sale.subscription") - - @api.depends("subscription_id", "subscription_id.partner_id") + def get_attribute(self, argocd_identifier): + self.ensure_one() + variant_value = self.subscription_line_id.product_id.product_template_variant_value_ids.filtered( + lambda kv: kv.attribute_id.argocd_identifier == argocd_identifier + ).product_attribute_value_id + return variant_value.argocd_name or variant_value.name + + @api.depends("subscription_line_id", "subscription_line_id.sale_subscription_id") + def _compute_subscription_id(self): + for app in self.filtered(lambda a: a.subscription_line_id): + app.subscription_id = app.subscription_line_id.sale_subscription_id + + @api.depends( + "subscription_id", + "subscription_id.partner_id", + "subscription_id.end_partner_id", + ) def _compute_partner_id(self): for app in self.filtered(lambda a: a.subscription_id): - app.partner_id = app.subscription_id.partner_id - - def _get_deployment_notification_mail_template(self): - self.ensure_one() - return "argocd_sale.deployment_notification_mail_template" - - def send_deployment_notification(self): - self.ensure_one() - if not self.partner_id: - raise UserError(_("Please provide a partner")) - mail_template_id = self._get_deployment_notification_mail_template() - template = self.env.ref(mail_template_id) - template.sudo().send_mail(self.id, force_send=True) - - def deploy(self): - res = super().deploy() - if self.template_id.auto_send_deployment_notification and self.partner_id: - self.send_deployment_notification() - return res + app.partner_id = ( + app.subscription_id.end_partner_id or app.subscription_id.partner_id + ) diff --git a/argocd_sale/models/application_template.py b/argocd_sale/models/application_template.py deleted file mode 100644 index 03f0d50..0000000 --- a/argocd_sale/models/application_template.py +++ /dev/null @@ -1,11 +0,0 @@ -from odoo import fields, models - - -class ApplicationTemplate(models.Model): - _inherit = "argocd.application.template" - - auto_send_deployment_notification = fields.Boolean( - default=True, - string="Automatically send deployment notification", - help="Determines if an email is automatically send to the partner if deployed.", - ) diff --git a/argocd_sale/models/product_attribute.py b/argocd_sale/models/product_attribute.py new file mode 100644 index 0000000..8fb9b44 --- /dev/null +++ b/argocd_sale/models/product_attribute.py @@ -0,0 +1,10 @@ +from odoo import fields, models + + +class ProductAttribute(models.Model): + _inherit = "product.attribute" + + argocd_identifier = fields.Char( + string="ArgoCD Identifier", + help="Makes it easier to look it up when rendering the YAML config for applications", + ) diff --git a/argocd_sale/models/product_attribute_value.py b/argocd_sale/models/product_attribute_value.py new file mode 100644 index 0000000..26c6e3e --- /dev/null +++ b/argocd_sale/models/product_attribute_value.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ProductAttributeValue(models.Model): + _inherit = "product.attribute.value" + + argocd_name = fields.Char(string="Value in ArgoCD") diff --git a/argocd_sale/models/product_template.py b/argocd_sale/models/product_template.py index 9e1f91b..3c16194 100644 --- a/argocd_sale/models/product_template.py +++ b/argocd_sale/models/product_template.py @@ -4,6 +4,11 @@ class ProductTemplate(models.Model): _inherit = "product.template" + application_set_id = fields.Many2one( + comodel_name="argocd.application.set", + help="The application set in which this product will be deployed.", + ) + application_template_id = fields.Many2one( string="Application Template", comodel_name="argocd.application.template" ) @@ -13,11 +18,6 @@ class ProductTemplate(models.Model): string="Application Tags", ) - application_filter_ids = fields.Many2many( - comodel_name="argocd.application.template", - string="Only applies to (leave empty for all)", - ) - reseller_partner_ids = fields.Many2many( comodel_name="res.partner", relation="product_reseller_rel", @@ -26,7 +26,9 @@ class ProductTemplate(models.Model): column2="partner_id", ) - application_set_id = fields.Many2one( - comodel_name="argocd.application.set", - help="The application set in which this product will be deployed.", + allowed_reseller_partner_ids = fields.Many2many( + comodel_name="res.partner", + relation="product_reseller_all_rel", + column1="product_template_id", + column2="partner_id", ) diff --git a/argocd_sale/models/res_partner.py b/argocd_sale/models/res_partner.py index 99b2230..b808a45 100644 --- a/argocd_sale/models/res_partner.py +++ b/argocd_sale/models/res_partner.py @@ -7,13 +7,42 @@ class ResPartner(models.Model): reselling_product_ids = fields.Many2many( comodel_name="product.template", relation="product_reseller_rel", - string="Resellers", + string="Reselling Products", + column1="partner_id", + column2="product_template_id", + ) + allowed_reselling_products_ids = fields.Many2many( + comodel_name="product.template", + compute="_compute_allowed_reselling_products_ids", + store=True, + relation="product_reseller_all_rel", column1="partner_id", column2="product_template_id", ) is_reseller = fields.Boolean(compute="_compute_is_reseller") - @api.depends("reselling_product_ids") + @api.depends( + "reselling_product_ids", "parent_id", "parent_id.reselling_product_ids" + ) + def _compute_allowed_reselling_products_ids(self): + for partner in self: + allowed_reselling_products_ids = partner.reselling_product_ids + if partner.parent_id: + allowed_reselling_products_ids += ( + partner.parent_id.reselling_product_ids + ) # Notice no recursion + partner.allowed_reselling_products_ids = allowed_reselling_products_ids + + @api.depends("allowed_reselling_products_ids") def _compute_is_reseller(self): for partner in self: - partner.is_reseller = bool(partner.reselling_product_ids) + partner.is_reseller = bool(partner.allowed_reselling_products_ids) + + def to_valid_subdomain(self): + self.ensure_one() + replacements = {" ": "-", ".": "", "&": "-", "_": "-"} + name = self.display_name + name = name.strip().lower() + for replace in replacements: + name = name.replace(replace, replacements[replace]) + return "".join(c for c in name if c.isalnum() or c == "-") diff --git a/argocd_sale/models/sale_subscription_line.py b/argocd_sale/models/sale_subscription_line.py new file mode 100644 index 0000000..0a754c3 --- /dev/null +++ b/argocd_sale/models/sale_subscription_line.py @@ -0,0 +1,88 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class SubscriptionLine(models.Model): + _inherit = "sale.subscription.line" + + application_ids = fields.One2many( + comodel_name="argocd.application", + inverse_name="subscription_line_id", + help="This is essentially a one2one as subscription_line_id" + " is unique in argocd.application", + ) + + def _to_application_name(self): + self.ensure_one() + replacements = {" ": "-", ".": "", "&": "-"} + # It's not possible to have more than one application linked to a + # subscription line because of the sql constraint. + # Let's assume that here. + product = self.product_id + partner = self.sale_subscription_id.partner_id.commercial_partner_id + name = "-".join([partner.display_name, product.default_code or product.name]) + name = name.strip().lower() + for replace in replacements: + name = name.replace(replace, replacements[replace]) + return "".join(c for c in name if c.isalnum() or c == "-") + + def write(self, vals): + to_redeploy = self.env["argocd.application"] + if "product_id" in vals: + product = self.env["product.product"].browse(vals["product_id"]) + # TODO: Enforce only up here e.g. 50GB to 20GB in some cases should not be allowed, but 5 users to 4 should + changed_lines = self.filtered( + lambda l: l.application_ids and product != l.product_id + ) + invalid_changes = changed_lines.filtered( + lambda l: product.product_tmpl_id != l.product_id.product_tmpl_id + ) + # TODO: Enforce only up here e.g. 50GB to 20GB in some cases should not be allowed, but 5 users to 4 should + if invalid_changes: + raise UserError( + _( + "This variant has a different product template, please create a new line and delete this one instead." + ) + ) + to_redeploy += changed_lines.mapped("application_ids") + + if "product_uom_qty" in vals: + qty = int( + vals["product_uom_qty"] + ) # Cast to int just to make sure. I'm not sure if it's required + to_redeploy += self.filtered( + lambda l: l.application_ids and l.product_uom_qty != qty + ).mapped("application_ids") + res = super().write(vals) + for app in to_redeploy: + app.render_config() + app.deploy() + return res + + @api.ondelete(at_uninstall=False) + def _unlink_and_destroy_app(self): + for line in self.filtered(lambda l: l.application_ids): + delta = line.sale_subscription_id.recurring_next_date - fields.Date.today() + line.application_ids.destroy(eta=int(delta.total_seconds())) + + def _invoice_paid_hook(self): + self.ensure_one() + application_sudo = self.env["argocd.application"].sudo() + + if self.application_ids or not self.product_id.application_template_id: + return + + name = application_sudo.find_next_available_name( + self.product_id.application_set_id, self._to_application_name() + ) + application = application_sudo.create( + { + "name": name, + "subscription_line_id": self.id, + "tag_ids": self.product_id.application_tag_ids.ids, + "template_id": self.product_id.application_template_id.id, + "application_set_id": self.product_id.application_set_id.id, + } + ) + application.render_config() + application.deploy() diff --git a/argocd_sale/models/subscription.py b/argocd_sale/models/subscription.py index edc169a..0215118 100644 --- a/argocd_sale/models/subscription.py +++ b/argocd_sale/models/subscription.py @@ -1,9 +1,6 @@ from datetime import timedelta -from dateutil.relativedelta import relativedelta - -from odoo import Command, _, api, fields, models -from odoo.exceptions import ValidationError +from odoo import Command, _, fields, models class Subscription(models.Model): @@ -12,24 +9,12 @@ class Subscription(models.Model): application_ids = fields.One2many( comodel_name="argocd.application", inverse_name="subscription_id" ) + end_partner_id = fields.Many2one(comodel_name="res.partner") + application_count = fields.Integer(compute="_compute_application_count") - free_trial_end_date = fields.Date() - - def create_invoice(self): - res = super().create_invoice() - if ( - self.sale_subscription_line_ids.filtered( - lambda l: l.product_id.application_template_id - ) - and len(self.invoice_ids) == 1 - ): - # Set price of the first invoice to 1.00 - free_period = self._get_free_period() - if free_period: - self.invoice_ids.invoice_line_ids.filtered( - lambda l: l.product_id.application_template_id - ).price_unit = 1.0 - return res + def _compute_application_count(self): + for sub in self: + sub.application_count = len(sub.application_ids) def _get_grace_period(self): return int( @@ -38,66 +23,22 @@ def _get_grace_period(self): .get_param("argocd_sale.grace_period", "0") ) - @api.constrains("sale_subscription_line_ids") - def _check_multiple_application_products(self): - app_lines = self.sale_subscription_line_ids.filtered( - lambda l: l.product_id.application_template_id - ) - if len(app_lines) > 1: - raise ValidationError( - _("Subscription can only have one application, please remove one") - ) - - def _customer_name_to_application_name(self): - self.ensure_one() - replacements = {" ": "-", ".": "", "&": "-"} - partner = self.partner_id.commercial_partner_id - name = partner.display_name - name = name.strip().lower() - for replace in replacements: - name = name.replace(replace, replacements[replace]) - return "".join(c for c in name if c.isalnum() or c == "-") - def _invoice_paid_hook(self): - application_sudo = self.env["argocd.application"].sudo() - - for subscription in self.filtered( - lambda i: len(i.invoice_ids) == 1 - and i.sale_subscription_line_ids.filtered( + for subscription in self: + # Start the subscription, which is not done by subscription_oca, so we do it here for our purposes + # this probably should be moved to subscription_oca. + if len( + subscription.invoice_ids + ) == 1 and subscription.sale_subscription_line_ids.filtered( lambda l: l.product_id.application_template_id - ) - ): # Create the application after the first invoice has been paid - free_period = subscription._get_free_period() - if free_period: - today = fields.Datetime.today() - subscription.recurring_next_date = today + free_period - subscription.free_trial_end_date = today + free_period - subscription.action_start_subscription() + ): + subscription.action_start_subscription() + lines = subscription.sale_subscription_line_ids.filtered( lambda l: l.product_id.application_template_id ) for line in lines: - name = application_sudo.find_next_available_name( - self._customer_name_to_application_name() - ) - tags = subscription.sale_subscription_line_ids.filtered( - lambda l: l.product_id.application_tag_ids - and not l.product_id.application_filter_ids # All lines with modules linked to them - or line.product_id.application_template_id # If there's no filter - in l.product_id.application_filter_ids # If there's a filter - ).mapped("product_id.application_tag_ids") - - application = application_sudo.create( - { - "name": name, - "subscription_id": subscription.id, - "tag_ids": tags.ids, - "template_id": line.product_id.application_template_id.id, - "application_set_id": line.product_id.application_set_id.id, - } - ) - application.render_config() - application.deploy() + line._invoice_paid_hook() def _do_grace_period_action(self): """ @@ -109,7 +50,8 @@ def _do_grace_period_action(self): "argocd_sale.grace_period_action" ) if not grace_period_action: - return False # Do nothing + return False + linked_apps = self.mapped("sale_subscription_line_ids.application_ids") if grace_period_action == "add_tag": grace_period_tag_id = int( self.env["ir.config_parameter"].get_param( @@ -121,70 +63,46 @@ def _do_grace_period_action(self): tag = self.env["argocd.application.tag"].browse(grace_period_tag_id) if not tag: return False - self.mapped("application_ids").write({"tag_ids": [Command.link(tag.id)]}) + linked_apps.write({"tag_ids": [Command.link(tag.id)]}) elif grace_period_action == "destroy_app": - self.mapped("application_ids").destroy() + linked_apps.destroy() return True - def cron_update_payment_provider_subscriptions(self): + def cron_update_payment_provider_payments(self): # Process last payments first because in here paid_for_date can be updated - res = super().cron_update_payment_provider_subscriptions() + res = super().cron_update_payment_provider_payments() period = self._get_grace_period() if not period: return res today = fields.Date.today() late_date = today - timedelta(days=period) late_subs = self.search( - [ - ("paid_for_date", "<", late_date), - ("in_progress", "=", True), - "|", - ("free_trial_end_date", "<", today), - ("free_trial_end_date", "=", False), - ] + [("paid_for_date", "<", late_date), ("in_progress", "=", True)] ) - if late_subs.filtered( - lambda s: not s.free_trial_end_date - or s.free_trial_end_date + timedelta(days=period) < today - ): - late_subs.close_subscription() - late_subs._do_grace_period_action() + for late_sub in late_subs: + late_sub.with_context( + no_destroy_app=True + ).close_subscription() # no_destroy_app since we're doing the grace period action after this. + late_subs._do_grace_period_action() return res - def _get_free_period(self): - self.ensure_one() - if ( - self.partner_id.is_reseller - or self.partner_id.parent_id - and self.partner_id.parent_id.is_reseller - ): - return None - existing_subs = self.partner_id.subscription_ids - if self.partner_id.parent_id: - existing_subs += self.partner_id.parent_id.subscription_ids - existing_subs -= self - if existing_subs: - return None - sudo_config = self.env["ir.config_parameter"].sudo() - free_period = int( - sudo_config.get_param("argocd_sale.subscription_free_period", "0") - ) - if not free_period: - return None - free_period_type = sudo_config.get_param( - "argocd_sale.subscription_free_period_type" - ) - valid_period_types = ( - "years", - "months", - "days", - "leapdays", - "weeks", - "hours", - "minutes", - "seconds", - "microseconds", - ) - if free_period_type not in valid_period_types: - return None - return relativedelta(**{free_period_type: free_period}) + def close_subscription(self, close_reason_id=False): + if not self.env.context.get( + "no_destroy_app", False + ): # This is fine since portal users don't have write access on sale.subscription and the super writes the record + # Destroy app + self.ensure_one() + delta = self.recurring_next_date - fields.Date.today() + for line in self.filtered(lambda l: l.application_ids): + line.application_ids.destroy(eta=int(delta.total_seconds())) + return super().close_subscription(close_reason_id) + + def view_applications(self): + return { + "name": _("Applications"), + "view_type": "form", + "view_mode": "tree,form", + "res_model": "argocd.application", + "type": "ir.actions.act_window", + "domain": [("id", "in", self.application_ids.ids)], + } diff --git a/argocd_sale/tests/__init__.py b/argocd_sale/tests/__init__.py index 1228ca3..aa69326 100644 --- a/argocd_sale/tests/__init__.py +++ b/argocd_sale/tests/__init__.py @@ -1,5 +1,3 @@ from . import test_reseller from . import test_grace_period -from . import test_free_period -from . import test_both_periods_in_conjuction from . import test_invoice_to_application diff --git a/argocd_sale/tests/test_both_periods_in_conjuction.py b/argocd_sale/tests/test_both_periods_in_conjuction.py deleted file mode 100644 index 7d3a99e..0000000 --- a/argocd_sale/tests/test_both_periods_in_conjuction.py +++ /dev/null @@ -1,112 +0,0 @@ -from dateutil.relativedelta import relativedelta - -from odoo import Command, fields -from odoo.tests.common import TransactionCase - - -class TestFreePeriod(TransactionCase): - @classmethod - def setUpClass(cls): - super(TestFreePeriod, cls).setUpClass() - cls.sub_product_tmpl = cls.env.ref( - "argocd_sale.demo_curq_basis_product_template" - ) - cls.sub_product = cls.sub_product_tmpl.product_variant_ids[0] - cls.sub_tmpl = cls.env.ref("argocd_sale.demo_subscription_template") - cls.partner_id = cls.env.ref("base.partner_admin") - cls.disable_odoo_tag = cls.env["argocd.application.tag"].create( - {"name": "Disable Odoo", "key": "disable_odoo", "is_odoo_module": True} - ) - cls.grace_period = 7 - cls.free_period = 14 - cls.env["ir.config_parameter"].set_param( - "argocd_sale.grace_period", cls.grace_period - ) - cls.env["ir.config_parameter"].set_param( - "argocd_sale.grace_period_tag_id", cls.disable_odoo_tag.id - ) - cls.env["ir.config_parameter"].set_param( - "argocd_sale.grace_period_action", "add_tag" - ) - cls.env["ir.config_parameter"].set_param( - "argocd_sale.subscription_free_period", cls.free_period - ) - cls.env["ir.config_parameter"].set_param( - "argocd_sale.subscription_free_period_type", "days" - ) - - def test_in_trial(self): - """Subs in trial shouldn't be affected by grace period""" - sub = self.env["sale.subscription"].create( - { - "template_id": self.sub_tmpl.id, - "sale_subscription_line_ids": [ - Command.create({"product_id": self.sub_product.id}) - ], - "partner_id": self.partner_id.id, - "pricelist_id": self.partner_id.property_product_pricelist.id, - } - ) - sub.generate_invoice() - sub._invoice_paid_hook() # Trial end date should be today + 7 days - sub.paid_for_date = fields.Date.today() - relativedelta( - days=self.grace_period + 1 - ) # Make sure it's out of the grace period - - self.env["sale.subscription"].cron_update_payment_provider_subscriptions() - self.assertNotIn( - self.disable_odoo_tag, - sub.mapped("application_ids.tag_ids"), - "This sub is in the trial period and shouldn't be affected by grace period", - ) - - def test_just_out_of_trial(self): - """Subs out of trial but not passed the grace period shouldn't be affected by grace period""" - sub = self.env["sale.subscription"].create( - { - "template_id": self.sub_tmpl.id, - "sale_subscription_line_ids": [ - Command.create({"product_id": self.sub_product.id}) - ], - "partner_id": self.partner_id.id, - "pricelist_id": self.partner_id.property_product_pricelist.id, - } - ) - sub.generate_invoice() - sub._invoice_paid_hook() - sub.free_trial_end_date = fields.Date.today() - relativedelta( - days=2 - ) # Two days ago the free trial ended - sub.paid_for_date = sub.free_trial_end_date - relativedelta( - days=self.free_period - ) # But no payment has been made yet (< grace period) - # A payment must be made before trial period end + grace_period - self.env["sale.subscription"].cron_update_payment_provider_subscriptions() - self.assertNotIn( - self.disable_odoo_tag, - sub.mapped("application_ids.tag_ids"), - "This sub is out of the trial period but grace period has not passed so shouldn't be affected by grace period", - ) - - def test_out_of_trial(self): - """Subs out of trial and pass the grace period should be affected""" - sub = self.env["sale.subscription"].create( - { - "template_id": self.sub_tmpl.id, - "sale_subscription_line_ids": [ - Command.create({"product_id": self.sub_product.id}) - ], - "partner_id": self.partner_id.id, - "pricelist_id": self.partner_id.property_product_pricelist.id, - } - ) - sub.generate_invoice() - sub._invoice_paid_hook() - sub.free_trial_end_date = fields.Date.today() - relativedelta( - days=self.grace_period + 1 - ) # Free trial ended - sub.paid_for_date = sub.free_trial_end_date - relativedelta( - days=self.free_period - ) # To make it realistic the date the trial started - self.env["sale.subscription"].cron_update_payment_provider_subscriptions() - self.assertIn(self.disable_odoo_tag, sub.mapped("application_ids.tag_ids")) diff --git a/argocd_sale/tests/test_free_period.py b/argocd_sale/tests/test_free_period.py deleted file mode 100644 index 849ef35..0000000 --- a/argocd_sale/tests/test_free_period.py +++ /dev/null @@ -1,95 +0,0 @@ -from dateutil.relativedelta import relativedelta - -from odoo import Command, fields -from odoo.tests.common import TransactionCase - - -class TestFreePeriod(TransactionCase): - @classmethod - def setUpClass(cls): - super(TestFreePeriod, cls).setUpClass() - cls.sub_product_tmpl = cls.env.ref( - "argocd_sale.demo_curq_basis_product_template" - ) - cls.sub_product = cls.sub_product_tmpl.product_variant_ids[0] - cls.sub_tmpl = cls.env.ref("argocd_sale.demo_subscription_template") - cls.partner_id = cls.env.ref("base.partner_admin") - cls.sub_product.list_price = 30.0 # Make sure it's not 1.0 - - def test_disabled(self): - self.env["ir.config_parameter"].set_param( - "argocd_sale.subscription_free_period", 3 - ) - self.env["ir.config_parameter"].set_param( - "argocd_sale.subscription_free_period_type", "" # This should disable it. - ) - sub = self.env["sale.subscription"].create( - { - "template_id": self.sub_tmpl.id, - "sale_subscription_line_ids": [ - Command.create({"product_id": self.sub_product.id}) - ], - "partner_id": self.partner_id.id, - "pricelist_id": self.partner_id.property_product_pricelist.id, - } - ) - sub.generate_invoice() - sub._invoice_paid_hook() - self.assertEqual( - sub.invoice_ids.amount_untaxed, - 30.0, - "Free period should be disabled", - ) - - def test_next_payment_date_and_price(self): - self.env["ir.config_parameter"].set_param( - "argocd_sale.subscription_free_period", 3 - ) - self.env["ir.config_parameter"].set_param( - "argocd_sale.subscription_free_period_type", "months" - ) - expected_next_payment_date = fields.Date.today() + relativedelta(months=3) - sub = self.env["sale.subscription"].create( - { - "template_id": self.sub_tmpl.id, - "sale_subscription_line_ids": [ - Command.create({"product_id": self.sub_product.id}) - ], - "partner_id": self.partner_id.id, - "pricelist_id": self.partner_id.property_product_pricelist.id, - } - ) - sub.generate_invoice() - sub._invoice_paid_hook() - self.assertEqual(sub.recurring_next_date, expected_next_payment_date) - self.assertEqual( - sub.invoice_ids.amount_untaxed, - 1.0, - "Invoice amount of first invoice must be 1.0", - ) - - sub.generate_invoice() - sub._invoice_paid_hook() - self.assertEqual( - sub.invoice_ids[0].amount_untaxed, # It's order newest to oldest - 30.0, - "Next invoice should be the normal price", - ) - - sub2 = self.env["sale.subscription"].create( - { - "template_id": self.sub_tmpl.id, - "sale_subscription_line_ids": [ - Command.create({"product_id": self.sub_product.id}) - ], - "partner_id": self.partner_id.id, - "pricelist_id": self.partner_id.property_product_pricelist.id, - } - ) - sub2.generate_invoice() - sub2._invoice_paid_hook() - self.assertEqual( - sub2.invoice_ids.amount_untaxed, - 30.0, - "Invoice of new second subscription must be the normal price", - ) diff --git a/argocd_sale/tests/test_grace_period.py b/argocd_sale/tests/test_grace_period.py index 69f6f87..fd946b7 100644 --- a/argocd_sale/tests/test_grace_period.py +++ b/argocd_sale/tests/test_grace_period.py @@ -28,9 +28,6 @@ def setUpClass(cls): cls.env["ir.config_parameter"].set_param( "argocd_sale.grace_period_tag_id", cls.disable_odoo_tag.id ) - cls.env["ir.config_parameter"].set_param( - "argocd_sale.subscription_free_period_type", "" # Disable free period - ) def _create_and_prepare_sub(self, paid_for_date=False, create_invoice=True): sub = self.env["sale.subscription"].create( diff --git a/argocd_sale/tests/test_invoice_to_application.py b/argocd_sale/tests/test_invoice_to_application.py index 95f02e8..57e7152 100644 --- a/argocd_sale/tests/test_invoice_to_application.py +++ b/argocd_sale/tests/test_invoice_to_application.py @@ -20,8 +20,6 @@ def _create_subscription(self): "template_id": self.env.ref( "argocd_deployer.application_set_template_default" ).id, - "domain_format": "1", - "subdomain_format": "2", } ) product.product_tmpl_id.application_set_id = application_set diff --git a/argocd_sale/views/application_template_view.xml b/argocd_sale/views/application_template_view.xml deleted file mode 100644 index 57af979..0000000 --- a/argocd_sale/views/application_template_view.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - argocd.application.template - - - - - - - - diff --git a/argocd_sale/views/application_view.xml b/argocd_sale/views/application_view.xml index 89b348d..501cdf1 100644 --- a/argocd_sale/views/application_view.xml +++ b/argocd_sale/views/application_view.xml @@ -6,16 +6,8 @@ - - - - + + diff --git a/argocd_sale/views/product_attribute_view.xml b/argocd_sale/views/product_attribute_view.xml new file mode 100644 index 0000000..745dfbe --- /dev/null +++ b/argocd_sale/views/product_attribute_view.xml @@ -0,0 +1,15 @@ + + + + product.attribute + + + + + + + + + + + diff --git a/argocd_sale/views/product_template.xml b/argocd_sale/views/product_template.xml index 613b78e..73f94b0 100644 --- a/argocd_sale/views/product_template.xml +++ b/argocd_sale/views/product_template.xml @@ -12,13 +12,8 @@ - - + diff --git a/argocd_sale/views/sale_subscription_view.xml b/argocd_sale/views/sale_subscription_view.xml new file mode 100644 index 0000000..bcdbd26 --- /dev/null +++ b/argocd_sale/views/sale_subscription_view.xml @@ -0,0 +1,18 @@ + + + + + sale.subscription + + + + + + + + + + + + diff --git a/argocd_website/__manifest__.py b/argocd_website/__manifest__.py index f7671e5..3deee1e 100644 --- a/argocd_website/__manifest__.py +++ b/argocd_website/__manifest__.py @@ -8,6 +8,7 @@ "depends": [ "website", "payment", + "account_payment", "argocd_sale", "portal", "auth_signup", @@ -16,8 +17,8 @@ "subscription_portal", ], "data": [ - "data/mail_template_data.xml", "data/ir_config_parameter_data.xml", + "data/ir_cron.xml", "security/ir_model_access.xml", "security/ir_rule.xml", "templates/website.xml", @@ -27,7 +28,10 @@ "assets": { "web.assets_frontend": [ "argocd_website/static/src/js/portal.js", + "argocd_website/static/src/js/website.js", "argocd_website/static/src/scss/portal.scss", + "argocd_website/static/src/scss/website.scss", + "argocd_website/static/src/xml/website.xml", ] }, "external_dependencies": {"python": ["yaml", "requests", "dnspython==2.6.1"]}, diff --git a/argocd_website/controllers/__init__.py b/argocd_website/controllers/__init__.py index 984e838..10bb7ff 100644 --- a/argocd_website/controllers/__init__.py +++ b/argocd_website/controllers/__init__.py @@ -1,2 +1,4 @@ from . import main from . import portal +from . import payment +from . import subscription diff --git a/argocd_website/controllers/main.py b/argocd_website/controllers/main.py index 3072958..7ea0e49 100644 --- a/argocd_website/controllers/main.py +++ b/argocd_website/controllers/main.py @@ -1,12 +1,12 @@ import re -from odoo import Command, _ +from odoo import _, api from odoo.exceptions import ValidationError from odoo.http import Controller, request, route class MainController(Controller): - def _validate(self, post, captcha_enabled): + def _validate(self, post, captcha_enabled, is_public, is_reseller): if captcha_enabled: try: request.env["librecaptcha"].answer( @@ -15,39 +15,115 @@ def _validate(self, post, captcha_enabled): except ValidationError as e: return {"subject": "captcha", "message": str(e)} + if is_public or is_reseller: + if not post["email"]: + return {"subject": "email", "message": _("Email Address is required.")} + if not post["name"]: + return {"subject": "name", "message": _("Company Name is required.")} + if not post["street_name"]: + return {"subject": "street_name", "message": _("Street is required.")} + if not post["street_number"]: + return { + "subject": "street_number", + "message": _("Street Number is required."), + } + if not post["zip"]: + return {"subject": "zip", "message": _("Zip is required.")} + if not post["city"]: + return {"subject": "city", "message": _("City is required.")} + if not post["company_registry"]: + return { + "subject": "company_registry", + "message": _("CoC number is required."), + } + if not re.match( + "^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$", + post["email"], + ): + return {"subject": "email", "message": _("Invalid email address.")} + if "terms_of_use" not in post: return { "subject": "terms_of_use", "message": _("Please accept the terms of use."), } - if not post["email"]: - return {"subject": "email", "message": _("Email Address is required.")} - if not post["name"]: - return {"subject": "name", "message": _("Company Name is required.")} - if not post["street_name"]: - return {"subject": "street_name", "message": _("Street is required.")} - if not post["street_number"]: - return { - "subject": "street_number", - "message": _("Street Number is required."), - } - if not post["zip"]: - return {"subject": "zip", "message": _("Zip is required.")} - if not post["city"]: - return {"subject": "city", "message": _("City is required.")} - if not post["coc"]: - return {"subject": "coc", "message": _("CoC number is required.")} - if not re.match( - "^.+\\@(\\[?)[a-zA-Z0-9\\-\\.]+\\.([a-zA-Z]{2,3}|[0-9]{1,3})(\\]?)$", - post["email"], - ): - return {"subject": "email", "message": _("Invalid email address.")} + if is_public: + existing_users = ( + request.env["res.users"] + .sudo() + .search([("login", "=", post["email"])], count=True) + ) + if existing_users: + return { + "subject": "email", + "message": _("Email address already in use."), + } return False + @route( + "/application/update_subscription_product", + type="json", + auth="public", + website=True, + ) + def update_subscription_product(self, product_template_id, combination): + website = request.website.sudo() + website.update_subscription_product(product_template_id, combination) + + @route( + "/application/remove_subscription_product", + type="json", + auth="public", + website=True, + ) + def remove_subscription_product(self, product_template_id): + website = request.website.sudo() + website.remove_subscription_product(product_template_id) + + @route( + "/application/get_subscription_details", + type="json", + auth="public", + website=True, + ) + def get_subscription_details(self): + website = request.website.sudo() + sub = website.ensure_subscription() + currency = sub.currency_id + return { + "currency_id": sub.currency_id.id, + "amount_tax": sub.amount_tax, + "amount_total": sub.amount_total, + "amount_tax_formatted": currency.format(sub.amount_tax), + "amount_total_formatted": currency.format(sub.amount_total), + "lines": [ + { + "name": line.product_id.product_tmpl_id.name, + "price_base": line.product_id.product_tmpl_id.list_price, + "price_base_formatted": currency.format( + line.product_id.product_tmpl_id.list_price + ), + "variant_values": [ + { + "name": variant.attribute_id.name, + "value": variant.name, + "price_extra": variant.price_extra, + "price_extra_formatted": currency.format( + variant.price_extra + ), + } + for variant in line.product_id.product_template_variant_value_ids + ], + } + for line in sub.sale_subscription_line_ids + ], + } + @route( [ - """/application/additional/""" + """/application/order/""", + """/application/order""", ], type="http", auth="public", @@ -55,169 +131,154 @@ def _validate(self, post, captcha_enabled): methods=["GET"], sitemap=True, ) - def additional(self, product): - if ( - not product.application_template_id - or not product.application_template_id.active - or not product.sale_ok - ): - return request.not_found() - - additional_products = ( - request.env["product.product"] - .search( - [ - ("application_tag_ids", "!=", False), - ("sale_ok", "=", True), - "|", - ("application_filter_ids", "=", False), - ( - "application_filter_ids", - "in", - product.application_template_id.ids, - ), - ] + def order(self, product=False): + main_product_tmpl = request.env["product.template"] + if product: + if not product.application_template_id or not product.sale_ok: + return request.not_found() + main_product_tmpl = product.product_tmpl_id + optional_products = main_product_tmpl.optional_product_ids.filtered_domain( + [("application_template_id", "!=", False), ("sale_ok", "=", True)] + ) + else: + optional_products = request.env["product.template"].search( + [("application_template_id", "!=", False), ("sale_ok", "=", True)] + ) + website = request.website.sudo() + if "last_main_product_tmpl_id" in request.session: + last_main_product_tmpl_id = request.session["last_main_product_tmpl_id"] + if ( + last_main_product_tmpl_id + and not main_product_tmpl + or last_main_product_tmpl_id != main_product_tmpl.id + ): + website.reset_subscription() + subscription = website.ensure_subscription() + if main_product_tmpl and not subscription.sale_subscription_line_ids: + website.update_subscription_product( + main_product_tmpl.id, + product.product_template_variant_value_ids.ids, ) - .sudo() + request.session["last_main_product_tmpl_id"] = ( + main_product_tmpl and main_product_tmpl.id or False ) - if not additional_products: - return request.redirect("/application/signup/%s" % product.id) + return request.render( - "argocd_website.additional", + "argocd_website.order", { - "product": product, - "additional_products": additional_products, + "main_product_tmpl": main_product_tmpl, + "optional_products": optional_products, + "subscription": subscription, + "current_step": "configure", }, ) @route( - [ - """/application/signup/""" - ], + ["""/application/signup"""], type="http", - auth="user", + auth="public", website=True, methods=["GET", "POST"], sitemap=True, ) - def form(self, product, **post): - if ( - not product.application_template_id - or not product.application_template_id.active - or not product.sale_ok - ): - return request.not_found() - - error = False - default = False + def signup(self, **post): + website = request.website.sudo() captcha_enabled = request.env["librecaptcha"].is_enabled() - # Check additional products - additional_products = request.env["product.product"] - for key in post: - if not key.startswith("additional_product_"): - continue - additional_product_id = int(key.replace("additional_product_", "")) - additional_product = request.env["product.product"].browse( - additional_product_id - ) - if ( - not additional_product.application_tag_ids - or not additional_product.sale_ok - ): # Is not a additional modules product - return request.not_found() # TODO: Find a better response for this - if ( - additional_product.application_filter_ids - and product.application_template_id - not in additional_product.application_filter_ids - ): # Is not suitable for this application (template) - return request.not_found() # TODO: Find a better response for this - additional_products += additional_product + subscription = website.ensure_subscription() + if not subscription.sale_subscription_line_ids: + return request.redirect("/application/order") + + user = request.env.user + user_is_public = user._is_public() + render_values = { + "subscription": subscription, + "user": user, + "user_is_public": user_is_public, # Shortcut + "user_is_reseller": user.partner_id.is_reseller, + "captcha_enabled": captcha_enabled, + } if request.httprequest.method == "POST": - error = self._validate(post, captcha_enabled) - if not error: - partner = ( - request.env["res.partner"] - .sudo() - .create( - { - "company_type": "company", - "name": post["name"], - "type": "other", - "email": post["email"], - "street": " ".join( - [ - post["street_name"], - post["street_number"], - post["street_number2"], - ] - ), - "company_registry": post["coc"], - "zip": post["zip"], - "city": post["city"], - "customer_rank": 1, - "lang": "nl_NL", - "parent_id": request.env.user.partner_id.id, - } - ) + error = self._validate( + post, captcha_enabled, user_is_public, user.partner_id.is_reseller + ) + render_values.update(default=post, error=error) + if error: + return request.render("argocd_website.signup", render_values) + + # Prepare post data for the ORM + if user_is_public or user.partner_id.is_reseller: + values = { + "street": " ".join( + [ + post["street_name"], + post["street_number"], + post["street_number2"], + ] + ), + "type": user_is_public and "invoice" or "other", + "company_type": "company", + "customer_rank": 1, + "lang": request.env.lang, + "name": post["name"], + "email": post["email"], + "company_registry": post["company_registry"], + "zip": post["zip"], + "city": post["city"], + } + + if user_is_public: + # Create user + users_sudo = request.env["res.users"].sudo() + signup_values = values.copy() + signup_values.update( + login=post["email"], tz=request.httprequest.cookies.get("tz") ) - subscription = ( - request.env["sale.subscription"] - .sudo() - .create( - { - "partner_id": partner.id, - "sale_subscription_line_ids": [ - Command.create({"product_id": product.id}) - ] - + [ - Command.create({"product_id": additional_product.id}) - for additional_product in additional_products - ], - "pricelist_id": partner.property_product_pricelist.id, # pricelist_id is done with an onchange in subscription_oca 👴 - } - ) + login = users_sudo.signup(signup_values) + users_sudo.reset_password(post["email"]) + new_user = users_sudo.search([("login", "=", login)]) + env = api.Environment( + request.env.cr, new_user.id, request.session.context ) + request.session.pre_login = login + request.session.pre_uid = new_user.id + request.session.finalize(env) + request.env = env + request.update_context(**request.session.context) + subscription.user_id = new_user + subscription.partner_id = new_user.partner_id + elif user.partner_id.is_reseller: + # Create end customer + reselling_partner = user.partner_id.parent_id or user.partner_id + partner = request.env["res.partner"].sudo().create(values) + partner.parent_id = reselling_partner + + subscription.partner_id = reselling_partner + subscription.user_id = user + subscription.end_partner_id = partner + else: + # Link subscription to current user + subscription.partner_id = user.partner_id + subscription.user_id = user + + if not subscription.invoice_ids: subscription.generate_invoice() - subscription.invoice_ids.ensure_one() - invoice_id = subscription.invoice_ids.id - ctx = request.env.context.copy() - ctx.update( - { - "active_id": invoice_id, - "active_model": "account.move", - } - ) - link_wizard = ( - request.env["payment.link.wizard"] - .sudo() - .with_context(ctx) - .create({}) - ) - link_wizard._compute_link() - return request.redirect(link_wizard.link) - default = post - - # Create query string for additional products - additional_products_query = "" - if additional_products: - additional_products_query = "?" - additional_products_query += "&".join( - ["additional_product_%s=1" % p_id for p_id in additional_products.ids] - ) # e.g. ?additional_product_12=1&additional_product_78=1 + subscription.invoice_ids.ensure_one() + invoice_id = subscription.invoice_ids.id + ctx = request.env.context.copy() + ctx.update( + { + "active_id": invoice_id, + "active_model": "account.move", + } + ) + link_wizard = ( + request.env["payment.link.wizard"].sudo().with_context(ctx).create({}) + ) + link_wizard._compute_link() + return request.redirect( + link_wizard.link + ) # This will redirect to /payment/confirmation if the invoice has been paid - return request.render( - "argocd_website.form", - { - "currency": request.website.company_id.currency_id, - "product": product, - "additional_products": additional_products, - "total": product.list_price - + sum(additional_products.mapped("list_price")), - "error": error, - "default": default, - "additional_products_query": additional_products_query, - "captcha_enabled": captcha_enabled, - }, - ) + return request.render("argocd_website.signup", render_values) diff --git a/argocd_website/controllers/payment.py b/argocd_website/controllers/payment.py new file mode 100644 index 0000000..48d4757 --- /dev/null +++ b/argocd_website/controllers/payment.py @@ -0,0 +1,18 @@ +from odoo.http import request + +from odoo.addons.payment.controllers.portal import PaymentPortal + + +class ArgocdPaymentPortal(PaymentPortal): + def _get_custom_rendering_context_values(self, **kwargs): + res = super()._get_custom_rendering_context_values(**kwargs) + is_paying_for_app_subscription = False + if "invoice_id" in kwargs: + invoice = request.env["account.move"].sudo().browse(kwargs["invoice_id"]) + is_paying_for_app_subscription = bool( + invoice.line_ids.filtered( + lambda l: l.product_id.application_template_id + ) + ) + res.update(is_paying_for_app_subscription=is_paying_for_app_subscription) + return res diff --git a/argocd_website/controllers/portal.py b/argocd_website/controllers/portal.py index 536a6fd..123054b 100644 --- a/argocd_website/controllers/portal.py +++ b/argocd_website/controllers/portal.py @@ -1,6 +1,6 @@ import logging -from odoo import Command, _, fields, http +from odoo import Command, _, http from odoo.exceptions import AccessError, MissingError, ValidationError from odoo.http import request @@ -28,7 +28,7 @@ def _prepare_home_portal_values(self, counters): auth="user", website=True, ) - def portal_my_contracts(self, page=1, sortby=None, **kw): + def portal_my_applications(self, page=1, sortby=None, **kw): values = self._prepare_portal_layout_values() searchbar_sortings = { "name": {"label": _("Name"), "order": "name desc"}, @@ -51,9 +51,6 @@ def portal_my_contracts(self, page=1, sortby=None, **kw): apps = request.env["argocd.application"].search( [], order=order, limit=self._items_per_page, offset=pager["offset"] ) - products = request.env["product.product"].search( - [("application_template_id", "!=", False), ("sale_ok", "=", True)] - ) values.update( { "apps": apps.sudo(), # We don't have access to the invoice lines without this @@ -62,7 +59,6 @@ def portal_my_contracts(self, page=1, sortby=None, **kw): "default_url": "/my/applications", "searchbar_sortings": searchbar_sortings, "sortby": sortby, - "products": products.sudo(), } ) return request.render("argocd_website.portal_my_applications", values) @@ -85,61 +81,6 @@ def portal_my_application_detail(self, app_id, **kw): } return request.render("argocd_website.portal_application_page", values) - @http.route( - ["/my/applications//request-delete"], - type="http", - auth="user", - website=True, - ) - def portal_my_application_request_delete(self, app_id, **kw): - try: - app_sudo = self._document_check_access("argocd.application", app_id) - except (AccessError, MissingError): - return request.redirect("/my") - app_sudo.request_destroy() - return request.redirect("/my/applications/%s?message=request_deletion" % app_id) - - @http.route( - ["/my/applications//confirm-delete"], - type="http", - auth="user", - website=True, - ) - def portal_my_application_confirm_delete(self, app_id, **kw): - try: - app_sudo = self._document_check_access("argocd.application", app_id) - except (AccessError, MissingError): - return request.redirect("/my/applications") - if not app_sudo.deletion_token: - return request.redirect("/my/applications") - if app_sudo.deletion_token != kw.get("token"): - return request.render( - "argocd_website.error_page", {"message": _("Invalid token")} - ) - if fields.Datetime.now() > app_sudo.deletion_token_expiration: - return request.render( - "argocd_website.error_page", {"message": _("Token expired")} - ) - - if request.httprequest.method == "POST": - try: - app_sudo.confirm_destroy(kw.get("token")) - except ValidationError as e: - return request.render("argocd_website.error_page", {"message": str(e)}) - return request.redirect( - "/my/applications/%s?message=pending_deletion" % app_id - ) - - values = { - "page_name": "Applications", - "app": app_sudo, - "message": kw.get("message"), - } - - return request.render( - "argocd_website.portal_application_confirm_deletion_page", values - ) - @http.route( ["/my/applications//domain-names"], type="http", diff --git a/argocd_website/controllers/subscription.py b/argocd_website/controllers/subscription.py new file mode 100644 index 0000000..abe54b6 --- /dev/null +++ b/argocd_website/controllers/subscription.py @@ -0,0 +1,21 @@ +from odoo.osv import expression + +from odoo.addons.subscription_portal.controllers.main import PortalSubscription + + +class ArgoCDPortalSubscription(PortalSubscription): + def _get_filter_domain(self, kw): + res = super()._get_filter_domain(kw) + res = expression.AND( + [ + res, + [ + "|", + ("website_id", "=", False), + "&", + ("stage_id.type", "!=", "draft"), + ("stage_id", "!=", False), + ], + ] + ) + return res diff --git a/argocd_website/data/ir_config_parameter_data.xml b/argocd_website/data/ir_config_parameter_data.xml index 56093ab..6d0b9ef 100644 --- a/argocd_website/data/ir_config_parameter_data.xml +++ b/argocd_website/data/ir_config_parameter_data.xml @@ -4,4 +4,8 @@ argocd_website.subscription_template_id 0 + + argocd_website.subscription_abandoned_period + 14 + diff --git a/argocd_website/data/ir_cron.xml b/argocd_website/data/ir_cron.xml new file mode 100644 index 0000000..9cf420f --- /dev/null +++ b/argocd_website/data/ir_cron.xml @@ -0,0 +1,13 @@ + + + + ArgoCD Website: Cleanup abandoned subscriptions + + code + model._cron_cleanup_abandoned() + 1 + days + -1 + + + diff --git a/argocd_website/data/mail_template_data.xml b/argocd_website/data/mail_template_data.xml deleted file mode 100644 index baaaa92..0000000 --- a/argocd_website/data/mail_template_data.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - Request Deletion - - {{ object.partner_id.id }} - {{ object.partner_id.email }} - {{ object.partner_id.lang }} - Deletion Request for '{{ object.name }}' - - Dear Customer, - - Are you sure you want to delete application ""? - - Continue - - - diff --git a/argocd_website/models/__init__.py b/argocd_website/models/__init__.py index 63ba269..a885555 100644 --- a/argocd_website/models/__init__.py +++ b/argocd_website/models/__init__.py @@ -1,2 +1,3 @@ from . import application from . import subscription +from . import website diff --git a/argocd_website/models/application.py b/argocd_website/models/application.py index 0f327b9..7b04a34 100644 --- a/argocd_website/models/application.py +++ b/argocd_website/models/application.py @@ -1,15 +1,10 @@ import logging -from datetime import timedelta import requests -import yaml -from yaml import Loader from odoo import _, fields, models from odoo.exceptions import MissingError, ValidationError -from odoo.addons.auth_signup.models.res_partner import random_token - _logger = logging.getLogger(__name__) try: @@ -31,35 +26,15 @@ def _compute_access_url(self): def check_health(self): self.ensure_one() - try: - config = yaml.load(self.config, Loader=Loader) - helm = yaml.load(config["helm"], Loader=Loader) - res = requests.get("https://%s" % self._get_domain(helm)) - except Exception: - return False - return res.ok - - def request_destroy(self): - # We don't want user to easily delete their applications - self.ensure_one() - self.deletion_token = random_token() - self.deletion_token_expiration = fields.Datetime.now() + timedelta(days=1) - - template = self.env.ref("argocd_website.delete_request_mail_template") - template.send_mail(self.id) - - def confirm_destroy(self, token): - self.ensure_one() - if self.deletion_token != token: - raise ValidationError(_("Invalid token")) - if fields.Datetime.now() > self.deletion_token_expiration: - raise ValidationError(_("Token expired")) - - self.deletion_token = False - self.deletion_token_expiration = False - - # Destroy and delete app record - self.destroy() + statuses = [] + urls = self.get_urls() + for url in urls: + try: + response = requests.get(url[0]) + statuses.append(response.ok) + except Exception: + statuses.append(False) + return statuses def dns_cname_check(self, domain, tag_id=None): """ diff --git a/argocd_website/models/subscription.py b/argocd_website/models/subscription.py index f7d0be4..5710367 100644 --- a/argocd_website/models/subscription.py +++ b/argocd_website/models/subscription.py @@ -1,3 +1,5 @@ +from datetime import timedelta + from odoo import fields, models @@ -7,6 +9,7 @@ class Subscription(models.Model): template_id = fields.Many2one( default=lambda self: self._default_subscription_template_id() ) + website_id = fields.Many2one(comodel_name="website") def _default_subscription_template_id(self): website = self.env["website"].get_current_website() @@ -20,9 +23,31 @@ def _default_subscription_template_id(self): template = self.env["sale.subscription.template"].browse(template_id) return template - def _stop_service_hook(self): - res = super()._stop_service_hook() - apps_to_destroy = self.application_ids - for app in apps_to_destroy: - app.destroy() - return res + def _cron_cleanup_abandoned(self): + period = int( + self.env["ir.config_parameter"] + .sudo() + .get_param("argocd_website.subscription_abandoned_period" "0") + ) + if not period: + return + abandoned_date = fields.Datetime.now() - timedelta(days=period) + self.search( + [ + ( + "website_id", + "!=", + False, + "create_date", + "<=", + fields.Datetime.to_string(abandoned_date), + "|", + "stage_id.type", + "=", + "draft", + "stage_id", + "=", + False, + ) + ] + ).unlink() diff --git a/argocd_website/models/website.py b/argocd_website/models/website.py new file mode 100644 index 0000000..fc7a3d9 --- /dev/null +++ b/argocd_website/models/website.py @@ -0,0 +1,92 @@ +from odoo import Command, _, api, models +from odoo.exceptions import UserError +from odoo.http import request + + +class Website(models.Model): + _inherit = "website" + + def reset_subscription(self): + subscription = self.ensure_subscription() + subscription.sale_subscription_line_ids = [Command.clear()] + + def update_subscription_product(self, product_template_id, combination): + product_template = self.env["product.template"].browse(product_template_id) + + if not product_template.application_template_id or not product_template.sale_ok: + raise UserError(_("Product not available")) + + combination = self.env["product.template.attribute.value"].browse(combination) + combination_possible = product_template._is_combination_possible(combination) + if not combination_possible: + raise UserError(_("Combination is not possible")) + product = product_template._get_variant_for_combination(combination) + + subscription = self.ensure_subscription() + existing_line = subscription.sale_subscription_line_ids.filtered( + lambda l: l.product_id.product_tmpl_id == product_template + ) + + if not existing_line: + subscription.sale_subscription_line_ids = [ + Command.create({"product_id": product.id}) + ] + else: + existing_line.product_id = product + subscription._compute_total() # Depends is not correctly set on this method + subscription.invoice_ids.unlink() + + def remove_subscription_product(self, product_template_id): + subscription = self.ensure_subscription() + to_remove = subscription.sale_subscription_line_ids.filtered( + lambda l: l.product_id.product_tmpl_id.id == product_template_id + ) + subscription.sale_subscription_line_ids = [ + Command.unlink(t.id) for t in to_remove + ] + subscription.invoice_ids.unlink() + + @api.returns("sale.subscription") + def ensure_subscription(self): + """ + Get current draft subscription. The draft subscription is like a shopping cart but for a subscription + """ + self.ensure_one() + subscription = self.env["sale.subscription"] + subscription_id = request.session.get( + "subscription_id" + ) # Session is retrieved by cookie, so we don't need to filter on website + if subscription_id: + subscription = subscription.browse(subscription_id) + if ( + not subscription.exists() + or (subscription.stage_id and subscription.stage_id.type != "draft") + or not subscription.website_id + or subscription.invoice_ids + ): + subscription = self.env["sale.subscription"] + + user = request.env.user or self.user_id + partner = user.partner_id.commercial_partner_id + + if not subscription: + draft_stage = self.env["sale.subscription.stage"].search( + [("type", "=", "draft")], order="sequence desc", limit=1 + ) + subscription = subscription.create( + { + "partner_id": partner.id, + "website_id": self.id, + "pricelist_id": partner.property_product_pricelist.id, + "stage_id": draft_stage and draft_stage.id, + "user_id": user.id, + } + ) + request.session["subscription_id"] = subscription.id + else: + subscription.partner_id = partner + subscription.user_id = user + subscription.onchange_partner_id() + subscription.onchange_partner_id_fpos() + + return subscription diff --git a/argocd_website/security/ir_model_access.xml b/argocd_website/security/ir_model_access.xml index ce1e590..3aaa479 100644 --- a/argocd_website/security/ir_model_access.xml +++ b/argocd_website/security/ir_model_access.xml @@ -50,37 +50,13 @@ - - argocd_application_template_portal_access - + + argocd_application_domain_portal_access + - - - argocd_application_template_public_access - - - - - - - - - - - application_tag_portal_access - - - - - - - diff --git a/argocd_website/security/ir_rule.xml b/argocd_website/security/ir_rule.xml index 4fadd69..e894b74 100644 --- a/argocd_website/security/ir_rule.xml +++ b/argocd_website/security/ir_rule.xml @@ -5,13 +5,11 @@ [ - '|', - ('application_tag_ids', '!=', False), + ('sale_ok', '=', True), ('application_template_id', '!=', False), '|', - ('reseller_partner_ids', '=', False), - ('reseller_partner_ids', 'in', [user.partner_id.id]), - ('sale_ok', '=', True), + ('allowed_reseller_partner_ids', '=', False), + ('allowed_reseller_partner_ids', 'in', [user.partner_id.id]), ] @@ -22,13 +20,11 @@ [ - '|', - ('application_tag_ids', '!=', False), + ('sale_ok', '=', True), ('application_template_id', '!=', False), '|', - ('reseller_partner_ids', '=', False), - ('reseller_partner_ids', 'in', [user.partner_id.id]), - ('sale_ok', '=', True), + ('allowed_reseller_partner_ids', '=', False), + ('allowed_reseller_partner_ids', 'in', [user.partner_id.id]), ] @@ -38,7 +34,16 @@ argocd_application_portal_access - ['|', '|', '&', ('partner_id.parent_id', '!=', False), ('partner_id.parent_id', '=', user.partner_id.parent_id.id), ('partner_id', '=', user.partner_id.id), ('partner_id', 'child_of', user.partner_id.id)] + ['|', '|', '&', ('partner_id.parent_id', '!=', False), ('partner_id.parent_id', '=', user.partner_id.parent_id.id), ('partner_id', '=', user.partner_id.id), ('partner_id', 'child_of', user.partner_id.id)] + + + + + + argocd_application_domain_portal_rule + + + ['|', '|', '&', ('application_id.partner_id.parent_id', '!=', False), ('application_id.partner_id.parent_id', '=', user.partner_id.parent_id.id), ('application_id.partner_id', '=', user.partner_id.id), ('application_id.partner_id', 'child_of', user.partner_id.id)] diff --git a/argocd_website/static/src/js/portal.js b/argocd_website/static/src/js/portal.js index 5bf4539..6b66fe7 100644 --- a/argocd_website/static/src/js/portal.js +++ b/argocd_website/static/src/js/portal.js @@ -21,12 +21,16 @@ odoo.define("argocd_website.portal", function (require) { model: "argocd.application", method: "check_health", args: [parseInt(appId, 10)] - }).then(function (healthy) { - $el.removeClass(["fa-spin", "fa-circle-o-notch"]); - if (healthy) { - $el.addClass(["fa-heart", "text-success"]); - } else { - $el.addClass(["fa-times", "text-danger"]); + }).then(function (healthStatuses) { + $el.html(""); + for (var i in healthStatuses) { + var $statusEl = $(""); + if (healthStatuses[i]) { + $statusEl.addClass(["fa-heart", "text-success"]); + } else { + $statusEl.addClass(["fa-times", "text-danger"]); + } + $el.append($statusEl); } }); } diff --git a/argocd_website/static/src/js/website.js b/argocd_website/static/src/js/website.js new file mode 100644 index 0000000..24058fa --- /dev/null +++ b/argocd_website/static/src/js/website.js @@ -0,0 +1,151 @@ +odoo.define("argocd_website.website", function (require) { + "use strict"; + + var publicWidget = require("web.public.widget"); + var { SIZES, uiService } = require('@web/core/ui/ui_service'); + var { qweb } = require("web.core"); + require("website.content.menu"); + + publicWidget.registry.OrderAppProductConfigurator = publicWidget.Widget.extend({ + selector: ".js_order_app_product", + events: { + "change select.js_order_app_attribute": "_onAttributeChange", + "change .js_order_app_attribute input": "_onAttributeChange", + "change .js_order_app_toggle": "_onToggleChange", + "click .js_order_app_product_header": "_onClickHeader" + }, + + init: function (websiteRoot) { + this._super.apply(this, arguments); + this._websiteRoot = websiteRoot; + }, + + start: function () { + this._super.apply(this, arguments); + this.productTemplateId = parseInt(this.$el.attr("data-id"), 10); + this.$attributes = this.$el.find(".js_order_app_attribute"); + this.$toggle = this.$el.find(".js_order_app_toggle"); + this.$header = this.$el.find(".js_order_app_product_header"); + }, + + _isActive: function () { + if (!this.$toggle.length) return true; + return this.$toggle.is(":checked"); + }, + + _onClickHeader: function (ev) { + if (ev.target.nodeName === "INPUT") return; + this.$toggle.click(); + }, + + _onAttributeChange: function () { + // TODO: Look for available combinations, and change the other attribute selectors accordingly + if (!this._isActive()) return; + this._updateSubscriptionProduct(); + }, + + _onToggleChange: function () { + if (this._isActive()) { + this._updateSubscriptionProduct(); + this.$header.addClass("bg-light"); + } else { + this._removeSubscriptionProduct(); + this.$header.removeClass("bg-light"); + } + }, + + _removeSubscriptionProduct: function () { + return this._rpc({ + route: "/application/remove_subscription_product", + params: { + product_template_id: this.productTemplateId + } + }).then(function () { + this._websiteRoot.trigger("refreshSubscription"); + }.bind(this)); + }, + + _updateSubscriptionProduct: function() { + var combination = this.$attributes.map(function () { + var $el = $(this); + var valueId = null; + if ($el.is("select")) { + valueId = $el.val(); + } else { + valueId = $el.find(":checked").val(); + } + return parseInt(valueId, 10); + }).get(); + + return this._rpc({ + route: "/application/update_subscription_product", + params: { + product_template_id: this.productTemplateId, + combination: combination + } + }).then(function () { + this._websiteRoot.trigger("refreshSubscription"); + }.bind(this)); + } + }); + + publicWidget.registry.OrderAppDetails = publicWidget.Widget.extend({ + selector: ".js_order_app_details", + + init: function (websiteRoot) { + this._super.apply(this, arguments); + this._websiteRoot = websiteRoot; + }, + + start: function () { + this._super(); + this.$proceedBtn = this.$el.find(".js_order_app_proceed"); + this.$list = this.$el.find(".js_order_app_list"); + this.$loader = this.$el.find(".spinner-border"); + + this.refreshSubscription(); + this._websiteRoot.on("refreshSubscription", this, this.refreshSubscription); + }, + + refreshSubscription: function () { + this.$loader.removeClass("d-none"); + this.$proceedBtn.addClass("d-none"); + this.$list.addClass("d-none"); + + this._rpc({ + route: "/application/get_subscription_details" + }).then(function (data) { + var details = qweb.render( + "argo_website.List", { + sub: data + } + ); + this.$list.html(details); + + this.$loader.addClass("d-none"); + this.$proceedBtn.removeClass("d-none"); + this.$list.removeClass("d-none"); + this.$proceedBtn.toggleClass("disabled", !data.lines.length); + }.bind(this)); + } + }); + + var UpdateOrderDetailsPaddingMixin = { + start: function () { + var res = this._super(...arguments); + this.$orderDetails = this.$main.find(".js_order_app_details"); + return res; + }, + + _updateMainPaddingTop: function () { + var isLarge = uiService.getSize() >= SIZES.LG; + this.$orderDetails.css("padding-top", this.fixedHeader && this._isShown() && isLarge ? this.headerHeight : ""); + return this._super(...arguments); + } + }; + + publicWidget.registry.StandardAffixedHeader.include(UpdateOrderDetailsPaddingMixin) + + publicWidget.registry.FixedHeader.include(UpdateOrderDetailsPaddingMixin); + +}); diff --git a/argocd_website/static/src/scss/website.scss b/argocd_website/static/src/scss/website.scss new file mode 100644 index 0000000..b2e860e --- /dev/null +++ b/argocd_website/static/src/scss/website.scss @@ -0,0 +1,32 @@ +.js_order_app_product { + > .row { + border: $border-width solid $border-color; + &:not(:last-child) { + border-bottom: 0; + } + + &:last-child { + border-bottom: $border-width solid $border-color; + } + } + + .js_order_app_product_header { + cursor: pointer; + input { + cursor: pointer; + } + } +} + +.js_order_app_details { + top: 1em; +} + + +.argocd_website_order_progress_bar.s_process_steps { + @include media-breakpoint-down(lg) { + .s_process_step .s_process_step_connector { + display: block; + } + } +} diff --git a/argocd_website/static/src/xml/website.xml b/argocd_website/static/src/xml/website.xml new file mode 100644 index 0000000..6e2086c --- /dev/null +++ b/argocd_website/static/src/xml/website.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + : + + + + + + + + + + + Tax + + + + Total + + + + + + + Empty + + + diff --git a/argocd_website/templates/portal.xml b/argocd_website/templates/portal.xml index d4d6b2b..f14d9f6 100644 --- a/argocd_website/templates/portal.xml +++ b/argocd_website/templates/portal.xml @@ -35,14 +35,21 @@ - + + + + + + + - + @@ -100,9 +107,8 @@ - - + + @@ -118,20 +124,9 @@ You have no applications currently. - - - Request new application - - - - - - - - - + + Request new application + @@ -145,25 +140,10 @@ Application - - - - - - - - - - - - + + + @@ -186,83 +166,35 @@ Chamber of Commerce #: - + Health: - - + + - - - - - - - - - Your subscription - - - - - - - excl. VAT - - - - - - Product / service - Price - - - - - - / month - - - + + + + + + + + + : + + + + + - - - - - - - - - - - Delete Application - - - - Are you sure you want to delete this application? You'll get an email to confirm the deletion. - - - - - - - - @@ -285,34 +217,6 @@ - - - - - - - - Application - - - - - - - - Are you sure you want to delete this application ()? - This action is irreversible! - - - - - Delete - Cancel - - - - - - diff --git a/argocd_website/templates/website.xml b/argocd_website/templates/website.xml index c325695..be2a789 100644 --- a/argocd_website/templates/website.xml +++ b/argocd_website/templates/website.xml @@ -1,248 +1,368 @@ - - - + + + + + + + + + + + + + Configure + + + + + + + + + + + Sign up + + + + + + + + + + + Payment + + + + + + + + + + + + + + + + + + + + (+ ) + + + + + + + + + + + (+ ) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - + + + + Your Order + + + + - - - + - - / month + + + + + + + + - + - + + - - - - - - - - - - + + + + + + + + + + + Go to payment - Continue - + - - - - - - - - - - + + + + + - - - - Company information - - - - Email Address * - - - - - - - This will be used to validate your registration and login. - - - - - Company Name * - - - - - - - CoC Number * - - - - - - - Address * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Your order Click for details - - excl. VAT - - - - - - - Product / service - Price - - - - - - / month - - - - - - - / month - - - - - - - + + + + + Company details + + + + Email Address * + + + + + + + This will be used to validate your registration and login. + + + + + Company Name * + + + + + + + CoC Number * + + + + + + + Address * + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Address: + + + Chamber of Commerce #: + + + + Edit + + + + + + + Account + + + You are currently not logged in if you already have an account, please make sure to login. + + + + + + + + I agree to the terms of use + + + + + + + + + + + + + + + - - - - - I agree to the terms of use - - - - - - - + + + + + + + + + + + - - - - + + + Go to payment - - Go to payment - - - - + + + @@ -269,4 +389,55 @@ + + + + + not is_paying_for_app_subscription + + + + + + + + + + + + + + + End Customer: + + + + + + + + + + + + + diff --git a/subscription_portal/controllers/main.py b/subscription_portal/controllers/main.py index 6f8e9d6..3d86e5f 100644 --- a/subscription_portal/controllers/main.py +++ b/subscription_portal/controllers/main.py @@ -11,7 +11,7 @@ def _prepare_home_portal_values(self, counters): if "subscription_count" in counters: subscription_model = request.env["sale.subscription"] subscription_count = ( - subscription_model.search_count([]) + subscription_model.search_count(self._get_filter_domain({})) if subscription_model.check_access_rights("read", raise_exception=False) else 0 ) diff --git a/subscription_portal/models/sale_subscription.py b/subscription_portal/models/sale_subscription.py index c15e081..bc0a561 100644 --- a/subscription_portal/models/sale_subscription.py +++ b/subscription_portal/models/sale_subscription.py @@ -48,25 +48,5 @@ def confirm_cancellation(self, token, close_reason_id=False): self.cancellation_token = False self.cancellation_token_expiration = False - self.date_stop = self.recurring_next_date self.close_subscription(close_reason_id) - - def _stop_service_hook(self): - self.ensure_one() - self.date_stop = False - - def cron_subscription_management(self): - today = fields.Date.today() - for subscription in self.search( - [ - ( - "date_stop", - "!=", - False, - ), # Not sure if any date is greater than False - ("date_stop", "<", today), # We give them the whole day - ] - ): - subscription._stop_service_hook() - return super().cron_subscription_management()
Dear Customer,
Are you sure you want to delete application ""?
You have no applications currently.
Are you sure you want to delete this application? You'll get an email to confirm the deletion.
- Are you sure you want to delete this application ()? - This action is irreversible! -