Exemplo n.º 1
0
class pos_config(osv.osv):
    _inherit = 'pos.config'
    _columns = {
        'iface_splitbill':
        fields.boolean('Bill Splitting',
                       help='Enables Bill Splitting in the Point of Sale'),
        'iface_printbill':
        fields.boolean('Bill Printing',
                       help='Allows to print the Bill before payment'),
        'iface_orderline_notes':
        fields.boolean('Orderline Notes',
                       help='Allow custom notes on Orderlines'),
        'floor_ids':
        fields.one2many(
            'restaurant.floor',
            'pos_config_id',
            'Restaurant Floors',
            help='The restaurant floors served by this point of sale'),
        'printer_ids':
        fields.many2many('restaurant.printer',
                         'pos_config_printer_rel',
                         'config_id',
                         'printer_id',
                         string='Order Printers'),
    }
    _defaults = {
        'iface_splitbill': False,
        'iface_printbill': False,
    }
Exemplo n.º 2
0
class restaurant_printer(osv.osv):
    _name = 'restaurant.printer'

    _columns = {
        'name':
        fields.char('Printer Name',
                    size=32,
                    required=True,
                    help='An internal identification of the printer'),
        'proxy_ip':
        fields.char(
            'Proxy IP Address',
            size=32,
            help="The IP Address or hostname of the Printer's hardware proxy"),
        'product_categories_ids':
        fields.many2many('pos.category',
                         'printer_category_rel',
                         'printer_id',
                         'category_id',
                         string='Printed Product Categories'),
    }

    _defaults = {
        'name': 'Printer',
    }
Exemplo n.º 3
0
class hr_holidays_summary_employee(osv.osv_memory):
    _name = 'hr.holidays.summary.employee'
    _description = 'HR Leaves Summary Report By Employee'
    _columns = {
        'date_from':
        fields.date('From', required=True),
        'emp':
        fields.many2many('hr.employee', 'summary_emp_rel', 'sum_id', 'emp_id',
                         'Employee(s)'),
        'holiday_type':
        fields.selection([('Approved', 'Approved'), ('Confirmed', 'Confirmed'),
                          ('both', 'Both Approved and Confirmed')],
                         'Select Leave Type',
                         required=True)
    }

    _defaults = {
        'date_from': lambda *a: time.strftime('%Y-%m-01'),
        'holiday_type': 'Approved',
    }

    def print_report(self, cr, uid, ids, context=None):
        data = self.read(cr, uid, ids, context=context)[0]
        data['emp'] = context.get('active_ids', [])
        datas = {'ids': [], 'model': 'hr.employee', 'form': data}
        return self.pool['report'].get_action(
            cr,
            uid,
            data['emp'],
            'hr_holidays.report_holidayssummary',
            data=datas,
            context=context)
Exemplo n.º 4
0
class res_partner_tags(osv.Model):
    _description = 'Partner Tags - These tags can be used on website to find customers by sector, or ... '
    _name = 'res.partner.tag'
    _inherit = 'website.published.mixin'

    def get_selection_class(self, cr, uid, context=None):
        classname = ['default', 'primary', 'success', 'warning', 'danger']
        return [(x, str.title(x)) for x in classname]

    _columns = {
        'name':
        fields.char('Category Name', required=True, translate=True),
        'partner_ids':
        fields.many2many('res.partner',
                         'res_partner_res_partner_tag_rel',
                         id1='tag_id',
                         id2='partner_id',
                         string='Partners'),
        'classname':
        fields.selection(get_selection_class,
                         'Class',
                         help="Bootstrap class to customize the color",
                         required=True),
        'active':
        fields.boolean('Active'),
    }
    _defaults = {
        'active': True,
        'website_published': True,
        'classname': 'default',
    }
Exemplo n.º 5
0
class hr_holidays_summary_dept(osv.osv_memory):
    _name = 'hr.holidays.summary.dept'
    _description = 'HR Leaves Summary Report By Department'
    _columns = {
        'date_from':
        fields.date('From', required=True),
        'depts':
        fields.many2many('hr.department', 'summary_dept_rel', 'sum_id',
                         'dept_id', 'Department(s)'),
        'holiday_type':
        fields.selection([('Approved', 'Approved'), ('Confirmed', 'Confirmed'),
                          ('both', 'Both Approved and Confirmed')],
                         'Leave Type',
                         required=True)
    }

    _defaults = {
        'date_from': lambda *a: time.strftime('%Y-%m-01'),
        'holiday_type': 'Approved'
    }

    def print_report(self, cr, uid, ids, context=None):
        data = self.read(cr, uid, ids, context=context)[0]
        if not data['depts']:
            raise UserError(
                _('You have to select at least one Department. And try again.')
            )
        datas = {'ids': [], 'model': 'hr.department', 'form': data}
        return self.pool['report'].get_action(
            cr,
            uid,
            data['depts'],
            'hr_holidays.report_holidayssummary',
            data=datas,
            context=context)
Exemplo n.º 6
0
class res_partner(osv.Model):
    _inherit = 'res.partner'

    _columns = {
        'website_tag_ids':
        fields.many2many('res.partner.tag',
                         id1='partner_id',
                         id2='tag_id',
                         string='Website tags',
                         oldname="tag_ids"),
    }
Exemplo n.º 7
0
class hr_salary_employee_bymonth(osv.osv_memory):

    _name = 'hr.salary.employee.month'
    _description = 'Hr Salary Employee By Month Report'
    _columns = {
        'start_date':
        fields.date('Start Date', required=True),
        'end_date':
        fields.date('End Date', required=True),
        'employee_ids':
        fields.many2many('hr.employee',
                         'payroll_year_rel',
                         'payroll_year_id',
                         'employee_id',
                         'Employees',
                         required=True),
        'category_id':
        fields.many2one('hr.salary.rule.category', 'Category', required=True),
    }

    def _get_default_category(self, cr, uid, context=None):
        category_ids = self.pool.get('hr.salary.rule.category').search(
            cr, uid, [('code', '=', 'NET')], context=context)
        return category_ids and category_ids[0] or False

    _defaults = {
        'start_date': lambda *a: time.strftime('%Y-01-01'),
        'end_date': lambda *a: time.strftime('%Y-%m-%d'),
        'category_id': _get_default_category
    }

    def print_report(self, cr, uid, ids, context=None):
        """
         To get the date and print the report
         @param self: The object pointer.
         @param cr: A database cursor
         @param uid: ID of the user currently logged in
         @param context: A standard dictionary
         @return: return report
        """
        if context is None:
            context = {}
        datas = {'ids': context.get('active_ids', [])}

        res = self.read(cr, uid, ids, context=context)
        res = res and res[0] or {}
        datas.update({'form': res})
        return self.pool['report'].get_action(
            cr,
            uid,
            ids,
            'l10n_in_hr_payroll.report_hrsalarybymonth',
            data=datas,
            context=context)
Exemplo n.º 8
0
class product_template(osv.Model):
    _inherit = "product.template"

    _columns = {
        'optional_product_ids':
        fields.many2many('product.template',
                         'product_optional_rel',
                         'src_id',
                         'dest_id',
                         string='Optional Products',
                         help="Products to propose when add to cart."),
    }
Exemplo n.º 9
0
class sale_order(osv.osv):
    _name = "sale.order"
    _inherit = ['sale.order', 'utm.mixin']
    _columns = {
        'tag_ids':
        fields.many2many('crm.lead.tag', 'sale_order_tag_rel', 'order_id',
                         'tag_id', 'Tags'),
        'opportunity_id':
        fields.many2one('crm.lead',
                        'Opportunity',
                        domain="[('type', '=', 'opportunity')]")
    }
Exemplo n.º 10
0
class res_company(osv.osv):
    """Override company to add Header object link a company can have many header and logos"""

    _inherit = "res.company"
    _columns = {
        'header_image':
        fields.many2many(
            'ir.header_img',
            'company_img_rel',
            'company_id',
            'img_id',
            'Available Images',
        ),
        'header_webkit':
        fields.many2many(
            'ir.header_webkit',
            'company_html_rel',
            'company_id',
            'html_id',
            'Available html',
        ),
    }
Exemplo n.º 11
0
class BlogTag(osv.Model):
    _name = 'blog.tag'
    _description = 'Blog Tag'
    _inherit = ['website.seo.metadata']
    _order = 'name'
    _columns = {
        'name': fields.char('Name', required=True),
        'post_ids': fields.many2many(
            'blog.post', string='Posts',
        ),
    }
    _sql_constraints = [
            ('name_uniq', 'unique (name)', "Tag name already exists !"),
    ]
Exemplo n.º 12
0
class test_uninstall_model(Model):
    """
    This model uses different types of columns to make it possible to test
    the uninstall feature of YuanCloud.
    """
    _name = 'test_uninstall.model'

    _columns = {
        'name': fields.char('Name'),
        'ref': fields.many2one('res.users', string='User'),
        'rel': fields.many2many('res.users', string='Users'),
    }

    _sql_constraints = [('name_uniq', 'unique (name)',
                         'Each name must be unique.')]
Exemplo n.º 13
0
class hr_employee_category(osv.Model):

    _name = "hr.employee.category"
    _description = "Employee Category"
    _columns = {
        'name':
        fields.char("Employee Tag", required=True),
        'color':
        fields.integer('Color Index'),
        'employee_ids':
        fields.many2many('hr.employee', 'employee_category_rel', 'category_id',
                         'emp_id', 'Employees'),
    }
    _sql_constraints = [
        ('name_uniq', 'unique (name)', "Tag name already exists !"),
    ]
Exemplo n.º 14
0
class yearly_salary_detail(osv.osv_memory):

    _name = 'yearly.salary.detail'
    _description = 'Hr Salary Employee By Category Report'
    _columns = {
        'employee_ids':
        fields.many2many('hr.employee',
                         'payroll_emp_rel',
                         'payroll_id',
                         'employee_id',
                         'Employees',
                         required=True),
        'date_from':
        fields.date('Start Date', required=True),
        'date_to':
        fields.date('End Date', required=True),
    }

    _defaults = {
        'date_from': lambda *a: time.strftime('%Y-01-01'),
        'date_to': lambda *a: time.strftime('%Y-%m-%d'),
    }

    def print_report(self, cr, uid, ids, context=None):
        """
         To get the date and print the report
         @param self: The object pointer.
         @param cr: A database cursor
         @param uid: ID of the user currently logged in
         @param context: A standard dictionary
         @return: return report
        """
        if context is None:
            context = {}
        datas = {'ids': context.get('active_ids', [])}

        res = self.read(cr, uid, ids, context=context)
        res = res and res[0] or {}
        datas.update({'form': res})
        return self.pool['report'].get_action(
            cr,
            uid,
            ids,
            'l10n_in_hr_payroll.report_hryearlysalary',
            data=datas,
            context=context)
Exemplo n.º 15
0
class pos_details(osv.osv_memory):
    _name = 'pos.details'
    _description = 'Sales Details'

    _columns = {
        'date_start':
        fields.date('Date Start', required=True),
        'date_end':
        fields.date('Date End', required=True),
        'user_ids':
        fields.many2many('res.users', 'pos_details_report_user_rel', 'user_id',
                         'wizard_id', 'Salespeople'),
    }
    _defaults = {
        'date_start': fields.date.context_today,
        'date_end': fields.date.context_today,
    }

    def print_report(self, cr, uid, ids, context=None):
        """
         To get the date and print the report
         @param self: The object pointer.
         @param cr: A database cursor
         @param uid: ID of the user currently logged in
         @param context: A standard dictionary
         @return : retrun report
        """
        if context is None:
            context = {}
        datas = {'ids': context.get('active_ids', [])}
        res = self.read(cr,
                        uid,
                        ids, ['date_start', 'date_end', 'user_ids'],
                        context=context)
        res = res and res[0] or {}
        datas['form'] = res
        if res.get('id', False):
            datas['ids'] = [res['id']]
        return self.pool['report'].get_action(
            cr,
            uid, [],
            'point_of_sale.report_detailsofsales',
            data=datas,
            context=context)
Exemplo n.º 16
0
class purchase_requisition_partner(osv.osv_memory):
    _name = "purchase.requisition.partner"
    _description = "Purchase Requisition Partner"
    _columns = {
        'partner_ids':
        fields.many2many('res.partner',
                         'purchase_requisition_supplier_rel',
                         'requisition_id',
                         'partner_id',
                         string='Vendors',
                         required=True,
                         domain=[('supplier', '=', True)])
    }

    def view_init(self, cr, uid, fields_list, context=None):
        if context is None:
            context = {}
        res = super(purchase_requisition_partner,
                    self).view_init(cr, uid, fields_list, context=context)
        record_id = context and context.get('active_id', False) or False
        tender = self.pool.get('purchase.requisition').browse(cr,
                                                              uid,
                                                              record_id,
                                                              context=context)
        if not tender.line_ids:
            raise UserError(
                _('Define product(s) you want to include in the call for tenders.'
                  ))
        return res

    def create_order(self, cr, uid, ids, context=None):
        active_ids = context and context.get('active_ids', [])
        purchase_requisition = self.pool.get('purchase.requisition')
        for wizard in self.browse(cr, uid, ids, context=context):
            for partner_id in wizard.partner_ids:
                purchase_requisition.make_purchase_order(cr,
                                                         uid,
                                                         active_ids,
                                                         partner_id.id,
                                                         context=context)
        return {'type': 'ir.actions.act_window_close'}
Exemplo n.º 17
0
class website_pricelist(osv.Model):
    _name = 'website_pricelist'
    _description = 'Website Pricelist'

    def _get_display_name(self, cr, uid, ids, name, arg, context=None):
        result = {}
        for o in self.browse(cr, uid, ids, context=context):
            result[o.id] = _("Website Pricelist for %s") % o.pricelist_id.name
        return result

    _columns = {
        'name': fields.function(_get_display_name, string='Pricelist Name', type="char"),
        'website_id': fields.many2one('website', string="Website", required=True),
        'selectable': fields.boolean('Selectable', help="Allow the end user to choose this price list"),
        'pricelist_id': fields.many2one('product.pricelist', string='Pricelist'),
        'country_group_ids': fields.many2many('res.country.group', 'res_country_group_website_pricelist_rel',
                                              'website_pricelist_id', 'res_country_group_id', string='Country Groups'),
    }

    def clear_cache(self):
        # website._get_pl() is cached to avoid to recompute at each request the
        # list of available pricelists. So, we need to invalidate the cache when
        # we change the config of website price list to force to recompute.
        website = self.pool['website']
        website._get_pl.clear_cache(website)

    def create(self, cr, uid, data, context=None):
        res = super(website_pricelist, self).create(cr, uid, data, context=context)
        self.clear_cache()
        return res

    def write(self, cr, uid, ids, data, context=None):
        res = super(website_pricelist, self).write(cr, uid, ids, data, context=context)
        self.clear_cache()
        return res

    def unlink(self, cr, uid, ids, context=None):
        res = super(website_pricelist, self).unlink(cr, uid, ids, context=context)
        self.clear_cache()
        return res
Exemplo n.º 18
0
class mrp_repair_fee(osv.osv, ProductChangeMixin):
    _name = 'mrp.repair.fee'
    _description = 'Repair Fees Line'

    def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
        """ Calculates amount.
        @param field_name: Name of field.
        @param arg: Argument
        @return: Dictionary of values.
        """
        res = {}
        tax_obj = self.pool.get('account.tax')
        cur_obj = self.pool.get('res.currency')
        for line in self.browse(cr, uid, ids, context=context):
            if line.to_invoice:
                cur = line.repair_id.pricelist_id.currency_id
                taxes = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, cur.id, line.product_uom_qty, line.product_id.id, line.repair_id.partner_id.id)
                res[line.id] = taxes['total_included']
            else:
                res[line.id] = 0
        return res

    _columns = {
        'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', required=True, ondelete='cascade', select=True),
        'name': fields.char('Description', select=True, required=True),
        'product_id': fields.many2one('product.product', 'Product'),
        'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
        'price_unit': fields.float('Unit Price', required=True),
        'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
        'price_subtotal': fields.function(_amount_line, string='Subtotal', digits=0),
        'tax_id': fields.many2many('account.tax', 'repair_fee_line_tax', 'repair_fee_line_id', 'tax_id', 'Taxes'),
        'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True, copy=False),
        'to_invoice': fields.boolean('To Invoice'),
        'invoiced': fields.boolean('Invoiced', readonly=True, copy=False),
    }

    _defaults = {
        'to_invoice': lambda *a: True,
    }
Exemplo n.º 19
0
class product_category(osv.osv):
    _inherit = 'product.category'

    def calculate_total_routes(self, cr, uid, ids, name, args, context=None):
        res = {}
        for categ in self.browse(cr, uid, ids, context=context):
            categ2 = categ
            routes = [x.id for x in categ.route_ids]
            while categ2.parent_id:
                categ2 = categ2.parent_id
                routes += [x.id for x in categ2.route_ids]
            res[categ.id] = routes
        return res

    _columns = {
        'route_ids':
        fields.many2many('stock.location.route',
                         'stock_location_route_categ',
                         'categ_id',
                         'route_id',
                         'Routes',
                         domain="[('product_categ_selectable', '=', True)]"),
        'removal_strategy_id':
        fields.many2one(
            'product.removal',
            'Force Removal Strategy',
            help=
            "Set a specific removal strategy that will be used regardless of the source location for this product category"
        ),
        'total_route_ids':
        fields.function(calculate_total_routes,
                        relation='stock.location.route',
                        type='many2many',
                        string='Total routes',
                        readonly=True),
    }
Exemplo n.º 20
0
class MailComposeMessage(osv.TransientModel):
    """Add concept of mass mailing campaign to the mail.compose.message wizard
    """
    _inherit = 'mail.compose.message'

    _columns = {
        'mass_mailing_campaign_id':
        fields.many2one('mail.mass_mailing.campaign', 'Mass Mailing Campaign'),
        'mass_mailing_id':
        fields.many2one('mail.mass_mailing',
                        'Mass Mailing',
                        ondelete='cascade'),
        'mass_mailing_name':
        fields.char('Mass Mailing'),
        'mailing_list_ids':
        fields.many2many('mail.mass_mailing.list', string='Mailing List'),
    }

    def get_mail_values(self, cr, uid, ids, res_ids, context=None):
        """ Override method that generated the mail content by creating the
        mail.mail.statistics values in the o2m of mail_mail, when doing pure
        email mass mailing. """
        res = super(MailComposeMessage, self).get_mail_values(cr,
                                                              uid,
                                                              ids,
                                                              res_ids,
                                                              context=context)
        # TDE: arg was wiards, not ids - but new API -> multi with ensure_one
        wizard = self.browse(cr, uid, ids[0], context=context)
        # use only for allowed models in mass mailing
        if wizard.composition_mode == 'mass_mail' and \
                (wizard.mass_mailing_name or wizard.mass_mailing_id) and \
                wizard.model in [item[0] for item in self.pool['mail.mass_mailing']._get_mailing_model(cr, uid, context=context)]:
            mass_mailing = wizard.mass_mailing_id
            if not mass_mailing:
                reply_to_mode = wizard.no_auto_thread and 'email' or 'thread'
                reply_to = wizard.no_auto_thread and wizard.reply_to or False
                mass_mailing_id = self.pool['mail.mass_mailing'].create(
                    cr,
                    uid, {
                        'mass_mailing_campaign_id':
                        wizard.mass_mailing_campaign_id
                        and wizard.mass_mailing_campaign_id.id or False,
                        'name':
                        wizard.mass_mailing_name,
                        'template_id':
                        wizard.template_id and wizard.template_id.id or False,
                        'state':
                        'done',
                        'reply_to_mode':
                        reply_to_mode,
                        'reply_to':
                        reply_to,
                        'sent_date':
                        fields.datetime.now(),
                        'body_html':
                        wizard.body,
                        'mailing_model':
                        wizard.model,
                        'mailing_domain':
                        wizard.active_domain,
                    },
                    context=context)
                mass_mailing = self.pool['mail.mass_mailing'].browse(
                    cr, uid, mass_mailing_id, context=context)
            for res_id in res_ids:
                res[res_id].update({
                    'mailing_id':
                    mass_mailing.id,
                    'statistics_ids': [(0, 0, {
                        'model': wizard.model,
                        'res_id': res_id,
                        'mass_mailing_id': mass_mailing.id,
                    })],
                    # email-mode: keep original message for routing
                    'notification':
                    mass_mailing.reply_to_mode == 'thread',
                    'auto_delete':
                    not mass_mailing.keep_archives,
                })
        return res
Exemplo n.º 21
0
class BlogPost(osv.Model):
    _name = "blog.post"
    _description = "Blog Post"
    _inherit = ['mail.thread', 'website.seo.metadata', 'website.published.mixin']
    _order = 'id DESC'
    _mail_post_access = 'read'

    def _website_url(self, cr, uid, ids, field_name, arg, context=None):
        res = super(BlogPost, self)._website_url(cr, uid, ids, field_name, arg, context=context)
        for blog_post in self.browse(cr, uid, ids, context=context):
            res[blog_post.id] = "/blog/%s/post/%s" % (slug(blog_post.blog_id), slug(blog_post))
        return res

    def _compute_ranking(self, cr, uid, ids, name, arg, context=None):
        res = {}
        for blog_post in self.browse(cr, uid, ids, context=context):
            age = datetime.now() - datetime.strptime(blog_post.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
            res[blog_post.id] = blog_post.visits * (0.5+random.random()) / max(3, age.days)
        return res

    def _default_content(self, cr, uid, context=None):
        return '''  <div class="container">
                        <section class="mt16 mb16">
                            <p class="o_default_snippet_text">''' + _("Start writing here...") + '''</p>
                        </section>
                    </div> '''

    _columns = {
        'name': fields.char('Title', required=True, translate=True),
        'subtitle': fields.char('Sub Title', translate=True),
        'author_id': fields.many2one('res.partner', 'Author'),
        'cover_properties': fields.text('Cover Properties'),
        'blog_id': fields.many2one(
            'blog.blog', 'Blog',
            required=True, ondelete='cascade',
        ),
        'tag_ids': fields.many2many(
            'blog.tag', string='Tags',
        ),
        'content': fields.html('Content', translate=True, sanitize=False),
        'website_message_ids': fields.one2many(
            'mail.message', 'res_id',
            domain=lambda self: [
                '&', '&', ('model', '=', self._name), ('message_type', '=', 'comment'), ('path', '=', False)
            ],
            string='Website Messages',
            help="Website communication history",
        ),
        # creation / update stuff
        'create_date': fields.datetime(
            'Created on',
            select=True, readonly=True,
        ),
        'create_uid': fields.many2one(
            'res.users', 'Author',
            select=True, readonly=True,
        ),
        'write_date': fields.datetime(
            'Last Modified on',
            select=True, readonly=True,
        ),
        'write_uid': fields.many2one(
            'res.users', 'Last Contributor',
            select=True, readonly=True,
        ),
        'author_avatar': fields.related(
            'author_id', 'image_small',
            string="Avatar", type="binary"),
        'visits': fields.integer('No of Views'),
        'ranking': fields.function(_compute_ranking, string='Ranking', type='float'),
    }

    _defaults = {
        'name': '',
        'content': _default_content,
        'cover_properties': '{"background-image": "none", "background-color": "oe_none", "opacity": "0.6", "resize_class": ""}',
        'author_id': lambda self, cr, uid, ctx=None: self.pool['res.users'].browse(cr, uid, uid, context=ctx).partner_id.id,
    }

    def html_tag_nodes(self, html, attribute=None, tags=None, context=None):
        """ Processing of html content to tag paragraphs and set them an unique
        ID.
        :return result: (html, mappin), where html is the updated html with ID
                        and mapping is a list of (old_ID, new_ID), where old_ID
                        is None is the paragraph is a new one. """

        existing_attributes = []
        mapping = []
        if not html:
            return html, mapping
        if tags is None:
            tags = ['p']
        if attribute is None:
            attribute = 'data-unique-id'

        # form a tree
        root = lxml.html.fragment_fromstring(html, create_parent='div')
        if not len(root) and root.text is None and root.tail is None:
            return html, mapping

        # check all nodes, replace :
        # - img src -> check URL
        # - a href -> check URL
        for node in root.iter():
            if node.tag not in tags:
                continue
            ancestor_tags = [parent.tag for parent in node.iterancestors()]

            old_attribute = node.get(attribute)
            new_attribute = old_attribute
            if not new_attribute or (old_attribute in existing_attributes):
                if ancestor_tags:
                    ancestor_tags.pop()
                counter = random.randint(10000, 99999)
                ancestor_tags.append('counter_%s' % counter)
                new_attribute = '/'.join(reversed(ancestor_tags))
                node.set(attribute, new_attribute)

            existing_attributes.append(new_attribute)
            mapping.append((old_attribute, new_attribute))

        html = lxml.html.tostring(root, pretty_print=False, method='html')
        # this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that
        if html.startswith('<div>') and html.endswith('</div>'):
            html = html[5:-6]
        return html, mapping

    def _postproces_content(self, cr, uid, id, content=None, context=None):
        if content is None:
            content = self.browse(cr, uid, id, context=context).content
        if content is False:
            return content

        content, mapping = self.html_tag_nodes(content, attribute='data-chatter-id', tags=['p'], context=context)
        if id:  # not creating
            existing = [x[0] for x in mapping if x[0]]
            msg_ids = self.pool['mail.message'].search(cr, SUPERUSER_ID, [
                ('res_id', '=', id),
                ('model', '=', self._name),
                ('path', 'not in', existing),
                ('path', '!=', False)
            ], context=context)
            self.pool['mail.message'].unlink(cr, SUPERUSER_ID, msg_ids, context=context)

        return content

    def _check_for_publication(self, cr, uid, ids, vals, context=None):
        if vals.get('website_published'):
            base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
            for post in self.browse(cr, uid, ids, context=context):
                post.blog_id.message_post(
                    body='<p>%(post_publication)s <a href="%(base_url)s/blog/%(blog_slug)s/post/%(post_slug)s">%(post_link)s</a></p>' % {
                        'post_publication': _('A new post %s has been published on the %s blog.') % (post.name, post.blog_id.name),
                        'post_link': _('Click here to access the post.'),
                        'base_url': base_url,
                        'blog_slug': slug(post.blog_id),
                        'post_slug': slug(post),
                    },
                    subtype='website_blog.mt_blog_blog_published')
            return True
        return False

    def create(self, cr, uid, vals, context=None):
        if context is None:
            context = {}
        if 'content' in vals:
            vals['content'] = self._postproces_content(cr, uid, None, vals['content'], context=context)
        create_context = dict(context, mail_create_nolog=True)
        post_id = super(BlogPost, self).create(cr, uid, vals, context=create_context)
        self._check_for_publication(cr, uid, [post_id], vals, context=context)
        return post_id

    def write(self, cr, uid, ids, vals, context=None):
        if isinstance(ids, (int, long)):
            ids = [ids]
        if 'content' in vals:
            vals['content'] = self._postproces_content(cr, uid, ids[0], vals['content'], context=context)
        result = super(BlogPost, self).write(cr, uid, ids, vals, context)
        self._check_for_publication(cr, uid, ids, vals, context=context)
        return result

    def get_access_action(self, cr, uid, ids, context=None):
        """ Override method that generated the link to access the document. Instead
        of the classic form view, redirect to the post on the website directly """
        post = self.browse(cr, uid, ids[0], context=context)
        return {
            'type': 'ir.actions.act_url',
            'url': '/blog/%s/post/%s' % (post.blog_id.id, post.id),
            'target': 'self',
            'res_id': self.id,
        }

    def _notification_get_recipient_groups(self, cr, uid, ids, message, recipients, context=None):
        """ Override to set the access button: everyone can see an access button
        on their notification email. It will lead on the website view of the
        post. """
        res = super(BlogPost, self)._notification_get_recipient_groups(cr, uid, ids, message, recipients, context=context)
        access_action = self._notification_link_helper('view', model=message.model, res_id=message.res_id)
        for category, data in res.iteritems():
            res[category]['button_access'] = {'url': access_action, 'title': _('View Blog Post')}
        return res
Exemplo n.º 22
0
class product_template(osv.Model):
    _inherit = [
        "product.template", "website.seo.metadata", 'website.published.mixin',
        'rating.mixin'
    ]
    _order = 'website_published desc, website_sequence desc, name'
    _name = 'product.template'
    _mail_post_access = 'read'

    def _website_url(self, cr, uid, ids, field_name, arg, context=None):
        res = super(product_template, self)._website_url(cr,
                                                         uid,
                                                         ids,
                                                         field_name,
                                                         arg,
                                                         context=context)
        for product in self.browse(cr, uid, ids, context=context):
            res[product.id] = "/shop/product/%s" % (product.id, )
        return res

    _columns = {
        # TODO FIXME tde: when website_mail/mail_thread.py inheritance work -> this field won't be necessary
        'website_message_ids':
        fields.one2many(
            'mail.message',
            'res_id',
            domain=lambda self: [
                '&', ('model', '=', self._name),
                ('message_type', '=', 'comment')
            ],
            string='Website Comments',
        ),
        'website_description':
        fields.html('Description for the website',
                    sanitize=False,
                    translate=True),
        'alternative_product_ids':
        fields.many2many('product.template',
                         'product_alternative_rel',
                         'src_id',
                         'dest_id',
                         string='Suggested Products',
                         help='Appear on the product page'),
        'accessory_product_ids':
        fields.many2many('product.product',
                         'product_accessory_rel',
                         'src_id',
                         'dest_id',
                         string='Accessory Products',
                         help='Appear on the shopping cart'),
        'website_size_x':
        fields.integer('Size X'),
        'website_size_y':
        fields.integer('Size Y'),
        'website_style_ids':
        fields.many2many('product.style', string='Styles'),
        'website_sequence':
        fields.integer(
            'Sequence',
            help="Determine the display order in the Website E-commerce"),
        'public_categ_ids':
        fields.many2many(
            'product.public.category',
            string='Website Product Category',
            help=
            "Those categories are used to group similar products for e-commerce."
        ),
    }

    def _defaults_website_sequence(self, cr, uid, *l, **kwargs):
        cr.execute('SELECT MIN(website_sequence)-1 FROM product_template')
        next_sequence = cr.fetchone()[0] or 10
        return next_sequence

    _defaults = {
        'website_size_x': 1,
        'website_size_y': 1,
        'website_sequence': _defaults_website_sequence,
    }

    def set_sequence_top(self, cr, uid, ids, context=None):
        cr.execute('SELECT MAX(website_sequence) FROM product_template')
        max_sequence = cr.fetchone()[0] or 0
        return self.write(cr,
                          uid,
                          ids, {'website_sequence': max_sequence + 1},
                          context=context)

    def set_sequence_bottom(self, cr, uid, ids, context=None):
        cr.execute('SELECT MIN(website_sequence) FROM product_template')
        min_sequence = cr.fetchone()[0] or 0
        return self.write(cr,
                          uid,
                          ids, {'website_sequence': min_sequence - 1},
                          context=context)

    def set_sequence_up(self, cr, uid, ids, context=None):
        product = self.browse(cr, uid, ids[0], context=context)
        cr.execute("""  SELECT id, website_sequence FROM product_template
                        WHERE website_sequence > %s AND website_published = %s ORDER BY website_sequence ASC LIMIT 1"""
                   % (product.website_sequence, product.website_published))
        prev = cr.fetchone()
        if prev:
            self.write(cr,
                       uid, [prev[0]],
                       {'website_sequence': product.website_sequence},
                       context=context)
            return self.write(cr,
                              uid, [ids[0]], {'website_sequence': prev[1]},
                              context=context)
        else:
            return self.set_sequence_top(cr, uid, ids, context=context)

    def set_sequence_down(self, cr, uid, ids, context=None):
        product = self.browse(cr, uid, ids[0], context=context)
        cr.execute("""  SELECT id, website_sequence FROM product_template
                        WHERE website_sequence < %s AND website_published = %s ORDER BY website_sequence DESC LIMIT 1"""
                   % (product.website_sequence, product.website_published))
        next = cr.fetchone()
        if next:
            self.write(cr,
                       uid, [next[0]],
                       {'website_sequence': product.website_sequence},
                       context=context)
            return self.write(cr,
                              uid, [ids[0]], {'website_sequence': next[1]},
                              context=context)
        else:
            return self.set_sequence_bottom(cr, uid, ids, context=context)
Exemplo n.º 23
0
class mrp_repair_line(osv.osv, ProductChangeMixin):
    _name = 'mrp.repair.line'
    _description = 'Repair Line'

    def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
        """ Calculates amount.
        @param field_name: Name of field.
        @param arg: Argument
        @return: Dictionary of values.
        """
        res = {}
        tax_obj = self.pool.get('account.tax')
        # cur_obj = self.pool.get('res.currency')
        for line in self.browse(cr, uid, ids, context=context):
            if line.to_invoice:
                cur = line.repair_id.pricelist_id.currency_id
                taxes = tax_obj.compute_all(cr, uid, line.tax_id, line.price_unit, cur.id, line.product_uom_qty, line.product_id.id, line.repair_id.partner_id.id)
                #res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
                res[line.id] = taxes['total_included']
            else:
                res[line.id] = 0
        return res

    _columns = {
        'name': fields.char('Description', required=True),
        'repair_id': fields.many2one('mrp.repair', 'Repair Order Reference', ondelete='cascade', select=True),
        'type': fields.selection([('add', 'Add'), ('remove', 'Remove')], 'Type', required=True),
        'to_invoice': fields.boolean('To Invoice'),
        'product_id': fields.many2one('product.product', 'Product', required=True),
        'invoiced': fields.boolean('Invoiced', readonly=True, copy=False),
        'price_unit': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')),
        'price_subtotal': fields.function(_amount_line, string='Subtotal', digits=0),
        'tax_id': fields.many2many('account.tax', 'repair_operation_line_tax', 'repair_operation_line_id', 'tax_id', 'Taxes'),
        'product_uom_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
        'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
        'invoice_line_id': fields.many2one('account.invoice.line', 'Invoice Line', readonly=True, copy=False),
        'location_id': fields.many2one('stock.location', 'Source Location', required=True, select=True),
        'location_dest_id': fields.many2one('stock.location', 'Dest. Location', required=True, select=True),
        'move_id': fields.many2one('stock.move', 'Inventory Move', readonly=True, copy=False),
        'lot_id': fields.many2one('stock.production.lot', 'Lot'),
        'state': fields.selection([
                    ('draft', 'Draft'),
                    ('confirmed', 'Confirmed'),
                    ('done', 'Done'),
                    ('cancel', 'Cancelled')], 'Status', required=True, readonly=True, copy=False,
                    help=' * The \'Draft\' status is set automatically as draft when repair order in draft status. \
                        \n* The \'Confirmed\' status is set automatically as confirm when repair order in confirm status. \
                        \n* The \'Done\' status is set automatically when repair order is completed.\
                        \n* The \'Cancelled\' status is set automatically when user cancel repair order.'),
    }
    _defaults = {
        'state': lambda *a: 'draft',
        'product_uom_qty': lambda *a: 1,
    }

    def onchange_operation_type(self, cr, uid, ids, type, guarantee_limit, company_id=False, context=None):
        """ On change of operation type it sets source location, destination location
        and to invoice field.
        @param product: Changed operation type.
        @param guarantee_limit: Guarantee limit of current record.
        @return: Dictionary of values.
        """
        if not type:
            return {'value': {
                'location_id': False,
                'location_dest_id': False
                }}
        location_obj = self.pool.get('stock.location')
        warehouse_obj = self.pool.get('stock.warehouse')
        location_id = location_obj.search(cr, uid, [('usage', '=', 'production')], context=context)
        location_id = location_id and location_id[0] or False

        if type == 'add':
            # TOCHECK: Find stock location for user's company warehouse or
            # repair order's company's warehouse (company_id field is added in fix of lp:831583)
            args = company_id and [('company_id', '=', company_id)] or []
            warehouse_ids = warehouse_obj.search(cr, uid, args, context=context)
            stock_id = False
            if warehouse_ids:
                stock_id = warehouse_obj.browse(cr, uid, warehouse_ids[0], context=context).lot_stock_id.id
            to_invoice = (guarantee_limit and datetime.strptime(guarantee_limit, '%Y-%m-%d') < datetime.now())

            return {'value': {
                'to_invoice': to_invoice,
                'location_id': stock_id,
                'location_dest_id': location_id
                }}
        scrap_location_ids = location_obj.search(cr, uid, [('scrap_location', '=', True)], context=context)

        return {'value': {
                'to_invoice': False,
                'location_id': location_id,
                'location_dest_id': scrap_location_ids and scrap_location_ids[0] or False,
                }}
Exemplo n.º 24
0
class stock_landed_cost(osv.osv):
    _name = 'stock.landed.cost'
    _description = 'Stock Landed Cost'
    _inherit = 'mail.thread'

    def _total_amount(self, cr, uid, ids, name, args, context=None):
        result = {}
        for cost in self.browse(cr, uid, ids, context=context):
            total = 0.0
            for line in cost.cost_lines:
                total += line.price_unit
            result[cost.id] = total
        return result

    def _get_cost_line(self, cr, uid, ids, context=None):
        cost_to_recompute = []
        for line in self.pool.get('stock.landed.cost.lines').browse(
                cr, uid, ids, context=context):
            cost_to_recompute.append(line.cost_id.id)
        return cost_to_recompute

    def get_valuation_lines(self,
                            cr,
                            uid,
                            ids,
                            picking_ids=None,
                            context=None):
        picking_obj = self.pool.get('stock.picking')
        lines = []
        if not picking_ids:
            return lines

        for picking in picking_obj.browse(cr, uid, picking_ids):
            for move in picking.move_lines:
                #it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost
                if move.product_id.valuation != 'real_time' or move.product_id.cost_method != 'real':
                    continue
                total_cost = 0.0
                weight = move.product_id and move.product_id.weight * move.product_qty
                volume = move.product_id and move.product_id.volume * move.product_qty
                for quant in move.quant_ids:
                    total_cost += quant.cost * quant.qty
                vals = dict(product_id=move.product_id.id,
                            move_id=move.id,
                            quantity=move.product_uom_qty,
                            former_cost=total_cost,
                            weight=weight,
                            volume=volume)
                lines.append(vals)
        if not lines:
            raise UserError(
                _('The selected picking does not contain any move that would be impacted by landed costs. Landed costs are only possible for products configured in real time valuation with real price costing method. Please make sure it is the case, or you selected the correct picking'
                  ))
        return lines

    _columns = {
        'name':
        fields.char('Name',
                    track_visibility='always',
                    readonly=True,
                    copy=False),
        'date':
        fields.date('Date',
                    required=True,
                    states={'done': [('readonly', True)]},
                    track_visibility='onchange',
                    copy=False),
        'picking_ids':
        fields.many2many('stock.picking',
                         string='Pickings',
                         states={'done': [('readonly', True)]},
                         copy=False),
        'cost_lines':
        fields.one2many('stock.landed.cost.lines',
                        'cost_id',
                        'Cost Lines',
                        states={'done': [('readonly', True)]},
                        copy=True),
        'valuation_adjustment_lines':
        fields.one2many('stock.valuation.adjustment.lines',
                        'cost_id',
                        'Valuation Adjustments',
                        states={'done': [('readonly', True)]}),
        'description':
        fields.text('Item Description', states={'done': [('readonly', True)]}),
        'amount_total':
        fields.function(_total_amount,
                        type='float',
                        string='Total',
                        digits=0,
                        store={
                            'stock.landed.cost':
                            (lambda self, cr, uid, ids, c={}: ids,
                             ['cost_lines'], 20),
                            'stock.landed.cost.lines':
                            (_get_cost_line,
                             ['price_unit', 'quantity', 'cost_id'], 20),
                        },
                        track_visibility='always'),
        'state':
        fields.selection([('draft', 'Draft'), ('done', 'Posted'),
                          ('cancel', 'Cancelled')],
                         'State',
                         readonly=True,
                         track_visibility='onchange',
                         copy=False),
        'account_move_id':
        fields.many2one('account.move',
                        'Journal Entry',
                        readonly=True,
                        copy=False),
        'account_journal_id':
        fields.many2one('account.journal',
                        'Account Journal',
                        required=True,
                        states={'done': [('readonly', True)]}),
    }

    _defaults = {
        'name':
        lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(
            cr, uid, 'stock.landed.cost'),
        'state':
        'draft',
        'date':
        fields.date.context_today,
    }

    def _create_accounting_entries(self,
                                   cr,
                                   uid,
                                   line,
                                   move_id,
                                   qty_out,
                                   context=None):
        product_obj = self.pool.get('product.template')
        cost_product = line.cost_line_id and line.cost_line_id.product_id
        if not cost_product:
            return False
        accounts = product_obj.browse(cr,
                                      uid,
                                      line.product_id.product_tmpl_id.id,
                                      context=context).get_product_accounts()
        debit_account_id = accounts.get(
            'stock_valuation',
            False) and accounts['stock_valuation'].id or False
        already_out_account_id = accounts['stock_output'].id
        credit_account_id = line.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id

        if not credit_account_id:
            raise UserError(
                _('Please configure Stock Expense Account for product: %s.') %
                (cost_product.name))

        return self._create_account_move_line(cr,
                                              uid,
                                              line,
                                              move_id,
                                              credit_account_id,
                                              debit_account_id,
                                              qty_out,
                                              already_out_account_id,
                                              context=context)

    def _create_account_move_line(self,
                                  cr,
                                  uid,
                                  line,
                                  move_id,
                                  credit_account_id,
                                  debit_account_id,
                                  qty_out,
                                  already_out_account_id,
                                  context=None):
        """
        Generate the account.move.line values to track the landed cost.
        Afterwards, for the goods that are already out of stock, we should create the out moves
        """
        aml_obj = self.pool.get('account.move.line')
        if context is None:
            context = {}
        ctx = context.copy()
        ctx['check_move_validity'] = False
        base_line = {
            'name': line.name,
            'move_id': move_id,
            'product_id': line.product_id.id,
            'quantity': line.quantity,
        }
        debit_line = dict(base_line, account_id=debit_account_id)
        credit_line = dict(base_line, account_id=credit_account_id)
        diff = line.additional_landed_cost
        if diff > 0:
            debit_line['debit'] = diff
            credit_line['credit'] = diff
        else:
            # negative cost, reverse the entry
            debit_line['credit'] = -diff
            credit_line['debit'] = -diff
        aml_obj.create(cr, uid, debit_line, context=ctx)
        aml_obj.create(cr, uid, credit_line, context=ctx)

        #Create account move lines for quants already out of stock
        if qty_out > 0:
            debit_line = dict(debit_line,
                              name=(line.name + ": " + str(qty_out) +
                                    _(' already out')),
                              quantity=qty_out,
                              account_id=already_out_account_id)
            credit_line = dict(credit_line,
                               name=(line.name + ": " + str(qty_out) +
                                     _(' already out')),
                               quantity=qty_out,
                               account_id=debit_account_id)
            diff = diff * qty_out / line.quantity
            if diff > 0:
                debit_line['debit'] = diff
                credit_line['credit'] = diff
            else:
                # negative cost, reverse the entry
                debit_line['credit'] = -diff
                credit_line['debit'] = -diff
            aml_obj.create(cr, uid, debit_line, context=ctx)
            aml_obj.create(cr, uid, credit_line, context=ctx)
        self.pool.get('account.move').assert_balanced(cr,
                                                      uid, [move_id],
                                                      context=context)
        return True

    def _create_account_move(self, cr, uid, cost, context=None):
        vals = {
            'journal_id': cost.account_journal_id.id,
            'date': cost.date,
            'ref': cost.name
        }
        return self.pool.get('account.move').create(cr,
                                                    uid,
                                                    vals,
                                                    context=context)

    def _check_sum(self, cr, uid, landed_cost, context=None):
        """
        Will check if each cost line its valuation lines sum to the correct amount
        and if the overall total amount is correct also
        """
        costcor = {}
        tot = 0
        for valuation_line in landed_cost.valuation_adjustment_lines:
            if costcor.get(valuation_line.cost_line_id):
                costcor[valuation_line.
                        cost_line_id] += valuation_line.additional_landed_cost
            else:
                costcor[valuation_line.
                        cost_line_id] = valuation_line.additional_landed_cost
            tot += valuation_line.additional_landed_cost

        prec = self.pool['decimal.precision'].precision_get(cr, uid, 'Account')
        # float_compare returns 0 for equal amounts
        res = not bool(
            float_compare(tot, landed_cost.amount_total,
                          precision_digits=prec))
        for costl in costcor.keys():
            if float_compare(costcor[costl],
                             costl.price_unit,
                             precision_digits=prec):
                res = False
        return res

    def button_validate(self, cr, uid, ids, context=None):
        quant_obj = self.pool.get('stock.quant')

        for cost in self.browse(cr, uid, ids, context=context):
            if cost.state != 'draft':
                raise UserError(_('Only draft landed costs can be validated'))
            if not cost.valuation_adjustment_lines or not self._check_sum(
                    cr, uid, cost, context=context):
                raise UserError(
                    _('You cannot validate a landed cost which has no valid valuation adjustments lines. Did you click on Compute?'
                      ))
            move_id = self._create_account_move(cr, uid, cost, context=context)
            for line in cost.valuation_adjustment_lines:
                if not line.move_id:
                    continue
                per_unit = line.final_cost / line.quantity
                diff = per_unit - line.former_cost_per_unit
                quants = [quant for quant in line.move_id.quant_ids]
                quant_dict = {}
                for quant in quants:
                    if quant.id not in quant_dict:
                        quant_dict[quant.id] = quant.cost + diff
                    else:
                        quant_dict[quant.id] += diff
                for key, value in quant_dict.items():
                    quant_obj.write(cr,
                                    SUPERUSER_ID,
                                    key, {'cost': value},
                                    context=context)
                qty_out = 0
                for quant in line.move_id.quant_ids:
                    if quant.location_id.usage != 'internal':
                        qty_out += quant.qty
                self._create_accounting_entries(cr,
                                                uid,
                                                line,
                                                move_id,
                                                qty_out,
                                                context=context)
            self.write(cr,
                       uid,
                       cost.id, {
                           'state': 'done',
                           'account_move_id': move_id
                       },
                       context=context)
            self.pool.get('account.move').post(cr,
                                               uid, [move_id],
                                               context=context)
        return True

    def button_cancel(self, cr, uid, ids, context=None):
        cost = self.browse(cr, uid, ids, context=context)
        if cost.state == 'done':
            raise UserError(
                _('Validated landed costs cannot be cancelled, '
                  'but you could create negative landed costs to reverse them')
            )
        return cost.write({'state': 'cancel'})

    def unlink(self, cr, uid, ids, context=None):
        # cancel or raise first
        self.button_cancel(cr, uid, ids, context)
        return super(stock_landed_cost, self).unlink(cr,
                                                     uid,
                                                     ids,
                                                     context=context)

    def compute_landed_cost(self, cr, uid, ids, context=None):
        line_obj = self.pool.get('stock.valuation.adjustment.lines')
        unlink_ids = line_obj.search(cr,
                                     uid, [('cost_id', 'in', ids)],
                                     context=context)
        line_obj.unlink(cr, uid, unlink_ids, context=context)
        digits = dp.get_precision('Product Price')(cr)
        towrite_dict = {}
        for cost in self.browse(cr, uid, ids, context=None):
            if not cost.picking_ids:
                continue
            picking_ids = [p.id for p in cost.picking_ids]
            total_qty = 0.0
            total_cost = 0.0
            total_weight = 0.0
            total_volume = 0.0
            total_line = 0.0
            vals = self.get_valuation_lines(cr,
                                            uid, [cost.id],
                                            picking_ids=picking_ids,
                                            context=context)
            for v in vals:
                for line in cost.cost_lines:
                    v.update({'cost_id': cost.id, 'cost_line_id': line.id})
                    self.pool.get('stock.valuation.adjustment.lines').create(
                        cr, uid, v, context=context)
                total_qty += v.get('quantity', 0.0)
                total_cost += v.get('former_cost', 0.0)
                total_weight += v.get('weight', 0.0)
                total_volume += v.get('volume', 0.0)
                total_line += 1

            for line in cost.cost_lines:
                value_split = 0.0
                for valuation in cost.valuation_adjustment_lines:
                    value = 0.0
                    if valuation.cost_line_id and valuation.cost_line_id.id == line.id:
                        if line.split_method == 'by_quantity' and total_qty:
                            per_unit = (line.price_unit / total_qty)
                            value = valuation.quantity * per_unit
                        elif line.split_method == 'by_weight' and total_weight:
                            per_unit = (line.price_unit / total_weight)
                            value = valuation.weight * per_unit
                        elif line.split_method == 'by_volume' and total_volume:
                            per_unit = (line.price_unit / total_volume)
                            value = valuation.volume * per_unit
                        elif line.split_method == 'equal':
                            value = (line.price_unit / total_line)
                        elif line.split_method == 'by_current_cost_price' and total_cost:
                            per_unit = (line.price_unit / total_cost)
                            value = valuation.former_cost * per_unit
                        else:
                            value = (line.price_unit / total_line)

                        if digits:
                            value = float_round(value,
                                                precision_digits=digits[1],
                                                rounding_method='UP')
                            fnc = min if line.price_unit > 0 else max
                            value = fnc(value, line.price_unit - value_split)
                            value_split += value

                        if valuation.id not in towrite_dict:
                            towrite_dict[valuation.id] = value
                        else:
                            towrite_dict[valuation.id] += value
        if towrite_dict:
            for key, value in towrite_dict.items():
                line_obj.write(cr,
                               uid,
                               key, {'additional_landed_cost': value},
                               context=context)
        return True

    def _track_subtype(self, cr, uid, ids, init_values, context=None):
        record = self.browse(cr, uid, ids[0], context=context)
        if 'state' in init_values and record.state == 'done':
            return 'stock_landed_costs.mt_stock_landed_cost_open'
        return super(stock_landed_cost, self)._track_subtype(cr,
                                                             uid,
                                                             ids,
                                                             init_values,
                                                             context=context)
Exemplo n.º 25
0
class gamification_badge(osv.Model):
    """Badge object that users can send and receive"""

    CAN_GRANT = 1
    NOBODY_CAN_GRANT = 2
    USER_NOT_VIP = 3
    BADGE_REQUIRED = 4
    TOO_MANY = 5

    _name = 'gamification.badge'
    _description = 'Gamification badge'
    _inherit = ['mail.thread']

    def _get_owners_info(self, cr, uid, ids, name, args, context=None):
        """Return:
            the list of unique res.users ids having received this badge
            the total number of time this badge was granted
            the total number of users this badge was granted to
        """
        result = dict((res_id, {'stat_count': 0, 'stat_count_distinct': 0, 'unique_owner_ids': []}) for res_id in ids)

        cr.execute("""
            SELECT badge_id, count(user_id) as stat_count,
                count(distinct(user_id)) as stat_count_distinct,
                array_agg(distinct(user_id)) as unique_owner_ids
            FROM gamification_badge_user
            WHERE badge_id in %s
            GROUP BY badge_id
            """, (tuple(ids),))
        for (badge_id, stat_count, stat_count_distinct, unique_owner_ids) in cr.fetchall():
            result[badge_id] = {
                'stat_count': stat_count,
                'stat_count_distinct': stat_count_distinct,
                'unique_owner_ids': unique_owner_ids,
            }
        return result

    def _get_badge_user_stats(self, cr, uid, ids, name, args, context=None):
        """Return stats related to badge users"""
        result = dict.fromkeys(ids, False)
        badge_user_obj = self.pool.get('gamification.badge.user')
        first_month_day = date.today().replace(day=1).strftime(DF)
        for bid in ids:
            result[bid] = {
                'stat_my': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid)], context=context, count=True),
                'stat_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_date', '>=', first_month_day)], context=context, count=True),
                'stat_my_this_month': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('user_id', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True),
                'stat_my_monthly_sending': badge_user_obj.search(cr, uid, [('badge_id', '=', bid), ('create_uid', '=', uid), ('create_date', '>=', first_month_day)], context=context, count=True)
            }
        return result

    def _remaining_sending_calc(self, cr, uid, ids, name, args, context=None):
        """Computes the number of badges remaining the user can send

        0 if not allowed or no remaining
        integer if limited sending
        -1 if infinite (should not be displayed)
        """
        result = dict.fromkeys(ids, False)
        for badge in self.browse(cr, uid, ids, context=context):
            if self._can_grant_badge(cr, uid, badge.id, context) != 1:
                # if the user cannot grant this badge at all, result is 0
                result[badge.id] = 0
            elif not badge.rule_max:
                # if there is no limitation, -1 is returned which means 'infinite'
                result[badge.id] = -1
            else:
                result[badge.id] = badge.rule_max_number - badge.stat_my_monthly_sending
        return result

    _columns = {
        'name': fields.char('Badge', required=True, translate=True),
        'description': fields.text('Description', translate=True),
        'image': fields.binary("Image", attachment=True,
            help="This field holds the image used for the badge, limited to 256x256"),
        'rule_auth': fields.selection([
                ('everyone', 'Everyone'),
                ('users', 'A selected list of users'),
                ('having', 'People having some badges'),
                ('nobody', 'No one, assigned through challenges'),
            ],
            string="Allowance to Grant",
            help="Who can grant this badge",
            required=True),
        'rule_auth_user_ids': fields.many2many('res.users', 'rel_badge_auth_users',
            string='Authorized Users',
            help="Only these people can give this badge"),
        'rule_auth_badge_ids': fields.many2many('gamification.badge',
            'gamification_badge_rule_badge_rel', 'badge1_id', 'badge2_id',
            string='Required Badges',
            help="Only the people having these badges can give this badge"),

        'rule_max': fields.boolean('Monthly Limited Sending',
            help="Check to set a monthly limit per person of sending this badge"),
        'rule_max_number': fields.integer('Limitation Number',
            help="The maximum number of time this badge can be sent per month per person."),
        'stat_my_monthly_sending': fields.function(_get_badge_user_stats,
            type="integer",
            string='My Monthly Sending Total',
            multi='badge_users',
            help="The number of time the current user has sent this badge this month."),
        'remaining_sending': fields.function(_remaining_sending_calc, type='integer',
            string='Remaining Sending Allowed', help="If a maxium is set"),

        'challenge_ids': fields.one2many('gamification.challenge', 'reward_id',
            string="Reward of Challenges"),

        'goal_definition_ids': fields.many2many('gamification.goal.definition', 'badge_unlocked_definition_rel',
            string='Rewarded by',
            help="The users that have succeeded theses goals will receive automatically the badge."),

        'owner_ids': fields.one2many('gamification.badge.user', 'badge_id',
            string='Owners', help='The list of instances of this badge granted to users'),
        'active': fields.boolean('Active'),
        'unique_owner_ids': fields.function(_get_owners_info,
            string='Unique Owners',
            help="The list of unique users having received this badge.",
            multi='unique_users',
            type="many2many", relation="res.users"),

        'stat_count': fields.function(_get_owners_info, string='Total',
            type="integer",
            multi='unique_users',
            help="The number of time this badge has been received."),
        'stat_count_distinct': fields.function(_get_owners_info,
            type="integer",
            string='Number of users',
            multi='unique_users',
            help="The number of time this badge has been received by unique users."),
        'stat_this_month': fields.function(_get_badge_user_stats,
            type="integer",
            string='Monthly total',
            multi='badge_users',
            help="The number of time this badge has been received this month."),
        'stat_my': fields.function(_get_badge_user_stats, string='My Total',
            type="integer",
            multi='badge_users',
            help="The number of time the current user has received this badge."),
        'stat_my_this_month': fields.function(_get_badge_user_stats,
            type="integer",
            string='My Monthly Total',
            multi='badge_users',
            help="The number of time the current user has received this badge this month."),
    }

    _defaults = {
        'rule_auth': 'everyone',
        'active': True,
    }

    def check_granting(self, cr, uid, badge_id, context=None):
        """Check the user 'uid' can grant the badge 'badge_id' and raise the appropriate exception
        if not

        Do not check for SUPERUSER_ID
        """
        status_code = self._can_grant_badge(cr, uid, badge_id, context=context)
        if status_code == self.CAN_GRANT:
            return True
        elif status_code == self.NOBODY_CAN_GRANT:
            raise UserError(_('This badge can not be sent by users.'))
        elif status_code == self.USER_NOT_VIP:
            raise UserError(_('You are not in the user allowed list.'))
        elif status_code == self.BADGE_REQUIRED:
            raise UserError(_('You do not have the required badges.'))
        elif status_code == self.TOO_MANY:
            raise UserError(_('You have already sent this badge too many time this month.'))
        else:
            _logger.exception("Unknown badge status code: %d" % int(status_code))
        return False

    def _can_grant_badge(self, cr, uid, badge_id, context=None):
        """Check if a user can grant a badge to another user

        :param uid: the id of the res.users trying to send the badge
        :param badge_id: the granted badge id
        :return: integer representing the permission.
        """
        if uid == SUPERUSER_ID:
            return self.CAN_GRANT

        badge = self.browse(cr, uid, badge_id, context=context)

        if badge.rule_auth == 'nobody':
            return self.NOBODY_CAN_GRANT

        elif badge.rule_auth == 'users' and uid not in [user.id for user in badge.rule_auth_user_ids]:
            return self.USER_NOT_VIP

        elif badge.rule_auth == 'having':
            all_user_badges = self.pool.get('gamification.badge.user').search(cr, uid, [('user_id', '=', uid)], context=context)
            for required_badge in badge.rule_auth_badge_ids:
                if required_badge.id not in all_user_badges:
                    return self.BADGE_REQUIRED

        if badge.rule_max and badge.stat_my_monthly_sending >= badge.rule_max_number:
            return self.TOO_MANY

        # badge.rule_auth == 'everyone' -> no check
        return self.CAN_GRANT

    def check_progress(self, cr, uid, context=None):
        try:
            model, res_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'gamification', 'badge_hidden')
        except ValueError:
            return True
        badge_user_obj = self.pool.get('gamification.badge.user')
        if not badge_user_obj.search(cr, uid, [('user_id', '=', uid), ('badge_id', '=', res_id)], context=context):
            values = {
                'user_id': uid,
                'badge_id': res_id,
            }
            badge_user_obj.create(cr, SUPERUSER_ID, values, context=context)
        return True
Exemplo n.º 26
0
class note_note(osv.osv):
    """ Note """
    _name = 'note.note'
    _inherit = ['mail.thread']
    _description = "Note"

    #writing method (no modification of values)
    def name_create(self, cr, uid, name, context=None):
        rec_id = self.create(cr, uid, {'memo': name}, context=context)
        return self.name_get(cr, uid, [rec_id], context)[0]

    #read the first line (convert hml into text)
    def _get_note_first_line(self,
                             cr,
                             uid,
                             ids,
                             name="",
                             args={},
                             context=None):
        res = {}
        for note in self.browse(cr, uid, ids, context=context):
            res[note.id] = (note.memo and html2plaintext(note.memo)
                            or "").strip().replace('*', '').split("\n")[0]

        return res

    def onclick_note_is_done(self, cr, uid, ids, context=None):
        return self.write(cr,
                          uid,
                          ids, {
                              'open': False,
                              'date_done': fields.date.today()
                          },
                          context=context)

    def onclick_note_not_done(self, cr, uid, ids, context=None):
        return self.write(cr, uid, ids, {'open': True}, context=context)

    #return the default stage for the uid user
    def _get_default_stage_id(self, cr, uid, context=None):
        ids = self.pool.get('note.stage').search(cr,
                                                 uid, [('user_id', '=', uid)],
                                                 context=context)
        return ids and ids[0] or False

    def _set_stage_per_user(self,
                            cr,
                            uid,
                            id,
                            name,
                            value,
                            args=None,
                            context=None):
        note = self.browse(cr, uid, id, context=context)
        if not value: return False
        stage_ids = [value] + [
            stage.id for stage in note.stage_ids if stage.user_id.id != uid
        ]
        return self.write(cr,
                          uid, [id], {'stage_ids': [(6, 0, set(stage_ids))]},
                          context=context)

    def _get_stage_per_user(self, cr, uid, ids, name, args, context=None):
        result = dict.fromkeys(ids, False)
        for record in self.browse(cr, uid, ids, context=context):
            for stage in record.stage_ids:
                if stage.user_id.id == uid:
                    result[record.id] = stage.id
        return result

    _columns = {
        'name':
        fields.function(_get_note_first_line,
                        string='Note Summary',
                        type='text',
                        store=True),
        'user_id':
        fields.many2one('res.users', 'Owner'),
        'memo':
        fields.html('Note Content'),
        'sequence':
        fields.integer('Sequence'),
        'stage_id':
        fields.function(_get_stage_per_user,
                        fnct_inv=_set_stage_per_user,
                        string='Stage',
                        type='many2one',
                        relation='note.stage'),
        'stage_ids':
        fields.many2many('note.stage', 'note_stage_rel', 'note_id', 'stage_id',
                         'Stages of Users'),
        'open':
        fields.boolean('Active', track_visibility='onchange'),
        'date_done':
        fields.date('Date done'),
        'color':
        fields.integer('Color Index'),
        'tag_ids':
        fields.many2many('note.tag', 'note_tags_rel', 'note_id', 'tag_id',
                         'Tags'),
    }
    _defaults = {
        'user_id': lambda self, cr, uid, ctx=None: uid,
        'open': 1,
        'stage_id': _get_default_stage_id,
    }
    _order = 'sequence'

    def read_group(self,
                   cr,
                   uid,
                   domain,
                   fields,
                   groupby,
                   offset=0,
                   limit=None,
                   context=None,
                   orderby=False,
                   lazy=True):
        if groupby and groupby[0] == "stage_id":

            #search all stages
            current_stage_ids = self.pool.get('note.stage').search(
                cr, uid, [('user_id', '=', uid)], context=context)

            if current_stage_ids:  #if the user have some stages
                stages = self.pool['note.stage'].browse(cr,
                                                        uid,
                                                        current_stage_ids,
                                                        context=context)

                result = [{ #notes by stage for stages user
                        '__context': {'group_by': groupby[1:]},
                        '__domain': domain + [('stage_ids.id', '=', stage.id)],
                        'stage_id': (stage.id, stage.name),
                        'stage_id_count': self.search(cr,uid, domain+[('stage_ids', '=', stage.id)], context=context, count=True),
                        '__fold': stage.fold,
                    } for stage in stages]

                #note without user's stage
                nb_notes_ws = self.search(
                    cr,
                    uid,
                    domain + [('stage_ids', 'not in', current_stage_ids)],
                    context=context,
                    count=True)
                if nb_notes_ws:
                    # add note to the first column if it's the first stage
                    dom_not_in = ('stage_ids', 'not in', current_stage_ids)
                    if result and result[0]['stage_id'][
                            0] == current_stage_ids[0]:
                        dom_in = result[0]['__domain'].pop()
                        result[0]['__domain'] = domain + [
                            '|', dom_in, dom_not_in
                        ]
                        result[0]['stage_id_count'] += nb_notes_ws
                    else:
                        # add the first stage column
                        result = [{
                            '__context': {
                                'group_by': groupby[1:]
                            },
                            '__domain': domain + [dom_not_in],
                            'stage_id': (stages[0].id, stages[0].name),
                            'stage_id_count': nb_notes_ws,
                            '__fold': stages[0].name,
                        }] + result

            else:  # if stage_ids is empty

                #note without user's stage
                nb_notes_ws = self.search(cr,
                                          uid,
                                          domain,
                                          context=context,
                                          count=True)
                if nb_notes_ws:
                    result = [{  #notes for unknown stage
                        '__context': {
                            'group_by': groupby[1:]
                        },
                        '__domain': domain,
                        'stage_id': False,
                        'stage_id_count': nb_notes_ws
                    }]
                else:
                    result = []
            return result

        else:
            return super(note_note, self).read_group(cr,
                                                     uid,
                                                     domain,
                                                     fields,
                                                     groupby,
                                                     offset=offset,
                                                     limit=limit,
                                                     context=context,
                                                     orderby=orderby,
                                                     lazy=lazy)

    def _notification_get_recipient_groups(self,
                                           cr,
                                           uid,
                                           ids,
                                           message,
                                           recipients,
                                           context=None):
        res = super(note_note,
                    self)._notification_get_recipient_groups(cr,
                                                             uid,
                                                             ids,
                                                             message,
                                                             recipients,
                                                             context=context)
        new_action_id = self.pool['ir.model.data'].xmlid_to_res_id(
            cr, uid, 'note.action_note_note')
        new_action = self._notification_link_helper(cr,
                                                    uid,
                                                    ids,
                                                    'new',
                                                    context=context,
                                                    action_id=new_action_id)
        res['user'] = {
            'actions': [{
                'url': new_action,
                'title': _('New Note')
            }]
        }
        return res
Exemplo n.º 27
0
class CountryGroup(osv.Model):
    _inherit = 'res.country.group'
    _columns = {
        'website_pricelist_ids': fields.many2many('website_pricelist', 'res_country_group_website_pricelist_rel',
                                                  'res_country_group_id', 'website_pricelist_id', string='Website Price Lists'),
    }
Exemplo n.º 28
0
class make_procurement(osv.osv_memory):
    _name = 'make.procurement'
    _description = 'Make Procurements'

    def onchange_product_id(self, cr, uid, ids, prod_id, context=None):
        product = self.pool.get('product.product').browse(cr,
                                                          uid,
                                                          prod_id,
                                                          context=context)
        return {
            'value': {
                'uom_id':
                product.uom_id.id,
                'product_tmpl_id':
                product.product_tmpl_id.id,
                'product_variant_count':
                product.product_tmpl_id.product_variant_count
            }
        }

    _columns = {
        'qty':
        fields.float('Quantity', digits=(16, 2), required=True),
        'res_model':
        fields.char('Res Model'),
        'product_id':
        fields.many2one('product.product', 'Product', required=True),
        'product_tmpl_id':
        fields.many2one('product.template', 'Template', required=True),
        'product_variant_count':
        fields.related('product_tmpl_id',
                       'product_variant_count',
                       type='integer',
                       string='Variant Number'),
        'uom_id':
        fields.many2one('product.uom', 'Unit of Measure', required=True),
        'warehouse_id':
        fields.many2one('stock.warehouse', 'Warehouse', required=True),
        'date_planned':
        fields.date('Planned Date', required=True),
        'route_ids':
        fields.many2many('stock.location.route', string='Preferred Routes'),
    }

    _defaults = {
        'date_planned': fields.date.context_today,
        'qty': lambda *args: 1.0,
    }

    def make_procurement(self, cr, uid, ids, context=None):
        """ Creates procurement order for selected product. """
        user = self.pool.get('res.users').browse(cr, uid, uid,
                                                 context=context).login
        wh_obj = self.pool.get('stock.warehouse')
        procurement_obj = self.pool.get('procurement.order')
        data_obj = self.pool.get('ir.model.data')

        for proc in self.browse(cr, uid, ids, context=context):
            wh = wh_obj.browse(cr, uid, proc.warehouse_id.id, context=context)
            procure_id = procurement_obj.create(
                cr, uid, {
                    'name': 'INT: ' + str(user),
                    'date_planned': proc.date_planned,
                    'product_id': proc.product_id.id,
                    'product_qty': proc.qty,
                    'product_uom': proc.uom_id.id,
                    'warehouse_id': proc.warehouse_id.id,
                    'location_id': wh.lot_stock_id.id,
                    'company_id': wh.company_id.id,
                    'route_ids': [(6, 0, proc.route_ids.ids)],
                })
            procurement_obj.signal_workflow(cr, uid, [procure_id],
                                            'button_confirm')

        id2 = data_obj._get_id(cr, uid, 'procurement', 'procurement_tree_view')
        id3 = data_obj._get_id(cr, uid, 'procurement', 'procurement_form_view')

        if id2:
            id2 = data_obj.browse(cr, uid, id2, context=context).res_id
        if id3:
            id3 = data_obj.browse(cr, uid, id3, context=context).res_id

        return {
            'view_type': 'form',
            'view_mode': 'tree,form',
            'res_model': 'procurement.order',
            'res_id': procure_id,
            'views': [(id3, 'form'), (id2, 'tree')],
            'type': 'ir.actions.act_window',
        }

    def default_get(self, cr, uid, fields, context=None):
        if context is None:
            context = {}
        record_id = context.get('active_id')

        if context.get('active_model') == 'product.template':
            product_ids = self.pool.get('product.product').search(
                cr,
                uid, [('product_tmpl_id', '=', context.get('active_id'))],
                context=context)
            if product_ids:
                record_id = product_ids[0]

        res = super(make_procurement, self).default_get(cr,
                                                        uid,
                                                        fields,
                                                        context=context)

        if record_id and 'product_id' in fields:
            proxy = self.pool.get('product.product')
            product_ids = proxy.search(cr,
                                       uid, [('id', '=', record_id)],
                                       context=context,
                                       limit=1)
            if product_ids:
                product_id = product_ids[0]

                product = self.pool.get('product.product').browse(
                    cr, uid, product_id, context=context)
                res['product_id'] = product.id
                res['uom_id'] = product.uom_id.id

        if 'warehouse_id' in fields:
            warehouse_id = self.pool.get('stock.warehouse').search(
                cr, uid, [], context=context)
            res['warehouse_id'] = warehouse_id[0] if warehouse_id else False

        return res

    def create(self, cr, uid, values, context=None):
        if values.get('product_id'):
            values.update(
                self.onchange_product_id(cr,
                                         uid,
                                         None,
                                         values['product_id'],
                                         context=context)['value'])
        return super(make_procurement, self).create(cr,
                                                    uid,
                                                    values,
                                                    context=context)
Exemplo n.º 29
0
class MassMailing(osv.Model):
    """ MassMailing models a wave of emails for a mass mailign campaign.
    A mass mailing is an occurence of sending emails. """

    _name = 'mail.mass_mailing'
    _description = 'Mass Mailing'
    # number of periods for tracking mail_mail statistics
    _period_number = 6
    _order = 'sent_date DESC'
    # _send_trigger = 5  # Number under which mails are send directly

    _inherit = ['utm.mixin']

    def _get_statistics(self, cr, uid, ids, name, arg, context=None):
        """ Compute statistics of the mass mailing """
        results = {}
        cr.execute(
            """
            SELECT
                m.id as mailing_id,
                COUNT(s.id) AS total,
                COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null THEN 1 ELSE null END) AS scheduled,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
                COUNT(CASE WHEN s.sent is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
                COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
                COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied,
                COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced,
                COUNT(CASE WHEN s.exception is not null THEN 1 ELSE null END) AS failed
            FROM
                mail_mail_statistics s
            RIGHT JOIN
                mail_mass_mailing m
                ON (m.id = s.mass_mailing_id)
            WHERE
                m.id IN %s
            GROUP BY
                m.id
        """, (tuple(ids), ))
        for row in cr.dictfetchall():
            results[row.pop('mailing_id')] = row
            total = row['total'] or 1
            row['received_ratio'] = 100.0 * row['delivered'] / total
            row['opened_ratio'] = 100.0 * row['opened'] / total
            row['replied_ratio'] = 100.0 * row['replied'] / total
            row['bounced_ratio'] = 100.0 * row['bounced'] / total
        return results

    def _get_mailing_model(self, cr, uid, context=None):
        res = []
        for model_name in self.pool:
            model = self.pool[model_name]
            if hasattr(model, '_mail_mass_mailing') and getattr(
                    model, '_mail_mass_mailing'):
                res.append((model._name, getattr(model, '_mail_mass_mailing')))
        res.append(('mail.mass_mailing.contact', _('Mailing List')))
        return res

    def _get_clicks_ratio(self, cr, uid, ids, name, arg, context=None):
        res = dict.fromkeys(ids, 0)
        cr.execute(
            """
            SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_id AS id 
            FROM mail_mail_statistics AS stats
            LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id
            WHERE stats.mass_mailing_id IN %s
            GROUP BY stats.mass_mailing_id
        """, (tuple(ids), ))

        for record in cr.dictfetchall():
            res[record['id']] = 100 * record['nb_clicks'] / record['nb_mails']

        return res

    def _get_next_departure(self, cr, uid, ids, name, arg, context=None):
        mass_mailings = self.browse(cr, uid, ids, context=context)
        cron_next_call = self.pool.get('ir.model.data').xmlid_to_object(
            cr,
            SUPERUSER_ID,
            'mass_mailing.ir_cron_mass_mailing_queue',
            context=context).nextcall

        result = {}
        for mass_mailing in mass_mailings:
            schedule_date = mass_mailing.schedule_date
            if schedule_date:
                if datetime.now() > datetime.strptime(
                        schedule_date, tools.DEFAULT_SERVER_DATETIME_FORMAT):
                    result[mass_mailing.id] = cron_next_call
                else:
                    result[mass_mailing.id] = schedule_date
            else:
                result[mass_mailing.id] = cron_next_call
        return result

    def _get_total(self, cr, uid, ids, name, arg, context=None):
        mass_mailings = self.browse(cr, uid, ids, context=context)

        result = {}
        for mass_mailing in mass_mailings:
            mailing = self.browse(cr, uid, mass_mailing.id, context=context)
            result[mass_mailing.id] = len(
                self.get_recipients(cr, SUPERUSER_ID, mailing,
                                    context=context))
        return result

    # indirections for inheritance
    _mailing_model = lambda self, *args, **kwargs: self._get_mailing_model(
        *args, **kwargs)

    _columns = {
        'name':
        fields.char('Subject', required=True),
        'active':
        fields.boolean('Active'),
        'email_from':
        fields.char('From', required=True),
        'create_date':
        fields.datetime('Creation Date'),
        'sent_date':
        fields.datetime('Sent Date', oldname='date', copy=False),
        'schedule_date':
        fields.datetime('Schedule in the Future'),
        'body_html':
        fields.html('Body', translate=True),
        'attachment_ids':
        fields.many2many('ir.attachment', 'mass_mailing_ir_attachments_rel',
                         'mass_mailing_id', 'attachment_id', 'Attachments'),
        'keep_archives':
        fields.boolean('Keep Archives'),
        'mass_mailing_campaign_id':
        fields.many2one(
            'mail.mass_mailing.campaign',
            'Mass Mailing Campaign',
            ondelete='set null',
        ),
        'clicks_ratio':
        fields.function(
            _get_clicks_ratio,
            string="Number of Clicks",
            type="integer",
        ),
        'state':
        fields.selection([('draft', 'Draft'), ('in_queue', 'In Queue'),
                          ('sending', 'Sending'), ('done', 'Sent')],
                         string='Status',
                         required=True,
                         copy=False),
        'color':
        fields.related(
            'mass_mailing_campaign_id',
            'color',
            type='integer',
            string='Color Index',
        ),
        # mailing options
        'reply_to_mode':
        fields.selection(
            [('thread', 'Followers of leads/applicants'),
             ('email', 'Specified Email Address')],
            string='Reply-To Mode',
            required=True,
        ),
        'reply_to':
        fields.char('Reply To', help='Preferred Reply-To Address'),
        # recipients
        'mailing_model':
        fields.selection(_mailing_model,
                         string='Recipients Model',
                         required=True),
        'mailing_domain':
        fields.char('Domain', oldname='domain'),
        'contact_list_ids':
        fields.many2many(
            'mail.mass_mailing.list',
            'mail_mass_mailing_list_rel',
            string='Mailing Lists',
        ),
        'contact_ab_pc':
        fields.integer(
            'A/B Testing percentage',
            help=
            'Percentage of the contacts that will be mailed. Recipients will be taken randomly.'
        ),
        # statistics data
        'statistics_ids':
        fields.one2many(
            'mail.mail.statistics',
            'mass_mailing_id',
            'Emails Statistics',
        ),
        'total':
        fields.function(
            _get_total,
            string='Total',
            type='integer',
        ),
        'scheduled':
        fields.function(
            _get_statistics,
            string='Scheduled',
            type='integer',
            multi='_get_statistics',
        ),
        'failed':
        fields.function(
            _get_statistics,
            string='Failed',
            type='integer',
            multi='_get_statistics',
        ),
        'sent':
        fields.function(
            _get_statistics,
            string='Sent',
            type='integer',
            multi='_get_statistics',
        ),
        'delivered':
        fields.function(
            _get_statistics,
            string='Delivered',
            type='integer',
            multi='_get_statistics',
        ),
        'opened':
        fields.function(
            _get_statistics,
            string='Opened',
            type='integer',
            multi='_get_statistics',
        ),
        'replied':
        fields.function(
            _get_statistics,
            string='Replied',
            type='integer',
            multi='_get_statistics',
        ),
        'bounced':
        fields.function(
            _get_statistics,
            string='Bounced',
            type='integer',
            multi='_get_statistics',
        ),
        'failed':
        fields.function(
            _get_statistics,
            string='Failed',
            type='integer',
            multi='_get_statistics',
        ),
        'received_ratio':
        fields.function(
            _get_statistics,
            string='Received Ratio',
            type='integer',
            multi='_get_statistics',
        ),
        'opened_ratio':
        fields.function(
            _get_statistics,
            string='Opened Ratio',
            type='integer',
            multi='_get_statistics',
        ),
        'replied_ratio':
        fields.function(
            _get_statistics,
            string='Replied Ratio',
            type='integer',
            multi='_get_statistics',
        ),
        'bounced_ratio':
        fields.function(
            _get_statistics,
            String='Bouncded Ratio',
            type='integer',
            multi='_get_statistics',
        ),
        'next_departure':
        fields.function(_get_next_departure,
                        string='Next Departure',
                        type='datetime'),
    }

    def mass_mailing_statistics_action(self, cr, uid, ids, context=None):
        res = self.pool['ir.actions.act_window'].for_xml_id(
            cr,
            uid,
            'mass_mailing',
            'action_view_mass_mailing_statistics',
            context=context)
        link_click_ids = self.pool['link.tracker.click'].search(
            cr, uid, [('mass_mailing_id', 'in', ids)], context=context)
        res['domain'] = [('id', 'in', link_click_ids)]
        return res

    def default_get(self, cr, uid, fields, context=None):
        res = super(MassMailing, self).default_get(cr,
                                                   uid,
                                                   fields,
                                                   context=context)
        if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get(
                'mailing_model'):
            if res['mailing_model'] in [
                    'res.partner', 'mail.mass_mailing.contact'
            ]:
                res['reply_to_mode'] = 'email'
            else:
                res['reply_to_mode'] = 'thread'
        return res

    _defaults = {
        'active':
        True,
        'state':
        'draft',
        'email_from':
        lambda self, cr, uid, ctx=None: self.pool[
            'mail.message']._get_default_from(cr, uid, context=ctx),
        'reply_to':
        lambda self, cr, uid, ctx=None: self.pool['mail.message'].
        _get_default_from(cr, uid, context=ctx),
        'mailing_model':
        'mail.mass_mailing.contact',
        'contact_ab_pc':
        100,
        'mailing_domain': [],
    }

    def onchange_mass_mailing_campaign_id(self,
                                          cr,
                                          uid,
                                          id,
                                          mass_mailing_campaign_ids,
                                          context=None):
        if mass_mailing_campaign_ids:
            mass_mailing_campaign = self.pool[
                'mail.mass_mailing.campaign'].browse(cr,
                                                     uid,
                                                     mass_mailing_campaign_ids,
                                                     context=context)

            dic = {
                'campaign_id': mass_mailing_campaign[0].campaign_id.id,
                'source_id': mass_mailing_campaign[0].source_id.id,
                'medium_id': mass_mailing_campaign[0].medium_id.id
            }
            return {'value': dic}

    #------------------------------------------------------
    # Technical stuff
    #------------------------------------------------------

    def copy_data(self, cr, uid, id, default=None, context=None):
        mailing = self.browse(cr, uid, id, context=context)
        default = dict(default or {}, name=_('%s (copy)') % mailing.name)
        return super(MassMailing, self).copy_data(cr,
                                                  uid,
                                                  id,
                                                  default,
                                                  context=context)

    def read_group(self,
                   cr,
                   uid,
                   domain,
                   fields,
                   groupby,
                   offset=0,
                   limit=None,
                   context=None,
                   orderby=False,
                   lazy=True):
        """ Override read_group to always display all states. """
        if groupby and groupby[0] == "state":
            # Default result structure
            # states = self._get_state_list(cr, uid, context=context)
            states = [('draft', 'Draft'), ('in_queue', 'In Queue'),
                      ('sending', 'Sending'), ('done', 'Sent')]
            read_group_all_states = [{
                '__context': {
                    'group_by': groupby[1:]
                },
                '__domain':
                domain + [('state', '=', state_value)],
                'state':
                state_value,
                'state_count':
                0,
            } for state_value, state_name in states]
            # Get standard results
            read_group_res = super(MassMailing,
                                   self).read_group(cr,
                                                    uid,
                                                    domain,
                                                    fields,
                                                    groupby,
                                                    offset=offset,
                                                    limit=limit,
                                                    context=context,
                                                    orderby=orderby)
            # Update standard results with default results
            result = []
            for state_value, state_name in states:
                res = filter(lambda x: x['state'] == state_value,
                             read_group_res)
                if not res:
                    res = filter(lambda x: x['state'] == state_value,
                                 read_group_all_states)
                res[0]['state'] = [state_value, state_name]
                result.append(res[0])
            return result
        else:
            return super(MassMailing, self).read_group(cr,
                                                       uid,
                                                       domain,
                                                       fields,
                                                       groupby,
                                                       offset=offset,
                                                       limit=limit,
                                                       context=context,
                                                       orderby=orderby)

    def update_opt_out(self,
                       cr,
                       uid,
                       mailing_id,
                       email,
                       res_ids,
                       value,
                       context=None):
        mailing = self.browse(cr, uid, mailing_id, context=context)
        model = self.pool[mailing.mailing_model]
        if 'opt_out' in model._fields:
            email_fname = 'email_from'
            if 'email' in model._fields:
                email_fname = 'email'
            record_ids = model.search(cr,
                                      uid, [('id', 'in', res_ids),
                                            (email_fname, 'ilike', email)],
                                      context=context)
            model.write(cr,
                        uid,
                        record_ids, {'opt_out': value},
                        context=context)

    #------------------------------------------------------
    # Views & Actions
    #------------------------------------------------------

    def on_change_model_and_list(self,
                                 cr,
                                 uid,
                                 ids,
                                 mailing_model,
                                 list_ids,
                                 context=None):
        value = {}
        if mailing_model == 'mail.mass_mailing.contact':
            mailing_list_ids = set()
            for item in list_ids:
                if isinstance(item, (int, long)):
                    mailing_list_ids.add(item)
                elif len(item) == 2 and item[0] == 4:  # 4, id
                    mailing_list_ids.add(item[1])
                elif len(item) == 3:  # 6, 0, ids
                    mailing_list_ids |= set(item[2])
            if mailing_list_ids:
                value[
                    'mailing_domain'] = "[('list_id', 'in', %s), ('opt_out', '=', False)]" % list(
                        mailing_list_ids)
            else:
                value['mailing_domain'] = "[('list_id', '=', False)]"
        else:
            value['mailing_domain'] = []
        value['body_html'] = "on_change_model_and_list"
        return {'value': value}

    def action_duplicate(self, cr, uid, ids, context=None):
        copy_id = None
        for mid in ids:
            copy_id = self.copy(cr, uid, mid, context=context)
        if copy_id:
            return {
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'mail.mass_mailing',
                'res_id': copy_id,
                'context': context,
                'flags': {
                    'initial_mode': 'edit'
                },
            }
        return False

    def action_test_mailing(self, cr, uid, ids, context=None):
        ctx = dict(context, default_mass_mailing_id=ids[0])
        return {
            'name': _('Test Mailing'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'mail.mass_mailing.test',
            'target': 'new',
            'context': ctx,
        }

    #------------------------------------------------------
    # Email Sending
    #------------------------------------------------------

    def get_recipients(self, cr, uid, mailing, context=None):
        if mailing.mailing_domain:
            domain = eval(mailing.mailing_domain)
            res_ids = self.pool[mailing.mailing_model].search(cr,
                                                              uid,
                                                              domain,
                                                              context=context)
        else:
            res_ids = []
            domain = [('id', 'in', res_ids)]

        # randomly choose a fragment
        if mailing.contact_ab_pc < 100:
            contact_nbr = self.pool[mailing.mailing_model].search(
                cr, uid, domain, count=True, context=context)
            topick = int(contact_nbr / 100.0 * mailing.contact_ab_pc)
            if mailing.mass_mailing_campaign_id and mailing.mass_mailing_campaign_id.unique_ab_testing:
                already_mailed = self.pool[
                    'mail.mass_mailing.campaign'].get_recipients(
                        cr,
                        uid, [mailing.mass_mailing_campaign_id.id],
                        context=context)[mailing.mass_mailing_campaign_id.id]
            else:
                already_mailed = set([])
            remaining = set(res_ids).difference(already_mailed)
            if topick > len(remaining):
                topick = len(remaining)
            res_ids = random.sample(remaining, topick)
        return res_ids

    def get_remaining_recipients(self, cr, uid, mailing, context=None):
        res_ids = self.get_recipients(cr, uid, mailing, context=context)
        already_mailed = self.pool['mail.mail.statistics'].search_read(
            cr,
            uid, [('model', '=', mailing.mailing_model),
                  ('res_id', 'in', res_ids),
                  ('mass_mailing_id', '=', mailing.id)], ['res_id'],
            context=context)
        already_mailed_res_ids = [
            record['res_id'] for record in already_mailed
        ]
        return list(set(res_ids) - set(already_mailed_res_ids))

    def send_mail(self, cr, uid, ids, context=None):
        author_id = self.pool['res.users'].browse(
            cr, uid, uid, context=context).partner_id.id
        for mailing in self.browse(cr, uid, ids, context=context):
            # instantiate an email composer + send emails
            res_ids = self.get_remaining_recipients(cr,
                                                    uid,
                                                    mailing,
                                                    context=context)
            if not res_ids:
                raise UserError(_('Please select recipients.'))

            if context:
                comp_ctx = dict(context, active_ids=res_ids)
            else:
                comp_ctx = {'active_ids': res_ids}

            # Convert links in absolute URLs before the application of the shortener
            self.write(cr,
                       uid, [mailing.id], {
                           'body_html':
                           self.pool['mail.template']._replace_local_links(
                               cr, uid, mailing.body_html, context)
                       },
                       context=context)

            composer_values = {
                'author_id':
                author_id,
                'attachment_ids':
                [(4, attachment.id) for attachment in mailing.attachment_ids],
                'body':
                self.convert_links(cr, uid, [mailing.id],
                                   context=context)[mailing.id],
                'subject':
                mailing.name,
                'model':
                mailing.mailing_model,
                'email_from':
                mailing.email_from,
                'record_name':
                False,
                'composition_mode':
                'mass_mail',
                'mass_mailing_id':
                mailing.id,
                'mailing_list_ids':
                [(4, l.id) for l in mailing.contact_list_ids],
                'no_auto_thread':
                mailing.reply_to_mode != 'thread',
            }
            if mailing.reply_to_mode == 'email':
                composer_values['reply_to'] = mailing.reply_to

            composer_id = self.pool['mail.compose.message'].create(
                cr, uid, composer_values, context=comp_ctx)
            self.pool['mail.compose.message'].send_mail(cr,
                                                        uid, [composer_id],
                                                        auto_commit=True,
                                                        context=comp_ctx)
            self.write(cr,
                       uid, [mailing.id], {'state': 'done'},
                       context=context)
        return True

    def convert_links(self, cr, uid, ids, context=None):
        res = {}
        for mass_mailing in self.browse(cr, uid, ids, context=context):
            utm_mixin = mass_mailing.mass_mailing_campaign_id if mass_mailing.mass_mailing_campaign_id else mass_mailing
            html = mass_mailing.body_html if mass_mailing.body_html else ''

            vals = {'mass_mailing_id': mass_mailing.id}

            if mass_mailing.mass_mailing_campaign_id:
                vals[
                    'mass_mailing_campaign_id'] = mass_mailing.mass_mailing_campaign_id.id
            if utm_mixin.campaign_id:
                vals['campaign_id'] = utm_mixin.campaign_id.id
            if utm_mixin.source_id:
                vals['source_id'] = utm_mixin.source_id.id
            if utm_mixin.medium_id:
                vals['medium_id'] = utm_mixin.medium_id.id

            res[mass_mailing.id] = self.pool['link.tracker'].convert_links(
                cr,
                uid,
                html,
                vals,
                blacklist=['/unsubscribe_from_list'],
                context=context)

        return res

    def put_in_queue(self, cr, uid, ids, context=None):
        self.write(cr,
                   uid,
                   ids, {
                       'sent_date': fields.datetime.now(),
                       'state': 'in_queue'
                   },
                   context=context)

    def cancel_mass_mailing(self, cr, uid, ids, context=None):
        self.write(cr, uid, ids, {'state': 'draft'}, context=context)

    def retry_failed_mail(self, cr, uid, mass_mailing_ids, context=None):
        mail_mail_ids = self.pool.get('mail.mail').search(
            cr,
            uid, [('mailing_id', 'in', mass_mailing_ids),
                  ('state', '=', 'exception')],
            context=context)
        self.pool.get('mail.mail').unlink(cr,
                                          uid,
                                          mail_mail_ids,
                                          context=context)

        mail_mail_statistics_ids = self.pool.get(
            'mail.mail.statistics').search(
                cr, uid, [('mail_mail_id_int', 'in', mail_mail_ids)])
        self.pool.get('mail.mail.statistics').unlink(cr,
                                                     uid,
                                                     mail_mail_statistics_ids,
                                                     context=context)

        self.write(cr, uid, mass_mailing_ids, {'state': 'in_queue'})

    def _process_mass_mailing_queue(self, cr, uid, context=None):
        now = datetime.now().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
        mass_mailing_ids = self.search(cr,
                                       uid, [('state', 'in',
                                              ('in_queue', 'sending')), '|',
                                             ('schedule_date', '<', now),
                                             ('schedule_date', '=', False)],
                                       context=context)

        for mass_mailing_id in mass_mailing_ids:
            mass_mailing_record = self.browse(cr,
                                              uid,
                                              mass_mailing_id,
                                              context=context)

            if len(
                    self.get_remaining_recipients(
                        cr, uid, mass_mailing_record, context=context)) > 0:
                self.write(cr,
                           uid, [mass_mailing_id], {'state': 'sending'},
                           context=context)
                self.send_mail(cr, uid, [mass_mailing_id], context=context)
            else:
                self.write(cr,
                           uid, [mass_mailing_id], {'state': 'done'},
                           context=context)
Exemplo n.º 30
0
class MassMailingCampaign(osv.Model):
    """Model of mass mailing campaigns. """
    _name = "mail.mass_mailing.campaign"
    _description = 'Mass Mailing Campaign'

    _inherit = ['utm.mixin']
    _inherits = {'utm.campaign': 'campaign_id'}

    def _get_statistics(self, cr, uid, ids, name, arg, context=None):
        """ Compute statistics of the mass mailing campaign """
        results = {}
        cr.execute(
            """
            SELECT
                c.id as campaign_id,
                COUNT(s.id) AS total,
                COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null THEN 1 ELSE null END) AS scheduled,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
                COUNT(CASE WHEN s.id is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
                COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
                COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied ,
                COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced
            FROM
                mail_mail_statistics s
            RIGHT JOIN
                mail_mass_mailing_campaign c
                ON (c.id = s.mass_mailing_campaign_id)
            WHERE
                c.id IN %s
            GROUP BY
                c.id
        """, (tuple(ids), ))
        for row in cr.dictfetchall():
            results[row.pop('campaign_id')] = row
            total = row['total'] or 1
            row['delivered'] = row['sent'] - row['bounced']
            row['received_ratio'] = 100.0 * row['delivered'] / total
            row['opened_ratio'] = 100.0 * row['opened'] / total
            row['replied_ratio'] = 100.0 * row['replied'] / total
            row['bounced_ratio'] = 100.0 * row['bounced'] / total
        return results

    def _get_clicks_ratio(self, cr, uid, ids, name, arg, context=None):
        res = dict.fromkeys(ids, 0)
        cr.execute(
            """
            SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_campaign_id AS id 
            FROM mail_mail_statistics AS stats
            LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id
            WHERE stats.mass_mailing_campaign_id IN %s
            GROUP BY stats.mass_mailing_campaign_id
        """, (tuple(ids), ))

        for record in cr.dictfetchall():
            res[record['id']] = 100 * record['nb_clicks'] / record['nb_mails']

        return res

    def _get_total_mailings(self, cr, uid, ids, field_name, arg, context=None):
        result = dict.fromkeys(ids, 0)
        for mail in self.pool['mail.mass_mailing'].read_group(
                cr,
                uid, [('mass_mailing_campaign_id', 'in', ids)],
            ['mass_mailing_campaign_id'], ['mass_mailing_campaign_id'],
                context=context):
            result[mail['mass_mailing_campaign_id']
                   [0]] = mail['mass_mailing_campaign_id_count']
        return result

    _columns = {
        'name':
        fields.char('Name', required=True),
        'stage_id':
        fields.many2one('mail.mass_mailing.stage', 'Stage', required=True),
        'user_id':
        fields.many2one(
            'res.users',
            'Responsible',
            required=True,
        ),
        'campaign_id':
        fields.many2one('utm.campaign',
                        'campaign_id',
                        required=True,
                        ondelete='cascade'),
        'tag_ids':
        fields.many2many('mail.mass_mailing.tag',
                         'mail_mass_mailing_tag_rel',
                         'tag_id',
                         'campaign_id',
                         string='Tags'),
        'mass_mailing_ids':
        fields.one2many(
            'mail.mass_mailing',
            'mass_mailing_campaign_id',
            'Mass Mailings',
        ),
        'unique_ab_testing':
        fields.boolean(
            'AB Testing',
            help=
            'If checked, recipients will be mailed only once, allowing to send'
            'various mailings in a single campaign to test the effectiveness'
            'of the mailings.'),
        'color':
        fields.integer('Color Index'),
        'clicks_ratio':
        fields.function(
            _get_clicks_ratio,
            string="Number of clicks",
            type="integer",
        ),
        # stat fields
        'total':
        fields.function(_get_statistics,
                        string='Total',
                        type='integer',
                        multi='_get_statistics'),
        'scheduled':
        fields.function(_get_statistics,
                        string='Scheduled',
                        type='integer',
                        multi='_get_statistics'),
        'failed':
        fields.function(_get_statistics,
                        string='Failed',
                        type='integer',
                        multi='_get_statistics'),
        'sent':
        fields.function(_get_statistics,
                        string='Sent Emails',
                        type='integer',
                        multi='_get_statistics'),
        'delivered':
        fields.function(
            _get_statistics,
            string='Delivered',
            type='integer',
            multi='_get_statistics',
        ),
        'opened':
        fields.function(
            _get_statistics,
            string='Opened',
            type='integer',
            multi='_get_statistics',
        ),
        'replied':
        fields.function(_get_statistics,
                        string='Replied',
                        type='integer',
                        multi='_get_statistics'),
        'bounced':
        fields.function(_get_statistics,
                        string='Bounced',
                        type='integer',
                        multi='_get_statistics'),
        'received_ratio':
        fields.function(
            _get_statistics,
            string='Received Ratio',
            type='integer',
            multi='_get_statistics',
        ),
        'opened_ratio':
        fields.function(
            _get_statistics,
            string='Opened Ratio',
            type='integer',
            multi='_get_statistics',
        ),
        'replied_ratio':
        fields.function(
            _get_statistics,
            string='Replied Ratio',
            type='integer',
            multi='_get_statistics',
        ),
        'total_mailings':
        fields.function(_get_total_mailings, string='Mailings',
                        type='integer'),
        'bounced_ratio':
        fields.function(
            _get_statistics,
            string='Bounced Ratio',
            type='integer',
            multi='_get_statistics',
        ),
    }

    def _get_default_stage_id(self, cr, uid, context=None):
        stage_ids = self.pool['mail.mass_mailing.stage'].search(
            cr, uid, [], limit=1, context=context)
        return stage_ids and stage_ids[0] or False

    def _get_source_id(self, cr, uid, context=None):
        return self.pool['ir.model.data'].xmlid_to_res_id(
            cr, uid, 'utm.utm_source_newsletter')

    def _get_medium_id(self, cr, uid, context=None):
        return self.pool['ir.model.data'].xmlid_to_res_id(
            cr, uid, 'utm.utm_medium_email')

    _defaults = {
        'user_id': lambda self, cr, uid, ctx=None: uid,
        'stage_id': lambda self, *args: self._get_default_stage_id(*args),
        'source_id': lambda self, *args: self._get_source_id(*args),
        'medium_id': lambda self, *args: self._get_medium_id(*args),
    }

    def mass_mailing_statistics_action(self, cr, uid, ids, context=None):
        res = self.pool['ir.actions.act_window'].for_xml_id(
            cr,
            uid,
            'mass_mailing',
            'action_view_mass_mailing_statistics',
            context=context)
        res['domain'] = [('mass_mailing_campaign_id', 'in', ids)]
        return res

    def get_recipients(self, cr, uid, ids, model=None, context=None):
        """Return the recipients of a mailing campaign. This is based on the statistics
        build for each mailing. """
        Statistics = self.pool['mail.mail.statistics']
        res = dict.fromkeys(ids, False)
        for cid in ids:
            domain = [('mass_mailing_campaign_id', '=', cid)]
            if model:
                domain += [('model', '=', model)]
            stat_ids = Statistics.search(cr, uid, domain, context=context)
            res[cid] = set(stat.res_id for stat in Statistics.browse(
                cr, uid, stat_ids, context=context))
        return res

    def on_change_campaign_name(self, cr, uid, ids, name, context=None):
        if name:
            mass_mailing_campaign = self.browse(cr, uid, ids, context=context)
            if mass_mailing_campaign.campaign_id:
                utm_campaign_id = mass_mailing_campaign.campaign_id.id
                self.pool['utm.campaign'].write(cr,
                                                uid, [utm_campaign_id],
                                                {'name': name},
                                                context=context)
            else:
                utm_campaign_id = self.pool['utm.campaign'].create(
                    cr, uid, {'name': name}, context=context)

            return {'value': {'campaign_id': utm_campaign_id}}

    def read_group(self,
                   cr,
                   uid,
                   domain,
                   fields,
                   groupby,
                   offset=0,
                   limit=None,
                   context=None,
                   orderby=False,
                   lazy=True):
        """ Override read_group to always display all states. """
        if groupby and groupby[0] == "stage_id":
            # Default result structure
            states_read = self.pool['mail.mass_mailing.stage'].search_read(
                cr, uid, [], ['name'], context=context)
            states = [(state['id'], state['name']) for state in states_read]
            read_group_all_states = [{
                '__context': {
                    'group_by': groupby[1:]
                },
                '__domain':
                domain + [('stage_id', '=', state_value)],
                'stage_id':
                state_value,
                'state_count':
                0,
            } for state_value, state_name in states]
            # Get standard results
            read_group_res = super(MassMailingCampaign,
                                   self).read_group(cr,
                                                    uid,
                                                    domain,
                                                    fields,
                                                    groupby,
                                                    offset=offset,
                                                    limit=limit,
                                                    context=context,
                                                    orderby=orderby)
            # Update standard results with default results
            result = []
            for state_value, state_name in states:
                res = filter(
                    lambda x: x['stage_id'] == (state_value, state_name),
                    read_group_res)
                if not res:
                    res = filter(lambda x: x['stage_id'] == state_value,
                                 read_group_all_states)
                res[0]['stage_id'] = [state_value, state_name]
                result.append(res[0])
            return result
        else:
            return super(MassMailingCampaign, self).read_group(cr,
                                                               uid,
                                                               domain,
                                                               fields,
                                                               groupby,
                                                               offset=offset,
                                                               limit=limit,
                                                               context=context,
                                                               orderby=orderby)