class product_template(osv.Model): _inherit = "product.template" _columns = { 'website_description': fields.html('Description for the website' ), # hack, if website_sale is not installed 'quote_description': fields.html('Description for the quote'), }
def test_00_sanitize(self): cr, uid, context = self.cr, self.uid, {} old_columns = self.partner._columns self.partner._columns = dict(old_columns) self.partner._columns.update({ 'comment': fields.html('Secure Html', sanitize=False), }) some_ugly_html = """<p>Oops this should maybe be sanitized % if object.some_field and not object.oriented: <table> % if object.other_field: <tr style="border: 10px solid black;"> ${object.mako_thing} <td> </tr> % endif <tr> %if object.dummy_field: <p>Youpie</p> %endif""" pid = self.partner.create(cr, uid, { 'name': 'Raoul Poilvache', 'comment': some_ugly_html, }, context=context) partner = self.partner.browse(cr, uid, pid, context=context) self.assertEqual(partner.comment, some_ugly_html, 'Error in HTML field: content was sanitized but field has sanitize=False') self.partner._columns.update({ 'comment': fields.html('Unsecure Html', sanitize=True), }) self.partner.write(cr, uid, [pid], { 'comment': some_ugly_html, }, context=context) partner = self.partner.browse(cr, uid, pid, context=context) # sanitize should have closed tags left open in the original html self.assertIn('</table>', partner.comment, 'Error in HTML field: content does not seem to have been sanitized despise sanitize=True') self.assertIn('</td>', partner.comment, 'Error in HTML field: content does not seem to have been sanitized despise sanitize=True') self.assertIn('<tr style="', partner.comment, 'Style attr should not have been stripped') self.partner._columns['comment'] = fields.html('Stripped Html', sanitize=True, strip_style=True) self.partner.write(cr, uid, [pid], {'comment': some_ugly_html}, context=context) partner = self.partner.browse(cr, uid, pid, context=context) self.assertNotIn('<tr style="', partner.comment, 'Style attr should have been stripped') self.partner._columns = old_columns
class MassMailingList(osv.Model): """Model of a contact list. """ _name = 'mail.mass_mailing.list' _order = 'name' _description = 'Mailing List' def _get_contact_nbr(self, cr, uid, ids, name, arg, context=None): result = dict.fromkeys(ids, 0) Contacts = self.pool.get('mail.mass_mailing.contact') for group in Contacts.read_group(cr, uid, [('list_id', 'in', ids), ('opt_out', '!=', True)], ['list_id'], ['list_id'], context=context): result[group['list_id'][0]] = group['list_id_count'] return result _columns = { 'name': fields.char('Mailing List', required=True), 'active': fields.boolean('Active'), 'create_date': fields.datetime('Creation Date'), 'contact_nbr': fields.function( _get_contact_nbr, type='integer', string='Number of Contacts', ), 'popup_content': fields.html("Website Popup Content", translate=True, required=True, sanitize=False), 'popup_redirect_url': fields.char("Website Popup Redirect URL"), } def _get_default_popup_content(self, cr, uid, context=None): return """<div class="modal-header text-center"> <h3 class="modal-title mt8">YuanCloud Presents</h3> </div> <div class="o_popup_message"> <font>7</font> <strong>Business Hacks</strong> <span> to<br/>boost your marketing</span> </div> <p class="o_message_paragraph">Join our Marketing newsletter and get <strong>this white paper instantly</strong></p>""" _defaults = { 'active': True, 'popup_content': _get_default_popup_content, 'popup_redirect_url': '/', }
class WebsiteResPartner(osv.Model): _name = 'res.partner' _inherit = [ 'res.partner', 'website.seo.metadata', 'website.published.mixin' ] def _get_ids(self, cr, uid, ids, flds, args, context=None): return {i: i for i in ids} def _set_private(self, cr, uid, ids, field_name, value, arg, context=None): return self.write(cr, uid, ids, {'website_published': not value}, context=context) def _get_private(self, cr, uid, ids, field_name, arg, context=None): return dict((rec.id, not rec.website_published) for rec in self.browse(cr, uid, ids, context=context)) def _search_private(self, cr, uid, obj, name, args, context=None): return [('website_published', '=', not args[0][2])] _columns = { 'website_private': fields.function(_get_private, fnct_inv=_set_private, fnct_search=_search_private, type='boolean', string='Private Profile'), 'website_description': fields.html('Website Partner Full Description', strip_style=True), 'website_short_description': fields.text('Website Partner Short Description'), # hack to allow using plain browse record in qweb views 'self': fields.function(_get_ids, type='many2one', relation=_name), } def _website_url(self, cr, uid, ids, field_name, arg, context=None): res = super(WebsiteResPartner, self)._website_url(cr, uid, ids, field_name, arg, context=context) for partner in self.browse(cr, uid, ids, context=context): res[partner.id] = "/partners/%s" % slug(partner) return res _defaults = { 'website_private': True, }
class test_converter(orm.Model): _name = 'web_editor.converter.test' # disable translation export for those brilliant field labels and values _translate = False _columns = { 'char': fields.char(), 'integer': fields.integer(), 'float': fields.float(), 'numeric': fields.float(digits=(16, 2)), 'many2one': fields.many2one('web_editor.converter.test.sub'), 'binary': fields.binary(), 'date': fields.date(), 'datetime': fields.datetime(), 'selection': fields.selection([ (1, "réponse A"), (2, "réponse B"), (3, "réponse C"), (4, "réponse D"), ]), 'selection_str': fields.selection( [ ('A', "Qu'il n'est pas arrivé à Toronto"), ('B', "Qu'il était supposé arriver à Toronto"), ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), ('D', "La réponse D"), ], string= u"Lorsqu'un pancake prend l'avion à destination de Toronto et " u"qu'il fait une escale technique à St Claude, on dit:"), 'html': fields.html(), 'text': fields.text(), }
class sale_order_line(osv.osv): _inherit = "sale.order.line" _description = "Sales Order Line" _columns = { 'website_description': fields.html('Line Description'), 'option_line_id': fields.one2many('sale.order.option', 'line_id', 'Optional Products Lines'), } def _inject_quote_description(self, cr, uid, values, context=None): values = dict(values or {}) if not values.get('website_description') and values.get('product_id'): product = self.pool['product.product'].browse(cr, uid, values['product_id'], context=context) values[ 'website_description'] = product.quote_description or product.website_description return values def create(self, cr, uid, values, context=None): values = self._inject_quote_description(cr, uid, values, context) ret = super(sale_order_line, self).create(cr, uid, values, context=context) # hack because create don t make the job for a related field if values.get('website_description'): self.write(cr, uid, ret, {'website_description': values['website_description']}, context=context) return ret def write(self, cr, uid, ids, values, context=None): values = self._inject_quote_description(cr, uid, values, context) return super(sale_order_line, self).write(cr, uid, ids, values, context=context)
class sale_quote_template(osv.osv): _name = "sale.quote.template" _description = "Sale Quotation Template" _columns = { 'name': fields.char('Quotation Template', required=True), 'website_description': fields.html('Description', translate=True), 'quote_line': fields.one2many('sale.quote.line', 'quote_id', 'Quotation Template Lines', copy=True), 'note': fields.text('Terms and conditions'), 'options': fields.one2many('sale.quote.option', 'template_id', 'Optional Products Lines', copy=True), 'number_of_days': fields.integer( 'Quotation Duration', help= 'Number of days for the validity date computation of the quotation' ), 'require_payment': fields.selection( [(0, 'Not mandatory on website quote validation'), (1, 'Immediate after website order validation')], 'Payment', help= "Require immediate payment by the customer when validating the order from the website quote" ), } def open_template(self, cr, uid, quote_id, context=None): return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/quote/template/%d' % quote_id[0] }
class Documentation(osv.Model): _name = 'forum.documentation.toc' _description = 'Documentation ToC' _inherit = ['website.seo.metadata'] _order = "parent_left" _parent_order = "sequence, name" _parent_store = True def name_get(self, cr, uid, ids, context=None): if isinstance(ids, (list, tuple)) and not len(ids): return [] if isinstance(ids, (long, int)): ids = [ids] reads = self.read(cr, uid, ids, ['name', 'parent_id'], context=context) res = [] for record in reads: name = record['name'] if record['parent_id']: name = record['parent_id'][1]+' / '+name res.append((record['id'], name)) return res # TODO master remove me def _name_get_fnc(self, cr, uid, ids, prop, unknow_none, context=None): res = self.name_get(cr, uid, ids, context=context) return dict(res) _columns = { 'sequence': fields.integer('Sequence'), 'name': fields.char('Name', required=True, translate=True), 'introduction': fields.html('Introduction', translate=True), 'parent_id': fields.many2one('forum.documentation.toc', 'Parent Table Of Content', ondelete='cascade'), 'child_ids': fields.one2many('forum.documentation.toc', 'parent_id', 'Children Table Of Content'), 'parent_left': fields.integer('Left Parent', select=True), 'parent_right': fields.integer('Right Parent', select=True), 'post_ids': fields.one2many('forum.post', 'documentation_toc_id', 'Posts'), 'forum_id': fields.many2one('forum.forum', 'Forum', required=True), } _constraints = [ (osv.osv._check_recursion, 'Error ! You cannot create recursive categories.', ['parent_id']) ]
class hr_job(osv.osv): _name = 'hr.job' _inherit = ['hr.job', 'website.seo.metadata', 'website.published.mixin'] def _website_url(self, cr, uid, ids, field_name, arg, context=None): res = super(hr_job, self)._website_url(cr, uid, ids, field_name, arg, context=context) for job in self.browse(cr, uid, ids, context=context): res[job.id] = "/jobs/detail/%s" % job.id return res def job_open(self, cr, uid, ids, context=None): self.write(cr, uid, ids, {'website_published': False}, context=context) return super(hr_job, self).job_open(cr, uid, ids, context) _columns = { 'website_description': fields.html('Website description', translate=True), }
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 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 sale_quote_option(osv.osv): _name = "sale.quote.option" _description = "Quotation Option" _columns = { 'template_id': fields.many2one('sale.quote.template', 'Quotation Template Reference', ondelete='cascade', select=True, required=True), 'name': fields.text('Description', required=True, translate=True), 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], required=True), 'website_description': fields.html('Option Description', translate=True), 'price_unit': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')), 'discount': fields.float('Discount (%)', digits_compute=dp.get_precision('Discount')), 'uom_id': fields.many2one('product.uom', 'Unit of Measure ', required=True), 'quantity': fields.float('Quantity', required=True, digits_compute=dp.get_precision('Product UoS')), } _defaults = { 'quantity': 1, } def on_change_product_id(self, cr, uid, ids, product, uom_id=None, context=None): vals, domain = {}, [] product_obj = self.pool.get('product.product').browse(cr, uid, product, context=context) name = product_obj.name if product_obj.description_sale: name += '\n' + product_obj.description_sale vals.update({ 'price_unit': product_obj.list_price, 'website_description': product_obj.product_tmpl_id.quote_description, 'name': name, 'uom_id': uom_id or product_obj.uom_id.id, }) uom_obj = self.pool.get('product.uom') if vals['uom_id'] != product_obj.uom_id.id: selected_uom = uom_obj.browse(cr, uid, vals['uom_id'], context=context) new_price = uom_obj._compute_price(cr, uid, product_obj.uom_id.id, vals['price_unit'], vals['uom_id']) vals['price_unit'] = new_price if not uom_id: domain = { 'uom_id': [('category_id', '=', product_obj.uom_id.category_id.id)] } return {'value': vals, 'domain': domain} def product_uom_change(self, cr, uid, ids, product, uom_id, context=None): if not uom_id: return {'value': {'price_unit': 0.0, 'uom_id': False}} return self.on_change_product_id(cr, uid, ids, product, uom_id=uom_id, context=context)
class sale_order(osv.osv): _inherit = 'sale.order' def _get_total(self, cr, uid, ids, name, arg, context=None): res = {} for order in self.browse(cr, uid, ids, context=context): total = 0.0 for line in order.order_line: total += line.price_subtotal + line.price_unit * ( (line.discount or 0.0) / 100.0) * line.product_uom_qty res[order.id] = total return res _columns = { 'access_token': fields.char('Security Token', required=True, copy=False), 'template_id': fields.many2one('sale.quote.template', 'Quotation Template', readonly=True, states={ 'draft': [('readonly', False)], 'sent': [('readonly', False)] }), 'website_description': fields.html('Description', translate=True), 'options': fields.one2many('sale.order.option', 'order_id', 'Optional Products Lines', readonly=True, states={ 'draft': [('readonly', False)], 'sent': [('readonly', False)] }, copy=True), 'amount_undiscounted': fields.function(_get_total, string='Amount Before Discount', type="float", digits=0), 'quote_viewed': fields.boolean('Quotation Viewed'), 'require_payment': fields.selection( [(0, 'Not mandatory on website quote validation'), (1, 'Immediate after website order validation')], 'Payment', help= "Require immediate payment by the customer when validating the order from the website quote" ), } def _get_template_id(self, cr, uid, context=None): try: template_id = self.pool.get('ir.model.data').get_object_reference( cr, uid, 'website_quote', 'website_quote_template_default')[1] except ValueError: template_id = False return template_id _defaults = { 'access_token': lambda self, cr, uid, ctx={}: str(uuid.uuid4()), 'template_id': _get_template_id, } def open_quotation(self, cr, uid, quote_id, context=None): quote = self.browse(cr, uid, quote_id[0], context=context) self.write(cr, uid, quote_id[0], {'quote_viewed': True}, context=context) return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/quote/%s/%s' % (quote.id, quote.access_token) } def onchange_template_id(self, cr, uid, ids, template_id, partner=False, fiscal_position_id=False, pricelist_id=False, context=None): if not template_id: return {} if partner: context = dict(context or {}) context['lang'] = self.pool['res.partner'].browse( cr, uid, partner, context).lang pricelist_obj = self.pool['product.pricelist'] lines = [(5, )] quote_template = self.pool.get('sale.quote.template').browse( cr, uid, template_id, context=context) for line in quote_template.quote_line: res = self.pool.get('sale.order.line').product_id_change( cr, uid, False, False, line.product_id.id, line.product_uom_qty, line.product_uom_id.id, line.product_uom_qty, line.product_uom_id.id, line.name, partner, False, True, time.strftime('%Y-%m-%d'), False, fiscal_position_id, True, context) data = res.get('value', {}) if pricelist_id: uom_context = context.copy() uom_context['uom'] = line.product_uom_id.id price = pricelist_obj.price_get( cr, uid, [pricelist_id], line.product_id.id, 1, context=uom_context)[pricelist_id] else: price = line.price_unit if 'tax_id' in data: data['tax_id'] = [(6, 0, data['tax_id'])] else: fpos = (fiscal_position_id and self.pool['account.fiscal.position'].browse( cr, uid, fiscal_position_id)) or False taxes = fpos.map_tax( line.product_id.product_tmpl_id.taxes_id ).ids if fpos else line.product_id.product_tmpl_id.taxes_id.ids data['tax_id'] = [(6, 0, taxes)] data.update({ 'name': line.name, 'price_unit': price, 'discount': line.discount, 'product_uom_qty': line.product_uom_qty, 'product_id': line.product_id.id, 'product_uom': line.product_uom_id.id, 'website_description': line.website_description, 'state': 'draft', }) lines.append((0, 0, data)) options = [] for option in quote_template.options: if pricelist_id: uom_context = context.copy() uom_context['uom'] = option.uom_id.id price = pricelist_obj.price_get( cr, uid, [pricelist_id], option.product_id.id, 1, context=uom_context)[pricelist_id] else: price = option.price_unit options.append((0, 0, { 'product_id': option.product_id.id, 'name': option.name, 'quantity': option.quantity, 'uom_id': option.uom_id.id, 'price_unit': price, 'discount': option.discount, 'website_description': option.website_description, })) date = False if quote_template.number_of_days > 0: date = (datetime.datetime.now() + datetime.timedelta( quote_template.number_of_days)).strftime("%Y-%m-%d") data = { 'order_line': lines, 'website_description': quote_template.website_description, 'options': options, 'validity_date': date, 'require_payment': quote_template.require_payment } if quote_template.note: data['note'] = quote_template.note return {'value': data} def recommended_products(self, cr, uid, ids, context=None): order_line = self.browse(cr, uid, ids[0], context=context).order_line product_pool = self.pool.get('product.product') products = [] for line in order_line: products += line.product_id.product_tmpl_id.recommended_products( context=context) return products 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 online quote if exists. """ quote = self.browse(cr, uid, ids[0], context=context) if not quote.template_id: return super(sale_order, self).get_access_action(cr, uid, ids, context=context) return { 'type': 'ir.actions.act_url', 'url': '/quote/%s' % quote.id, 'target': 'self', 'res_id': quote.id, } def action_quotation_send(self, cr, uid, ids, context=None): action = super(sale_order, self).action_quotation_send(cr, uid, ids, context=context) ir_model_data = self.pool.get('ir.model.data') quote_template_id = self.read(cr, uid, ids, ['template_id'], context=context)[0]['template_id'] if quote_template_id: try: template_id = ir_model_data.get_object_reference( cr, uid, 'website_quote', 'email_template_edi_sale')[1] except ValueError: pass else: action['context'].update({ 'default_template_id': template_id, 'default_use_template': True }) return action def _confirm_online_quote(self, cr, uid, order_id, tx, context=None): """ Payment callback: validate the order and write tx details in chatter """ order = self.browse(cr, uid, order_id, context=context) # create draft invoice if transaction is ok if tx and tx.state == 'done': if order.state in ['draft', 'sent']: self.signal_workflow(cr, SUPERUSER_ID, [order.id], 'manual_invoice', context=context) message = _('Order payed by %s. Transaction: %s. Amount: %s.') % ( tx.partner_id.name, tx.acquirer_reference, tx.amount) self.message_post(cr, uid, order_id, body=message, type='comment', subtype='mt_comment', context=context) return True return False def create(self, cr, uid, values, context=None): if not values.get('template_id'): defaults = self.default_get(cr, uid, ['template_id'], context=context) template_values = self.onchange_template_id( cr, uid, [], defaults.get('template_id'), partner=values.get('partner_id'), fiscal_position_id=values.get('fiscal_position'), context=context).get('value', {}) values = dict(template_values, **values) return super(sale_order, self).create(cr, uid, values, context=context)
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 test_model(orm.Model): _name = 'test_converter.test_model' _columns = { 'char': fields.char(), 'integer': fields.integer(), 'float': fields.float(), 'numeric': fields.float(digits=(16, 2)), 'many2one': fields.many2one('test_converter.test_model.sub'), 'binary': fields.binary(), 'date': fields.date(), 'datetime': fields.datetime(), 'selection': fields.selection([ (1, "réponse A"), (2, "réponse B"), (3, "réponse C"), (4, "réponse D"), ]), 'selection_str': fields.selection( [ ('A', "Qu'il n'est pas arrivé à Toronto"), ('B', "Qu'il était supposé arriver à Toronto"), ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), ('D', "La réponse D"), ], string="Lorsqu'un pancake prend l'avion à destination de Toronto et " "qu'il fait une escale technique à St Claude, on dit:"), 'html': fields.html(), 'text': fields.text(), } # `base` module does not contains any model that implement the `_group_by_full` functionality # test this feature here... def _gbf_m2o(self, cr, uid, ids, domain, read_group_order, access_rights_uid, context): Sub = self.pool['test_converter.test_model.sub'] all_ids = Sub._search(cr, uid, [], access_rights_uid=access_rights_uid, context=context) result = Sub.name_get(cr, access_rights_uid or uid, all_ids, context=context) folds = {i: i not in ids for i, _ in result} return result, folds _group_by_full = { 'many2one': _gbf_m2o, }
class PaymentAcquirer(osv.Model): """ Acquirer Model. Each specific acquirer can extend the model by adding its own fields, using the acquirer_name as a prefix for the new fields. Using the required_if_provider='<name>' attribute on fields it is possible to have required fields that depend on a specific acquirer. Each acquirer has a link to an ir.ui.view record that is a template of a button used to display the payment form. See examples in ``payment_ogone`` and ``payment_paypal`` modules. Methods that should be added in an acquirer-specific implementation: - ``<name>_form_generate_values(self, cr, uid, id, reference, amount, currency, partner_id=False, partner_values=None, tx_custom_values=None, context=None)``: method that generates the values used to render the form button template. - ``<name>_get_form_action_url(self, cr, uid, id, context=None):``: method that returns the url of the button form. It is used for example in ecommerce application, if you want to post some data to the acquirer. - ``<name>_compute_fees(self, cr, uid, id, amount, currency_id, country_id, context=None)``: computed the fees of the acquirer, using generic fields defined on the acquirer model (see fields definition). Each acquirer should also define controllers to handle communication between YuanCloud and the acquirer. It generally consists in return urls given to the button form and that the acquirer uses to send the customer back after the transaction, with transaction details given as a POST request. """ _name = 'payment.acquirer' _description = 'Payment Acquirer' _order = 'sequence' def _get_providers(self, cr, uid, context=None): return [] # indirection to ease inheritance _provider_selection = lambda self, *args, **kwargs: self._get_providers(*args, **kwargs) _columns = { 'name': fields.char('Name', required=True, translate=True), 'provider': fields.selection(_provider_selection, string='Provider', required=True), 'company_id': fields.many2one('res.company', 'Company', required=True), 'pre_msg': fields.html('Help Message', translate=True, help='Message displayed to explain and help the payment process.'), 'post_msg': fields.html('Thanks Message', help='Message displayed after having done the payment process.'), 'view_template_id': fields.many2one('ir.ui.view', 'Form Button Template', required=True), 'registration_view_template_id': fields.many2one('ir.ui.view', 'S2S Form Template', domain=[('type', '=', 'qweb')], help="Template for method registration"), 'environment': fields.selection( [('test', 'Test'), ('prod', 'Production')], string='Environment', oldname='env'), 'website_published': fields.boolean( 'Visible in Portal / Website', copy=False, help="Make this payment acquirer available (Customer invoices, etc.)"), 'auto_confirm': fields.selection( [('none', 'No automatic confirmation'), ('at_pay_confirm', 'At payment with acquirer confirmation'), ('at_pay_now', 'At payment no acquirer confirmation needed')], string='Order Confirmation', required=True), 'pending_msg': fields.html('Pending Message', translate=True, help='Message displayed, if order is in pending state after having done the payment process.'), 'done_msg': fields.html('Done Message', translate=True, help='Message displayed, if order is done successfully after having done the payment process.'), 'cancel_msg': fields.html('Cancel Message', translate=True, help='Message displayed, if order is cancel during the payment process.'), 'error_msg': fields.html('Error Message', translate=True, help='Message displayed, if error is occur during the payment process.'), # Fees 'fees_active': fields.boolean('Add Extra Fees'), 'fees_dom_fixed': fields.float('Fixed domestic fees'), 'fees_dom_var': fields.float('Variable domestic fees (in percents)'), 'fees_int_fixed': fields.float('Fixed international fees'), 'fees_int_var': fields.float('Variable international fees (in percents)'), 'sequence': fields.integer('Sequence', help="Determine the display order"), } image = yuancloud.fields.Binary("Image", attachment=True, help="This field holds the image used for this provider, limited to 1024x1024px") image_medium = yuancloud.fields.Binary("Medium-sized image", compute='_compute_images', inverse='_inverse_image_medium', store=True, attachment=True, help="Medium-sized image of this provider. It is automatically "\ "resized as a 128x128px image, with aspect ratio preserved. "\ "Use this field in form views or some kanban views.") image_small = yuancloud.fields.Binary("Small-sized image", compute='_compute_images', inverse='_inverse_image_small', store=True, attachment=True, help="Small-sized image of this provider. It is automatically "\ "resized as a 64x64px image, with aspect ratio preserved. "\ "Use this field anywhere a small image is required.") @yuancloud.api.depends('image') def _compute_images(self): for rec in self: rec.image_medium = yuancloud.tools.image_resize_image_medium(rec.image) rec.image_small = yuancloud.tools.image_resize_image_small(rec.image) def _inverse_image_medium(self): for rec in self: rec.image = yuancloud.tools.image_resize_image_big(rec.image_medium) def _inverse_image_small(self): for rec in self: rec.image = yuancloud.tools.image_resize_image_big(rec.image_small) _defaults = { 'company_id': lambda self, cr, uid, obj, ctx=None: self.pool['res.users'].browse(cr, uid, uid).company_id.id, 'environment': 'prod', 'website_published': False, 'auto_confirm': 'at_pay_confirm', 'pending_msg': '<i>Pending,</i> Your online payment has been successfully processed. But your order is not validated yet.', 'done_msg': '<i>Done,</i> Your online payment has been successfully processed. Thank you for your order.', 'cancel_msg': '<i>Cancel,</i> Your payment has been cancelled.', 'error_msg': "<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but won't be paid. Don't hesitate to contact us if you have any questions on the status of your order." } def _check_required_if_provider(self, cr, uid, ids, context=None): """ If the field has 'required_if_provider="<provider>"' attribute, then it required if record.provider is <provider>. """ for acquirer in self.browse(cr, uid, ids, context=context): if any(getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in self._fields.items()): return False return True _constraints = [ (_check_required_if_provider, 'Required fields not filled', ['required for this provider']), ] def get_form_action_url(self, cr, uid, id, context=None): """ Returns the form action URL, for form-based acquirer implementations. """ acquirer = self.browse(cr, uid, id, context=context) if hasattr(self, '%s_get_form_action_url' % acquirer.provider): return getattr(self, '%s_get_form_action_url' % acquirer.provider)(cr, uid, id, context=context) return False def render(self, cr, uid, id, reference, amount, currency_id, partner_id=False, values=None, context=None): """ Renders the form template of the given acquirer as a qWeb template. :param string reference: the transaction reference :param float amount: the amount the buyer has to pay :param currency_id: currency id :param dict partner_id: optional partner_id to fill values :param dict values: a dictionary of values for the transction that is given to the acquirer-specific method generating the form values :param dict context: YuanCloud context All templates will receive: - acquirer: the payment.acquirer browse record - user: the current user browse record - currency_id: id of the transaction currency - amount: amount of the transaction - reference: reference of the transaction - partner_*: partner-related values - partner: optional partner browse record - 'feedback_url': feedback URL, controler that manage answer of the acquirer (without base url) -> FIXME - 'return_url': URL for coming back after payment validation (wihout base url) -> FIXME - 'cancel_url': URL if the client cancels the payment -> FIXME - 'error_url': URL if there is an issue with the payment -> FIXME - context: YuanCloud context dictionary """ if context is None: context = {} if values is None: values = {} acquirer = self.browse(cr, uid, id, context=context) # reference and amount values.setdefault('reference', reference) amount = float_round(amount, 2) values.setdefault('amount', amount) # currency id currency_id = values.setdefault('currency_id', currency_id) if currency_id: currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context) else: currency = self.pool['res.users'].browse(cr, uid, uid, context=context).company_id.currency_id values['currency'] = currency # Fill partner_* using values['partner_id'] or partner_id arguement partner_id = values.get('partner_id', partner_id) if partner_id: partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context) values.update({ 'partner': partner, 'partner_id': partner_id, 'partner_name': partner.name, 'partner_lang': partner.lang, 'partner_email': partner.email, 'partner_zip': partner.zip, 'partner_city': partner.city, 'partner_address': _partner_format_address(partner.street, partner.street2), 'partner_country_id': partner.country_id.id, 'partner_country': partner.country_id, 'partner_phone': partner.phone, 'partner_state': partner.state_id, }) if values.get('partner_name'): values.update({ 'partner_first_name': _partner_split_name(values.get('partner_name'))[0], 'partner_last_name': _partner_split_name(values.get('partner_name'))[1], }) # Fix address, country fields if not values.get('partner_address'): values['address'] = _partner_format_address(values.get('partner_street', ''), values.get('partner_street2', '')) if not values.get('partner_country') and values.get('partner_country_id'): values['country'] = self.pool['res.country'].browse(cr, uid, values.get('partner_country_id'), context=context) # compute fees fees_method_name = '%s_compute_fees' % acquirer.provider if hasattr(self, fees_method_name): fees = getattr(self, fees_method_name)(cr, uid, id, values['amount'], values['currency_id'], values['partner_country_id'], context=None) values['fees'] = float_round(fees, 2) # call <name>_form_generate_values to update the tx dict with acqurier specific values cust_method_name = '%s_form_generate_values' % (acquirer.provider) if hasattr(self, cust_method_name): method = getattr(self, cust_method_name) values = method(cr, uid, id, values, context=context) values.update({ 'tx_url': context.get('tx_url', self.get_form_action_url(cr, uid, id, context=context)), 'submit_class': context.get('submit_class', 'btn btn-link'), 'submit_txt': context.get('submit_txt'), 'acquirer': acquirer, 'user': self.pool.get("res.users").browse(cr, uid, uid, context=context), 'context': context, 'type': values.get('type') or 'form', }) values.setdefault('return_url', False) # because render accepts view ids but not qweb -> need to use the xml_id return self.pool['ir.ui.view'].render(cr, uid, acquirer.view_template_id.xml_id, values, engine='ir.qweb', context=context) def _registration_render(self, cr, uid, id, partner_id, qweb_context=None, context=None): acquirer = self.browse(cr, uid, id, context=context) if qweb_context is None: qweb_context = {} qweb_context.update(id=id, partner_id=partner_id) method_name = '_%s_registration_form_generate_values' % (acquirer.provider,) if hasattr(self, method_name): method = getattr(self, method_name) qweb_context.update(method(cr, uid, id, qweb_context, context=context)) return self.pool['ir.ui.view'].render(cr, uid, acquirer.registration_view_template_id.xml_id, qweb_context, engine='ir.qweb', context=context) def s2s_process(self, cr, uid, id, data, context=None): acquirer = self.browse(cr, uid, id, context=context) cust_method_name = '%s_s2s_form_process' % (acquirer.provider) if not self.s2s_validate(cr, uid, id, data, context=context): return False if hasattr(self, cust_method_name): method = getattr(self, cust_method_name) return method(cr, uid, data, context=context) return True def s2s_validate(self, cr, uid, id, data, context=None): acquirer = self.browse(cr, uid, id, context=context) cust_method_name = '%s_s2s_form_validate' % (acquirer.provider) if hasattr(self, cust_method_name): method = getattr(self, cust_method_name) return method(cr, uid, id, data, context=context) return True
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 sale_order_option(osv.osv): _name = "sale.order.option" _description = "Sale Options" _columns = { 'order_id': fields.many2one('sale.order', 'Sale Order Reference', ondelete='cascade', select=True), 'line_id': fields.many2one('sale.order.line', on_delete="set null"), 'name': fields.text('Description', required=True), 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)]), 'website_description': fields.html('Line Description'), 'price_unit': fields.float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price')), 'discount': fields.float('Discount (%)', digits_compute=dp.get_precision('Discount')), 'uom_id': fields.many2one('product.uom', 'Unit of Measure ', required=True), 'quantity': fields.float('Quantity', required=True, digits_compute=dp.get_precision('Product UoS')), } _defaults = { 'quantity': 1, } # TODO master: to remove, replaced by onchange of the new api def on_change_product_id(self, cr, uid, ids, product, uom_id=None, context=None): vals, domain = {}, [] if not product: return vals product_obj = self.pool.get('product.product').browse(cr, uid, product, context=context) name = product_obj.name if product_obj.description_sale: name += '\n' + product_obj.description_sale vals.update({ 'price_unit': product_obj.list_price, 'website_description': product_obj and (product_obj.quote_description or product_obj.website_description), 'name': name, 'uom_id': uom_id or product_obj.uom_id.id, }) uom_obj = self.pool.get('product.uom') if vals['uom_id'] != product_obj.uom_id.id: selected_uom = uom_obj.browse(cr, uid, vals['uom_id'], context=context) new_price = uom_obj._compute_price(cr, uid, product_obj.uom_id.id, vals['price_unit'], vals['uom_id']) vals['price_unit'] = new_price if not uom_id: domain = { 'uom_id': [('category_id', '=', product_obj.uom_id.category_id.id)] } return {'value': vals, 'domain': domain} # TODO master: to remove, replaced by onchange of the new api def product_uom_change(self, cr, uid, ids, product, uom_id, context=None): context = context or {} if not uom_id: return {'value': {'price_unit': 0.0, 'uom_id': False}} return self.on_change_product_id(cr, uid, ids, product, uom_id=uom_id, context=context) @api.onchange('product_id', 'uom_id') def _onchange_product_id(self): if not self.product_id: return product = self.product_id.with_context( lang=self.order_id.partner_id.lang) self.price_unit = product.list_price self.website_description = product.quote_description or product.website_description self.name = product.name if product.description_sale: self.name += '\n' + product.description_sale self.uom_id = product.product_tmpl_id.uom_id if product and self.order_id.pricelist_id: partner_id = self.order_id.partner_id.id pricelist = self.order_id.pricelist_id.id self.price_unit = self.order_id.pricelist_id.price_get( product.id, self.quantity, partner_id)[pricelist] if self.uom_id and self.uom_id != self.product_id.uom_id: self.price_unit = self.product_id.uom_id._compute_price( self.price_unit, self.uom_id.id) domain = { 'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } return {'domain': domain}