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 res_users(osv.osv): """ User class. A res.users record models an OpenERP user and is different from an employee. res.users class now inherits from res.partner. The partner model is used to store the data related to the partner: lang, name, address, avatar, ... The user model is now dedicated to technical data. """ __admin_ids = {} _uid_cache = {} _inherits = { 'res.partner': 'partner_id', } _name = "res.users" _description = 'Users' def _set_new_password(self, cr, uid, id, name, value, args, context=None): if value is False: # Do not update the password if no value is provided, ignore silently. # For example web client submits False values for all empty fields. return if uid == id: # To change their own password users must use the client-specific change password wizard, # so that the new password is immediately used for further RPC requests, otherwise the user # will face unexpected 'Access Denied' exceptions. raise UserError(_('Please use the change password wizard (in User Preferences or User menu) to change your own password.')) self.write(cr, uid, id, {'password': value}) def _get_password(self, cr, uid, ids, arg, karg, context=None): return dict.fromkeys(ids, '') _columns = { 'id': fields.integer('ID'), 'login_date': fields.datetime('Latest connection', select=1, copy=False), 'partner_id': fields.many2one('res.partner', required=True, string='Related Partner', ondelete='restrict', help='Partner-related data of the user', auto_join=True), 'login': fields.char('Login', size=64, required=True, help="Used to log into the system"), 'password': fields.char('Password', size=64, invisible=True, copy=False, help="Keep empty if you don't want the user to be able to connect on the system."), 'new_password': fields.function(_get_password, type='char', size=64, fnct_inv=_set_new_password, string='Set Password', help="Specify a value only when creating a user or if you're "\ "changing the user's password, otherwise leave empty. After "\ "a change of password, the user has to login again."), 'signature': fields.html('Signature'), 'active': fields.boolean('Active'), 'action_id': fields.many2one('ir.actions.actions', 'Home Action', help="If specified, this action will be opened at log on for this user, in addition to the standard menu."), 'groups_id': fields.many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', 'Groups'), # Special behavior for this field: res.company.search() will only return the companies # available to the current user (should be the user's companies?), when the user_preference # context is set. 'company_id': fields.many2one('res.company', 'Company', required=True, help='The company this user is currently working for.', context={'user_preference': True}), 'company_ids':fields.many2many('res.company','res_company_users_rel','user_id','cid','Companies'), } # overridden inherited fields to bypass access rights, in case you have # access to the user but not its corresponding partner name = openerp.fields.Char(related='partner_id.name', inherited=True) email = openerp.fields.Char(related='partner_id.email', inherited=True) def on_change_login(self, cr, uid, ids, login, context=None): if login and tools.single_email_re.match(login): return {'value': {'email': login}} return {} def onchange_state(self, cr, uid, ids, state_id, context=None): partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)] return self.pool.get('res.partner').onchange_state(cr, uid, partner_ids, state_id, context=context) def onchange_type(self, cr, uid, ids, is_company, context=None): """ Wrapper on the user.partner onchange_type, because some calls to the partner form view applied to the user may trigger the partner.onchange_type method, but applied to the user object. """ partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)] return self.pool['res.partner'].onchange_type(cr, uid, partner_ids, is_company, context=context) def onchange_address(self, cr, uid, ids, use_parent_address, parent_id, context=None): """ Wrapper on the user.partner onchange_address, because some calls to the partner form view applied to the user may trigger the partner.onchange_type method, but applied to the user object. """ partner_ids = [user.partner_id.id for user in self.browse(cr, uid, ids, context=context)] return self.pool['res.partner'].onchange_address(cr, uid, partner_ids, use_parent_address, parent_id, context=context) def _check_company(self, cr, uid, ids, context=None): return all(((this.company_id in this.company_ids) or not this.company_ids) for this in self.browse(cr, uid, ids, context)) _constraints = [ (_check_company, 'The chosen company is not in the allowed companies for this user', ['company_id', 'company_ids']), ] _sql_constraints = [ ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !') ] def _get_company(self,cr, uid, context=None, uid2=False): if not uid2: uid2 = uid # Use read() to compute default company, and pass load=_classic_write to # avoid useless name_get() calls. This will avoid prefetching fields # while computing default values for new db columns, as the # db backend may not be fully initialized yet. user_data = self.pool['res.users'].read(cr, uid, uid2, ['company_id'], context=context, load='_classic_write') comp_id = user_data['company_id'] return comp_id or False def _get_companies(self, cr, uid, context=None): c = self._get_company(cr, uid, context) if c: return [c] return False def _get_group(self,cr, uid, context=None): dataobj = self.pool.get('ir.model.data') result = [] try: dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_user') result.append(group_id) dummy,group_id = dataobj.get_object_reference(cr, SUPERUSER_ID, 'base', 'group_partner_manager') result.append(group_id) except ValueError: # If these groups does not exists anymore pass return result def _get_default_image(self, cr, uid, context=None): return self.pool['res.partner']._get_default_image(cr, uid, False, colorize=True, context=context) _defaults = { 'password': '', 'active': True, 'customer': False, 'company_id': _get_company, 'company_ids': _get_companies, 'groups_id': _get_group, 'image': _get_default_image, } # User can write on a few of his own fields (but not his groups for example) SELF_WRITEABLE_FIELDS = ['password', 'signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz'] # User can read a few of his own fields SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update'] def read(self, cr, uid, ids, fields=None, context=None, load='_classic_read'): def override_password(o): if ('id' not in o or o['id'] != uid): for f in USER_PRIVATE_FIELDS: if f in o: o[f] = '********' return o if fields and (ids == [uid] or ids == uid): for key in fields: if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')): break else: # safe fields only, so we read as super-user to bypass access rights uid = SUPERUSER_ID result = super(res_users, self).read(cr, uid, ids, fields=fields, context=context, load=load) canwrite = self.pool['ir.model.access'].check(cr, uid, 'res.users', 'write', False) if not canwrite: if isinstance(ids, (int, long)): result = override_password(result) else: result = map(override_password, result) return result def create(self, cr, uid, vals, context=None): user_id = super(res_users, self).create(cr, uid, vals, context=context) user = self.browse(cr, uid, user_id, context=context) if user.partner_id.company_id: user.partner_id.write({'company_id': user.company_id.id}) return user_id def write(self, cr, uid, ids, values, context=None): if not hasattr(ids, '__iter__'): ids = [ids] if ids == [uid]: for key in values.keys(): if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')): break else: if 'company_id' in values: user = self.browse(cr, SUPERUSER_ID, uid, context=context) if not (values['company_id'] in user.company_ids.ids): del values['company_id'] uid = 1 # safe fields only, so we write as super-user to bypass access rights res = super(res_users, self).write(cr, uid, ids, values, context=context) if 'company_id' in values: for user in self.browse(cr, uid, ids, context=context): # if partner is global we keep it that way if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']: user.partner_id.write({'company_id': user.company_id.id}) # clear caches linked to the users self.pool['ir.model.access'].call_cache_clearing_methods(cr) clear = partial(self.pool['ir.rule'].clear_cache, cr) map(clear, ids) db = cr.dbname if db in self._uid_cache: for id in ids: if id in self._uid_cache[db]: del self._uid_cache[db][id] self._context_get.clear_cache(self) self.has_group.clear_cache(self) return res def unlink(self, cr, uid, ids, context=None): if 1 in ids: raise UserError(_('You can not remove the admin user as it is used internally for resources created by Odoo (updates, module installation, ...)')) db = cr.dbname if db in self._uid_cache: for id in ids: if id in self._uid_cache[db]: del self._uid_cache[db][id] return super(res_users, self).unlink(cr, uid, ids, context=context) def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): if not args: args=[] if not context: context={} ids = [] if name and operator in ['=', 'ilike']: ids = self.search(cr, user, [('login','=',name)]+ args, limit=limit, context=context) if not ids: ids = self.search(cr, user, [('name',operator,name)]+ args, limit=limit, context=context) return self.name_get(cr, user, ids, context=context) def copy(self, cr, uid, id, default=None, context=None): user2copy = self.read(cr, uid, [id], ['login','name'])[0] default = dict(default or {}) if ('name' not in default) and ('partner_id' not in default): default['name'] = _("%s (copy)") % user2copy['name'] if 'login' not in default: default['login'] = _("%s (copy)") % user2copy['login'] return super(res_users, self).copy(cr, uid, id, default, context) @tools.ormcache(skiparg=2) def _context_get(self, cr, uid): user = self.browse(cr, SUPERUSER_ID, uid) result = {} for k in self._fields: if k.startswith('context_'): context_key = k[8:] elif k in ['lang', 'tz']: context_key = k else: context_key = False if context_key: res = getattr(user, k) or False if isinstance(res, models.BaseModel): res = res.id result[context_key] = res or False return result def context_get(self, cr, uid, context=None): return self._context_get(cr, uid) def action_get(self, cr, uid, context=None): dataobj = self.pool['ir.model.data'] data_id = dataobj._get_id(cr, SUPERUSER_ID, 'base', 'action_res_users_my') return dataobj.browse(cr, uid, data_id, context=context).res_id def check_super(self, passwd): if passwd == tools.config['admin_passwd']: return True else: raise openerp.exceptions.AccessDenied() def check_credentials(self, cr, uid, password): """ Override this method to plug additional authentication methods""" res = self.search(cr, SUPERUSER_ID, [('id','=',uid),('password','=',password)]) if not res: raise openerp.exceptions.AccessDenied() def _login(self, db, login, password): if not password: return False user_id = False cr = self.pool.cursor() try: # autocommit: our single update request will be performed atomically. # (In this way, there is no opportunity to have two transactions # interleaving their cr.execute()..cr.commit() calls and have one # of them rolled back due to a concurrent access.) cr.autocommit(True) # check if user exists res = self.search(cr, SUPERUSER_ID, [('login','=',login)]) if res: user_id = res[0] # check credentials self.check_credentials(cr, user_id, password) # We effectively unconditionally write the res_users line. # Even w/ autocommit there's a chance the user row will be locked, # in which case we can't delay the login just for the purpose of # update the last login date - hence we use FOR UPDATE NOWAIT to # try to get the lock - fail-fast # Failing to acquire the lock on the res_users row probably means # another request is holding it. No big deal, we don't want to # prevent/delay login in that case. It will also have been logged # as a SQL error, if anyone cares. try: # NO KEY introduced in PostgreSQL 9.3 http://www.postgresql.org/docs/9.3/static/release-9-3.html#AEN115299 update_clause = 'NO KEY UPDATE' if cr._cnx.server_version >= 90300 else 'UPDATE' cr.execute("SELECT id FROM res_users WHERE id=%%s FOR %s NOWAIT" % update_clause, (user_id,), log_exceptions=False) cr.execute("UPDATE res_users SET login_date = now() AT TIME ZONE 'UTC' WHERE id=%s", (user_id,)) self.invalidate_cache(cr, user_id, ['login_date'], [user_id]) except Exception: _logger.debug("Failed to update last_login for db:%s login:%s", db, login, exc_info=True) except openerp.exceptions.AccessDenied: _logger.info("Login failed for db:%s login:%s", db, login) user_id = False finally: cr.close() return user_id def authenticate(self, db, login, password, user_agent_env): """Verifies and returns the user ID corresponding to the given ``login`` and ``password`` combination, or False if there was no matching user. :param str db: the database on which user is trying to authenticate :param str login: username :param str password: user password :param dict user_agent_env: environment dictionary describing any relevant environment attributes """ uid = self._login(db, login, password) if uid == openerp.SUPERUSER_ID: # Successfully logged in as admin! # Attempt to guess the web base url... if user_agent_env and user_agent_env.get('base_location'): cr = self.pool.cursor() try: base = user_agent_env['base_location'] ICP = self.pool['ir.config_parameter'] if not ICP.get_param(cr, uid, 'web.base.url.freeze'): ICP.set_param(cr, uid, 'web.base.url', base) cr.commit() except Exception: _logger.exception("Failed to update web.base.url configuration parameter") finally: cr.close() return uid def check(self, db, uid, passwd): """Verifies that the given (uid, password) is authorized for the database ``db`` and raise an exception if it is not.""" if not passwd: # empty passwords disallowed for obvious security reasons raise openerp.exceptions.AccessDenied() if self._uid_cache.get(db, {}).get(uid) == passwd: return cr = self.pool.cursor() try: self.check_credentials(cr, uid, passwd) if self._uid_cache.has_key(db): self._uid_cache[db][uid] = passwd else: self._uid_cache[db] = {uid:passwd} finally: cr.close() def change_password(self, cr, uid, old_passwd, new_passwd, context=None): """Change current user password. Old password must be provided explicitly to prevent hijacking an existing user session, or for cases where the cleartext password is not used to authenticate requests. :return: True :raise: openerp.exceptions.AccessDenied when old password is wrong :raise: except_osv when new password is not set or empty """ self.check(cr.dbname, uid, old_passwd) if new_passwd: return self.write(cr, uid, uid, {'password': new_passwd}) raise UserError(_("Setting empty passwords is not allowed for security reasons!")) def preference_save(self, cr, uid, ids, context=None): return { 'type': 'ir.actions.client', 'tag': 'reload_context', } def preference_change_password(self, cr, uid, ids, context=None): return { 'type': 'ir.actions.client', 'tag': 'change_password', 'target': 'new', } @tools.ormcache(skiparg=2) def has_group(self, cr, uid, group_ext_id): """Checks whether user belongs to given group. :param str group_ext_id: external ID (XML ID) of the group. Must be provided in fully-qualified form (``module.ext_id``), as there is no implicit module to use.. :return: True if the current user is a member of the group with the given external ID (XML ID), else False. """ assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified" module, ext_id = group_ext_id.split('.') cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""", (uid, module, ext_id)) return bool(cr.fetchone()) def get_company_currency_id(self, cr, uid, context=None): return self.browse(cr, uid, uid, context=context).company_id.currency_id.id
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 = self.uom_id or product.uom_id pricelist = self.order_id.pricelist_id if pricelist and product: partner_id = self.order_id.partner_id.id self.price_unit = pricelist.with_context( uom=self.uom_id.id).price_get(product.id, self.quantity, partner_id)[pricelist.id] domain = { 'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } return {'domain': domain}
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: date = time.strftime('%Y-%m-%d') 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, date, False, fiscal_position_id, True, context) data = res.get('value', {}) if pricelist_id: pricelist = self.pool['product.pricelist'].browse( cr, uid, [pricelist_id], context=context)[0] data.update(self.pool['sale.order.line']._get_purchase_price( cr, uid, pricelist, line.product_id, line.product_uom_id, date, context=context)) 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', 'customer_lead': self._get_customer_lead(cr, uid, line.product_id.product_tmpl_id), }) 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.action_confirm(cr, SUPERUSER_ID, order.id, 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, 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) def _get_payment_type(self, cr, uid, ids, context=None): return 'form' def _set_default_value_on_column(self, cr, column_name, context=None): if column_name != 'access_token': super(sale_order, self)._set_default_value_on_column(cr, column_name, context=context) else: query = """UPDATE %(table_name)s SET %(column_name)s = md5(random()::text || clock_timestamp()::text)::uuid WHERE %(column_name)s IS NULL """ % { 'table_name': self._table, 'column_name': column_name } cr.execute(query)
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.product_uom_qty * line.price_unit) 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'), '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.boolean( 'Immediate 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 True 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: price = pricelist_obj.price_get(cr, uid, [pricelist_id], line.product_id.id, 1, context=context)[pricelist_id] else: price = line.price_unit if 'tax_id' in data: data['tax_id'] = [(6, 0, data['tax_id'])] 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: price = pricelist_obj.price_get(cr, uid, [pricelist_id], option.product_id.id, 1, context=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, 'note': quote_template.note, 'options': options, 'validity_date': date, 'require_payment': quote_template.require_payment } 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, id, 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, id, context=context) if not quote.template_id: return super(sale_order, self).get_access_action(cr, uid, id, context=context) return { 'type': 'ir.actions.act_url', 'url': '/quote/%s' % id, 'target': 'self', 'res_id': 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
class crops(osv.Model): _name = 'crops' _description = 'Crops' _columns = { 'irrigation_line': fields.one2many('irrigation.details', 'irrigation'), 'landpreparation_line': fields.one2many('landpreparation.details', 'landpreparation'), 'fertilizer_line': fields.one2many('fertilizer.details', 'fertilizer'), 'sowing_line': fields.one2many('sowing.details', 'sowing'), 'sampling_line': fields.one2many('sampling.details', 'sampling'), 'survey_line': fields.one2many('survey.details', 'survey'), 'harvesting_line': fields.one2many('harvesting.details', 'harvesting'), 'scouting_line': fields.one2many('scouting.details', 'scout'), 'pest_line': fields.one2many('pest.issue', 'pest'), 'weeding_line': fields.one2many('weeding.controle', 'weeding'), 'disease_line': fields.one2many('disease.issue', 'disease'), 'weed_line': fields.one2many('weed.issue', 'weed'), 'insect_line': fields.one2many('insect.issue', 'insect'), 'treat_line': fields.one2many('treat.issue', 'treat'), 'crop_name': fields.char('Cultivation Name', size=64, required=True), 'land_name': fields.many2one('plot', 'Plot Name'), 'type': fields.char('Crop Type', size=64), 'fert_used': fields.char('Fertilizers Used', size=64), 'sowing_date': fields.datetime('Planting Date'), 'harvesting_date': fields.datetime('Harvesting Date'), 'crop_variety': fields.char('Crop Variety', size=64), 'year': fields.integer('Year of Cultivation', size=64), 'current_crop': fields.char('Current Crop', size=64), 'crop_yield': fields.integer('Yield', size=64), 'des': fields.html('Description'), 'current_market_price': fields.integer('Market Price'), 'actual_productivity': fields.integer("Actual Productivity", size=64), 'estimated_productivity': fields.integer('Estimated Productivity', size=64), 'state': fields.selection([('initial', 'Initial'), ('surveyed', 'Surveyed'), ('landprepared', 'Land Preparation'), ('planted', 'Planting'), ('intercropping', 'Intercropping'), ('harvest', 'Harvesting'), ('ratoon', 'Ratooning')], 'Status', readonly=False, help='The status of the current cultivation', copy=False), }
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' def __get_bar_values(self, cr, uid, obj, domain, read_fields, value_field, groupby_field, date_begin, context=None): """ Generic method to generate data for bar chart values using SparklineBarWidget. This method performs obj.read_group(cr, uid, domain, read_fields, groupby_field). :param obj: the target model (i.e. crm_lead) :param domain: the domain applied to the read_group :param list read_fields: the list of fields to read in the read_group :param str value_field: the field used to compute the value of the bar slice :param str groupby_field: the fields used to group :return list section_result: a list of dicts: [ { 'value': (int) bar_column_value, 'tootip': (str) bar_column_tooltip, } ] """ date_begin = date_begin.date() section_result = [{ 'value': 0, 'tooltip': (date_begin + relativedelta.relativedelta(days=i)).strftime('%d %B %Y'), } for i in range(0, self._period_number)] group_obj = obj.read_group(cr, uid, domain, read_fields, groupby_field, context=context) field_col_info = obj._all_columns.get(groupby_field.split(':')[0]) pattern = tools.DEFAULT_SERVER_DATE_FORMAT if field_col_info.column._type == 'date' else tools.DEFAULT_SERVER_DATETIME_FORMAT for group in group_obj: group_begin_date = datetime.strptime(group['__domain'][0][2], pattern).date() timedelta = relativedelta.relativedelta(group_begin_date, date_begin) section_result[timedelta.days] = { 'value': group.get(value_field, 0), 'tooltip': group.get(groupby_field) } return section_result def _get_daily_statistics(self, cr, uid, ids, field_name, arg, context=None): """ Get the daily statistics of the mass mailing. This is done by a grouping on opened and replied fields. Using custom format in context, we obtain results for the next 6 days following the mass mailing date. """ obj = self.pool['mail.mail.statistics'] res = {} for mailing in self.browse(cr, uid, ids, context=context): res[mailing.id] = {} date = mailing.sent_date if mailing.sent_date else mailing.create_date date_begin = datetime.strptime( date, tools.DEFAULT_SERVER_DATETIME_FORMAT) date_end = date_begin + relativedelta.relativedelta( days=self._period_number - 1) date_begin_str = date_begin.strftime( tools.DEFAULT_SERVER_DATETIME_FORMAT) date_end_str = date_end.strftime( tools.DEFAULT_SERVER_DATETIME_FORMAT) domain = [('mass_mailing_id', '=', mailing.id), ('opened', '>=', date_begin_str), ('opened', '<=', date_end_str)] res[mailing.id]['opened_daily'] = json.dumps( self.__get_bar_values(cr, uid, obj, domain, ['opened'], 'opened_count', 'opened:day', date_begin, context=context)) domain = [('mass_mailing_id', '=', mailing.id), ('replied', '>=', date_begin_str), ('replied', '<=', date_end_str)] res[mailing.id]['replied_daily'] = json.dumps( self.__get_bar_values(cr, uid, obj, domain, ['replied'], 'replied_count', 'replied:day', date_begin, context=context)) return res def _get_statistics(self, cr, uid, ids, name, arg, context=None): """ Compute statistics of the mass mailing campaign """ 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.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 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['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 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 # indirections for inheritance _mailing_model = lambda self, *args, **kwargs: self._get_mailing_model( *args, **kwargs) _columns = { 'name': fields.char('Subject', required=True), 'email_from': fields.char('From', required=True), 'create_date': fields.datetime('Creation Date'), 'sent_date': fields.datetime('Sent Date', oldname='date', copy=False), 'body_html': fields.html('Body'), 'attachment_ids': fields.many2many('ir.attachment', 'mass_mailing_ir_attachments_rel', 'mass_mailing_id', 'attachment_id', 'Attachments'), 'mass_mailing_campaign_id': fields.many2one( 'mail.mass_mailing.campaign', 'Mass Mailing Campaign', ondelete='set null', ), 'state': fields.selection( [('draft', 'Draft'), ('test', 'Tested'), ('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', 'In Document'), ('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( 'AB 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_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', 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', ), # daily ratio 'opened_daily': fields.function( _get_daily_statistics, string='Opened', type='char', multi='_get_daily_statistics', ), 'replied_daily': fields.function( _get_daily_statistics, string='Replied', type='char', multi='_get_daily_statistics', ) } 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 = { '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, } #------------------------------------------------------ # 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'), ('test', 'Tested'), ('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) #------------------------------------------------------ # 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) == 3: mailing_list_ids |= set(item[2]) if mailing_list_ids: value['mailing_domain'] = "[('list_id', 'in', %s)]" % list( mailing_list_ids) else: value['mailing_domain'] = "[('list_id', '=', False)]" else: value['mailing_domain'] = False 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, } 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, } def action_edit_html(self, cr, uid, ids, context=None): if not len(ids) == 1: raise ValueError('One and only one ID allowed for this action') mail = self.browse(cr, uid, ids[0], context=context) url = '/website_mail/email_designer?model=mail.mass_mailing&res_id=%d&template_model=%s&enable_editor=1' % ( ids[0], mail.mailing_model) return { 'name': _('Open with Visual Editor'), 'type': 'ir.actions.act_url', 'url': url, 'target': 'self', } #------------------------------------------------------ # 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 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_recipients(cr, uid, mailing, context=context) if not res_ids: raise Warning('Please select recipients.') comp_ctx = dict(context, active_ids=res_ids) composer_values = { 'author_id': author_id, 'body': mailing.body_html, '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], 'same_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], context=comp_ctx) self.write(cr, uid, [mailing.id], { 'sent_date': fields.datetime.now(), 'state': 'done' }, context=context) return True
class BlogPost(osv.Model): _name = "blog.post" _description = "Blog Post" _inherit = ['mail.thread', 'website.seo.metadata'] _order = 'id DESC' 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 _columns = { 'name': fields.char('Title', required=True, translate=True), 'subtitle': fields.char('Sub Title', translate=True), 'author_id': fields.many2one('res.partner', 'Author'), 'background_image': fields.binary('Background Image', oldname='content_image'), '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 control 'website_published': fields.boolean( 'Publish', help="Publish on the website", copy=False, ), 'website_message_ids': fields.one2many( 'mail.message', 'res_id', domain=lambda self: [ '&', '&', ('model', '=', self._name), ('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': _('Blog Post Title'), 'subtitle': _('Subtitle'), '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. """ mapping = [] if not html: return html, mapping if tags is None: tags = ['p'] if attribute is None: attribute = 'data-unique-id' counter = 0 # 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 not node.tag in tags: continue ancestor_tags = [parent.tag for parent in node.iterancestors()] if ancestor_tags: ancestor_tags.pop() ancestor_tags.append('counter_%s' % counter) new_attribute = '/'.join(reversed(ancestor_tags)) old_attribute = node.get(attribute) node.set(attribute, new_attribute) mapping.append((old_attribute, counter)) counter += 1 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) for old_attribute, new_attribute in mapping: if not old_attribute: continue msg_ids = self.pool['mail.message'].search( cr, SUPERUSER_ID, [('path', '=', old_attribute)], context=context) self.pool['mail.message'].write(cr, SUPERUSER_ID, msg_ids, {'path': new_attribute}, 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', context=context) 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 'content' in vals: vals['content'] = self._postproces_content(cr, uid, None, 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
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, id, 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, id, 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 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) #used for undisplay the follower if it's the current user def _get_my_current_partner(self, cr, uid, ids, name, args, context=None): user = self.pool.get('res.users').browse(cr, uid, uid, context=context) pid = user.partner_id and user.partner_id.id or False return dict.fromkeys(ids, pid) #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), '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'), 'current_partner_id': fields.function(_get_my_current_partner, type="many2one", relation='res.partner', string="Owner"), } _defaults = { '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): 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 #dict of stages: map les ids sur les noms stage_name = dict( self.pool.get('note.stage').name_get(cr, uid, current_stage_ids, context=context)) result = [ { #notes by stage for stages user '__context': { 'group_by': groupby[1:] }, '__domain': domain + [('stage_ids.id', '=', current_stage_id)], 'stage_id': (current_stage_id, stage_name[current_stage_id]), 'stage_id_count': self.search(cr, uid, domain + [('stage_ids', '=', current_stage_id)], context=context, count=True) } for current_stage_id in current_stage_ids ] #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 ] else: # add the first stage column result = [{ '__context': { 'group_by': groupby[1:] }, '__domain': domain + [dom_not_in], 'stage_id': (current_stage_ids[0], stage_name[current_stage_ids[0]]), 'stage_id_count': nb_notes_ws }] + 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(self, cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby)
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 OpenERP 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 = openerp.fields.Binary("Image", attachment=True, help="This field holds the image used for this provider, limited to 1024x1024px") image_medium = openerp.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 = openerp.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.") @openerp.api.depends('image') def _compute_images(self): for rec in self: rec.image_medium = openerp.tools.image_resize_image_medium(rec.image) rec.image_small = openerp.tools.image_resize_image_small(rec.image) def _inverse_image_medium(self): for rec in self: rec.image = openerp.tools.image_resize_image_big(rec.image_medium) def _inverse_image_small(self): for rec in self: rec.image = openerp.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: OpenERP 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: OpenERP 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 product_template(osv.Model): _inherit = "product.template" def _get_parallax_image(self, cr, uid, ids, name, args, context=None): result = dict.fromkeys(ids, False) for obj in self.browse(cr, uid, ids, context=context): result[obj.id] = tools.image_get_resized_images( obj.parallax_image, big_name='parallax_image', medium_name='parallax_image_medium', small_name='parallax_image_small', avoid_resize_medium=True) return result def _set_parallax_image(self, cr, uid, id, name, value, args, context=None): return self.write(cr, uid, [id], {'parallax_image': value}, context=context) def _get_square_image(self, cr, uid, ids, name, args, context=None): result = dict.fromkeys(ids, False) for obj in self.browse(cr, uid, ids, context=context): # need to return also an dict for the image like result[1] = {'image_square': base_64_data} result[obj.id] = {'image_square': False} if obj.image: result[obj.id] = { 'image_square': resize_to_thumbnail( img=obj.image, box=(440, 440), ) } return result def _set_square_image(self, cr, uid, id, name, value, args, context=None): return self.write(cr, uid, [id], {'image': value}, context=context) # OVERRIDE orignal image functional fields to store full size images def _set_image(self, cr, uid, id, name, value, args, context=None): return self.write(cr, uid, [id], {'image': value}, context=context) def _get_image(self, cr, uid, ids, name, args, context=None): result = dict.fromkeys(ids, False) for obj in self.browse(cr, uid, ids, context=context): result[obj.id] = tools.image_get_resized_images( obj.image, avoid_resize_medium=True) return result _columns = { 'hide_payment': fields.boolean('Hide complete Checkout Panel'), 'hide_price': fields.boolean('Hide Price in Shop overview Pages'), 'hide_quantity': fields.boolean('Hide Product-Quantity-Selector in CP'), 'simple_checkout': fields.boolean('Simple Checkout'), 'price_donate': fields.boolean('Arbitrary Price'), 'price_donate_min': fields.integer(string='Minimum Arbitrary Price'), 'payment_interval_ids': fields.many2many('product.payment_interval', string='Payment Intervals'), 'hide_search': fields.boolean('Hide Search Field'), 'hide_categories': fields.boolean('Hide Categories Navigation'), 'hide_image': fields.boolean('Hide Image in Checkout Panel'), 'hide_salesdesc': fields.boolean('Hide Text in Checkout Panel'), 'hide_panelfooter': fields.boolean('Hide Checkout Panel Footer'), 'show_desctop': fields.boolean('Show additional Description above Checkout Panel'), 'show_descbottom': fields.boolean('Show additional Description below Checkout Panel'), 'desc_short_top': fields.html(string='Banner Product Description - Top'), 'desc_short': fields.html(string='Banner Product Description - Center'), 'desc_short_bottom': fields.html(string='Banner Product Description - Bottom'), 'image_square': fields.function(_get_square_image, fnct_inv=_set_square_image, string="Square Image (Auto crop and zoom)", type="binary", multi="_get_square_image", store={'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10)}), 'parallax_image': fields.binary(string='Background Parallax Image'), 'parallax_image_medium': fields.function(_get_parallax_image, fnct_inv=_set_parallax_image, string="Background Parallax Image", type="binary", multi="_get_parallax_image", store={ 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['parallax_image'], 10), }, help="Medium-sized image of the background. It is automatically "\ "resized as a 128x128px image, with aspect ratio preserved, "\ "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."), 'parallax_speed': fields.selection([('static', 'Static'), ('slow', 'Slow')], string='Parallax Speed'), # OVERRIDE orignal image functional fields to store full size images 'image_medium': fields.function(_get_image, fnct_inv=_set_image, string="Medium-sized image", type="binary", multi="_get_image", store={ 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), }, help="Medium-sized image of the product. It is automatically "\ "resized as a 128x128px image, with aspect ratio preserved, "\ "only when the image exceeds one of those sizes. Use this field in form views or some kanban views."), 'image_small': fields.function(_get_image, fnct_inv=_set_image, string="Small-sized image", type="binary", multi="_get_image", store={ 'product.template': (lambda self, cr, uid, ids, c={}: ids, ['image'], 10), }, help="Small-sized image of the product. It is automatically "\ "resized as a 64x64px image, with aspect ratio preserved. "\ "Use this field anywhere a small image is required."), } _defaults = { 'price_donate_min': 0, 'parallax_speed': 'slow', 'hide_quantity': True, }
class product_template(osv.Model): _inherit = 'product.template' def _sold_total(self, cr, uid, ids, field_name, arg, context=None): res = dict.fromkeys(ids, 0) for template in self.browse(cr, SUPERUSER_ID, ids, context=context): res[template.id] = sum( [p.sold_total for p in template.product_variant_ids]) return res def _funding_reached(self, cr, uid, ids, field_name, arg, context=None): res = dict.fromkeys(ids, 0) for ptemplate in self.browse(cr, SUPERUSER_ID, ids, context=context): try: res[ptemplate.id] = int( round(ptemplate.sold_total / (ptemplate.funding_goal / 100))) except: res[ptemplate.id] = int(0) return res def action_view_sales_sold_total(self, cr, uid, ids, context=None): act_obj = self.pool.get('ir.actions.act_window') mod_obj = self.pool.get('ir.model.data') # find the related product.product ids product_ids = [] for template in self.browse(cr, uid, ids, context=context): product_ids += [x.id for x in template.product_variant_ids] domain = [ ('state', 'in', ["confirmed", "done"]), ('product_id', 'in', product_ids), ] # get the tree view result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree', raise_if_not_found=True) result = act_obj.read(cr, uid, [result], context=context)[0] # add the search domain result['domain'] = str(domain) return result # Hack because i could not find a way to browse res.partner.name in qweb template - always error 403 access rights # The positive side effect is better security since no one can browse res.partner fully! def _get_name(self, cr, uid, ids, flds, args, context=None): res = dict.fromkeys(ids, 0) for ptemplate in self.browse(cr, SUPERUSER_ID, ids, context=context): if ptemplate.funding_user: res[ptemplate.id] = ptemplate.funding_user.name else: res[ptemplate.id] = False return res _columns = { 'sold_total': fields.function(_sold_total, string='# Sold Total', type='float'), 'funding_goal': fields.float(string='Funding Goal'), 'funding_desc': fields.html(string='Funding Description (HTML Field below Bar)'), 'funding_reached': fields.function(_funding_reached, string='Funding reached in %', type='integer'), 'funding_user': fields.many2one('res.partner', string='Funding-Campaign User'), 'funding_user_name': fields.function(_get_name, string="Funding-Campaign User Name", type='char'), 'hide_fundingtextinlist': fields.boolean('Hide Funding-Text in Overview-Pages'), 'hide_fundingbarinlist': fields.boolean('Hide Funding-Bar in Overview-Pages'), 'hide_fundingtextincp': fields.boolean('Hide Funding-Text in Checkout-Panel'), 'hide_fundingbarincp': fields.boolean('Hide Funding-Bar in Checkout-Panel'), 'hide_fundingtext': fields.boolean('Hide Funding-Text in Page'), 'hide_fundingbar': fields.boolean('Hide Funding-Bar in Page'), 'hide_fundingdesc': fields.boolean('Hide Funding-Description in Page'), }
class ebay_seller_list(osv.osv): _name = "ebay.seller.list" _description = "ebay sell" def _get_thumbnail(self, cr, uid, ids, field_name, arg, context): if context is None: context = {} res = {} for record in self.browse(cr, uid, ids, context=context): link = "http://thumbs3.ebaystatic.com/pict/%s8080.jpg" % record.item_id res[record.id] = base64.encodestring(urllib2.urlopen(link).read()) return res _columns = { 'buy_it_now_price': fields.float('Buy It Now Price'), 'currency': fields.char('Currency ID', size=3), 'hit_count': fields.integer('Hit Count', readonly=True), 'item_id': fields.char('Item ID', size=38, readonly=True), # ListingDetails 'end_time': fields.datetime('End Time', readonly=True), 'start_time': fields.datetime('Start Time', readonly=True), 'view_item_url': fields.char('View Item URL', readonly=True), 'quantity': fields.char('Quantity'), # SellingStatus 'quantity_sold': fields.integer('Quantity Sold', readonly=True), 'start_price': fields.float('StartPrice'), 'name': fields.char('Title', size=80), 'watch_count': fields.integer('Watch Count', readonly=True), 'user_id': fields.many2one('ebay.user', 'Seller', ondelete='cascade'), # Additional Info 'thumbnail': fields.function(_get_thumbnail, type='binary', method=True, string="Thumbnail"), 'picture': fields.html('Picture', readonly=True), 'average_monthly_sales': fields.integer('Average Monthly Sales', readonly=True), } _order = 'average_monthly_sales desc' def create_items(self, cr, uid, user, items, context=None): monthly_sales = 0 monthly_sales_volume = 0 now = datetime.now() for item in ebay_repeatable_list(items): if item.ListingType not in ('FixedPriceItem', 'StoresFixedPrice'): continue vals = dict() vals['buy_it_now_price'] = float(item.BuyItNowPrice.value) vals['currency'] = item.Currency vals['hit_count'] = item.HitCount if item.has_key( 'HitCount') else 0 vals['item_id'] = item.ItemID listing_details = item.ListingDetails vals['end_time'] = listing_details.EndTime start_time = listing_details.StartTime vals['start_time'] = start_time vals['view_item_url'] = listing_details.ViewItemURL vals['quantity'] = int(item.Quantity) selling_status = item.SellingStatus start_price = float(item.StartPrice.value) quantity_sold = int(selling_status.QuantitySold) vals['quantity_sold'] = quantity_sold vals['start_price'] = start_price vals['name'] = item.Title vals['watch_count'] = item.WatchCount if item.has_key( 'WatchCount') else 0 vals['user_id'] = user.id delta_days = (now - start_time).days if delta_days <= 0: delta_days = 1 average_monthly_sales = quantity_sold * 30 / delta_days monthly_sales += start_price * average_monthly_sales monthly_sales_volume += average_monthly_sales vals['average_monthly_sales'] = average_monthly_sales if item.has_key( 'PictureDetails' ) and item.PictureDetails and item.PictureDetails.has_key( 'PictureURL'): picture_url = item.PictureDetails.PictureURL vals[ 'picture'] = '<img src="%s" width="500"/>' % ebay_repeatable_list( picture_url)[0] self.create(cr, uid, vals, context=context) return monthly_sales, monthly_sales_volume def get_seller_list_call(self, cr, uid, user, call_param, parallel=None, context=None): output_selector = [ 'HasMoreItems', 'ItemArray.Item.BuyItNowPrice', 'ItemArray.Item.Currency', 'ItemArray.Item.ItemID', 'ItemArray.Item.ListingDetails.ConvertedStartPrice', 'ItemArray.Item.ListingDetails.StartTime', 'ItemArray.Item.ListingDetails.EndTime', 'ItemArray.Item.ListingDetails.ViewItemURL', 'ItemArray.Item.ListingType', 'ItemArray.Item.PictureDetails.PictureURL', 'ItemArray.Item.PrimaryCategory', 'ItemArray.Item.Quantity', 'ItemArray.Item.SellingStatus.QuantitySold', 'ItemArray.Item.StartPrice', 'ItemArray.Item.Title', 'ItemsPerPage', 'PageNumber', 'PaginationResult', 'ReturnedItemCountActual', ] call_name = 'GetSellerList' call_data = dict() call_data['EndTimeFrom'] = call_param['end_time_from'] call_data['EndTimeTo'] = call_param['end_time_to'] call_data['IncludeWatchCount'] = True call_data['Pagination'] = { 'EntriesPerPage': call_param['entries_per_page'], 'PageNumber': call_param['page_number'], } call_data['UserID'] = user.name call_data['DetailLevel'] = 'ReturnAll' call_data['OutputSelector'] = output_selector api = self.pool.get('ebay.ebay').trading(cr, uid, user, call_name, parallel=parallel, context=context) api.execute(call_name, call_data) return api def get_seller_list(self, cr, uid, user, context=None): last_updated = user.last_updated if last_updated: now_time = datetime.now() last_updated = datetime.strptime( last_updated, tools.DEFAULT_SERVER_DATETIME_FORMAT) delta = (now_time - last_updated).days if delta < 7: return True cr.execute( 'delete from ebay_seller_list \ where user_id=%s', (user.id, )) now = datetime.now() end_time_from = now.isoformat() end_time_to = (now + timedelta(30)).isoformat() entries_per_page = 160 page_number = 1 call_param = dict(end_time_from=end_time_from, end_time_to=end_time_to, entries_per_page=entries_per_page, page_number=page_number) reply = self.get_seller_list_call(cr, uid, user, call_param, context=context).response.reply total_number_of_pages = int(reply.PaginationResult.TotalNumberOfPages) if total_number_of_pages == 0: return True monthly_sales, monthly_sales_volume = self.create_items( cr, uid, user, reply.ItemArray.Item, context=context) page_number = 2 total_number_of_pages += 1 while page_number < total_number_of_pages: parallel = Parallel() multiple_threads = 0 apis = list() while page_number < total_number_of_pages and multiple_threads < 5: call_param = dict(end_time_from=end_time_from, end_time_to=end_time_to, entries_per_page=entries_per_page, page_number=page_number) apis.append( self.get_seller_list_call(cr, uid, user, call_param, parallel=parallel, context=context)) page_number += 1 multiple_threads += 1 parallel.wait(120) for api in apis: reply = api.response.reply if reply.Ack in ('Success', 'Warning'): _monthly_sales, _monthly_sales_volume = self.create_items( cr, uid, user, reply.ItemArray.Item, context=context) monthly_sales += _monthly_sales monthly_sales_volume += _monthly_sales_volume else: raise ConnectionError(api.error()) return user.write( dict(last_updated=fields.datetime.now(), monthly_sales=monthly_sales, monthly_sales_volume=monthly_sales_volume))
class OEMName(models.Model): _name = 'mft.oem_name' _columns = { 'name': fields.char(string='OEM Name', readonly=True, select=True, states={'draft': [('readonly', False)]}), 'language': fields.char(string='Language', readonly=True, select=True, states={'draft': [('readonly', False)]}), 'website': fields.text(string='Website', readonly=True, select=True, states={'draft': [('readonly', False)]}), 'oemname_env': fields.one2many('mft.oemname_env', 'oemname_id', string='Self-Defined ENV', readonly=True, select=True, states={'draft': [('readonly', False)]}), 'description': fields.html(string='Description', readonly=True, select=True, states={'draft': [('readonly', False)]}), 'state': fields.selection([ ('draft', 'Draft'), ('toconfirm', 'Request Confirm'), ('confirmed', 'Confirmed'), ('cancel', 'Cancel'), ], 'State', readonly=True, copy=False, select=True), #'wo_ids': fields.one2many('mft.work_order','oem_id',string='成品工单') } _defaults = {'state': 'draft'} def action_toconfirm(self, cr, uid, ids, context=None): if context is None: context = {} self.write(cr, uid, ids, {'state': 'toconfirm'}) return True def action_confirmed(self, cr, uid, ids, context=None): if context is None: context = {} self.write(cr, uid, ids, {'state': 'confirmed'}) return True def action_recover(self, cr, uid, ids, context=None): if context is None: context = {} self.write(cr, uid, ids, {'state': 'draft'}) return True def unlink(self, cr, uid, ids, context=None): product_names = self.read(cr, uid, ids, ['state'], context=context) unlink_ids = [] for p in product_names: if p['state'] in ['draft']: unlink_ids.append(p['id']) else: raise osv.except_osv(_('Invalid Action!'), 'Only draft can be deleted!') return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
class task(osv.osv): _name = "project.task" _description = "Task" _date_name = "date_start" _inherit = ['mail.thread', 'ir.needaction_mixin'] _mail_post_access = 'read' def _get_default_partner(self, cr, uid, context=None): if context is None: context = {} if 'default_project_id' in context: project = self.pool.get('project.project').browse(cr, uid, context['default_project_id'], context=context) if project and project.partner_id: return project.partner_id.id return False def _get_default_stage_id(self, cr, uid, context=None): """ Gives default stage_id """ if context is None: context = {} return self.stage_find(cr, uid, [], context.get('default_project_id'), [('fold', '=', False)], context=context) def _read_group_stage_ids(self, cr, uid, ids, domain, read_group_order=None, access_rights_uid=None, context=None): if context is None: context = {} stage_obj = self.pool.get('project.task.type') order = stage_obj._order access_rights_uid = access_rights_uid or uid if read_group_order == 'stage_id desc': order = '%s desc' % order if 'default_project_id' in context: search_domain = ['|', ('project_ids', '=', context['default_project_id']), ('id', 'in', ids)] else: search_domain = [('id', 'in', ids)] stage_ids = stage_obj._search(cr, uid, search_domain, order=order, access_rights_uid=access_rights_uid, context=context) result = stage_obj.name_get(cr, access_rights_uid, stage_ids, context=context) # restore order of the search result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0]))) fold = {} for stage in stage_obj.browse(cr, access_rights_uid, stage_ids, context=context): fold[stage.id] = stage.fold or False return result, fold _group_by_full = { 'stage_id': _read_group_stage_ids, } def onchange_remaining(self, cr, uid, ids, remaining=0.0, planned=0.0): if remaining and not planned: return {'value': {'planned_hours': remaining}} return {} def onchange_planned(self, cr, uid, ids, planned=0.0, effective=0.0): return {'value': {'remaining_hours': planned - effective}} def onchange_project(self, cr, uid, id, project_id, context=None): if project_id: project = self.pool.get('project.project').browse(cr, uid, project_id, context=context) if project and project.partner_id: return {'value': {'partner_id': project.partner_id.id}} return {'value': {'partner_id': False}} def onchange_user_id(self, cr, uid, ids, user_id, context=None): vals = {} if user_id: vals['date_start'] = fields.datetime.now() return {'value': vals} def duplicate_task(self, cr, uid, map_ids, context=None): mapper = lambda t: map_ids.get(t.id, t.id) for task in self.browse(cr, uid, map_ids.values(), context): new_child_ids = set(map(mapper, task.child_ids)) new_parent_ids = set(map(mapper, task.parent_ids)) if new_child_ids or new_parent_ids: task.write({'parent_ids': [(6,0,list(new_parent_ids))], 'child_ids': [(6,0,list(new_child_ids))]}) def copy_data(self, cr, uid, id, default=None, context=None): if default is None: default = {} if not default.get('name'): current = self.browse(cr, uid, id, context=context) default['name'] = _("%s (copy)") % current.name return super(task, self).copy_data(cr, uid, id, default, context) def _is_template(self, cr, uid, ids, field_name, arg, context=None): res = {} for task in self.browse(cr, uid, ids, context=context): res[task.id] = True if task.project_id: if task.project_id.active == False or task.project_id.state == 'template': res[task.id] = False return res def _compute_displayed_image(self, cr, uid, ids, prop, arg, context=None): res = {} for line in self.browse(cr, uid, ids, context=context): res[line.id] = line.attachment_ids and line.attachment_ids.filtered(lambda x: x.file_type_icon == 'webimage')[0] or None return res _columns = { 'active': fields.function(_is_template, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."), 'name': fields.char('Task Title', track_visibility='onchange', size=128, required=True, select=True), 'description': fields.html('Description'), 'priority': fields.selection([('0','Normal'), ('1','High')], 'Priority', select=True), 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."), 'stage_id': fields.many2one('project.task.type', 'Stage', track_visibility='onchange', select=True, domain="[('project_ids', '=', project_id)]", copy=False), 'tag_ids': fields.many2many('project.tags', string='Tags', oldname='categ_ids'), 'kanban_state': fields.selection([('normal', 'In Progress'),('done', 'Ready for next stage'),('blocked', 'Blocked')], 'Kanban State', track_visibility='onchange', help="A task's kanban state indicates special situations affecting it:\n" " * Normal is the default situation\n" " * Blocked indicates something is preventing the progress of this task\n" " * Ready for next stage indicates the task is ready to be pulled to the next stage", required=True, copy=False), 'create_date': fields.datetime('Create Date', readonly=True, select=True), 'write_date': fields.datetime('Last Modification Date', readonly=True, select=True), #not displayed in the view but it might be useful with base_action_rule module (and it needs to be defined first for that) 'date_start': fields.datetime('Starting Date', select=True, copy=False), 'date_end': fields.datetime('Ending Date', select=True, copy=False), 'date_assign': fields.datetime('Assigning Date', select=True, copy=False, readonly=True), 'date_deadline': fields.date('Deadline', select=True, copy=False), 'date_last_stage_update': fields.datetime('Last Stage Update', select=True, copy=False, readonly=True), 'project_id': fields.many2one('project.project', 'Project', ondelete='set null', select=True, track_visibility='onchange', change_default=True), 'parent_ids': fields.many2many('project.task', 'project_task_parent_rel', 'task_id', 'parent_id', 'Parent Tasks'), 'child_ids': fields.many2many('project.task', 'project_task_parent_rel', 'parent_id', 'task_id', 'Delegated Tasks'), 'notes': fields.text('Notes'), 'planned_hours': fields.float('Initially Planned Hours', help='Estimated time to do the task, usually set by the project manager when the task is in draft state.'), 'remaining_hours': fields.float('Remaining Hours', digits=(16,2), help="Total remaining time, can be re-estimated periodically by the assignee of the task."), 'user_id': fields.many2one('res.users', 'Assigned to', select=True, track_visibility='onchange'), 'partner_id': fields.many2one('res.partner', 'Customer'), 'manager_id': fields.related('project_id', 'analytic_account_id', 'user_id', type='many2one', relation='res.users', string='Project Manager'), 'company_id': fields.many2one('res.company', 'Company'), 'id': fields.integer('ID', readonly=True), 'color': fields.integer('Color Index'), 'user_email': fields.related('user_id', 'email', type='char', string='User Email', readonly=True), 'attachment_ids': fields.one2many('ir.attachment', 'res_id', domain=lambda self: [('res_model', '=', self._name)], auto_join=True, string='Attachments'), 'displayed_image_id': fields.function(_compute_displayed_image, relation='ir.attachment', type="many2one", string='Attachment'), } _defaults = { 'stage_id': _get_default_stage_id, 'project_id': lambda self, cr, uid, ctx=None: ctx.get('default_project_id') if ctx is not None else False, 'date_last_stage_update': fields.datetime.now, 'kanban_state': 'normal', 'priority': '0', 'sequence': 10, 'active': True, 'user_id': lambda obj, cr, uid, ctx=None: uid, 'company_id': lambda self, cr, uid, ctx=None: self.pool.get('res.company')._company_default_get(cr, uid, 'project.task', context=ctx), 'partner_id': lambda self, cr, uid, ctx=None: self._get_default_partner(cr, uid, context=ctx), 'date_start': fields.datetime.now, } _order = "priority desc, sequence, date_start, name, id" def _check_recursion(self, cr, uid, ids, context=None): for id in ids: visited_branch = set() visited_node = set() res = self._check_cycle(cr, uid, id, visited_branch, visited_node, context=context) if not res: return False return True def _check_cycle(self, cr, uid, id, visited_branch, visited_node, context=None): if id in visited_branch: #Cycle return False if id in visited_node: #Already tested don't work one more time for nothing return True visited_branch.add(id) visited_node.add(id) #visit child using DFS task = self.browse(cr, uid, id, context=context) for child in task.child_ids: res = self._check_cycle(cr, uid, child.id, visited_branch, visited_node, context=context) if not res: return False visited_branch.remove(id) return True def _check_dates(self, cr, uid, ids, context=None): if context == None: context = {} obj_task = self.browse(cr, uid, ids[0], context=context) start = obj_task.date_start or False end = obj_task.date_end or False if start and end : if start > end: return False return True _constraints = [ (_check_recursion, 'Error ! You cannot create recursive tasks.', ['parent_ids']), (_check_dates, 'Error ! Task starting date must be lower than its ending date.', ['date_start','date_end']) ] # Override view according to the company definition def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): users_obj = self.pool.get('res.users') if context is None: context = {} # read uom as admin to avoid access rights issues, e.g. for portal/share users, # this should be safe (no context passed to avoid side-effects) obj_tm = users_obj.browse(cr, SUPERUSER_ID, uid, context=context).company_id.project_time_mode_id tm = obj_tm and obj_tm.name or 'Hours' res = super(task, self).fields_view_get(cr, uid, view_id=view_id, view_type=view_type, context=context, toolbar=toolbar, submenu=submenu) # read uom as admin to avoid access rights issues, e.g. for portal/share users, # this should be safe (no context passed to avoid side-effects) obj_tm = users_obj.browse(cr, SUPERUSER_ID, uid, context=context).company_id.project_time_mode_id try: # using get_object to get translation value uom_hour = self.pool['ir.model.data'].get_object(cr, uid, 'product', 'product_uom_hour', context=context) except ValueError: uom_hour = False if not obj_tm or not uom_hour or obj_tm.id == uom_hour.id: return res eview = etree.fromstring(res['arch']) # if the project_time_mode_id is not in hours (so in days), display it as a float field def _check_rec(eview): if eview.attrib.get('widget','') == 'float_time': eview.set('widget','float') for child in eview: _check_rec(child) return True _check_rec(eview) res['arch'] = etree.tostring(eview) # replace reference of 'Hours' to 'Day(s)' for f in res['fields']: # TODO this NOT work in different language than english # the field 'Initially Planned Hours' should be replaced by 'Initially Planned Days' # but string 'Initially Planned Days' is not available in translation if 'Hours' in res['fields'][f]['string']: res['fields'][f]['string'] = res['fields'][f]['string'].replace('Hours', obj_tm.name) return res def get_empty_list_help(self, cr, uid, help, context=None): context = dict(context or {}) context['empty_list_help_id'] = context.get('default_project_id') context['empty_list_help_model'] = 'project.project' context['empty_list_help_document_name'] = _("tasks") return super(task, self).get_empty_list_help(cr, uid, help, context=context) # ---------------------------------------- # Case management # ---------------------------------------- def stage_find(self, cr, uid, cases, section_id, domain=[], order='sequence', context=None): """ Override of the base.stage method Parameter of the stage search taken from the lead: - section_id: if set, stages must belong to this section or be a default stage; if not set, stages must be default stages """ if isinstance(cases, (int, long)): cases = self.browse(cr, uid, cases, context=context) # collect all section_ids section_ids = [] if section_id: section_ids.append(section_id) for task in cases: if task.project_id: section_ids.append(task.project_id.id) search_domain = [] if section_ids: search_domain = [('|')] * (len(section_ids) - 1) for section_id in section_ids: search_domain.append(('project_ids', '=', section_id)) search_domain += list(domain) # perform search, return the first found stage_ids = self.pool.get('project.task.type').search(cr, uid, search_domain, order=order, context=context) if stage_ids: return stage_ids[0] return False def _check_child_task(self, cr, uid, ids, context=None): if context == None: context = {} tasks = self.browse(cr, uid, ids, context=context) for task in tasks: if task.child_ids: for child in task.child_ids: if child.stage_id and not child.stage_id.fold: raise UserError(_("Child task still open.\nPlease cancel or complete child task first.")) return True def _store_history(self, cr, uid, ids, context=None): for task in self.browse(cr, uid, ids, context=context): self.pool.get('project.task.history').create(cr, uid, { 'task_id': task.id, 'remaining_hours': task.remaining_hours, 'planned_hours': task.planned_hours, 'kanban_state': task.kanban_state, 'type_id': task.stage_id.id, 'user_id': task.user_id.id }, context=context) return True # ------------------------------------------------ # CRUD overrides # ------------------------------------------------ def create(self, cr, uid, vals, context=None): context = dict(context or {}) # for default stage if vals.get('project_id') and not context.get('default_project_id'): context['default_project_id'] = vals.get('project_id') # user_id change: update date_assign if vals.get('user_id'): vals['date_assign'] = fields.datetime.now() # context: no_log, because subtype already handle this create_context = dict(context, mail_create_nolog=True) task_id = super(task, self).create(cr, uid, vals, context=create_context) self._store_history(cr, uid, [task_id], context=context) return task_id def write(self, cr, uid, ids, vals, context=None): if isinstance(ids, (int, long)): ids = [ids] # stage change: update date_last_stage_update if 'stage_id' in vals: vals['date_last_stage_update'] = fields.datetime.now() # user_id change: update date_assign if vals.get('user_id'): vals['date_assign'] = fields.datetime.now() # Overridden to reset the kanban_state to normal whenever # the stage (stage_id) of the task changes. if vals and not 'kanban_state' in vals and 'stage_id' in vals: new_stage = vals.get('stage_id') vals_reset_kstate = dict(vals, kanban_state='normal') for t in self.browse(cr, uid, ids, context=context): write_vals = vals_reset_kstate if t.stage_id.id != new_stage else vals super(task, self).write(cr, uid, [t.id], write_vals, context=context) result = True else: result = super(task, self).write(cr, uid, ids, vals, context=context) if any(item in vals for item in ['stage_id', 'remaining_hours', 'user_id', 'kanban_state']): self._store_history(cr, uid, ids, context=context) return result def unlink(self, cr, uid, ids, context=None): if context is None: context = {} self._check_child_task(cr, uid, ids, context=context) res = super(task, self).unlink(cr, uid, ids, context) return res def _get_total_hours(self): return self.remaining_hours def _generate_task(self, cr, uid, tasks, ident=4, context=None): context = context or {} result = "" ident = ' '*ident company = self.pool["res.users"].browse(cr, uid, uid, context=context).company_id duration_uom = { 'day(s)': 'd', 'days': 'd', 'day': 'd', 'd': 'd', 'month(s)': 'm', 'months': 'm', 'month': 'month', 'm': 'm', 'week(s)': 'w', 'weeks': 'w', 'week': 'w', 'w': 'w', 'hour(s)': 'H', 'hours': 'H', 'hour': 'H', 'h': 'H', }.get(company.project_time_mode_id.name.lower(), "hour(s)") for task in tasks: if task.stage_id and task.stage_id.fold: continue result += ''' %sdef Task_%s(): %s todo = \"%.2f%s\" %s effort = \"%.2f%s\"''' % (ident, task.id, ident, task.remaining_hours, duration_uom, ident, task._get_total_hours(), duration_uom) start = [] for t2 in task.parent_ids: start.append("up.Task_%s.end" % (t2.id,)) if start: result += ''' %s start = max(%s) ''' % (ident,','.join(start)) if task.user_id: result += ''' %s resource = %s ''' % (ident, 'User_'+str(task.user_id.id)) result += "\n" return result # --------------------------------------------------- # Mail gateway # --------------------------------------------------- def _track_subtype(self, cr, uid, ids, init_values, context=None): record = self.browse(cr, uid, ids[0], context=context) if 'kanban_state' in init_values and record.kanban_state == 'blocked': return 'project.mt_task_blocked' elif 'kanban_state' in init_values and record.kanban_state == 'done': return 'project.mt_task_ready' elif 'user_id' in init_values and record.user_id: # assigned -> new return 'project.mt_task_new' elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1: # start stage -> new return 'project.mt_task_new' elif 'stage_id' in init_values: return 'project.mt_task_stage' return super(task, self)._track_subtype(cr, uid, ids, init_values, context=context) @api.cr_uid_context def message_get_reply_to(self, cr, uid, ids, default=None, context=None): """ Override to get the reply_to of the parent project. """ tasks = self.browse(cr, SUPERUSER_ID, ids, context=context) project_ids = set([task.project_id.id for task in tasks if task.project_id]) aliases = self.pool['project.project'].message_get_reply_to(cr, uid, list(project_ids), default=default, context=context) return dict((task.id, aliases.get(task.project_id and task.project_id.id or 0, False)) for task in tasks) def message_new(self, cr, uid, msg, custom_values=None, context=None): """ Override to updates the document according to the email. """ if custom_values is None: custom_values = {} defaults = { 'name': msg.get('subject'), 'planned_hours': 0.0, 'partner_id': msg.get('author_id', False) } defaults.update(custom_values) res = super(task, self).message_new(cr, uid, msg, custom_values=defaults, context=context) email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or '')) new_task = self.browse(cr, uid, res, context=context) if new_task.project_id and new_task.project_id.alias_name: # check left-part is not already an alias email_list = filter(lambda x: x.split('@')[0] != new_task.project_id.alias_name, email_list) partner_ids = filter(lambda x: x, self._find_partner_from_emails(cr, uid, email_list, check_followers=False)) self.message_subscribe(cr, uid, [res], partner_ids, context=context) return res def message_update(self, cr, uid, ids, msg, update_vals=None, context=None): """ Override to update the task according to the email. """ if update_vals is None: update_vals = {} maps = { 'cost': 'planned_hours', } for line in msg['body'].split('\n'): line = line.strip() res = tools.command_re.match(line) if res: match = res.group(1).lower() field = maps.get(match) if field: try: update_vals[field] = float(res.group(2).lower()) except (ValueError, TypeError): pass return super(task, self).message_update(cr, uid, ids, msg, update_vals=update_vals, context=context) def message_get_suggested_recipients(self, cr, uid, ids, context=None): recipients = super(task, self).message_get_suggested_recipients(cr, uid, ids, context=context) for data in self.browse(cr, uid, ids, context=context): if data.partner_id: reason = _('Customer Email') if data.partner_id.email else _('Customer') data._message_add_suggested_recipient(recipients, partner=data.partner_id, reason=reason) return recipients
class SaleOrder(orm.Model): """Adds condition to SO""" _inherit = "sale.order" _description = 'Sale Order' _columns = { 'text_condition1': fields.many2one('sale.condition_text', 'Header', domain=[('type', '=', 'header')]), 'text_condition2': fields.many2one('sale.condition_text', 'Footer', domain=[('type', '=', 'footer')]), 'note1': fields.html('Header'), 'note2': fields.html('Footer') } def _set_condition(self, cursor, uid, inv_id, commentid, key, partner_id=False): """Set the text of the notes in invoices""" if not commentid: return {} if not partner_id: raise osv.except_osv( _('No Customer Defined !'), _('Before choosing condition text select a customer.')) lang = self.pool.get('res.partner').browse(cursor, uid, partner_id).lang or 'en_US' cond = self.pool.get('sale.condition_text').browse( cursor, uid, commentid, {'lang': lang}) return {'value': {key: cond.text}} def set_header(self, cursor, uid, inv_id, commentid, partner_id=False): return self._set_condition(cursor, uid, inv_id, commentid, 'note1', partner_id) def set_footer(self, cursor, uid, inv_id, commentid, partner_id=False): return self._set_condition(cursor, uid, inv_id, commentid, 'note2', partner_id) def print_quotation(self, cursor, uid, ids, context=None): ''' This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow ''' assert len( ids ) == 1, 'This option should only be used for a single id at a time' wf_service = netsvc.LocalService("workflow") wf_service.trg_validate(uid, 'sale.order', ids[0], 'quotation_sent', cursor) datas = { 'model': 'sale.order', 'ids': ids, 'form': self.read(cursor, uid, ids[0], context=context), } return { 'type': 'ir.actions.report.xml', 'report_name': 'sale.order.webkit', 'datas': datas, 'nodestroy': True }
class email_template(osv.osv): "Templates for sending email" _name = "email.template" _description = 'Email Templates' _order = 'name' def default_get(self, cr, uid, fields, context=None): res = super(email_template, self).default_get(cr, uid, fields, context) if res.get('model'): res['model_id'] = self.pool['ir.model'].search(cr, uid, [('model', '=', res.pop('model'))], context=context)[0] return res def _replace_local_links(self, cr, uid, html, context=None): """ Post-processing of html content to replace local links to absolute links, using web.base.url as base url. """ if not html: return html # form a tree root = lxml.html.fromstring(html) if not len(root) and root.text is None and root.tail is None: html = '<div>%s</div>' % html root = lxml.html.fromstring(html) base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url') (base_scheme, base_netloc, bpath, bparams, bquery, bfragment) = urlparse.urlparse(base_url) def _process_link(url): new_url = url (scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url) if not scheme and not netloc: new_url = urlparse.urlunparse((base_scheme, base_netloc, path, params, query, fragment)) return new_url # check all nodes, replace : # - img src -> check URL # - a href -> check URL for node in root.iter(): if node.tag == 'a' and node.get('href'): node.set('href', _process_link(node.get('href'))) elif node.tag == 'img' and not node.get('src', 'data').startswith('data'): node.set('src', _process_link(node.get('src'))) 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 def render_post_process(self, cr, uid, html, context=None): html = self._replace_local_links(cr, uid, html, context=context) return html def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False): """Render the given template text, replace mako expressions ``${expr}`` with the result of evaluating these expressions with an evaluation context containing: * ``user``: browse_record of the current user * ``object``: browse_record of the document record this mail is related to * ``context``: the context passed to the mail composition wizard :param str template: the template text to render :param str model: model name of the document record this mail is related to. :param int res_ids: list of ids of document records those mails are related to. """ if context is None: context = {} res_ids = filter(None, res_ids) # to avoid browsing [None] below results = dict.fromkeys(res_ids, u"") # try to load the template try: template = mako_template_env.from_string(tools.ustr(template)) except Exception: _logger.exception("Failed to load template %r", template) return results # prepare template variables user = self.pool.get('res.users').browse(cr, uid, uid, context=context) records = self.pool[model].browse(cr, uid, res_ids, context=context) or [None] variables = { 'format_tz': lambda dt, tz=False, format=False, context=context: format_tz(self.pool, cr, uid, dt, tz, format, context), 'user': user, 'ctx': context, # context kw would clash with mako internals } for record in records: res_id = record.id if record else None variables['object'] = record try: render_result = template.render(variables) except Exception: _logger.exception("Failed to render template %r using values %r" % (template, variables)) render_result = u"" if render_result == u"False": render_result = u"" results[res_id] = render_result if post_process: for res_id, result in results.iteritems(): results[res_id] = self.render_post_process(cr, uid, result, context=context) return results def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None): if context is None: context = {} if res_ids is None: res_ids = [None] results = dict.fromkeys(res_ids, False) if not template_id: return results template = self.browse(cr, uid, template_id, context) langs = self.render_template_batch(cr, uid, template.lang, template.model, res_ids, context) for res_id, lang in langs.iteritems(): if lang: # Use translated template if necessary ctx = context.copy() ctx['lang'] = lang template = self.browse(cr, uid, template.id, ctx) else: template = self.browse(cr, uid, int(template_id), context) results[res_id] = template return results def onchange_model_id(self, cr, uid, ids, model_id, context=None): mod_name = False if model_id: mod_name = self.pool.get('ir.model').browse(cr, uid, model_id, context).model return {'value': {'model': mod_name}} _columns = { 'name': fields.char('Name'), 'model_id': fields.many2one('ir.model', 'Applies to', help="The kind of document with with this template can be used"), 'model': fields.related('model_id', 'model', type='char', string='Related Document Model', select=True, store=True, readonly=True), 'lang': fields.char('Language', help="Optional translation language (ISO code) to select when sending out an email. " "If not set, the english version will be used. " "This should usually be a placeholder expression " "that provides the appropriate language, e.g. " "${object.partner_id.lang}.", placeholder="${object.partner_id.lang}"), 'user_signature': fields.boolean('Add Signature', help="If checked, the user's signature will be appended to the text version " "of the message"), 'subject': fields.char('Subject', translate=True, help="Subject (placeholders may be used here)",), 'email_from': fields.char('From', help="Sender address (placeholders may be used here). If not set, the default " "value will be the author's email alias if configured, or email address."), 'use_default_to': fields.boolean( 'Default recipients', help="Default recipients of the record:\n" "- partner (using id on a partner or the partner_id field) OR\n" "- email (using email_from or email field)"), 'email_to': fields.char('To (Emails)', help="Comma-separated recipient addresses (placeholders may be used here)"), 'partner_to': fields.char('To (Partners)', help="Comma-separated ids of recipient partners (placeholders may be used here)", oldname='email_recipients'), 'email_cc': fields.char('Cc', help="Carbon copy recipients (placeholders may be used here)"), 'reply_to': fields.char('Reply-To', help="Preferred response address (placeholders may be used here)"), 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False, help="Optional preferred server for outgoing mails. If not set, the highest " "priority one will be used."), 'body_html': fields.html('Body', translate=True, sanitize=False, help="Rich-text/HTML version of the message (placeholders may be used here)"), 'report_name': fields.char('Report Filename', translate=True, help="Name to use for the generated report file (may contain placeholders)\n" "The extension can be omitted and will then come from the report type."), 'report_template': fields.many2one('ir.actions.report.xml', 'Optional report to print and attach'), 'ref_ir_act_window': fields.many2one('ir.actions.act_window', 'Sidebar action', readonly=True, copy=False, help="Sidebar action to make this template available on records " "of the related document model"), 'ref_ir_value': fields.many2one('ir.values', 'Sidebar Button', readonly=True, copy=False, help="Sidebar button to open the sidebar action"), 'attachment_ids': fields.many2many('ir.attachment', 'email_template_attachment_rel', 'email_template_id', 'attachment_id', 'Attachments', help="You may attach files to this template, to be added to all " "emails created from this template"), 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete this email after sending it, to save space"), # Fake fields used to implement the placeholder assistant 'model_object_field': fields.many2one('ir.model.fields', string="Field", help="Select target field from the related document model.\n" "If it is a relationship field you will be able to select " "a target field at the destination of the relationship."), 'sub_object': fields.many2one('ir.model', 'Sub-model', readonly=True, help="When a relationship field is selected as first field, " "this field shows the document model the relationship goes to."), 'sub_model_object_field': fields.many2one('ir.model.fields', 'Sub-field', help="When a relationship field is selected as first field, " "this field lets you select the target field within the " "destination document model (sub-model)."), 'null_value': fields.char('Default Value', help="Optional value to use if the target field is empty"), 'copyvalue': fields.char('Placeholder Expression', help="Final placeholder expression, to be copy-pasted in the desired template field."), } _defaults = { 'auto_delete': True, } def create_action(self, cr, uid, ids, context=None): action_obj = self.pool.get('ir.actions.act_window') data_obj = self.pool.get('ir.model.data') for template in self.browse(cr, uid, ids, context=context): src_obj = template.model_id.model model_data_id = data_obj._get_id(cr, uid, 'mail', 'email_compose_message_wizard_form') res_id = data_obj.browse(cr, uid, model_data_id, context=context).res_id button_name = _('Send Mail (%s)') % template.name act_id = action_obj.create(cr, SUPERUSER_ID, { 'name': button_name, 'type': 'ir.actions.act_window', 'res_model': 'mail.compose.message', 'src_model': src_obj, 'view_type': 'form', 'context': "{'default_composition_mode': 'mass_mail', 'default_template_id' : %d, 'default_use_template': True}" % (template.id), 'view_mode':'form,tree', 'view_id': res_id, 'target': 'new', 'auto_refresh':1 }, context) ir_values_id = self.pool.get('ir.values').create(cr, SUPERUSER_ID, { 'name': button_name, 'model': src_obj, 'key2': 'client_action_multi', 'value': "ir.actions.act_window,%s" % act_id, 'object': True, }, context) template.write({ 'ref_ir_act_window': act_id, 'ref_ir_value': ir_values_id, }) return True def unlink_action(self, cr, uid, ids, context=None): for template in self.browse(cr, uid, ids, context=context): try: if template.ref_ir_act_window: self.pool.get('ir.actions.act_window').unlink(cr, SUPERUSER_ID, template.ref_ir_act_window.id, context) if template.ref_ir_value: ir_values_obj = self.pool.get('ir.values') ir_values_obj.unlink(cr, SUPERUSER_ID, template.ref_ir_value.id, context) except Exception: raise osv.except_osv(_("Warning"), _("Deletion of the action record failed.")) return True def unlink(self, cr, uid, ids, context=None): self.unlink_action(cr, uid, ids, context=context) return super(email_template, self).unlink(cr, uid, ids, context=context) def copy(self, cr, uid, id, default=None, context=None): template = self.browse(cr, uid, id, context=context) default = dict(default or {}, name=_("%s (copy)") % template.name) return super(email_template, self).copy(cr, uid, id, default, context) def build_expression(self, field_name, sub_field_name, null_value): """Returns a placeholder expression for use in a template field, based on the values provided in the placeholder assistant. :param field_name: main field name :param sub_field_name: sub field name (M2O) :param null_value: default value if the target value is empty :return: final placeholder expression """ expression = '' if field_name: expression = "${object." + field_name if sub_field_name: expression += "." + sub_field_name if null_value: expression += " or '''%s'''" % null_value expression += "}" return expression def onchange_sub_model_object_value_field(self, cr, uid, ids, model_object_field, sub_model_object_field=False, null_value=None, context=None): result = { 'sub_object': False, 'copyvalue': False, 'sub_model_object_field': False, 'null_value': False } if model_object_field: fields_obj = self.pool.get('ir.model.fields') field_value = fields_obj.browse(cr, uid, model_object_field, context) if field_value.ttype in ['many2one', 'one2many', 'many2many']: res_ids = self.pool.get('ir.model').search(cr, uid, [('model', '=', field_value.relation)], context=context) sub_field_value = False if sub_model_object_field: sub_field_value = fields_obj.browse(cr, uid, sub_model_object_field, context) if res_ids: result.update({ 'sub_object': res_ids[0], 'copyvalue': self.build_expression(field_value.name, sub_field_value and sub_field_value.name or False, null_value or False), 'sub_model_object_field': sub_model_object_field or False, 'null_value': null_value or False }) else: result.update({ 'copyvalue': self.build_expression(field_value.name, False, null_value or False), 'null_value': null_value or False }) return {'value': result} def generate_recipients_batch(self, cr, uid, results, template_id, res_ids, context=None): """Generates the recipients of the template. Default values can ben generated instead of the template values if requested by template or context. Emails (email_to, email_cc) can be transformed into partners if requested in the context. """ if context is None: context = {} template = self.browse(cr, uid, template_id, context=context) if template.use_default_to or context.get('tpl_force_default_to'): ctx = dict(context, thread_model=template.model) default_recipients = self.pool['mail.thread'].message_get_default_recipients(cr, uid, res_ids, context=ctx) for res_id, recipients in default_recipients.iteritems(): results[res_id].pop('partner_to', None) results[res_id].update(recipients) for res_id, values in results.iteritems(): partner_ids = values.get('partner_ids', list()) if context and context.get('tpl_partners_only'): mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', '')) for mail in mails: partner_id = self.pool.get('res.partner').find_or_create(cr, uid, mail, context=context) partner_ids.append(partner_id) partner_to = values.pop('partner_to', '') if partner_to: # placeholders could generate '', 3, 2 due to some empty field values tpl_partner_ids = [int(pid) for pid in partner_to.split(',') if pid] partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context) results[res_id]['partner_ids'] = partner_ids return results def generate_email_batch(self, cr, uid, template_id, res_ids, context=None, fields=None): """Generates an email from the template for given the given model based on records given by res_ids. :param template_id: id of the template to render. :param res_id: id of the record to use for rendering the template (model is taken from template definition) :returns: a dict containing all relevant fields for creating a new mail.mail entry, with one extra key ``attachments``, in the format [(report_name, data)] where data is base64 encoded. """ if context is None: context = {} if fields is None: fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to'] report_xml_pool = self.pool.get('ir.actions.report.xml') res_ids_to_templates = self.get_email_template_batch(cr, uid, template_id, res_ids, context) # templates: res_id -> template; template -> res_ids templates_to_res_ids = {} for res_id, template in res_ids_to_templates.iteritems(): templates_to_res_ids.setdefault(template, []).append(res_id) results = dict() for template, template_res_ids in templates_to_res_ids.iteritems(): # generate fields value for all res_ids linked to the current template ctx = context.copy() if template.lang: ctx['lang'] = template._context.get('lang') for field in fields: generated_field_values = self.render_template_batch( cr, uid, getattr(template, field), template.model, template_res_ids, post_process=(field == 'body_html'), context=ctx) for res_id, field_value in generated_field_values.iteritems(): results.setdefault(res_id, dict())[field] = field_value # compute recipients results = self.generate_recipients_batch(cr, uid, results, template.id, template_res_ids, context=context) # update values for all res_ids for res_id in template_res_ids: values = results[res_id] # body: add user signature, sanitize if 'body_html' in fields and template.user_signature: signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature if signature: values['body_html'] = tools.append_content_to_html(values['body_html'], signature, plaintext=False) if values.get('body_html'): values['body'] = tools.html_sanitize(values['body_html']) # technical settings values.update( mail_server_id=template.mail_server_id.id or False, auto_delete=template.auto_delete, model=template.model, res_id=res_id or False, attachment_ids=[attach.id for attach in template.attachment_ids], ) # Add report in attachments: generate once for all template_res_ids if template.report_template: # Fix : Force report to use res ids and not active_ids if ctx and 'active_ids' in ctx: del ctx['active_ids'] for res_id in template_res_ids: attachments = [] report_name = self.render_template(cr, uid, template.report_name, template.model, res_id, context=ctx) report = report_xml_pool.browse(cr, uid, template.report_template.id, context) report_service = report.report_name if report.report_type in ['qweb-html', 'qweb-pdf']: result, format = self.pool['report'].get_pdf(cr, uid, [res_id], report_service, context=ctx), 'pdf' else: result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx) # TODO in trunk, change return format to binary to match message_post expected format result = base64.b64encode(result) if not report_name: report_name = 'report.' + report_service ext = "." + format if not report_name.endswith(ext): report_name += ext attachments.append((report_name, result)) results[res_id]['attachments'] = attachments return results @api.cr_uid_id_context def send_mail(self, cr, uid, template_id, res_id, force_send=False, raise_exception=False, context=None): """Generates a new mail message for the given template and record, and schedules it for delivery through the ``mail`` module's scheduler. :param int template_id: id of the template to render :param int res_id: id of the record to render the template with (model is taken from the template) :param bool force_send: if True, the generated mail.message is immediately sent after being created, as if the scheduler was executed for this message only. :returns: id of the mail.message that was created """ if context is None: context = {} mail_mail = self.pool.get('mail.mail') ir_attachment = self.pool.get('ir.attachment') # create a mail_mail based on values, without attachments values = self.generate_email(cr, uid, template_id, res_id, context=context) if not values.get('email_from'): raise osv.except_osv(_('Warning!'), _("Sender email is missing or empty after template rendering. Specify one to deliver your message")) values['recipient_ids'] = [(4, pid) for pid in values.get('partner_ids', list())] attachment_ids = values.pop('attachment_ids', []) attachments = values.pop('attachments', []) msg_id = mail_mail.create(cr, uid, values, context=context) mail = mail_mail.browse(cr, uid, msg_id, context=context) # manage attachments for attachment in attachments: attachment_data = { 'name': attachment[0], 'datas_fname': attachment[0], 'datas': attachment[1], 'res_model': 'mail.message', 'res_id': mail.mail_message_id.id, } context = dict(context) context.pop('default_type', None) attachment_ids.append(ir_attachment.create(cr, uid, attachment_data, context=context)) if attachment_ids: values['attachment_ids'] = [(6, 0, attachment_ids)] mail_mail.write(cr, uid, msg_id, {'attachment_ids': [(6, 0, attachment_ids)]}, context=context) if force_send: mail_mail.send(cr, uid, [msg_id], raise_exception=raise_exception, context=context) return msg_id # Compatibility method def render_template(self, cr, uid, template, model, res_id, context=None): return self.render_template_batch(cr, uid, template, model, [res_id], context)[res_id] def get_email_template(self, cr, uid, template_id=False, record_id=None, context=None): return self.get_email_template_batch(cr, uid, template_id, [record_id], context)[record_id] def generate_email(self, cr, uid, template_id, res_id, context=None): return self.generate_email_batch(cr, uid, template_id, [res_id], context=context)[res_id]
class PaymentTxOgone(osv.Model): _inherit = 'payment.transaction' # ogone status _ogone_valid_tx_status = [5, 9] _ogone_wait_tx_status = [41, 50, 51, 52, 55, 56, 91, 92, 99] _ogone_pending_tx_status = [46] # 3DS HTML response _ogone_cancel_tx_status = [1] _columns = { 'ogone_3ds': fields.boolean('3DS Activated'), 'ogone_3ds_html': fields.html('3DS HTML'), 'ogone_complus': fields.char('Complus'), 'ogone_payid': fields.char('PayID', help='Payment ID, generated by Ogone') } # -------------------------------------------------- # FORM RELATED METHODS # -------------------------------------------------- def _ogone_form_get_tx_from_data(self, cr, uid, data, context=None): """ Given a data dict coming from ogone, verify it and find the related transaction record. """ reference, pay_id, shasign = data.get('orderID'), data.get( 'PAYID'), data.get('SHASIGN') if not reference or not pay_id or not shasign: error_msg = 'Ogone: received data with missing reference (%s) or pay_id (%s) or shashign (%s)' % ( reference, pay_id, shasign) _logger.error(error_msg) raise ValidationError(error_msg) # find tx -> @TDENOTE use paytid ? tx_ids = self.search(cr, uid, [('reference', '=', reference)], context=context) if not tx_ids or len(tx_ids) > 1: error_msg = 'Ogone: received data for reference %s' % (reference) if not tx_ids: error_msg += '; no order found' else: error_msg += '; multiple order found' _logger.error(error_msg) raise ValidationError(error_msg) tx = self.pool['payment.transaction'].browse(cr, uid, tx_ids[0], context=context) # verify shasign shasign_check = self.pool['payment.acquirer']._ogone_generate_shasign( tx.acquirer_id, 'out', data) if shasign_check.upper() != shasign.upper(): error_msg = 'Ogone: invalid shasign, received %s, computed %s, for data %s' % ( shasign, shasign_check, data) _logger.error(error_msg) raise ValidationError(error_msg) return tx def _ogone_form_get_invalid_parameters(self, cr, uid, tx, data, context=None): invalid_parameters = [] # TODO: txn_id: should be false at draft, set afterwards, and verified with txn details if tx.acquirer_reference and data.get( 'PAYID') != tx.acquirer_reference: invalid_parameters.append( ('PAYID', data.get('PAYID'), tx.acquirer_reference)) # check what is bought if float_compare(float(data.get('amount', '0.0')), tx.amount, 2) != 0: invalid_parameters.append( ('amount', data.get('amount'), '%.2f' % tx.amount)) if data.get('currency') != tx.currency_id.name: invalid_parameters.append( ('currency', data.get('currency'), tx.currency_id.name)) return invalid_parameters def _ogone_form_validate(self, cr, uid, tx, data, context=None): if tx.state == 'done': _logger.warning( 'Ogone: trying to validate an already validated tx (ref %s)' % tx.reference) return True status = int(data.get('STATUS', '0')) if status in self._ogone_valid_tx_status: tx.write({ 'state': 'done', 'date_validate': datetime.strptime( data['TRXDATE'], '%m/%d/%y').strftime(DEFAULT_SERVER_DATE_FORMAT), 'acquirer_reference': data['PAYID'], }) return True elif status in self._ogone_cancel_tx_status: tx.write({ 'state': 'cancel', 'acquirer_reference': data.get('PAYID'), }) elif status in self._ogone_pending_tx_status or status in self._ogone_wait_tx_status: tx.write({ 'state': 'pending', 'acquirer_reference': data.get('PAYID'), }) else: error = 'Ogone: feedback error: %(error_str)s\n\n%(error_code)s: %(error_msg)s' % { 'error_str': data.get('NCERRORPLUS'), 'error_code': data.get('NCERROR'), 'error_msg': ogone.OGONE_ERROR_MAP.get(data.get('NCERROR')), } _logger.info(error) tx.write({ 'state': 'error', 'state_message': error, 'acquirer_reference': data.get('PAYID'), }) return False # -------------------------------------------------- # S2S RELATED METHODS # -------------------------------------------------- def ogone_s2s_create_alias(self, cr, uid, id, values, context=None): """ Create an alias at Ogone via batch. .. versionadded:: pre-v8 saas-3 .. warning:: Experimental code. You should not use it before OpenERP v8 official release. """ tx = self.browse(cr, uid, id, context=context) assert tx.type == 'server2server', 'Calling s2s dedicated method for a %s acquirer' % tx.type alias = 'OPENERP-%d-%d' % (tx.partner_id.id, tx.id) expiry_date = '%s%s' % (values['expiry_date_mm'], values['expiry_date_yy'][2:]) line = 'ADDALIAS;%(alias)s;%(holder_name)s;%(number)s;%(expiry_date)s;%(brand)s;%(pspid)s' line = line % dict(values, alias=alias, expiry_date=expiry_date, pspid=tx.acquirer_id.ogone_pspid) tx_data = { 'FILE_REFERENCE': 'OPENERP-NEW-ALIAS-%s' % time.time(), # something unique, 'TRANSACTION_CODE': 'ATR', 'OPERATION': 'SAL', 'NB_PAYMENTS': 1, # even if we do not actually have any payment, ogone want it to not be 0 'FILE': line, 'REPLY_TYPE': 'XML', 'PSPID': tx.acquirer_id.ogone_pspid, 'USERID': tx.acquirer_id.ogone_userid, 'PSWD': tx.acquirer_id.ogone_password, 'PROCESS_MODE': 'CHECKANDPROCESS', } # TODO: fix URL computation request = urllib2.Request(tx.acquirer_id.ogone_afu_agree_url, urlencode(tx_data)) result = urllib2.urlopen(request).read() try: tree = objectify.fromstring(result) except etree.XMLSyntaxError: _logger.exception('Invalid xml response from ogone') return None error_code = error_str = None if hasattr(tree, 'PARAMS_ERROR'): error_code = tree.NCERROR.text error_str = 'PARAMS ERROR: %s' % (tree.PARAMS_ERROR.text or '', ) else: node = tree.FORMAT_CHECK error_node = getattr(node, 'FORMAT_CHECK_ERROR', None) if error_node is not None: error_code = error_node.NCERROR.text error_str = 'CHECK ERROR: %s' % (error_node.ERROR.text or '', ) if error_code: error_msg = ogone.OGONE_ERROR_MAP.get(error_code) error = '%s\n\n%s: %s' % (error_str, error_code, error_msg) _logger.error(error) raise Exception(error) # TODO specific exception tx.write({'partner_reference': alias}) return True def ogone_s2s_generate_values(self, cr, uid, id, custom_values, context=None): """ Generate valid Ogone values for a s2s tx. .. versionadded:: pre-v8 saas-3 .. warning:: Experimental code. You should not use it before OpenERP v8 official release. """ tx = self.browse(cr, uid, id, context=context) tx_data = { 'PSPID': tx.acquirer_id.ogone_pspid, 'USERID': tx.acquirer_id.ogone_userid, 'PSWD': tx.acquirer_id.ogone_password, 'OrderID': tx.reference, 'amount': '%d' % int(float_round(tx.amount, 2) * 100), # tde check amount or str * 100 ? 'CURRENCY': tx.currency_id.name, 'LANGUAGE': tx.partner_lang, 'OPERATION': 'SAL', 'ECI': 2, # Recurring (from MOTO) 'ALIAS': tx.partner_reference, 'RTIMEOUT': 30, } if custom_values.get('ogone_cvc'): tx_data['CVC'] = custom_values.get('ogone_cvc') if custom_values.pop('ogone_3ds', None): tx_data.update({ 'FLAG3D': 'Y', # YEAH!! }) if custom_values.get('ogone_complus'): tx_data['COMPLUS'] = custom_values.get('ogone_complus') if custom_values.get('ogone_accept_url'): pass shasign = self.pool['payment.acquirer']._ogone_generate_shasign( tx.acquirer_id, 'in', tx_data) tx_data['SHASIGN'] = shasign return tx_data def ogone_s2s_feedback(self, cr, uid, data, context=None): """ .. versionadded:: pre-v8 saas-3 .. warning:: Experimental code. You should not use it before OpenERP v8 official release. """ pass def ogone_s2s_execute(self, cr, uid, id, values, context=None): """ .. versionadded:: pre-v8 saas-3 .. warning:: Experimental code. You should not use it before OpenERP v8 official release. """ tx = self.browse(cr, uid, id, context=context) tx_data = self.ogone_s2s_generate_values(cr, uid, id, values, context=context) _logger.debug('Generated Ogone s2s data %s', pformat(tx_data)) request = urllib2.Request(tx.acquirer_id.ogone_direct_order_url, urlencode(tx_data)) result = urllib2.urlopen(request).read() _logger.debug('Contacted Ogone direct order; result %s', result) tree = objectify.fromstring(result) payid = tree.get('PAYID') query_direct_data = dict( PSPID=tx.acquirer_id.ogone_pspid, USERID=tx.acquirer_id.ogone_userid, PSWD=tx.acquirer_id.ogone_password, ID=payid, ) query_direct_url = 'https://secure.ogone.com/ncol/%s/querydirect.asp' % ( tx.acquirer_id.environment, ) tries = 2 tx_done = False tx_status = False while not tx_done or tries > 0: try: tree = objectify.fromstring(result) except etree.XMLSyntaxError: # invalid response from ogone _logger.exception('Invalid xml response from ogone') raise # see https://secure.ogone.com/ncol/paymentinfos1.asp VALID_TX = [5, 9] WAIT_TX = [41, 50, 51, 52, 55, 56, 91, 92, 99] PENDING_TX = [46] # 3DS HTML response # other status are errors... status = tree.get('STATUS') if status == '': status = None else: status = int(status) if status in VALID_TX: tx_status = True tx_done = True elif status in PENDING_TX: html = str(tree.HTML_ANSWER) tx_data.update(ogone_3ds_html=html.decode('base64')) tx_status = False tx_done = True elif status in WAIT_TX: time.sleep(1500) request = urllib2.Request(query_direct_url, urlencode(query_direct_data)) result = urllib2.urlopen(request).read() _logger.debug('Contacted Ogone query direct; result %s', result) else: error_code = tree.get('NCERROR') if not ogone.retryable(error_code): error_str = tree.get('NCERRORPLUS') error_msg = ogone.OGONE_ERROR_MAP.get(error_code) error = 'ERROR: %s\n\n%s: %s' % (error_str, error_code, error_msg) _logger.info(error) raise Exception(error) tries = tries - 1 if not tx_done and tries == 0: raise Exception('Cannot get transaction status...') return tx_status
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 WebNote(orm.Model): _name = 'web.note' def name_get(self, cr, uid, ids, context=None): if context is None: context = {} if isinstance(ids, (int, long)): ids = [ids] res = [] for record in self.browse(cr, uid, ids, context=context): name = remove_extra_spaces(remove_html_tags(record.message or '')) res.append((record.id, name)) return res 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) def type_selection(self, cr, uid, context=None): return (('private', 'private'), ('internal', 'internal'), ('external', 'external')) def onchange_container_id(self, cr, uid, ids, container_id=False, message=None, context=None): result = {} if container_id: container = self.pool.get('web.note.container').\ browse(cr, uid, container_id, context=context) result['value'] = { 'sequence': container.sequence or self._defaults.get('sequence', 0), } if container.pattern: result['value']['message'] = container.pattern return result _order = 'sequence,create_date' _columns = { 'type': fields.selection(_type_selection, 'Note type', required=True), 'message': fields.html('Message'), 'display_name': fields.function(_name_get_fnc, type="char", string='Note', store=False), 'container_id': fields.many2one('web.note.container', 'Note-Container', help='Containers include ' 'templates for order and heading.'), 'sequence': fields.integer('Order in report'), 'create_uid': fields.many2one('res.users', 'User', readonly=True), } _defaults = { 'sequence': 10, }
class sale_order_option(osv.osv): _name = "sale.order.option" _description = "Sale Options" _order = 'sequence, id' _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)]), 'layout_category_id': fields.many2one('sale.layout_category', string='Section'), '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')), 'sequence': fields.integer( 'Sequence', help= "Gives the sequence order when displaying a list of suggested product." ), } _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 = self.uom_id or product.uom_id pricelist = self.order_id.pricelist_id if pricelist and product: partner_id = self.order_id.partner_id.id self.price_unit = pricelist.with_context( uom=self.uom_id.id).price_get(product.id, self.quantity, partner_id)[pricelist.id] domain = { 'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } return {'domain': domain} @api.multi def button_add_to_order(self): order = self.order_id if order.state not in ['draft', 'sent']: return False option = self if option.product_id in [line.product_id for line in order.order_line]: line = [ line for line in order.order_line if line.product_id == option.product_id ][0] vals = { 'product_uom_qty': line.product_uom_qty + 1, } line.write(vals) else: vals = { 'price_unit': option.price_unit, 'website_description': option.website_description, 'name': option.name, 'order_id': order.id, 'product_id': option.product_id.id, 'layout_category_id': option.layout_category_id.id, 'product_uom_qty': option.quantity, 'product_uom': option.uom_id.id, 'discount': option.discount, } line = self.env['sale.order.line'].create(vals) self.env['sale.order.line']._compute_tax_id() self.env['sale.order.option'].write({'line_id': line}) return {'type': 'ir.actions.client', 'tag': 'reload'}
class employee_requisition(osv.Model): _name = 'employee.requisition' _description = 'Employee Requisition' _inherit = ['mail.thread', 'ir.needaction_mixin'] _track = { 'state': { 'hr_holidays.mt_holidays_approved': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'validate', 'hr_holidays.mt_holidays_refused': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'refuse', 'hr_holidays.mt_holidays_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'confirm', }, } def _employee_get(self, cr, uid, context=None): ids = self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context) if ids: # print "My ID.........",ids print self.pool.get('hr.employee').browse(cr, uid, ids[0]).parent_id.name print self.pool.get('hr.employee').browse( cr, uid, ids[0]).parent_id.user_id.id return ids[0] return False def _deaprtment_get(self, cr, uid, context=None): ids = self.pool.get('hr.employee').search(cr, uid, [('user_id', '=', uid)], context=context) if ids: department_id = self.pool.get('hr.employee').browse( cr, uid, ids).department_id return department_id.id return False def create(self, cr, uid, vals, context=None): vals['state'] = 'confirm' if 'created_by' in vals and vals['created_by']: emp_id = self.pool.get('hr.employee').browse( cr, uid, vals['created_by']) if emp_id.parent_id and emp_id.parent_id.user_id.id: vals['approve_user_id'] = emp_id.parent_id.user_id.id vals['user_id'] = emp_id.user_id and emp_id.user_id.id or False res = super(employee_requisition, self).create(cr, uid, vals, context=None) return res def set_recruit(self, cr, uid, ids, context=None): res_list = [] for job in self.browse(cr, uid, ids, context=context).job_line: res_list.append({'job_id': job.job_id.id, 'vacancy': job.vacancy}) job_ids = self.pool.get('hr.job').search(cr, uid, []) for each in self.pool.get('hr.job').browse(cr, uid, job_ids): if each.no_of_recruitment > 0: res_list.append({ 'job_id': each.id, 'vacancy': each.no_of_recruitment }) for key, group in itertools.groupby(sorted(res_list), lambda item: item["job_id"]): no_of_recruitment = sum([item["vacancy"] for item in group]) self.pool.get('hr.job').write( cr, uid, [key], { 'state': 'recruit', 'no_of_recruitment': no_of_recruitment }, context=context) res = self.write(cr, uid, ids, {'state': 'recruit'}, context=context) return res def set_recruit_close(self, cr, uid, ids, context=None): for job in self.browse(cr, uid, ids, context=context).job_line: no_of_recruitment = job.vacancy == 0 and 1 or job.vacancy self.pool.get('hr.job').write(cr, uid, [job.job_id.id], { 'state': 'open', 'no_of_recruitment': 0 }, context=context) res = self.write(cr, uid, ids, {'state': 'open'}, context=context) return res _columns = { 'name': fields.char('Ref Number'), 'department_id': fields.many2one('hr.department', 'Department Name'), 'address_id': fields.many2many('res.partner', 'res_partner_requision_rel', 'requision_id', 'requision_address_id', 'Location'), 'job_line': fields.one2many('requision.job.line', 'requision_id', 'Position'), 'date_request': fields.date('Date Request'), 'created_by': fields.many2one('hr.employee', 'Created By'), 'reporting_authority': fields.many2one('hr.employee', 'Reporting Authority'), 'job_description': fields.html('Job Description'), 'primary_responsibilities': fields.html('Responsibilities'), 'preferred_indust': fields.char('Preferred Industry'), 'type_ids': fields.many2many('hr.recruitment.degree', 'education_group_rel', 'requision_id', 'education_master_id', 'Education Qualification'), 'state': fields.selection( [('draft', 'To Submit'), ('confirm', 'To Approve'), ('refuse', 'Refused'), ('validate1', 'Approval'), ('validate', 'Approved'), ('recruit', 'Recruitment in Progress'), ('open', 'Recruitment Closed')], 'Status', track_visibility='onchange', help= 'The status is set to \'To Submit\', when a holiday request is created.\ \nThe status is \'To Approve\', when holiday request is confirmed by user.\ \nThe status is \'Refused\', when holiday request is refused by manager.\ \nThe status is \'Approved\', when holiday request is approved by manager.' ), 'user_id': fields.many2one('res.users', 'User'), 'approve_user_id': fields.many2one('res.users', 'Approve User') } _defaults = { 'state': 'draft', 'department_id': _deaprtment_get, 'date_request': fields.date.context_today, 'user_id': lambda obj, cr, uid, context: uid, 'created_by': _employee_get, } def approve_requision_req(self, cr, uid, ids, context=None): res = self.write(cr, uid, ids, {'state': 'validate'}, context=None) return res def refuse_requision_req(self, cr, uid, ids, context=None): res = self.write(cr, uid, ids, {'state': 'refuse'}, context=None) return res def reset_to_approve(self, cr, uid, ids, context=None): res = self.write(cr, uid, ids, {'state': 'confirm'}, context=None) return res
class ProtocolloMailPecWizard(osv.TransientModel): """ A wizard to manage the creation of document protocollo from mail or pec message """ _name = 'protocollo.mailpec.wizard' _description = 'Create Protocollo From Mail or PEC' _rec_name = 'subject' def _get_doc_principale_option(self, cr, uid, context=None): options = [] attach_lower_limit = 0 configurazione_ids = self.pool.get('protocollo.configurazione').search( cr, uid, []) configurazione = self.pool.get('protocollo.configurazione').browse( cr, uid, configurazione_ids[0]) message = None if context and context.has_key('active_id') and context['active_id']: message = self.pool.get('mail.message').browse( cr, uid, context['active_id']) if configurazione.select_body: options.append(('testo', 'Corpo del messaggio')) if message and message.eml and configurazione.select_eml: options.append(('eml', 'Intero messaggio (file EML)')) attach_lower_limit = 1 #al momento le PEC si includono anche l'attachment EML quindi il controllo parte da 1 if 'attachment_ids' in context and len( context['attachment_ids'][0] [2]) > attach_lower_limit and configurazione.select_attachments: options.insert(0, ('allegato', 'Allegato')) return options def on_change_attachment(self, cr, uid, ids, attachment_id, context=None): values = {'preview': False} if attachment_id: ir_attachment = self.pool.get('ir.attachment').browse( cr, uid, attachment_id) for attach in ir_attachment: if attach.file_type == 'application/pdf': values = { 'preview': attach.datas, } return {'value': values} else: return None _columns = { 'registration_employee_department_id': fields.many2one('hr.department', 'Il mio ufficio'), 'registration_employee_department_id_invisible': fields.boolean('Campo registration_employee_department_id invisible', readonly=True), 'subject': fields.text('Oggetto', readonly=True), 'body': fields.html('Corpo della mail', readonly=True), 'receiving_date': fields.datetime('Data Ricezione', required=False, readonly=True), 'message_id': fields.integer('Id', required=True, readonly=True), 'select_doc_principale': fields.selection(_get_doc_principale_option, 'Seleziona il documento da protocollare', select=True, required=True), 'doc_principale': fields.many2one('ir.attachment', 'Allegato', domain="[('datas_fname', '=', 'original_email.eml')]"), 'is_attach_message': fields.related('ir.attachment', 'doc_principale', type='boolean', string="Author's Avatar"), 'doc_fname': fields.related('doc_principale', 'datas_fname', type='char', readonly=True), 'doc_description': fields.char('Descrizione documento', size=256, readonly=False), 'preview': fields.binary('Anteprima allegato PDF'), 'sender_receivers': fields.one2many('protocollo.sender_receiver.wizard', 'wizard_id', 'Mittenti/Destinatari', required=True, limit=1), 'documento_descrizione_required_wizard': fields.boolean('Descrizione documento obbligatorio', readonly=1) # 'dossier_ids': fields.many2many( # 'protocollo.dossier', # 'dossier_protocollo_pec_rel', # 'wizard_id', 'dossier_id', # 'Fascicoli'), # TODO: insert assigne here # 'notes': fields.text('Note'), } # def _default_doc_principale(self, cr, uid, context): # id = 0 # mail_message = self.pool.get('mail.message').browse(cr, uid, context['active_id'], context=context) # for attach in mail_message.attachment_ids: # if attach.name == 'original_email.eml': # id = attach.id # return id def _default_registration_employee_department_id(self, cr, uid, context): department_ids = self.pool.get('hr.department').search( cr, uid, [('can_used_to_protocol', '=', True)]) if department_ids: return department_ids[0] return False def _default_registration_employee_department_id_invisible( self, cr, uid, context): department_ids = self.pool.get('hr.department').search( cr, uid, [('can_used_to_protocol', '=', True)]) if len(department_ids) == 1: return True return False def _default_subject(self, cr, uid, context): mail_message = self.pool.get('mail.message').browse( cr, uid, context['active_id'], context=context) return mail_message.subject def _default_id(self, cr, uid, context): mail_message = self.pool.get('mail.message').browse( cr, uid, context['active_id'], context=context) return mail_message.id def _default_receiving_date(self, cr, uid, context): mail_message = self.pool.get('mail.message').browse( cr, uid, context['active_id'], context=context) if mail_message.server_received_datetime: return mail_message.server_received_datetime return mail_message.date def _default_body(self, cr, uid, context): mail_message = self.pool.get('mail.message').browse( cr, uid, context['active_id'], context=context) return mail_message.body def _default_sender_receivers(self, cr, uid, context): mail_message = self.pool.get('mail.message').browse( cr, uid, context['active_id'], context=context) partner = mail_message.author_id res = [] sr_substring = re.findall('<[^>]+>', mail_message.email_from) if len(sr_substring): sr_email = sr_substring[0].strip('<>') sr_name = mail_message.email_from.replace(sr_substring[0], '').replace('"', '').strip() else: sr_name = '' sr_email = mail_message.email_from if partner: res.append({ 'partner_id': partner.id, 'type': partner.is_company and 'legal' or 'individual', 'name': partner.name, 'street': partner.street, 'zip': partner.zip, 'city': partner.city, 'country_id': partner.country_id.id, 'email': partner.email, 'phone': partner.phone, 'fax': partner.fax, 'mobile': partner.mobile, 'pec_mail': partner.pec_mail }) elif 'message_type' in context and context['message_type'] == 'mail': res.append({ 'name': sr_name, 'email': sr_email, 'type': 'individual', }) elif 'message_type' in context and context['message_type'] == 'pec': res.append({ 'name': sr_name, 'pec_mail': sr_email, 'type': 'individual', }) return res def _default_documento_descrizione_wizard_required(self, cr, uid, context): configurazione_ids = self.pool.get('protocollo.configurazione').search( cr, uid, []) configurazione = self.pool.get('protocollo.configurazione').browse( cr, uid, configurazione_ids[0]) return configurazione.documento_descrizione_required _defaults = { 'registration_employee_department_id': _default_registration_employee_department_id, 'registration_employee_department_id_invisible': _default_registration_employee_department_id_invisible, 'subject': _default_subject, 'message_id': _default_id, 'receiving_date': _default_receiving_date, 'body': _default_body, 'sender_receivers': _default_sender_receivers, 'documento_descrizione_required_wizard': _default_documento_descrizione_wizard_required # 'doc_principale': _default_doc_principale, } def action_save(self, cr, uid, ids, context=None): wizard = self.browse(cr, uid, ids[0], context=context) protocollo_obj = self.pool.get('protocollo.protocollo') sender_receiver_obj = self.pool.get('protocollo.sender_receiver') protocollo_typology_obj = self.pool.get('protocollo.typology') mail_message_obj = self.pool.get('mail.message') mail_message = mail_message_obj.browse(cr, uid, context['active_id'], context=context) employee = self.pool.get('hr.employee').get_department_employee( cr, uid, wizard.registration_employee_department_id.id) vals = {} vals['type'] = 'in' vals['receiving_date'] = wizard.receiving_date vals[ 'subject'] = wizard.subject if wizard.select_doc_principale == 'eml' else '' vals['body'] = wizard.body vals['mail_pec_ref'] = context['active_id'] vals['user_id'] = uid vals[ 'registration_employee_department_id'] = wizard.registration_employee_department_id.id vals[ 'registration_employee_department_name'] = wizard.registration_employee_department_id.complete_name vals['registration_employee_id'] = employee.id vals['registration_employee_name'] = employee.name_related sender_receiver = [] is_pec = False is_segnatura = False # Estrae i dati del mittente dalla segnatura configurazione_ids = self.pool.get('protocollo.configurazione').search( cr, uid, []) configurazione = self.pool.get('protocollo.configurazione').browse( cr, uid, configurazione_ids[0]) if 'message_type' in context and context['message_type'] == 'pec': is_pec = True if is_pec: srvals = {} typology_id = protocollo_typology_obj.search( cr, uid, [('pec', '=', True)])[0] messaggio_pec_obj = self.pool.get('protocollo.messaggio.pec') messaggio_pec_id = messaggio_pec_obj.create( cr, uid, { 'type': 'messaggio', 'messaggio_ref': mail_message.id }) if configurazione.segnatura_xml_parse: srvals = self.elaboraSegnatura(cr, uid, protocollo_obj, mail_message) if len(srvals) > 0 and len(srvals['mittente']) > 0: is_segnatura = True sender_segnatura_xml_parse = configurazione.sender_segnatura_xml_parse if is_pec and is_segnatura: srvals['mittente']['pec_messaggio_ids'] = [[ 6, 0, [messaggio_pec_id] ]] if sender_segnatura_xml_parse: sender_receiver.append( sender_receiver_obj.create(cr, uid, srvals['mittente'])) if (is_pec and (is_segnatura is False or (is_segnatura and not sender_segnatura_xml_parse)) ) or is_pec is False: for send_rec in wizard.sender_receivers: srvals = { 'type': send_rec.type, 'source': 'sender', 'partner_id': send_rec.partner_id and send_rec.partner_id.id or False, 'name': send_rec.name, 'street': send_rec.street, 'zip': send_rec.zip, 'city': send_rec.city, 'country_id': send_rec.country_id and send_rec.country_id.id or False, 'phone': send_rec.phone, 'fax': send_rec.fax, 'mobile': send_rec.mobile, } if is_pec: srvals['pec_mail'] = send_rec.pec_mail srvals['pec_messaggio_ids'] = [[6, 0, [messaggio_pec_id]]] else: srvals['pec_mail'] = '' srvals['email'] = send_rec.email srvals['sharedmail_messaggio_ids'] = [ (4, context['active_id']) ] sender_receiver.append( sender_receiver_obj.create(cr, uid, srvals)) vals['sender_receivers'] = [[6, 0, sender_receiver]] if 'protocollo' in srvals: vals['sender_protocol'] = srvals['protocollo']['sender_protocol'] vals['sender_register'] = srvals['protocollo']['sender_register'] vals['sender_registration_date'] = srvals['protocollo'][ 'sender_registration_date'] if is_pec is False: typology_id = protocollo_typology_obj.search( cr, uid, [('sharedmail', '=', True)])[0] vals['typology'] = typology_id protocollo_id = protocollo_obj.create(cr, uid, vals) if is_pec: self.pool.get('mail.message').write( cr, SUPERUSER_ID, context['active_id'], {'pec_protocol_ref': protocollo_id}, context=context) else: self.pool.get('mail.message').write( cr, SUPERUSER_ID, context['active_id'], {'sharedmail_protocol_ref': protocollo_id}, context=context) action_class = "history_icon print" post_vars = { 'subject': "Creata Bozza Protocollo", 'body': "<div class='%s'><ul><li>Messaggio convertito in bozza di protocollo</li></ul></div>" % action_class, 'model': "protocollo.protocollo", 'res_id': context['active_id'], } thread_pool = self.pool.get('protocollo.protocollo') thread_pool.message_post(cr, uid, protocollo_id, type="notification", context=context, **post_vars) # Attachments file_data_list = [] body_pdf_content = base64.b64encode( ConversionUtility.html_to_pdf(wizard.body)) body_pdf_name = "mailbody.pdf" if wizard.select_doc_principale == 'testo': protocollo_obj.carica_documento_principale(cr, uid, protocollo_id, body_pdf_content, body_pdf_name, wizard.doc_description, {'skip_check': True}) else: file_data_list.append({ 'datas': body_pdf_content, 'datas_fname': body_pdf_name, 'datas_description': '' }) for attach in mail_message.attachment_ids: if attach.name == 'original_email.eml': if wizard.select_doc_principale == 'eml': protocollo_obj.carica_documento_principale( cr, uid, protocollo_id, attach.datas, attach.name, wizard.doc_description, {'skip_check': True}) else: if wizard.select_doc_principale == 'allegato' and attach.id == wizard.doc_principale.id: if attach.datas and attach.name: protocollo_obj.carica_documento_principale( cr, uid, protocollo_id, attach.datas, attach.name, wizard.doc_description, {'skip_check': True}) else: file_data_list.append({ 'datas': attach.datas, 'datas_fname': attach.name, 'datas_description': '' }) if file_data_list: protocollo_obj.carica_documenti_secondari(cr, uid, protocollo_id, file_data_list) obj_model = self.pool.get('ir.model.data') model_data_ids = obj_model.search( cr, uid, [('model', '=', 'ir.ui.view'), ('name', '=', 'protocollo_protocollo_form')]) resource_id = obj_model.read(cr, uid, model_data_ids, fields=['res_id'])[0]['res_id'] return { 'view_type': 'form', 'view_mode': 'form', 'res_model': 'protocollo.protocollo', 'res_id': protocollo_id, 'views': [(resource_id, 'form')], 'type': 'ir.actions.act_window', 'context': context, 'flags': { 'initial_mode': 'edit' } } def elaboraSegnatura(self, cr, uid, protocollo_obj, mail_message): srvals = {} srvals_mittente = {} srvals_protocollo = {} for attach in mail_message.attachment_ids: if attach.name.lower() == 'segnatura.xml' and attach.datas: attach_path = self.pool.get('ir.attachment')._full_path( cr, uid, attach.store_fname) xml = open(attach_path, "rb").read() content_encode = xml.decode("latin").encode("utf8") tree = etree.fromstring(content_encode) segnatura_xml = SegnaturaXMLParser(tree) srvals_mittente = self.getDatiSegnaturaMittente(segnatura_xml) srvals_protocollo = self.getDatiSegnaturaProtocollo( segnatura_xml) srvals_mittente['pec_mail'] = mail_message.email_from.encode( 'utf8') srvals['mittente'] = srvals_mittente srvals['protocollo'] = srvals_protocollo return srvals def getDatiSegnaturaMittente(self, segnatura_xml): srvals = { 'type': segnatura_xml.getTipoMittente(), 'pa_type': segnatura_xml.getTipoAmministrazione(), 'source': 'sender', 'partner_id': False, 'name': segnatura_xml.getDenominazioneCompleta(), 'street': segnatura_xml.getToponimo(), 'zip': segnatura_xml.getCAP(), 'city': segnatura_xml.getComune(), 'country_id': False, 'email': segnatura_xml.getIndirizzoTelematico(), 'phone': segnatura_xml.getTelefono(), 'fax': segnatura_xml.getFax(), 'ipa_code': segnatura_xml.getCodiceUnitaOrganizzativa(), 'ident_code': segnatura_xml.getCodiceAOO(), 'amm_code': segnatura_xml.getCodiceAmministrazione() } return srvals def getDatiSegnaturaProtocollo(self, segnatura_xml): srvals = { 'sender_protocol': segnatura_xml.getNumeroRegistrazione(), 'sender_register': segnatura_xml.getCodiceRegistro(), 'sender_registration_date': segnatura_xml.getDataRegistrazione() } return srvals
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'): if getattr(model, 'message_mass_mailing_enabled'): res.append( (model._name, model.message_mass_mailing_enabled())) else: res.append((model._name, 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' ctx = dict(context or {}, active_test=False) record_ids = model.search(cr, uid, [('id', 'in', res_ids), (email_fname, 'ilike', email)], context=ctx) 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)]" elif 'opt_out' in self.pool[mailing_model]._fields: value['mailing_domain'] = "[('opt_out', '=', 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 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, } @api.onchange('product_id') def _onchange_product_id(self): if not self.product_id: return product = self.product_id self.price_unit = product.list_price self.website_description = product.product_tmpl_id.quote_description self.name = product.name self.uom_id = product.uom_id domain = { 'uom_id': [('category_id', '=', self.product_id.uom_id.category_id.id)] } return {'domain': domain} @api.onchange('uom_id') def _onchange_product_uom(self): if not self.product_id: return if not self.uom_id: self.price_unit = 0.0 return if self.uom_id.id != self.product_id.uom_id.id: new_price = self.product_id.uom_id._compute_price( self.product_id.uom_id.id, self.price_unit, self.uom_id.id) self.price_unit = new_price 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 confirm_operation_wizard(models.TransientModel): _name = 'confirm.operation.wizard' _description = 'Wizard di Conferma operazione' _columns = { 'message': fields.html(string="Conferma operazione", readonly=True, store=False) } def _default_message(self, cr, uid, context=None): res = "" if context is None: context = {} if 'action_not_protocol_pec' in context and context[ 'action_not_protocol_pec']: res = '<p>Vuoi archiviare questo messaggio PEC?</p>' if 'action_not_protocol_sharedmail' in context and context[ 'action_not_protocol_sharedmail']: res = '<p>Vuoi archiviare questo messaggio e-mail?</p>' if 'action_not_protocol_document' in context and context[ 'action_not_protocol_document']: res = '<p>Vuoi archiviare questo messaggio documento?</p>' return res _defaults = {'message': _default_message} def go_to_operation(self, cr, uid, ids, context=None): if context is None: context = {} if 'action_not_protocol_sharedmail' in context and context[ 'action_not_protocol_sharedmail']: message_obj = self.pool.get('mail.message') message = message_obj.browse(cr, SUPERUSER_ID, context['active_id']) if message.sharedmail_state == 'new': message_obj.write(cr, SUPERUSER_ID, context['active_id'], {'sharedmail_state': 'not_protocol'}) else: raise orm.except_orm( _("Avviso"), _("Messaggio gia' archiviato in precedenza: aggiorna la pagina" )) if 'action_not_protocol_pec' in context and context[ 'action_not_protocol_pec']: message_obj = self.pool.get('mail.message') message = message_obj.browse(cr, SUPERUSER_ID, context['active_id']) if message.pec_state == 'new': message_obj.write(cr, SUPERUSER_ID, context['active_id'], {'pec_state': 'not_protocol'}) else: raise orm.except_orm( _("Avviso"), _("Messaggio gia' archiviato in precedenza: aggiorna la pagina" )) if 'action_not_protocol_document' in context and context[ 'action_not_protocol_document']: document_obj = self.pool.get('gedoc.document') document = document_obj.browse(cr, SUPERUSER_ID, context['active_id']) if document.doc_protocol_state == 'new': document_obj.write(cr, SUPERUSER_ID, context['active_id'], {'doc_protocol_state': 'not_protocol'}) else: raise orm.except_orm( _("Avviso"), _("Documento gia' archiviato in precedenza: aggiorna la pagina" )) return True
class SaleOrderLine(orm.Model): """ADD HTML note to sale order lines""" _inherit = "sale.order.line" _columns = {'formatted_note': fields.html('Formatted Note')}
class ProductTemplate(orm.Model): _inherit = 'product.template' _columns = {'description': fields.html('Invoice description')}
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 OpenERP 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' 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( '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.'), 'validation': fields.selection( [('manual', 'Manual'), ('automatic', 'Automatic')], string='Process Method', help= 'Static payments are payments like transfer, that require manual steps.' ), 'view_template_id': fields.many2one('ir.ui.view', 'Form Button Template', required=True), '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.)"), # Fees 'fees_active': fields.boolean('Compute 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)'), } _defaults = { 'company_id': lambda self, cr, uid, obj, ctx=None: self.pool['res.users'].browse( cr, uid, uid).company_id.id, 'environment': 'test', 'validation': 'automatic', 'website_published': True, } 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 form_preprocess_values(self, cr, uid, id, reference, amount, currency_id, tx_id, partner_id, partner_values, tx_values, context=None): """ Pre process values before giving them to the acquirer-specific render methods. Those methods will receive: - partner_values: will contain name, lang, email, zip, address, city, country_id (int or False), country (browse or False), phone, reference - tx_values: will contain reference, amount, currency_id (int or False), currency (browse or False), partner (browse or False) """ acquirer = self.browse(cr, uid, id, context=context) if tx_id: tx = self.pool.get('payment.transaction').browse(cr, uid, tx_id, context=context) tx_data = { 'reference': tx.reference, 'amount': tx.amount, 'currency_id': tx.currency_id.id, 'currency': tx.currency_id, 'partner': tx.partner_id, } partner_data = { 'name': tx.partner_name, 'lang': tx.partner_lang, 'email': tx.partner_email, 'zip': tx.partner_zip, 'address': tx.partner_address, 'city': tx.partner_city, 'country_id': tx.partner_country_id.id, 'country': tx.partner_country_id, 'phone': tx.partner_phone, 'reference': tx.partner_reference, 'state': None, } else: if partner_id: partner = self.pool['res.partner'].browse(cr, uid, partner_id, context=context) partner_data = { 'name': partner.name, 'lang': partner.lang, 'email': partner.email, 'zip': partner.zip, 'city': partner.city, 'address': _partner_format_address(partner.street, partner.street2), 'country_id': partner.country_id.id, 'country': partner.country_id, 'phone': partner.phone, 'state': partner.state_id, } else: partner, partner_data = False, {} partner_data.update(partner_values) 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 tx_data = { 'reference': reference, 'amount': amount, 'currency_id': currency.id, 'currency': currency, 'partner': partner, } # update tx values tx_data.update(tx_values) # update partner values if not partner_data.get('address'): partner_data['address'] = _partner_format_address( partner_data.get('street', ''), partner_data.get('street2', '')) if not partner_data.get('country') and partner_data.get('country_id'): partner_data['country'] = self.pool['res.country'].browse( cr, uid, partner_data.get('country_id'), context=context) partner_data.update({ 'first_name': _partner_split_name(partner_data['name'])[0], 'last_name': _partner_split_name(partner_data['name'])[1], }) # 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, tx_data['amount'], tx_data['currency_id'], partner_data['country_id'], context=None) tx_data['fees'] = float_round(fees, 2) return (partner_data, tx_data) def render(self, cr, uid, id, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, context=None): """ Renders the form template of the given acquirer as a qWeb template. 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: the current partner browse record, if any (not necessarily set) - partner_values: a dictionary of partner-related values - tx_values: a dictionary of transaction related values that depends on the acquirer. Some specific keys should be managed in each provider, depending on the features it offers: - '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: OpenERP context dictionary :param string reference: the transaction reference :param float amount: the amount the buyer has to pay :param res.currency browse record currency: currency :param int tx_id: id of a transaction; if set, bypasses all other given values and only render the already-stored transaction :param res.partner browse record partner_id: the buyer :param dict partner_values: a dictionary of values for the buyer (see above) :param dict tx_custom_values: a dictionary of values for the transction that is given to the acquirer-specific method generating the form values :param dict context: OpenERP context """ if context is None: context = {} if tx_values is None: tx_values = {} if partner_values is None: partner_values = {} acquirer = self.browse(cr, uid, id, context=context) # pre-process values amount = float_round(amount, 2) partner_values, tx_values = self.form_preprocess_values( cr, uid, id, reference, amount, currency_id, tx_id, partner_id, partner_values, tx_values, context=context) # 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) partner_values, tx_values = method(cr, uid, id, partner_values, tx_values, context=context) qweb_context = { '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), 'reference': tx_values['reference'], 'amount': tx_values['amount'], 'currency': tx_values['currency'], 'partner': tx_values.get('partner'), 'partner_values': partner_values, 'tx_values': tx_values, 'context': context, } # 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, qweb_context, engine='ir.qweb', context=context) def _wrap_payment_block(self, cr, uid, html_block, amount, currency_id, context=None): payment_header = _('Pay safely online') amount_str = float_repr( amount, self.pool.get('decimal.precision').precision_get( cr, uid, 'Account')) currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context) currency_str = currency.symbol or currency.name amount = u"%s %s" % ( (currency_str, amount_str) if currency.position == 'before' else (amount_str, currency_str)) result = u"""<div class="payment_acquirers"> <div class="payment_header"> <div class="payment_amount">%s</div> %s </div> %%s </div>""" % (amount, payment_header) return result % html_block.decode("utf-8") def render_payment_block(self, cr, uid, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, company_id=None, context=None): html_forms = [] domain = [('website_published', '=', True), ('validation', '=', 'automatic')] if company_id: domain.append(('company_id', '=', company_id)) acquirer_ids = self.search(cr, uid, domain, context=context) for acquirer_id in acquirer_ids: button = self.render(cr, uid, acquirer_id, reference, amount, currency_id, tx_id, partner_id, partner_values, tx_values, context) html_forms.append(button) if not html_forms: return '' html_block = '\n'.join(filter(None, html_forms)) return self._wrap_payment_block(cr, uid, html_block, amount, currency_id, context=context)
class DeliveryCompany(models.Model): _name = 'mft.delivery_company' _columns = { 'name': fields.char(string='Delivery Company', readonly=True, states={'draft': [('readonly', False)]}), 'contact_name': fields.char(string='Contact Name', readonly=True, states={'draft': [('readonly', False)]}), 'contact_phone': fields.char(string='Contact Phone', readonly=True, states={'draft': [('readonly', False)]}), 'description': fields.html(string='Description', readonly=True, states={'draft': [('readonly', False)]}), 'state': fields.selection([ ('draft', 'Draft'), ('toconfirm', 'Request Confirm'), ('confirmed', 'Confirmed'), ('cancel', 'Cancel'), ], 'State', readonly=True, copy=False), #'wo_ids': fields.one2many('mft.work_order','oem_id',string='成品工单') } _defaults = {'state': 'draft'} def action_toconfirm(self, cr, uid, ids, context=None): if context is None: context = {} self.write(cr, uid, ids, {'state': 'toconfirm'}) return True def action_confirmed(self, cr, uid, ids, context=None): if context is None: context = {} self.write(cr, uid, ids, {'state': 'confirmed'}) return True def action_recover(self, cr, uid, ids, context=None): if context is None: context = {} self.write(cr, uid, ids, {'state': 'draft'}) return True def unlink(self, cr, uid, ids, context=None): product_names = self.read(cr, uid, ids, ['state'], context=context) unlink_ids = [] for p in product_names: if p['state'] in ['draft']: unlink_ids.append(p['id']) else: raise osv.except_osv(_('Invalid Action!'), '只有草稿才能删除!') return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)