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, }
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', }
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)
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', }
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)
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"), }
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)
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."), }
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')]") }
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', ), }
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 !"), ]
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.')]
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 !"), ]
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)
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)
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'}
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
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, }
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), }
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
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
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)
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, }}
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)
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
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
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'), }
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)
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)
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)