示例#1
0
class PartnerCategory(models.Model):
    _description = 'Partner Tags'
    _name = 'res.partner.category'
    _order = 'parent_left, name'
    _parent_store = True
    _parent_order = 'name'

    name = fields.Char(string='Tag Name', required=True, translate=True)
    color = fields.Integer(string='Color Index')
    parent_id = fields.Many2one('res.partner.category',
                                string='Parent Category',
                                index=True,
                                ondelete='cascade')
    child_ids = fields.One2many('res.partner.category',
                                'parent_id',
                                string='Child Tags')
    active = fields.Boolean(
        default=True,
        help=
        "The active field allows you to hide the category without removing it."
    )
    parent_left = fields.Integer(string='Left parent', index=True)
    parent_right = fields.Integer(string='Right parent', index=True)
    partner_ids = fields.Many2many('res.partner',
                                   column1='category_id',
                                   column2='partner_id',
                                   string='Partners')

    @api.constrains('parent_id')
    def _check_parent_id(self):
        if not self._check_recursion():
            raise ValidationError(
                _('Error ! You can not create recursive tags.'))

    @api.multi
    def name_get(self):
        """ Return the categories' display name, including their direct
            parent by default.

            If ``context['partner_category_display']`` is ``'short'``, the short
            version of the category name (without the direct parent) is used.
            The default is the long version.
        """
        if self._context.get('partner_category_display') == 'short':
            return super(PartnerCategory, self).name_get()

        res = []
        for category in self:
            names = []
            current = category
            while current:
                names.append(current.name)
                current = current.parent_id
            res.append((category.id, ' / '.join(reversed(names))))
        return res

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        args = args or []
        if name:
            # Be sure name_search is symetric to name_get
            name = name.split(' / ')[-1]
            args = [('name', operator, name)] + args
        return self.search(args, limit=limit).name_get()
示例#2
0
class SaleOrder(models.Model):
    _inherit = "sale.order"

    website_order_line = fields.One2many(
        'sale.order.line',
        compute='_compute_website_order_line',
        string='Order Lines displayed on Website',
        help=
        'Order Lines to be displayed on the website. They should not be used for computation purpose.',
    )
    website_id = fields.Many2one('website',
                                 string='Website',
                                 help='Website reference for quotation/order.')
    cart_quantity = fields.Integer(compute='_compute_cart_info',
                                   string='Cart Quantity')
    only_services = fields.Boolean(compute='_compute_cart_info',
                                   string='Only Services')
    can_directly_mark_as_paid = fields.Boolean(
        compute='_compute_can_directly_mark_as_paid',
        string="Can be directly marked as paid",
        store=True,
        help=
        """Checked if the sales order can directly be marked as paid, i.e. if the quotation
                is sent or confirmed and if the payment acquire is of the type transfer or manual"""
    )
    is_abandoned_cart = fields.Boolean('Abandoned Cart',
                                       compute='_compute_abandoned_cart',
                                       search='_search_abandoned_cart')
    cart_recovery_email_sent = fields.Boolean(
        'Cart recovery email already sent')

    @api.depends('state', 'payment_tx_id', 'payment_tx_id.state',
                 'payment_acquirer_id', 'payment_acquirer_id.provider')
    def _compute_can_directly_mark_as_paid(self):
        for order in self:
            order.can_directly_mark_as_paid = order.state in [
                'sent', 'sale'
            ] and order.payment_tx_id and order.payment_acquirer_id.provider in [
                'transfer', 'manual'
            ]

    @api.one
    def _compute_website_order_line(self):
        self.website_order_line = self.order_line

    @api.multi
    @api.depends('website_order_line.product_uom_qty',
                 'website_order_line.product_id')
    def _compute_cart_info(self):
        for order in self:
            order.cart_quantity = int(
                sum(order.mapped('website_order_line.product_uom_qty')))
            order.only_services = all(l.product_id.type in ('service',
                                                            'digital')
                                      for l in order.website_order_line)

    @api.multi
    @api.depends('team_id.team_type', 'date_order', 'order_line', 'state',
                 'partner_id')
    def _compute_abandoned_cart(self):
        abandoned_delay = float(
            self.env['ir.config_parameter'].sudo().get_param(
                'website_sale.cart_abandoned_delay', '1.0'))
        abandoned_datetime = fields.Datetime.to_string(
            datetime.utcnow() - relativedelta(hours=abandoned_delay))
        for order in self:
            domain = order.date_order and order.date_order <= abandoned_datetime and order.team_id.team_type == 'website' and order.state == 'draft' and order.partner_id.id != self.env.ref(
                'base.public_partner').id and order.order_line
            order.is_abandoned_cart = bool(domain)

    def _search_abandoned_cart(self, operator, value):
        abandoned_delay = float(
            self.env['ir.config_parameter'].sudo().get_param(
                'website_sale.cart_abandoned_delay', '1.0'))
        abandoned_datetime = fields.Datetime.to_string(
            datetime.utcnow() - relativedelta(hours=abandoned_delay))
        abandoned_domain = expression.normalize_domain([
            ('date_order', '<=', abandoned_datetime),
            ('team_id.team_type', '=', 'website'), ('state', '=', 'draft'),
            ('partner_id', '!=', self.env.ref('base.public_partner').id),
            ('order_line', '!=', False)
        ])
        # is_abandoned domain possibilities
        if (operator not in expression.NEGATIVE_TERM_OPERATORS
                and value) or (operator in expression.NEGATIVE_TERM_OPERATORS
                               and not value):
            return abandoned_domain
        return expression.distribute_not(['!'] +
                                         abandoned_domain)  # negative domain

    @api.multi
    def _cart_find_product_line(self, product_id=None, line_id=None, **kwargs):
        self.ensure_one()
        product = self.env['product.product'].browse(product_id)

        # split lines with the same product if it has untracked attributes
        if product and product.mapped('attribute_line_ids').filtered(
                lambda r: not r.attribute_id.create_variant) and not line_id:
            return self.env['sale.order.line']

        domain = [('order_id', '=', self.id), ('product_id', '=', product_id)]
        if line_id:
            domain += [('id', '=', line_id)]
        return self.env['sale.order.line'].sudo().search(domain)

    @api.multi
    def _website_product_id_change(self, order_id, product_id, qty=0):
        order = self.sudo().browse(order_id)
        product_context = dict(self.env.context)
        product_context.setdefault('lang', order.partner_id.lang)
        product_context.update({
            'partner': order.partner_id.id,
            'quantity': qty,
            'date': order.date_order,
            'pricelist': order.pricelist_id.id,
        })
        product = self.env['product.product'].with_context(
            product_context).browse(product_id)
        discount = 0

        if order.pricelist_id.discount_policy == 'without_discount':
            # This part is pretty much a copy-paste of the method '_onchange_discount' of
            # 'sale.order.line'.
            price, rule_id = order.pricelist_id.with_context(
                product_context).get_product_price_rule(
                    product, qty or 1.0, order.partner_id)
            pu, currency_id = request.env['sale.order.line'].with_context(
                product_context)._get_real_price_currency(
                    product, rule_id, qty, product.uom_id,
                    order.pricelist_id.id)
            if pu != 0:
                if order.pricelist_id.currency_id.id != currency_id:
                    # we need new_list_price in the same currency as price, which is in the SO's pricelist's currency
                    pu = request.env['res.currency'].browse(
                        currency_id).with_context(product_context).compute(
                            pu, order.pricelist_id.currency_id)
                discount = (pu - price) / pu * 100
                if discount < 0:
                    # In case the discount is negative, we don't want to show it to the customer,
                    # but we still want to use the price defined on the pricelist
                    discount = 0
                    pu = price
        else:
            pu = product.price
            if order.pricelist_id and order.partner_id:
                order_line = order._cart_find_product_line(product.id)
                if order_line:
                    pu = self.env[
                        'account.tax']._fix_tax_included_price_company(
                            pu, product.taxes_id, order_line[0].tax_id,
                            self.company_id)

        return {
            'product_id': product_id,
            'product_uom_qty': qty,
            'order_id': order_id,
            'product_uom': product.uom_id.id,
            'price_unit': pu,
            'discount': discount,
        }

    @api.multi
    def _get_line_description(self, order_id, product_id, attributes=None):
        if not attributes:
            attributes = {}

        order = self.sudo().browse(order_id)
        product_context = dict(self.env.context)
        product_context.setdefault('lang', order.partner_id.lang)
        product = self.env['product.product'].with_context(
            product_context).browse(product_id)

        name = product.display_name

        # add untracked attributes in the name
        untracked_attributes = []
        for k, v in attributes.items():
            # attribute should be like 'attribute-48-1' where 48 is the product_id, 1 is the attribute_id and v is the attribute value
            attribute_value = self.env['product.attribute.value'].sudo(
            ).browse(int(v))
            if attribute_value and not attribute_value.attribute_id.create_variant:
                untracked_attributes.append(attribute_value.name)
        if untracked_attributes:
            name += '\n%s' % (', '.join(untracked_attributes))

        if product.description_sale:
            name += '\n%s' % (product.description_sale)

        return name

    @api.multi
    def _cart_update(self,
                     product_id=None,
                     line_id=None,
                     add_qty=0,
                     set_qty=0,
                     attributes=None,
                     **kwargs):
        """ Add or set product quantity, add_qty can be negative """
        self.ensure_one()
        SaleOrderLineSudo = self.env['sale.order.line'].sudo()

        try:
            if add_qty:
                add_qty = float(add_qty)
        except ValueError:
            add_qty = 1
        try:
            if set_qty:
                set_qty = float(set_qty)
        except ValueError:
            set_qty = 0
        quantity = 0
        order_line = False
        if self.state != 'draft':
            request.session['sale_order_id'] = None
            raise UserError(
                _('It is forbidden to modify a sales order which is not in draft status'
                  ))
        if line_id is not False:
            order_lines = self._cart_find_product_line(product_id, line_id,
                                                       **kwargs)
            order_line = order_lines and order_lines[0]

        # Create line if no line with product_id can be located
        if not order_line:
            values = self._website_product_id_change(self.id,
                                                     product_id,
                                                     qty=1)
            values['name'] = self._get_line_description(self.id,
                                                        product_id,
                                                        attributes=attributes)
            order_line = SaleOrderLineSudo.create(values)

            try:
                order_line._compute_tax_id()
            except ValidationError as e:
                # The validation may occur in backend (eg: taxcloud) but should fail silently in frontend
                _logger.debug("ValidationError occurs during tax compute. %s" %
                              (e))
            if add_qty:
                add_qty -= 1

        # compute new quantity
        if set_qty:
            quantity = set_qty
        elif add_qty is not None:
            quantity = order_line.product_uom_qty + (add_qty or 0)

        # Remove zero of negative lines
        if quantity <= 0:
            order_line.unlink()
        else:
            # update line
            values = self._website_product_id_change(self.id,
                                                     product_id,
                                                     qty=quantity)
            if self.pricelist_id.discount_policy == 'with_discount' and not self.env.context.get(
                    'fixed_price'):
                order = self.sudo().browse(self.id)
                product_context = dict(self.env.context)
                product_context.setdefault('lang', order.partner_id.lang)
                product_context.update({
                    'partner': order.partner_id.id,
                    'quantity': quantity,
                    'date': order.date_order,
                    'pricelist': order.pricelist_id.id,
                })
                product = self.env['product.product'].with_context(
                    product_context).browse(product_id)
                values['price_unit'] = self.env[
                    'account.tax']._fix_tax_included_price_company(
                        order_line._get_display_price(product),
                        order_line.product_id.taxes_id, order_line.tax_id,
                        self.company_id)

            order_line.write(values)

        return {'line_id': order_line.id, 'quantity': quantity}

    def _cart_accessories(self):
        """ Suggest accessories based on 'Accessory Products' of products in cart """
        for order in self:
            accessory_products = order.website_order_line.mapped(
                'product_id.accessory_product_ids').filtered(
                    lambda product: product.website_published)
            accessory_products -= order.website_order_line.mapped('product_id')
            return random.sample(accessory_products, len(accessory_products))

    @api.multi
    def action_recovery_email_send(self):
        composer_form_view_id = self.env.ref(
            'mail.email_compose_message_wizard_form').id
        try:
            default_template = self.env.ref(
                'website_sale.mail_template_sale_cart_recovery',
                raise_if_not_found=False)
            default_template_id = default_template.id if default_template else False
            template_id = int(self.env['ir.config_parameter'].sudo().get_param(
                'website_sale.cart_recovery_mail_template_id',
                default_template_id))
        except:
            template_id = False
        return {
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'mail.compose.message',
            'view_id': composer_form_view_id,
            'target': 'new',
            'context': {
                'default_composition_mode':
                'mass_mail' if len(self) > 1 else 'comment',
                'default_res_id': self.ids[0],
                'default_model': 'sale.order',
                'default_use_template': bool(template_id),
                'default_template_id': template_id,
                'website_sale_send_recovery_email': True,
                'active_ids': self.ids,
            },
        }

    def action_mark_as_paid(self):
        """ Mark directly a sales order as paid if:
                - State: Quotation Sent, or sales order
                - Provider: wire transfer or manual config
            The transaction is marked as done
            The invoice may be generated and marked as paid if configured in the website settings
            """
        self.ensure_one()
        if self.can_directly_mark_as_paid:
            self.action_confirm()
            if self.env['ir.config_parameter'].sudo().get_param(
                    'website_sale.automatic_invoice', default=False):
                self.payment_tx_id._generate_and_pay_invoice()
            self.payment_tx_id.state = 'done'
        else:
            raise ValidationError(
                _("The quote should be sent and the payment acquirer type should be manual or wire transfer"
                  ))

    @api.multi
    def _prepare_invoice(self):
        res = super(SaleOrder, self)._prepare_invoice()
        res['website_id'] = self.website_id.id
        return res

    @api.model
    def send_cart_recovery_mail(self):
        for val in self.search([('state', 'in', ['draft', 'sent'])]):
            template = False
            try:
                template = self.env.ref(
                    'website_sale.mail_template_sale_cart_recovery',
                    raise_if_not_found=False)
            except ValueError:
                pass
            if val.partner_id.email and template and val.is_abandoned_cart \
                    and not val.cart_recovery_email_sent:
                template.with_context(lang=val.partner_id.lang).send_mail(
                    val.id, force_send=True, raise_exception=True)
                val.cart_recovery_email_sent = True
示例#3
0
class SaleOrderLine(models.Model):
    _inherit = 'sale.order.line'

    product_packaging = fields.Many2one('product.packaging',
                                        string='Package',
                                        default=False)
    route_id = fields.Many2one('stock.location.route',
                               string='Route',
                               domain=[('sale_selectable', '=', True)],
                               ondelete='restrict')
    move_ids = fields.One2many('stock.move',
                               'sale_line_id',
                               string='Stock Moves')

    @api.model
    def create(self, values):
        line = super(SaleOrderLine, self).create(values)
        if line.state == 'sale':
            line._action_launch_procurement_rule()
        return line

    @api.multi
    def write(self, values):
        lines = False
        if 'product_uom_qty' in values:
            precision = self.env['decimal.precision'].precision_get(
                'Product Unit of Measure')
            lines = self.filtered(
                lambda r: r.state == 'sale' and float_compare(
                    r.product_uom_qty,
                    values['product_uom_qty'],
                    precision_digits=precision) == -1)
        res = super(SaleOrderLine, self).write(values)
        if lines:
            lines._action_launch_procurement_rule()
        return res

    @api.depends('order_id.state')
    def _compute_invoice_status(self):
        super(SaleOrderLine, self)._compute_invoice_status()
        for line in self:
            # We handle the following specific situation: a physical product is partially delivered,
            # but we would like to set its invoice status to 'Fully Invoiced'. The use case is for
            # products sold by weight, where the delivered quantity rarely matches exactly the
            # quantity ordered.
            if line.order_id.state == 'done'\
                    and line.invoice_status == 'no'\
                    and line.product_id.type in ['consu', 'product']\
                    and line.product_id.invoice_policy == 'delivery'\
                    and line.move_ids \
                    and all(move.state in ['done', 'cancel'] for move in line.move_ids):
                line.invoice_status = 'invoiced'

    @api.depends('move_ids')
    def _compute_product_updatable(self):
        for line in self:
            if not line.move_ids:
                super(SaleOrderLine, line)._compute_product_updatable()
            else:
                line.product_updatable = False

    @api.multi
    @api.depends('product_id')
    def _compute_qty_delivered_updateable(self):
        # prefetch field before filtering
        self.mapped('product_id')
        # on consumable or stockable products, qty_delivered_updateable defaults
        # to False; on other lines use the original computation
        lines = self.filtered(lambda line: line.product_id.type not in
                              ('consu', 'product'))
        lines = lines.with_prefetch(self._prefetch)
        super(SaleOrderLine, lines)._compute_qty_delivered_updateable()

    @api.onchange('product_id')
    def _onchange_product_id_set_customer_lead(self):
        self.customer_lead = self.product_id.sale_delay

    @api.onchange('product_packaging')
    def _onchange_product_packaging(self):
        if self.product_packaging:
            return self._check_package()

    @api.onchange('product_id')
    def _onchange_product_id_uom_check_availability(self):
        if not self.product_uom or (self.product_id.uom_id.category_id.id !=
                                    self.product_uom.category_id.id):
            self.product_uom = self.product_id.uom_id
        self._onchange_product_id_check_availability()

    @api.onchange('product_uom_qty', 'product_uom', 'route_id')
    def _onchange_product_id_check_availability(self):
        if not self.product_id or not self.product_uom_qty or not self.product_uom:
            self.product_packaging = False
            return {}
        if self.product_id.type == 'product':
            precision = self.env['decimal.precision'].precision_get(
                'Product Unit of Measure')
            product = self.product_id.with_context(
                warehouse=self.order_id.warehouse_id.id)
            product_qty = self.product_uom._compute_quantity(
                self.product_uom_qty, self.product_id.uom_id)
            if float_compare(product.virtual_available,
                             product_qty,
                             precision_digits=precision) == -1:
                is_available = self._check_routing()
                if not is_available:
                    message =  _('You plan to sell %s %s but you only have %s %s available in %s warehouse.') % \
                            (self.product_uom_qty, self.product_uom.name, product.virtual_available, product.uom_id.name, self.order_id.warehouse_id.name)
                    # We check if some products are available in other warehouses.
                    if float_compare(product.virtual_available,
                                     self.product_id.virtual_available,
                                     precision_digits=precision) == -1:
                        message += _('\nThere are %s %s available accross all warehouses.') % \
                                (self.product_id.virtual_available, product.uom_id.name)

                    warning_mess = {
                        'title': _('Not enough inventory!'),
                        'message': message
                    }
                    return {'warning': warning_mess}
        return {}

    @api.onchange('product_uom_qty')
    def _onchange_product_uom_qty(self):
        if self.state == 'sale' and self.product_id.type in [
                'product', 'consu'
        ] and self.product_uom_qty < self._origin.product_uom_qty:
            # Do not display this warning if the new quantity is below the delivered
            # one; the `write` will raise an `UserError` anyway.
            if self.product_uom_qty < self.qty_delivered:
                return {}
            warning_mess = {
                'title':
                _('Ordered quantity decreased!'),
                'message':
                _('You are decreasing the ordered quantity! Do not forget to manually update the delivery order if needed.'
                  ),
            }
            return {'warning': warning_mess}
        return {}

    @api.multi
    def _prepare_procurement_values(self, group_id=False):
        """ Prepare specific key for moves or other components that will be created from a procurement rule
        comming from a sale order line. This method could be override in order to add other custom key that could
        be used in move/po creation.
        """
        values = super(SaleOrderLine,
                       self)._prepare_procurement_values(group_id)
        self.ensure_one()
        date_planned = datetime.strptime(self.order_id.confirmation_date, DEFAULT_SERVER_DATETIME_FORMAT)\
            + timedelta(days=self.customer_lead or 0.0) - timedelta(days=self.order_id.company_id.security_lead)
        values.update({
            'company_id':
            self.order_id.company_id,
            'group_id':
            group_id,
            'sale_line_id':
            self.id,
            'date_planned':
            date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
            'route_ids':
            self.route_id,
            'warehouse_id':
            self.order_id.warehouse_id or False,
            'partner_dest_id':
            self.order_id.partner_shipping_id
        })
        return values

    @api.multi
    def _action_launch_procurement_rule(self):
        """
        Launch procurement group run method with required/custom fields genrated by a
        sale order line. procurement group will launch '_run_move', '_run_buy' or '_run_manufacture'
        depending on the sale order line product rule.
        """
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        errors = []
        for line in self:
            if line.state != 'sale' or not line.product_id.type in ('consu',
                                                                    'product'):
                continue
            qty = 0.0
            for move in line.move_ids.filtered(lambda r: r.state != 'cancel'):
                qty += move.product_qty
            if float_compare(qty,
                             line.product_uom_qty,
                             precision_digits=precision) >= 0:
                continue

            if not line.order_id.procurement_group_id:
                line.order_id.procurement_group_id = self.env[
                    'procurement.group'].create({
                        'name':
                        line.order_id.name,
                        'move_type':
                        line.order_id.picking_policy,
                        'sale_id':
                        line.order_id.id,
                        'partner_id':
                        line.order_id.partner_shipping_id.id,
                    })
            values = line._prepare_procurement_values(
                group_id=line.order_id.procurement_group_id)
            product_qty = line.product_uom_qty - qty
            try:
                self.env['procurement.group'].run(
                    line.product_id, product_qty, line.product_uom,
                    line.order_id.partner_shipping_id.property_stock_customer,
                    line.name, line.order_id.name, values)
            except UserError as error:
                errors.append(error.name)
        if errors:
            raise UserError('\n'.join(errors))
        return True

    @api.multi
    def _get_delivered_qty(self):
        self.ensure_one()
        super(SaleOrderLine, self)._get_delivered_qty()
        qty = 0.0
        for move in self.move_ids.filtered(
                lambda r: r.state == 'done' and not r.scrapped):
            if move.location_dest_id.usage == "customer":
                if not move.origin_returned_move_id:
                    qty += move.product_uom._compute_quantity(
                        move.product_uom_qty, self.product_uom)
            elif move.location_dest_id.usage != "customer" and move.to_refund:
                qty -= move.product_uom._compute_quantity(
                    move.product_uom_qty, self.product_uom)
        return qty

    @api.multi
    def _check_package(self):
        default_uom = self.product_id.uom_id
        pack = self.product_packaging
        qty = self.product_uom_qty
        q = default_uom._compute_quantity(pack.qty, self.product_uom)
        if qty and q and (qty % q):
            newqty = qty - (qty % q) + q
            return {
                'warning': {
                    'title':
                    _('Warning'),
                    'message':
                    _("This product is packaged by %.2f %s. You should sell %.2f %s."
                      ) % (pack.qty, default_uom.name, newqty,
                           self.product_uom.name),
                },
            }
        return {}

    def _check_routing(self):
        """ Verify the route of the product based on the warehouse
            return True if the product availibility in stock does not need to be verified,
            which is the case in MTO, Cross-Dock or Drop-Shipping
        """
        is_available = False
        product_routes = self.route_id or (
            self.product_id.route_ids +
            self.product_id.categ_id.total_route_ids)

        # Check MTO
        wh_mto_route = self.order_id.warehouse_id.mto_pull_id.route_id
        if wh_mto_route and wh_mto_route <= product_routes:
            is_available = True
        else:
            mto_route = False
            try:
                mto_route = self.env['stock.warehouse']._get_mto_route()
            except UserError:
                # if route MTO not found in ir_model_data, we treat the product as in MTS
                pass
            if mto_route and mto_route in product_routes:
                is_available = True

        # Check Drop-Shipping
        if not is_available:
            for pull_rule in product_routes.mapped('pull_ids'):
                if pull_rule.picking_type_id.sudo().default_location_src_id.usage == 'supplier' and\
                        pull_rule.picking_type_id.sudo().default_location_dest_id.usage == 'customer':
                    is_available = True
                    break

        return is_available

    def _update_line_quantity(self, values):
        if self.mapped('qty_delivered') and values['product_uom_qty'] < max(
                self.mapped('qty_delivered')):
            raise UserError(
                'You cannot decrease the ordered quantity below the delivered quantity.\n'
                'Create a return first.')
        for line in self:
            pickings = self.order_id.picking_ids.filtered(
                lambda p: p.state not in ('done', 'cancel'))
            for picking in pickings:
                picking.message_post(
                    "The quantity of %s has been updated from %d to %d in %s" %
                    (line.product_id.name, line.product_uom_qty,
                     values['product_uom_qty'], self.order_id.name))
        super(SaleOrderLine, self)._update_line_quantity(values)
示例#4
0
class MrpProductProduce(models.TransientModel):
    _name = "mrp.product.produce"
    _description = "Record Production"

    @api.model
    def default_get(self, fields):
        res = super(MrpProductProduce, self).default_get(fields)
        if self._context and self._context.get('active_id'):
            production = self.env['mrp.production'].browse(
                self._context['active_id'])
            serial_finished = (production.product_id.tracking == 'serial')
            if serial_finished:
                todo_quantity = 1.0
            else:
                main_product_moves = production.move_finished_ids.filtered(
                    lambda x: x.product_id.id == production.product_id.id)
                todo_quantity = production.product_qty - sum(
                    main_product_moves.mapped('quantity_done'))
                todo_quantity = todo_quantity if (todo_quantity > 0) else 0
            if 'production_id' in fields:
                res['production_id'] = production.id
            if 'product_id' in fields:
                res['product_id'] = production.product_id.id
            if 'product_uom_id' in fields:
                res['product_uom_id'] = production.product_uom_id.id
            if 'serial' in fields:
                res['serial'] = bool(serial_finished)
            if 'product_qty' in fields:
                res['product_qty'] = todo_quantity
            if 'produce_line_ids' in fields:
                lines = []
                for move in production.move_raw_ids.filtered(
                        lambda x: (x.product_id.tracking != 'none') and x.state
                        not in ('done', 'cancel') and x.bom_line_id):
                    qty_to_consume = float_round(
                        todo_quantity / move.bom_line_id.bom_id.product_qty *
                        move.bom_line_id.product_qty,
                        precision_rounding=move.product_uom.rounding,
                        rounding_method="UP")
                    for move_line in move.move_line_ids:
                        if float_compare(qty_to_consume,
                                         0.0,
                                         precision_rounding=move.product_uom.
                                         rounding) <= 0:
                            break
                        if move_line.lot_produced_id or float_compare(
                                move_line.product_uom_qty,
                                move_line.qty_done,
                                precision_rounding=move.product_uom.rounding
                        ) <= 0:
                            continue
                        to_consume_in_line = min(qty_to_consume,
                                                 move_line.product_uom_qty)
                        lines.append({
                            'move_id': move.id,
                            'qty_to_consume': to_consume_in_line,
                            'qty_done': 0.0,
                            'lot_id': move_line.lot_id.id,
                            'product_uom_id': move.product_uom.id,
                            'product_id': move.product_id.id,
                        })
                        qty_to_consume -= to_consume_in_line
                    if float_compare(
                            qty_to_consume,
                            0.0,
                            precision_rounding=move.product_uom.rounding) > 0:
                        if move.product_id.tracking == 'serial':
                            while float_compare(qty_to_consume,
                                                0.0,
                                                precision_rounding=move.
                                                product_uom.rounding) > 0:
                                lines.append({
                                    'move_id':
                                    move.id,
                                    'qty_to_consume':
                                    1,
                                    'qty_done':
                                    0.0,
                                    'product_uom_id':
                                    move.product_uom.id,
                                    'product_id':
                                    move.product_id.id,
                                })
                                qty_to_consume -= 1
                        else:
                            lines.append({
                                'move_id': move.id,
                                'qty_to_consume': qty_to_consume,
                                'qty_done': 0.0,
                                'product_uom_id': move.product_uom.id,
                                'product_id': move.product_id.id,
                            })

                res['produce_line_ids'] = [(0, 0, x) for x in lines]
        return res

    serial = fields.Boolean('Requires Serial')
    production_id = fields.Many2one('mrp.production', 'Production')
    product_id = fields.Many2one('product.product', 'Product')
    product_qty = fields.Float(
        string='Quantity',
        digits=dp.get_precision('Product Unit of Measure'),
        required=True)
    product_uom_id = fields.Many2one('product.uom', 'Unit of Measure')
    lot_id = fields.Many2one('stock.production.lot', string='Lot')
    produce_line_ids = fields.One2many('mrp.product.produce.line',
                                       'product_produce_id',
                                       string='Product to Track')
    product_tracking = fields.Selection(related="product_id.tracking",
                                        readonly=True)

    @api.multi
    def do_produce(self):
        # Nothing to do for lots since values are created using default data (stock.move.lots)
        quantity = self.product_qty
        if float_compare(quantity,
                         0,
                         precision_rounding=self.product_uom_id.rounding) <= 0:
            raise UserError(
                _("The production order for '%s' has no quantity specified") %
                self.product_id.display_name)
        for move in self.production_id.move_raw_ids:
            # TODO currently not possible to guess if the user updated quantity by hand or automatically by the produce wizard.
            if move.product_id.tracking == 'none' and move.state not in (
                    'done', 'cancel') and move.unit_factor:
                rounding = move.product_uom.rounding
                if self.product_id.tracking != 'none':
                    qty_to_add = float_round(quantity * move.unit_factor,
                                             precision_rounding=rounding)
                    move._generate_consumed_move_line(qty_to_add, self.lot_id)
                elif len(move._get_move_lines()) < 2:
                    move.quantity_done += float_round(
                        quantity * move.unit_factor,
                        precision_rounding=rounding)
                else:
                    move._set_quantity_done(quantity * move.unit_factor)
        for move in self.production_id.move_finished_ids:
            if move.product_id.tracking == 'none' and move.state not in (
                    'done', 'cancel'):
                rounding = move.product_uom.rounding
                if move.product_id.id == self.production_id.product_id.id:
                    move.quantity_done += float_round(
                        quantity, precision_rounding=rounding)
                elif move.unit_factor:
                    # byproducts handling
                    move.quantity_done += float_round(
                        quantity * move.unit_factor,
                        precision_rounding=rounding)
        self.check_finished_move_lots()
        if self.production_id.state == 'confirmed':
            self.production_id.write({
                'state': 'progress',
                'date_start': datetime.now(),
            })
        return {'type': 'ir.actions.act_window_close'}

    @api.multi
    def check_finished_move_lots(self):
        produce_move = self.production_id.move_finished_ids.filtered(
            lambda x: x.product_id == self.product_id and x.state not in
            ('done', 'cancel'))
        if produce_move and produce_move.product_id.tracking != 'none':
            if not self.lot_id:
                raise UserError(
                    _('You need to provide a lot for the finished product'))
            existing_move_line = produce_move.move_line_ids.filtered(
                lambda x: x.lot_id == self.lot_id)
            if existing_move_line:
                if self.product_id.tracking == 'serial':
                    raise UserError(
                        _('You cannot produce the same serial number twice.'))
                existing_move_line.product_uom_qty += self.product_qty
                existing_move_line.qty_done += self.product_qty
            else:
                location_dest_id = produce_move.location_dest_id.get_putaway_strategy(
                    self.product_id).id or produce_move.location_dest_id.id
                vals = {
                    'move_id': produce_move.id,
                    'product_id': produce_move.product_id.id,
                    'production_id': self.production_id.id,
                    'product_uom_qty': self.product_qty,
                    'product_uom_id': produce_move.product_uom.id,
                    'qty_done': self.product_qty,
                    'lot_id': self.lot_id.id,
                    'location_id': produce_move.location_id.id,
                    'location_dest_id': location_dest_id,
                }
                self.env['stock.move.line'].create(vals)

        for pl in self.produce_line_ids:
            if pl.qty_done:
                if not pl.lot_id:
                    raise UserError(
                        _('Please enter a lot or serial number for %s !' %
                          pl.product_id.name))
                if not pl.move_id:
                    # Find move_id that would match
                    move_id = self.production_id.move_raw_ids.filtered(
                        lambda x: x.product_id == pl.product_id and x.state
                        not in ('done', 'cancel'))
                    if move_id:
                        pl.move_id = move_id
                    else:
                        # create a move and put it in there
                        order = self.production_id
                        pl.move_id = self.env['stock.move'].create({
                            'name':
                            order.name,
                            'product_id':
                            pl.product_id.id,
                            'product_uom':
                            pl.product_uom_id.id,
                            'location_id':
                            order.location_src_id.id,
                            'location_dest_id':
                            self.product_id.property_stock_production.id,
                            'raw_material_production_id':
                            order.id,
                            'group_id':
                            order.procurement_group_id.id,
                            'origin':
                            order.name,
                            'state':
                            'confirmed'
                        })
                pl.move_id._generate_consumed_move_line(pl.qty_done,
                                                        self.lot_id,
                                                        lot=pl.lot_id)
        return True
示例#5
0
class Location(models.Model):
    _name = "stock.location"
    _description = "Inventory Locations"
    _parent_name = "location_id"
    _parent_store = True
    _parent_order = 'name'
    _order = 'parent_left'
    _rec_name = 'complete_name'

    @api.model
    def default_get(self, fields):
        res = super(Location, self).default_get(fields)
        if 'barcode' in fields and 'barcode' not in res and res.get(
                'complete_name'):
            res['barcode'] = res['complete_name']
        return res

    name = fields.Char('Location Name', required=True, translate=True)
    complete_name = fields.Char("Full Location Name",
                                compute='_compute_complete_name',
                                store=True)
    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "By unchecking the active field, you may hide a location without deleting it."
    )
    usage = fields.Selection(
        [('supplier', 'Vendor Location'), ('view', 'View'),
         ('internal', 'Internal Location'), ('customer', 'Customer Location'),
         ('inventory', 'Inventory Loss'), ('procurement', 'Procurement'),
         ('production', 'Production'), ('transit', 'Transit Location')],
        string='Location Type',
        default='internal',
        index=True,
        required=True,
        help=
        "* Vendor Location: Virtual location representing the source location for products coming from your vendors"
        "\n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products"
        "\n* Internal Location: Physical locations inside your own warehouses,"
        "\n* Customer Location: Virtual location representing the destination location for products sent to your customers"
        "\n* Inventory Loss: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)"
        "\n* Procurement: Virtual location serving as temporary counterpart for procurement operations when the source (vendor or production) is not known yet. This location should be empty when the procurement scheduler has finished running."
        "\n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products"
        "\n* Transit Location: Counterpart location that should be used in inter-company or inter-warehouses operations"
    )
    location_id = fields.Many2one(
        'stock.location',
        'Parent Location',
        index=True,
        ondelete='cascade',
        help=
        "The parent location that includes this location. Example : The 'Dispatch Zone' is the 'Gate 1' parent location."
    )
    child_ids = fields.One2many('stock.location', 'location_id', 'Contains')
    partner_id = fields.Many2one('res.partner',
                                 'Owner',
                                 help="Owner of the location if not internal")
    comment = fields.Text('Additional Information')
    posx = fields.Integer(
        'Corridor (X)',
        default=0,
        help="Optional localization details, for information purpose only")
    posy = fields.Integer(
        'Shelves (Y)',
        default=0,
        help="Optional localization details, for information purpose only")
    posz = fields.Integer(
        'Height (Z)',
        default=0,
        help="Optional localization details, for information purpose only")
    parent_left = fields.Integer('Left Parent', index=True)
    parent_right = fields.Integer('Right Parent', index=True)
    company_id = fields.Many2one(
        'res.company',
        'Company',
        default=lambda self: self.env['res.company']._company_default_get(
            'stock.location'),
        index=True,
        help='Let this field empty if this location is shared between companies'
    )
    scrap_location = fields.Boolean(
        'Is a Scrap Location?',
        default=False,
        help=
        'Check this box to allow using this location to put scrapped/damaged goods.'
    )
    return_location = fields.Boolean(
        'Is a Return Location?',
        help='Check this box to allow using this location as a return location.'
    )
    removal_strategy_id = fields.Many2one(
        'product.removal',
        'Removal Strategy',
        help=
        "Defines the default method used for suggesting the exact location (shelf) where to take the products from, which lot etc. for this location. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."
    )
    putaway_strategy_id = fields.Many2one(
        'product.putaway',
        'Put Away Strategy',
        help=
        "Defines the default method used for suggesting the exact location (shelf) where to store the products. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here."
    )
    barcode = fields.Char('Barcode', copy=False, oldname='loc_barcode')

    _sql_constraints = [
        ('barcode_company_uniq', 'unique (barcode,company_id)',
         'The barcode for a location must be unique per company !')
    ]

    @api.one
    @api.depends('name', 'location_id.name')
    def _compute_complete_name(self):
        """ Forms complete name of location from parent location to child location. """
        name = self.name
        current = self
        while current.location_id:
            current = current.location_id
            name = '%s/%s' % (current.name, name)
        self.complete_name = name

    def name_get(self):
        ret_list = []
        for location in self:
            orig_location = location
            name = location.name
            while location.location_id and location.usage != 'view':
                location = location.location_id
                if not name:
                    raise UserError(
                        _('You have to set a name for this location.'))
                name = location.name + "/" + name
            ret_list.append((orig_location.id, name))
        return ret_list

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        """ search full name and barcode """
        if args is None:
            args = []
        recs = self.search([
            '|', ('barcode', operator, name), ('complete_name', operator, name)
        ] + args,
                           limit=limit)
        return recs.name_get()

    def get_putaway_strategy(self, product):
        ''' Returns the location where the product has to be put, if any compliant putaway strategy is found. Otherwise returns None.'''
        current_location = self
        putaway_location = self.env['stock.location']
        while current_location and not putaway_location:
            if current_location.putaway_strategy_id:
                putaway_location = current_location.putaway_strategy_id.putaway_apply(
                    product)
            current_location = current_location.location_id
        return putaway_location

    @api.returns('stock.warehouse', lambda value: value.id)
    def get_warehouse(self):
        """ Returns warehouse id of warehouse that contains location """
        return self.env['stock.warehouse'].search(
            [('view_location_id.parent_left', '<=', self.parent_left),
             ('view_location_id.parent_right', '>=', self.parent_left)],
            limit=1)

    def should_bypass_reservation(self):
        self.ensure_one()
        return self.usage in ('supplier', 'customer', 'inventory',
                              'production') or self.scrap_location
示例#6
0
class View(models.Model):

    _name = "ir.ui.view"
    _inherit = ["ir.ui.view", "website.seo.metadata"]

    customize_show = fields.Boolean("Show As Optional Inherit", default=False)
    # @todo Flectra:
    # Remove ondelete='cascade' (But need to check side-effects!!)
    #
    # When we Uninstall ``website`` module then some of
    # ``portal`` module views are also deleted (For Ex. portal_show_sign_in)
    # Find proper way to do the same, remove website_id from different
    # module's view & keep those views, do not delete those views permanently.
    website_id = fields.Many2one('website',
                                 ondelete='cascade',
                                 string="Website")
    page_ids = fields.One2many('website.page',
                               compute='_compute_page_ids',
                               store=False)
    is_cloned = fields.Boolean(string='Cloned',
                               copy=False,
                               default=False,
                               help="This view is cloned"
                               "(not present physically in file system) "
                               "from default website's view for "
                               "supporting multi-website feature.")

    @api.one
    def _compute_page_ids(self):
        self.page_ids = self.env['website.page'].search([('view_id', '=',
                                                          self.id)])

    @api.multi
    def unlink(self):
        result = super(View, self).unlink()
        self.clear_caches()
        return result

    @api.multi
    def _sort_suitability_key(self):
        """ Key function to sort views by descending suitability
            Suitability of a view is defined as follow:
                * if the view and request website_id are matched
                * then if the view has no set website
        """
        self.ensure_one()
        context_website_id = self.env.context.get('website_id', 1)
        website_id = self.website_id.id or 0
        different_website = context_website_id != website_id
        return (different_website, website_id)

    def filter_duplicate(self):
        """ Filter current recordset only keeping the most suitable view per distinct key """
        filtered = self.env['ir.ui.view']
        for dummy, group in groupby(self, key=lambda record: record.key):
            filtered += sorted(
                group, key=lambda record: record._sort_suitability_key())[0]
        return filtered

    @api.model
    def _view_obj(self, view_id):
        if isinstance(view_id, pycompat.string_types):
            if 'website_id' in self._context:
                domain = [('key', '=', view_id), '|',
                          ('website_id', '=', False),
                          ('website_id', '=', self._context.get('website_id'))]
                order = 'website_id'
            else:
                domain = [('key', '=', view_id)]
                order = self._order
            views = self.search(domain, order=order)
            if views:
                return views.filter_duplicate()
            else:
                return self.env.ref(view_id)
        elif isinstance(view_id, pycompat.integer_types):
            return self.browse(view_id)

        # assume it's already a view object (WTF?)
        return view_id

    @api.model
    def _get_inheriting_views_arch_domain(self, view_id, model):
        domain = super(View,
                       self)._get_inheriting_views_arch_domain(view_id, model)
        return [
            '|', ('website_id', '=', False),
            ('website_id', '=', self.env.context.get('website_id'))
        ] + domain

    @api.model
    @tools.ormcache_context('self._uid', 'xml_id', keys=('website_id', ))
    def get_view_id(self, xml_id):
        if 'website_id' in self._context and not isinstance(
                xml_id, pycompat.integer_types):
            domain = [('key', '=', xml_id), '|',
                      ('website_id', '=', self._context['website_id']),
                      ('website_id', '=', False)]
            view = self.search(domain, order='website_id', limit=1)
            if not view:
                _logger.warning("Could not find view object with xml_id '%s'",
                                xml_id)
                raise ValueError('View %r in website %r not found' %
                                 (xml_id, self._context['website_id']))
            return view.id
        return super(View, self).get_view_id(xml_id)

    @api.multi
    def render(self, values=None, engine='ir.qweb'):
        """ Render the template. If website is enabled on request, then extend rendering context with website values. """
        new_context = dict(self._context)
        if request and getattr(request, 'is_frontend', False):

            editable = request.website.is_publisher()
            translatable = editable and self._context.get(
                'lang') != request.website.default_lang_code
            editable = not translatable and editable

            # in edit mode ir.ui.view will tag nodes
            if not translatable and not self.env.context.get(
                    'rendering_bundle'):
                if editable:
                    new_context = dict(self._context, inherit_branding=True)
                elif request.env.user.has_group(
                        'website.group_website_publisher'):
                    new_context = dict(self._context,
                                       inherit_branding_auto=True)

        if self._context != new_context:
            self = self.with_context(new_context)
        return super(View, self).render(values, engine=engine)

    @api.model
    def _prepare_qcontext(self):
        """ Returns the qcontext : rendering context with website specific value (required
            to render website layout template)
        """
        qcontext = super(View, self)._prepare_qcontext()

        if request and getattr(request, 'is_frontend', False):
            editable = request.website.is_publisher()
            translatable = editable and self._context.get(
                'lang') != request.env['ir.http']._get_default_lang().code
            editable = not translatable and editable

            if 'main_object' not in qcontext:
                qcontext['main_object'] = self

            qcontext.update(
                dict(
                    self._context.copy(),
                    website=request.website,
                    url_for=url_for,
                    res_company=request.website.company_id.sudo(),
                    default_lang_code=request.env['ir.http']._get_default_lang(
                    ).code,
                    languages=request.env['ir.http']._get_language_codes(),
                    translatable=translatable,
                    editable=editable,
                    menu_data=self.env['ir.ui.menu'].load_menus_root()
                    if request.website.is_user() else None,
                ))

        return qcontext

    @api.model
    def get_default_lang_code(self):
        website_id = self.env.context.get('website_id')
        if website_id:
            lang_code = self.env['website'].browse(
                website_id).default_lang_code
            return lang_code
        else:
            return super(View, self).get_default_lang_code()

    @api.multi
    def redirect_to_page_manager(self):
        return {
            'type': 'ir.actions.act_url',
            'url': '/website/pages',
            'target': 'self',
        }

    # Multi Website: Automated Action On Create Rule
    ##################################################
    # If views are manually created for default website,
    # then it'll automatically cloned for other websites.
    #
    # As this method is also called when new website is created.
    # Because at the time of website creation ``Home`` page will be cloned,
    # So, this method will automatically triggered to
    # cloned all customize view(s).
    @api.model
    def multi_website_view_rule(self):
        default_website = self.env['website'].search([('is_default_website',
                                                       '=', True)])
        ir_model_data = self.env['ir.model.data']
        for website in self.env['website'].search([('is_default_website', '=',
                                                    False)]):
            for cus_view in self.search([('website_id', '=',
                                          default_website.id),
                                         ('customize_show', '=', True),
                                         ('is_cloned', '=', False), '|',
                                         ('active', '=', False),
                                         ('active', '=', True)]):
                if not self.search([('key', '=', cus_view.key + '_' +
                                     website.website_code), '|',
                                    ('active', '=', False),
                                    ('active', '=', True)]):
                    new_cus_view = cus_view.copy({
                        'is_cloned':
                        True,
                        'key':
                        cus_view.key + '_' + website.website_code,
                        'website_id':
                        website.id
                    })
                    new_inherit_id = self.search([
                        ('key', '=', new_cus_view.inherit_id.key + '_' +
                         website.website_code), '|', ('active', '=', False),
                        ('active', '=', True)
                    ])
                    if new_cus_view.inherit_id and new_inherit_id:
                        new_cus_view.write({
                            'inherit_id': new_inherit_id.id,
                        })
                    model_data_id = ir_model_data.create({
                        'model':
                        cus_view.model_data_id.model,
                        'name':
                        cus_view.model_data_id.name + '_' +
                        website.website_code,
                        'res_id':
                        new_cus_view.id,
                        'module':
                        cus_view.model_data_id.module,
                    })
                    new_cus_view.write({'model_data_id': model_data_id})

    # Add the website_id to each customize QWeb view(s) at the time
    # of creation of new customize QWeb view(s).
    @api.model
    def create(self, values):
        # For Theme's View(s)
        if values.get('key') and values.get('type') == 'qweb' and \
                self.env.context.get('install_mode_data'):
            module_name = self.env.context['install_mode_data']['module']
            module_obj = self.env['ir.module.module'].sudo().search([
                ('name', '=', module_name)
            ])
            if module_obj and \
                    (module_obj.category_id.name == 'Theme'
                     or (module_obj.category_id.parent_id
                         and module_obj.category_id.parent_id.name
                         == 'Theme')):
                values.update({
                    'website_id': module_obj.website_ids.id,
                })
        return super(View, self).create(self._compute_defaults(values))

    # Keep other website's view as it is when run server using -i/-u
    # As other website's views are not present anywhere in FS(file system).
    # So, once those are created/cloned from default website,
    # they can be changed/updated via debug mode only(ir.ui.view)
    # Menu: Settings/Technical/User Interface/Views
    #
    # Scenario 1:
    # -----------
    # For Delete those views, Manually set ``is_cloned`` field to ``False``
    # @todo Flectra:
    # But Actually View is not deleted, It'll create again from
    # default website's view,
    # Find a way to delete website specific views form DB.
    #
    # Scenario 2:
    # -----------
    # If you write the code for already cloned views in FS(file system)/Module
    # to upgrade/update those views, then at the time of module update
    # process that cloned views id are found in FS(file system)/Module,
    # So in those particular views ``is_cloned`` will automatically
    # set to ``False`` (Definitely it'll be done from another method!!),
    # because now those views are not anymore cloned,
    # now they are physically present!!
    @api.multi
    def unlink(self):
        for view in self:
            if view.is_cloned:
                # Do not delete cloned view(s)
                # ----------------
                # 'View(s) that you want delete are '
                # 'cloned view(s).\n'
                # 'Cloned view(s) are automatically created '
                # 'for supporting multi website feature.\n'
                # 'If you still want to delete this view(s) '
                # 'then first Uncheck(set to False) the '
                # 'cloned field.\n'
                # 'By deleting cloned view(s) multi website '
                # 'will not work properly.\n'
                # 'So, Be sure before deleting view(s).'
                return True
            return super(View, view).unlink()
示例#7
0
class ResourceCalendar(models.Model):
    """ Calendar model for a resource. It has

     - attendance_ids: list of resource.calendar.attendance that are a working
                       interval in a given weekday.
     - leave_ids: list of leaves linked to this calendar. A leave can be general
                  or linked to a specific resource, depending on its resource_id.

    All methods in this class use intervals. An interval is a tuple holding
    (begin_datetime, end_datetime). A list of intervals is therefore a list of
    tuples, holding several intervals of work or leaves. """
    _name = "resource.calendar"
    _description = "Resource Calendar"
    _interval_obj = namedtuple('Interval',
                               ('start_datetime', 'end_datetime', 'data'))

    @api.model
    def default_get(self, fields):
        res = super(ResourceCalendar, self).default_get(fields)
        if not res.get('name') and res.get('company_id'):
            res['name'] = _(
                'Working Hours of %s') % self.env['res.company'].browse(
                    res['company_id']).name
        return res

    def _get_default_attendance_ids(self):
        return [(0, 0, {
            'name': _('Monday Morning'),
            'dayofweek': '0',
            'hour_from': 8,
            'hour_to': 12
        }),
                (0, 0, {
                    'name': _('Monday Evening'),
                    'dayofweek': '0',
                    'hour_from': 13,
                    'hour_to': 17
                }),
                (0, 0, {
                    'name': _('Tuesday Morning'),
                    'dayofweek': '1',
                    'hour_from': 8,
                    'hour_to': 12
                }),
                (0, 0, {
                    'name': _('Tuesday Evening'),
                    'dayofweek': '1',
                    'hour_from': 13,
                    'hour_to': 17
                }),
                (0, 0, {
                    'name': _('Wednesday Morning'),
                    'dayofweek': '2',
                    'hour_from': 8,
                    'hour_to': 12
                }),
                (0, 0, {
                    'name': _('Wednesday Evening'),
                    'dayofweek': '2',
                    'hour_from': 13,
                    'hour_to': 17
                }),
                (0, 0, {
                    'name': _('Thursday Morning'),
                    'dayofweek': '3',
                    'hour_from': 8,
                    'hour_to': 12
                }),
                (0, 0, {
                    'name': _('Thursday Evening'),
                    'dayofweek': '3',
                    'hour_from': 13,
                    'hour_to': 17
                }),
                (0, 0, {
                    'name': _('Friday Morning'),
                    'dayofweek': '4',
                    'hour_from': 8,
                    'hour_to': 12
                }),
                (0, 0, {
                    'name': _('Friday Evening'),
                    'dayofweek': '4',
                    'hour_from': 13,
                    'hour_to': 17
                })]

    name = fields.Char(required=True)
    company_id = fields.Many2one(
        'res.company',
        'Company',
        default=lambda self: self.env['res.company']._company_default_get())
    attendance_ids = fields.One2many('resource.calendar.attendance',
                                     'calendar_id',
                                     'Working Time',
                                     copy=True,
                                     default=_get_default_attendance_ids)
    leave_ids = fields.One2many('resource.calendar.leaves', 'calendar_id',
                                'Leaves')
    global_leave_ids = fields.One2many('resource.calendar.leaves',
                                       'calendar_id',
                                       'Global Leaves',
                                       domain=[('resource_id', '=', False)])

    # --------------------------------------------------
    # Utility methods
    # --------------------------------------------------

    def _merge_kw(self, kw, kw_ext):
        new_kw = dict(kw, **kw_ext)
        new_kw.update(
            attendances=kw.get('attendances',
                               self.env['resource.calendar.attendance'])
            | kw_ext.get('attendances',
                         self.env['resource.calendar.attendance']),
            leaves=kw.get('leaves', self.env['resource.calendar.leaves'])
            | kw_ext.get('leaves', self.env['resource.calendar.leaves']))
        return new_kw

    def _interval_new(self, start_datetime, end_datetime, kw=None):
        kw = kw if kw is not None else dict()
        kw.setdefault('attendances', self.env['resource.calendar.attendance'])
        kw.setdefault('leaves', self.env['resource.calendar.leaves'])
        return self._interval_obj(start_datetime, end_datetime, kw)

    def _interval_exclude_left(self, interval, interval_dst):
        return self._interval_obj(
            interval.start_datetime > interval_dst.end_datetime
            and interval.start_datetime or interval_dst.end_datetime,
            interval.end_datetime,
            self._merge_kw(interval.data, interval_dst.data))

    def _interval_exclude_right(self, interval, interval_dst):
        return self._interval_obj(
            interval.start_datetime,
            interval.end_datetime < interval_dst.start_datetime
            and interval.end_datetime or interval_dst.start_datetime,
            self._merge_kw(interval.data, interval_dst.data))

    def _interval_or(self, interval, interval_dst):
        return self._interval_obj(
            interval.start_datetime < interval_dst.start_datetime
            and interval.start_datetime or interval_dst.start_datetime,
            interval.end_datetime > interval_dst.end_datetime
            and interval.end_datetime or interval_dst.end_datetime,
            self._merge_kw(interval.data, interval_dst.data))

    def _interval_and(self, interval, interval_dst):
        if interval.start_datetime > interval_dst.end_datetime or interval.end_datetime < interval_dst.start_datetime:
            return None
        return self._interval_obj(
            interval.start_datetime > interval_dst.start_datetime
            and interval.start_datetime or interval_dst.start_datetime,
            interval.end_datetime < interval_dst.end_datetime
            and interval.end_datetime or interval_dst.end_datetime,
            self._merge_kw(interval.data, interval_dst.data))

    def _interval_merge(self, intervals):
        """ Sort intervals based on starting datetime and merge overlapping intervals.

        :return list cleaned: sorted intervals merged without overlap """
        intervals = sorted(intervals,
                           key=itemgetter(0))  # sort on first datetime
        cleaned = []
        working_interval = None
        while intervals:
            current_interval = intervals.pop(0)
            if not working_interval:  # init
                working_interval = self._interval_new(*current_interval)
            elif working_interval[1] < current_interval[
                    0]:  # interval is disjoint
                cleaned.append(working_interval)
                working_interval = self._interval_new(*current_interval)
            elif working_interval[1] < current_interval[
                    1]:  # union of greater intervals
                working_interval = self._interval_or(working_interval,
                                                     current_interval)
        if working_interval:  # handle void lists
            cleaned.append(working_interval)
        return cleaned

    @api.model
    def _interval_remove_leaves(self, interval, leave_intervals):
        """ Remove leave intervals from a base interval

        :param tuple interval: an interval (see above) that is the base interval
                               from which the leave intervals will be removed
        :param list leave_intervals: leave intervals to remove
        :return list intervals: ordered intervals with leaves removed """
        intervals = []
        leave_intervals = self._interval_merge(leave_intervals)
        current_interval = interval
        for leave in leave_intervals:
            # skip if ending before the current start datetime
            if leave[1] <= current_interval[0]:
                continue
            # skip if starting after current end datetime; break as leaves are ordered and
            # are therefore all out of range
            if leave[0] >= current_interval[1]:
                break
            # begins within current interval: close current interval and begin a new one
            # that begins at the leave end datetime
            if current_interval[0] < leave[0] < current_interval[1]:
                intervals.append(
                    self._interval_exclude_right(current_interval, leave))
                current_interval = self._interval_exclude_left(interval, leave)
            # ends within current interval: set current start datetime as leave end datetime
            if current_interval[0] <= leave[1]:
                current_interval = self._interval_exclude_left(interval, leave)
        if current_interval and current_interval[0] < interval[
                1]:  # remove intervals moved outside base interval due to leaves
            intervals.append(current_interval)
        return intervals

    @api.model
    def _interval_schedule_hours(self, intervals, hour, backwards=False):
        """ Schedule hours in intervals. The last matching interval is truncated
        to match the specified hours. This method can be applied backwards meaning
        scheduling hours going in the past. In that case truncating last interval
        is done accordingly. If number of hours to schedule is greater than possible
        scheduling in the given intervals, returned result equals intervals.

        :param list intervals:  a list of time intervals
        :param int/float hours: number of hours to schedule. It will be converted
                                into a timedelta, but should be submitted as an
                                int or float
        :param boolean backwards: schedule starting from last hour

        :return list results: a list of time intervals """
        if backwards:
            intervals.reverse(
            )  # first interval is the last working interval of the day
        results = []
        res = timedelta()
        limit = timedelta(hours=hour)
        for interval in intervals:
            res += interval[1] - interval[0]
            if res > limit and not backwards:
                interval = (
                    interval[0], interval[1] +
                    relativedelta(seconds=(limit - res).total_seconds()))
            elif res > limit:
                interval = (
                    interval[0] +
                    relativedelta(seconds=(res - limit).total_seconds()),
                    interval[1])
            results.append(interval)
            if res > limit:
                break
        if backwards:
            results.reverse()  # return interval with increasing starting times
        return results

    # --------------------------------------------------
    # Date and hours computation
    # --------------------------------------------------

    @api.multi
    def _get_day_attendances(self, day_date, start_time, end_time):
        """ Given a day date, return matching attendances. Those can be limited
        by starting and ending time objects. """
        self.ensure_one()
        weekday = day_date.weekday()
        attendances = self.env['resource.calendar.attendance']

        for attendance in self.attendance_ids.filtered(
                lambda att: int(att.dayofweek) == weekday and not (
                    att.date_from and fields.Date.from_string(att.date_from) >
                    day_date) and not (att.date_to and fields.Date.from_string(
                        att.date_to) < day_date)):
            if start_time and float_to_time(attendance.hour_to) < start_time:
                continue
            if end_time and float_to_time(attendance.hour_from) > end_time:
                continue
            attendances |= attendance
        return attendances

    @api.multi
    def _get_weekdays(self):
        """ Return the list of weekdays that contain at least one working
        interval. """
        self.ensure_one()
        return list({int(d) for d in self.attendance_ids.mapped('dayofweek')})

    @api.multi
    def _get_next_work_day(self, day_date):
        """ Get following date of day_date, based on resource.calendar. """
        self.ensure_one()
        weekdays = self._get_weekdays()
        weekday = next(
            (item for item in weekdays if item > day_date.weekday()),
            weekdays[0])
        days = weekday - day_date.weekday()
        if days < 0:
            days = 7 + days

        return day_date + relativedelta(days=days)

    @api.multi
    def _get_previous_work_day(self, day_date):
        """ Get previous date of day_date, based on resource.calendar. """
        self.ensure_one()
        weekdays = self._get_weekdays()
        weekdays.reverse()
        weekday = next(
            (item for item in weekdays if item < day_date.weekday()),
            weekdays[0])
        days = weekday - day_date.weekday()
        if days > 0:
            days = days - 7

        return day_date + relativedelta(days=days)

    @api.multi
    def _get_leave_intervals(self,
                             resource_id=None,
                             start_datetime=None,
                             end_datetime=None):
        """Get the leaves of the calendar. Leaves can be filtered on the resource,
        and on a start and end datetime.

        Leaves are encoded from a given timezone given by their tz field. COnverting
        them in naive user timezone require to use the leave timezone, not the current
        user timezone. For example people managing leaves could be from different
        timezones and the correct one is the one used when encoding them.

        :return list leaves: list of time intervals """
        self.ensure_one()
        if resource_id:
            domain = [
                '|', ('resource_id', '=', resource_id),
                ('resource_id', '=', False)
            ]
        else:
            domain = [('resource_id', '=', False)]
        if start_datetime:
            # domain += [('date_to', '>', fields.Datetime.to_string(to_naive_utc(start_datetime, self.env.user)))]
            domain += [('date_to', '>',
                        fields.Datetime.to_string(start_datetime +
                                                  timedelta(days=-1)))]
        if end_datetime:
            # domain += [('date_from', '<', fields.Datetime.to_string(to_naive_utc(end_datetime, self.env.user)))]
            domain += [
                ('date_from', '<',
                 fields.Datetime.to_string(end_datetime + timedelta(days=1)))
            ]
        leaves = self.env['resource.calendar.leaves'].search(domain +
                                                             [('calendar_id',
                                                               '=', self.id)])

        filtered_leaves = self.env['resource.calendar.leaves']
        for leave in leaves:
            if start_datetime:
                leave_date_to = to_tz(
                    fields.Datetime.from_string(leave.date_to), leave.tz)
                if not leave_date_to >= start_datetime:
                    continue
            if end_datetime:
                leave_date_from = to_tz(
                    fields.Datetime.from_string(leave.date_from), leave.tz)
                if not leave_date_from <= end_datetime:
                    continue
            filtered_leaves += leave

        return [
            self._interval_new(
                to_tz(fields.Datetime.from_string(leave.date_from), leave.tz),
                to_tz(fields.Datetime.from_string(leave.date_to), leave.tz),
                {'leaves': leave}) for leave in filtered_leaves
        ]

    def _iter_day_attendance_intervals(self, day_date, start_time, end_time):
        """ Get an iterator of all interval of current day attendances. """
        for calendar_working_day in self._get_day_attendances(
                day_date, start_time, end_time):
            from_time = float_to_time(calendar_working_day.hour_from)
            to_time = float_to_time(calendar_working_day.hour_to)

            dt_f = datetime.datetime.combine(day_date,
                                             max(from_time, start_time))
            dt_t = datetime.datetime.combine(day_date, min(to_time, end_time))

            yield self._interval_new(dt_f, dt_t,
                                     {'attendances': calendar_working_day})

    @api.multi
    def _get_day_work_intervals(self,
                                day_date,
                                start_time=None,
                                end_time=None,
                                compute_leaves=False,
                                resource_id=None):
        """ Get the working intervals of the day given by day_date based on
        current calendar. Input should be given in current user timezone and
        output is given in naive UTC, ready to be used by the orm or webclient.

        :param time start_time: time object that is the beginning hours in user TZ
        :param time end_time: time object that is the ending hours in user TZ
        :param boolean compute_leaves: indicates whether to compute the
                                       leaves based on calendar and resource.
        :param int resource_id: the id of the resource to take into account when
                                computing the work intervals. Leaves notably are
                                filtered according to the resource.

        :return list intervals: list of time intervals in UTC """
        self.ensure_one()

        if not start_time:
            start_time = datetime.time.min
        if not end_time:
            end_time = datetime.time.max

        working_intervals = [
            att_interval
            for att_interval in self._iter_day_attendance_intervals(
                day_date, start_time, end_time)
        ]

        # filter according to leaves
        if compute_leaves:
            leaves = self._get_leave_intervals(
                resource_id=resource_id,
                start_datetime=datetime.datetime.combine(day_date, start_time),
                end_datetime=datetime.datetime.combine(day_date, end_time))
            working_intervals = [
                sub_interval for interval in working_intervals
                for sub_interval in self._interval_remove_leaves(
                    interval, leaves)
            ]

        # adapt tz
        return [
            self._interval_new(to_naive_utc(interval[0], self.env.user),
                               to_naive_utc(interval[1], self.env.user),
                               interval[2]) for interval in working_intervals
        ]

    def _get_day_leave_intervals(self, day_date, start_time, end_time,
                                 resource_id):
        """ Get the leave intervals of the day given by day_date based on current
        calendar. Input should be given in current user timezone and
        output is given in naive UTC, ready to be used by the orm or webclient.

        :param time start_time: time object that is the beginning hours in user TZ
        :param time end_time: time object that is the ending hours in user TZ
        :param int resource_id: the id of the resource to take into account when
                                computing the leaves.

        :return list intervals: list of time intervals in UTC """
        self.ensure_one()

        if not start_time:
            start_time = datetime.time.min
        if not end_time:
            end_time = datetime.time.max

        working_intervals = [
            att_interval
            for att_interval in self._iter_day_attendance_intervals(
                day_date, start_time, end_time)
        ]

        leaves_intervals = self._get_leave_intervals(
            resource_id=resource_id,
            start_datetime=datetime.datetime.combine(day_date, start_time),
            end_datetime=datetime.datetime.combine(day_date, end_time))

        final_intervals = [
            i for i in [
                self._interval_and(leave_interval, work_interval)
                for leave_interval in leaves_intervals
                for work_interval in working_intervals
            ] if i
        ]

        # adapt tz
        return [
            self._interval_new(to_naive_utc(interval[0], self.env.user),
                               to_naive_utc(interval[1], self.env.user),
                               interval[2]) for interval in final_intervals
        ]

    # --------------------------------------------------
    # Main computation API
    # --------------------------------------------------

    def _iter_work_intervals(self,
                             start_dt,
                             end_dt,
                             resource_id,
                             compute_leaves=True):
        """ Lists the current resource's work intervals between the two provided
        datetimes (inclusive) expressed in UTC, for each worked day. """
        if not end_dt:
            end_dt = datetime.datetime.combine(start_dt.date(),
                                               datetime.time.max)

        start_dt = to_naive_user_tz(start_dt, self.env.user)
        end_dt = to_naive_user_tz(end_dt, self.env.user)

        for day in rrule.rrule(rrule.DAILY,
                               dtstart=start_dt,
                               until=end_dt,
                               byweekday=self._get_weekdays()):
            start_time = datetime.time.min
            if day.date() == start_dt.date():
                start_time = start_dt.time()
            end_time = datetime.time.max
            if day.date() == end_dt.date(
            ) and end_dt.time() != datetime.time():
                end_time = end_dt.time()

            intervals = self._get_day_work_intervals(
                day.date(),
                start_time=start_time,
                end_time=end_time,
                compute_leaves=compute_leaves,
                resource_id=resource_id)
            if intervals:
                yield intervals

    def _iter_leave_intervals(self, start_dt, end_dt, resource_id):
        """ Lists the current resource's leave intervals between the two provided
        datetimes (inclusive) expressed in UTC. """
        if not end_dt:
            end_dt = datetime.datetime.combine(start_dt.date(),
                                               datetime.time.max)

        start_dt = to_naive_user_tz(start_dt, self.env.user)
        end_dt = to_naive_user_tz(end_dt, self.env.user)

        for day in rrule.rrule(rrule.DAILY,
                               dtstart=start_dt,
                               until=end_dt,
                               byweekday=self._get_weekdays()):
            start_time = datetime.time.min
            if day.date() == start_dt.date():
                start_time = start_dt.time()
            end_time = datetime.time.max
            if day.date() == end_dt.date(
            ) and end_dt.time() != datetime.time():
                end_time = end_dt.time()

            intervals = self._get_day_leave_intervals(day.date(), start_time,
                                                      end_time, resource_id)

            if intervals:
                yield intervals

    def _iter_work_hours_count(self, from_datetime, to_datetime, resource_id):
        """ Lists the current resource's work hours count between the two provided
        datetime expressed in naive UTC. """

        for interval in self._iter_work_intervals(from_datetime, to_datetime,
                                                  resource_id):
            td = timedelta()
            for work_interval in interval:
                td += work_interval[1] - work_interval[0]
            yield (interval[0][0].date(), td.total_seconds() / 3600.0)

    def _iter_work_days(self, from_date, to_date, resource_id):
        """ Lists the current resource's work days between the two provided
        dates (inclusive) expressed in naive UTC.

        Work days are the company or service's open days (as defined by the
        resource.calendar) minus the resource's own leaves.

        :param datetime.date from_date: start of the interval to check for
                                        work days (inclusive)
        :param datetime.date to_date: end of the interval to check for work
                                      days (inclusive)
        :rtype: list(datetime.date)
        """
        for interval in self._iter_work_intervals(
                datetime.datetime(from_date.year, from_date.month,
                                  from_date.day),
                datetime.datetime(to_date.year, to_date.month, to_date.day),
                resource_id):
            yield interval[0][0].date()

    @api.multi
    def _is_work_day(self, date, resource_id):
        """ Whether the provided date is a work day for the subject resource.

        :type date: datetime.date
        :rtype: bool """
        return bool(next(self._iter_work_days(date, date, resource_id), False))

    @api.multi
    def get_work_hours_count(self,
                             start_dt,
                             end_dt,
                             resource_id,
                             compute_leaves=True):
        """ Count number of work hours between two datetimes. For compute_leaves,
        resource_id: see _get_day_work_intervals. """
        res = timedelta()
        for intervals in self._iter_work_intervals(
                start_dt, end_dt, resource_id, compute_leaves=compute_leaves):
            for interval in intervals:
                res += interval[1] - interval[0]
        return res.total_seconds() / 3600.0

    # --------------------------------------------------
    # Scheduling API
    # --------------------------------------------------

    @api.multi
    def _schedule_hours(self,
                        hours,
                        day_dt,
                        compute_leaves=False,
                        resource_id=None):
        """ Schedule hours of work, using a calendar and an optional resource to
        compute working and leave days. This method can be used backwards, i.e.
        scheduling days before a deadline. For compute_leaves, resource_id:
        see _get_day_work_intervals. This method does not use rrule because
        rrule does not allow backwards computation.

        :param int hours: number of hours to schedule. Use a negative number to
                          compute a backwards scheduling.
        :param datetime day_dt: reference date to compute working days. If days is
                                > 0 date is the starting date. If days is < 0
                                date is the ending date.

        :return list intervals: list of time intervals in naive UTC """
        self.ensure_one()
        backwards = (hours < 0)
        intervals = []
        remaining_hours, iterations = abs(hours * 1.0), 0

        day_dt_tz = to_naive_user_tz(day_dt, self.env.user)
        current_datetime = day_dt_tz

        call_args = dict(compute_leaves=compute_leaves,
                         resource_id=resource_id)

        while float_compare(
                remaining_hours, 0.0,
                precision_digits=2) in (1, 0) and iterations < 1000:
            if backwards:
                call_args['end_time'] = current_datetime.time()
            else:
                call_args['start_time'] = current_datetime.time()

            working_intervals = self._get_day_work_intervals(
                current_datetime.date(), **call_args)

            if working_intervals:
                new_working_intervals = self._interval_schedule_hours(
                    working_intervals, remaining_hours, backwards=backwards)

                res = timedelta()
                for interval in working_intervals:
                    res += interval[1] - interval[0]
                remaining_hours -= res.total_seconds() / 3600.0

                intervals = intervals + new_working_intervals if not backwards else new_working_intervals + intervals
            # get next day
            if backwards:
                current_datetime = datetime.datetime.combine(
                    self._get_previous_work_day(current_datetime),
                    datetime.time(23, 59, 59))
            else:
                current_datetime = datetime.datetime.combine(
                    self._get_next_work_day(current_datetime), datetime.time())
            # avoid infinite loops
            iterations += 1

        return intervals

    @api.multi
    def plan_hours(self,
                   hours,
                   day_dt,
                   compute_leaves=False,
                   resource_id=None):
        """ Return datetime after having planned hours """
        res = self._schedule_hours(hours, day_dt, compute_leaves, resource_id)
        if res and hours < 0.0:
            return res[0][0]
        elif res:
            return res[-1][1]
        return False

    @api.multi
    def _schedule_days(self,
                       days,
                       day_dt,
                       compute_leaves=False,
                       resource_id=None):
        """Schedule days of work, using a calendar and an optional resource to
        compute working and leave days. This method can be used backwards, i.e.
        scheduling days before a deadline. For compute_leaves, resource_id:
        see _get_day_work_intervals. This method does not use rrule because
        rrule does not allow backwards computation.

        :param int days: number of days to schedule. Use a negative number to
                         compute a backwards scheduling.
        :param date day_dt: reference datetime to compute working days. If days is > 0
                            date is the starting date. If days is < 0 date is the
                            ending date.

        :return list intervals: list of time intervals in naive UTC """
        backwards = (days < 0)
        intervals = []
        planned_days, iterations = 0, 0

        day_dt_tz = to_naive_user_tz(day_dt, self.env.user)
        current_datetime = day_dt_tz.replace(hour=0,
                                             minute=0,
                                             second=0,
                                             microsecond=0)

        while planned_days < abs(days) and iterations < 100:
            working_intervals = self._get_day_work_intervals(
                current_datetime.date(),
                compute_leaves=compute_leaves,
                resource_id=resource_id)
            if not self or working_intervals:  # no calendar -> no working hours, but day is considered as worked
                planned_days += 1
                intervals += working_intervals
            # get next day
            if backwards:
                current_datetime = self._get_previous_work_day(
                    current_datetime)
            else:
                current_datetime = self._get_next_work_day(current_datetime)
            # avoid infinite loops
            iterations += 1

        return intervals

    @api.multi
    def plan_days(self, days, day_dt, compute_leaves=False, resource_id=None):
        """ Returns the datetime of a days scheduling. """
        res = self._schedule_days(days, day_dt, compute_leaves, resource_id)
        return res and res[-1][1] or False
示例#8
0
class tabungan(models.Model):
    _name = 'siswa_tab_ocb11.tabungan'

    name = fields.Char(string='Kode Pembayaran', requred=True, default='New')
    state = fields.Selection([('draft', 'Draft'), ('post', 'Posted')], string='State', required=True, default='draft')
    siswa_id = fields.Many2one('res.partner', string="Siswa", required=True)
    induk = fields.Char(string='Induk', related='siswa_id.induk')
    saldo_tabungan = fields.Float('Saldo Tabungan', compute="_compute_get_saldo", store=True)
    active_rombel_id = fields.Many2one('siswa_ocb11.rombel', related='siswa_id.active_rombel_id', string='Rombongan Belajar')
    tanggal = fields.Date(string='Tanggal', required=True, default=datetime.today().date())
    jumlah = fields.Float(string='Jumlah', required=True, default=0)
    jumlah_temp = fields.Float(string='Jumlah', required=True, default=0)
    jenis = fields.Selection([('setor', 'Setoran'), ('tarik', 'Tarik Tunai')], string='Jenis', required=True, default='setor')
    confirm_ids = fields.One2many('siswa_tab_ocb11.action_confirm', inverse_name="tabungan_id")
    desc = fields.Char('Keterangan')
    tahunajaran_id = fields.Many2one('siswa_ocb11.tahunajaran', string='Tahun Ajaran', required=True, default=lambda x: x.env['siswa_ocb11.tahunajaran'].search([('active', '=', True)]))

    @api.depends('siswa_id')
    def _compute_get_saldo(self):
        for rec in self:
            rec.saldo_tabungan = rec.siswa_id.saldo_tabungan

    @api.model
    def create(self, vals):
        # # cek saldo siswa
        siswa = self.env['res.partner'].search([('id', '=', vals['siswa_id'])])

        can_draw = True
        if siswa.saldo_tabungan == 0:
            vals['jenis'] = 'setor'
        else:
            if vals['jenis'] == 'tarik':
                if siswa.saldo_tabungan < vals['jumlah_temp']:
                    can_draw = False

        if can_draw:
            if vals.get('name', _('New')) == _('New'):
                vals['name'] = 'DRAFT/TAB/' + str(datetime.today().date().strftime('%d%m%y/%H%M%S'))

            if vals['jenis'] == 'tarik' :
                vals['jumlah_temp'] = -vals['jumlah_temp']
            result = super(tabungan, self).create(vals)
            return result

        else:
            print('Gagal Simpan Tabungan')
            # return {'warning': {
            #             'title': _('Warning'),
            #             'message': _('Saldo tabungan tidak mencukupi.')
            #             }}
            raise exceptions.except_orm(_('Warning'), _('Saldo tabungan tidak mencukupi.'))
    
    @api.multi
    def write(self, values):
        self.ensure_one()
        if 'jumlah_temp' in values:
            if 'jenis' in values:
                if values['jenis'] == 'tarik':
                    values['jumlah_temp'] = -values['jumlah_temp']
            else:
                if self.jenis == 'tarik':
                    values['jumlah_temp'] = -values['jumlah_temp']
        else:
            if self.jenis == 'tarik':
                values['jumlah_temp'] = -self.jumlah_temp

        result = super(tabungan, self).write(values)
        self.update_saldo_siswa()
        return result
    
    def update_saldo_siswa(self):
        self.ensure_one()
        # update saldo siswa
        tabs = self.env['siswa_tab_ocb11.tabungan'].search([('siswa_id', '=', self.siswa_id.id), ('state', '=', 'post')])
        self.env['res.partner'].search([('id', '=', self.siswa_id.id)]).write({
            'saldo_tabungan' : sum(x.jumlah for x in tabs)
        })

    def action_confirm(self):
        self.ensure_one()
        # generate code
        name_seq = self.env['ir.sequence'].next_by_code('tabungan.siswa.tab.ocb11') or _('New')        
        # update name to database
        self.write({
            'name' : name_seq,
            'jumlah' : self.jumlah_temp,
            'state' : 'post'
        })
        self.confirm_ids = (0, 0, {
            'name' : name_seq
        })
        self.update_saldo_siswa()

        # update compute tabungan
        self.update_saldo_tabungan_dashboard()
    
    def update_saldo_tabungan_dashboard(self):
        # dash_tab_id = self.env['ir.model.data'].search([('name','=','default_dashboard_tabungan')]).res_id
        # dash_tab = self.env['siswa_tab_ocb11.dashboard_tabungan'].search([('id','=',dash_tab_id)])
        
        dash_tab = self.env['siswa_tab_ocb11.dashboard_tabungan'].search([('id', 'ilike', '%')])
        for dash in dash_tab:
            dash.compute_saldo_tabungan()  
    
    def action_cancel(self):
        self.ensure_one()
        # delete confirm_ids
        self.confirm_ids.unlink()
        # update name to database
        self.write({
            'name' : 'DRAFT/' + self.name,
            'state' : 'draft',
            'jumlah' : 0
        })
        # update saldo siswa
        self.update_saldo_siswa()

        # update compute tabungan
        self.update_saldo_tabungan_dashboard()
示例#9
0
class PurchaseOrderLine(models.Model):
    _inherit = 'purchase.order.line'

    qty_received_method = fields.Selection(selection_add=[('stock_moves', 'Stock Moves')])

    move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, copy=False)
    orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint')
    move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves')
    product_description_variants = fields.Char('Custom Description')
    propagate_cancel = fields.Boolean('Propagate cancellation', default=True)

    def _compute_qty_received_method(self):
        super(PurchaseOrderLine, self)._compute_qty_received_method()
        for line in self.filtered(lambda l: not l.display_type):
            if line.product_id.type in ['consu', 'product']:
                line.qty_received_method = 'stock_moves'

    @api.depends('move_ids.state', 'move_ids.product_uom_qty', 'move_ids.product_uom')
    def _compute_qty_received(self):
        super(PurchaseOrderLine, self)._compute_qty_received()
        for line in self:
            if line.qty_received_method == 'stock_moves':
                total = 0.0
                # In case of a BOM in kit, the products delivered do not correspond to the products in
                # the PO. Therefore, we can skip them since they will be handled later on.
                for move in line.move_ids.filtered(lambda m: m.product_id == line.product_id):
                    if move.state == 'done':
                        if move.location_dest_id.usage == "supplier":
                            if move.to_refund:
                                total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
                        elif move.origin_returned_move_id and move.origin_returned_move_id._is_dropshipped() and not move._is_dropshipped_returned():
                            # Edge case: the dropship is returned to the stock, no to the supplier.
                            # In this case, the received quantity on the PO is set although we didn't
                            # receive the product physically in our stock. To avoid counting the
                            # quantity twice, we do nothing.
                            pass
                        elif (
                            move.location_dest_id.usage == "internal"
                            and move.to_refund
                            and move.location_dest_id
                            not in self.env["stock.location"].search(
                                [("id", "child_of", move.warehouse_id.view_location_id.id)]
                            )
                        ):
                            total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
                        else:
                            total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
                line._track_qty_received(total)
                line.qty_received = total

    @api.model_create_multi
    def create(self, vals_list):
        lines = super(PurchaseOrderLine, self).create(vals_list)
        lines.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
        return lines

    def write(self, values):
        for line in self.filtered(lambda l: not l.display_type):
            # PO date_planned overrides any PO line date_planned values
            if values.get('date_planned'):
                new_date = fields.Datetime.to_datetime(values['date_planned'])
                self._update_move_date_deadline(new_date)
        result = super(PurchaseOrderLine, self).write(values)
        if 'product_qty' in values:
            self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
        return result

    # --------------------------------------------------
    # Business methods
    # --------------------------------------------------

    def _update_move_date_deadline(self, new_date):
        """ Updates corresponding move picking line deadline dates that are not yet completed. """
        moves_to_update = self.move_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
        if not moves_to_update:
            moves_to_update = self.move_dest_ids.filtered(lambda m: m.state not in ('done', 'cancel'))
        for move in moves_to_update:
            move.date_deadline = new_date + relativedelta(days=move.company_id.po_lead)

    def _create_or_update_picking(self):
        for line in self:
            if line.product_id and line.product_id.type in ('product', 'consu'):
                # Prevent decreasing below received quantity
                if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0:
                    raise UserError(_('You cannot decrease the ordered quantity below the received quantity.\n'
                                      'Create a return first.'))

                if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1:
                    # If the quantity is now below the invoiced quantity, create an activity on the vendor bill
                    # inviting the user to create a refund.
                    line.invoice_lines[0].move_id.activity_schedule(
                        'mail.mail_activity_data_warning',
                        note=_('The quantities on your purchase order indicate less than billed. You should ask for a refund.'))

                # If the user increased quantity of existing line or created a new line
                pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit', 'customer'))
                picking = pickings and pickings[0] or False
                if not picking:
                    res = line.order_id._prepare_picking()
                    picking = self.env['stock.picking'].create(res)

                moves = line._create_stock_moves(picking)
                moves._action_confirm()._action_assign()

    def _get_stock_move_price_unit(self):
        self.ensure_one()
        line = self[0]
        order = line.order_id
        price_unit = line.price_unit
        if line.taxes_id:
            price_unit = line.taxes_id.with_context(round=False).compute_all(
                price_unit, currency=line.order_id.currency_id, quantity=1.0, product=line.product_id, partner=line.order_id.partner_id
            )['total_void']
        if line.product_uom.id != line.product_id.uom_id.id:
            price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
        if order.currency_id != order.company_id.currency_id:
            price_unit = order.currency_id._convert(
                price_unit, order.company_id.currency_id, self.company_id, self.date_order or fields.Date.today(), round=False)
        return price_unit

    def _prepare_stock_moves(self, picking):
        """ Prepare the stock moves data for one order line. This function returns a list of
        dictionary ready to be used in stock.move's create()
        """
        self.ensure_one()
        res = []
        if self.product_id.type not in ['product', 'consu']:
            return res

        qty = 0.0
        price_unit = self._get_stock_move_price_unit()
        outgoing_moves, incoming_moves = self._get_outgoing_incoming_moves()
        for move in outgoing_moves:
            qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
        for move in incoming_moves:
            qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')

        move_dests = self.move_dest_ids
        if not move_dests:
            move_dests = self.move_ids.move_dest_ids.filtered(lambda m: m.state != 'cancel' and not m.location_dest_id.usage == 'supplier')

        if not move_dests:
            qty_to_attach = 0
            qty_to_push = self.product_qty - qty
        else:
            move_dests_initial_demand = self.product_id.uom_id._compute_quantity(
                sum(move_dests.filtered(lambda m: m.state != 'cancel' and not m.location_dest_id.usage == 'supplier').mapped('product_qty')),
                self.product_uom, rounding_method='HALF-UP')
            qty_to_attach = move_dests_initial_demand - qty
            qty_to_push = self.product_qty - move_dests_initial_demand

        if float_compare(qty_to_attach, 0.0, precision_rounding=self.product_uom.rounding) > 0:
            product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities(qty_to_attach, self.product_id.uom_id)
            res.append(self._prepare_stock_move_vals(picking, price_unit, product_uom_qty, product_uom))
        if float_compare(qty_to_push, 0.0, precision_rounding=self.product_uom.rounding) > 0:
            product_uom_qty, product_uom = self.product_uom._adjust_uom_quantities(qty_to_push, self.product_id.uom_id)
            extra_move_vals = self._prepare_stock_move_vals(picking, price_unit, product_uom_qty, product_uom)
            extra_move_vals['move_dest_ids'] = False  # don't attach
            res.append(extra_move_vals)
        return res

    def _prepare_stock_move_vals(self, picking, price_unit, product_uom_qty, product_uom):
        self.ensure_one()
        product = self.product_id.with_context(lang=self.order_id.dest_address_id.lang or self.env.user.lang)
        description_picking = product._get_description(self.order_id.picking_type_id)
        if self.product_description_variants:
            description_picking += "\n" + self.product_description_variants
        date_planned = self.date_planned or self.order_id.date_planned
        return {
            # truncate to 2000 to avoid triggering index limit error
            # TODO: remove index in master?
            'name': (self.name or '')[:2000],
            'product_id': self.product_id.id,
            'date': date_planned,
            'date_deadline': date_planned + relativedelta(days=self.order_id.company_id.po_lead),
            'location_id': self.order_id.partner_id.property_stock_supplier.id,
            'location_dest_id': (self.orderpoint_id and not (self.move_ids | self.move_dest_ids)) and self.orderpoint_id.location_id.id or self.order_id._get_destination_location(),
            'picking_id': picking.id,
            'partner_id': self.order_id.dest_address_id.id,
            'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids],
            'state': 'draft',
            'purchase_line_id': self.id,
            'company_id': self.order_id.company_id.id,
            'price_unit': price_unit,
            'picking_type_id': self.order_id.picking_type_id.id,
            'group_id': self.order_id.group_id.id,
            'origin': self.order_id.name,
            'description_picking': description_picking,
            'propagate_cancel': self.propagate_cancel,
            'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids])] or [],
            'warehouse_id': self.order_id.picking_type_id.warehouse_id.id,
            'product_uom_qty': product_uom_qty,
            'product_uom': product_uom.id,
        }

    @api.model
    def _prepare_purchase_order_line_from_procurement(self, product_id, product_qty, product_uom, company_id, values, po):
        line_description = ''
        if values.get('product_description_variants'):
            line_description = values['product_description_variants']
        supplier = values.get('supplier')
        res = self._prepare_purchase_order_line(product_id, product_qty, product_uom, company_id, supplier, po)
        # We need to keep the vendor name set in _prepare_purchase_order_line. To avoid redundancy
        # in the line name, we add the line_description only if different from the product name.
        # This way, we shoud not lose any valuable information.
        if line_description and product_id.name != line_description:
            res['name'] += '\n' + line_description
        res['move_dest_ids'] = [(4, x.id) for x in values.get('move_dest_ids', [])]
        res['orderpoint_id'] = values.get('orderpoint_id', False) and values.get('orderpoint_id').id
        res['propagate_cancel'] = values.get('propagate_cancel')
        res['product_description_variants'] = values.get('product_description_variants')
        return res

    def _create_stock_moves(self, picking):
        values = []
        for line in self.filtered(lambda l: not l.display_type):
            for val in line._prepare_stock_moves(picking):
                values.append(val)
            line.move_dest_ids.created_purchase_line_id = False

        return self.env['stock.move'].create(values)

    def _find_candidate(self, product_id, product_qty, product_uom, location_id, name, origin, company_id, values):
        """ Return the record in self where the procument with values passed as
        args can be merged. If it returns an empty record then a new line will
        be created.
        """
        description_picking = ''
        if values.get('product_description_variants'):
            description_picking = values['product_description_variants']
        lines = self.filtered(
            lambda l: l.propagate_cancel == values['propagate_cancel']
            and ((values['orderpoint_id'] and not values['move_dest_ids']) and l.orderpoint_id == values['orderpoint_id'] or True)
        )

        # In case 'product_description_variants' is in the values, we also filter on the PO line
        # name. This way, we can merge lines with the same description. To do so, we need the
        # product name in the context of the PO partner.
        if lines and values.get('product_description_variants'):
            partner = self.mapped('order_id.partner_id')[:1]
            product_lang = product_id.with_context(
                lang=partner.lang,
                partner_id=partner.id,
            )
            name = product_lang.display_name
            if product_lang.description_purchase:
                name += '\n' + product_lang.description_purchase
            lines = lines.filtered(lambda l: l.name == name + '\n' + description_picking)
            if lines:
                return lines[0]

        return lines and lines[0] or self.env['purchase.order.line']

    def _get_outgoing_incoming_moves(self):
        outgoing_moves = self.env['stock.move']
        incoming_moves = self.env['stock.move']

        for move in self.move_ids.filtered(lambda r: r.state != 'cancel' and not r.scrapped and self.product_id == r.product_id):
            if move.location_dest_id.usage == "supplier" and move.to_refund:
                outgoing_moves |= move
            elif move.location_dest_id.usage != "supplier":
                if not move.origin_returned_move_id or (move.origin_returned_move_id and move.to_refund):
                    incoming_moves |= move

        return outgoing_moves, incoming_moves

    def _update_date_planned(self, updated_date):
        move_to_update = self.move_ids.filtered(lambda m: m.state not in ['done', 'cancel'])
        if not self.move_ids or move_to_update:  # Only change the date if there is no move done or none
            super()._update_date_planned(updated_date)
        if move_to_update:
            self._update_move_date_deadline(updated_date)

    @api.model
    def _update_qty_received_method(self):
        """Update qty_received_method for old PO before install this module."""
        self.search([])._compute_qty_received_method()
示例#10
0
class StockMove(models.Model):
    _inherit = "stock.move"

    requistion_line_ids = fields.One2many('purchase.requisition.line',
                                          'move_dest_id')
示例#11
0
class AccountVoucher(models.Model):
    _name = 'account.voucher'
    _description = 'Accounting Voucher'
    _inherit = ['mail.thread']
    _order = "date desc, id desc"

    @api.model
    def _default_journal(self):
        voucher_type = self._context.get('voucher_type', 'sale')
        company_id = self._context.get('company_id',
                                       self.env.user.company_id.id)
        domain = [
            ('type', '=', voucher_type),
            ('company_id', '=', company_id),
        ]
        return self.env['account.journal'].search(domain, limit=1)

    voucher_type = fields.Selection([('sale', 'Sale'),
                                     ('purchase', 'Purchase')],
                                    string='Type',
                                    readonly=True,
                                    states={'draft': [('readonly', False)]},
                                    oldname="type")
    name = fields.Char('Payment Reference',
                       readonly=True,
                       states={'draft': [('readonly', False)]},
                       default='')
    date = fields.Date("Bill Date",
                       readonly=True,
                       index=True,
                       states={'draft': [('readonly', False)]},
                       copy=False,
                       default=fields.Date.context_today)
    account_date = fields.Date("Accounting Date",
                               readonly=True,
                               index=True,
                               states={'draft': [('readonly', False)]},
                               help="Effective date for accounting entries",
                               copy=False,
                               default=fields.Date.context_today)
    journal_id = fields.Many2one('account.journal',
                                 'Journal',
                                 required=True,
                                 readonly=True,
                                 states={'draft': [('readonly', False)]},
                                 default=_default_journal)
    payment_journal_id = fields.Many2one(
        'account.journal',
        string='Payment Method',
        readonly=True,
        store=False,
        states={'draft': [('readonly', False)]},
        domain="[('type', 'in', ['cash', 'bank'])]",
        compute='_compute_payment_journal_id',
        inverse='_inverse_payment_journal_id')
    account_id = fields.Many2one(
        'account.account',
        'Account',
        required=True,
        readonly=True,
        states={'draft': [('readonly', False)]},
        domain=
        "[('deprecated', '=', False), ('internal_type','=', (pay_now == 'pay_now' and 'liquidity' or voucher_type == 'purchase' and 'payable' or 'receivable'))]"
    )
    line_ids = fields.One2many('account.voucher.line',
                               'voucher_id',
                               'Voucher Lines',
                               readonly=True,
                               copy=True,
                               states={'draft': [('readonly', False)]})
    narration = fields.Text('Notes',
                            readonly=True,
                            states={'draft': [('readonly', False)]})
    currency_id = fields.Many2one('res.currency',
                                  compute='_get_journal_currency',
                                  string='Currency',
                                  readonly=True,
                                  required=True,
                                  default=lambda self: self._get_currency())
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 store=True,
                                 required=True,
                                 readonly=True,
                                 states={'draft': [('readonly', False)]},
                                 related='journal_id.company_id',
                                 default=lambda self: self._get_company())
    state = fields.Selection(
        [('draft', 'Draft'), ('cancel', 'Cancelled'),
         ('proforma', 'Pro-forma'), ('posted', 'Posted')],
        'Status',
        readonly=True,
        track_visibility='onchange',
        copy=False,
        default='draft',
        help=
        " * The 'Draft' status is used when a user is encoding a new and unconfirmed Voucher.\n"
        " * The 'Pro-forma' status is used when the voucher does not have a voucher number.\n"
        " * The 'Posted' status is used when user create voucher,a voucher number is generated and voucher entries are created in account.\n"
        " * The 'Cancelled' status is used when user cancel voucher.")
    reference = fields.Char('Bill Reference',
                            readonly=True,
                            states={'draft': [('readonly', False)]},
                            help="The partner reference of this document.",
                            copy=False)
    amount = fields.Monetary(string='Total',
                             store=True,
                             readonly=True,
                             compute='_compute_total')
    tax_amount = fields.Monetary(readonly=True,
                                 store=True,
                                 compute='_compute_total')
    tax_correction = fields.Monetary(
        readonly=True,
        states={'draft': [('readonly', False)]},
        help=
        'In case we have a rounding problem in the tax, use this field to correct it'
    )
    number = fields.Char(readonly=True, copy=False)
    move_id = fields.Many2one('account.move', 'Journal Entry', copy=False)
    partner_id = fields.Many2one('res.partner',
                                 'Partner',
                                 change_default=1,
                                 readonly=True,
                                 states={'draft': [('readonly', False)]})
    paid = fields.Boolean(compute='_check_paid',
                          help="The Voucher has been totally paid.")
    pay_now = fields.Selection([
        ('pay_now', 'Pay Directly'),
        ('pay_later', 'Pay Later'),
    ],
                               'Payment',
                               index=True,
                               readonly=True,
                               states={'draft': [('readonly', False)]},
                               default='pay_later')
    date_due = fields.Date('Due Date',
                           readonly=True,
                           index=True,
                           states={'draft': [('readonly', False)]})
    branch_id = fields.Many2one(
        'res.branch',
        'Branch',
        ondelete="restrict",
        default=lambda self: self.env['res.users']._get_default_branch())

    @api.constrains('company_id', 'branch_id')
    def _check_company_branch(self):
        for record in self:
            if record.branch_id and record.company_id != record.branch_id.company_id:
                raise ValidationError(
                    _('Configuration Error of Company:\n'
                      'The Company (%s) in the voucher and '
                      'the Company (%s) of Branch must '
                      'be the same company!') %
                    (record.company_id.name, record.branch_id.company_id.name))

    @api.one
    @api.depends('move_id.line_ids.reconciled',
                 'move_id.line_ids.account_id.internal_type')
    def _check_paid(self):
        self.paid = any([((line.account_id.internal_type, 'in',
                           ('receivable', 'payable')) and line.reconciled)
                         for line in self.move_id.line_ids])

    @api.model
    def _get_currency(self):
        journal = self.env['account.journal'].browse(
            self.env.context.get('default_journal_id', False))
        if journal.currency_id:
            return journal.currency_id.id
        return self.env.user.company_id.currency_id.id

    @api.model
    def _get_company(self):
        return self._context.get('company_id', self.env.user.company_id.id)

    @api.multi
    @api.depends('name', 'number')
    def name_get(self):
        return [(r.id, (r.number or _('Voucher'))) for r in self]

    @api.one
    @api.depends('journal_id', 'company_id')
    def _get_journal_currency(self):
        self.currency_id = self.journal_id.currency_id.id or self.company_id.currency_id.id

    @api.depends('company_id', 'pay_now', 'account_id')
    def _compute_payment_journal_id(self):
        for voucher in self:
            if voucher.pay_now != 'pay_now':
                continue
            domain = [
                ('type', 'in', ('bank', 'cash')),
                ('company_id', '=', voucher.company_id.id),
            ]
            if voucher.account_id and voucher.account_id.internal_type == 'liquidity':
                field = 'default_debit_account_id' if voucher.voucher_type == 'sale' else 'default_credit_account_id'
                domain.append((field, '=', voucher.account_id.id))
            voucher.payment_journal_id = self.env['account.journal'].search(
                domain, limit=1)

    def _inverse_payment_journal_id(self):
        for voucher in self:
            if voucher.pay_now != 'pay_now':
                continue
            if voucher.voucher_type == 'sale':
                voucher.account_id = voucher.payment_journal_id.default_debit_account_id
            else:
                voucher.account_id = voucher.payment_journal_id.default_credit_account_id

    @api.multi
    @api.depends('tax_correction', 'line_ids.price_subtotal')
    def _compute_total(self):
        for voucher in self:
            total = 0
            tax_amount = 0
            for line in voucher.line_ids:
                tax_info = line.tax_ids.compute_all(line.price_unit,
                                                    voucher.currency_id,
                                                    line.quantity,
                                                    line.product_id,
                                                    voucher.partner_id)
                total += tax_info.get('total_included', 0.0)
                tax_amount += sum([
                    t.get('amount', 0.0) for t in tax_info.get('taxes', False)
                ])
            voucher.amount = total + voucher.tax_correction
            voucher.tax_amount = tax_amount

    @api.one
    @api.depends('account_pay_now_id', 'account_pay_later_id', 'pay_now')
    def _get_account(self):
        self.account_id = self.account_pay_now_id if self.pay_now == 'pay_now' else self.account_pay_later_id

    @api.onchange('date')
    def onchange_date(self):
        self.account_date = self.date

    @api.onchange('partner_id', 'pay_now')
    def onchange_partner_id(self):
        pay_journal_domain = [('type', 'in', ['cash', 'bank'])]
        if self.pay_now != 'pay_now':
            if self.partner_id:
                self.account_id = self.partner_id.property_account_receivable_id \
                    if self.voucher_type == 'sale' else self.partner_id.property_account_payable_id
            else:
                account_type = self.voucher_type == 'purchase' and 'payable' or 'receivable'
                domain = [('deprecated', '=', False),
                          ('internal_type', '=', account_type)]

                self.account_id = self.env['account.account'].search(domain,
                                                                     limit=1)
        else:
            if self.voucher_type == 'purchase':
                pay_journal_domain.append(
                    ('outbound_payment_method_ids', '!=', False))
            else:
                pay_journal_domain.append(
                    ('inbound_payment_method_ids', '!=', False))
        return {'domain': {'payment_journal_id': pay_journal_domain}}

    @api.multi
    def proforma_voucher(self):
        self.action_move_line_create()

    @api.multi
    def action_cancel_draft(self):
        self.write({'state': 'draft'})

    @api.multi
    def cancel_voucher(self):
        for voucher in self:
            voucher.move_id.button_cancel()
            voucher.move_id.unlink()
        self.write({'state': 'cancel', 'move_id': False})

    @api.multi
    def unlink(self):
        for voucher in self:
            if voucher.state not in ('draft', 'cancel'):
                raise UserError(
                    _('Cannot delete voucher(s) which are already opened or paid.'
                      ))
        return super(AccountVoucher, self).unlink()

    @api.multi
    def first_move_line_get(self, move_id, company_currency, current_currency):
        debit = credit = 0.0
        if self.voucher_type == 'purchase':
            credit = self._convert_amount(self.amount)
        elif self.voucher_type == 'sale':
            debit = self._convert_amount(self.amount)
        if debit < 0.0: debit = 0.0
        if credit < 0.0: credit = 0.0
        sign = debit - credit < 0 and -1 or 1
        #set the first line of the voucher
        move_line = {
            'name':
            self.name or '/',
            'debit':
            debit,
            'credit':
            credit,
            'account_id':
            self.account_id.id,
            'move_id':
            move_id,
            'journal_id':
            self.journal_id.id,
            'partner_id':
            self.partner_id.commercial_partner_id.id,
            'currency_id':
            company_currency != current_currency and current_currency or False,
            'amount_currency': (
                sign * abs(self.amount)  # amount < 0 for refunds
                if company_currency != current_currency else 0.0),
            'date':
            self.account_date,
            'date_maturity':
            self.date_due,
            'payment_id':
            self._context.get('payment_id'),
            'branch_id':
            self.branch_id.id,
        }
        return move_line

    @api.multi
    def account_move_get(self):
        if self.number:
            name = self.number
        elif self.journal_id.sequence_id:
            if not self.journal_id.sequence_id.active:
                raise UserError(
                    _('Please activate the sequence of selected journal !'))
            name = self.journal_id.sequence_id.with_context(
                ir_sequence_date=self.date).next_by_id()
        else:
            raise UserError(_('Please define a sequence on the journal.'))
        move = {
            'name': name,
            'journal_id': self.journal_id.id,
            'narration': self.narration,
            'date': self.account_date,
            'ref': self.reference,
            'branch_id': self.branch_id.id
        }
        return move

    @api.multi
    def _convert_amount(self, amount):
        '''
        This function convert the amount given in company currency. It takes either the rate in the voucher (if the
        payment_rate_currency_id is relevant) either the rate encoded in the system.
        :param amount: float. The amount to convert
        :param voucher: id of the voucher on which we want the conversion
        :param context: to context to use for the conversion. It may contain the key 'date' set to the voucher date
            field in order to select the good rate to use.
        :return: the amount in the currency of the voucher's company
        :rtype: float
        '''
        for voucher in self:
            return voucher.currency_id.compute(amount,
                                               voucher.company_id.currency_id)

    @api.multi
    def voucher_pay_now_payment_create(self):
        if self.voucher_type == 'sale':
            payment_methods = self.journal_id.inbound_payment_method_ids
            payment_type = 'inbound'
            partner_type = 'customer'
            sequence_code = 'account.payment.customer.invoice'
        else:
            payment_methods = self.journal_id.outbound_payment_method_ids
            payment_type = 'outbound'
            partner_type = 'supplier'
            sequence_code = 'account.payment.supplier.invoice'
        name = self.env['ir.sequence'].with_context(
            ir_sequence_date=self.date).next_by_code(sequence_code)
        return {
            'name': name,
            'payment_type': payment_type,
            'payment_method_id': payment_methods and payment_methods[0].id
            or False,
            'partner_type': partner_type,
            'partner_id': self.partner_id.commercial_partner_id.id,
            'amount': self.amount,
            'currency_id': self.currency_id.id,
            'payment_date': self.date,
            'journal_id': self.payment_journal_id.id,
            'company_id': self.company_id.id,
            'communication': self.name,
            'state': 'reconciled',
        }

    @api.multi
    def voucher_move_line_create(self, line_total, move_id, company_currency,
                                 current_currency):
        '''
        Create one account move line, on the given account move, per voucher line where amount is not 0.0.
        It returns Tuple with tot_line what is total of difference between debit and credit and
        a list of lists with ids to be reconciled with this format (total_deb_cred,list_of_lists).

        :param voucher_id: Voucher id what we are working with
        :param line_total: Amount of the first line, which correspond to the amount we should totally split among all voucher lines.
        :param move_id: Account move wher those lines will be joined.
        :param company_currency: id of currency of the company to which the voucher belong
        :param current_currency: id of currency of the voucher
        :return: Tuple build as (remaining amount not allocated on voucher lines, list of account_move_line created in this method)
        :rtype: tuple(float, list of int)
        '''
        for line in self.line_ids:
            #create one move line per voucher line where amount is not 0.0
            if not line.price_subtotal:
                continue
            # convert the amount set on the voucher line into the currency of the voucher's company
            # this calls res_curreny.compute() with the right context,
            # so that it will take either the rate on the voucher if it is relevant or will use the default behaviour
            amount = self._convert_amount(line.price_unit * line.quantity)
            move_line = {
                'journal_id':
                self.journal_id.id,
                'name':
                line.name or '/',
                'account_id':
                line.account_id.id,
                'move_id':
                move_id,
                'partner_id':
                self.partner_id.commercial_partner_id.id,
                'analytic_account_id':
                line.account_analytic_id and line.account_analytic_id.id
                or False,
                'quantity':
                1,
                'credit':
                abs(amount) if self.voucher_type == 'sale' else 0.0,
                'debit':
                abs(amount) if self.voucher_type == 'purchase' else 0.0,
                'date':
                self.account_date,
                'tax_ids': [(4, t.id) for t in line.tax_ids],
                'amount_currency':
                line.price_subtotal
                if current_currency != company_currency else 0.0,
                'currency_id':
                company_currency != current_currency and current_currency
                or False,
                'payment_id':
                self._context.get('payment_id'),
            }
            self.env['account.move.line'].with_context(
                apply_taxes=True).create(move_line)
        return line_total

    @api.multi
    def action_move_line_create(self):
        '''
        Confirm the vouchers given in ids and create the journal entries for each of them
        '''
        for voucher in self:
            local_context = dict(
                self._context, force_company=voucher.journal_id.company_id.id)
            if voucher.move_id:
                continue
            company_currency = voucher.journal_id.company_id.currency_id.id
            current_currency = voucher.currency_id.id or company_currency
            # we select the context to use accordingly if it's a multicurrency case or not
            # But for the operations made by _convert_amount, we always need to give the date in the context
            ctx = local_context.copy()
            ctx['date'] = voucher.account_date
            ctx['check_move_validity'] = False
            # Create a payment to allow the reconciliation when pay_now = 'pay_now'.
            if self.pay_now == 'pay_now' and self.amount > 0:
                ctx['payment_id'] = self.env['account.payment'].create(
                    self.voucher_pay_now_payment_create()).id
            # Create the account move record.
            move = self.env['account.move'].create(voucher.account_move_get())
            # Get the name of the account_move just created
            # Create the first line of the voucher
            move_line = self.env['account.move.line'].with_context(ctx).create(
                voucher.with_context(ctx).first_move_line_get(
                    move.id, company_currency, current_currency))
            line_total = move_line.debit - move_line.credit
            if voucher.voucher_type == 'sale':
                line_total = line_total - voucher._convert_amount(
                    voucher.tax_amount)
            elif voucher.voucher_type == 'purchase':
                line_total = line_total + voucher._convert_amount(
                    voucher.tax_amount)
            # Create one move line per voucher line where amount is not 0.0
            line_total = voucher.with_context(ctx).voucher_move_line_create(
                line_total, move.id, company_currency, current_currency)

            # Add tax correction to move line if any tax correction specified
            if voucher.tax_correction != 0.0:
                tax_move_line = self.env['account.move.line'].search(
                    [('move_id', '=', move.id), ('tax_line_id', '!=', False)],
                    limit=1)
                if len(tax_move_line):
                    tax_move_line.write({
                        'debit':
                        tax_move_line.debit + voucher.tax_correction
                        if tax_move_line.debit > 0 else 0,
                        'credit':
                        tax_move_line.credit + voucher.tax_correction
                        if tax_move_line.credit > 0 else 0
                    })

            # We post the voucher.
            voucher.write({
                'move_id': move.id,
                'state': 'posted',
                'number': move.name
            })
            move.post()
        return True

    @api.multi
    def _track_subtype(self, init_values):
        if 'state' in init_values:
            return 'account_voucher.mt_voucher_state_change'
        return super(AccountVoucher, self)._track_subtype(init_values)
示例#12
0
class PurchaseRequisition(models.Model):
    _name = "purchase.requisition"
    _description = "Purchase Requisition"
    _inherit = ['mail.thread']
    _order = "id desc"

    def _get_picking_in(self):
        pick_in = self.env.ref('stock.picking_type_in',
                               raise_if_not_found=False)
        company = self.env['res.company']._company_default_get(
            'purchase.requisition')
        if not pick_in or pick_in.sudo(
        ).warehouse_id.company_id.id != company.id:
            pick_in = self.env['stock.picking.type'].search(
                [('warehouse_id.company_id', '=', company.id),
                 ('code', '=', 'incoming')],
                limit=1,
            )
        return pick_in

    def _get_type_id(self):
        return self.env['purchase.requisition.type'].search([], limit=1)

    name = fields.Char(string='Agreement Reference',
                       required=True,
                       copy=False,
                       default=lambda self: self.env['ir.sequence'].
                       next_by_code('purchase.order.requisition'))
    origin = fields.Char(string='Source Document')
    order_count = fields.Integer(compute='_compute_orders_number',
                                 string='Number of Orders')
    vendor_id = fields.Many2one('res.partner', string="Vendor")
    type_id = fields.Many2one('purchase.requisition.type',
                              string="Agreement Type",
                              required=True,
                              default=_get_type_id)
    ordering_date = fields.Date(string="Ordering Date")
    date_end = fields.Datetime(string='Agreement Deadline')
    schedule_date = fields.Date(
        string='Delivery Date',
        index=True,
        help=
        "The expected and scheduled delivery date where all the products are received"
    )
    user_id = fields.Many2one('res.users',
                              string='Responsible',
                              default=lambda self: self.env.user)
    description = fields.Text()
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 required=True,
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('purchase.requisition'))
    purchase_ids = fields.One2many('purchase.order',
                                   'requisition_id',
                                   string='Purchase Orders',
                                   states={'done': [('readonly', True)]})
    line_ids = fields.One2many('purchase.requisition.line',
                               'requisition_id',
                               string='Products to Purchase',
                               states={'done': [('readonly', True)]},
                               copy=True)
    warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse')
    state = fields.Selection([('draft', 'Draft'), ('in_progress', 'Confirmed'),
                              ('open', 'Bid Selection'), ('done', 'Done'),
                              ('cancel', 'Cancelled')],
                             'Status',
                             track_visibility='onchange',
                             required=True,
                             copy=False,
                             default='draft')
    account_analytic_id = fields.Many2one('account.analytic.account',
                                          'Analytic Account')
    picking_type_id = fields.Many2one('stock.picking.type',
                                      'Operation Type',
                                      required=True,
                                      default=_get_picking_in)

    @api.multi
    @api.depends('purchase_ids')
    def _compute_orders_number(self):
        for requisition in self:
            requisition.order_count = len(requisition.purchase_ids)

    @api.multi
    def action_cancel(self):
        # try to set all associated quotations to cancel state
        for requisition in self:
            requisition.purchase_ids.button_cancel()
            for po in requisition.purchase_ids:
                po.message_post(body=_(
                    'Cancelled by the agreement associated to this quotation.')
                                )
        self.write({'state': 'cancel'})

    @api.multi
    def action_in_progress(self):
        if not all(obj.line_ids for obj in self):
            raise UserError(
                _('You cannot confirm call because there is no product line.'))
        self.write({'state': 'in_progress'})

    @api.multi
    def action_open(self):
        self.write({'state': 'open'})

    @api.multi
    def action_draft(self):
        self.write({'state': 'draft'})

    @api.multi
    def action_done(self):
        """
        Generate all purchase order based on selected lines, should only be called on one agreement at a time
        """
        if any(purchase_order.state in ['draft', 'sent', 'to approve']
               for purchase_order in self.mapped('purchase_ids')):
            raise UserError(
                _('You have to cancel or validate every RfQ before closing the purchase requisition.'
                  ))
        self.write({'state': 'done'})

    def _prepare_tender_values(self, product_id, product_qty, product_uom,
                               location_id, name, origin, values):
        return {
            'origin':
            origin,
            'date_end':
            values['date_planned'],
            'warehouse_id':
            values.get('warehouse_id') and values['warehouse_id'].id or False,
            'company_id':
            values['company_id'].id,
            'line_ids': [(0, 0, {
                'product_id':
                product_id.id,
                'product_uom_id':
                product_uom.id,
                'product_qty':
                product_qty,
                'move_dest_id':
                values.get('move_dest_ids') and values['move_dest_ids'][0].id
                or False,
            })],
        }
示例#13
0
class DmsDirectory(models.Model):

    _name = "dms.directory"
    _description = "Directory"

    _inherit = [
        "portal.mixin",
        "dms.security.mixin",
        "dms.mixins.thumbnail",
        "mail.thread",
        "mail.activity.mixin",
        "mail.alias.mixin",
    ]

    _rec_name = "complete_name"
    _order = "complete_name"

    _parent_store = True
    _parent_name = "parent_id"

    name = fields.Char(string="Name", required=True, index=True)

    parent_path = fields.Char(index=True)
    is_root_directory = fields.Boolean(
        string="Is Root Directory",
        default=False,
        help="""Indicates if the directory is a root directory.
        A root directory has a settings object, while a directory with a set
        parent inherits the settings form its parent.""",
    )

    root_storage_id = fields.Many2one(
        comodel_name="dms.storage",
        string="Root Storage",
        ondelete="restrict",
        compute="_compute_directory_type",
        store=True,
        readonly=False,
        copy=True,
    )

    storage_id = fields.Many2one(
        compute="_compute_storage",
        comodel_name="dms.storage",
        string="Storage",
        ondelete="restrict",
        auto_join=True,
        store=True,
    )

    parent_id = fields.Many2one(
        comodel_name="dms.directory",
        domain="[('permission_create', '=', True)]",
        string="Parent Directory",
        ondelete="restrict",
        auto_join=True,
        index=True,
        store=True,
        readonly=False,
        compute="_compute_directory_type",
        copy=True,
    )

    complete_name = fields.Char("Complete Name",
                                compute="_compute_complete_name",
                                store=True)
    child_directory_ids = fields.One2many(
        comodel_name="dms.directory",
        inverse_name="parent_id",
        string="Subdirectories",
        auto_join=False,
        copy=False,
    )
    is_hidden = fields.Boolean(string="Storage is Hidden",
                               related="storage_id.is_hidden",
                               readonly=True)
    company_id = fields.Many2one(
        related="storage_id.company_id",
        comodel_name="res.company",
        string="Company",
        readonly=True,
        store=True,
        index=True,
    )

    color = fields.Integer(string="Color", default=0)

    category_id = fields.Many2one(
        comodel_name="dms.category",
        context="{'dms_category_show_path': True}",
        string="Category",
    )

    tag_ids = fields.Many2many(
        comodel_name="dms.tag",
        relation="dms_directory_tag_rel",
        domain="""[
            '|', ['category_id', '=', False],
            ['category_id', 'child_of', category_id]]
        """,
        column1="did",
        column2="tid",
        string="Tags",
        compute="_compute_tags",
        readonly=False,
        store=True,
    )

    user_star_ids = fields.Many2many(
        comodel_name="res.users",
        relation="dms_directory_star_rel",
        column1="did",
        column2="uid",
        string="Stars",
    )

    starred = fields.Boolean(
        compute="_compute_starred",
        inverse="_inverse_starred",
        search="_search_starred",
        string="Starred",
    )

    file_ids = fields.One2many(
        comodel_name="dms.file",
        inverse_name="directory_id",
        string="Files",
        auto_join=False,
        copy=False,
    )

    count_directories = fields.Integer(compute="_compute_count_directories",
                                       string="Count Subdirectories Title")

    count_files = fields.Integer(compute="_compute_count_files",
                                 string="Count Files Title")

    count_directories_title = fields.Char(compute="_compute_count_directories",
                                          string="Count Subdirectories")

    count_files_title = fields.Char(compute="_compute_count_files",
                                    string="Count Files")

    count_elements = fields.Integer(compute="_compute_count_elements",
                                    string="Count Elements")

    count_total_directories = fields.Integer(
        compute="_compute_count_total_directories",
        string="Total Subdirectories")

    count_total_files = fields.Integer(compute="_compute_count_total_files",
                                       string="Total Files")

    count_total_elements = fields.Integer(
        compute="_compute_count_total_elements", string="Total Elements")

    size = fields.Integer(compute="_compute_size", string="Size")

    inherit_group_ids = fields.Boolean(string="Inherit Groups", default=True)

    alias_process = fields.Selection(
        selection=[("files", "Single Files"), ("directory", "Subdirectory")],
        required=True,
        default="directory",
        string="Unpack Emails as",
        help="""\
                Define how incoming emails are processed:\n
                - Single Files: The email gets attached to the directory and
                all attachments are created as files.\n
                - Subdirectory: A new subdirectory is created for each email
                and the mail is attached to this subdirectory. The attachments
                are created as files of the subdirectory.
                """,
    )

    def _get_share_url(self, redirect=False, signup_partner=False, pid=None):
        self.ensure_one()
        return "/my/dms/directory/{}?access_token={}&db={}".format(
            self.id,
            self._portal_ensure_token(),
            self.env.cr.dbname,
        )

    def check_access_token(self, access_token=False):
        res = False
        if access_token:
            items = self.env["dms.directory"].search([("access_token", "=",
                                                       access_token)])
            if items:
                item = items[0]
                if item.id == self.id:
                    return True
                else:
                    directory_item = self
                    while directory_item.parent_id:
                        if directory_item.id == item.id:
                            return True
                        directory_item = directory_item.parent_id
                    # Fix last level
                    if directory_item.id == item.id:
                        return True
        return res

    def _alias_get_creation_values(self):
        values = super(DmsDirectory, self)._alias_get_creation_values()
        values['alias_model_id'] = self.env['ir.model']._get(
            'dms.directory').id

        if self.id:
            values['alias_defaults'] = defaults = ast.literal_eval(
                self.alias_defaults or "{}")
            defaults['parent_id'] = self.id
        return values

    @api.model
    def _get_parent_categories(self, access_token):
        self.ensure_one()
        directories = [self]
        current_directory = self
        if access_token:
            # Only show parent categories to access_token
            stop = False
            while current_directory.parent_id and not stop:
                if current_directory.access_token == access_token:
                    stop = False
                else:
                    directories.append(current_directory.parent_id)
                current_directory = current_directory.parent_id
        else:
            while (current_directory.parent_id and
                   current_directory.parent_id.check_access("read", False)):
                directories.append(current_directory.parent_id)
                current_directory = current_directory.parent_id
        return directories[::-1]

    def _get_own_root_directories(self, user_id):
        ids = []
        items = (self.env["dms.directory"].with_user(user_id).search([
            ("is_hidden", "=", False)
        ]))
        for item in items:
            current_directory = item
            while (current_directory.parent_id and
                   current_directory.parent_id.check_access("read", False)):
                current_directory = current_directory.parent_id

            if current_directory.id not in ids:
                ids.append(current_directory.id)

        return ids

    def check_access(self, operation, raise_exception=False):
        res = super(DmsDirectory, self).check_access(operation,
                                                     raise_exception)
        if self.env.user.has_group("base.group_portal"):
            if self.id in self._get_ids_without_access_groups(operation):
                res = False
        # Fix show breadcrumb with share button (public)
        if self.env.user.has_group("base.group_public"):
            res = True
        return res

    allowed_model_ids = fields.Many2many(compute="_compute_allowed_model_ids",
                                         comodel_name="ir.model",
                                         store=False)
    model_id = fields.Many2one(
        comodel_name="ir.model",
        domain="[('id', 'in', allowed_model_ids)]",
        compute="_compute_model_id",
        inverse="_inverse_model_id",
        string="Model",
        store=True,
    )
    res_model = fields.Char(string="Linked attachments model")
    res_id = fields.Integer(string="Linked attachments record ID")
    record_ref = fields.Reference(string="Record Referenced",
                                  compute="_compute_record_ref",
                                  selection=[])
    storage_id_save_type = fields.Selection(related="storage_id.save_type",
                                            store=False)

    @api.depends("root_storage_id", "storage_id")
    def _compute_allowed_model_ids(self):
        for record in self:
            record.allowed_model_ids = False
            if record.root_storage_id and record.root_storage_id.model_ids:
                record.allowed_model_ids = record.root_storage_id.model_ids.ids
            elif record.storage_id and record.storage_id.model_ids:
                record.allowed_model_ids = record.storage_id.model_ids.ids

    @api.depends("res_model")
    def _compute_model_id(self):
        for record in self:
            if not record.res_model:
                record.model_id = False
                continue
            record.model_id = self.env["ir.model"].search([("model", "=",
                                                            record.res_model)])

    def _inverse_model_id(self):
        for record in self:
            record.res_model = record.model_id.model

    @api.depends("res_model", "res_id")
    def _compute_record_ref(self):
        for record in self:
            record.record_ref = False
            if record.res_model and record.res_id:
                record.record_ref = "{},{}".format(record.res_model,
                                                   record.res_id)

    @api.depends("name", "complete_name")
    def _compute_display_name(self):
        if not self.env.context.get("directory_short_name", False):
            return super()._compute_display_name()
        for record in self:
            record.display_name = record.name

    def toggle_starred(self):
        updates = defaultdict(set)
        for record in self:
            vals = {"starred": not record.starred}
            updates[tools.frozendict(vals)].add(record.id)
        with self.env.norecompute():
            for vals, ids in updates.items():
                self.browse(ids).write(dict(vals))
        self.recompute()

    # ----------------------------------------------------------
    # Actions
    # ----------------------------------------------------------

    def action_save_onboarding_directory_step(self):
        self.env.user.company_id.set_onboarding_step_done(
            "documents_onboarding_directory_state")

    # ----------------------------------------------------------
    # SearchPanel
    # ----------------------------------------------------------

    @api.model
    def _search_panel_directory(self, **kwargs):
        search_domain = (kwargs.get("search_domain", []), )
        if search_domain and len(search_domain):
            for domain in search_domain[0]:
                if domain[0] == "parent_id":
                    return domain[1], domain[2]
        return None, None

    # ----------------------------------------------------------
    # Search
    # ----------------------------------------------------------
    @api.model
    def _search(
        self,
        args,
        offset=0,
        limit=None,
        order=None,
        count=False,
        access_rights_uid=None,
    ):
        result = super(DmsDirectory, self)._search(args, offset, limit, order,
                                                   False, access_rights_uid)
        if result:
            directory_ids = set(result)
            if self.env.user.has_group("base.group_portal"):
                exclude_ids = self._get_ids_without_access_groups("read")
                directory_ids -= set(exclude_ids)
                return directory_ids
        return result

    @api.model
    def _search_starred(self, operator, operand):
        if operator == "=" and operand:
            return [("user_star_ids", "in", [self.env.uid])]
        return [("user_star_ids", "not in", [self.env.uid])]

    @api.depends("name", "parent_id.complete_name")
    def _compute_complete_name(self):
        for category in self:
            if category.parent_id:
                category.complete_name = "{} / {}".format(
                    category.parent_id.complete_name,
                    category.name,
                )
            else:
                category.complete_name = category.name

    @api.depends("root_storage_id", "parent_id")
    def _compute_storage(self):
        for record in self:
            if record.is_root_directory:
                record.storage_id = record.root_storage_id
            else:
                record.storage_id = record.parent_id.storage_id

    @api.depends("user_star_ids")
    def _compute_starred(self):
        for record in self:
            record.starred = self.env.user in record.user_star_ids

    @api.depends("child_directory_ids")
    def _compute_count_directories(self):
        for record in self:
            directories = len(record.child_directory_ids)
            record.count_directories = directories
            record.count_directories_title = _(
                "%s Subdirectories") % directories

    @api.depends("file_ids")
    def _compute_count_files(self):
        for record in self:
            files = len(record.file_ids)
            record.count_files = files
            record.count_files_title = _("%s Files") % files

    @api.depends("child_directory_ids", "file_ids")
    def _compute_count_elements(self):
        for record in self:
            elements = record.count_files
            elements += record.count_directories
            record.count_elements = elements

    def _compute_count_total_directories(self):
        for record in self:
            count = self.search_count([("id", "child_of", record.id)])
            count = count - 1 if count > 0 else 0
            record.count_total_directories = count

    def _compute_count_total_files(self):
        model = self.env["dms.file"]
        for record in self:
            record.count_total_files = model.search_count([
                ("directory_id", "child_of", record.id)
            ])

    def _compute_count_total_elements(self):
        for record in self:
            total_elements = record.count_total_files
            total_elements += record.count_total_directories
            record.count_total_elements = total_elements

    def _compute_size(self):
        sudo_model = self.env["dms.file"].sudo()
        for record in self:
            # Avoid NewId
            if not record.id:
                record.size = 0
                continue
            recs = sudo_model.search_read(
                domain=[("directory_id", "child_of", record.id)],
                fields=["size"],
            )
            record.size = sum(rec.get("size", 0) for rec in recs)

    @api.depends("inherit_group_ids", "parent_path")
    def _compute_groups(self):
        records = self.filtered(lambda record: record.parent_path)
        paths = [
            list(map(int,
                     rec.parent_path.split("/")[:-1])) for rec in records
        ]
        ids = paths and set(functools.reduce(operator.concat, paths)) or []
        read = self.browse(ids).read(["inherit_group_ids", "group_ids"])
        data = {entry.pop("id"): entry for entry in read}
        for record in records:
            complete_group_ids = set()
            for directory_id in reversed(
                    list(map(int,
                             record.parent_path.split("/")[:-1]))):
                if directory_id in data:
                    complete_group_ids |= set(data[directory_id].get(
                        "group_ids", []))
                    if not data[directory_id].get("inherit_group_ids"):
                        break
            record.update(
                {"complete_group_ids": [(6, 0, list(complete_group_ids))]})
        for record in self - records:
            if record.parent_id and record.inherit_group_ids:
                complete_groups = record.parent_id.complete_group_ids
                record.complete_group_ids = record.group_ids | complete_groups
            else:
                record.complete_group_ids = record.group_ids

    # ----------------------------------------------------------
    # View
    # ----------------------------------------------------------

    @api.depends("is_root_directory")
    def _compute_directory_type(self):
        for record in self:
            if record.is_root_directory:
                record.parent_id = None
            else:
                record.root_storage_id = None

    @api.depends("category_id")
    def _compute_tags(self):
        for record in self:
            tags = record.tag_ids.filtered(
                lambda rec: not rec.category_id or rec.category_id == record.
                category_id)
            record.tag_ids = tags

    # ----------------------------------------------------------
    # Constrains
    # ----------------------------------------------------------

    @api.constrains("parent_id")
    def _check_directory_recursion(self):
        if not self._check_recursion():
            raise ValidationError(
                _("Error! You cannot create recursive directories."))
        return True

    @api.constrains("is_root_directory", "root_storage_id", "parent_id")
    def _check_directory_storage(self):
        for record in self:
            if record.is_root_directory and not record.root_storage_id:
                raise ValidationError(
                    _("A root directory has to have a root storage."))
            if not record.is_root_directory and not record.parent_id:
                raise ValidationError(
                    _("A directory has to have a parent directory."))
            if record.parent_id and (record.is_root_directory
                                     or record.root_storage_id):
                raise ValidationError(
                    _("A directory can't be a root and have a parent directory."
                      ))

    @api.constrains("parent_id")
    def _check_directory_access(self):
        for record in self:
            if not record.parent_id.check_access("create",
                                                 raise_exception=False):
                raise ValidationError(
                    _("The parent directory has to have the permission "
                      "to create directories."))

    @api.constrains("name")
    def _check_name(self):
        for record in self:
            if not check_name(record.name):
                raise ValidationError(_("The directory name is invalid."))
            if record.is_root_directory:
                childs = record.sudo(
                ).root_storage_id.root_directory_ids.name_get()
            else:
                childs = record.sudo().parent_id.child_directory_ids.name_get()
            if list(
                    filter(
                        lambda child: child[1] == record.name and child[0] !=
                        record.id,
                        childs,
                    )):
                raise ValidationError(
                    _("A directory with the same name already exists."))

    # ----------------------------------------------------------
    # Create, Update, Delete
    # ----------------------------------------------------------

    def _inverse_starred(self):
        starred_records = self.env["dms.directory"].sudo()
        not_starred_records = self.env["dms.directory"].sudo()
        for record in self:
            if not record.starred and self.env.user in record.user_star_ids:
                starred_records |= record
            elif record.starred and self.env.user not in record.user_star_ids:
                not_starred_records |= record
        not_starred_records.write({"user_star_ids": [(4, self.env.uid)]})
        starred_records.write({"user_star_ids": [(3, self.env.uid)]})

    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or [])
        if "root_storage_id" in default:
            storage = self.env["dms.storage"].browse(
                default["root_storage_id"])
            names = storage.sudo().root_directory_ids.mapped("name")
        elif "parent_id" in default:
            parent_directory = self.browse(default["parent_id"])
            names = parent_directory.sudo().child_directory_ids.mapped("name")
        elif self.is_root_directory:
            names = self.sudo().root_storage_id.root_directory_ids.mapped(
                "name")
        else:
            names = self.sudo().parent_id.child_directory_ids.mapped("name")
        default.update({"name": unique_name(self.name, names)})
        new = super().copy(default)
        for record in self.file_ids:
            record.copy({"directory_id": new.id})
        for record in self.child_directory_ids:
            record.copy({"parent_id": new.id})
        return new

    @api.model
    def get_alias_model_name(self, vals):
        return vals.get("alias_model", "dms.directory")

    def get_alias_values(self):
        values = super().get_alias_values()
        values["alias_defaults"] = {"parent_id": self.id}
        return values

    @api.model
    def message_new(self, msg_dict, custom_values=None):
        custom_values = custom_values if custom_values is not None else {}
        parent_directory_id = custom_values.get("parent_id", None)
        parent_directory = self.sudo().browse(parent_directory_id)
        if not parent_directory_id or not parent_directory.exists():
            raise ValueError("No directory could be found!")
        if parent_directory.alias_process == "files":
            parent_directory._process_message(msg_dict)
            return parent_directory
        names = parent_directory.child_directory_ids.mapped("name")
        subject = slugify(msg_dict.get("subject", _("Alias-Mail-Extraction")))
        defaults = dict(
            {"name": unique_name(subject, names, escape_suffix=True)},
            **custom_values)
        directory = super().message_new(msg_dict, custom_values=defaults)
        directory._process_message(msg_dict)
        return directory

    def message_update(self, msg_dict, update_vals=None):
        self._process_message(msg_dict, extra_values=update_vals)
        return super().message_update(msg_dict, update_vals=update_vals)

    def _process_message(self, msg_dict, extra_values=False):
        names = self.sudo().file_ids.mapped("name")
        for attachment in msg_dict["attachments"]:
            uname = unique_name(attachment.fname, names, escape_suffix=True)
            self.env["dms.file"].sudo().create({
                "content":
                base64.b64encode(attachment.content),
                "directory_id":
                self.id,
                "name":
                uname,
            })
            names.append(uname)

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            if vals.get("root_storage_id", False):
                vals["storage_id"] = vals["root_storage_id"]
            if vals.get("parent_id", False):
                parent = self.browse([vals["parent_id"]])
                data = next(iter(parent.sudo().read(["storage_id"])), {})
                vals["storage_id"] = self._convert_to_write(data).get(
                    "storage_id")
        return super().create(vals_list)

    def write(self, vals):
        # Groups part
        if any(key in vals for key in ["group_ids", "inherit_group_ids"]):
            with self.env.norecompute():
                res = super(DmsDirectory, self).write(vals)
                domain = [("id", "child_of", self.ids)]
                records = self.sudo().search(domain)
                records.modified(["group_ids"])
            records.recompute()
        else:
            res = super().write(vals)

        if self and any(field for field in vals
                        if field in ["root_storage_id", "parent_id"]):
            records = self.sudo().search([("id", "child_of", self.ids)]) - self
            if "root_storage_id" in vals:
                records.write({"storage_id": vals["root_storage_id"]})
            elif "parent_id" in vals:
                parent = self.browse([vals["parent_id"]])
                data = next(iter(parent.sudo().read(["storage_id"])), {})
                records.write({
                    "storage_id":
                    self._convert_to_write(data).get("storage_id")
                })
        return res

    def unlink(self):
        if self and self.check_access("unlink", raise_exception=True):
            domain = [
                "&",
                ("directory_id", "child_of", self.ids),
                "&",
                ("locked_by", "!=", self.env.uid),
                ("locked_by", "!=", False),
            ]
            if self.env["dms.file"].sudo().search(domain):
                raise AccessError(
                    _("A file is locked, the folder cannot be deleted."))
            self.env["dms.file"].sudo().search([("directory_id", "child_of",
                                                 self.ids)]).unlink()
            return super(DmsDirectory,
                         self.sudo().search([("id", "child_of", self.ids)
                                             ])).unlink()
        return super().unlink()
示例#14
0
class Challenge(models.Model):
    """Gamification challenge

    Set of predifined objectives assigned to people with rules for recurrence and
    rewards

    If 'user_ids' is defined and 'period' is different than 'one', the set will
    be assigned to the users for each period (eg: every 1st of each month if
    'monthly' is selected)
    """

    _name = 'gamification.challenge'
    _description = 'Gamification Challenge'
    _inherit = 'mail.thread'
    _order = 'end_date, start_date, name, id'

    name = fields.Char("Challenge Name", required=True, translate=True)
    description = fields.Text("Description", translate=True)
    state = fields.Selection([
            ('draft', "Draft"),
            ('inprogress', "In Progress"),
            ('done', "Done"),
        ], default='draft', copy=False,
        string="State", required=True, tracking=True)
    manager_id = fields.Many2one(
        'res.users', default=lambda self: self.env.uid,
        string="Responsible", help="The user responsible for the challenge.",)

    user_ids = fields.Many2many('res.users', 'gamification_challenge_users_rel', string="Users", help="List of users participating to the challenge")
    user_domain = fields.Char("User domain", help="Alternative to a list of users")

    period = fields.Selection([
            ('once', "Non recurring"),
            ('daily', "Daily"),
            ('weekly', "Weekly"),
            ('monthly', "Monthly"),
            ('yearly', "Yearly")
        ], default='once',
        string="Periodicity",
        help="Period of automatic goal assigment. If none is selected, should be launched manually.",
        required=True)
    start_date = fields.Date("Start Date", help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date.")
    end_date = fields.Date("End Date", help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date.")

    invited_user_ids = fields.Many2many('res.users', 'gamification_invited_user_ids_rel', string="Suggest to users")

    line_ids = fields.One2many('gamification.challenge.line', 'challenge_id',
                                  string="Lines",
                                  help="List of goals that will be set",
                                  required=True, copy=True)

    reward_id = fields.Many2one('gamification.badge', string="For Every Succeeding User")
    reward_first_id = fields.Many2one('gamification.badge', string="For 1st user")
    reward_second_id = fields.Many2one('gamification.badge', string="For 2nd user")
    reward_third_id = fields.Many2one('gamification.badge', string="For 3rd user")
    reward_failure = fields.Boolean("Reward Bests if not Succeeded?")
    reward_realtime = fields.Boolean("Reward as soon as every goal is reached", default=True, help="With this option enabled, a user can receive a badge only once. The top 3 badges are still rewarded only at the end of the challenge.")

    visibility_mode = fields.Selection([
            ('personal', "Individual Goals"),
            ('ranking', "Leader Board (Group Ranking)"),
        ], default='personal',
        string="Display Mode", required=True)

    report_message_frequency = fields.Selection([
            ('never', "Never"),
            ('onchange', "On change"),
            ('daily', "Daily"),
            ('weekly', "Weekly"),
            ('monthly', "Monthly"),
            ('yearly', "Yearly")
        ], default='never',
        string="Report Frequency", required=True)
    report_message_group_id = fields.Many2one('mail.channel', string="Send a copy to", help="Group that will receive a copy of the report in addition to the user")
    report_template_id = fields.Many2one('mail.template', default=lambda self: self._get_report_template(), string="Report Template", required=True)
    remind_update_delay = fields.Integer("Non-updated manual goals will be reminded after", help="Never reminded if no value or zero is specified.")
    last_report_date = fields.Date("Last Report Date", default=fields.Date.today)
    next_report_date = fields.Date("Next Report Date", compute='_get_next_report_date', store=True)

    challenge_category = fields.Selection([
        ('hr', 'Human Resources / Engagement'),
        ('other', 'Settings / Gamification Tools'),
    ], string="Appears in", required=True, default='hr',
       help="Define the visibility of the challenge through menus")

    REPORT_OFFSETS = {
        'daily': timedelta(days=1),
        'weekly': timedelta(days=7),
        'monthly': relativedelta(months=1),
        'yearly': relativedelta(years=1),
    }
    @api.depends('last_report_date', 'report_message_frequency')
    def _get_next_report_date(self):
        """ Return the next report date based on the last report date and
        report period.
        """
        for challenge in self:
            last = challenge.last_report_date
            offset = self.REPORT_OFFSETS.get(challenge.report_message_frequency)

            if offset:
                challenge.next_report_date = last + offset
            else:
                challenge.next_report_date = False

    def _get_report_template(self):
        template = self.env.ref('gamification.simple_report_template', raise_if_not_found=False)

        return template.id if template else False

    @api.model
    def create(self, vals):
        """Overwrite the create method to add the user of groups"""

        if vals.get('user_domain'):
            users = self._get_challenger_users(ustr(vals.get('user_domain')))

            if not vals.get('user_ids'):
                vals['user_ids'] = []
            vals['user_ids'].extend((4, user.id) for user in users)

        return super(Challenge, self).create(vals)

    def write(self, vals):
        if vals.get('user_domain'):
            users = self._get_challenger_users(ustr(vals.get('user_domain')))

            if not vals.get('user_ids'):
                vals['user_ids'] = []
            vals['user_ids'].extend((4, user.id) for user in users)

        write_res = super(Challenge, self).write(vals)

        if vals.get('report_message_frequency', 'never') != 'never':
            # _recompute_challenge_users do not set users for challenges with no reports, subscribing them now
            for challenge in self:
                challenge.message_subscribe([user.partner_id.id for user in challenge.user_ids])

        if vals.get('state') == 'inprogress':
            self._recompute_challenge_users()
            self._generate_goals_from_challenge()

        elif vals.get('state') == 'done':
            self._check_challenge_reward(force=True)

        elif vals.get('state') == 'draft':
            # resetting progress
            if self.env['gamification.goal'].search([('challenge_id', 'in', self.ids), ('state', '=', 'inprogress')], limit=1):
                raise exceptions.UserError(_("You can not reset a challenge with unfinished goals."))

        return write_res


    ##### Update #####

    @api.model # FIXME: check how cron functions are called to see if decorator necessary
    def _cron_update(self, ids=False, commit=True):
        """Daily cron check.

        - Start planned challenges (in draft and with start_date = today)
        - Create the missing goals (eg: modified the challenge to add lines)
        - Update every running challenge
        """
        # in cron mode, will do intermediate commits
        # cannot be replaced by a parameter because it is intended to impact side-effects of
        # write operations
        self = self.with_context(commit_gamification=commit)
        # start scheduled challenges
        planned_challenges = self.search([
            ('state', '=', 'draft'),
            ('start_date', '<=', fields.Date.today())
        ])
        if planned_challenges:
            planned_challenges.write({'state': 'inprogress'})

        # close scheduled challenges
        scheduled_challenges = self.search([
            ('state', '=', 'inprogress'),
            ('end_date', '<', fields.Date.today())
        ])
        if scheduled_challenges:
            scheduled_challenges.write({'state': 'done'})

        records = self.browse(ids) if ids else self.search([('state', '=', 'inprogress')])

        return records._update_all()

    def _update_all(self):
        """Update the challenges and related goals

        :param list(int) ids: the ids of the challenges to update, if False will
        update only challenges in progress."""
        if not self:
            return True

        Goals = self.env['gamification.goal']

        # include yesterday goals to update the goals that just ended
        # exclude goals for users that did not connect since the last update
        yesterday = fields.Date.to_string(date.today() - timedelta(days=1))
        self.env.cr.execute("""SELECT gg.id
                        FROM gamification_goal as gg
                        JOIN res_users_log as log ON gg.user_id = log.create_uid
                       WHERE gg.write_date < log.create_date
                         AND gg.closed IS NOT TRUE
                         AND gg.challenge_id IN %s
                         AND (gg.state = 'inprogress'
                              OR (gg.state = 'reached' AND gg.end_date >= %s))
                      GROUP BY gg.id
        """, [tuple(self.ids), yesterday])

        Goals.browse(goal_id for [goal_id] in self.env.cr.fetchall()).update_goal()

        self._recompute_challenge_users()
        self._generate_goals_from_challenge()

        for challenge in self:
            if challenge.last_report_date != fields.Date.today():
                # goals closed but still opened at the last report date
                closed_goals_to_report = Goals.search([
                    ('challenge_id', '=', challenge.id),
                    ('start_date', '>=', challenge.last_report_date),
                    ('end_date', '<=', challenge.last_report_date)
                ])

                if challenge.next_report_date and fields.Date.today() >= challenge.next_report_date:
                    challenge.report_progress()
                elif closed_goals_to_report:
                    # some goals need a final report
                    challenge.report_progress(subset_goals=closed_goals_to_report)

        self._check_challenge_reward()
        return True

    def _get_challenger_users(self, domain):
        user_domain = ast.literal_eval(domain)
        return self.env['res.users'].search(user_domain)

    def _recompute_challenge_users(self):
        """Recompute the domain to add new users and remove the one no longer matching the domain"""
        for challenge in self.filtered(lambda c: c.user_domain):
            current_users = challenge.user_ids
            new_users = self._get_challenger_users(challenge.user_domain)

            if current_users != new_users:
                challenge.user_ids = new_users

        return True

    def action_start(self):
        """Start a challenge"""
        return self.write({'state': 'inprogress'})

    def action_check(self):
        """Check a challenge

        Create goals that haven't been created yet (eg: if added users)
        Recompute the current value for each goal related"""
        self.env['gamification.goal'].search([
            ('challenge_id', 'in', self.ids),
            ('state', '=', 'inprogress')
        ]).unlink()

        return self._update_all()

    def action_report_progress(self):
        """Manual report of a goal, does not influence automatic report frequency"""
        for challenge in self:
            challenge.report_progress()
        return True

    ##### Automatic actions #####

    def _generate_goals_from_challenge(self):
        """Generate the goals for each line and user.

        If goals already exist for this line and user, the line is skipped. This
        can be called after each change in the list of users or lines.
        :param list(int) ids: the list of challenge concerned"""

        Goals = self.env['gamification.goal']
        for challenge in self:
            (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
            to_update = Goals.browse(())

            for line in challenge.line_ids:
                # there is potentially a lot of users
                # detect the ones with no goal linked to this line
                date_clause = ""
                query_params = [line.id]
                if start_date:
                    date_clause += " AND g.start_date = %s"
                    query_params.append(start_date)
                if end_date:
                    date_clause += " AND g.end_date = %s"
                    query_params.append(end_date)

                query = """SELECT u.id AS user_id
                             FROM res_users u
                        LEFT JOIN gamification_goal g
                               ON (u.id = g.user_id)
                            WHERE line_id = %s
                              {date_clause}
                        """.format(date_clause=date_clause)
                self.env.cr.execute(query, query_params)
                user_with_goal_ids = {it for [it] in self.env.cr._obj}

                participant_user_ids = set(challenge.user_ids.ids)
                user_squating_challenge_ids = user_with_goal_ids - participant_user_ids
                if user_squating_challenge_ids:
                    # users that used to match the challenge
                    Goals.search([
                        ('challenge_id', '=', challenge.id),
                        ('user_id', 'in', list(user_squating_challenge_ids))
                    ]).unlink()

                values = {
                    'definition_id': line.definition_id.id,
                    'line_id': line.id,
                    'target_goal': line.target_goal,
                    'state': 'inprogress',
                }

                if start_date:
                    values['start_date'] = start_date
                if end_date:
                    values['end_date'] = end_date

                # the goal is initialised over the limit to make sure we will compute it at least once
                if line.condition == 'higher':
                    values['current'] = min(line.target_goal - 1, 0)
                else:
                    values['current'] = max(line.target_goal + 1, 0)

                if challenge.remind_update_delay:
                    values['remind_update_delay'] = challenge.remind_update_delay

                for user_id in (participant_user_ids - user_with_goal_ids):
                    values['user_id'] = user_id
                    to_update |= Goals.create(values)

            to_update.update_goal()

            if self.env.context.get('commit_gamification'):
                self.env.cr.commit()

        return True

    ##### JS utilities #####

    def _get_serialized_challenge_lines(self, user=(), restrict_goals=(), restrict_top=0):
        """Return a serialised version of the goals information if the user has not completed every goal

        :param user: user retrieving progress (False if no distinction,
                     only for ranking challenges)
        :param restrict_goals: compute only the results for this subset of
                               gamification.goal ids, if False retrieve every
                               goal of current running challenge
        :param int restrict_top: for challenge lines where visibility_mode is
                                 ``ranking``, retrieve only the best
                                 ``restrict_top`` results and itself, if 0
                                 retrieve all restrict_goal_ids has priority
                                 over restrict_top

        format list
        # if visibility_mode == 'ranking'
        {
            'name': <gamification.goal.description name>,
            'description': <gamification.goal.description description>,
            'condition': <reach condition {lower,higher}>,
            'computation_mode': <target computation {manually,count,sum,python}>,
            'monetary': <{True,False}>,
            'suffix': <value suffix>,
            'action': <{True,False}>,
            'display_mode': <{progress,boolean}>,
            'target': <challenge line target>,
            'own_goal_id': <gamification.goal id where user_id == uid>,
            'goals': [
                {
                    'id': <gamification.goal id>,
                    'rank': <user ranking>,
                    'user_id': <res.users id>,
                    'name': <res.users name>,
                    'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
                    'completeness': <percentage>,
                    'current': <current value>,
                }
            ]
        },
        # if visibility_mode == 'personal'
        {
            'id': <gamification.goal id>,
            'name': <gamification.goal.description name>,
            'description': <gamification.goal.description description>,
            'condition': <reach condition {lower,higher}>,
            'computation_mode': <target computation {manually,count,sum,python}>,
            'monetary': <{True,False}>,
            'suffix': <value suffix>,
            'action': <{True,False}>,
            'display_mode': <{progress,boolean}>,
            'target': <challenge line target>,
            'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
            'completeness': <percentage>,
            'current': <current value>,
        }
        """
        Goals = self.env['gamification.goal']
        (start_date, end_date) = start_end_date_for_period(self.period)

        res_lines = []
        for line in self.line_ids:
            line_data = {
                'name': line.definition_id.name,
                'description': line.definition_id.description,
                'condition': line.definition_id.condition,
                'computation_mode': line.definition_id.computation_mode,
                'monetary': line.definition_id.monetary,
                'suffix': line.definition_id.suffix,
                'action': True if line.definition_id.action_id else False,
                'display_mode': line.definition_id.display_mode,
                'target': line.target_goal,
            }
            domain = [
                ('line_id', '=', line.id),
                ('state', '!=', 'draft'),
            ]
            if restrict_goals:
                domain.append(('id', 'in', restrict_goals.ids))
            else:
                # if no subset goals, use the dates for restriction
                if start_date:
                    domain.append(('start_date', '=', start_date))
                if end_date:
                    domain.append(('end_date', '=', end_date))

            if self.visibility_mode == 'personal':
                if not user:
                    raise exceptions.UserError(_("Retrieving progress for personal challenge without user information"))

                domain.append(('user_id', '=', user.id))

                goal = Goals.search(domain, limit=1)
                if not goal:
                    continue

                if goal.state != 'reached':
                    return []
                line_data.update(goal.read(['id', 'current', 'completeness', 'state'])[0])
                res_lines.append(line_data)
                continue

            line_data['own_goal_id'] = False,
            line_data['goals'] = []
            if line.condition=='higher':
                goals = Goals.search(domain, order="completeness desc, current desc")
            else:
                goals = Goals.search(domain, order="completeness desc, current asc")
            if not goals:
                continue

            for ranking, goal in enumerate(goals):
                if user and goal.user_id == user:
                    line_data['own_goal_id'] = goal.id
                elif restrict_top and ranking > restrict_top:
                    # not own goal and too low to be in top
                    continue

                line_data['goals'].append({
                    'id': goal.id,
                    'user_id': goal.user_id.id,
                    'name': goal.user_id.name,
                    'rank': ranking,
                    'current': goal.current,
                    'completeness': goal.completeness,
                    'state': goal.state,
                })
            if len(goals) < 3:
                # display at least the top 3 in the results
                missing = 3 - len(goals)
                for ranking, mock_goal in enumerate([{'id': False,
                                                      'user_id': False,
                                                      'name': '',
                                                      'current': 0,
                                                      'completeness': 0,
                                                      'state': False}] * missing,
                                                    start=len(goals)):
                    mock_goal['rank'] = ranking
                    line_data['goals'].append(mock_goal)

            res_lines.append(line_data)
        return res_lines

    ##### Reporting #####

    def report_progress(self, users=(), subset_goals=False):
        """Post report about the progress of the goals

        :param users: users that are concerned by the report. If False, will
                      send the report to every user concerned (goal users and
                      group that receive a copy). Only used for challenge with
                      a visibility mode set to 'personal'.
        :param subset_goals: goals to restrict the report
        """

        challenge = self

        if challenge.visibility_mode == 'ranking':
            lines_boards = challenge._get_serialized_challenge_lines(restrict_goals=subset_goals)

            body_html = challenge.report_template_id.with_context(challenge_lines=lines_boards)._render_field('body_html', challenge.ids)[challenge.id]

            # send to every follower and participant of the challenge
            challenge.message_post(
                body=body_html,
                partner_ids=challenge.mapped('user_ids.partner_id.id'),
                subtype_xmlid='mail.mt_comment',
                email_layout_xmlid='mail.mail_notification_light',
                )
            if challenge.report_message_group_id:
                challenge.report_message_group_id.message_post(
                    body=body_html,
                    subtype_xmlid='mail.mt_comment')

        else:
            # generate individual reports
            for user in (users or challenge.user_ids):
                lines = challenge._get_serialized_challenge_lines(user, restrict_goals=subset_goals)
                if not lines:
                    continue

                body_html = challenge.report_template_id.with_user(user).with_context(challenge_lines=lines)._render_field('body_html', challenge.ids)[challenge.id]

                # notify message only to users, do not post on the challenge
                challenge.message_notify(
                    body=body_html,
                    partner_ids=[user.partner_id.id],
                    subtype_xmlid='mail.mt_comment',
                    email_layout_xmlid='mail.mail_notification_light',
                )
                if challenge.report_message_group_id:
                    challenge.report_message_group_id.message_post(
                        body=body_html,
                        subtype_xmlid='mail.mt_comment',
                        email_layout_xmlid='mail.mail_notification_light',
                    )
        return challenge.write({'last_report_date': fields.Date.today()})

    ##### Challenges #####
    def accept_challenge(self):
        user = self.env.user
        sudoed = self.sudo()
        sudoed.message_post(body=_("%s has joined the challenge", user.name))
        sudoed.write({'invited_user_ids': [(3, user.id)], 'user_ids': [(4, user.id)]})
        return sudoed._generate_goals_from_challenge()

    def discard_challenge(self):
        """The user discard the suggested challenge"""
        user = self.env.user
        sudoed = self.sudo()
        sudoed.message_post(body=_("%s has refused the challenge", user.name))
        return sudoed.write({'invited_user_ids': (3, user.id)})

    def _check_challenge_reward(self, force=False):
        """Actions for the end of a challenge

        If a reward was selected, grant it to the correct users.
        Rewards granted at:
            - the end date for a challenge with no periodicity
            - the end of a period for challenge with periodicity
            - when a challenge is manually closed
        (if no end date, a running challenge is never rewarded)
        """
        commit = self.env.context.get('commit_gamification') and self.env.cr.commit

        for challenge in self:
            (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
            yesterday = date.today() - timedelta(days=1)

            rewarded_users = self.env['res.users']
            challenge_ended = force or end_date == fields.Date.to_string(yesterday)
            if challenge.reward_id and (challenge_ended or challenge.reward_realtime):
                # not using start_date as intemportal goals have a start date but no end_date
                reached_goals = self.env['gamification.goal'].read_group([
                    ('challenge_id', '=', challenge.id),
                    ('end_date', '=', end_date),
                    ('state', '=', 'reached')
                ], fields=['user_id'], groupby=['user_id'])
                for reach_goals_user in reached_goals:
                    if reach_goals_user['user_id_count'] == len(challenge.line_ids):
                        # the user has succeeded every assigned goal
                        user = self.env['res.users'].browse(reach_goals_user['user_id'][0])
                        if challenge.reward_realtime:
                            badges = self.env['gamification.badge.user'].search_count([
                                ('challenge_id', '=', challenge.id),
                                ('badge_id', '=', challenge.reward_id.id),
                                ('user_id', '=', user.id),
                            ])
                            if badges > 0:
                                # has already recieved the badge for this challenge
                                continue
                        challenge._reward_user(user, challenge.reward_id)
                        rewarded_users |= user
                        if commit:
                            commit()

            if challenge_ended:
                # open chatter message
                message_body = _("The challenge %s is finished.", challenge.name)

                if rewarded_users:
                    user_names = rewarded_users.name_get()
                    message_body += _(
                        "<br/>Reward (badge %(badge_name)s) for every succeeding user was sent to %(users)s.",
                        badge_name=challenge.reward_id.name,
                        users=", ".join(name for (user_id, name) in user_names)
                    )
                else:
                    message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewarded for this challenge.")

                # reward bests
                reward_message = _("<br/> %(rank)d. %(user_name)s - %(reward_name)s")
                if challenge.reward_first_id:
                    (first_user, second_user, third_user) = challenge._get_topN_users(MAX_VISIBILITY_RANKING)
                    if first_user:
                        challenge._reward_user(first_user, challenge.reward_first_id)
                        message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
                        message_body += reward_message % {
                            'rank': 1,
                            'user_name': first_user.name,
                            'reward_name': challenge.reward_first_id.name,
                        }
                    else:
                        message_body += _("Nobody reached the required conditions to receive special badges.")

                    if second_user and challenge.reward_second_id:
                        challenge._reward_user(second_user, challenge.reward_second_id)
                        message_body += reward_message % {
                            'rank': 2,
                            'user_name': second_user.name,
                            'reward_name': challenge.reward_second_id.name,
                        }
                    if third_user and challenge.reward_third_id:
                        challenge._reward_user(third_user, challenge.reward_third_id)
                        message_body += reward_message % {
                            'rank': 3,
                            'user_name': third_user.name,
                            'reward_name': challenge.reward_third_id.name,
                        }

                challenge.message_post(
                    partner_ids=[user.partner_id.id for user in challenge.user_ids],
                    body=message_body)
                if commit:
                    commit()

        return True

    def _get_topN_users(self, n):
        """Get the top N users for a defined challenge

        Ranking criterias:
            1. succeed every goal of the challenge
            2. total completeness of each goal (can be over 100)

        Only users having reached every goal of the challenge will be returned
        unless the challenge ``reward_failure`` is set, in which case any user
        may be considered.

        :returns: an iterable of exactly N records, either User objects or
                  False if there was no user for the rank. There can be no
                  False between two users (if users[k] = False then
                  users[k+1] = False
        """
        Goals = self.env['gamification.goal']
        (start_date, end_date) = start_end_date_for_period(self.period, self.start_date, self.end_date)
        challengers = []
        for user in self.user_ids:
            all_reached = True
            total_completeness = 0
            # every goal of the user for the running period
            goal_ids = Goals.search([
                ('challenge_id', '=', self.id),
                ('user_id', '=', user.id),
                ('start_date', '=', start_date),
                ('end_date', '=', end_date)
            ])
            for goal in goal_ids:
                if goal.state != 'reached':
                    all_reached = False
                if goal.definition_condition == 'higher':
                    # can be over 100
                    total_completeness += (100.0 * goal.current / goal.target_goal) if goal.target_goal else 0
                elif goal.state == 'reached':
                    # for lower goals, can not get percentage so 0 or 100
                    total_completeness += 100

            challengers.append({'user': user, 'all_reached': all_reached, 'total_completeness': total_completeness})

        challengers.sort(key=lambda k: (k['all_reached'], k['total_completeness']), reverse=True)
        if not self.reward_failure:
            # only keep the fully successful challengers at the front, could
            # probably use filter since the successful ones are at the front
            challengers = itertools.takewhile(lambda c: c['all_reached'], challengers)

        # append a tail of False, then keep the first N
        challengers = itertools.islice(
            itertools.chain(
                (c['user'] for c in challengers),
                itertools.repeat(False),
            ), 0, n
        )

        return tuple(challengers)

    def _reward_user(self, user, badge):
        """Create a badge user and send the badge to him

        :param user: the user to reward
        :param badge: the concerned badge
        """
        return self.env['gamification.badge.user'].create({
            'user_id': user.id,
            'badge_id': badge.id,
            'challenge_id': self.id
        })._send_badge()
示例#15
0
文件: product.py 项目: mausvt/flectra
class ProductProduct(models.Model):
    _name = "product.product"
    _description = "Product"
    _inherits = {'product.template': 'product_tmpl_id'}
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'default_code, name, id'

    # price: total price, context dependent (partner, pricelist, quantity)
    price = fields.Float(
        'Price', compute='_compute_product_price',
        digits='Product Price', inverse='_set_product_price')
    # price_extra: catalog extra value only, sum of variant extra attributes
    price_extra = fields.Float(
        'Variant Price Extra', compute='_compute_product_price_extra',
        digits='Product Price',
        help="This is the sum of the extra price of all attributes")
    # lst_price: catalog value + extra, context dependent (uom)
    lst_price = fields.Float(
        'Public Price', compute='_compute_product_lst_price',
        digits='Product Price', inverse='_set_product_lst_price',
        help="The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices.")

    default_code = fields.Char('Internal Reference', index=True)
    code = fields.Char('Reference', compute='_compute_product_code')
    partner_ref = fields.Char('Customer Ref', compute='_compute_partner_ref')

    active = fields.Boolean(
        'Active', default=True,
        help="If unchecked, it will allow you to hide the product without removing it.")
    product_tmpl_id = fields.Many2one(
        'product.template', 'Product Template',
        auto_join=True, index=True, ondelete="cascade", required=True)
    barcode = fields.Char(
        'Barcode', copy=False,
        help="International Article Number used for product identification.")
    product_template_attribute_value_ids = fields.Many2many('product.template.attribute.value', relation='product_variant_combination', string="Attribute Values", ondelete='restrict')
    combination_indices = fields.Char(compute='_compute_combination_indices', store=True, index=True)
    is_product_variant = fields.Boolean(compute='_compute_is_product_variant')

    standard_price = fields.Float(
        'Cost', company_dependent=True,
        digits='Product Price',
        groups="base.group_user",
        help="""In Standard Price & AVCO: value of the product (automatically computed in AVCO).
        In FIFO: value of the next unit that will leave the stock (automatically computed).
        Used to value the product when the purchase cost is not known (e.g. inventory adjustment).
        Used to compute margins on sale orders.""")
    volume = fields.Float('Volume', digits='Volume')
    weight = fields.Float('Weight', digits='Stock Weight')

    pricelist_item_count = fields.Integer("Number of price rules", compute="_compute_variant_item_count")

    packaging_ids = fields.One2many(
        'product.packaging', 'product_id', 'Product Packages',
        help="Gives the different ways to package the same product.")

    # all image fields are base64 encoded and PIL-supported

    # all image_variant fields are technical and should not be displayed to the user
    image_variant_1920 = fields.Image("Variant Image", max_width=1920, max_height=1920)

    # resized fields stored (as attachment) for performance
    image_variant_1024 = fields.Image("Variant Image 1024", related="image_variant_1920", max_width=1024, max_height=1024, store=True)
    image_variant_512 = fields.Image("Variant Image 512", related="image_variant_1920", max_width=512, max_height=512, store=True)
    image_variant_256 = fields.Image("Variant Image 256", related="image_variant_1920", max_width=256, max_height=256, store=True)
    image_variant_128 = fields.Image("Variant Image 128", related="image_variant_1920", max_width=128, max_height=128, store=True)
    can_image_variant_1024_be_zoomed = fields.Boolean("Can Variant Image 1024 be zoomed", compute='_compute_can_image_variant_1024_be_zoomed', store=True)

    # Computed fields that are used to create a fallback to the template if
    # necessary, it's recommended to display those fields to the user.
    image_1920 = fields.Image("Image", compute='_compute_image_1920', inverse='_set_image_1920')
    image_1024 = fields.Image("Image 1024", compute='_compute_image_1024')
    image_512 = fields.Image("Image 512", compute='_compute_image_512')
    image_256 = fields.Image("Image 256", compute='_compute_image_256')
    image_128 = fields.Image("Image 128", compute='_compute_image_128')
    can_image_1024_be_zoomed = fields.Boolean("Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed')

    @api.depends('image_variant_1920', 'image_variant_1024')
    def _compute_can_image_variant_1024_be_zoomed(self):
        for record in self:
            record.can_image_variant_1024_be_zoomed = record.image_variant_1920 and tools.is_image_size_above(record.image_variant_1920, record.image_variant_1024)

    def _compute_image_1920(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_1920 = record.image_variant_1920 or record.product_tmpl_id.image_1920

    def _set_image_1920(self):
        for record in self:
            if (
                # We are trying to remove an image even though it is already
                # not set, remove it from the template instead.
                not record.image_1920 and not record.image_variant_1920 or
                # We are trying to add an image, but the template image is
                # not set, write on the template instead.
                record.image_1920 and not record.product_tmpl_id.image_1920 or
                # There is only one variant, always write on the template.
                self.search_count([
                    ('product_tmpl_id', '=', record.product_tmpl_id.id),
                    ('active', '=', True),
                ]) <= 1
            ):
                record.image_variant_1920 = False
                record.product_tmpl_id.image_1920 = record.image_1920
            else:
                record.image_variant_1920 = record.image_1920

    def _compute_image_1024(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_1024 = record.image_variant_1024 or record.product_tmpl_id.image_1024

    def _compute_image_512(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_512 = record.image_variant_512 or record.product_tmpl_id.image_512

    def _compute_image_256(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_256 = record.image_variant_256 or record.product_tmpl_id.image_256

    def _compute_image_128(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_128 = record.image_variant_128 or record.product_tmpl_id.image_128

    def _compute_can_image_1024_be_zoomed(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.can_image_1024_be_zoomed = record.can_image_variant_1024_be_zoomed if record.image_variant_1920 else record.product_tmpl_id.can_image_1024_be_zoomed

    def init(self):
        """Ensure there is at most one active variant for each combination.

        There could be no variant for a combination if using dynamic attributes.
        """
        self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS product_product_combination_unique ON %s (product_tmpl_id, combination_indices) WHERE active is true"
            % self._table)

    _sql_constraints = [
        ('barcode_uniq', 'unique(barcode)', "A barcode can only be assigned to one product !"),
    ]

    def _get_invoice_policy(self):
        return False

    @api.depends('product_template_attribute_value_ids')
    def _compute_combination_indices(self):
        for product in self:
            product.combination_indices = product.product_template_attribute_value_ids._ids2str()

    def _compute_is_product_variant(self):
        self.is_product_variant = True

    @api.depends_context('pricelist', 'partner', 'quantity', 'uom', 'date', 'no_variant_attributes_price_extra')
    def _compute_product_price(self):
        prices = {}
        pricelist_id_or_name = self._context.get('pricelist')
        if pricelist_id_or_name:
            pricelist = None
            partner = self.env.context.get('partner', False)
            quantity = self.env.context.get('quantity', 1.0)

            # Support context pricelists specified as list, display_name or ID for compatibility
            if isinstance(pricelist_id_or_name, list):
                pricelist_id_or_name = pricelist_id_or_name[0]
            if isinstance(pricelist_id_or_name, str):
                pricelist_name_search = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
                if pricelist_name_search:
                    pricelist = self.env['product.pricelist'].browse([pricelist_name_search[0][0]])
            elif isinstance(pricelist_id_or_name, int):
                pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name)

            if pricelist:
                quantities = [quantity] * len(self)
                partners = [partner] * len(self)
                prices = pricelist.get_products_price(self, quantities, partners)

        for product in self:
            product.price = prices.get(product.id, 0.0)

    def _set_product_price(self):
        for product in self:
            if self._context.get('uom'):
                value = self.env['uom.uom'].browse(self._context['uom'])._compute_price(product.price, product.uom_id)
            else:
                value = product.price
            value -= product.price_extra
            product.write({'list_price': value})

    def _set_product_lst_price(self):
        for product in self:
            if self._context.get('uom'):
                value = self.env['uom.uom'].browse(self._context['uom'])._compute_price(product.lst_price, product.uom_id)
            else:
                value = product.lst_price
            value -= product.price_extra
            product.write({'list_price': value})

    def _compute_product_price_extra(self):
        for product in self:
            product.price_extra = sum(product.product_template_attribute_value_ids.mapped('price_extra'))

    @api.depends('list_price', 'price_extra')
    @api.depends_context('uom')
    def _compute_product_lst_price(self):
        to_uom = None
        if 'uom' in self._context:
            to_uom = self.env['uom.uom'].browse(self._context['uom'])

        for product in self:
            if to_uom:
                list_price = product.uom_id._compute_price(product.list_price, to_uom)
            else:
                list_price = product.list_price
            product.lst_price = list_price + product.price_extra

    @api.depends_context('partner_id')
    def _compute_product_code(self):
        for product in self:
            for supplier_info in product.seller_ids:
                if supplier_info.name.id == product._context.get('partner_id'):
                    product.code = supplier_info.product_code or product.default_code
                    break
            else:
                product.code = product.default_code

    @api.depends_context('partner_id')
    def _compute_partner_ref(self):
        for product in self:
            for supplier_info in product.seller_ids:
                if supplier_info.name.id == product._context.get('partner_id'):
                    product_name = supplier_info.product_name or product.default_code or product.name
                    product.partner_ref = '%s%s' % (product.code and '[%s] ' % product.code or '', product_name)
                    break
            else:
                product.partner_ref = product.display_name

    def _compute_variant_item_count(self):
        for product in self:
            domain = ['|',
                '&', ('product_tmpl_id', '=', product.product_tmpl_id.id), ('applied_on', '=', '1_product'),
                '&', ('product_id', '=', product.id), ('applied_on', '=', '0_product_variant')]
            product.pricelist_item_count = self.env['product.pricelist.item'].search_count(domain)

    @api.onchange('uom_id')
    def _onchange_uom_id(self):
        if self.uom_id:
            self.uom_po_id = self.uom_id.id

    @api.onchange('uom_po_id')
    def _onchange_uom(self):
        if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id:
            self.uom_po_id = self.uom_id

    @api.model_create_multi
    def create(self, vals_list):
        products = super(ProductProduct, self.with_context(create_product_product=True)).create(vals_list)
        # `_get_variant_id_for_combination` depends on existing variants
        self.clear_caches()
        return products

    def write(self, values):
        res = super(ProductProduct, self).write(values)
        if 'product_template_attribute_value_ids' in values:
            # `_get_variant_id_for_combination` depends on `product_template_attribute_value_ids`
            self.clear_caches()
        elif 'active' in values:
            # `_get_first_possible_variant_id` depends on variants active state
            self.clear_caches()
        return res

    def unlink(self):
        unlink_products = self.env['product.product']
        unlink_templates = self.env['product.template']
        for product in self:
            # If there is an image set on the variant and no image set on the
            # template, move the image to the template.
            if product.image_variant_1920 and not product.product_tmpl_id.image_1920:
                product.product_tmpl_id.image_1920 = product.image_variant_1920
            # Check if product still exists, in case it has been unlinked by unlinking its template
            if not product.exists():
                continue
            # Check if the product is last product of this template...
            other_products = self.search([('product_tmpl_id', '=', product.product_tmpl_id.id), ('id', '!=', product.id)])
            # ... and do not delete product template if it's configured to be created "on demand"
            if not other_products and not product.product_tmpl_id.has_dynamic_attributes():
                unlink_templates |= product.product_tmpl_id
            unlink_products |= product
        res = super(ProductProduct, unlink_products).unlink()
        # delete templates after calling super, as deleting template could lead to deleting
        # products due to ondelete='cascade'
        unlink_templates.unlink()
        # `_get_variant_id_for_combination` depends on existing variants
        self.clear_caches()
        return res

    def _filter_to_unlink(self, check_access=True):
        return self

    def _unlink_or_archive(self, check_access=True):
        """Unlink or archive products.
        Try in batch as much as possible because it is much faster.
        Use dichotomy when an exception occurs.
        """

        # Avoid access errors in case the products is shared amongst companies
        # but the underlying objects are not. If unlink fails because of an
        # AccessError (e.g. while recomputing fields), the 'write' call will
        # fail as well for the same reason since the field has been set to
        # recompute.
        if check_access:
            self.check_access_rights('unlink')
            self.check_access_rule('unlink')
            self.check_access_rights('write')
            self.check_access_rule('write')
            self = self.sudo()
            to_unlink = self._filter_to_unlink()
            to_archive = self - to_unlink
            to_archive.write({'active': False})
            self = to_unlink

        try:
            with self.env.cr.savepoint(), tools.mute_logger('flectra.sql_db'):
                self.unlink()
        except Exception:
            # We catch all kind of exceptions to be sure that the operation
            # doesn't fail.
            if len(self) > 1:
                self[:len(self) // 2]._unlink_or_archive(check_access=False)
                self[len(self) // 2:]._unlink_or_archive(check_access=False)
            else:
                if self.active:
                    # Note: this can still fail if something is preventing
                    # from archiving.
                    # This is the case from existing stock reordering rules.
                    self.write({'active': False})

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        """Variants are generated depending on the configuration of attributes
        and values on the template, so copying them does not make sense.

        For convenience the template is copied instead and its first variant is
        returned.
        """
        return self.product_tmpl_id.copy(default=default).product_variant_id

    @api.model
    def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
        # TDE FIXME: strange
        if self._context.get('search_default_categ_id'):
            args.append((('categ_id', 'child_of', self._context['search_default_categ_id'])))
        return super(ProductProduct, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)

    @api.depends_context('display_default_code', 'seller_id')
    def _compute_display_name(self):
        # `display_name` is calling `name_get()`` which is overidden on product
        # to depend on `display_default_code` and `seller_id`
        return super()._compute_display_name()

    def name_get(self):
        # TDE: this could be cleaned a bit I think

        def _name_get(d):
            name = d.get('name', '')
            code = self._context.get('display_default_code', True) and d.get('default_code', False) or False
            if code:
                name = '[%s] %s' % (code,name)
            return (d['id'], name)

        partner_id = self._context.get('partner_id')
        if partner_id:
            partner_ids = [partner_id, self.env['res.partner'].browse(partner_id).commercial_partner_id.id]
        else:
            partner_ids = []
        company_id = self.env.context.get('company_id')

        # all user don't have access to seller and partner
        # check access and use superuser
        self.check_access_rights("read")
        self.check_access_rule("read")

        result = []

        # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
        # Use `load=False` to not call `name_get` for the `product_tmpl_id`
        self.sudo().read(['name', 'default_code', 'product_tmpl_id'], load=False)

        product_template_ids = self.sudo().mapped('product_tmpl_id').ids

        if partner_ids:
            supplier_info = self.env['product.supplierinfo'].sudo().search([
                ('product_tmpl_id', 'in', product_template_ids),
                ('name', 'in', partner_ids),
            ])
            # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
            # Use `load=False` to not call `name_get` for the `product_tmpl_id` and `product_id`
            supplier_info.sudo().read(['product_tmpl_id', 'product_id', 'product_name', 'product_code'], load=False)
            supplier_info_by_template = {}
            for r in supplier_info:
                supplier_info_by_template.setdefault(r.product_tmpl_id, []).append(r)
        for product in self.sudo():
            variant = product.product_template_attribute_value_ids._get_combination_name()

            name = variant and "%s (%s)" % (product.name, variant) or product.name
            sellers = self.env['product.supplierinfo'].sudo().browse(self.env.context.get('seller_id')) or []
            if not sellers and partner_ids:
                product_supplier_info = supplier_info_by_template.get(product.product_tmpl_id, [])
                sellers = [x for x in product_supplier_info if x.product_id and x.product_id == product]
                if not sellers:
                    sellers = [x for x in product_supplier_info if not x.product_id]
                # Filter out sellers based on the company. This is done afterwards for a better
                # code readability. At this point, only a few sellers should remain, so it should
                # not be a performance issue.
                if company_id:
                    sellers = [x for x in sellers if x.company_id.id in [company_id, False]]
            if sellers:
                for s in sellers:
                    seller_variant = s.product_name and (
                        variant and "%s (%s)" % (s.product_name, variant) or s.product_name
                        ) or False
                    mydict = {
                              'id': product.id,
                              'name': seller_variant or name,
                              'default_code': s.product_code or product.default_code,
                              }
                    temp = _name_get(mydict)
                    if temp not in result:
                        result.append(temp)
            else:
                mydict = {
                          'id': product.id,
                          'name': name,
                          'default_code': product.default_code,
                          }
                result.append(_name_get(mydict))
        return result

    @api.model
    def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
        if not args:
            args = []
        if name:
            positive_operators = ['=', 'ilike', '=ilike', 'like', '=like']
            product_ids = []
            if operator in positive_operators:
                product_ids = list(self._search([('default_code', '=', name)] + args, limit=limit, access_rights_uid=name_get_uid))
                if not product_ids:
                    product_ids = list(self._search([('barcode', '=', name)] + args, limit=limit, access_rights_uid=name_get_uid))
            if not product_ids and operator not in expression.NEGATIVE_TERM_OPERATORS:
                # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
                # on a database with thousands of matching products, due to the huge merge+unique needed for the
                # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
                # Performing a quick memory merge of ids in Python will give much better performance
                product_ids = list(self._search(args + [('default_code', operator, name)], limit=limit))
                if not limit or len(product_ids) < limit:
                    # we may underrun the limit because of dupes in the results, that's fine
                    limit2 = (limit - len(product_ids)) if limit else False
                    product2_ids = self._search(args + [('name', operator, name), ('id', 'not in', product_ids)], limit=limit2, access_rights_uid=name_get_uid)
                    product_ids.extend(product2_ids)
            elif not product_ids and operator in expression.NEGATIVE_TERM_OPERATORS:
                domain = expression.OR([
                    ['&', ('default_code', operator, name), ('name', operator, name)],
                    ['&', ('default_code', '=', False), ('name', operator, name)],
                ])
                domain = expression.AND([args, domain])
                product_ids = list(self._search(domain, limit=limit, access_rights_uid=name_get_uid))
            if not product_ids and operator in positive_operators:
                ptrn = re.compile('(\[(.*?)\])')
                res = ptrn.search(name)
                if res:
                    product_ids = list(self._search([('default_code', '=', res.group(2))] + args, limit=limit, access_rights_uid=name_get_uid))
            # still no results, partner in context: search on supplier info as last hope to find something
            if not product_ids and self._context.get('partner_id'):
                suppliers_ids = self.env['product.supplierinfo']._search([
                    ('name', '=', self._context.get('partner_id')),
                    '|',
                    ('product_code', operator, name),
                    ('product_name', operator, name)], access_rights_uid=name_get_uid)
                if suppliers_ids:
                    product_ids = self._search([('product_tmpl_id.seller_ids', 'in', suppliers_ids)], limit=limit, access_rights_uid=name_get_uid)
        else:
            product_ids = self._search(args, limit=limit, access_rights_uid=name_get_uid)
        return product_ids

    @api.model
    def view_header_get(self, view_id, view_type):
        if self._context.get('categ_id'):
            return _(
                'Products: %(category)s',
                category=self.env['product.category'].browse(self.env.context['categ_id']).name,
            )
        return super().view_header_get(view_id, view_type)

    def open_pricelist_rules(self):
        self.ensure_one()
        domain = ['|',
            '&', ('product_tmpl_id', '=', self.product_tmpl_id.id), ('applied_on', '=', '1_product'),
            '&', ('product_id', '=', self.id), ('applied_on', '=', '0_product_variant')]
        return {
            'name': _('Price Rules'),
            'view_mode': 'tree,form',
            'views': [(self.env.ref('product.product_pricelist_item_tree_view_from_product').id, 'tree'), (False, 'form')],
            'res_model': 'product.pricelist.item',
            'type': 'ir.actions.act_window',
            'target': 'current',
            'domain': domain,
            'context': {
                'default_product_id': self.id,
                'default_applied_on': '0_product_variant',
            }
        }

    def open_product_template(self):
        """ Utility method used to add an "Open Template" button in product views """
        self.ensure_one()
        return {'type': 'ir.actions.act_window',
                'res_model': 'product.template',
                'view_mode': 'form',
                'res_id': self.product_tmpl_id.id,
                'target': 'new'}

    def _prepare_sellers(self, params=False):
        return self.seller_ids.filtered(lambda s: s.name.active).sorted(lambda s: (s.sequence, -s.min_qty, s.price, s.id))

    def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False):
        self.ensure_one()
        if date is None:
            date = fields.Date.context_today(self)
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')

        res = self.env['product.supplierinfo']
        sellers = self._prepare_sellers(params)
        sellers = sellers.filtered(lambda s: not s.company_id or s.company_id.id == self.env.company.id)
        for seller in sellers:
            # Set quantity in UoM of seller
            quantity_uom_seller = quantity
            if quantity_uom_seller and uom_id and uom_id != seller.product_uom:
                quantity_uom_seller = uom_id._compute_quantity(quantity_uom_seller, seller.product_uom)

            if seller.date_start and seller.date_start > date:
                continue
            if seller.date_end and seller.date_end < date:
                continue
            if partner_id and seller.name not in [partner_id, partner_id.parent_id]:
                continue
            if quantity is not None and float_compare(quantity_uom_seller, seller.min_qty, precision_digits=precision) == -1:
                continue
            if seller.product_id and seller.product_id != self:
                continue
            if not res or res.name == seller.name:
                res |= seller
        return res.sorted('price')[:1]

    def price_compute(self, price_type, uom=False, currency=False, company=None):
        # TDE FIXME: delegate to template or not ? fields are reencoded here ...
        # compatibility about context keys used a bit everywhere in the code
        if not uom and self._context.get('uom'):
            uom = self.env['uom.uom'].browse(self._context['uom'])
        if not currency and self._context.get('currency'):
            currency = self.env['res.currency'].browse(self._context['currency'])

        products = self
        if price_type == 'standard_price':
            # standard_price field can only be seen by users in base.group_user
            # Thus, in order to compute the sale price from the cost for users not in this group
            # We fetch the standard price as the superuser
            products = self.with_company(company or self.env.company).sudo()

        prices = dict.fromkeys(self.ids, 0.0)
        for product in products:
            prices[product.id] = product[price_type] or 0.0
            if price_type == 'list_price':
                prices[product.id] += product.price_extra
                # we need to add the price from the attributes that do not generate variants
                # (see field product.attribute create_variant)
                if self._context.get('no_variant_attributes_price_extra'):
                    # we have a list of price_extra that comes from the attribute values, we need to sum all that
                    prices[product.id] += sum(self._context.get('no_variant_attributes_price_extra'))

            if uom:
                prices[product.id] = product.uom_id._compute_price(prices[product.id], uom)

            # Convert from current user company currency to asked one
            # This is right cause a field cannot be in more than one currency
            if currency:
                prices[product.id] = product.currency_id._convert(
                    prices[product.id], currency, product.company_id, fields.Date.today())

        return prices

    @api.model
    def get_empty_list_help(self, help):
        self = self.with_context(
            empty_list_help_document_name=_("product"),
        )
        return super(ProductProduct, self).get_empty_list_help(help)

    def get_product_multiline_description_sale(self):
        """ Compute a multiline description of this product, in the context of sales
                (do not use for purchases or other display reasons that don't intend to use "description_sale").
            It will often be used as the default description of a sale order line referencing this product.
        """
        name = self.display_name
        if self.description_sale:
            name += '\n' + self.description_sale

        return name

    def _is_variant_possible(self, parent_combination=None):
        """Return whether the variant is possible based on its own combination,
        and optionally a parent combination.

        See `_is_combination_possible` for more information.

        :param parent_combination: combination from which `self` is an
            optional or accessory product.
        :type parent_combination: recordset `product.template.attribute.value`

        :return: ẁhether the variant is possible based on its own combination
        :rtype: bool
        """
        self.ensure_one()
        return self.product_tmpl_id._is_combination_possible(self.product_template_attribute_value_ids, parent_combination=parent_combination, ignore_no_variant=True)

    def toggle_active(self):
        """ Archiving related product.template if there is not any more active product.product
        (and vice versa, unarchiving the related product template if there is now an active product.product) """
        result = super().toggle_active()
        # We deactivate product templates which are active with no active variants.
        tmpl_to_deactivate = self.filtered(lambda product: (product.product_tmpl_id.active
                                                            and not product.product_tmpl_id.product_variant_ids)).mapped('product_tmpl_id')
        # We activate product templates which are inactive with active variants.
        tmpl_to_activate = self.filtered(lambda product: (not product.product_tmpl_id.active
                                                          and product.product_tmpl_id.product_variant_ids)).mapped('product_tmpl_id')
        (tmpl_to_deactivate + tmpl_to_activate).toggle_active()
        return result
class account_financial_report(models.Model):
    _name = "account.financial.report"
    _description = "Account Report"

    @api.multi
    @api.depends('parent_id', 'parent_id.level')
    def _get_level(self):
        '''Returns a dictionary with key=the ID of a record and value = the level of this  
           record in the tree structure.'''
        for report in self:
            level = 0
            if report.parent_id:
                level = report.parent_id.level + 1
            report.level = level

    def _get_children_by_order(self):
        '''returns a recordset of all the children computed recursively, and sorted by sequence. Ready for the printing'''
        res = self
        children = self.search([('parent_id', 'in', self.ids)],
                               order='sequence ASC')
        if children:
            for child in children:
                res += child._get_children_by_order()
        return res

    name = fields.Char('Report Name', required=True, translate=True)
    parent_id = fields.Many2one('account.financial.report', 'Parent')
    children_ids = fields.One2many('account.financial.report', 'parent_id',
                                   'Account Report')
    sequence = fields.Integer('Sequence')
    level = fields.Integer(compute='_get_level', string='Level', store=True)
    type = fields.Selection([
        ('sum', 'View'),
        ('accounts', 'Accounts'),
        ('account_type', 'Account Type'),
        ('account_report', 'Report Value'),
    ],
                            'Type',
                            default='sum')
    account_ids = fields.Many2many('account.account',
                                   'account_account_financial_report',
                                   'report_line_id', 'account_id', 'Accounts')
    account_report_id = fields.Many2one('account.financial.report',
                                        'Report Value')
    account_type_ids = fields.Many2many(
        'account.account.type', 'account_account_financial_report_type',
        'report_id', 'account_type_id', 'Account Types')
    sign = fields.Selection(
        [(-1, 'Reverse balance sign'), (1, 'Preserve balance sign')],
        'Sign on Reports',
        required=True,
        default=1,
        help=
        'For accounts that are typically more debited than credited and that you would like to print as negative amounts in your reports, you should reverse the sign of the balance; e.g.: Expense account. The same applies for accounts that are typically more credited than debited and that you would like to print as positive amounts in your reports; e.g.: Income account.'
    )
    display_detail = fields.Selection(
        [('no_detail', 'No detail'), ('detail_flat', 'Display children flat'),
         ('detail_with_hierarchy', 'Display children with hierarchy')],
        'Display details',
        default='detail_flat')
    style_overwrite = fields.Selection(
        [
            (0, 'Automatic formatting'),
            (1, 'Main Title 1 (bold, underlined)'),
            (2, 'Title 2 (bold)'),
            (3, 'Title 3 (bold, smaller)'),
            (4, 'Normal Text'),
            (5, 'Italic Text (smaller)'),
            (6, 'Smallest Text'),
        ],
        'Financial Report Style',
        default=0,
        help=
        "You can set up here the format you want this record to be displayed. If you leave the automatic formatting, it will be computed based on the financial reports hierarchy (auto-computed field 'level')."
    )
示例#17
0
class ProductTemplate(models.Model):
    _inherit = "product.template"

    multi_images = fields.One2many('multi.images', 'product_template_id',
                                   'Multi Images')
示例#18
0
class ResPartner(models.Model):
    _name = 'res.partner'
    _inherit = 'res.partner'

    @api.depends_context('company')
    def _credit_debit_get(self):
        tables, where_clause, where_params = self.env[
            'account.move.line'].with_context(
                state='posted', company_id=self.env.company.id)._query_get()
        where_params = [tuple(self.ids)] + where_params
        if where_clause:
            where_clause = 'AND ' + where_clause
        self._cr.execute(
            """SELECT account_move_line.partner_id, act.type, SUM(account_move_line.amount_residual)
                      FROM """ + tables + """
                      LEFT JOIN account_account a ON (account_move_line.account_id=a.id)
                      LEFT JOIN account_account_type act ON (a.user_type_id=act.id)
                      WHERE act.type IN ('receivable','payable')
                      AND account_move_line.partner_id IN %s
                      AND account_move_line.reconciled IS NOT TRUE
                      """ + where_clause + """
                      GROUP BY account_move_line.partner_id, act.type
                      """, where_params)
        treated = self.browse()
        for pid, type, val in self._cr.fetchall():
            partner = self.browse(pid)
            if type == 'receivable':
                partner.credit = val
                if partner not in treated:
                    partner.debit = False
                    treated |= partner
            elif type == 'payable':
                partner.debit = -val
                if partner not in treated:
                    partner.credit = False
                    treated |= partner
        remaining = (self - treated)
        remaining.debit = False
        remaining.credit = False

    def _asset_difference_search(self, account_type, operator, operand):
        if operator not in ('<', '=', '>', '>=', '<='):
            return []
        if type(operand) not in (float, int):
            return []
        sign = 1
        if account_type == 'payable':
            sign = -1
        res = self._cr.execute(
            '''
            SELECT partner.id
            FROM res_partner partner
            LEFT JOIN account_move_line aml ON aml.partner_id = partner.id
            JOIN account_move move ON move.id = aml.move_id
            RIGHT JOIN account_account acc ON aml.account_id = acc.id
            WHERE acc.internal_type = %s
              AND NOT acc.deprecated AND acc.company_id = %s
              AND move.state = 'posted'
            GROUP BY partner.id
            HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator +
            ''' %s''',
            (account_type, self.env.user.company_id.id, sign, operand))
        res = self._cr.fetchall()
        if not res:
            return [('id', '=', '0')]
        return [('id', 'in', [r[0] for r in res])]

    @api.model
    def _credit_search(self, operator, operand):
        return self._asset_difference_search('receivable', operator, operand)

    @api.model
    def _debit_search(self, operator, operand):
        return self._asset_difference_search('payable', operator, operand)

    def _invoice_total(self):
        self.total_invoiced = 0
        if not self.ids:
            return True

        all_partners_and_children = {}
        all_partner_ids = []
        for partner in self.filtered('id'):
            # price_total is in the company currency
            all_partners_and_children[partner] = self.with_context(
                active_test=False).search([('id', 'child_of', partner.id)]).ids
            all_partner_ids += all_partners_and_children[partner]

        domain = [
            ('partner_id', 'in', all_partner_ids),
            ('state', 'not in', ['draft', 'cancel']),
            ('move_type', 'in', ('out_invoice', 'out_refund')),
        ]
        price_totals = self.env['account.invoice.report'].read_group(
            domain, ['price_subtotal'], ['partner_id'])
        for partner, child_ids in all_partners_and_children.items():
            partner.total_invoiced = sum(
                price['price_subtotal'] for price in price_totals
                if price['partner_id'][0] in child_ids)

    def _compute_journal_item_count(self):
        AccountMoveLine = self.env['account.move.line']
        for partner in self:
            partner.journal_item_count = AccountMoveLine.search_count([
                ('partner_id', '=', partner.id)
            ])

    def _compute_has_unreconciled_entries(self):
        for partner in self:
            # Avoid useless work if has_unreconciled_entries is not relevant for this partner
            if not partner.active or not partner.is_company and partner.parent_id:
                partner.has_unreconciled_entries = False
                continue
            self.env.cr.execute(
                """ SELECT 1 FROM(
                        SELECT
                            p.last_time_entries_checked AS last_time_entries_checked,
                            MAX(l.write_date) AS max_date
                        FROM
                            account_move_line l
                            RIGHT JOIN account_account a ON (a.id = l.account_id)
                            RIGHT JOIN res_partner p ON (l.partner_id = p.id)
                        WHERE
                            p.id = %s
                            AND EXISTS (
                                SELECT 1
                                FROM account_move_line l
                                WHERE l.account_id = a.id
                                AND l.partner_id = p.id
                                AND l.amount_residual > 0
                            )
                            AND EXISTS (
                                SELECT 1
                                FROM account_move_line l
                                WHERE l.account_id = a.id
                                AND l.partner_id = p.id
                                AND l.amount_residual < 0
                            )
                        GROUP BY p.last_time_entries_checked
                    ) as s
                    WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked)
                """, (partner.id, ))
            partner.has_unreconciled_entries = self.env.cr.rowcount == 1

    def mark_as_reconciled(self):
        self.env['account.partial.reconcile'].check_access_rights('write')
        return self.sudo().write({
            'last_time_entries_checked':
            time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
        })

    def _get_company_currency(self):
        for partner in self:
            if partner.company_id:
                partner.currency_id = partner.sudo().company_id.currency_id
            else:
                partner.currency_id = self.env.company.currency_id

    credit = fields.Monetary(compute='_credit_debit_get',
                             search=_credit_search,
                             string='Total Receivable',
                             help="Total amount this customer owes you.")
    debit = fields.Monetary(
        compute='_credit_debit_get',
        search=_debit_search,
        string='Total Payable',
        help="Total amount you have to pay to this vendor.")
    debit_limit = fields.Monetary('Payable Limit')
    total_invoiced = fields.Monetary(
        compute='_invoice_total',
        string="Total Invoiced",
        groups='account.group_account_invoice,account.group_account_readonly')
    currency_id = fields.Many2one(
        'res.currency',
        compute='_get_company_currency',
        readonly=True,
        string="Currency",
        help='Utility field to express amount currency')
    journal_item_count = fields.Integer(compute='_compute_journal_item_count',
                                        string="Journal Items")
    property_account_payable_id = fields.Many2one(
        'account.account',
        company_dependent=True,
        string="Account Payable",
        domain=
        "[('internal_type', '=', 'payable'), ('deprecated', '=', False), ('company_id', '=', current_company_id)]",
        help=
        "This account will be used instead of the default one as the payable account for the current partner",
        required=True)
    property_account_receivable_id = fields.Many2one(
        'account.account',
        company_dependent=True,
        string="Account Receivable",
        domain=
        "[('internal_type', '=', 'receivable'), ('deprecated', '=', False), ('company_id', '=', current_company_id)]",
        help=
        "This account will be used instead of the default one as the receivable account for the current partner",
        required=True)
    property_account_position_id = fields.Many2one(
        'account.fiscal.position',
        company_dependent=True,
        string="Fiscal Position",
        domain="[('company_id', '=', current_company_id)]",
        help=
        "The fiscal position determines the taxes/accounts used for this contact."
    )
    property_payment_term_id = fields.Many2one(
        'account.payment.term',
        company_dependent=True,
        string='Customer Payment Terms',
        domain="[('company_id', 'in', [current_company_id, False])]",
        help=
        "This payment term will be used instead of the default one for sales orders and customer invoices"
    )
    property_supplier_payment_term_id = fields.Many2one(
        'account.payment.term',
        company_dependent=True,
        string='Vendor Payment Terms',
        domain="[('company_id', 'in', [current_company_id, False])]",
        help=
        "This payment term will be used instead of the default one for purchase orders and vendor bills"
    )
    ref_company_ids = fields.One2many(
        'res.company', 'partner_id', string='Companies that refers to partner')
    has_unreconciled_entries = fields.Boolean(
        compute='_compute_has_unreconciled_entries',
        help=
        "The partner has at least one unreconciled debit and credit since last time the invoices & payments matching was performed."
    )
    last_time_entries_checked = fields.Datetime(
        string='Latest Invoices & Payments Matching Date',
        readonly=True,
        copy=False,
        help=
        'Last time the invoices & payments matching was performed for this partner. '
        'It is set either if there\'s not at least an unreconciled debit and an unreconciled credit '
        'or if you click the "Done" button.')
    invoice_ids = fields.One2many('account.move',
                                  'partner_id',
                                  string='Invoices',
                                  readonly=True,
                                  copy=False)
    contract_ids = fields.One2many('account.analytic.account',
                                   'partner_id',
                                   string='Partner Contracts',
                                   readonly=True)
    bank_account_count = fields.Integer(compute='_compute_bank_count',
                                        string="Bank")
    trust = fields.Selection([('good', 'Good Debtor'),
                              ('normal', 'Normal Debtor'),
                              ('bad', 'Bad Debtor')],
                             string='Degree of trust you have in this debtor',
                             default='normal',
                             company_dependent=True)
    invoice_warn = fields.Selection(WARNING_MESSAGE,
                                    'Invoice',
                                    help=WARNING_HELP,
                                    default="no-message")
    invoice_warn_msg = fields.Text('Message for Invoice')
    # Computed fields to order the partners as suppliers/customers according to the
    # amount of their generated incoming/outgoing account moves
    supplier_rank = fields.Integer(default=0)
    customer_rank = fields.Integer(default=0)

    def _get_name_search_order_by_fields(self):
        res = super()._get_name_search_order_by_fields()
        partner_search_mode = self.env.context.get('res_partner_search_mode')
        if not partner_search_mode in ('customer', 'supplier'):
            return res
        order_by_field = 'COALESCE(res_partner.%s, 0) DESC,'
        if partner_search_mode == 'customer':
            field = 'customer_rank'
        else:
            field = 'supplier_rank'

        order_by_field = order_by_field % field
        return '%s, %s' % (res,
                           order_by_field % field) if res else order_by_field

    def _compute_bank_count(self):
        bank_data = self.env['res.partner.bank'].read_group(
            [('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id'])
        mapped_data = dict([(bank['partner_id'][0], bank['partner_id_count'])
                            for bank in bank_data])
        for partner in self:
            partner.bank_account_count = mapped_data.get(partner.id, 0)

    def _find_accounting_partner(self, partner):
        ''' Find the partner for which the accounting entries will be created '''
        return partner.commercial_partner_id

    @api.model
    def _commercial_fields(self):
        return super(ResPartner, self)._commercial_fields() + \
            ['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id',
             'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked']

    def action_view_partner_invoices(self):
        self.ensure_one()
        action = self.env["ir.actions.actions"]._for_xml_id(
            "account.action_move_out_invoice_type")
        action['domain'] = [
            ('move_type', 'in', ('out_invoice', 'out_refund')),
            ('partner_id', 'child_of', self.id),
        ]
        action['context'] = {
            'default_move_type': 'out_invoice',
            'move_type': 'out_invoice',
            'journal_type': 'sale',
            'search_default_unpaid': 1
        }
        return action

    def can_edit_vat(self):
        ''' Can't edit `vat` if there is (non draft) issued invoices. '''
        can_edit_vat = super(ResPartner, self).can_edit_vat()
        if not can_edit_vat:
            return can_edit_vat
        has_invoice = self.env['account.move'].search(
            [('move_type', 'in', ['out_invoice', 'out_refund']),
             ('partner_id', 'child_of', self.commercial_partner_id.id),
             ('state', '=', 'posted')],
            limit=1)
        return can_edit_vat and not (bool(has_invoice))

    @api.model_create_multi
    def create(self, vals_list):
        search_partner_mode = self.env.context.get('res_partner_search_mode')
        is_customer = search_partner_mode == 'customer'
        is_supplier = search_partner_mode == 'supplier'
        if search_partner_mode:
            for vals in vals_list:
                if is_customer and 'customer_rank' not in vals:
                    vals['customer_rank'] = 1
                elif is_supplier and 'supplier_rank' not in vals:
                    vals['supplier_rank'] = 1
        return super().create(vals_list)

    def _increase_rank(self, field, n=1):
        if self.ids and field in ['customer_rank', 'supplier_rank']:
            try:
                with self.env.cr.savepoint(flush=False):
                    query = sql.SQL("""
                        SELECT {field} FROM res_partner WHERE ID IN %(partner_ids)s FOR UPDATE NOWAIT;
                        UPDATE res_partner SET {field} = {field} + %(n)s
                        WHERE id IN %(partner_ids)s
                    """).format(field=sql.Identifier(field))
                    self.env.cr.execute(query, {
                        'partner_ids': tuple(self.ids),
                        'n': n
                    })
                    for partner in self:
                        self.env.cache.remove(partner, partner._fields[field])
            except DatabaseError as e:
                if e.pgcode == '55P03':
                    _logger.debug(
                        'Another transaction already locked partner rows. Cannot update partner ranks.'
                    )
                else:
                    raise e
示例#19
0
class QuantPackage(models.Model):
    """ Packages containing quants and/or other packages """
    _name = "stock.quant.package"
    _description = "Physical Packages"
    _order = 'name'

    name = fields.Char(
        'Package Reference', copy=False, index=True,
        default=lambda self: self.env['ir.sequence'].next_by_code('stock.quant.package') or _('Unknown Pack'))
    quant_ids = fields.One2many('stock.quant', 'package_id', 'Bulk Content', readonly=True)
    packaging_id = fields.Many2one(
        'product.packaging', 'Package Type', index=True,
        help="This field should be completed only if everything inside the package share the same product, otherwise it doesn't really makes sense.")
    location_id = fields.Many2one(
        'stock.location', 'Location', compute='_compute_package_info', search='_search_location',
        index=True, readonly=True)
    company_id = fields.Many2one(
        'res.company', 'Company', compute='_compute_package_info', search='_search_company',
        index=True, readonly=True)
    owner_id = fields.Many2one(
        'res.partner', 'Owner', compute='_compute_package_info', search='_search_owner',
        index=True, readonly=True)
    move_line_ids = fields.One2many('stock.move.line', 'result_package_id')
    current_picking_move_line_ids = fields.One2many('stock.move.line', compute="_compute_current_picking_info")
    current_picking_id = fields.Boolean(compute="_compute_current_picking_info")
    current_source_location_id = fields.Many2one('stock.location', compute="_compute_current_picking_info")
    current_destination_location_id = fields.Many2one('stock.location', compute="_compute_current_picking_info")
    is_processed = fields.Boolean(compute="_compute_current_picking_info")

    @api.depends('quant_ids.package_id', 'quant_ids.location_id', 'quant_ids.company_id', 'quant_ids.owner_id')
    def _compute_package_info(self):
        for package in self:
            values = {'location_id': False, 'company_id': self.env.user.company_id.id, 'owner_id': False}
            if package.quant_ids:
                values['location_id'] = package.quant_ids[0].location_id
            package.location_id = values['location_id']
            package.company_id = values['company_id']
            package.owner_id = values['owner_id']

    def name_get(self):
        return list(self._compute_complete_name().items())

    def _compute_complete_name(self):
        """ Forms complete name of location from parent location to child location. """
        res = {}
        for package in self:
            name = package.name
            res[package.id] = name
        return res

    def _compute_current_picking_info(self):
        """ When a package is in displayed in picking, it gets the picking id trough the context, and this function
        populates the different fields used when we move entire packages in pickings.
        """
        for package in self:
            picking_id = self.env.context.get('picking_id')
            if not picking_id:
                package.current_picking_move_line_ids = False
                package.current_picking_id = False
                package.is_processed = False
                package.current_source_location_id = False
                package.current_destination_location_id = False
                continue
            package.current_picking_move_line_ids = package.move_line_ids.filtered(lambda ml: ml.picking_id.id == picking_id)
            package.current_picking_id = True
            package.current_source_location_id = package.current_picking_move_line_ids[:1].location_id
            package.current_destination_location_id = package.current_picking_move_line_ids[:1].location_dest_id
            package.is_processed = not bool(package.current_picking_move_line_ids.filtered(lambda ml: ml.qty_done < ml.product_uom_qty))

    def action_toggle_processed(self):
        """ This method set the quantity done to the reserved quantity of all move lines of a package or to 0 if the package is already processed"""
        picking_id = self.env.context.get('picking_id')
        if picking_id:
            self.ensure_one()
            move_lines = self.current_picking_move_line_ids
            if move_lines.filtered(lambda ml: ml.qty_done < ml.product_uom_qty):
                destination_location = self.env.context.get('destination_location')
                for ml in move_lines:
                    vals = {'qty_done': ml.product_uom_qty}
                    if destination_location:
                        vals['location_dest_id'] = destination_location
                    ml.write(vals)
            else:
                for ml in move_lines:
                    ml.qty_done = 0


    def _search_location(self, operator, value):
        if value:
            packs = self.search([('quant_ids.location_id', operator, value)])
        else:
            packs = self.search([('quant_ids', operator, value)])
        if packs:
            return [('id', 'in', packs.ids)]
        else:
            return [('id', '=', False)]

    def _search_company(self, operator, value):
        if value:
            packs = self.search([('quant_ids.company_id', operator, value)])
        else:
            packs = self.search([('quant_ids', operator, value)])
        if packs:
            return [('id', 'parent_of', packs.ids)]
        else:
            return [('id', '=', False)]

    def _search_owner(self, operator, value):
        if value:
            packs = self.search([('quant_ids.owner_id', operator, value)])
        else:
            packs = self.search([('quant_ids', operator, value)])
        if packs:
            return [('id', 'parent_of', packs.ids)]
        else:
            return [('id', '=', False)]

    def _check_location_constraint(self):
        '''checks that all quants in a package are stored in the same location. This function cannot be used
           as a constraint because it needs to be checked on pack operations (they may not call write on the
           package)
        '''
        for pack in self:
            locations = pack.get_content().filtered(lambda quant: quant.qty > 0.0).mapped('location_id')
            if len(locations) != 1:
                raise UserError(_('Everything inside a package should be in the same location'))
        return True

    def unpack(self):
        for package in self:
            move_lines_to_remove = package.move_line_ids.filtered(lambda move_line: move_line.state != 'done')
            if move_lines_to_remove:
                move_lines_to_remove.write({'result_package_id': False})
            else:
                move_line_to_modify = self.env['stock.move.line'].search([
                    ('package_id', '=', package.id),
                    ('state', 'in', ('assigned', 'partially_available')),
                    ('product_qty', '!=', 0),
                ])
                move_line_to_modify.write({'package_id': False})
                package.mapped('quant_ids').sudo().write({'package_id': False})

    def action_view_picking(self):
        action = self.env.ref('stock.action_picking_tree_all').read()[0]
        domain = ['|', ('result_package_id', 'in', self.ids), ('package_id', 'in', self.ids)]
        pickings = self.env['stock.move.line'].search(domain).mapped('picking_id')
        action['domain'] = [('id', 'in', pickings.ids)]
        return action

    def view_content_package(self):
        action = self.env['ir.actions.act_window'].for_xml_id('stock', 'quantsact')
        action['domain'] = [('id', 'in', self._get_contained_quants().ids)]
        return action

    def _get_contained_quants(self):
        return self.env['stock.quant'].search([('package_id', 'in', self.ids)])

    def _get_all_products_quantities(self):
        '''This function computes the different product quantities for the given package
        '''
        # TDE CLEANME: probably to move somewhere else, like in pack op
        res = {}
        for quant in self._get_contained_quants():
            if quant.product_id not in res:
                res[quant.product_id] = 0
            res[quant.product_id] += quant.quantity
        return res
示例#20
0
class ProductTemplate(models.Model):
    _name = "product.template"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = "Product Template"
    _order = "name"

    def _get_default_category_id(self):
        if self._context.get('categ_id') or self._context.get(
                'default_categ_id'):
            return self._context.get('categ_id') or self._context.get(
                'default_categ_id')
        category = self.env.ref('product.product_category_all',
                                raise_if_not_found=False)
        if not category:
            category = self.env['product.category'].search([], limit=1)
        if category:
            return category.id
        else:
            err_msg = _(
                'You must define at least one product category in order to be able to create products.'
            )
            redir_msg = _('Go to Internal Categories')
            raise RedirectWarning(
                err_msg,
                self.env.ref('product.product_category_action_form').id,
                redir_msg)

    def _get_default_uom_id(self):
        return self.env["product.uom"].search([], limit=1, order='id').id

    name = fields.Char('Name', index=True, required=True, translate=True)
    sequence = fields.Integer(
        'Sequence',
        default=1,
        help='Gives the sequence order when displaying a product list')
    description = fields.Text(
        'Description',
        translate=True,
        help=
        "A precise description of the Product, used only for internal information purposes."
    )
    description_purchase = fields.Text(
        'Purchase Description',
        translate=True,
        help=
        "A description of the Product that you want to communicate to your vendors. "
        "This description will be copied to every Purchase Order, Receipt and Vendor Bill/Credit Note."
    )
    description_sale = fields.Text(
        'Sale Description',
        translate=True,
        help=
        "A description of the Product that you want to communicate to your customers. "
        "This description will be copied to every Sales Order, Delivery Order and Customer Invoice/Credit Note"
    )
    type = fields.Selection(
        [('consu', _('Consumable')), ('service', _('Service'))],
        string='Product Type',
        default='consu',
        required=True,
        help=
        'A stockable product is a product for which you manage stock. The "Inventory" app has to be installed.\n'
        'A consumable product, on the other hand, is a product for which stock is not managed.\n'
        'A service is a non-material product you provide.\n'
        'A digital content is a non-material product you sell online. The files attached to the products are the one that are sold on '
        'the e-commerce such as e-books, music, pictures,... The "Digital Product" module has to be installed.'
    )
    rental = fields.Boolean('Can be Rent')
    categ_id = fields.Many2one('product.category',
                               'Internal Category',
                               change_default=True,
                               default=_get_default_category_id,
                               required=True,
                               help="Select category for the current product")

    currency_id = fields.Many2one('res.currency',
                                  'Currency',
                                  compute='_compute_currency_id')

    # price fields
    price = fields.Float('Price',
                         compute='_compute_template_price',
                         inverse='_set_template_price',
                         digits=dp.get_precision('Product Price'))
    list_price = fields.Float(
        'Sales Price',
        default=1.0,
        digits=dp.get_precision('Product Price'),
        help=
        "Base price to compute the customer price. Sometimes called the catalog price."
    )
    lst_price = fields.Float('Public Price',
                             related='list_price',
                             digits=dp.get_precision('Product Price'))
    standard_price = fields.Float(
        'Cost',
        compute='_compute_standard_price',
        inverse='_set_standard_price',
        search='_search_standard_price',
        digits=dp.get_precision('Product Price'),
        groups="base.group_user",
        help=
        "Cost used for stock valuation in standard price and as a first price to set in average/fifo. "
        "Also used as a base price for pricelists. "
        "Expressed in the default unit of measure of the product. ")

    volume = fields.Float('Volume',
                          compute='_compute_volume',
                          inverse='_set_volume',
                          help="The volume in m3.",
                          store=True)
    weight = fields.Float(
        'Weight',
        compute='_compute_weight',
        digits=dp.get_precision('Stock Weight'),
        inverse='_set_weight',
        store=True,
        help=
        "The weight of the contents in Kg, not including any packaging, etc.")

    sale_ok = fields.Boolean(
        'Can be Sold',
        default=True,
        help="Specify if the product can be selected in a sales order line.")
    purchase_ok = fields.Boolean('Can be Purchased', default=True)
    pricelist_id = fields.Many2one(
        'product.pricelist',
        'Pricelist',
        store=False,
        help=
        'Technical field. Used for searching on pricelists, not stored in database.'
    )
    uom_id = fields.Many2one(
        'product.uom',
        'Unit of Measure',
        default=_get_default_uom_id,
        required=True,
        help="Default Unit of Measure used for all stock operation.")
    uom_po_id = fields.Many2one(
        'product.uom',
        'Purchase Unit of Measure',
        default=_get_default_uom_id,
        required=True,
        help=
        "Default Unit of Measure used for purchase orders. It must be in the same category than the default unit of measure."
    )
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('product.template'),
                                 index=1)
    packaging_ids = fields.One2many(
        'product.packaging',
        string="Product Packages",
        compute="_compute_packaging_ids",
        inverse="_set_packaging_ids",
        help="Gives the different ways to package the same product.")
    seller_ids = fields.One2many('product.supplierinfo', 'product_tmpl_id',
                                 'Vendors')
    variant_seller_ids = fields.One2many('product.supplierinfo',
                                         'product_tmpl_id')

    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "If unchecked, it will allow you to hide the product without removing it."
    )
    color = fields.Integer('Color Index')

    is_product_variant = fields.Boolean(string='Is a product variant',
                                        compute='_compute_is_product_variant')
    attribute_line_ids = fields.One2many('product.attribute.line',
                                         'product_tmpl_id',
                                         'Product Attributes')
    product_variant_ids = fields.One2many('product.product',
                                          'product_tmpl_id',
                                          'Products',
                                          required=True)
    # performance: product_variant_id provides prefetching on the first product variant only
    product_variant_id = fields.Many2one('product.product',
                                         'Product',
                                         compute='_compute_product_variant_id')

    product_variant_count = fields.Integer(
        '# Product Variants', compute='_compute_product_variant_count')

    # related to display product product information if is_product_variant
    barcode = fields.Char('Barcode',
                          oldname='ean13',
                          related='product_variant_ids.barcode')
    default_code = fields.Char('Internal Reference',
                               compute='_compute_default_code',
                               inverse='_set_default_code',
                               store=True)

    item_ids = fields.One2many('product.pricelist.item', 'product_tmpl_id',
                               'Pricelist Items')

    # image: all image fields are base64 encoded and PIL-supported
    image = fields.Binary(
        "Image",
        attachment=True,
        help=
        "This field holds the image used as image for the product, limited to 1024x1024px."
    )
    image_medium = fields.Binary(
        "Medium-sized image",
        attachment=True,
        help="Medium-sized image of the product. It is automatically "
        "resized as a 128x128px image, with aspect ratio preserved, "
        "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."
    )
    image_small = fields.Binary(
        "Small-sized image",
        attachment=True,
        help="Small-sized image of the product. It is automatically "
        "resized as a 64x64px image, with aspect ratio preserved. "
        "Use this field anywhere a small image is required.")

    @api.depends('product_variant_ids')
    def _compute_product_variant_id(self):
        for p in self:
            p.product_variant_id = p.product_variant_ids[:1].id

    @api.multi
    def _compute_currency_id(self):
        try:
            main_company = self.sudo().env.ref('base.main_company')
        except ValueError:
            main_company = self.env['res.company'].sudo().search([],
                                                                 limit=1,
                                                                 order="id")
        for template in self:
            template.currency_id = template.company_id.sudo(
            ).currency_id.id or main_company.currency_id.id

    @api.multi
    def _compute_template_price(self):
        prices = {}
        pricelist_id_or_name = self._context.get('pricelist')
        if pricelist_id_or_name:
            pricelist = None
            partner = self._context.get('partner')
            quantity = self._context.get('quantity', 1.0)

            # Support context pricelists specified as display_name or ID for compatibility
            if isinstance(pricelist_id_or_name, pycompat.string_types):
                pricelist_data = self.env['product.pricelist'].name_search(
                    pricelist_id_or_name, operator='=', limit=1)
                if pricelist_data:
                    pricelist = self.env['product.pricelist'].browse(
                        pricelist_data[0][0])
            elif isinstance(pricelist_id_or_name, pycompat.integer_types):
                pricelist = self.env['product.pricelist'].browse(
                    pricelist_id_or_name)

            if pricelist:
                quantities = [quantity] * len(self)
                partners = [partner] * len(self)
                prices = pricelist.get_products_price(self, quantities,
                                                      partners)

        for template in self:
            template.price = prices.get(template.id, 0.0)

    @api.multi
    def _set_template_price(self):
        if self._context.get('uom'):
            for template in self:
                value = self.env['product.uom'].browse(
                    self._context['uom'])._compute_price(
                        template.price, template.uom_id)
                template.write({'list_price': value})
        else:
            self.write({'list_price': self.price})

    @api.depends('product_variant_ids', 'product_variant_ids.standard_price')
    def _compute_standard_price(self):
        unique_variants = self.filtered(
            lambda template: len(template.product_variant_ids) == 1)
        for template in unique_variants:
            template.standard_price = template.product_variant_ids.standard_price
        for template in (self - unique_variants):
            template.standard_price = 0.0

    @api.one
    def _set_standard_price(self):
        if len(self.product_variant_ids) == 1:
            self.product_variant_ids.standard_price = self.standard_price

    def _search_standard_price(self, operator, value):
        products = self.env['product.product'].search(
            [('standard_price', operator, value)], limit=None)
        return [('id', 'in', products.mapped('product_tmpl_id').ids)]

    @api.depends('product_variant_ids', 'product_variant_ids.volume')
    def _compute_volume(self):
        unique_variants = self.filtered(
            lambda template: len(template.product_variant_ids) == 1)
        for template in unique_variants:
            template.volume = template.product_variant_ids.volume
        for template in (self - unique_variants):
            template.volume = 0.0

    @api.one
    def _set_volume(self):
        if len(self.product_variant_ids) == 1:
            self.product_variant_ids.volume = self.volume

    @api.depends('product_variant_ids', 'product_variant_ids.weight')
    def _compute_weight(self):
        unique_variants = self.filtered(
            lambda template: len(template.product_variant_ids) == 1)
        for template in unique_variants:
            template.weight = template.product_variant_ids.weight
        for template in (self - unique_variants):
            template.weight = 0.0

    def _compute_is_product_variant(self):
        for template in self:
            if template._name == 'product.template':
                template.is_product_variant = False
            else:
                template.is_product_variant = True

    @api.one
    def _set_weight(self):
        if len(self.product_variant_ids) == 1:
            self.product_variant_ids.weight = self.weight

    @api.one
    @api.depends('product_variant_ids.product_tmpl_id')
    def _compute_product_variant_count(self):
        # do not pollute variants to be prefetched when counting variants
        self.product_variant_count = len(
            self.with_prefetch().product_variant_ids)

    @api.depends('product_variant_ids', 'product_variant_ids.default_code')
    def _compute_default_code(self):
        unique_variants = self.filtered(
            lambda template: len(template.product_variant_ids) == 1)
        for template in unique_variants:
            template.default_code = template.product_variant_ids.default_code
        for template in (self - unique_variants):
            template.default_code = ''

    @api.one
    def _set_default_code(self):
        if len(self.product_variant_ids) == 1:
            self.product_variant_ids.default_code = self.default_code

    @api.depends('product_variant_ids', 'product_variant_ids.packaging_ids')
    def _compute_packaging_ids(self):
        for p in self:
            if len(p.product_variant_ids) == 1:
                p.packaging_ids = p.product_variant_ids.packaging_ids

    def _set_packaging_ids(self):
        for p in self:
            if len(p.product_variant_ids) == 1:
                p.product_variant_ids.packaging_ids = p.packaging_ids

    @api.constrains('uom_id', 'uom_po_id')
    def _check_uom(self):
        if any(template.uom_id and template.uom_po_id and
               template.uom_id.category_id != template.uom_po_id.category_id
               for template in self):
            raise ValidationError(
                _('Error: The default Unit of Measure and the purchase Unit of Measure must be in the same category.'
                  ))
        return True

    @api.onchange('uom_id')
    def _onchange_uom_id(self):
        if self.uom_id:
            self.uom_po_id = self.uom_id.id

    @api.model
    def create(self, vals):
        ''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
        # TDE FIXME: context brol
        tools.image_resize_images(vals)
        template = super(ProductTemplate, self).create(vals)
        if "create_product_product" not in self._context:
            template.with_context(create_from_tmpl=True).create_variant_ids()

        # This is needed to set given values to first variant after creation
        related_vals = {}
        if vals.get('barcode'):
            related_vals['barcode'] = vals['barcode']
        if vals.get('default_code'):
            related_vals['default_code'] = vals['default_code']
        if vals.get('standard_price'):
            related_vals['standard_price'] = vals['standard_price']
        if vals.get('volume'):
            related_vals['volume'] = vals['volume']
        if vals.get('weight'):
            related_vals['weight'] = vals['weight']
        if related_vals:
            template.write(related_vals)
        return template

    @api.multi
    def write(self, vals):
        tools.image_resize_images(vals)
        res = super(ProductTemplate, self).write(vals)
        if 'attribute_line_ids' in vals or vals.get('active'):
            self.create_variant_ids()
        if 'active' in vals and not vals.get('active'):
            self.with_context(
                active_test=False).mapped('product_variant_ids').write(
                    {'active': vals.get('active')})
        return res

    @api.multi
    def copy(self, default=None):
        # TDE FIXME: should probably be copy_data
        self.ensure_one()
        if default is None:
            default = {}
        if 'name' not in default:
            default['name'] = _("%s (copy)") % self.name
        return super(ProductTemplate, self).copy(default=default)

    @api.multi
    def name_get(self):
        return [(template.id, '%s%s' %
                 (template.default_code and '[%s] ' % template.default_code
                  or '', template.name)) for template in self]

    @api.model
    def name_search(self, name='', args=None, operator='ilike', limit=100):
        # Only use the product.product heuristics if there is a search term and the domain
        # does not specify a match on `product.template` IDs.
        if not name or any(term[0] == 'id' for term in (args or [])):
            return super(ProductTemplate, self).name_search(name=name,
                                                            args=args,
                                                            operator=operator,
                                                            limit=limit)

        Product = self.env['product.product']
        templates = self.browse([])
        while True:
            domain = templates and [
                ('product_tmpl_id', 'not in', templates.ids)
            ] or []
            args = args if args is not None else []
            products_ns = Product.name_search(name,
                                              args + domain,
                                              operator=operator)
            products = Product.browse([x[0] for x in products_ns])
            templates |= products.mapped('product_tmpl_id')
            if (not products) or (limit and (len(templates) > limit)):
                break

        # re-apply product.template order + name_get
        return super(ProductTemplate,
                     self).name_search('',
                                       args=[('id', 'in',
                                              list(set(templates.ids)))],
                                       operator='ilike',
                                       limit=limit)

    @api.multi
    def price_compute(self,
                      price_type,
                      uom=False,
                      currency=False,
                      company=False):
        # TDE FIXME: delegate to template or not ? fields are reencoded here ...
        # compatibility about context keys used a bit everywhere in the code
        if not uom and self._context.get('uom'):
            uom = self.env['product.uom'].browse(self._context['uom'])
        if not currency and self._context.get('currency'):
            currency = self.env['res.currency'].browse(
                self._context['currency'])

        templates = self
        if price_type == 'standard_price':
            # standard_price field can only be seen by users in base.group_user
            # Thus, in order to compute the sale price from the cost for users not in this group
            # We fetch the standard price as the superuser
            templates = self.with_context(
                force_company=company and company.id or self._context.get(
                    'force_company', self.env.user.company_id.id)).sudo()

        prices = dict.fromkeys(self.ids, 0.0)
        for template in templates:
            prices[template.id] = template[price_type] or 0.0

            if uom:
                prices[template.id] = template.uom_id._compute_price(
                    prices[template.id], uom)

            # Convert from current user company currency to asked one
            # This is right cause a field cannot be in more than one currency
            if currency:
                prices[template.id] = template.currency_id.compute(
                    prices[template.id], currency)

        return prices

    # compatibility to remove after v10 - DEPRECATED
    @api.model
    def _price_get(self, products, ptype='list_price'):
        return products.price_compute(ptype)

    @api.multi
    def create_variant_ids(self):
        Product = self.env["product.product"]
        AttributeValues = self.env['product.attribute.value']
        for tmpl_id in self.with_context(active_test=False):
            # adding an attribute with only one value should not recreate product
            # write this attribute on every product to make sure we don't lose them
            variant_alone = tmpl_id.attribute_line_ids.filtered(
                lambda line: line.attribute_id.create_variant and len(
                    line.value_ids) == 1).mapped('value_ids')
            for value_id in variant_alone:
                updated_products = tmpl_id.product_variant_ids.filtered(
                    lambda product: value_id.attribute_id not in product.
                    mapped('attribute_value_ids.attribute_id'))
                updated_products.write(
                    {'attribute_value_ids': [(4, value_id.id)]})

            # iterator of n-uple of product.attribute.value *ids*
            variant_matrix = [
                AttributeValues.browse(value_ids)
                for value_ids in itertools.product(
                    *(line.value_ids.ids for line in tmpl_id.attribute_line_ids
                      if line.value_ids[:1].attribute_id.create_variant))
            ]

            # get the value (id) sets of existing variants
            existing_variants = {
                frozenset(
                    variant.attribute_value_ids.filtered(
                        lambda r: r.attribute_id.create_variant).ids)
                for variant in tmpl_id.product_variant_ids
            }
            # -> for each value set, create a recordset of values to create a
            #    variant for if the value set isn't already a variant
            to_create_variants = [
                value_ids for value_ids in variant_matrix
                if set(value_ids.ids) not in existing_variants
            ]

            # check product
            variants_to_activate = self.env['product.product']
            variants_to_unlink = self.env['product.product']
            for product_id in tmpl_id.product_variant_ids:
                if not product_id.active and product_id.attribute_value_ids.filtered(
                        lambda r: r.attribute_id.create_variant
                ) in variant_matrix:
                    variants_to_activate |= product_id
                elif product_id.attribute_value_ids.filtered(
                        lambda r: r.attribute_id.create_variant
                ) not in variant_matrix:
                    variants_to_unlink |= product_id
            if variants_to_activate:
                variants_to_activate.write({'active': True})

            # create new product
            for variant_ids in to_create_variants:
                new_variant = Product.create({
                    'product_tmpl_id':
                    tmpl_id.id,
                    'attribute_value_ids': [(6, 0, variant_ids.ids)]
                })

            # unlink or inactive product
            for variant in variants_to_unlink:
                try:
                    with self._cr.savepoint(), tools.mute_logger(
                            'flectra.sql_db'):
                        variant.unlink()
                # We catch all kind of exception to be sure that the operation doesn't fail.
                except (psycopg2.Error, except_orm):
                    variant.write({'active': False})
                    pass
        return True
示例#21
0
class AccountAnalyticAccount(models.Model):
    _name = 'account.analytic.account'
    _inherit = ['mail.thread', 'ir.branch.company.mixin']
    _description = 'Analytic Account'
    _order = 'code, name asc'

    @api.multi
    def _compute_debit_credit_balance(self):
        analytic_line_obj = self.env['account.analytic.line']
        domain = [('account_id', 'in', self.mapped('id'))]
        if self._context.get('from_date', False):
            domain.append(('date', '>=', self._context['from_date']))
        if self._context.get('to_date', False):
            domain.append(('date', '<=', self._context['to_date']))

        account_amounts = analytic_line_obj.search_read(
            domain, ['account_id', 'amount'])
        account_ids = set([line['account_id'][0] for line in account_amounts])
        data_debit = {account_id: 0.0 for account_id in account_ids}
        data_credit = {account_id: 0.0 for account_id in account_ids}
        for account_amount in account_amounts:
            if account_amount['amount'] < 0.0:
                data_debit[account_amount['account_id']
                           [0]] += account_amount['amount']
            else:
                data_credit[account_amount['account_id']
                            [0]] += account_amount['amount']

        for account in self:
            account.debit = abs(data_debit.get(account.id, 0.0))
            account.credit = data_credit.get(account.id, 0.0)
            account.balance = account.credit - account.debit

    name = fields.Char(string='Analytic Account',
                       index=True,
                       required=True,
                       track_visibility='onchange')
    code = fields.Char(string='Reference',
                       index=True,
                       track_visibility='onchange')
    active = fields.Boolean(
        'Active',
        help=
        "If the active field is set to False, it will allow you to hide the account without removing it.",
        default=True)

    tag_ids = fields.Many2many('account.analytic.tag',
                               'account_analytic_account_tag_rel',
                               'account_id',
                               'tag_id',
                               string='Tags',
                               copy=True)
    line_ids = fields.One2many('account.analytic.line',
                               'account_id',
                               string="Analytic Lines")

    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 required=True,
                                 default=lambda self: self.env.user.company_id)

    # use auto_join to speed up name_search call
    partner_id = fields.Many2one('res.partner',
                                 string='Customer',
                                 auto_join=True,
                                 track_visibility='onchange')

    balance = fields.Monetary(compute='_compute_debit_credit_balance',
                              string='Balance')
    debit = fields.Monetary(compute='_compute_debit_credit_balance',
                            string='Debit')
    credit = fields.Monetary(compute='_compute_debit_credit_balance',
                             string='Credit')

    currency_id = fields.Many2one(related="company_id.currency_id",
                                  string="Currency",
                                  readonly=True)

    @api.multi
    def name_get(self):
        res = []
        for analytic in self:
            name = analytic.name
            if analytic.code:
                name = '[' + analytic.code + '] ' + name
            if analytic.partner_id:
                name = name + ' - ' + analytic.partner_id.commercial_partner_id.name
            res.append((analytic.id, name))
        return res

    @api.model
    def name_search(self, name='', args=None, operator='ilike', limit=100):
        if operator not in ('ilike', 'like', '=', '=like', '=ilike'):
            return super(AccountAnalyticAccount,
                         self).name_search(name, args, operator, limit)
        args = args or []
        domain = ['|', ('code', operator, name), ('name', operator, name)]
        partners = self.env['res.partner'].search([('name', operator, name)],
                                                  limit=limit)
        if partners:
            domain = ['|'] + domain + [('partner_id', 'in', partners.ids)]
        recs = self.search(domain + args, limit=limit)
        return recs.name_get()
示例#22
0
文件: partner.py 项目: ly2ly/flectra
class AccountFiscalPosition(models.Model):
    _name = 'account.fiscal.position'
    _description = 'Fiscal Position'
    _order = 'sequence'

    sequence = fields.Integer()
    name = fields.Char(string='Fiscal Position', required=True)
    active = fields.Boolean(
        default=True,
        help=
        "By unchecking the active field, you may hide a fiscal position without deleting it."
    )
    company_id = fields.Many2one('res.company', string='Company')
    account_ids = fields.One2many('account.fiscal.position.account',
                                  'position_id',
                                  string='Account Mapping',
                                  copy=True)
    tax_ids = fields.One2many('account.fiscal.position.tax',
                              'position_id',
                              string='Tax Mapping',
                              copy=True)
    note = fields.Text(
        'Notes',
        translate=True,
        help="Legal mentions that have to be printed on the invoices.")
    auto_apply = fields.Boolean(
        string='Detect Automatically',
        help="Apply automatically this fiscal position.")
    vat_required = fields.Boolean(
        string='VAT required', help="Apply only if partner has a VAT number.")
    country_id = fields.Many2one(
        'res.country',
        string='Country',
        help="Apply only if delivery or invoicing country match.")
    country_group_id = fields.Many2one(
        'res.country.group',
        string='Country Group',
        help="Apply only if delivery or invocing country match the group.")
    state_ids = fields.Many2many('res.country.state', string='Federal States')
    zip_from = fields.Integer(string='Zip Range From', default=0)
    zip_to = fields.Integer(string='Zip Range To', default=0)
    # To be used in hiding the 'Federal States' field('attrs' in view side) when selected 'Country' has 0 states.
    states_count = fields.Integer(compute='_compute_states_count')

    @api.one
    def _compute_states_count(self):
        self.states_count = len(self.country_id.state_ids)

    @api.one
    @api.constrains('zip_from', 'zip_to')
    def _check_zip(self):
        if self.zip_from > self.zip_to:
            raise ValidationError(
                _('Invalid "Zip Range", please configure it properly.'))
        return True

    @api.model  # noqa
    def map_tax(self, taxes, product=None, partner=None):
        result = self.env['account.tax'].browse()
        for tax in taxes:
            tax_count = 0
            for t in self.tax_ids:
                if t.tax_src_id == tax:
                    tax_count += 1
                    if t.tax_dest_id:
                        result |= t.tax_dest_id
            if not tax_count:
                result |= tax
        return result

    @api.model
    def map_account(self, account):
        for pos in self.account_ids:
            if pos.account_src_id == account:
                return pos.account_dest_id
        return account

    @api.model
    def map_accounts(self, accounts):
        """ Receive a dictionary having accounts in values and try to replace those accounts accordingly to the fiscal position.
        """
        ref_dict = {}
        for line in self.account_ids:
            ref_dict[line.account_src_id] = line.account_dest_id
        for key, acc in accounts.items():
            if acc in ref_dict:
                accounts[key] = ref_dict[acc]
        return accounts

    @api.onchange('country_id')
    def _onchange_country_id(self):
        if self.country_id:
            self.zip_from = self.zip_to = self.country_group_id = False
            self.state_ids = [(5, )]
            self.states_count = len(self.country_id.state_ids)

    @api.onchange('country_group_id')
    def _onchange_country_group_id(self):
        if self.country_group_id:
            self.zip_from = self.zip_to = self.country_id = False
            self.state_ids = [(5, )]

    @api.model
    def _get_fpos_by_region(self,
                            country_id=False,
                            state_id=False,
                            zipcode=False,
                            vat_required=False):
        if not country_id:
            return False
        base_domain = [('auto_apply', '=', True),
                       ('vat_required', '=', vat_required)]
        if self.env.context.get('force_company'):
            base_domain.append(
                ('company_id', '=', self.env.context.get('force_company')))
        null_state_dom = state_domain = [('state_ids', '=', False)]
        null_zip_dom = zip_domain = [('zip_from', '=', 0), ('zip_to', '=', 0)]
        null_country_dom = [('country_id', '=', False),
                            ('country_group_id', '=', False)]

        if zipcode and zipcode.isdigit():
            zipcode = int(zipcode)
            zip_domain = [('zip_from', '<=', zipcode),
                          ('zip_to', '>=', zipcode)]
        else:
            zipcode = 0

        if state_id:
            state_domain = [('state_ids', '=', state_id)]

        domain_country = base_domain + [('country_id', '=', country_id)]
        domain_group = base_domain + [
            ('country_group_id.country_ids', '=', country_id)
        ]

        # Build domain to search records with exact matching criteria
        fpos = self.search(domain_country + state_domain + zip_domain, limit=1)
        # return records that fit the most the criteria, and fallback on less specific fiscal positions if any can be found
        if not fpos and state_id:
            fpos = self.search(domain_country + null_state_dom + zip_domain,
                               limit=1)
        if not fpos and zipcode:
            fpos = self.search(domain_country + state_domain + null_zip_dom,
                               limit=1)
        if not fpos and state_id and zipcode:
            fpos = self.search(domain_country + null_state_dom + null_zip_dom,
                               limit=1)

        # fallback: country group with no state/zip range
        if not fpos:
            fpos = self.search(domain_group + null_state_dom + null_zip_dom,
                               limit=1)

        if not fpos:
            # Fallback on catchall (no country, no group)
            fpos = self.search(base_domain + null_country_dom, limit=1)
        return fpos or False

    @api.model
    def get_fiscal_position(self, partner_id, delivery_id=None):
        if not partner_id:
            return False
        # This can be easily overriden to apply more complex fiscal rules
        PartnerObj = self.env['res.partner']
        partner = PartnerObj.browse(partner_id)

        # if no delivery use invoicing
        if delivery_id:
            delivery = PartnerObj.browse(delivery_id)
        else:
            delivery = partner

        # partner manually set fiscal position always win
        if delivery.property_account_position_id or partner.property_account_position_id:
            return delivery.property_account_position_id.id or partner.property_account_position_id.id

        # First search only matching VAT positions
        vat_required = bool(partner.vat)
        fp = self._get_fpos_by_region(delivery.country_id.id,
                                      delivery.state_id.id, delivery.zip,
                                      vat_required)

        # Then if VAT required found no match, try positions that do not require it
        if not fp and vat_required:
            fp = self._get_fpos_by_region(delivery.country_id.id,
                                          delivery.state_id.id, delivery.zip,
                                          False)

        return fp.id if fp else False
示例#23
0
class Post(models.Model):

    _name = 'forum.post'
    _description = 'Forum Post'
    _inherit = ['mail.thread', 'website.seo.metadata']
    _order = "is_correct DESC, vote_count DESC, write_date DESC"

    name = fields.Char('Title')
    forum_id = fields.Many2one('forum.forum', string='Forum', required=True)
    content = fields.Html('Content', strip_style=True)
    plain_content = fields.Text('Plain Content', compute='_get_plain_content', store=True)
    content_link = fields.Char('URL', help="URL of Link Articles")
    tag_ids = fields.Many2many('forum.tag', 'forum_tag_rel', 'forum_id', 'forum_tag_id', string='Tags')
    state = fields.Selection([('active', 'Active'), ('pending', 'Waiting Validation'), ('close', 'Close'), ('offensive', 'Offensive'), ('flagged', 'Flagged')], string='Status', default='active')
    views = fields.Integer('Number of Views', default=0)
    active = fields.Boolean('Active', default=True)
    post_type = fields.Selection([
        ('question', 'Question'),
        ('link', 'Article'),
        ('discussion', 'Discussion')],
        string='Type', default='question', required=True)
    website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])

    # history
    create_date = fields.Datetime('Asked on', index=True, readonly=True)
    create_uid = fields.Many2one('res.users', string='Created by', index=True, readonly=True)
    write_date = fields.Datetime('Update on', index=True, readonly=True)
    bump_date = fields.Datetime('Bumped on', readonly=True,
                                help="Technical field allowing to bump a question. Writing on this field will trigger "
                                     "a write on write_date and therefore bump the post. Directly writing on write_date "
                                     "is currently not supported and this field is a workaround.")
    write_uid = fields.Many2one('res.users', string='Updated by', index=True, readonly=True)
    relevancy = fields.Float('Relevance', compute="_compute_relevancy", store=True)

    # vote
    vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes')
    user_vote = fields.Integer('My Vote', compute='_get_user_vote')
    vote_count = fields.Integer('Total Votes', compute='_get_vote_count', store=True)

    # favorite
    favourite_ids = fields.Many2many('res.users', string='Favourite')
    user_favourite = fields.Boolean('Is Favourite', compute='_get_user_favourite')
    favourite_count = fields.Integer('Favorite Count', compute='_get_favorite_count', store=True)

    # hierarchy
    is_correct = fields.Boolean('Correct', help='Correct answer or answer accepted')
    parent_id = fields.Many2one('forum.post', string='Question', ondelete='cascade')
    self_reply = fields.Boolean('Reply to own question', compute='_is_self_reply', store=True)
    child_ids = fields.One2many('forum.post', 'parent_id', string='Answers')
    child_count = fields.Integer('Number of answers', compute='_get_child_count', store=True)
    uid_has_answered = fields.Boolean('Has Answered', compute='_get_uid_has_answered')
    has_validated_answer = fields.Boolean('Is answered', compute='_get_has_validated_answer', store=True)

    # offensive moderation tools
    flag_user_id = fields.Many2one('res.users', string='Flagged by')
    moderator_id = fields.Many2one('res.users', string='Reviewed by', readonly=True)

    # closing
    closed_reason_id = fields.Many2one('forum.post.reason', string='Reason')
    closed_uid = fields.Many2one('res.users', string='Closed by', index=True)
    closed_date = fields.Datetime('Closed on', readonly=True)

    # karma calculation and access
    karma_accept = fields.Integer('Convert comment to answer', compute='_get_post_karma_rights')
    karma_edit = fields.Integer('Karma to edit', compute='_get_post_karma_rights')
    karma_close = fields.Integer('Karma to close', compute='_get_post_karma_rights')
    karma_unlink = fields.Integer('Karma to unlink', compute='_get_post_karma_rights')
    karma_comment = fields.Integer('Karma to comment', compute='_get_post_karma_rights')
    karma_comment_convert = fields.Integer('Karma to convert comment to answer', compute='_get_post_karma_rights')
    karma_flag = fields.Integer('Flag a post as offensive', compute='_get_post_karma_rights')
    can_ask = fields.Boolean('Can Ask', compute='_get_post_karma_rights')
    can_answer = fields.Boolean('Can Answer', compute='_get_post_karma_rights')
    can_accept = fields.Boolean('Can Accept', compute='_get_post_karma_rights')
    can_edit = fields.Boolean('Can Edit', compute='_get_post_karma_rights')
    can_close = fields.Boolean('Can Close', compute='_get_post_karma_rights')
    can_unlink = fields.Boolean('Can Unlink', compute='_get_post_karma_rights')
    can_upvote = fields.Boolean('Can Upvote', compute='_get_post_karma_rights')
    can_downvote = fields.Boolean('Can Downvote', compute='_get_post_karma_rights')
    can_comment = fields.Boolean('Can Comment', compute='_get_post_karma_rights')
    can_comment_convert = fields.Boolean('Can Convert to Comment', compute='_get_post_karma_rights')
    can_view = fields.Boolean('Can View', compute='_get_post_karma_rights', search='_search_can_view')
    can_display_biography = fields.Boolean("Is the author's biography visible from his post", compute='_get_post_karma_rights')
    can_post = fields.Boolean('Can Automatically be Validated', compute='_get_post_karma_rights')
    can_flag = fields.Boolean('Can Flag', compute='_get_post_karma_rights')
    can_moderate = fields.Boolean('Can Moderate', compute='_get_post_karma_rights')

    website_id = fields.Many2one('website', string="Website")

    def _search_can_view(self, operator, value):
        if operator not in ('=', '!=', '<>'):
            raise ValueError('Invalid operator: %s' % (operator,))

        if not value:
            operator = operator == "=" and '!=' or '='
            value = True

        if self._uid == SUPERUSER_ID:
            return [(1, '=', 1)]

        user = self.env['res.users'].browse(self._uid)
        req = """
            SELECT p.id
            FROM forum_post p
                   LEFT JOIN res_users u ON p.create_uid = u.id
                   LEFT JOIN forum_forum f ON p.forum_id = f.id
            WHERE
                (p.create_uid = %s and f.karma_close_own <= %s)
                or (p.create_uid != %s and f.karma_close_all <= %s)
                or (
                    u.karma > 0
                    and (p.active or p.create_uid = %s)
                )
        """

        op = operator == "=" and "inselect" or "not inselect"

        # don't use param named because orm will add other param (test_active, ...)
        return [('id', op, (req, (user.id, user.karma, user.id, user.karma, user.id)))]

    @api.one
    @api.depends('content')
    def _get_plain_content(self):
        self.plain_content = tools.html2plaintext(self.content)[0:500] if self.content else False

    @api.one
    @api.depends('vote_count', 'forum_id.relevancy_post_vote', 'forum_id.relevancy_time_decay')
    def _compute_relevancy(self):
        if self.create_date:
            days = (datetime.today() - datetime.strptime(self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days
            self.relevancy = math.copysign(1, self.vote_count) * (abs(self.vote_count - 1) ** self.forum_id.relevancy_post_vote / (days + 2) ** self.forum_id.relevancy_time_decay)
        else:
            self.relevancy = 0

    @api.multi
    def _get_user_vote(self):
        votes = self.env['forum.post.vote'].search_read([('post_id', 'in', self._ids), ('user_id', '=', self._uid)], ['vote', 'post_id'])
        mapped_vote = dict([(v['post_id'][0], v['vote']) for v in votes])
        for vote in self:
            vote.user_vote = mapped_vote.get(vote.id, 0)

    @api.multi
    @api.depends('vote_ids.vote')
    def _get_vote_count(self):
        read_group_res = self.env['forum.post.vote'].read_group([('post_id', 'in', self._ids)], ['post_id', 'vote'], ['post_id', 'vote'], lazy=False)
        result = dict.fromkeys(self._ids, 0)
        for data in read_group_res:
            result[data['post_id'][0]] += data['__count'] * int(data['vote'])
        for post in self:
            post.vote_count = result[post.id]

    @api.one
    def _get_user_favourite(self):
        self.user_favourite = self._uid in self.favourite_ids.ids

    @api.one
    @api.depends('favourite_ids')
    def _get_favorite_count(self):
        self.favourite_count = len(self.favourite_ids)

    @api.one
    @api.depends('create_uid', 'parent_id')
    def _is_self_reply(self):
        self.self_reply = self.parent_id.create_uid.id == self._uid

    @api.one
    @api.depends('child_ids.create_uid', 'website_message_ids')
    def _get_child_count(self):
        def process(node):
            total = len(node.website_message_ids) + len(node.child_ids)
            for child in node.child_ids:
                total += process(child)
            return total
        self.child_count = process(self)

    @api.one
    def _get_uid_has_answered(self):
        self.uid_has_answered = any(answer.create_uid.id == self._uid for answer in self.child_ids)

    @api.one
    @api.depends('child_ids.is_correct')
    def _get_has_validated_answer(self):
        self.has_validated_answer = any(answer.is_correct for answer in self.child_ids)


    @api.multi
    def _get_post_karma_rights(self):
        user = self.env.user
        is_admin = user.id == SUPERUSER_ID
        # sudoed recordset instead of individual posts so values can be
        # prefetched in bulk
        for post, post_sudo in pycompat.izip(self, self.sudo()):
            is_creator = post.create_uid == user

            post.karma_accept = post.forum_id.karma_answer_accept_own if post.parent_id.create_uid == user else post.forum_id.karma_answer_accept_all
            post.karma_edit = post.forum_id.karma_edit_own if is_creator else post.forum_id.karma_edit_all
            post.karma_close = post.forum_id.karma_close_own if is_creator else post.forum_id.karma_close_all
            post.karma_unlink = post.forum_id.karma_unlink_own if is_creator else post.forum_id.karma_unlink_all
            post.karma_comment = post.forum_id.karma_comment_own if is_creator else post.forum_id.karma_comment_all
            post.karma_comment_convert = post.forum_id.karma_comment_convert_own if is_creator else post.forum_id.karma_comment_convert_all

            post.can_ask = is_admin or user.karma >= post.forum_id.karma_ask
            post.can_answer = is_admin or user.karma >= post.forum_id.karma_answer
            post.can_accept = is_admin or user.karma >= post.karma_accept
            post.can_edit = is_admin or user.karma >= post.karma_edit
            post.can_close = is_admin or user.karma >= post.karma_close
            post.can_unlink = is_admin or user.karma >= post.karma_unlink
            post.can_upvote = is_admin or user.karma >= post.forum_id.karma_upvote
            post.can_downvote = is_admin or user.karma >= post.forum_id.karma_downvote
            post.can_comment = is_admin or user.karma >= post.karma_comment
            post.can_comment_convert = is_admin or user.karma >= post.karma_comment_convert
            post.can_view = is_admin or user.karma >= post.karma_close or (post_sudo.create_uid.karma > 0 and (post_sudo.active or post_sudo.create_uid == user))
            post.can_display_biography = is_admin or post_sudo.create_uid.karma >= post.forum_id.karma_user_bio
            post.can_post = is_admin or user.karma >= post.forum_id.karma_post
            post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag
            post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate

    @api.one
    @api.constrains('post_type', 'forum_id')
    def _check_post_type(self):
        if (self.post_type == 'question' and not self.forum_id.allow_question) \
                or (self.post_type == 'discussion' and not self.forum_id.allow_discussion) \
                or (self.post_type == 'link' and not self.forum_id.allow_link):
            raise ValidationError(_('This forum does not allow %s') % self.post_type)

    def _update_content(self, content, forum_id):
        forum = self.env['forum.forum'].browse(forum_id)
        if content and self.env.user.karma < forum.karma_dofollow:
            for match in re.findall(r'<a\s.*href=".*?">', content):
                match = re.escape(match)  # replace parenthesis or special char in regex
                content = re.sub(match, match[:3] + 'rel="nofollow" ' + match[3:], content)

        if self.env.user.karma <= forum.karma_editor:
            filter_regexp = r'(<img.*?>)|(<a[^>]*?href[^>]*?>)|(<[a-z|A-Z]+[^>]*style\s*=\s*[\'"][^\'"]*\s*background[^:]*:[^url;]*url)'
            content_match = re.search(filter_regexp, content, re.I)
            if content_match:
                raise KarmaError('User karma not sufficient to post an image or link.')
        return content

    @api.model
    def create(self, vals):
        if 'content' in vals and vals.get('forum_id'):
            vals['content'] = self._update_content(vals['content'], vals['forum_id'])

        post = super(Post, self.with_context(mail_create_nolog=True)).create(vals)
        # deleted or closed questions
        if post.parent_id and (post.parent_id.state == 'close' or post.parent_id.active is False):
            raise UserError(_('Posting answer on a [Deleted] or [Closed] question is not possible'))
        # karma-based access
        if not post.parent_id and not post.can_ask:
            raise KarmaError('Not enough karma to create a new question')
        elif post.parent_id and not post.can_answer:
            raise KarmaError('Not enough karma to answer to a question')
        if not post.parent_id and not post.can_post:
            post.state = 'pending'

        # add karma for posting new questions
        if not post.parent_id and post.state == 'active':
            self.env.user.sudo().add_karma(post.forum_id.karma_gen_question_new)
        post.post_notification()
        return post

    @api.model
    def check_mail_message_access(self, res_ids, operation, model_name=None):
        if operation in ('write', 'unlink') and (not model_name or model_name == 'forum.post'):
            # Make sure only author or moderator can edit/delete messages
            if any(not post.can_edit for post in self.browse(res_ids)):
                raise KarmaError('Not enough karma to edit a post.')
        return super(Post, self).check_mail_message_access(res_ids, operation, model_name=model_name)

    @api.multi
    @api.depends('name', 'post_type')
    def name_get(self):
        result = []
        for post in self:
            if post.post_type == 'discussion' and post.parent_id and not post.name:
                result.append((post.id, '%s (%s)' % (post.parent_id.name, post.id)))
            else:
                result.append((post.id, '%s' % (post.name)))
        return result

    @api.multi
    def write(self, vals):
        if 'content' in vals:
            vals['content'] = self._update_content(vals['content'], self.forum_id.id)
        if 'state' in vals:
            if vals['state'] in ['active', 'close'] and any(not post.can_close for post in self):
                raise KarmaError('Not enough karma to close or reopen a post.')
        if 'active' in vals:
            if any(not post.can_unlink for post in self):
                raise KarmaError('Not enough karma to delete or reactivate a post')
        if 'is_correct' in vals:
            if any(not post.can_accept for post in self):
                raise KarmaError('Not enough karma to accept or refuse an answer')
            # update karma except for self-acceptance
            mult = 1 if vals['is_correct'] else -1
            for post in self:
                if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid:
                    post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * mult)
                    self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accept * mult)
        if 'tag_ids' in vals:
            tag_ids = set(tag.get('id') for tag in self.resolve_2many_commands('tag_ids', vals['tag_ids']))
            if any(set(post.tag_ids) != tag_ids for post in self) and any(self.env.user.karma < post.forum_id.karma_edit_retag for post in self):
                raise KarmaError(_('Not enough karma to retag.'))
        if any(key not in ['state', 'active', 'is_correct', 'closed_uid', 'closed_date', 'closed_reason_id', 'tag_ids'] for key in vals) and any(not post.can_edit for post in self):
            raise KarmaError('Not enough karma to edit a post.')

        res = super(Post, self).write(vals)

        # if post content modify, notify followers
        if 'content' in vals or 'name' in vals:
            for post in self:
                if post.parent_id:
                    body, subtype = _('Answer Edited'), 'website_forum.mt_answer_edit'
                    obj_id = post.parent_id
                else:
                    body, subtype = _('Question Edited'), 'website_forum.mt_question_edit'
                    obj_id = post
                obj_id.message_post(body=body, subtype=subtype)
        if 'active' in vals:
            answers = self.env['forum.post'].with_context(active_test=False).search([('parent_id', 'in', self.ids)])
            if answers:
                answers.write({'active': vals['active']})
        return res

    @api.multi
    def post_notification(self):
        for post in self:
            tag_partners = post.tag_ids.mapped('message_partner_ids')
            tag_channels = post.tag_ids.mapped('message_channel_ids')

            if post.state == 'active' and post.parent_id:
                post.parent_id.message_post_with_view(
                    'website_forum.forum_post_template_new_answer',
                    subject=_('Re: %s') % post.parent_id.name,
                    partner_ids=[(4, p.id) for p in tag_partners],
                    channel_ids=[(4, c.id) for c in tag_channels],
                    subtype_id=self.env['ir.model.data'].xmlid_to_res_id('website_forum.mt_answer_new'))
            elif post.state == 'active' and not post.parent_id:
                post.message_post_with_view(
                    'website_forum.forum_post_template_new_question',
                    subject=post.name,
                    partner_ids=[(4, p.id) for p in tag_partners],
                    channel_ids=[(4, c.id) for c in tag_channels],
                    subtype_id=self.env['ir.model.data'].xmlid_to_res_id('website_forum.mt_question_new'))
            elif post.state == 'pending' and not post.parent_id:
                # TDE FIXME: in master, you should probably use a subtype;
                # however here we remove subtype but set partner_ids
                partners = post.sudo().message_partner_ids | tag_partners
                partners = partners.filtered(lambda partner: partner.user_ids and any(user.karma >= post.forum_id.karma_moderate for user in partner.user_ids))

                post.message_post_with_view(
                    'website_forum.forum_post_template_validation',
                    subject=post.name,
                    partner_ids=partners.ids,
                    subtype_id=self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'))
        return True

    @api.multi
    def reopen(self):
        if any(post.parent_id or post.state != 'close' for post in self):
            return False

        reason_offensive = self.env.ref('website_forum.reason_7')
        reason_spam = self.env.ref('website_forum.reason_8')
        for post in self:
            if post.closed_reason_id in (reason_offensive, reason_spam):
                _logger.info('Upvoting user <%s>, reopening spam/offensive question',
                             post.create_uid)

                karma = post.forum_id.karma_gen_answer_flagged
                if post.closed_reason_id == reason_spam:
                    # If first post, increase the karma to add
                    count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)])
                    if count_post == 1:
                        karma *= 10
                post.create_uid.sudo().add_karma(karma * -1)

        self.sudo().write({'state': 'active'})

    @api.multi
    def close(self, reason_id):
        if any(post.parent_id for post in self):
            return False

        reason_offensive = self.env.ref('website_forum.reason_7').id
        reason_spam = self.env.ref('website_forum.reason_8').id
        if reason_id in (reason_offensive, reason_spam):
            for post in self:
                _logger.info('Downvoting user <%s> for posting spam/offensive contents',
                             post.create_uid)
                karma = post.forum_id.karma_gen_answer_flagged
                if reason_id == reason_spam:
                    # If first post, increase the karma to remove
                    count_post = post.search_count([('parent_id', '=', False), ('forum_id', '=', post.forum_id.id), ('create_uid', '=', post.create_uid.id)])
                    if count_post == 1:
                        karma *= 10
                post.create_uid.sudo().add_karma(karma)

        self.write({
            'state': 'close',
            'closed_uid': self._uid,
            'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT),
            'closed_reason_id': reason_id,
        })
        return True

    @api.one
    def validate(self):
        if not self.can_moderate:
            raise KarmaError('Not enough karma to validate a post')

        # if state == pending, no karma previously added for the new question
        if self.state == 'pending':
            self.create_uid.sudo().add_karma(self.forum_id.karma_gen_question_new)

        self.write({
            'state': 'active',
            'active': True,
            'moderator_id': self.env.user.id,
        })
        self.post_notification()
        return True

    @api.one
    def refuse(self):
        if not self.can_moderate:
            raise KarmaError('Not enough karma to refuse a post')

        self.moderator_id = self.env.user
        return True

    @api.one
    def flag(self):
        if not self.can_flag:
            raise KarmaError('Not enough karma to flag a post')

        if(self.state == 'flagged'):
            return {'error': 'post_already_flagged'}
        elif(self.state == 'active'):
            self.write({
                'state': 'flagged',
                'flag_user_id': self.env.user.id,
            })
            return self.can_moderate and {'success': 'post_flagged_moderator'} or {'success': 'post_flagged_non_moderator'}
        else:
            return {'error': 'post_non_flaggable'}

    @api.one
    def mark_as_offensive(self, reason_id):
        if not self.can_moderate:
            raise KarmaError('Not enough karma to mark a post as offensive')

        # remove some karma
        _logger.info('Downvoting user <%s> for posting spam/offensive contents', self.create_uid)
        self.create_uid.sudo().add_karma(self.forum_id.karma_gen_answer_flagged)

        self.write({
            'state': 'offensive',
            'moderator_id': self.env.user.id,
            'closed_date': datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT),
            'closed_reason_id': reason_id,
            'active': False,
        })
        return True

    @api.multi
    def unlink(self):
        if any(not post.can_unlink for post in self):
            raise KarmaError('Not enough karma to unlink a post')
        # if unlinking an answer with accepted answer: remove provided karma
        for post in self:
            if post.is_correct:
                post.create_uid.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1)
                self.env.user.sudo().add_karma(post.forum_id.karma_gen_answer_accepted * -1)
        return super(Post, self).unlink()

    @api.multi
    def bump(self):
        """ Bump a question: trigger a write_date by writing on a dummy bump_date
        field. One cannot bump a question more than once every 10 days. """
        self.ensure_one()
        if self.forum_id.allow_bump and not self.child_ids and (datetime.today() - datetime.strptime(self.write_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days > 9:
            # write through super to bypass karma; sudo to allow public user to bump any post
            return self.sudo().write({'bump_date': fields.Datetime.now()})
        return False

    @api.multi
    def vote(self, upvote=True):
        Vote = self.env['forum.post.vote']
        vote_ids = Vote.search([('post_id', 'in', self._ids), ('user_id', '=', self._uid)])
        new_vote = '1' if upvote else '-1'
        voted_forum_ids = set()
        if vote_ids:
            for vote in vote_ids:
                if upvote:
                    new_vote = '0' if vote.vote == '-1' else '1'
                else:
                    new_vote = '0' if vote.vote == '1' else '-1'
                vote.vote = new_vote
                voted_forum_ids.add(vote.post_id.id)
        for post_id in set(self._ids) - voted_forum_ids:
            for post_id in self._ids:
                Vote.create({'post_id': post_id, 'vote': new_vote})
        return {'vote_count': self.vote_count, 'user_vote': new_vote}

    @api.multi
    def convert_answer_to_comment(self):
        """ Tools to convert an answer (forum.post) to a comment (mail.message).
        The original post is unlinked and a new comment is posted on the question
        using the post create_uid as the comment's author. """
        self.ensure_one()
        if not self.parent_id:
            return self.env['mail.message']

        # karma-based action check: use the post field that computed own/all value
        if not self.can_comment_convert:
            raise KarmaError('Not enough karma to convert an answer to a comment')

        # post the message
        question = self.parent_id
        values = {
            'author_id': self.sudo().create_uid.partner_id.id,  # use sudo here because of access to res.users model
            'body': tools.html_sanitize(self.content, sanitize_attributes=True, strip_style=True, strip_classes=True),
            'message_type': 'comment',
            'subtype': 'mail.mt_comment',
            'date': self.create_date,
        }
        new_message = question.with_context(mail_create_nosubscribe=True).message_post(**values)

        # unlink the original answer, using SUPERUSER_ID to avoid karma issues
        self.sudo().unlink()

        return new_message

    @api.model
    def convert_comment_to_answer(self, message_id, default=None):
        """ Tool to convert a comment (mail.message) into an answer (forum.post).
        The original comment is unlinked and a new answer from the comment's author
        is created. Nothing is done if the comment's author already answered the
        question. """
        comment = self.env['mail.message'].sudo().browse(message_id)
        post = self.browse(comment.res_id)
        if not comment.author_id or not comment.author_id.user_ids:  # only comment posted by users can be converted
            return False

        # karma-based action check: must check the message's author to know if own / all
        karma_convert = comment.author_id.id == self.env.user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all
        can_convert = self.env.user.karma >= karma_convert
        if not can_convert:
            raise KarmaError('Not enough karma to convert a comment to an answer')

        # check the message's author has not already an answer
        question = post.parent_id if post.parent_id else post
        post_create_uid = comment.author_id.user_ids[0]
        if any(answer.create_uid.id == post_create_uid.id for answer in question.child_ids):
            return False

        # create the new post
        post_values = {
            'forum_id': question.forum_id.id,
            'content': comment.body,
            'parent_id': question.id,
        }
        # done with the author user to have create_uid correctly set
        new_post = self.sudo(post_create_uid.id).create(post_values)

        # delete comment
        comment.unlink()

        return new_post

    @api.one
    def unlink_comment(self, message_id):
        user = self.env.user
        comment = self.env['mail.message'].sudo().browse(message_id)
        if not comment.model == 'forum.post' or not comment.res_id == self.id:
            return False
        # karma-based action check: must check the message's author to know if own or all
        karma_unlink = comment.author_id.id == user.partner_id.id and self.forum_id.karma_comment_unlink_own or self.forum_id.karma_comment_unlink_all
        can_unlink = user.karma >= karma_unlink
        if not can_unlink:
            raise KarmaError('Not enough karma to unlink a comment')
        return comment.unlink()

    @api.multi
    def set_viewed(self):
        self._cr.execute("""UPDATE forum_post SET views = views+1 WHERE id IN %s""", (self._ids,))
        return True

    @api.multi
    def get_access_action(self, access_uid=None):
        """ Instead of the classic form view, redirect to the post on the website directly """
        self.ensure_one()
        return {
            'type': 'ir.actions.act_url',
            'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id),
            'target': 'self',
            'target_type': 'public',
            'res_id': self.id,
        }

    @api.multi
    def _notification_recipients(self, message, groups):
        groups = super(Post, self)._notification_recipients(message, groups)

        for group_name, group_method, group_data in groups:
            group_data['has_button_access'] = True

        return groups

    @api.multi
    @api.returns('self', lambda value: value.id)
    def message_post(self, message_type='notification', subtype=None, **kwargs):
        question_followers = self.env['res.partner']
        if self.ids and message_type == 'comment':  # user comments have a restriction on karma
            # add followers of comments on the parent post
            if self.parent_id:
                partner_ids = kwargs.get('partner_ids', [])
                comment_subtype = self.sudo().env.ref('mail.mt_comment')
                question_followers = self.env['mail.followers'].sudo().search([
                    ('res_model', '=', self._name),
                    ('res_id', '=', self.parent_id.id),
                    ('partner_id', '!=', False),
                ]).filtered(lambda fol: comment_subtype in fol.subtype_ids).mapped('partner_id')
                partner_ids += [(4, partner.id) for partner in question_followers]
                kwargs['partner_ids'] = partner_ids

            self.ensure_one()
            if not self.can_comment:
                raise KarmaError('Not enough karma to comment')
            if not kwargs.get('record_name') and self.parent_id:
                kwargs['record_name'] = self.parent_id.name
        return super(Post, self).message_post(message_type=message_type, subtype=subtype, **kwargs)

    @api.multi
    def message_get_message_notify_values(self, message, message_values):
        """ Override to avoid keeping all notified recipients of a comment.
        We avoid tracking needaction on post comments. Only emails should be
        sufficient. """
        if message.message_type == 'comment':
            return {
                'needaction_partner_ids': [],
                'partner_ids': [],
            }
        return {}
示例#24
0
文件: partner.py 项目: ly2ly/flectra
class ResPartner(models.Model):
    _name = 'res.partner'
    _inherit = 'res.partner'

    @api.multi
    def _credit_debit_get(self):
        tables, where_clause, where_params = self.env[
            'account.move.line']._query_get()
        where_params = [tuple(self.ids)] + where_params
        if where_clause:
            where_clause = 'AND ' + where_clause
        self._cr.execute(
            """SELECT account_move_line.partner_id, act.type, SUM(account_move_line.amount_residual)
                      FROM account_move_line
                      LEFT JOIN account_account a ON (account_move_line.account_id=a.id)
                      LEFT JOIN account_account_type act ON (a.user_type_id=act.id)
                      WHERE act.type IN ('receivable','payable')
                      AND account_move_line.partner_id IN %s
                      AND account_move_line.reconciled IS FALSE
                      """ + where_clause + """
                      GROUP BY account_move_line.partner_id, act.type
                      """, where_params)
        for pid, type, val in self._cr.fetchall():
            partner = self.browse(pid)
            if type == 'receivable':
                partner.credit = val
            elif type == 'payable':
                partner.debit = -val

    @api.multi
    def _asset_difference_search(self, account_type, operator, operand):
        if operator not in ('<', '=', '>', '>=', '<='):
            return []
        if type(operand) not in (float, int):
            return []
        sign = 1
        if account_type == 'payable':
            sign = -1
        res = self._cr.execute(
            '''
            SELECT partner.id
            FROM res_partner partner
            LEFT JOIN account_move_line aml ON aml.partner_id = partner.id
            RIGHT JOIN account_account acc ON aml.account_id = acc.id
            WHERE acc.internal_type = %s
              AND NOT acc.deprecated
            GROUP BY partner.id
            HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator +
            ''' %s''', (account_type, sign, operand))
        res = self._cr.fetchall()
        if not res:
            return [('id', '=', '0')]
        return [('id', 'in', [r[0] for r in res])]

    @api.model
    def _credit_search(self, operator, operand):
        return self._asset_difference_search('receivable', operator, operand)

    @api.model
    def _debit_search(self, operator, operand):
        return self._asset_difference_search('payable', operator, operand)

    @api.multi
    def _invoice_total(self):
        account_invoice_report = self.env['account.invoice.report']
        if not self.ids:
            self.total_invoiced = 0.0
            return True

        user_currency_id = self.env.user.company_id.currency_id.id
        all_partners_and_children = {}
        all_partner_ids = []
        for partner in self:
            # price_total is in the company currency
            all_partners_and_children[partner] = self.with_context(
                active_test=False).search([('id', 'child_of', partner.id)]).ids
            all_partner_ids += all_partners_and_children[partner]

        # searching account.invoice.report via the orm is comparatively expensive
        # (generates queries "id in []" forcing to build the full table).
        # In simple cases where all invoices are in the same currency than the user's company
        # access directly these elements

        # generate where clause to include multicompany rules
        where_query = account_invoice_report._where_calc([
            ('partner_id', 'in', all_partner_ids),
            ('state', 'not in', ['draft', 'cancel']),
            ('type', 'in', ('out_invoice', 'out_refund'))
        ])
        account_invoice_report._apply_ir_rules(where_query, 'read')
        from_clause, where_clause, where_clause_params = where_query.get_sql()

        # price_total is in the company currency
        query = """
                  SELECT SUM(price_total) as total, partner_id
                    FROM account_invoice_report account_invoice_report
                   WHERE %s
                   GROUP BY partner_id
                """ % where_clause
        self.env.cr.execute(query, where_clause_params)
        price_totals = self.env.cr.dictfetchall()
        for partner, child_ids in all_partners_and_children.items():
            partner.total_invoiced = sum(price['total']
                                         for price in price_totals
                                         if price['partner_id'] in child_ids)

    @api.multi
    def _journal_item_count(self):
        for partner in self:
            partner.journal_item_count = self.env[
                'account.move.line'].search_count([('partner_id', '=',
                                                    partner.id)])
            partner.contracts_count = self.env[
                'account.analytic.account'].search_count([('partner_id', '=',
                                                           partner.id)])

    def get_followup_lines_domain(self,
                                  date,
                                  overdue_only=False,
                                  only_unblocked=False):
        domain = [('reconciled', '=', False),
                  ('account_id.deprecated', '=', False),
                  ('account_id.internal_type', '=', 'receivable'), '|',
                  ('debit', '!=', 0), ('credit', '!=', 0),
                  ('company_id', '=', self.env.user.company_id.id)]
        if only_unblocked:
            domain += [('blocked', '=', False)]
        if self.ids:
            if 'exclude_given_ids' in self._context:
                domain += [('partner_id', 'not in', self.ids)]
            else:
                domain += [('partner_id', 'in', self.ids)]
        #adding the overdue lines
        overdue_domain = [
            '|', '&', ('date_maturity', '!=', False),
            ('date_maturity', '<', date), '&', ('date_maturity', '=', False),
            ('date', '<', date)
        ]
        if overdue_only:
            domain += overdue_domain
        return domain

    @api.one
    def _compute_has_unreconciled_entries(self):
        # Avoid useless work if has_unreconciled_entries is not relevant for this partner
        if not self.active or not self.is_company and self.parent_id:
            return
        self.env.cr.execute(
            """ SELECT 1 FROM(
                    SELECT
                        p.last_time_entries_checked AS last_time_entries_checked,
                        MAX(l.write_date) AS max_date
                    FROM
                        account_move_line l
                        RIGHT JOIN account_account a ON (a.id = l.account_id)
                        RIGHT JOIN res_partner p ON (l.partner_id = p.id)
                    WHERE
                        p.id = %s
                        AND EXISTS (
                            SELECT 1
                            FROM account_move_line l
                            WHERE l.account_id = a.id
                            AND l.partner_id = p.id
                            AND l.amount_residual > 0
                        )
                        AND EXISTS (
                            SELECT 1
                            FROM account_move_line l
                            WHERE l.account_id = a.id
                            AND l.partner_id = p.id
                            AND l.amount_residual < 0
                        )
                    GROUP BY p.last_time_entries_checked
                ) as s
                WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked)
            """, (self.id, ))
        self.has_unreconciled_entries = self.env.cr.rowcount == 1

    @api.multi
    def mark_as_reconciled(self):
        self.env['account.partial.reconcile'].check_access_rights('write')
        return self.sudo().with_context(
            company_id=self.env.user.company_id.id).write({
                'last_time_entries_checked':
                time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
            })

    @api.one
    def _get_company_currency(self):
        if self.company_id:
            self.currency_id = self.sudo().company_id.currency_id
        else:
            self.currency_id = self.env.user.company_id.currency_id

    credit = fields.Monetary(compute='_credit_debit_get',
                             search=_credit_search,
                             string='Total Receivable',
                             help="Total amount this customer owes you.")
    debit = fields.Monetary(
        compute='_credit_debit_get',
        search=_debit_search,
        string='Total Payable',
        help="Total amount you have to pay to this vendor.")
    debit_limit = fields.Monetary('Payable Limit')
    total_invoiced = fields.Monetary(compute='_invoice_total',
                                     string="Total Invoiced",
                                     groups='account.group_account_invoice')
    currency_id = fields.Many2one(
        'res.currency',
        compute='_get_company_currency',
        readonly=True,
        string="Currency",
        help='Utility field to express amount currency')

    contracts_count = fields.Integer(compute='_journal_item_count',
                                     string="Contracts",
                                     type='integer')
    journal_item_count = fields.Integer(compute='_journal_item_count',
                                        string="Journal Items",
                                        type="integer")
    property_account_payable_id = fields.Many2one(
        'account.account',
        company_dependent=True,
        string="Account Payable",
        oldname="property_account_payable",
        domain=
        "[('internal_type', '=', 'payable'), ('deprecated', '=', False)]",
        help=
        "This account will be used instead of the default one as the payable account for the current partner",
        required=True)
    property_account_receivable_id = fields.Many2one(
        'account.account',
        company_dependent=True,
        string="Account Receivable",
        oldname="property_account_receivable",
        domain=
        "[('internal_type', '=', 'receivable'), ('deprecated', '=', False)]",
        help=
        "This account will be used instead of the default one as the receivable account for the current partner",
        required=True)
    property_account_position_id = fields.Many2one(
        'account.fiscal.position',
        company_dependent=True,
        string="Fiscal Position",
        help=
        "The fiscal position will determine taxes and accounts used for the partner.",
        oldname="property_account_position")
    property_payment_term_id = fields.Many2one(
        'account.payment.term',
        company_dependent=True,
        string='Customer Payment Terms',
        help=
        "This payment term will be used instead of the default one for sales orders and customer invoices",
        oldname="property_payment_term")
    property_supplier_payment_term_id = fields.Many2one(
        'account.payment.term',
        company_dependent=True,
        string='Vendor Payment Terms',
        help=
        "This payment term will be used instead of the default one for purchase orders and vendor bills",
        oldname="property_supplier_payment_term")
    ref_company_ids = fields.One2many(
        'res.company',
        'partner_id',
        string='Companies that refers to partner',
        oldname="ref_companies")
    has_unreconciled_entries = fields.Boolean(
        compute='_compute_has_unreconciled_entries',
        help=
        "The partner has at least one unreconciled debit and credit since last time the invoices & payments matching was performed."
    )
    last_time_entries_checked = fields.Datetime(
        oldname='last_reconciliation_date',
        string='Latest Invoices & Payments Matching Date',
        readonly=True,
        copy=False,
        help=
        'Last time the invoices & payments matching was performed for this partner. '
        'It is set either if there\'s not at least an unreconciled debit and an unreconciled credit '
        'or if you click the "Done" button.')
    invoice_ids = fields.One2many('account.invoice',
                                  'partner_id',
                                  string='Invoices',
                                  readonly=True,
                                  copy=False)
    contract_ids = fields.One2many('account.analytic.account',
                                   'partner_id',
                                   string='Contracts',
                                   readonly=True)
    bank_account_count = fields.Integer(compute='_compute_bank_count',
                                        string="Bank")
    trust = fields.Selection([('good', 'Good Debtor'),
                              ('normal', 'Normal Debtor'),
                              ('bad', 'Bad Debtor')],
                             string='Degree of trust you have in this debtor',
                             default='normal',
                             company_dependent=True)
    invoice_warn = fields.Selection(WARNING_MESSAGE,
                                    'Invoice',
                                    help=WARNING_HELP,
                                    required=True,
                                    default="no-message")
    invoice_warn_msg = fields.Text('Message for Invoice')

    @api.multi
    def _compute_bank_count(self):
        bank_data = self.env['res.partner.bank'].read_group(
            [('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id'])
        mapped_data = dict([(bank['partner_id'][0], bank['partner_id_count'])
                            for bank in bank_data])
        for partner in self:
            partner.bank_account_count = mapped_data.get(partner.id, 0)

    def _find_accounting_partner(self, partner):
        ''' Find the partner for which the accounting entries will be created '''
        return partner.commercial_partner_id

    @api.model
    def _commercial_fields(self):
        return super(ResPartner, self)._commercial_fields() + \
            ['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id',
             'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked']

    @api.multi
    def action_view_partner_invoices(self):
        self.ensure_one()
        action = self.env.ref(
            'account.action_invoice_refund_out_tree').read()[0]
        action['domain'] = literal_eval(action['domain'])
        action['domain'].append(('partner_id', 'child_of', self.id))
        return action
示例#25
0
class Route(models.Model):
    _name = 'stock.location.route'
    _description = "Inventory Routes"
    _order = 'sequence'

    name = fields.Char('Route Name', required=True, translate=True)
    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "If the active field is set to False, it will allow you to hide the route without removing it."
    )
    sequence = fields.Integer('Sequence', default=0)
    pull_ids = fields.One2many('procurement.rule', 'route_id', 'Procurement Rules', copy=True,
        help="The demand represented by a procurement from e.g. a sale order, a reordering rule, another move, needs to be solved by applying a procurement rule. Depending on the action on the procurement rule,"\
        "this triggers a purchase order, manufacturing order or another move. This way we create chains in the reverse order from the endpoint with the original demand to the starting point. "\
        "That way, it is always known where we need to go and that is why they are preferred over push rules.")
    push_ids = fields.One2many(
        'stock.location.path',
        'route_id',
        'Push Rules',
        copy=True,
        help=
        "When a move is foreseen to a location, the push rule will automatically create a move to a next location after. This is mainly only needed when creating manual operations e.g. 2/3 step manual purchase order or 2/3 step finished product manual manufacturing order. In other cases, it is important to use pull rules where you know where you are going based on a demand."
    )
    product_selectable = fields.Boolean(
        'Applicable on Product',
        default=True,
        help=
        "When checked, the route will be selectable in the Inventory tab of the Product form.  It will take priority over the Warehouse route. "
    )
    product_categ_selectable = fields.Boolean(
        'Applicable on Product Category',
        help=
        "When checked, the route will be selectable on the Product Category.  It will take priority over the Warehouse route. "
    )
    warehouse_selectable = fields.Boolean(
        'Applicable on Warehouse',
        help=
        "When a warehouse is selected for this route, this route should be seen as the default route when products pass through this warehouse.  This behaviour can be overridden by the routes on the Product/Product Categories or by the Preferred Routes on the Procurement"
    )
    supplied_wh_id = fields.Many2one('stock.warehouse', 'Supplied Warehouse')
    supplier_wh_id = fields.Many2one('stock.warehouse', 'Supplying Warehouse')
    company_id = fields.Many2one(
        'res.company',
        'Company',
        default=lambda self: self.env['res.company']._company_default_get(
            'stock.location.route'),
        index=True,
        help=
        'Leave this field empty if this route is shared between all companies')
    product_ids = fields.Many2many('product.template', 'stock_route_product',
                                   'route_id', 'product_id', 'Products')
    categ_ids = fields.Many2many('product.category',
                                 'stock_location_route_categ', 'route_id',
                                 'categ_id', 'Product Categories')
    warehouse_ids = fields.Many2many('stock.warehouse',
                                     'stock_route_warehouse', 'route_id',
                                     'warehouse_id', 'Warehouses')

    def write(self, values):
        '''when a route is deactivated, deactivate also its pull and push rules'''
        res = super(Route, self).write(values)
        if 'active' in values:
            self.mapped('push_ids').filtered(
                lambda path: path.active != values['active']).write(
                    {'active': values['active']})
            self.mapped('pull_ids').filtered(
                lambda rule: rule.active != values['active']).write(
                    {'active': values['active']})
        return res

    def view_product_ids(self):
        return {
            'name': _('Products'),
            'view_type': 'form',
            'view_mode': 'tree,form',
            'res_model': 'product.template',
            'type': 'ir.actions.act_window',
            'domain': [('route_ids', 'in', self.ids)],
        }

    def view_categ_ids(self):
        return {
            'name': _('Product Categories'),
            'view_type': 'form',
            'view_mode': 'tree,form',
            'res_model': 'product.category',
            'type': 'ir.actions.act_window',
            'domain': [('route_ids', 'in', self.ids)],
        }
示例#26
0
class Users(models.Model):
    _inherit = 'res.users'

    def __init__(self, pool, cr):
        init_res = super(Users, self).__init__(pool, cr)
        type(self).SELF_WRITEABLE_FIELDS = list(
            set(self.SELF_WRITEABLE_FIELDS + [
                'country_id', 'city', 'website', 'website_description',
                'website_published'
            ]))
        return init_res

    create_date = fields.Datetime('Create Date',
                                  readonly=True,
                                  copy=False,
                                  index=True)
    karma = fields.Integer('Karma', default=0)
    badge_ids = fields.One2many('gamification.badge.user',
                                'user_id',
                                string='Badges',
                                copy=False)
    gold_badge = fields.Integer('Gold badges count',
                                compute="_get_user_badge_level")
    silver_badge = fields.Integer('Silver badges count',
                                  compute="_get_user_badge_level")
    bronze_badge = fields.Integer('Bronze badges count',
                                  compute="_get_user_badge_level")
    forum_waiting_posts_count = fields.Integer(
        'Waiting post', compute="_get_user_waiting_post")

    @api.multi
    @api.depends('badge_ids')
    def _get_user_badge_level(self):
        """ Return total badge per level of users
        TDE CLEANME: shouldn't check type is forum ? """
        for user in self:
            user.gold_badge = 0
            user.silver_badge = 0
            user.bronze_badge = 0

        self.env.cr.execute(
            """
            SELECT bu.user_id, b.level, count(1)
            FROM gamification_badge_user bu, gamification_badge b
            WHERE bu.user_id IN %s
              AND bu.badge_id = b.id
              AND b.level IS NOT NULL
            GROUP BY bu.user_id, b.level
            ORDER BY bu.user_id;
        """, [tuple(self.ids)])

        for (user_id, level, count) in self.env.cr.fetchall():
            # levels are gold, silver, bronze but fields have _badge postfix
            self.browse(user_id)['{}_badge'.format(level)] = count

    @api.multi
    def _get_user_waiting_post(self):
        for user in self:
            Post = self.env['forum.post']
            domain = [('parent_id', '=', False), ('state', '=', 'pending'),
                      ('create_uid', '=', user.id)]
            user.forum_waiting_posts_count = Post.search_count(domain)

    @api.model
    def _generate_forum_token(self, user_id, email):
        """Return a token for email validation. This token is valid for the day
        and is a hash based on a (secret) uuid generated by the forum module,
        the user_id, the email and currently the day (to be updated if necessary). """
        forum_uuid = self.env['ir.config_parameter'].sudo().get_param(
            'website_forum.uuid')
        return hashlib.sha256((
            u'%s-%s-%s-%s' %
            (datetime.now().replace(hour=0, minute=0, second=0, microsecond=0),
             forum_uuid, user_id, email)).encode('utf-8')).hexdigest()

    @api.one
    def send_forum_validation_email(self, forum_id=None):
        if not self.email:
            return False
        token = self._generate_forum_token(self.id, self.email)
        activation_template = self.env.ref('website_forum.validation_email')
        if activation_template:
            params = {'token': token, 'id': self.id, 'email': self.email}
            if forum_id:
                params['forum_id'] = forum_id
            base_url = self.env['ir.config_parameter'].sudo().get_param(
                'web.base.url')
            token_url = base_url + '/forum/validate_email?%s' % urls.url_encode(
                params)
            activation_template.sudo().with_context(
                token_url=token_url).send_mail(self.id, force_send=True)
        return True

    @api.one
    def process_forum_validation_token(self,
                                       token,
                                       email,
                                       forum_id=None,
                                       context=None):
        validation_token = self._generate_forum_token(self.id, email)
        if token == validation_token and self.karma == 0:
            karma = 3
            forum = None
            if forum_id:
                forum = self.env['forum.forum'].browse(forum_id)
            else:
                forum_ids = self.env['forum.forum'].search([], limit=1)
                if forum_ids:
                    forum = forum_ids[0]
            if forum:
                # karma gained: karma to ask a question and have 2 downvotes
                karma = forum.karma_ask + (-2 *
                                           forum.karma_gen_question_downvote)
            return self.write({'karma': karma})
        return False

    @api.multi
    def add_karma(self, karma):
        for user in self:
            user.karma += karma
        return True

    # Wrapper for call_kw with inherits
    @api.multi
    def open_website_url(self):
        return self.mapped('partner_id').open_website_url()
示例#27
0
class SaleOrder(models.Model):
    _inherit = "sale.order"

    @api.model
    def _default_warehouse_id(self):
        company = self.env.user.company_id.id
        warehouse_ids = self.env['stock.warehouse'].search(
            [('company_id', '=', company)], limit=1)
        return warehouse_ids

    incoterm = fields.Many2one(
        'stock.incoterms',
        'Incoterms',
        help=
        "International Commercial Terms are a series of predefined commercial terms used in international transactions."
    )
    picking_policy = fields.Selection(
        [('direct', 'Deliver each product when available'),
         ('one', 'Deliver all products at once')],
        string='Shipping Policy',
        required=True,
        readonly=True,
        default='direct',
        states={
            'draft': [('readonly', False)],
            'sent': [('readonly', False)]
        })
    warehouse_id = fields.Many2one('stock.warehouse',
                                   string='Warehouse',
                                   required=True,
                                   readonly=True,
                                   states={
                                       'draft': [('readonly', False)],
                                       'sent': [('readonly', False)]
                                   },
                                   default=_default_warehouse_id)
    picking_ids = fields.One2many('stock.picking',
                                  'sale_id',
                                  string='Pickings')
    delivery_count = fields.Integer(string='Delivery Orders',
                                    compute='_compute_picking_ids')
    procurement_group_id = fields.Many2one('procurement.group',
                                           'Procurement Group',
                                           copy=False)

    @api.multi
    def action_confirm(self):
        result = super(SaleOrder, self).action_confirm()
        for order in self:
            order.order_line._action_launch_procurement_rule()
        return result

    @api.depends('picking_ids')
    def _compute_picking_ids(self):
        for order in self:
            order.delivery_count = len(order.picking_ids)

    @api.onchange('warehouse_id')
    def _onchange_warehouse_id(self):
        if self.warehouse_id.company_id:
            self.company_id = self.warehouse_id.company_id.id

    @api.multi
    def action_view_delivery(self):
        '''
        This function returns an action that display existing delivery orders
        of given sales order ids. It can either be a in a list or in a form
        view, if there is only one delivery order to show.
        '''
        action = self.env.ref('stock.action_picking_tree_all').read()[0]

        pickings = self.mapped('picking_ids')
        if len(pickings) > 1:
            action['domain'] = [('id', 'in', pickings.ids)]
        elif pickings:
            action['views'] = [(self.env.ref('stock.view_picking_form').id,
                                'form')]
            action['res_id'] = pickings.id
        return action

    @api.multi
    def action_cancel(self):
        self.mapped('picking_ids').action_cancel()
        return super(SaleOrder, self).action_cancel()

    @api.multi
    def _prepare_invoice(self):
        invoice_vals = super(SaleOrder, self)._prepare_invoice()
        invoice_vals['incoterms_id'] = self.incoterm.id or False
        return invoice_vals

    @api.model
    def _get_customer_lead(self, product_tmpl_id):
        super(SaleOrder, self)._get_customer_lead(product_tmpl_id)
        return product_tmpl_id.sale_delay
示例#28
0
class HrSalaryRule(models.Model):
    _name = 'hr.salary.rule'

    name = fields.Char(required=True, translate=True)
    code = fields.Char(required=True,
        help="The code of salary rules can be used as reference in computation of other rules. "
             "In that case, it is case sensitive.")
    sequence = fields.Integer(required=True, index=True, default=5,
        help='Use to arrange calculation sequence')
    quantity = fields.Char(default='1.0',
        help="It is used in computation for percentage and fixed amount. "
             "For e.g. A rule for Meal Voucher having fixed amount of "
             u"1€ per worked day can have its quantity defined in expression "
             "like worked_days.WORK100.number_of_days.")
    category_id = fields.Many2one('hr.salary.rule.category', string='Category', required=True)
    active = fields.Boolean(default=True,
        help="If the active field is set to false, it will allow you to hide the salary rule without removing it.")
    appears_on_payslip = fields.Boolean(string='Appears on Payslip', default=True,
        help="Used to display the salary rule on payslip.")
    parent_rule_id = fields.Many2one('hr.salary.rule', string='Parent Salary Rule', index=True)
    company_id = fields.Many2one('res.company', string='Company',
        default=lambda self: self.env['res.company']._company_default_get())
    condition_select = fields.Selection([
        ('none', 'Always True'),
        ('range', 'Range'),
        ('python', 'Python Expression')
    ], string="Condition Based on", default='none', required=True)
    condition_range = fields.Char(string='Range Based on', default='contract.wage',
        help='This will be used to compute the % fields values; in general it is on basic, '
             'but you can also use categories code fields in lowercase as a variable names '
             '(hra, ma, lta, etc.) and the variable basic.')
    condition_python = fields.Text(string='Python Condition', required=True,
        default='''
                    # Available variables:
                    #----------------------
                    # payslip: object containing the payslips
                    # employee: hr.employee object
                    # contract: hr.contract object
                    # rules: object containing the rules code (previously computed)
                    # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
                    # worked_days: object containing the computed worked days
                    # inputs: object containing the computed inputs

                    # Note: returned value have to be set in the variable 'result'

                    result = rules.NET > categories.NET * 0.10''',
        help='Applied this rule for calculation if condition is true. You can specify condition like basic > 1000.')
    condition_range_min = fields.Float(string='Minimum Range', help="The minimum amount, applied for this rule.")
    condition_range_max = fields.Float(string='Maximum Range', help="The maximum amount, applied for this rule.")
    amount_select = fields.Selection([
        ('percentage', 'Percentage (%)'),
        ('fix', 'Fixed Amount'),
        ('code', 'Python Code'),
    ], string='Amount Type', index=True, required=True, default='fix', help="The computation method for the rule amount.")
    amount_fix = fields.Float(string='Fixed Amount', digits=dp.get_precision('Payroll'))
    amount_percentage = fields.Float(string='Percentage (%)', digits=dp.get_precision('Payroll Rate'),
        help='For example, enter 50.0 to apply a percentage of 50%')
    amount_python_compute = fields.Text(string='Python Code',
        default='''
                    # Available variables:
                    #----------------------
                    # payslip: object containing the payslips
                    # employee: hr.employee object
                    # contract: hr.contract object
                    # rules: object containing the rules code (previously computed)
                    # categories: object containing the computed salary rule categories (sum of amount of all rules belonging to that category).
                    # worked_days: object containing the computed worked days.
                    # inputs: object containing the computed inputs.

                    # Note: returned value have to be set in the variable 'result'

                    result = contract.wage * 0.10''')
    amount_percentage_base = fields.Char(string='Percentage based on', help='result will be affected to a variable')
    child_ids = fields.One2many('hr.salary.rule', 'parent_rule_id', string='Child Salary Rule', copy=True)
    register_id = fields.Many2one('hr.contribution.register', string='Contribution Register',
        help="Eventual third party involved in the salary payment of the employees.")
    input_ids = fields.One2many('hr.rule.input', 'input_id', string='Inputs', copy=True)
    note = fields.Text(string='Description')

    @api.multi
    def _recursive_search_of_rules(self):
        """
        @return: returns a list of tuple (id, sequence) which are all the children of the passed rule_ids
        """
        children_rules = []
        for rule in self.filtered(lambda rule: rule.child_ids):
            children_rules += rule.child_ids._recursive_search_of_rules()
        return [(rule.id, rule.sequence) for rule in self] + children_rules

    #TODO should add some checks on the type of result (should be float)
    @api.multi
    def _compute_rule(self, localdict):
        """
        :param localdict: dictionary containing the environement in which to compute the rule
        :return: returns a tuple build as the base/amount computed, the quantity and the rate
        :rtype: (float, float, float)
        """
        self.ensure_one()
        if self.amount_select == 'fix':
            try:
                return self.amount_fix, float(safe_eval(self.quantity, localdict)), 100.0
            except:
                raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code))
        elif self.amount_select == 'percentage':
            try:
                return (float(safe_eval(self.amount_percentage_base, localdict)),
                        float(safe_eval(self.quantity, localdict)),
                        self.amount_percentage)
            except:
                raise UserError(_('Wrong percentage base or quantity defined for salary rule %s (%s).') % (self.name, self.code))
        else:
            try:
                safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True)
                return float(localdict['result']), 'result_qty' in localdict and localdict['result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0
            except:
                raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code))

    @api.multi
    def _satisfy_condition(self, localdict):
        """
        @param contract_id: id of hr.contract to be tested
        @return: returns True if the given rule match the condition for the given contract. Return False otherwise.
        """
        self.ensure_one()

        if self.condition_select == 'none':
            return True
        elif self.condition_select == 'range':
            try:
                result = safe_eval(self.condition_range, localdict)
                return self.condition_range_min <= result and result <= self.condition_range_max or False
            except:
                raise UserError(_('Wrong range condition defined for salary rule %s (%s).') % (self.name, self.code))
        else:  # python code
            try:
                safe_eval(self.condition_python, localdict, mode='exec', nocopy=True)
                return 'result' in localdict and localdict['result'] or False
            except:
                raise UserError(_('Wrong python condition defined for salary rule %s (%s).') % (self.name, self.code))
示例#29
0
class ComputeContainer(models.Model):
    _name = _description = 'test_new_api.compute.container'

    name = fields.Char()
    member_ids = fields.One2many('test_new_api.compute.member', 'container_id')
示例#30
0
class Partner(models.Model):
    _description = 'Contact'
    _inherit = ['format.address.mixin']
    _name = "res.partner"
    _order = "display_name"

    def _default_category(self):
        return self.env['res.partner.category'].browse(
            self._context.get('category_id'))

    def _default_company(self):
        return self.env['res.company']._company_default_get('res.partner')

    name = fields.Char(index=True)
    display_name = fields.Char(compute='_compute_display_name',
                               store=True,
                               index=True)
    date = fields.Date(index=True)
    title = fields.Many2one('res.partner.title')
    parent_id = fields.Many2one('res.partner',
                                string='Related Company',
                                index=True)
    parent_name = fields.Char(related='parent_id.name',
                              readonly=True,
                              string='Parent name')
    child_ids = fields.One2many(
        'res.partner',
        'parent_id',
        string='Contacts',
        domain=[('active', '=', True)
                ])  # force "active_test" domain to bypass _search() override
    ref = fields.Char(string='Internal Reference', index=True)
    lang = fields.Selection(
        _lang_get,
        string='Language',
        default=lambda self: self.env.lang,
        help=
        "If the selected language is loaded in the system, all documents related to "
        "this contact will be printed in this language. If not, it will be English."
    )
    tz = fields.Selection(
        _tz_get,
        string='Timezone',
        default=lambda self: self._context.get('tz'),
        help=
        "The partner's timezone, used to output proper date and time values "
        "inside printed reports. It is important to set a value for this field. "
        "You should use the same timezone that is otherwise used to pick and "
        "render date and time values: your computer's timezone.")
    tz_offset = fields.Char(compute='_compute_tz_offset',
                            string='Timezone offset',
                            invisible=True)
    user_id = fields.Many2one(
        'res.users',
        string='Salesperson',
        help=
        'The internal user that is in charge of communicating with this contact if any.'
    )
    vat = fields.Char(string='TIN',
                      help="Tax Identification Number. "
                      "Fill it if the company is subjected to taxes. "
                      "Used by the some of the legal statements.")
    bank_ids = fields.One2many('res.partner.bank',
                               'partner_id',
                               string='Banks')
    website = fields.Char(help="Website of Partner or Company")
    comment = fields.Text(string='Notes')

    category_id = fields.Many2many('res.partner.category',
                                   column1='partner_id',
                                   column2='category_id',
                                   string='Tags',
                                   default=_default_category)
    credit_limit = fields.Float(string='Credit Limit')
    barcode = fields.Char(oldname='ean13')
    active = fields.Boolean(default=True)
    customer = fields.Boolean(
        string='Is a Customer',
        default=True,
        help="Check this box if this contact is a customer.")
    supplier = fields.Boolean(
        string='Is a Vendor',
        help="Check this box if this contact is a vendor. "
        "If it's not checked, purchase people will not see it when encoding a purchase order."
    )
    employee = fields.Boolean(
        help="Check this box if this contact is an Employee.")
    function = fields.Char(string='Job Position')
    type = fields.Selection(
        [('contact', 'Contact'), ('invoice', 'Invoice address'),
         ('delivery', 'Shipping address'), ('other', 'Other address')],
        string='Address Type',
        default='contact',
        help=
        "Used to select automatically the right address according to the context in sales and purchases documents."
    )
    street = fields.Char()
    street2 = fields.Char()
    zip = fields.Char(change_default=True)
    city = fields.Char()
    state_id = fields.Many2one("res.country.state",
                               string='State',
                               ondelete='restrict')
    country_id = fields.Many2one('res.country',
                                 string='Country',
                                 ondelete='restrict')
    email = fields.Char()
    email_formatted = fields.Char(
        'Formatted Email',
        compute='_compute_email_formatted',
        help='Format email address "Name <email@domain>"')
    phone = fields.Char()
    mobile = fields.Char()
    is_company = fields.Boolean(
        string='Is a Company',
        default=False,
        help="Check if the contact is a company, otherwise it is a person")
    industry_id = fields.Many2one('res.partner.industry', 'Industry')
    # company_type is only an interface field, do not use it in business logic
    company_type = fields.Selection(string='Company Type',
                                    selection=[('person', 'Individual'),
                                               ('company', 'Company')],
                                    compute='_compute_company_type',
                                    inverse='_write_company_type')
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 index=True,
                                 default=_default_company)
    color = fields.Integer(string='Color Index', default=0)
    user_ids = fields.One2many('res.users',
                               'partner_id',
                               string='Users',
                               auto_join=True)
    partner_share = fields.Boolean(
        'Share Partner',
        compute='_compute_partner_share',
        store=True,
        help=
        "Either customer (no user), either shared user. Indicated the current partner is a customer without "
        "access or with a limited access created for sharing data.")
    contact_address = fields.Char(compute='_compute_contact_address',
                                  string='Complete Address')

    # technical field used for managing commercial fields
    commercial_partner_id = fields.Many2one(
        'res.partner',
        compute='_compute_commercial_partner',
        string='Commercial Entity',
        store=True,
        index=True)
    commercial_partner_country_id = fields.Many2one(
        'res.country', related='commercial_partner_id.country_id', store=True)
    commercial_company_name = fields.Char(
        'Company Name Entity',
        compute='_compute_commercial_company_name',
        store=True)
    company_name = fields.Char('Company Name')

    # image: all image fields are base64 encoded and PIL-supported
    image = fields.Binary(
        "Image",
        attachment=True,
        help=
        "This field holds the image used as avatar for this contact, limited to 1024x1024px",
    )
    image_medium = fields.Binary("Medium-sized image", attachment=True,
        help="Medium-sized image of this contact. It is automatically "\
             "resized as a 128x128px image, with aspect ratio preserved. "\
             "Use this field in form views or some kanban views.")
    image_small = fields.Binary("Small-sized image", attachment=True,
        help="Small-sized image of this contact. It is automatically "\
             "resized as a 64x64px image, with aspect ratio preserved. "\
             "Use this field anywhere a small image is required.")
    # hack to allow using plain browse record in qweb views, and used in ir.qweb.field.contact
    self = fields.Many2one(comodel_name=_name, compute='_compute_get_ids')

    _sql_constraints = [
        ('check_name',
         "CHECK( (type='contact' AND name IS NOT NULL) or (type!='contact') )",
         'Contacts require a name.'),
    ]

    @api.depends('is_company', 'name', 'parent_id.name', 'type',
                 'company_name')
    def _compute_display_name(self):
        diff = dict(show_address=None, show_address_only=None, show_email=None)
        names = dict(self.with_context(**diff).name_get())
        for partner in self:
            partner.display_name = names.get(partner.id)

    @api.depends('tz')
    def _compute_tz_offset(self):
        for partner in self:
            partner.tz_offset = datetime.datetime.now(
                pytz.timezone(partner.tz or 'GMT')).strftime('%z')

    @api.depends('user_ids.share')
    def _compute_partner_share(self):
        for partner in self:
            partner.partner_share = not partner.user_ids or any(
                user.share for user in partner.user_ids)

    @api.depends(lambda self: self._display_address_depends())
    def _compute_contact_address(self):
        for partner in self:
            partner.contact_address = partner._display_address()

    @api.one
    def _compute_get_ids(self):
        self.self = self.id

    @api.depends('is_company', 'parent_id.commercial_partner_id')
    def _compute_commercial_partner(self):
        for partner in self:
            if partner.is_company or not partner.parent_id:
                partner.commercial_partner_id = partner
            else:
                partner.commercial_partner_id = partner.parent_id.commercial_partner_id

    @api.depends('company_name', 'parent_id.is_company',
                 'commercial_partner_id.name')
    def _compute_commercial_company_name(self):
        for partner in self:
            p = partner.commercial_partner_id
            partner.commercial_company_name = p.is_company and p.name or partner.company_name

    @api.model
    def _get_default_image(self, partner_type, is_company, parent_id):
        if getattr(threading.currentThread(), 'testing',
                   False) or self._context.get('install_mode'):
            return False

        colorize, img_path, image = False, False, False

        if partner_type in ['other'] and parent_id:
            parent_image = self.browse(parent_id).image
            image = parent_image and base64.b64decode(parent_image) or None

        if not image and partner_type == 'invoice':
            img_path = get_module_resource('base', 'static/src/img',
                                           'money.png')
        elif not image and partner_type == 'delivery':
            img_path = get_module_resource('base', 'static/src/img',
                                           'truck.png')
        elif not image and is_company:
            img_path = get_module_resource('base', 'static/src/img',
                                           'company_image.png')
        elif not image:
            img_path = get_module_resource('base', 'static/src/img',
                                           'avatar.png')
            colorize = True

        if img_path:
            with open(img_path, 'rb') as f:
                image = f.read()
        if image and colorize:
            image = tools.image_colorize(image)

        return tools.image_resize_image_big(base64.b64encode(image))

    @api.model
    def _fields_view_get(self,
                         view_id=None,
                         view_type='form',
                         toolbar=False,
                         submenu=False):
        if (not view_id) and (view_type
                              == 'form') and self._context.get('force_email'):
            view_id = self.env.ref('base.view_partner_simple_form').id
        res = super(Partner, self)._fields_view_get(view_id=view_id,
                                                    view_type=view_type,
                                                    toolbar=toolbar,
                                                    submenu=submenu)
        if view_type == 'form':
            res['arch'] = self._fields_view_get_address(res['arch'])
        return res

    @api.constrains('parent_id')
    def _check_parent_id(self):
        if not self._check_recursion():
            raise ValidationError(
                _('You cannot create recursive Partner hierarchies.'))

    @api.multi
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {}, name=_('%s (copy)') % self.name)
        return super(Partner, self).copy(default)

    @api.onchange('parent_id')
    def onchange_parent_id(self):
        # return values in result, as this method is used by _fields_sync()
        if not self.parent_id:
            return
        result = {}
        partner = getattr(self, '_origin', self)
        if partner.parent_id and partner.parent_id != self.parent_id:
            result['warning'] = {
                'title':
                _('Warning'),
                'message':
                _('Changing the company of a contact should only be done if it '
                  'was never correctly set. If an existing contact starts working for a new '
                  'company then a new contact should be created under that new '
                  'company. You can use the "Discard" button to abandon this change.'
                  )
            }
        if partner.type == 'contact' or self.type == 'contact':
            # for contacts: copy the parent address, if set (aka, at least one
            # value is set in the address: otherwise, keep the one from the
            # contact)
            address_fields = self._address_fields()
            if any(self.parent_id[key] for key in address_fields):

                def convert(value):
                    return value.id if isinstance(value,
                                                  models.BaseModel) else value

                result['value'] = {
                    key: convert(self.parent_id[key])
                    for key in address_fields
                }
        return result

    @api.onchange('country_id')
    def _onchange_country_id(self):
        if self.country_id:
            return {
                'domain': {
                    'state_id': [('country_id', '=', self.country_id.id)]
                }
            }
        else:
            return {'domain': {'state_id': []}}

    @api.onchange('email')
    def onchange_email(self):
        if not self.image and self._context.get(
                'gravatar_image') and self.email:
            self.image = self._get_gravatar_image(self.email)

    @api.depends('name', 'email')
    def _compute_email_formatted(self):
        for partner in self:
            partner.email_formatted = formataddr(
                (partner.name or u"False", partner.email or u"False"))

    @api.depends('is_company')
    def _compute_company_type(self):
        for partner in self:
            partner.company_type = 'company' if partner.is_company else 'person'

    def _write_company_type(self):
        for partner in self:
            partner.is_company = partner.company_type == 'company'

    @api.onchange('company_type')
    def onchange_company_type(self):
        self.is_company = (self.company_type == 'company')

    @api.multi
    def _update_fields_values(self, fields):
        """ Returns dict of write() values for synchronizing ``fields`` """
        values = {}
        for fname in fields:
            field = self._fields[fname]
            if field.type == 'many2one':
                values[fname] = self[fname].id
            elif field.type == 'one2many':
                raise AssertionError(
                    _('One2Many fields cannot be synchronized as part of `commercial_fields` or `address fields`'
                      ))
            elif field.type == 'many2many':
                values[fname] = [(6, 0, self[fname].ids)]
            else:
                values[fname] = self[fname]
        return values

    @api.model
    def _address_fields(self):
        """Returns the list of address fields that are synced from the parent."""
        return list(ADDRESS_FIELDS)

    @api.multi
    def update_address(self, vals):
        addr_vals = {
            key: vals[key]
            for key in self._address_fields() if key in vals
        }
        if addr_vals:
            return super(Partner, self).write(addr_vals)

    @api.model
    def _commercial_fields(self):
        """ Returns the list of fields that are managed by the commercial entity
        to which a partner belongs. These fields are meant to be hidden on
        partners that aren't `commercial entities` themselves, and will be
        delegated to the parent `commercial entity`. The list is meant to be
        extended by inheriting classes. """
        return ['vat', 'credit_limit']

    @api.multi
    def _commercial_sync_from_company(self):
        """ Handle sync of commercial fields when a new parent commercial entity is set,
        as if they were related fields """
        commercial_partner = self.commercial_partner_id
        if commercial_partner != self:
            sync_vals = commercial_partner._update_fields_values(
                self._commercial_fields())
            self.write(sync_vals)

    @api.multi
    def _commercial_sync_to_children(self):
        """ Handle sync of commercial fields to descendants """
        commercial_partner = self.commercial_partner_id
        sync_vals = commercial_partner._update_fields_values(
            self._commercial_fields())
        sync_children = self.child_ids.filtered(lambda c: not c.is_company)
        for child in sync_children:
            child._commercial_sync_to_children()
        sync_children._compute_commercial_partner()
        return sync_children.write(sync_vals)

    @api.multi
    def _fields_sync(self, values):
        """ Sync commercial fields and address fields from company and to children after create/update,
        just as if those were all modeled as fields.related to the parent """
        # 1. From UPSTREAM: sync from parent
        if values.get('parent_id') or values.get('type', 'contact'):
            # 1a. Commercial fields: sync if parent changed
            if values.get('parent_id'):
                self._commercial_sync_from_company()
            # 1b. Address fields: sync if parent or use_parent changed *and* both are now set
            if self.parent_id and self.type == 'contact':
                onchange_vals = self.onchange_parent_id().get('value', {})
                self.update_address(onchange_vals)

        # 2. To DOWNSTREAM: sync children
        if self.child_ids:
            # 2a. Commercial Fields: sync if commercial entity
            if self.commercial_partner_id == self:
                commercial_fields = self._commercial_fields()
                if any(field in values for field in commercial_fields):
                    self._commercial_sync_to_children()
            for child in self.child_ids.filtered(lambda c: not c.is_company):
                if child.commercial_partner_id != self.commercial_partner_id:
                    self._commercial_sync_to_children()
                    break
            # 2b. Address fields: sync if address changed
            address_fields = self._address_fields()
            if any(field in values for field in address_fields):
                contacts = self.child_ids.filtered(
                    lambda c: c.type == 'contact')
                contacts.update_address(values)

    @api.multi
    def _handle_first_contact_creation(self):
        """ On creation of first contact for a company (or root) that has no address, assume contact address
        was meant to be company address """
        parent = self.parent_id
        address_fields = self._address_fields()
        if (parent.is_company or not parent.parent_id) and len(parent.child_ids) == 1 and \
            any(self[f] for f in address_fields) and not any(parent[f] for f in address_fields):
            addr_vals = self._update_fields_values(address_fields)
            parent.update_address(addr_vals)

    def _clean_website(self, website):
        url = urls.url_parse(website)
        if not url.scheme:
            if not url.netloc:
                url = url.replace(netloc=url.path, path='')
            website = url.replace(scheme='http').to_url()
        return website

    @api.multi
    def write(self, vals):
        # res.partner must only allow to set the company_id of a partner if it
        # is the same as the company of all users that inherit from this partner
        # (this is to allow the code from res_users to write to the partner!) or
        # if setting the company_id to False (this is compatible with any user
        # company)
        if vals.get('website'):
            vals['website'] = self._clean_website(vals['website'])
        if vals.get('parent_id'):
            vals['company_name'] = False
        if vals.get('company_id'):
            company = self.env['res.company'].browse(vals['company_id'])
            for partner in self:
                if partner.user_ids:
                    companies = set(user.company_id
                                    for user in partner.user_ids)
                    if len(companies) > 1 or company not in companies:
                        raise UserError(
                            _("You can not change the company as the partner/user has multiple user linked with different companies."
                              ))
        tools.image_resize_images(vals)

        result = True
        # To write in SUPERUSER on field is_company and avoid access rights problems.
        if 'is_company' in vals and self.user_has_groups(
                'base.group_partner_manager'
        ) and not self.env.uid == SUPERUSER_ID:
            result = super(Partner, self.sudo()).write(
                {'is_company': vals.get('is_company')})
            del vals['is_company']
        result = result and super(Partner, self).write(vals)
        for partner in self:
            if any(
                    u.has_group('base.group_user') for u in partner.user_ids
                    if u != self.env.user):
                self.env['res.users'].check_access_rights('write')
            partner._fields_sync(vals)
        return result

    @api.model
    def create(self, vals):
        if vals.get('website'):
            vals['website'] = self._clean_website(vals['website'])
        if vals.get('parent_id'):
            vals['company_name'] = False
        # compute default image in create, because computing gravatar in the onchange
        # cannot be easily performed if default images are in the way
        if not vals.get('image'):
            vals['image'] = self._get_default_image(vals.get('type'),
                                                    vals.get('is_company'),
                                                    vals.get('parent_id'))
        tools.image_resize_images(vals)
        partner = super(Partner, self).create(vals)
        partner._fields_sync(vals)
        partner._handle_first_contact_creation()
        return partner

    @api.multi
    def create_company(self):
        self.ensure_one()
        if self.company_name:
            # Create parent company
            values = dict(name=self.company_name,
                          is_company=True,
                          vat=self.vat)
            values.update(self._update_fields_values(self._address_fields()))
            new_company = self.create(values)
            # Set new company as my parent
            self.write({
                'parent_id':
                new_company.id,
                'child_ids': [(1, partner_id, dict(parent_id=new_company.id))
                              for partner_id in self.child_ids.ids]
            })
        return True

    @api.multi
    def open_commercial_entity(self):
        """ Utility method used to add an "Open Company" button in partner views """
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'res.partner',
            'view_mode': 'form',
            'res_id': self.commercial_partner_id.id,
            'target': 'current',
            'flags': {
                'form': {
                    'action_buttons': True
                }
            }
        }

    @api.multi
    def open_parent(self):
        """ Utility method used to add an "Open Parent" button in partner views """
        self.ensure_one()
        address_form_id = self.env.ref('base.view_partner_address_form').id
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'res.partner',
            'view_mode': 'form',
            'views': [(address_form_id, 'form')],
            'res_id': self.parent_id.id,
            'target': 'new',
            'flags': {
                'form': {
                    'action_buttons': True
                }
            }
        }

    @api.multi
    def name_get(self):
        res = []
        for partner in self:
            name = partner.name or ''

            if partner.company_name or partner.parent_id:
                if not name and partner.type in [
                        'invoice', 'delivery', 'other'
                ]:
                    name = dict(
                        self.fields_get(['type'
                                         ])['type']['selection'])[partner.type]
                if not partner.is_company:
                    name = "%s, %s" % (partner.commercial_company_name
                                       or partner.parent_id.name, name)
            if self._context.get('show_address_only'):
                name = partner._display_address(without_company=True)
            if self._context.get('show_address'):
                name = name + "\n" + partner._display_address(
                    without_company=True)
            name = name.replace('\n\n', '\n')
            name = name.replace('\n\n', '\n')
            if self._context.get('show_email') and partner.email:
                name = "%s <%s>" % (name, partner.email)
            if self._context.get('html_format'):
                name = name.replace('\n', '<br/>')
            res.append((partner.id, name))
        return res

    def _parse_partner_name(self, text, context=None):
        """ Supported syntax:
            - 'Raoul <*****@*****.**>': will find name and email address
            - otherwise: default, everything is set as the name """
        emails = tools.email_split(text.replace(' ', ','))
        if emails:
            email = emails[0]
            name = text[:text.index(email)].replace('"',
                                                    '').replace('<',
                                                                '').strip()
        else:
            name, email = text, ''
        return name, email

    @api.model
    def name_create(self, name):
        """ Override of orm's name_create method for partners. The purpose is
            to handle some basic formats to create partners using the
            name_create.
            If only an email address is received and that the regex cannot find
            a name, the name will have the email value.
            If 'force_email' key in context: must find the email address. """
        name, email = self._parse_partner_name(name)
        if self._context.get('force_email') and not email:
            raise UserError(
                _("Couldn't create contact without email address!"))
        if not name and email:
            name = email
        partner = self.create({
            self._rec_name:
            name or email,
            'email':
            email or self.env.context.get('default_email', False)
        })
        return partner.name_get()[0]

    @api.model
    def _search(self,
                args,
                offset=0,
                limit=None,
                order=None,
                count=False,
                access_rights_uid=None):
        """ Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will
        always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """
        # a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions
        if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in') \
                and args[0][2] != [False]:
            self = self.with_context(active_test=False)
        return super(Partner,
                     self)._search(args,
                                   offset=offset,
                                   limit=limit,
                                   order=order,
                                   count=count,
                                   access_rights_uid=access_rights_uid)

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        if args is None:
            args = []
        if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'):
            self.check_access_rights('read')
            where_query = self._where_calc(args)
            self._apply_ir_rules(where_query, 'read')
            from_clause, where_clause, where_clause_params = where_query.get_sql(
            )
            where_str = where_clause and (" WHERE %s AND " %
                                          where_clause) or ' WHERE '

            # search on the name of the contacts and of its company
            search_name = name
            if operator in ('ilike', 'like'):
                search_name = '%%%s%%' % name
            if operator in ('=ilike', '=like'):
                operator = operator[1:]

            unaccent = get_unaccent_wrapper(self.env.cr)

            query = """SELECT id
                         FROM res_partner
                      {where} ({email} {operator} {percent}
                           OR {display_name} {operator} {percent}
                           OR {reference} {operator} {percent}
                           OR {vat} {operator} {percent})
                           -- don't panic, trust postgres bitmap
                     ORDER BY {display_name} {operator} {percent} desc,
                              {display_name}
                    """.format(
                where=where_str,
                operator=operator,
                email=unaccent('email'),
                display_name=unaccent('display_name'),
                reference=unaccent('ref'),
                percent=unaccent('%s'),
                vat=unaccent('vat'),
            )

            where_clause_params += [search_name] * 5
            if limit:
                query += ' limit %s'
                where_clause_params.append(limit)
            self.env.cr.execute(query, where_clause_params)
            partner_ids = [row[0] for row in self.env.cr.fetchall()]

            if partner_ids:
                return self.browse(partner_ids).name_get()
            else:
                return []
        return super(Partner, self).name_search(name,
                                                args,
                                                operator=operator,
                                                limit=limit)

    @api.model
    def find_or_create(self, email):
        """ Find a partner with the given ``email`` or use :py:method:`~.name_create`
            to create one

            :param str email: email-like string, which should contain at least one email,
                e.g. ``"Raoul Grosbedon <*****@*****.**>"``"""
        assert email, 'an email is required for find_or_create to work'
        emails = tools.email_split(email)
        if emails:
            email = emails[0]
        partners = self.search([('email', '=ilike', email)], limit=1)
        return partners.id or self.name_create(email)[0]

    def _get_gravatar_image(self, email):
        email_hash = hashlib.md5(email.lower().encode('utf-8')).hexdigest()
        url = "https://www.gravatar.com/avatar/" + email_hash
        try:
            res = requests.get(url, params={'d': '404', 's': '128'}, timeout=5)
            if res.status_code != requests.codes.ok:
                return False
        except requests.exceptions.ConnectionError as e:
            return False
        return base64.b64encode(res.content)

    @api.multi
    def _email_send(self, email_from, subject, body, on_error=None):
        for partner in self.filtered('email'):
            tools.email_send(email_from, [partner.email], subject, body,
                             on_error)
        return True

    @api.multi
    def address_get(self, adr_pref=None):
        """ Find contacts/addresses of the right type(s) by doing a depth-first-search
        through descendants within company boundaries (stop at entities flagged ``is_company``)
        then continuing the search at the ancestors that are within the same company boundaries.
        Defaults to partners of type ``'default'`` when the exact type is not found, or to the
        provided partner itself if no type ``'default'`` is found either. """
        adr_pref = set(adr_pref or [])
        if 'contact' not in adr_pref:
            adr_pref.add('contact')
        result = {}
        visited = set()
        for partner in self:
            current_partner = partner
            while current_partner:
                to_scan = [current_partner]
                # Scan descendants, DFS
                while to_scan:
                    record = to_scan.pop(0)
                    visited.add(record)
                    if record.type in adr_pref and not result.get(record.type):
                        result[record.type] = record.id
                    if len(result) == len(adr_pref):
                        return result
                    to_scan = [
                        c for c in record.child_ids if c not in visited
                        if not c.is_company
                    ] + to_scan

                # Continue scanning at ancestor if current_partner is not a commercial entity
                if current_partner.is_company or not current_partner.parent_id:
                    break
                current_partner = current_partner.parent_id

        # default to type 'contact' or the partner itself
        default = result.get('contact', self.id or False)
        for adr_type in adr_pref:
            result[adr_type] = result.get(adr_type) or default
        return result

    @api.model
    def view_header_get(self, view_id, view_type):
        res = super(Partner, self).view_header_get(view_id, view_type)
        if res: return res
        if not self._context.get('category_id'):
            return False
        return _('Partners: ') + self.env['res.partner.category'].browse(
            self._context['category_id']).name

    @api.model
    @api.returns('self')
    def main_partner(self):
        ''' Return the main partner '''
        return self.env.ref('base.main_partner')

    @api.model
    def _get_default_address_format(self):
        return "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s"

    @api.multi
    def _display_address(self, without_company=False):
        '''
        The purpose of this function is to build and return an address formatted accordingly to the
        standards of the country where it belongs.

        :param address: browse record of the res.partner to format
        :returns: the address formatted in a display that fit its country habits (or the default ones
            if not country is specified)
        :rtype: string
        '''
        # get the information that will be injected into the display format
        # get the address format
        address_format = self.country_id.address_format or \
            self._get_default_address_format()
        args = {
            'state_code': self.state_id.code or '',
            'state_name': self.state_id.name or '',
            'country_code': self.country_id.code or '',
            'country_name': self.country_id.name or '',
            'company_name': self.commercial_company_name or '',
        }
        for field in self._address_fields():
            args[field] = getattr(self, field) or ''
        if without_company:
            args['company_name'] = ''
        elif self.commercial_company_name:
            address_format = '%(company_name)s\n' + address_format
        return address_format % args

    def _display_address_depends(self):
        # field dependencies of method _display_address()
        return self._address_fields() + [
            'country_id.address_format',
            'country_id.code',
            'country_id.name',
            'company_name',
            'state_id.code',
            'state_id.name',
        ]