shopinvader/odoo-shopinvader

View on GitHub
shopinvader/models/shopinvader_variant.py

Summary

Maintainability
A
3 hrs
Test Coverage
# -*- coding: utf-8 -*-
# Copyright 2017 Akretion (http://www.akretion.com).
# @author Sébastien BEAU <sebastien.beau@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from contextlib import contextmanager

from odoo import api, fields, models
from odoo.tools import float_compare, float_round

from .tools import sanitize_attr_name


class ShopinvaderVariant(models.Model):
    _name = "shopinvader.variant"
    _description = "Shopinvader Variant"
    _inherits = {
        "shopinvader.product": "shopinvader_product_id",
        "product.product": "record_id",
    }

    default_code = fields.Char(related="record_id.default_code", readonly=True)
    shopinvader_product_id = fields.Many2one(
        "shopinvader.product", required=True, ondelete="cascade", index=True
    )
    record_id = fields.Many2one(
        "product.product", required=True, ondelete="cascade", index=True
    )
    object_id = fields.Integer(
        compute="_compute_object_id", store=True, index=True
    )
    variant_count = fields.Integer(related="product_variant_count")
    variant_attributes = fields.Serialized(
        compute="_compute_variant_attributes", string="Shopinvader Attributes"
    )
    main = fields.Boolean(compute="_compute_main_product")
    redirect_url_key = fields.Serialized(
        compute="_compute_redirect_url_key", string="Redirect Url Keys"
    )
    active = fields.Boolean(default=True)
    price = fields.Serialized(
        compute="_compute_price", string="Shopinvader Price"
    )
    short_name = fields.Char(compute="_compute_names")
    full_name = fields.Char(compute="_compute_names")

    # As field is defined on product.template, avoid 'inherits' bypass
    description = fields.Html(
        related="shopinvader_product_id.description", readonly=False
    )

    @contextmanager
    @api.multi
    def _action_product_toggle_active(self):
        """
        Action a deactivation of a variant, if every variants are disabled:
        disable the product too.
        Also when a variant is enabled, the related shopinvader product
        should be re-enabled too.
        :return:
        """
        product_active_dict = {
            p: p.active for p in self.mapped("shopinvader_product_id")
        }
        yield
        to_activate_ids = set()
        to_inactivate_ids = set()
        for variant in self:
            shopinv_product = variant.shopinvader_product_id
            if variant.active:
                # If the variant is active and the related shop. product is
                # not active, we have to active it.
                if not shopinv_product.active:
                    to_activate_ids.add(shopinv_product.id)
                continue
            # If the product is already disabled, we don't have anything to do!
            if not product_active_dict.get(shopinv_product, True):
                continue
            # If every variants of the product are disabled
            # (The product is enable; checked by previous IF).
            if all(
                [not v.active for v in shopinv_product.shopinvader_variant_ids]
            ):
                to_inactivate_ids.add(shopinv_product.id)
        if to_activate_ids:
            self.env["shopinvader.product"].browse(to_activate_ids).write(
                {"active": True}
            )
        if to_inactivate_ids:
            self.env["shopinvader.product"].browse(to_inactivate_ids).write(
                {"active": False}
            )

    @api.multi
    def write(self, vals):
        """
        Inherit to manage behaviour when the variant is disabled.
        We may have to disable also the shopinvader.product
        :param vals: dict
        :return: bool
        """
        with self._action_product_toggle_active():
            result = super(ShopinvaderVariant, self).write(vals)
        return result

    @api.multi
    def _build_seo_title(self):
        """
        Build the SEO product name.
        Call the same function on the related shopinvader product.
        :return: str
        """
        self.ensure_one()
        return self.shopinvader_product_id._build_seo_title()

    def _prepare_variant_name_and_short_name(self):
        self.ensure_one()
        attributes = self.attribute_line_ids.filtered(
            lambda l: len(l.value_ids) > 1
        ).mapped("attribute_id")
        short_name = self.attribute_value_ids._variant_name(attributes)
        full_name = self.shopinvader_display_name
        if short_name:
            full_name += " (%s)" % short_name
        return full_name, short_name

    def _compute_names(self):
        for record in self:
            (
                record.full_name,
                record.short_name,
            ) = record._prepare_variant_name_and_short_name()

    def _compute_price(self):
        for record in self:
            record.price = record._get_all_price()

    def _get_all_price(self):
        self.ensure_one()
        res = {}
        pricelist = self.backend_id.pricelist_id
        if pricelist:
            res["default"] = self._get_price(
                pricelist, None, self.backend_id.company_id
            )
        return res

    @api.depends("record_id")
    def _compute_object_id(self):
        for record in self:
            record.object_id = record.record_id.id

    def _compute_redirect_url_key(self):
        for record in self:
            res = []
            for url in record.redirect_url_url_ids:
                res.append(url.url_key)
            record.redirect_url_key = res

    def _compute_variant_attributes(self):
        for record in self:
            variant_attributes = dict()
            for att_value in record.attribute_value_ids:
                sanitized_key = sanitize_attr_name(att_value.attribute_id)
                variant_attributes[sanitized_key] = att_value.name
            record.variant_attributes = variant_attributes

    def _get_price(self, pricelist, fposition, company=None):
        self.ensure_one()
        return self._get_price_per_qty(1, pricelist, fposition, company)

    def _get_price_per_qty(self, qty, pricelist, fposition, company=None):
        product_id = self.record_id
        taxes = product_id.taxes_id.sudo().filtered(
            lambda r: not company or r.company_id == company
        )
        # get the expeced tax to apply from the fiscal position
        tax_id = fposition.map_tax(taxes, product_id) if fposition else taxes
        tax_id = tax_id and tax_id[0]
        product = product_id.with_context(
            quantity=qty, pricelist=pricelist.id, fiscal_position=fposition
        )
        final_price, rule_id = pricelist.get_product_price_rule(
            product, qty or 1.0, None
        )
        tax_included = tax_id.price_include
        account_tax_obj = self.env["account.tax"]
        # fix tax on the price
        value = account_tax_obj._fix_tax_included_price_company(
            final_price, product.taxes_id, tax_id, company
        )
        res = {
            "value": value,
            "tax_included": tax_included,
            "original_value": value,
            "discount": 0.0,
        }
        if pricelist.discount_policy == "without_discount":
            sol = self.env["sale.order.line"]
            new_list_price, currency_id = sol._get_real_price_currency(
                product, rule_id, qty or 1.0, product.uom_id, pricelist.id
            )
            # fix tax on the real price
            new_list_price = account_tax_obj._fix_tax_included_price_company(
                new_list_price, product.taxes_id, tax_id, company
            )
            product_precision = self.env["decimal.precision"].precision_get(
                "Product Price"
            )
            if (
                float_compare(
                    new_list_price, value, precision_digits=product_precision
                )
                == 0
            ):
                # Both prices are equals. Product is wihout discount, avoid
                # divide by 0 exception
                return res
            discount = (new_list_price - value) / new_list_price * 100
            # apply the right precision on discount
            dicount_precision = self.env["decimal.precision"].precision_get(
                "Discount"
            )
            discount = float_round(discount, dicount_precision)
            res.update(
                {"original_value": new_list_price, "discount": discount}
            )
        return res

    def _compute_main_product(self):
        for record in self:
            record.main = record == record.shopinvader_variant_ids[0]