class Channel(models.Model): """ A channel is a container of slides. It has group-based access configuration allowing to configure slide upload and access. Slides can be promoted in channels. """ _name = 'slide.channel' _description = 'Channel for Slides' _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.mixin' ] _order = 'sequence, id' _order_by_strategy = { 'most_viewed': 'total_views desc', 'most_voted': 'likes desc', 'latest': 'date_published desc', } name = fields.Char('Name', translate=True, required=True) active = fields.Boolean(default=True) description = fields.Html('Description', translate=html_translate, sanitize_attributes=False) sequence = fields.Integer(default=10, help='Display order') category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") promote_strategy = fields.Selection([('none', 'No Featured Presentation'), ('latest', 'Latest Published'), ('most_voted', 'Most Voted'), ('most_viewed', 'Most Viewed'), ('custom', 'Featured Presentation')], string="Featuring Policy", default='most_voted', required=True) custom_slide_id = fields.Many2one('slide.slide', string='Slide to Promote') promoted_slide_id = fields.Many2one('slide.slide', string='Featured Slide', compute='_compute_promoted_slide_id', store=True) @api.depends('custom_slide_id', 'promote_strategy', 'slide_ids.likes', 'slide_ids.total_views', "slide_ids.date_published") def _compute_promoted_slide_id(self): for record in self: if record.promote_strategy == 'none': record.promoted_slide_id = False elif record.promote_strategy == 'custom': record.promoted_slide_id = record.custom_slide_id elif record.promote_strategy: slides = self.env['slide.slide'].search( [('website_published', '=', True), ('channel_id', '=', record.id)], limit=1, order=self._order_by_strategy[record.promote_strategy]) record.promoted_slide_id = slides and slides[0] or False nbr_presentations = fields.Integer('Number of Presentations', compute='_count_presentations', store=True) nbr_documents = fields.Integer('Number of Documents', compute='_count_presentations', store=True) nbr_videos = fields.Integer('Number of Videos', compute='_count_presentations', store=True) nbr_infographics = fields.Integer('Number of Infographics', compute='_count_presentations', store=True) total = fields.Integer(compute='_count_presentations', store=True) @api.depends('slide_ids.slide_type', 'slide_ids.website_published') def _count_presentations(self): result = dict.fromkeys(self.ids, dict()) res = self.env['slide.slide'].read_group( [('website_published', '=', True), ('channel_id', 'in', self.ids)], ['channel_id', 'slide_type'], ['channel_id', 'slide_type'], lazy=False) for res_group in res: result[res_group['channel_id'][0]][res_group[ 'slide_type']] = result[res_group['channel_id'][0]].get( res_group['slide_type'], 0) + res_group['__count'] for record in self: record.nbr_presentations = result[record.id].get('presentation', 0) record.nbr_documents = result[record.id].get('document', 0) record.nbr_videos = result[record.id].get('video', 0) record.nbr_infographics = result[record.id].get('infographic', 0) record.total = record.nbr_presentations + record.nbr_documents + record.nbr_videos + record.nbr_infographics publish_template_id = fields.Many2one( 'mail.template', string='Published Template', help="Email template to send slide publication through email", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_published')) share_template_id = fields.Many2one( 'mail.template', string='Shared Template', help="Email template used when sharing a slide", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_shared')) visibility = fields.Selection( [('public', 'Public'), ('private', 'Private'), ('partial', 'Show channel but restrict presentations')], default='public', required=True) group_ids = fields.Many2many( 'res.groups', 'rel_channel_groups', 'channel_id', 'group_id', string='Channel Groups', help="Groups allowed to see presentations in this channel") access_error_msg = fields.Html( 'Error Message', help="Message to display when not accessible due to access rights", default= "<p>This channel is private and its content is restricted to some users.</p>", translate=html_translate, sanitize_attributes=False) upload_group_ids = fields.Many2many( 'res.groups', 'rel_upload_groups', 'channel_id', 'group_id', string='Upload Groups', help= "Groups allowed to upload presentations in this channel. If void, every user can upload." ) # not stored access fields, depending on each user can_see = fields.Boolean('Can See', compute='_compute_access', search='_search_can_see') can_see_full = fields.Boolean('Full Access', compute='_compute_access') can_upload = fields.Boolean('Can Upload', compute='_compute_access') def _search_can_see(self, operator, value): if operator not in ('=', '!=', '<>'): raise ValueError('Invalid operator: %s' % (operator, )) if not value: operator = operator == "=" and '!=' or '=' if self._uid == SUPERUSER_ID: return [(1, '=', 1)] # Better perfs to split request and use inner join that left join req = """ SELECT id FROM slide_channel WHERE visibility='public' UNION SELECT c.id FROM slide_channel c INNER JOIN rel_channel_groups rg on c.id = rg.channel_id INNER JOIN res_groups g on g.id = rg.group_id INNER JOIN res_groups_users_rel u on g.id = u.gid and uid = %s """ op = operator == "=" and "inselect" or "not inselect" # don't use param named because orm will add other param (test_active, ...) return [('id', op, (req, (self._uid)))] @api.one @api.depends('visibility', 'group_ids', 'upload_group_ids') def _compute_access(self): self.can_see = self.visibility in [ 'public', 'private' ] or bool(self.group_ids & self.env.user.groups_id) self.can_see_full = self.visibility == 'public' or bool( self.group_ids & self.env.user.groups_id) self.can_upload = self.can_see and (not self.upload_group_ids or bool(self.upload_group_ids & self.env.user.groups_id)) @api.multi @api.depends('name') def _compute_website_url(self): super(Channel, self)._compute_website_url() base_url = self.env['ir.config_parameter'].get_param('web.base.url') for channel in self: if channel.id: # avoid to perform a slug on a not yet saved record in case of an onchange. channel.website_url = '%s/slides/%s' % (base_url, slug(channel)) @api.onchange('visibility') def change_visibility(self): if self.visibility == 'public': self.group_ids = False @api.multi def write(self, vals): res = super(Channel, self).write(vals) if 'active' in vals: # archiving/unarchiving a channel does it on its slides, too self.with_context(active_test=False).mapped('slide_ids').write( {'active': vals['active']}) return res @api.multi @api.returns('self', lambda value: value.id) def message_post(self, parent_id=False, subtype=None, **kwargs): """ Temporary workaround to avoid spam. If someone replies on a channel through the 'Presentation Published' email, it should be considered as a note as we don't want all channel followers to be notified of this answer. """ self.ensure_one() if parent_id: parent_message = self.env['mail.message'].sudo().browse(parent_id) if parent_message.subtype_id and parent_message.subtype_id == self.env.ref( 'website_slides.mt_channel_slide_published'): if kwargs.get('subtype_id'): kwargs['subtype_id'] = False subtype = 'mail.mt_note' return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs)
class CreateChildAccountWizard(models.TransientModel, Glob_tag_Model): '''新增下级科目的向导''' _name = 'accountcore.create_child_account' _description = '新增下级科目向导' fatherAccountId = fields.Many2one('accountcore.account', string='上级科目', help='新增科目的直接上级科目') fatherAccountNumber = fields.Char(related='fatherAccountId.number', string='上级科目编码') org = fields.Many2one('accountcore.org', string='所属机构', help="科目所属机构", index=True, ondelete='restrict') accountsArch = fields.Many2one('accountcore.accounts_arch', string='所属科目体系', help="科目所属体系", index=True, ondelete='restrict') accountClass = fields.Many2one('accountcore.accountclass', string='科目类别', index=True, ondelete='restrict') number = fields.Char(string='科目编码', required=True) name = fields.Char(string='科目名称', required=True) direction = fields.Selection([('1', '借'), ('-1', '贷')], string='余额方向', required=True) cashFlowControl = fields.Boolean(string='分配现金流量') itemClasses = fields.Many2many('accountcore.itemclass', string='包含的核算项目类别', help="录入凭证时,提示选择该类别下的核算项目", ondelete='restrict') accountItemClass = fields.Many2one( 'accountcore.itemclass', string='作为明细科目的类别', help="录入凭证分录时必须输入的该类别下的一个核算项目,作用相当于明细科目", ondelete='restrict') explain = fields.Html(string='科目说明') @api.model def default_get(self, field_names): default = super().default_get(field_names) fatherAccountId = self.env.context.get('active_id') fatherAccount = self.env['accountcore.account'].sudo().search( [['id', '=', fatherAccountId]]) default['accountsArch'] = fatherAccount.accountsArch.id default['fatherAccountId'] = fatherAccountId default['org'] = fatherAccount.org.id default['accountClass'] = fatherAccount.accountClass.id default['direction'] = fatherAccount.direction default['cashFlowControl'] = fatherAccount.cashFlowControl default['number'] = fatherAccount.number + \ '.' + str(fatherAccount.currentChildNumber) return default @api.model def create(self, values): fatherAccountId = self.env.context.get('active_id') accountTable = self.env['accountcore.account'].sudo() fatherAccount = accountTable.search([['id', '=', fatherAccountId]]) newAccount = { 'fatherAccountId': fatherAccountId, 'org': fatherAccount.org.id, 'accountClass': fatherAccount.accountClass.id, 'cashFlowControl': values['cashFlowControl'], 'name': fatherAccount.name + '---' + values['name'], 'number': fatherAccount.number + '.' + str(fatherAccount.currentChildNumber) } fatherAccount.currentChildNumber = fatherAccount.currentChildNumber + 1 values.update(newAccount) rl = super(CreateChildAccountWizard, self).create(values) a = accountTable.create(values) # 添加到上级科目的直接下级 fatherAccount.write({'childs_ids': [(4, a.id)]}) return rl
class PaymentAcquirer(models.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, reference, amount, currency, partner_id=False, partner_values=None, tx_custom_values=None)``: method that generates the values used to render the form button template. - ``<name>_get_form_action_url(self):``: 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, amount, currency_id, country_id)``: computes 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 = 'website_published desc, sequence, name' name = fields.Char('Name', required=True, translate=True) description = fields.Html('Description') sequence = fields.Integer('Sequence', default=10, help="Determine the display order") provider = fields.Selection(selection=[('manual', 'Manual Configuration')], string='Provider', default='manual', required=True) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env.user.company_id.id, required=True) 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', default='test', oldname='env', required=True) website_published = fields.Boolean( 'Visible in Portal / Website', copy=False, help="Make this payment acquirer available (Customer invoices, etc.)") # Formerly associated to `authorize` option from auto_confirm capture_manually = fields.Boolean( string="Capture Amount Manually", help="Capture the amount from Odoo, when the delivery is completed.") # Formerly associated to `generate_and_pay_invoice` option from auto_confirm journal_id = fields.Many2one( 'account.journal', 'Payment Journal', domain=[('type', '=', 'bank')], default=lambda self: self.env['account.journal'].search( [('type', '=', 'bank')], limit=1), help= """Payments will be registered into this journal. If you get paid straight on your bank account, select your bank account. If you get paid in batch for several transactions, create a specific payment journal for this payment acquirer to easily manage the bank reconciliation. You hold the amount in a temporary transfer account of your books (created automatically when you create the payment journal). Then when you get paid on your bank account by the payment acquirer, you reconcile the bank statement line with this temporary transfer account. Use reconciliation templates to do it in one-click.""") specific_countries = fields.Boolean( string="Specific Countries", help= "If you leave it empty, the payment acquirer will be available for all the countries." ) country_ids = fields.Many2many( 'res.country', 'payment_country_rel', 'payment_id', 'country_id', 'Countries', help= "This payment gateway is available for selected countries. If none is selected it is available for all countries." ) pre_msg = fields.Html( 'Help Message', translate=True, help='Message displayed to explain and help the payment process.') post_msg = fields.Html( 'Thanks Message', translate=True, help='Message displayed after having done the payment process.') pending_msg = fields.Html( 'Pending Message', translate=True, default= '<i>Pending,</i> Your online payment has been successfully processed. But your order is not validated yet.', help= 'Message displayed, if order is in pending state after having done the payment process.' ) done_msg = fields.Html( 'Done Message', translate=True, default= '<i>Done,</i> Your online payment has been successfully processed. Thank you for your order.', help= 'Message displayed, if order is done successfully after having done the payment process.' ) cancel_msg = fields.Html( 'Cancel Message', translate=True, default='<i>Cancel,</i> Your payment has been cancelled.', help='Message displayed, if order is cancel during the payment process.' ) error_msg = fields.Html( 'Error Message', translate=True, default= '<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order.', help='Message displayed, if error is occur during the payment process.' ) save_token = fields.Selection( [('none', 'Never'), ('ask', 'Let the customer decide (recommended for eCommerce)'), ('always', 'Always (recommended for Subscriptions)')], string='Save Cards', default='none', help= "This option allows customers to save their credit card as a payment token and to reuse it for a later purchase." "If you manage subscriptions (recurring invoicing), you need it to automatically charge the customer when you " "issue an invoice.") token_implemented = fields.Boolean('Saving Card Data supported', compute='_compute_feature_support') authorize_implemented = fields.Boolean('Authorize Mechanism Supported', compute='_compute_feature_support') fees_implemented = fields.Boolean('Fees Computation Supported', compute='_compute_feature_support') 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)') # TDE FIXME: remove that brol module_id = fields.Many2one('ir.module.module', string='Corresponding Module') module_state = fields.Selection(selection=module.STATES, string='Installation State', related='module_id.state') image = fields.Binary( "Image", attachment=True, help= "This field holds the image used for this provider, limited to 1024x1024px" ) image_medium = fields.Binary( "Medium-sized image", 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 = fields.Binary( "Small-sized image", 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.") def _compute_feature_support(self): feature_support = self._get_feature_support() for acquirer in self: acquirer.fees_implemented = acquirer.provider in feature_support[ 'fees'] acquirer.authorize_implemented = acquirer.provider in feature_support[ 'authorize'] acquirer.token_implemented = acquirer.provider in feature_support[ 'tokenize'] @api.multi def _check_required_if_provider(self): """ If the field has 'required_if_provider="<provider>"' attribute, then it required if record.provider is <provider>. """ for acquirer in self: if any( getattr(f, 'required_if_provider', None) == acquirer.provider and not acquirer[k] for k, f in pycompat.items(self._fields)): return False return True _constraints = [ (_check_required_if_provider, 'Required fields not filled', []), ] def _get_feature_support(self): """Get advanced feature support by provider. Each provider should add its technical in the corresponding key for the following features: * fees: support payment fees computations * authorize: support authorizing payment (separates authorization and capture) * tokenize: support saving payment data in a payment.tokenize object """ return dict(authorize=[], tokenize=[], fees=[]) @api.model def create(self, vals): image_resize_images(vals) vals = self._check_journal_id(vals) return super(PaymentAcquirer, self).create(vals) @api.multi def write(self, vals): image_resize_images(vals) vals = self._check_journal_id(vals) return super(PaymentAcquirer, self).write(vals) def _check_journal_id(self, vals): if not vals.get('journal_id', False): default_journal = self.env['account.journal'].search( [('type', '=', 'bank')], limit=1) if default_journal: vals.update({'journal_id': default_journal.id}) return vals @api.multi def toggle_website_published(self): self.write({'website_published': not self.website_published}) return True @api.multi def get_form_action_url(self): """ Returns the form action URL, for form-based acquirer implementations. """ if hasattr(self, '%s_get_form_action_url' % self.provider): return getattr(self, '%s_get_form_action_url' % self.provider)() return False @api.multi def render(self, reference, amount, currency_id, partner_id=False, values=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 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: Odoo context """ if values is None: values = {} # 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.env['res.currency'].browse(currency_id) else: currency = self.env.user.company_id.currency_id values['currency'] = currency # Fill partner_* using values['partner_id'] or partner_id argument partner_id = values.get('partner_id', partner_id) billing_partner_id = values.get('billing_partner_id', partner_id) if partner_id: partner = self.env['res.partner'].browse(partner_id) if partner_id != billing_partner_id: billing_partner = self.env['res.partner'].browse( billing_partner_id) else: billing_partner = partner 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, 'billing_partner': billing_partner, 'billing_partner_id': billing_partner_id, 'billing_partner_name': billing_partner.name, 'billing_partner_lang': billing_partner.lang, 'billing_partner_email': billing_partner.email, 'billing_partner_zip': billing_partner.zip, 'billing_partner_city': billing_partner.city, 'billing_partner_address': _partner_format_address(billing_partner.street, billing_partner.street2), 'billing_partner_country_id': billing_partner.country_id.id, 'billing_partner_country': billing_partner.country_id, 'billing_partner_phone': billing_partner.phone, 'billing_partner_state': billing_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], }) if values.get('billing_partner_name'): values.update({ 'billing_partner_first_name': _partner_split_name(values.get('billing_partner_name'))[0], 'billing_partner_last_name': _partner_split_name(values.get('billing_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.env['res.country'].browse( values.get('partner_country_id')) if not values.get('billing_partner_address'): values['billing_address'] = _partner_format_address( values.get('billing_partner_street', ''), values.get('billing_partner_street2', '')) if not values.get('billing_partner_country') and values.get( 'billing_partner_country_id'): values['billing_country'] = self.env['res.country'].browse( values.get('billing_partner_country_id')) # compute fees fees_method_name = '%s_compute_fees' % self.provider if hasattr(self, fees_method_name): fees = getattr(self, fees_method_name)(values['amount'], values['currency_id'], values.get('partner_country_id')) 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' % (self.provider) if hasattr(self, cust_method_name): method = getattr(self, cust_method_name) values = method(values) values.update({ 'tx_url': self._context.get('tx_url', self.get_form_action_url()), 'submit_class': self._context.get('submit_class', 'btn btn-link'), 'submit_txt': self._context.get('submit_txt'), 'acquirer': self, 'user': self.env.user, 'context': self._context, 'type': values.get('type') or 'form', }) values.setdefault('return_url', False) return self.view_template_id.render(values, engine='ir.qweb') @api.multi def _registration_render(self, partner_id, qweb_context=None): if qweb_context is None: qweb_context = {} qweb_context.update(id=self.ids[0], partner_id=partner_id) method_name = '_%s_registration_form_generate_values' % ( self.provider, ) if hasattr(self, method_name): method = getattr(self, method_name) qweb_context.update(method(qweb_context)) return self.registration_view_template_id.render(qweb_context, engine='ir.qweb') @api.multi def s2s_process(self, data): cust_method_name = '%s_s2s_form_process' % (self.provider) if not self.s2s_validate(data): return False if hasattr(self, cust_method_name): method = getattr(self, cust_method_name) return method(data) return True @api.multi def s2s_validate(self, data): cust_method_name = '%s_s2s_form_validate' % (self.provider) if hasattr(self, cust_method_name): method = getattr(self, cust_method_name) return method(data) return True @api.multi def toggle_environment_value(self): prod = self.filtered(lambda acquirer: acquirer.environment == 'prod') prod.write({'environment': 'test'}) (self - prod).write({'environment': 'prod'}) @api.multi def button_immediate_install(self): # TDE FIXME: remove that brol if self.module_id and self.module_state != 'installed': self.module_id.button_immediate_install() context = dict(self._context, active_id=self.ids[0]) return { 'view_type': 'form', 'view_mode': 'form', 'res_model': 'payment.acquirer', 'type': 'ir.actions.act_window', 'res_id': self.ids[0], 'context': context, }
class Movimiento(models.Model): _name = "sa.movimiento" _description = "Movimiento" _inherit = "mail.thread" name = fields.Char(string="Nombre", required=True) type_move = fields.Selection(selection=[("ingreso", "Ingreso"), ("gasto", "Gasto")], string="Tipo de movimiento", default="ingreso", required=True, track_visibility="onchange") date = fields.Datetime(string="Fecha") amount = fields.Float("Monto", track_visibility="onchange") receipt_image = fields.Binary("Foto del recibo") notas = fields.Html("Notas") currency_id = fields.Many2one("res.currency", default=44) user_id = fields.Many2one("res.users", string="Usuario", default=lambda self: self.env.user.id) category_id = fields.Many2one("sa.category", "Categoria") tag_id = fields.Many2many("sa.tag", "sa_mov_sa_tag_rel", "move_id", "tag_id") email = fields.Char(related="user_id.email", string="Correo electrònico") @api.constrains("amount") def _check_amount(self): if not (self.amount >= 0 and self.amount <= 100000): raise ValidationError("El monto debe encontrarse entre 0 y 100000") @api.onchange("type_move") def onchange_type_move(self): if (self.type_move == "ingreso"): self.name = "Ingreso: " elif self.type_move == "gasto": self.name = "Egreso" @api.model def create(self, vals): name = vals.get("name", "-") amount = vals.get("amount", "0") type_move = vals.get("type_move", "") date = vals.get("date", "") user = self.env.user count_mov = user.count_movimientos if count_mov >= 5 and user.has_group("saldo_app.res_groups_user_free"): raise ValidationError("Solo puedes crear 5 movimientos por mes") notas = """ <p>Tipo de movimiento: {}</p> <p>Nombre: {}</p> <p>Monto: {}</p> <p>Fecha: {}</p> """ vals["notas"] = notas.format(type_move, name, amount, date) return super(Movimiento, self).create(vals) def unlink(self): for record in self: if record.amount >= 50: raise ValidationError( "Movimientos con montos mayores a 50 no pueden ser eliminados" ) return super(Movimiento, self).unlink()
class ResCompany(models.Model): _inherit = 'res.company' invoice_terms = fields.Html(string='Default Terms and Conditions', translate=True)
class DisciplinaryAction(models.Model): _name = 'disciplinary.action' _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Disciplinary Action" state = fields.Selection([ ('draft', 'Draft'), ('explain', 'Waiting Explanation'), ('submitted', 'Waiting Action'), ('action', 'Action Validated'), ('cancel', 'Cancelled'), ], default='draft', track_visibility='onchange') name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default=lambda self: _('New')) employee_name = fields.Many2one('hr.employee', string='Employee', required=True) department_name = fields.Many2one('hr.department', string='Department', required=True) discipline_reason = fields.Many2one('discipline.category', string='Reason', required=True) explanation = fields.Text(string="Explanation by Employee", help='Employee have to give Explanation' 'to manager about the violation of discipline') action = fields.Many2one('action.category', string="Action") read_only = fields.Boolean(compute="get_user", default=True) warning_letter = fields.Html(string="Warning Letter") suspension_letter = fields.Html(string="Suspension Letter") termination_letter = fields.Html(string="Termination Letter") warning = fields.Integer(default=False) action_details = fields.Text(string="Action Details") attachment_ids = fields.Many2many( 'ir.attachment', string="Attachments", help= "Employee can submit any documents which supports their explanation") note = fields.Text(string="Internal Note") joined_date = fields.Date(string="Joined Date") # assigning the sequence for the record @api.model def create(self, vals): vals['name'] = self.env['ir.sequence'].next_by_code( 'disciplinary.action') return super(DisciplinaryAction, self).create(vals) # Check the user is a manager or employee @api.depends('read_only') def get_user(self): res_user = self.env['res.users'].search([('id', '=', self._uid)]) if res_user.has_group('hr.group_hr_manager'): self.read_only = True else: self.read_only = False print(self.read_only) # Check the Action Selected @api.onchange('action') def onchange_action(self): if self.action.name == 'Written Warning': self.warning = 1 elif self.action.name == 'Suspend the Employee for one Week': self.warning = 2 elif self.action.name == 'Terminate the Employee': self.warning = 3 elif self.action.name == 'No Action': self.warning = 4 else: self.warning = 5 @api.onchange('employee_name') @api.depends('employee_name') def onchange_employee_name(self): department = self.env['hr.employee'].search([ ('name', '=', self.employee_name.name) ]) self.department_name = department.department_id.id if self.state == 'action': raise ValidationError(_('You Can not edit a Validated Action !!')) @api.onchange('discipline_reason') @api.depends('discipline_reason') def onchange_reason(self): if self.state == 'action': raise ValidationError(_('You Can not edit a Validated Action !!')) @api.multi def assign_function(self): for rec in self: rec.state = 'explain' @api.multi def cancel_function(self): for rec in self: rec.state = 'cancel' @api.multi def set_to_function(self): for rec in self: rec.state = 'draft' @api.multi def action_function(self): for rec in self: if not rec.action: raise ValidationError(_('You have to select an Action !!')) if self.warning == 1: if not rec.warning_letter or rec.warning_letter == '<p><br></p>': raise ValidationError( _('You have to fill up the Warning Letter in Action Information !!' )) elif self.warning == 2: if not rec.suspension_letter or rec.suspension_letter == '<p><br></p>': raise ValidationError( _('You have to fill up the Suspension Letter in Action Information !!' )) elif self.warning == 3: if not rec.termination_letter or rec.termination_letter == '<p><br></p>': raise ValidationError( _('You have to fill up the Termination Letter in Action Information !!' )) elif self.warning == 4: self.action_details = "No Action Proceed" elif self.warning == 5: if not rec.action_details: raise ValidationError( _('You have to fill up the Action Information !!')) rec.state = 'action' @api.multi def explanation_function(self): for rec in self: if not rec.explanation: raise ValidationError(_('You must give an explanation !!')) if len(self.explanation.split()) < 5: raise ValidationError( _('Your explanation must contain at least 5 words !!')) self.write({'state': 'submitted'})
class ms_pendaftaran(models.Model): _name = "ms.pendaftaran" _description = "Pendaftaran" _order = "name desc" name = fields.Char(string='Pendaftaran', default='/') state = fields.Selection([('draft', 'Draft'), ('confirm', 'Confirmed'), ('cancel', 'Cancelled')], string='State', default='draft') pasien_id = fields.Many2one('res.partner', domain=[('pasien', '=', True)], string='Pasien') poli_id = fields.Many2one('ms.poli', string='Poli yang Dituju') tanggal = fields.Datetime(string='Tanggal', default=fields.Datetime.now()) note = fields.Html(string='Note') @api.model def create(self, vals): vals['name'] = self.env['ir.sequence'].get_sequence( 'Pendaftaran', 'ms.pendaftaran', 'DFT/%(y)s/', 5) return super(ms_pendaftaran, self).create(vals) @api.multi def name_get(self): result = [] for me_id in self: result.append( (me_id.id, "%s - %s" % (me_id.name, me_id.pasien_id.name))) return result @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] if name: recs = self.search([ '|', ('pasien_id.name', operator, name), ('name', operator, name), ] + args, limit=limit) else: recs = self.search([] + args, limit=limit) return recs.name_get() @api.multi def action_confirm(self): for me_id in self: if me_id.state == 'draft': self.env['ms.pemeriksaan'].create({'pendaftaran_id': me_id.id}) me_id.write({'state': 'confirm'}) @api.multi def action_cancel(self): for me_id in self: pemeriksaan_ids = self.env['ms.pemeriksaan'].search([ ('pendaftaran_id', '=', me_id.id), ('state', '!=', 'cancel') ]) if pemeriksaan_ids: pemeriksaan_names = [ unicodedata.normalize('NFKD', pemeriksaan.name).encode( 'ascii', 'ignore') for pemeriksaan in pemeriksaan_ids ] raise Warning( "Silahkan cancel pemeriksaan %s terlebih dahulu !" % pemeriksaan_names) me_id.write({'state': 'cancel'}) @api.multi def unlink(self): for me_id in self: if me_id.state != 'draft': raise Warning( "Tidak bisa menghapus data pendaftaran yang bukan draft !") return super(ms_pendaftaran, self).unlink()
class Slide(models.Model): _name = 'slide.slide' _inherit = [ 'mail.thread', 'rating.mixin', 'image.mixin', 'website.seo.metadata', 'website.published.mixin' ] _description = 'Slides' _mail_post_access = 'read' _order_by_strategy = { 'sequence': 'category_sequence asc, sequence asc', 'most_viewed': 'total_views desc', 'most_voted': 'likes desc', 'latest': 'date_published desc', } _order = 'category_sequence asc, sequence asc' def _default_access_token(self): return str(uuid.uuid4()) # description name = fields.Char('Title', required=True, translate=True) active = fields.Boolean(default=True) sequence = fields.Integer('Sequence', default=10) category_sequence = fields.Integer('Category sequence', related="category_id.sequence", store=True) user_id = fields.Many2one('res.users', string='Uploaded by', default=lambda self: self.env.uid) description = fields.Text('Description', translate=True) channel_id = fields.Many2one('slide.channel', string="Channel", required=True) category_id = fields.Many2one('slide.category', string="Category", domain="[('channel_id', '=', channel_id)]") tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags') access_token = fields.Char("Security Token", copy=False, default=_default_access_token) is_preview = fields.Boolean( 'Is Preview', default=False, help= "The course is accessible by anyone : the users don't need to join the channel to access the content of the course." ) completion_time = fields.Float('# Hours', default=1, digits=(10, 4)) # subscribers partner_ids = fields.Many2many('res.partner', 'slide_slide_partner', 'slide_id', 'partner_id', string='Subscribers', groups='website.group_website_publisher') slide_partner_ids = fields.One2many( 'slide.slide.partner', 'slide_id', string='Subscribers information', groups='website.group_website_publisher') user_membership_id = fields.Many2one( 'slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id', help="Subscriber information for the current logged in user") # Quiz related fields question_ids = fields.One2many("slide.question", "slide_id", string="Questions") quiz_first_attempt_reward = fields.Integer("First attempt reward", default=10) quiz_second_attempt_reward = fields.Integer("Second attempt reward", default=7) quiz_third_attempt_reward = fields.Integer( "Third attempt reward", default=5, ) quiz_fourth_attempt_reward = fields.Integer( "Reward for every attempt after the third try", default=2) # content slide_type = fields.Selection( [('infographic', 'Infographic'), ('webpage', 'Web Page'), ('presentation', 'Presentation'), ('document', 'Document'), ('video', 'Video'), ('quiz', "Quiz")], string='Type', required=True, default='document', help= "The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document)." ) index_content = fields.Text('Transcript') datas = fields.Binary('Content', attachment=True) url = fields.Char('Document URL', help="Youtube or Google Document URL") document_id = fields.Char('Document ID', help="Youtube or Google Document ID") link_ids = fields.One2many('slide.slide.link', 'slide_id', string="External URL for this slide") mime_type = fields.Char('Mime-type') html_content = fields.Html( "HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True) # website website_id = fields.Many2one(related='channel_id.website_id', readonly=True) date_published = fields.Datetime('Publish Date') likes = fields.Integer('Likes', compute='_compute_user_info', store=True) dislikes = fields.Integer('Dislikes', compute='_compute_user_info', store=True) user_vote = fields.Integer('User vote', compute='_compute_user_info') embed_code = fields.Text('Embed Code', readonly=True, compute='_compute_embed_code') # views embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count") slide_views = fields.Integer('# of Website Views', store=True, compute="_compute_slide_views") public_views = fields.Integer('# of Public Views') total_views = fields.Integer("Total # Views", default="0", compute='_compute_total', store=True) _sql_constraints = [( 'exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both." )] @api.depends('slide_views', 'public_views') def _compute_total(self): for record in self: record.total_views = record.slide_views + record.public_views @api.depends('slide_partner_ids.vote') def _compute_user_info(self): slide_data = dict.fromkeys( self.ids, dict({ 'likes': 0, 'dislikes': 0, 'user_vote': False })) slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids) ]) for slide_partner in slide_partners: if slide_partner.vote == 1: slide_data[slide_partner.slide_id.id]['likes'] += 1 if slide_partner.partner_id == self.env.user.partner_id: slide_data[slide_partner.slide_id.id]['user_vote'] = 1 elif slide_partner.vote == -1: slide_data[slide_partner.slide_id.id]['dislikes'] += 1 if slide_partner.partner_id == self.env.user.partner_id: slide_data[slide_partner.slide_id.id]['user_vote'] = -1 for slide in self: slide.update(slide_data[slide.id]) @api.depends('slide_partner_ids.slide_id') def _compute_slide_views(self): # TODO awa: tried compute_sudo, for some reason it doesn't work in here... read_group_res = self.env['slide.slide.partner'].sudo().read_group( [('slide_id', 'in', self.ids)], ['slide_id'], groupby=['slide_id']) mapped_data = dict((res['slide_id'][0], res['slide_id_count']) for res in read_group_res) for slide in self: slide.slide_views = mapped_data.get(slide.id, 0) @api.depends('slide_partner_ids.partner_id') def _compute_user_membership_id(self): slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id), ]) for record in self: record.user_membership_id = next( (slide_partner for slide_partner in slide_partners if slide_partner.slide_id == record), self.env['slide.slide.partner']) @api.depends('document_id', 'slide_type', 'mime_type') def _compute_embed_code(self): base_url = request and request.httprequest.url_root or self.env[ 'ir.config_parameter'].sudo().get_param('web.base.url') if base_url[-1] == '/': base_url = base_url[:-1] for record in self: if record.datas and (not record.document_id or record.slide_type in ['document', 'presentation']): slide_url = base_url + url_for( '/slides/embed/%s?page=1' % record.id) record.embed_code = '<iframe src="%s" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % ( slide_url, 315, 420) elif record.slide_type == 'video' and record.document_id: if not record.mime_type: # embed youtube video record.embed_code = '<iframe src="//www.youtube.com/embed/%s?theme=light" allowFullScreen="true" frameborder="0"></iframe>' % ( record.document_id) else: # embed google doc video record.embed_code = '<iframe src="//drive.google.com/file/d/%s/preview" allowFullScreen="true" frameborder="0"></iframe>' % ( record.document_id) else: record.embed_code = False @api.onchange('url') def _on_change_url(self): self.ensure_one() if self.url: res = self._parse_document_url(self.url) if res.get('error'): raise Warning( _('Could not fetch data from url. Document or access right not available:\n%s' ) % res['error']) values = res['values'] if not values.get('document_id'): raise Warning( _('Please enter valid Youtube or Google Doc URL')) for key, value in values.items(): self[key] = value @api.depends('name', 'channel_id.website_id.domain') def _compute_website_url(self): # TDE FIXME: clena this link.tracker strange stuff super(Slide, self)._compute_website_url() for slide in self: if slide.id: # avoid to perform a slug on a not yet saved record in case of an onchange. base_url = slide.channel_id.get_base_url() # link_tracker is not in dependencies, so use it to shorten url only if installed. if self.env.registry.get('link.tracker'): url = self.env['link.tracker'].sudo().create({ 'url': '%s/slides/slide/%s' % (base_url, slug(slide)), 'title': slide.name, }).short_url else: url = '%s/slides/slide/%s' % (base_url, slug(slide)) slide.website_url = url @api.depends('channel_id.can_publish') def _compute_can_publish(self): for record in self: record.can_publish = record.channel_id.can_publish @api.model def _get_can_publish_error_message(self): return _( "Publishing is restricted to the responsible of training courses or members of the publisher group for documentation courses" ) # --------------------------------------------------------- # ORM Overrides # --------------------------------------------------------- @api.model def create(self, values): # Do not publish slide if user has not publisher rights channel = self.env['slide.channel'].browse(values['channel_id']) if not channel.can_publish: # 'website_published' is handled by mixin values['date_published'] = False if not values.get('index_content'): values['index_content'] = values.get('description') if values.get( 'slide_type') == 'infographic' and not values.get('image'): values['image'] = values['datas'] if values.get( 'website_published') and not values.get('date_published'): values['date_published'] = datetime.datetime.now() if values.get('url') and not values.get('document_id'): doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.items(): values.setdefault(key, value) slide = super(Slide, self).create(values) if slide.website_published: slide._post_publication() return slide def write(self, values): if values.get('url') and values['url'] != self.url: doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.items(): values.setdefault(key, value) res = super(Slide, self).write(values) if values.get('website_published'): self.date_published = datetime.datetime.now() self._post_publication() return res # --------------------------------------------------------- # Mail/Rating # --------------------------------------------------------- @api.returns('mail.message', lambda value: value.id) def message_post(self, message_type='notification', **kwargs): self.ensure_one() if message_type == 'comment' and not self.channel_id.can_comment: # user comments have a restriction on karma raise KarmaError(_('Not enough karma to comment')) return super(Slide, self).message_post(message_type=message_type, **kwargs) def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to website if it is published. """ self.ensure_one() if self.website_published: return { 'type': 'ir.actions.act_url', 'url': '%s' % self.website_url, 'target': 'self', 'target_type': 'public', 'res_id': self.id, } return super(Slide, self).get_access_action(access_uid) def _notify_get_groups(self): """ Add access button to everyone if the document is active. """ groups = super(Slide, self)._notify_get_groups() if self.website_published: for group_name, group_method, group_data in groups: group_data['has_button_access'] = True return groups # --------------------------------------------------------- # Business Methods # --------------------------------------------------------- def _post_publication(self): base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for slide in self.filtered(lambda slide: slide.website_published and slide.channel_id.publish_template_id): publish_template = slide.channel_id.publish_template_id html_body = publish_template.with_context( base_url=base_url)._render_template(publish_template.body_html, 'slide.slide', slide.id) subject = publish_template._render_template( publish_template.subject, 'slide.slide', slide.id) slide.channel_id.with_context( mail_create_nosubscribe=True).message_post( subject=subject, body=html_body, subtype='website_slides.mt_channel_slide_published', email_layout_xmlid='mail.mail_notification_light', ) return True def _generate_signed_token(self, partner_id): """ Lazy generate the acces_token and return it signed by the given partner_id :rtype tuple (string, int) :return (signed_token, partner_id) """ if not self.access_token: self.write({'access_token': self._default_access_token()}) return self._sign_token(partner_id) def _send_share_email(self, email): # TDE FIXME: template to check mail_ids = [] base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for record in self: if self.env.user.has_group('base.group_portal'): mail_ids.append( self.channel_id.share_template_id.with_context( user=self.env.user, email=email, base_url=base_url).sudo().send_mail( record.id, notif_layout='mail.mail_notification_light', email_values={ 'email_from': self.env['res.company'].catchall or self.env['res.company'].email })) else: mail_ids.append( self.channel_id.share_template_id.with_context( user=self.env.user, email=email, base_url=base_url).send_mail( record.id, notif_layout='mail.mail_notification_light')) return mail_ids def action_like(self): self.check_access_rights('read') self.check_access_rule('read') return self._action_vote(upvote=True) def action_dislike(self): self.check_access_rights('read') self.check_access_rule('read') return self._action_vote(upvote=False) def _action_vote(self, upvote=True): """ Private implementation of voting. It does not check for any real access rights; public methods should grant access before calling this method. :param upvote: if True, is a like; if False, is a dislike """ self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() slide_partners = SlidePartnerSudo.search([ ('slide_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id) ]) slide_id = slide_partners.mapped('slide_id') new_slides = self_sudo - slide_id channel = slide_id.channel_id karma_to_add = 0 for slide_partner in slide_partners: if upvote: new_vote = 0 if slide_partner.vote == -1 else 1 if slide_partner.vote != 1: karma_to_add += channel.karma_gen_slide_vote else: new_vote = 0 if slide_partner.vote == 1 else -1 if slide_partner.vote != -1: karma_to_add -= channel.karma_gen_slide_vote slide_partner.vote = new_vote for new_slide in new_slides: new_vote = 1 if upvote else -1 new_slide.write({ 'slide_partner_ids': [(0, 0, { 'vote': new_vote, 'partner_id': self.env.user.partner_id.id })] }) karma_to_add += new_slide.channel_id.karma_gen_slide_vote * ( 1 if upvote else -1) if karma_to_add: self.env.user.add_karma(karma_to_add) def action_set_viewed(self, quiz_attempts_inc=False): if not all(slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide as viewed if you are not among its members.' )) return bool( self._action_set_viewed(self.env.user.partner_id, quiz_attempts_inc=quiz_attempts_inc)) def _action_set_viewed(self, target_partner, quiz_attempts_inc=False): self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id)]) if quiz_attempts_inc: for exsting_slide in existing_sudo: exsting_slide.write({ 'quiz_attempts_count': exsting_slide.quiz_attempts_count + 1 }) new_slides = self_sudo - existing_sudo.mapped('slide_id') return SlidePartnerSudo.create([{ 'slide_id': new_slide.id, 'channel_id': new_slide.channel_id.id, 'partner_id': target_partner.id, 'quiz_attempts_count': 1 if quiz_attempts_inc else 0, 'vote': 0 } for new_slide in new_slides]) def action_set_completed(self): if not all(slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide as completed if you are not among its members.' )) return self._action_set_completed(self.env.user.partner_id) def _action_set_completed(self, target_partner): self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id)]) existing_sudo.write({'completed': True}) new_slides = self_sudo - existing_sudo.mapped('slide_id') SlidePartnerSudo.create([{ 'slide_id': new_slide.id, 'channel_id': new_slide.channel_id.id, 'partner_id': target_partner.id, 'vote': 0, 'completed': True } for new_slide in new_slides]) return True def _action_set_quiz_done(self): if not all(slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide quiz as completed if you are not among its members.' )) points = 0 for slide in self: user_membership_sudo = slide.user_membership_id.sudo() if not user_membership_sudo or user_membership_sudo.completed or not user_membership_sudo.quiz_attempts_count: continue gains = [ slide.quiz_first_attempt_reward, slide.quiz_second_attempt_reward, slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward ] points += gains[ user_membership_sudo.quiz_attempts_count - 1] if user_membership_sudo.quiz_attempts_count <= len( gains) else gains[-1] return self.env.user.sudo().add_karma(points) def _compute_quiz_info(self, target_partner, quiz_done=False): result = dict.fromkeys(self.ids, False) slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id) ]) slide_partners_map = dict( (sp.slide_id.id, sp) for sp in slide_partners) for slide in self: if not slide.question_ids: gains = [0] else: gains = [ slide.quiz_first_attempt_reward, slide.quiz_second_attempt_reward, slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward ] result[slide.id] = { 'quiz_karma_max': gains[0], # what could be gained if succeed at first try 'quiz_karma_gain': gains[0], # what would be gained at next test 'quiz_karma_won': 0, # what has been gained 'quiz_attempts_count': 0, # number of attempts } slide_partner = slide_partners_map.get(slide.id) if slide.question_ids and slide_partner: if slide_partner.quiz_attempts_count: result[slide.id]['quiz_karma_gain'] = gains[ slide_partner. quiz_attempts_count] if slide_partner.quiz_attempts_count < len( gains) else gains[-1] result[slide.id][ 'quiz_attempts_count'] = slide_partner.quiz_attempts_count if quiz_done or slide_partner.completed: result[slide.id]['quiz_karma_won'] = gains[ slide_partner.quiz_attempts_count - 1] if slide_partner.quiz_attempts_count < len( gains) else gains[-1] return result # -------------------------------------------------- # Parsing methods # -------------------------------------------------- @api.model def _fetch_data(self, base_url, data, content_type=False, extra_params=False): result = {'values': dict()} try: response = requests.get(base_url, params=data) response.raise_for_status() if content_type == 'json': result['values'] = response.json() elif content_type in ('image', 'pdf'): result['values'] = base64.b64encode(response.content) else: result['values'] = response.content except requests.exceptions.HTTPError as e: result['error'] = e.response.content except requests.exceptions.ConnectionError as e: result['error'] = str(e) return result def _find_document_data_from_url(self, url): url_obj = urls.url_parse(url) if url_obj.ascii_host == 'youtu.be': return ('youtube', url_obj.path[1:] if url_obj.path else False) elif url_obj.ascii_host in ('youtube.com', 'www.youtube.com', 'm.youtube.com'): v_query_value = url_obj.decode_query().get('v') if v_query_value: return ('youtube', v_query_value) split_path = url_obj.path.split('/') if len(split_path) >= 3 and split_path[1] in ('v', 'embed'): return ('youtube', split_path[2]) expr = re.compile( r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)' ) arg = expr.match(url) document_id = arg and arg.group(2) or False if document_id: return ('google', document_id) return (None, False) def _parse_document_url(self, url, only_preview_fields=False): document_source, document_id = self._find_document_data_from_url(url) if document_source and hasattr(self, '_parse_%s_document' % document_source): return getattr(self, '_parse_%s_document' % document_source)( document_id, only_preview_fields) return {'error': _('Unknown document')} def _parse_youtube_document(self, document_id, only_preview_fields): key = self.env['website'].get_current_website( ).website_slide_google_app_key fetch_res = self._fetch_data( 'https://www.googleapis.com/youtube/v3/videos', { 'id': document_id, 'key': key, 'part': 'snippet', 'fields': 'items(id,snippet)' }, 'json') if fetch_res.get('error'): return fetch_res values = {'slide_type': 'video', 'document_id': document_id} items = fetch_res['values'].get('items') if not items: return {'error': _('Please enter valid Youtube or Google Doc URL')} youtube_values = items[0] if youtube_values.get('snippet'): snippet = youtube_values['snippet'] if only_preview_fields: values.update({ 'url_src': snippet['thumbnails']['high']['url'], 'title': snippet['title'], 'description': snippet['description'] }) return values values.update({ 'name': snippet['title'], 'image': self._fetch_data(snippet['thumbnails']['high']['url'], {}, 'image')['values'], 'description': snippet['description'], 'mime_type': False, }) return {'values': values} @api.model def _parse_google_document(self, document_id, only_preview_fields): def get_slide_type(vals): # TDE FIXME: WTF ?? slide_type = 'presentation' if vals.get('image'): image = Image.open(io.BytesIO(base64.b64decode(vals['image']))) width, height = image.size if height > width: return 'document' return slide_type # Google drive doesn't use a simple API key to access the data, but requires an access # token. However, this token is generated in module google_drive, which is not in the # dependencies of website_slides. We still keep the 'key' parameter just in case, but that # is probably useless. params = {} params['projection'] = 'BASIC' if 'google.drive.config' in self.env: access_token = self.env['google.drive.config'].get_access_token() if access_token: params['access_token'] = access_token if not params.get('access_token'): params['key'] = self.env['website'].get_current_website( ).website_slide_google_app_key fetch_res = self._fetch_data( 'https://www.googleapis.com/drive/v2/files/%s' % document_id, params, "json") if fetch_res.get('error'): return fetch_res google_values = fetch_res['values'] if only_preview_fields: return { 'url_src': google_values['thumbnailLink'], 'title': google_values['title'], } values = { 'name': google_values['title'], 'image': self._fetch_data( google_values['thumbnailLink'].replace('=s220', ''), {}, 'image')['values'], 'mime_type': google_values['mimeType'], 'document_id': document_id, } if google_values['mimeType'].startswith('video/'): values['slide_type'] = 'video' elif google_values['mimeType'].startswith('image/'): values['datas'] = values['image'] values['slide_type'] = 'infographic' elif google_values['mimeType'].startswith( 'application/vnd.google-apps'): values['slide_type'] = get_slide_type(values) if 'exportLinks' in google_values: values['datas'] = self._fetch_data( google_values['exportLinks']['application/pdf'], params, 'pdf', extra_params=True)['values'] # Content indexing if google_values['exportLinks'].get('text/plain'): values['index_content'] = self._fetch_data( google_values['exportLinks']['text/plain'], params, extra_params=True)['values'] elif google_values['exportLinks'].get('text/csv'): values['index_content'] = self._fetch_data( google_values['exportLinks']['text/csv'], params, extra_params=True)['values'] elif google_values['mimeType'] == 'application/pdf': # TODO: Google Drive PDF document doesn't provide plain text transcript values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) return {'values': values} def _default_website_meta(self): res = super(Slide, self)._default_website_meta() res['default_opengraph']['og:title'] = res['default_twitter'][ 'twitter:title'] = self.name res['default_opengraph']['og:description'] = res['default_twitter'][ 'twitter:description'] = self.description res['default_opengraph']['og:image'] = res['default_twitter'][ 'twitter:image'] = "/web/image/slide.slide/%s/image" % (self.id) res['default_meta_description'] = self.description return res
class ProductPublicCategory(models.Model): _inherit = "product.public.category" website_description = fields.Html( sanitize_attributes=False, translate=html_translate )
class LastMovementProductReport(models.Model): _name = 'last.movement.product.report' _description = 'Last Movement Product Report' # uncomment if using tracking modules # _inherit = ['mail.thread', 'mail.activity.mixin'] _auto = False product_id = fields.Many2one('product.product', string="Product", readonly=True) last_purchase_id = fields.Many2one('purchase.order', string="Last Purchase", readonly=True,compute="_compute_last") last_sale_id = fields.Many2one('sale.order', string="Last Sale", readonly=True,compute="_compute_last") days_diff_po = fields.Integer("Days Diff PO", readonly=True) days_diff_so = fields.Integer("Days Diff SO", readonly=True) active = fields.Boolean("Active", readonly=True) product_tmpl_id = fields.Many2one('product.template', string="Product Templat", readonly=True) immediately_usable_qty = fields.Float(related="product_id.immediately_usable_qty") uom_id = fields.Many2one(related="product_id.uom_id") availability = fields.Html(string="Availability", compute="_compute_availability") def _item_availability(self, locs): self.ensure_one() statuses = [] Quant = self.env['stock.quant'] for loc in locs: product_context = self.with_context(location=loc.id).product_id # _logger.critical((self.product_id.name,product_context.qty_available, product_context.outgoing_qty, product_context.immediately_usable_qty)) prodquant = Quant.search([('location_id','=',loc.id), ('product_id','=',product_context.id)]) # _logger.warning(prodquant.mapped('reserved_quantity')) reserved = sum(prodquant.mapped('reserved_quantity')) stock = product_context.immediately_usable_qty - reserved if stock>0: statuses.append("%s = %s" % (loc.display_name, stock)) return "<br/>".join(statuses) def _compute_availability(self): locs = self.env['stock.location'].search([('usage','=','internal'),('scrap_location','=',False)]) for rec in self: item_availability = rec._item_availability(locs) rec.availability = item_availability # so_line_ids = fields.One2many('sale.order.line', 'product_id', string="Sale Item", domain=[('state','in',['done'])], readonly=True) def _compute_last(self): for rec in self: self.update({ 'last_sale_id':False, 'last_purchase_id':False }) def _select(self): query = """ WITH so AS ( select sol.id ,sol.order_id ,so.state ,so.date_order ,sol.product_id ,DATE_PART('day', now() - so.date_order) as days_diff FROM sale_order_line sol JOIN sale_order so on so.id = sol.order_id WHERE so.state in ('done') ), po AS ( select po.id ,po.date_order ,pol.product_id ,DATE_PART('day', now() - po.date_order) as days_diff FROM purchase_order_line AS pol JOIN purchase_order AS po ON po.id = pol.order_id WHERE po.state in ('done') ) SELECT p2.id ,p2.product_id ,p2.active ,p2.product_tmpl_id ,(CASE WHEN p2.days_diff_so IS NULL THEN 0 ELSE p2.days_diff_so END) AS days_diff_so ,(CASE WHEN p2.days_diff_po IS NULL THEN 0 ELSE p2.days_diff_po END) AS days_diff_po FROM ( SELECT pp.id ,pp.id AS product_id ,pp.product_tmpl_id as product_tmpl_id ,pp.active ,MIN(so.days_diff) as days_diff_so ,MIN(po.days_diff) as days_diff_po FROM product_product AS pp LEFT JOIN so ON so.product_id = pp.id LEFT JOIN po ON po.product_id = pp.id GROUP BY pp.id ,pp.product_tmpl_id ,pp.active ) AS p2 """ return query @api.model_cr def init(self): tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute("""CREATE or REPLACE VIEW %s as ( %s )""" % (self._table, self._select()))
class HouseTask(models.Model): _name = "house.task" _description = "House Task" DEFAULT_PYTHON_CODE = """# Available variables: # - env: Odoo Environment on which the action is triggered # - model: Odoo Model of the record on which the action is triggered; is a void recordset # - record: record on which the action is triggered; may be be void # - records: recordset of all records on which the action is triggered in multi-mode; may be void # - time, datetime, dateutil, timezone: useful Python libraries # - log: log(message, level='info'): logging function to record debug information in ir.logging table # - Warning: Warning Exception to use with raise # To return the next assigned user, assign: next = res.user record.\n\n\n\n""" name = fields.Char(required=True) description = fields.Html() active = fields.Boolean(default=True) user_ids = fields.Many2many( comodel_name="res.users", string="Users", required=True, ) period_min = fields.Integer( string="Days to Next", required=True, help="The minimum days until next turn is required.", ) period_max = fields.Integer( string="Days to Due", required=True, help="The number of days until next turn is considered late.", ) python_code = fields.Text( string='Python Code', groups='base.group_system', default=DEFAULT_PYTHON_CODE, help="Write Python code that will recide the next user assigned to" "the task.", ) code_prefix = fields.Char(string="Prefix for Task Turns", ) sequence_id = fields.Many2one( comodel_name="ir.sequence", string="Task Sequence", help="This field contains the information related to the numbering " "of the turns of this task.", copy=False, readonly=True, ) _sql_constraints = [ ('name_uniq', 'unique (name)', "Name must be unique"), ] @api.model def _prepare_ir_sequence(self, prefix): """Prepare the vals for creating the sequence :param prefix: a string with the prefix of the sequence. :return: a dict with the values. """ vals = { "name": "Housekeeping Task " + prefix, "code": "house.task.turn - " + prefix, "padding": 5, "prefix": prefix, "company_id": False, } return vals def write(self, vals): prefix = vals.get("code_prefix") if prefix: for rec in self: if rec.sequence_id: rec.sudo().sequence_id.prefix = prefix else: seq_vals = self._prepare_ir_sequence(prefix) rec.sequence_id = self.env["ir.sequence"].create(seq_vals) return super().write(vals) @api.model def create(self, vals): prefix = vals.get("code_prefix") if prefix: seq_vals = self._prepare_ir_sequence(prefix) sequence = self.env["ir.sequence"].create(seq_vals) vals["sequence_id"] = sequence.id return super().create(vals) def _evaluate_python_code(self): eval_ctx = {'rec': self, 'env': self.env} try: safe_eval(self.python_code, mode="exec", nocopy=True, globals_dict=eval_ctx) except Exception as error: raise UserError(_("Error evaluating python code.\n %s") % error) return eval_ctx.get('next') def generate(self): turn_model = self.env['house.task.turn'] for rec in self: pending = turn_model.search([('house_task_id', '=', rec.id), ('state', '=', 'pending')]) if pending: continue last = turn_model.search([('house_task_id', '=', rec.id), ('state', '=', 'done')], order='date_done desc', limit=1) if last and fields.Date.from_string(last.date_done) + timedelta( days=rec.period_min) > date.today(): continue assigned_to = rec._evaluate_python_code() if not assigned_to: continue turn_model.create({ 'house_task_id': rec.id, 'date_due': fields.Date.from_string(last.date_done) + timedelta(days=rec.period_max), 'user_id': assigned_to.id, })
class FleetVehicle(models.Model): _inherit = ['mail.thread', 'mail.activity.mixin'] _name = 'fleet.vehicle' _description = 'Vehicle' _order = 'license_plate asc, acquisition_date asc' def _get_default_state(self): state = self.env.ref('fleet.fleet_vehicle_state_registered', raise_if_not_found=False) return state if state and state.id else False name = fields.Char(compute="_compute_vehicle_name", store=True) description = fields.Html("Vehicle Description", help="Add a note about this vehicle") active = fields.Boolean('Active', default=True, tracking=True) manager_id = fields.Many2one( 'res.users', 'Fleet Manager', compute='_compute_manager_id', store=True, readonly=False, domain=lambda self: [('groups_id', 'in', self.env.ref('fleet.fleet_group_manager').id)], ) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env.company, ) currency_id = fields.Many2one('res.currency', related='company_id.currency_id') country_id = fields.Many2one('res.country', related='company_id.country_id') country_code = fields.Char(related='country_id.code') license_plate = fields.Char(tracking=True, help='License plate number of the vehicle (i = plate number for a car)') vin_sn = fields.Char('Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False) trailer_hook = fields.Boolean(default=False, string='Trailer Hitch', compute='_compute_model_fields', store=True, readonly=False) driver_id = fields.Many2one('res.partner', 'Driver', tracking=True, help='Driver address of the vehicle', copy=False) future_driver_id = fields.Many2one('res.partner', 'Future Driver', tracking=True, help='Next Driver Address of the vehicle', copy=False, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") model_id = fields.Many2one('fleet.vehicle.model', 'Model', tracking=True, required=True, help='Model of the vehicle') brand_id = fields.Many2one('fleet.vehicle.model.brand', 'Brand', related="model_id.brand_id", store=True, readonly=False) log_drivers = fields.One2many('fleet.vehicle.assignation.log', 'vehicle_id', string='Assignment Logs') log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs') log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts') contract_count = fields.Integer(compute="_compute_count_all", string='Contract Count') service_count = fields.Integer(compute="_compute_count_all", string='Services') odometer_count = fields.Integer(compute="_compute_count_all", string='Odometer') history_count = fields.Integer(compute="_compute_count_all", string="Drivers History Count") next_assignation_date = fields.Date('Assignment Date', help='This is the date at which the car will be available, if not set it means available instantly') acquisition_date = fields.Date('Immatriculation Date', required=False, default=fields.Date.today, help='Date when the vehicle has been immatriculated') write_off_date = fields.Date('Cancellation Date', tracking=True, help="Date when the vehicle's license plate has been cancelled/removed.") first_contract_date = fields.Date(string="First Contract Date", default=fields.Date.today) color = fields.Char(help='Color of the vehicle', compute='_compute_model_fields', store=True, readonly=False) state_id = fields.Many2one('fleet.vehicle.state', 'State', default=_get_default_state, group_expand='_read_group_stage_ids', tracking=True, help='Current state of the vehicle', ondelete="set null") location = fields.Char(help='Location of the vehicle (garage, ...)') seats = fields.Integer('Seats Number', help='Number of seats of the vehicle', compute='_compute_model_fields', store=True, readonly=False) model_year = fields.Char('Model Year', help='Year of the model', compute='_compute_model_fields', store=True, readonly=False) doors = fields.Integer('Doors Number', help='Number of doors of the vehicle', compute='_compute_model_fields', store=True, readonly=False) tag_ids = fields.Many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id', 'tag_id', 'Tags', copy=False) odometer = fields.Float(compute='_get_odometer', inverse='_set_odometer', string='Last Odometer', help='Odometer measure of the vehicle at the moment of this log') odometer_unit = fields.Selection([ ('kilometers', 'km'), ('miles', 'mi') ], 'Odometer Unit', default='kilometers', help='Unit of the odometer ', required=True) transmission = fields.Selection( [('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle', compute='_compute_model_fields', store=True, readonly=False) fuel_type = fields.Selection(FUEL_TYPES, 'Fuel Type', help='Fuel Used by the vehicle', compute='_compute_model_fields', store=True, readonly=False) horsepower = fields.Integer(compute='_compute_model_fields', store=True, readonly=False) horsepower_tax = fields.Float('Horsepower Taxation', compute='_compute_model_fields', store=True, readonly=False) power = fields.Integer('Power', help='Power in kW of the vehicle', compute='_compute_model_fields', store=True, readonly=False) co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle', compute='_compute_model_fields', store=True, readonly=False) co2_standard = fields.Char(compute='_compute_model_fields', store=True, readonly=False) image_128 = fields.Image(related='model_id.image_128', readonly=True) contract_renewal_due_soon = fields.Boolean(compute='_compute_contract_reminder', search='_search_contract_renewal_due_soon', string='Has Contracts to renew') contract_renewal_overdue = fields.Boolean(compute='_compute_contract_reminder', search='_search_get_overdue_contract_reminder', string='Has Contracts Overdue') contract_renewal_name = fields.Text(compute='_compute_contract_reminder', string='Name of contract to renew soon') contract_renewal_total = fields.Text(compute='_compute_contract_reminder', string='Total of contracts due or overdue minus one') contract_state = fields.Selection( [('futur', 'Incoming'), ('open', 'In Progress'), ('expired', 'Expired'), ('closed', 'Closed') ], string='Last Contract State', compute='_compute_contract_reminder', required=False) car_value = fields.Float(string="Catalog Value (VAT Incl.)", help='Value of the bought vehicle') net_car_value = fields.Float(string="Purchase Value", help="Purchase value of the vehicle") residual_value = fields.Float() plan_to_change_car = fields.Boolean(related='driver_id.plan_to_change_car', store=True, readonly=False) plan_to_change_bike = fields.Boolean(related='driver_id.plan_to_change_bike', store=True, readonly=False) vehicle_type = fields.Selection(related='model_id.vehicle_type') frame_type = fields.Selection([('diamant', 'Diamant'), ('trapez', 'Trapez'), ('wave', 'Wave')], help="Frame type of the bike") electric_assistance = fields.Boolean(compute='_compute_model_fields', store=True, readonly=False) frame_size = fields.Float() @api.depends('model_id') def _compute_model_fields(self): ''' Copies all the related fields from the model to the vehicle ''' model_values = dict() for vehicle in self.filtered('model_id'): if vehicle.model_id.id in model_values: write_vals = model_values[vehicle.model_id.id] else: # copy if value is truthy write_vals = {MODEL_FIELDS_TO_VEHICLE[key]: vehicle.model_id[key] for key in MODEL_FIELDS_TO_VEHICLE\ if vehicle.model_id[key]} model_values[vehicle.model_id.id] = write_vals vehicle.write(write_vals) @api.depends('model_id.brand_id.name', 'model_id.name', 'license_plate') def _compute_vehicle_name(self): for record in self: record.name = (record.model_id.brand_id.name or '') + '/' + (record.model_id.name or '') + '/' + (record.license_plate or _('No Plate')) def _get_odometer(self): FleetVehicalOdometer = self.env['fleet.vehicle.odometer'] for record in self: vehicle_odometer = FleetVehicalOdometer.search([('vehicle_id', '=', record.id)], limit=1, order='value desc') if vehicle_odometer: record.odometer = vehicle_odometer.value else: record.odometer = 0 def _set_odometer(self): for record in self: if record.odometer: date = fields.Date.context_today(record) data = {'value': record.odometer, 'date': date, 'vehicle_id': record.id} self.env['fleet.vehicle.odometer'].create(data) def _compute_count_all(self): Odometer = self.env['fleet.vehicle.odometer'] LogService = self.env['fleet.vehicle.log.services'] LogContract = self.env['fleet.vehicle.log.contract'] for record in self: record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)]) record.service_count = LogService.search_count([('vehicle_id', '=', record.id), ('active', '=', record.active)]) record.contract_count = LogContract.search_count([('vehicle_id', '=', record.id), ('state', '!=', 'closed'), ('active', '=', record.active)]) record.history_count = self.env['fleet.vehicle.assignation.log'].search_count([('vehicle_id', '=', record.id)]) @api.depends('log_contracts') def _compute_contract_reminder(self): params = self.env['ir.config_parameter'].sudo() delay_alert_contract = int(params.get_param('hr_fleet.delay_alert_contract', default=30)) for record in self: overdue = False due_soon = False total = 0 name = '' state = '' for element in record.log_contracts: if element.state in ('open', 'expired') and element.expiration_date: current_date_str = fields.Date.context_today(record) due_time_str = element.expiration_date current_date = fields.Date.from_string(current_date_str) due_time = fields.Date.from_string(due_time_str) diff_time = (due_time - current_date).days if diff_time < 0: overdue = True total += 1 if diff_time < delay_alert_contract: due_soon = True total += 1 if overdue or due_soon: log_contract = self.env['fleet.vehicle.log.contract'].search([ ('vehicle_id', '=', record.id), ('state', 'in', ('open', 'expired')) ], limit=1, order='expiration_date asc') if log_contract: # we display only the name of the oldest overdue/due soon contract name = log_contract.name state = log_contract.state record.contract_renewal_overdue = overdue record.contract_renewal_due_soon = due_soon record.contract_renewal_total = total - 1 # we remove 1 from the real total for display purposes record.contract_renewal_name = name record.contract_state = state def _get_analytic_name(self): # This function is used in fleet_account and is overrided in l10n_be_hr_payroll_fleet return self.license_plate or _('No plate') def _search_contract_renewal_due_soon(self, operator, value): params = self.env['ir.config_parameter'].sudo() delay_alert_contract = int(params.get_param('hr_fleet.delay_alert_contract', default=30)) res = [] assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported' if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False): search_operator = 'in' else: search_operator = 'not in' today = fields.Date.context_today(self) datetime_today = fields.Datetime.from_string(today) limit_date = fields.Datetime.to_string(datetime_today + relativedelta(days=+delay_alert_contract)) res_ids = self.env['fleet.vehicle.log.contract'].search([ ('expiration_date', '>', today), ('expiration_date', '<', limit_date), ('state', 'in', ['open', 'expired']) ]).mapped('id') res.append(('id', search_operator, res_ids)) return res def _search_get_overdue_contract_reminder(self, operator, value): res = [] assert operator in ('=', '!=', '<>') and value in (True, False), 'Operation not supported' if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False): search_operator = 'in' else: search_operator = 'not in' today = fields.Date.context_today(self) res_ids = self.env['fleet.vehicle.log.contract'].search([ ('expiration_date', '!=', False), ('expiration_date', '<', today), ('state', 'in', ['open', 'expired']) ]).mapped('id') res.append(('id', search_operator, res_ids)) return res @api.model def create(self, vals): # Fleet administrator may not have rights to create the plan_to_change_car value when the driver_id is a res.user # This trick is used to prevent access right error. ptc_value = 'plan_to_change_car' in vals.keys() and {'plan_to_change_car': vals.pop('plan_to_change_car')} res = super(FleetVehicle, self).create(vals) if ptc_value: res.sudo().write(ptc_value) if 'driver_id' in vals and vals['driver_id']: res.create_driver_history(vals) if 'future_driver_id' in vals and vals['future_driver_id']: state_waiting_list = self.env.ref('fleet.fleet_vehicle_state_waiting_list', raise_if_not_found=False) states = res.mapped('state_id').ids if not state_waiting_list or state_waiting_list.id not in states: future_driver = self.env['res.partner'].browse(vals['future_driver_id']) if self.vehicle_type == 'bike': future_driver.sudo().write({'plan_to_change_bike': True}) if self.vehicle_type == 'car': future_driver.sudo().write({'plan_to_change_car': True}) return res def write(self, vals): if 'driver_id' in vals and vals['driver_id']: driver_id = vals['driver_id'] for vehicle in self.filtered(lambda v: v.driver_id.id != driver_id): vehicle.create_driver_history(vals) if vehicle.driver_id: vehicle.activity_schedule( 'mail.mail_activity_data_todo', user_id=vehicle.manager_id.id or self.env.user.id, note=_('Specify the End date of %s') % vehicle.driver_id.name) if 'future_driver_id' in vals and vals['future_driver_id']: state_waiting_list = self.env.ref('fleet.fleet_vehicle_state_waiting_list', raise_if_not_found=False) states = self.mapped('state_id').ids if 'state_id' not in vals else [vals['state_id']] if not state_waiting_list or state_waiting_list.id not in states: future_driver = self.env['res.partner'].browse(vals['future_driver_id']) if self.vehicle_type == 'bike': future_driver.sudo().write({'plan_to_change_bike': True}) if self.vehicle_type == 'car': future_driver.sudo().write({'plan_to_change_car': True}) res = super(FleetVehicle, self).write(vals) return res def _get_driver_history_data(self, vals): self.ensure_one() return { 'vehicle_id': self.id, 'driver_id': vals['driver_id'], 'date_start': fields.Date.today(), } def create_driver_history(self, vals): for vehicle in self: self.env['fleet.vehicle.assignation.log'].create( vehicle._get_driver_history_data(vals), ) def action_accept_driver_change(self): # Find all the vehicles for which the driver is the future_driver_id # remove their driver_id and close their history using current date vehicles = self.search([('driver_id', 'in', self.mapped('future_driver_id').ids)]) vehicles.write({'driver_id': False}) for vehicle in self: if vehicle.vehicle_type == 'bike': vehicle.future_driver_id.sudo().write({'plan_to_change_bike': False}) if vehicle.vehicle_type == 'car': vehicle.future_driver_id.sudo().write({'plan_to_change_car': False}) vehicle.driver_id = vehicle.future_driver_id vehicle.future_driver_id = False def toggle_active(self): self.env['fleet.vehicle.log.contract'].with_context(active_test=False).search([('vehicle_id', 'in', self.ids)]).toggle_active() self.env['fleet.vehicle.log.services'].with_context(active_test=False).search([('vehicle_id', 'in', self.ids)]).toggle_active() super(FleetVehicle, self).toggle_active() @api.model def _read_group_stage_ids(self, stages, domain, order): return self.env['fleet.vehicle.state'].search([], order=order) @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): if 'co2' in fields: fields.remove('co2') return super(FleetVehicle, self).read_group(domain, fields, groupby, offset, limit, orderby, lazy) @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): args = args or [] if operator == 'ilike' and not (name or '').strip(): domain = [] else: domain = ['|', ('name', operator, name), ('driver_id.name', operator, name)] return self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid) def return_action_to_open(self): """ This opens the xml view specified in xml_id for the current vehicle """ self.ensure_one() xml_id = self.env.context.get('xml_id') if xml_id: res = self.env['ir.actions.act_window']._for_xml_id('fleet.%s' % xml_id) res.update( context=dict(self.env.context, default_vehicle_id=self.id, group_by=False), domain=[('vehicle_id', '=', self.id)] ) return res return False def act_show_log_cost(self): """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs @return: the costs log view """ self.ensure_one() copy_context = dict(self.env.context) copy_context.pop('group_by', None) res = self.env['ir.actions.act_window']._for_xml_id('fleet.fleet_vehicle_costs_action') res.update( context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True), domain=[('vehicle_id', '=', self.id)] ) return res def _track_subtype(self, init_values): self.ensure_one() if 'driver_id' in init_values: return self.env.ref('fleet.mt_fleet_driver_updated') return super(FleetVehicle, self)._track_subtype(init_values) def open_assignation_logs(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': 'Assignment Logs', 'view_mode': 'tree', 'res_model': 'fleet.vehicle.assignation.log', 'domain': [('vehicle_id', '=', self.id)], 'context': {'default_driver_id': self.driver_id.id, 'default_vehicle_id': self.id} }
class MailTemplate(models.Model): "Templates for sending email" _name = "mail.template" _description = 'Email Templates' _order = 'name' @api.model def default_get(self, fields): res = super(MailTemplate, self).default_get(fields) if res.get('model'): res['model_id'] = self.env['ir.model']._get(res.pop('model')).id return res name = fields.Char('Name') model_id = fields.Many2one( 'ir.model', 'Applies to', help="The type of document this template can be used with") model = fields.Char('Related Document Model', related='model_id.model', index=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)', oldname='email_recipients', help= "Comma-separated ids of recipient partners (placeholders may be used here)" ) 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) 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', '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', default=True, 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." ) scheduled_date = fields.Char( 'Scheduled Date', help= "If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible. Jinja2 placeholders may be used." ) @api.onchange('model_id') def onchange_model_id(self): # TDE CLEANME: should'nt it be a stored related ? if self.model_id: self.model = self.model_id.model else: self.model = False 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 @api.onchange('model_object_field', 'sub_model_object_field', 'null_value') def onchange_sub_model_object_value_field(self): if self.model_object_field: if self.model_object_field.ttype in [ 'many2one', 'one2many', 'many2many' ]: model = self.env['ir.model']._get( self.model_object_field.relation) if model: self.sub_object = model.id self.copyvalue = self.build_expression( self.model_object_field.name, self.sub_model_object_field and self.sub_model_object_field.name or False, self.null_value or False) else: self.sub_object = False self.sub_model_object_field = False self.copyvalue = self.build_expression( self.model_object_field.name, False, self.null_value or False) else: self.sub_object = False self.copyvalue = False self.sub_model_object_field = False self.null_value = False @api.multi def unlink(self): self.unlink_action() return super(MailTemplate, self).unlink() @api.multi def copy(self, default=None): default = dict(default or {}, name=_("%s (copy)") % self.name) return super(MailTemplate, self).copy(default=default) @api.multi def unlink_action(self): for template in self: if template.ref_ir_act_window: template.ref_ir_act_window.sudo().unlink() if template.ref_ir_value: template.ref_ir_value.sudo().unlink() return True @api.multi def create_action(self): ActWindowSudo = self.env['ir.actions.act_window'].sudo() IrValuesSudo = self.env['ir.values'].sudo() view = self.env.ref('mail.email_compose_message_wizard_form') for template in self: src_obj = template.model_id.model button_name = _('Send Mail (%s)') % template.name action = ActWindowSudo.create({ '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': view.id, 'target': 'new', }) ir_value = IrValuesSudo.create({ 'name': button_name, 'model': src_obj, 'key2': 'client_action_multi', 'value': "ir.actions.act_window,%s" % action.id }) template.write({ 'ref_ir_act_window': action.id, 'ref_ir_value': ir_value.id, }) return True # ---------------------------------------- # RENDERING # ---------------------------------------- @api.model def _replace_local_links(self, html): """ 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.env['ir.config_parameter'].sudo().get_param( 'web.base.url') base = urls.url_parse(base_url) def _process_link(url): new_url = urls.url_parse(url) if new_url.scheme and new_url.netloc: return url return new_url.replace(scheme=base.scheme, netloc=base.netloc).to_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 @api.model def render_post_process(self, html): html = self._replace_local_links(html) return html @api.model def render_template(self, template_txt, model, res_ids, 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``: Model of the current user - ``object``: record of the document record this mail is related to - ``context``: the context passed to the mail composition wizard :param str template_txt: 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. """ multi_mode = True if isinstance(res_ids, pycompat.integer_types): multi_mode = False res_ids = [res_ids] results = dict.fromkeys(res_ids, u"") # try to load the template try: mako_env = mako_safe_template_env if self.env.context.get( 'safe') else mako_template_env template = mako_env.from_string(tools.ustr(template_txt)) except Exception: _logger.info("Failed to load template %r", template_txt, exc_info=True) return multi_mode and results or results[res_ids[0]] # prepare template variables records = self.env[model].browse( it for it in res_ids if it) # filter to avoid browsing [None] res_to_rec = dict.fromkeys(res_ids, None) for record in records: res_to_rec[record.id] = record variables = { 'format_date': lambda date, format=False, context=self._context: format_date( self.env, date, format), 'format_tz': lambda dt, tz=False, format=False, context=self._context: format_tz(self.env, dt, tz, format), 'user': self.env.user, 'ctx': self._context, # context kw would clash with mako internals } for res_id, record in pycompat.items(res_to_rec): variables['object'] = record try: render_result = template.render(variables) except Exception: _logger.info("Failed to render template %r using values %r" % (template, variables), exc_info=True) raise UserError( _("Failed to render template %r using values %r") % (template, variables)) if render_result == u"False": render_result = u"" results[res_id] = render_result if post_process: for res_id, result in pycompat.items(results): results[res_id] = self.render_post_process(result) return multi_mode and results or results[res_ids[0]] @api.multi def get_email_template(self, res_ids): multi_mode = True if isinstance(res_ids, pycompat.integer_types): res_ids = [res_ids] multi_mode = False if res_ids is None: res_ids = [None] results = dict.fromkeys(res_ids, False) if not self.ids: return results self.ensure_one() langs = self.render_template(self.lang, self.model, res_ids) for res_id, lang in pycompat.items(langs): if lang: template = self.with_context(lang=lang) else: template = self results[res_id] = template return multi_mode and results or results[res_ids[0]] @api.multi def generate_recipients(self, results, res_ids): """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. """ self.ensure_one() if self.use_default_to or self._context.get('tpl_force_default_to'): default_recipients = self.env[ 'mail.thread'].message_get_default_recipients( res_model=self.model, res_ids=res_ids) for res_id, recipients in pycompat.items(default_recipients): results[res_id].pop('partner_to', None) results[res_id].update(recipients) for res_id, values in pycompat.items(results): partner_ids = values.get('partner_ids', list()) if self._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.env['res.partner'].find_or_create(mail) 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.env['res.partner'].sudo().browse( tpl_partner_ids).exists().ids results[res_id]['partner_ids'] = partner_ids return results @api.multi def generate_email(self, res_ids, 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. """ self.ensure_one() multi_mode = True if isinstance(res_ids, pycompat.integer_types): res_ids = [res_ids] multi_mode = False if fields is None: fields = [ 'subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'scheduled_date' ] res_ids_to_templates = self.get_email_template(res_ids) # templates: res_id -> template; template -> res_ids templates_to_res_ids = {} for res_id, template in pycompat.items(res_ids_to_templates): templates_to_res_ids.setdefault(template, []).append(res_id) results = dict() for template, template_res_ids in pycompat.items(templates_to_res_ids): Template = self.env['mail.template'] # generate fields value for all res_ids linked to the current template if template.lang: Template = Template.with_context( lang=template._context.get('lang')) for field in fields: Template = Template.with_context(safe=field in {'subject'}) generated_field_values = Template.render_template( getattr(template, field), template.model, template_res_ids, post_process=(field == 'body_html')) for res_id, field_value in pycompat.items( generated_field_values): results.setdefault(res_id, dict())[field] = field_value # compute recipients if any(field in fields for field in ['email_to', 'partner_to', 'email_cc']): results = template.generate_recipients(results, template_res_ids) # 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.env.user.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: for res_id in template_res_ids: attachments = [] report_name = self.render_template(template.report_name, template.model, res_id) report = template.report_template report_service = report.report_name if report.report_type not in ['qweb-html', 'qweb-pdf']: raise UserError( _('Unsupported report type %s found.') % report.report_type) result, format = report.render_qweb_pdf([res_id]) # 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 multi_mode and results or results[res_ids[0]] @api.multi def send_mail(self, res_id, force_send=False, raise_exception=False, email_values=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 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. :param dict email_values: if set, the generated mail.message is updated with given values dict :returns: id of the mail.message that was created """ self.ensure_one() Mail = self.env['mail.mail'] Attachment = self.env[ 'ir.attachment'] # TDE FIXME: should remove dfeault_type from context # create a mail_mail based on values, without attachments values = self.generate_email(res_id) values['recipient_ids'] = [ (4, pid) for pid in values.get('partner_ids', list()) ] values.update(email_values or {}) attachment_ids = values.pop('attachment_ids', []) attachments = values.pop('attachments', []) # add a protection against void email_from if 'email_from' in values and not values.get('email_from'): values.pop('email_from') mail = Mail.create(values) # 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, } attachment_ids.append(Attachment.create(attachment_data).id) if attachment_ids: values['attachment_ids'] = [(6, 0, attachment_ids)] mail.write({'attachment_ids': [(6, 0, attachment_ids)]}) if force_send: mail.send(raise_exception=raise_exception) return mail.id # TDE CLEANME: return mail + api.returns ?
class HrPlanActivityType(models.Model): _name = 'hr.plan.activity.type' _description = 'Plan activity type' _rec_name = 'summary' activity_type_id = fields.Many2one( 'mail.activity.type', 'Activity Type', default=lambda self: self.env.ref('mail.mail_activity_data_todo'), domain=lambda self: [ '|', ('res_model_id', '=', False), ('res_model_id', '=', self.env['ir.model']._get('hr.employee').id) ]) summary = fields.Char('Summary') responsible = fields.Selection([('coach', 'Coach'), ('manager', 'Manager'), ('employee', 'Employee'), ('other', 'Other')], default='employee', string='Responsible', required=True) responsible_id = fields.Many2one( 'res.users', 'Responsible Person', help='Specific responsible of activity if not linked to the employee.') note = fields.Html('Note') @api.onchange('activity_type_id') def _onchange_activity_type_id(self): if self.activity_type_id and self.activity_type_id.summary and not self.summary: self.summary = self.activity_type_id.summary def get_responsible_id(self, employee): if self.responsible == 'coach': if not employee.coach_id: raise UserError( _('Coach of employee %s is not set.') % employee.name) responsible = employee.coach_id.user_id if not responsible: raise UserError( _('User of coach of employee %s is not set.') % employee.name) elif self.responsible == 'manager': if not employee.parent_id: raise UserError( _('Manager of employee %s is not set.') % employee.name) responsible = employee.parent_id.user_id if not responsible: raise UserError( _('User of manager of employee %s is not set.') % employee.name) elif self.responsible == 'employee': responsible = employee.user_id if not responsible: raise UserError( _('User linked to employee %s is required.') % employee.name) elif self.responsible == 'other': responsible = self.responsible_id if not responsible: raise UserError( _('No specific user given on activity.') % employee.name) return responsible
class Users(models.Model): """ 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. """ _name = "res.users" _description = 'Users' _inherits = {'res.partner': 'partner_id'} _order = 'name, login' __uid_cache = defaultdict(dict) # {dbname: {uid: password}} # User can write on a few of his own fields (but not his groups for example) SELF_WRITEABLE_FIELDS = [ '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', 'action_id' ] def _default_groups(self): default_user = self.env.ref('base.default_user', raise_if_not_found=False) return (default_user or self.env['res.users']).groups_id def _companies_count(self): return self.env['res.company'].sudo().search_count([]) partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True, string='Related Partner', help='Partner-related data of the user') login = fields.Char(required=True, help="Used to log into the system") password = fields.Char( default='', 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.Char(string='Set Password', compute='_compute_password', inverse='_inverse_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() active = fields.Boolean(default=True) action_id = fields.Many2one( 'ir.actions.actions', string='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', string='Groups', default=_default_groups) log_ids = fields.One2many('res.users.log', 'create_uid', string='User log entries') login_date = fields.Datetime(related='log_ids.create_date', string='Latest connection') share = fields.Boolean( compute='_compute_share', compute_sudo=True, string='Share User', store=True, help= "External user with limited access, created only for the purpose of sharing data." ) companies_count = fields.Integer(compute='_compute_companies_count', string="Number of Companies", default=_companies_count) tz_offset = fields.Char(compute='_compute_tz_offset', string='Timezone offset', invisible=True) @api.model def _get_company(self): return self.env.user.company_id # 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', string='Company', required=True, default=_get_company, 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', string='Companies', default=_get_company) # overridden inherited fields to bypass access rights, in case you have # access to the user but not its corresponding partner name = fields.Char(related='partner_id.name', inherited=True) email = fields.Char(related='partner_id.email', inherited=True) _sql_constraints = [('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !')] def _compute_password(self): for user in self: user.password = '' def _inverse_password(self): for user in self: if not user.new_password: # Do not update the password if no value is provided, ignore silently. # For example web client submits False values for all empty fields. continue if user == self.env.user: # 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.' )) else: user.password = user.new_password @api.depends('groups_id') def _compute_share(self): for user in self: user.share = not user.has_group('base.group_user') @api.multi def _compute_companies_count(self): companies_count = self._companies_count() for user in self: user.companies_count = companies_count @api.depends('tz') def _compute_tz_offset(self): for user in self: user.tz_offset = datetime.datetime.now( pytz.timezone(user.tz or 'GMT')).strftime('%z') @api.onchange('login') def on_change_login(self): if self.login and tools.single_email_re.match(self.login): self.email = self.login @api.onchange('state_id') def onchange_state(self): return self.mapped('partner_id').onchange_state() @api.onchange('parent_id') def onchange_parent_id(self): return self.mapped('partner_id').onchange_parent_id() @api.multi @api.constrains('company_id', 'company_ids') def _check_company(self): if any(user.company_ids and user.company_id not in user.company_ids for user in self): raise ValidationError( _('The chosen company is not in the allowed companies for this user' )) @api.multi def read(self, fields=None, load='_classic_read'): if fields and self == self.env.user: 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 self = self.sudo() result = super(Users, self).read(fields=fields, load=load) canwrite = self.env['ir.model.access'].check('res.users', 'write', False) if not canwrite: def override_password(vals): if (vals['id'] != self._uid): for key in USER_PRIVATE_FIELDS: if key in vals: vals[key] = '********' return vals result = map(override_password, result) return result @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): groupby_fields = set( [groupby] if isinstance(groupby, basestring) else groupby) if groupby_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_("Invalid 'group by' parameter")) return super(Users, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): if self._uid != SUPERUSER_ID and args: domain_fields = { term[0] for term in args if isinstance(term, (tuple, list)) } if domain_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_('Invalid search criterion')) return super(Users, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) @api.model def create(self, vals): user = super(Users, self).create(vals) user.partner_id.active = user.active if user.partner_id.company_id: user.partner_id.write({'company_id': user.company_id.id}) return user @api.multi def write(self, values): if values.get('active') == False: for user in self: if user.id == SUPERUSER_ID: raise UserError(_("You cannot deactivate the admin user.")) elif user.id == self._uid: raise UserError( _("You cannot deactivate the user you're currently logged in as." )) if self == self.env.user: for key in values.keys(): if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')): break else: if 'company_id' in values: if values[ 'company_id'] not in self.env.user.company_ids.ids: del values['company_id'] # safe fields only, so we write as super-user to bypass access rights self = self.sudo() res = super(Users, self).write(values) if 'company_id' in values: for user in self: # if partner is global we keep it that way if user.partner_id.company_id.id != values['company_id']: user.partner_id.write({'company_id': user.company_id.id}) # clear default ir values when company changes self.env['ir.values'].get_defaults_dict.clear_cache( self.env['ir.values']) # clear caches linked to the users if 'groups_id' in values: self.env['ir.model.access'].call_cache_clearing_methods() self.env['ir.rule'].clear_caches() self.has_group.clear_cache(self) if any( key.startswith('context_') or key in ('lang', 'tz') for key in values): self.context_get.clear_cache(self) if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) return res @api.multi def unlink(self): if SUPERUSER_ID in self.ids: raise UserError( _('You can not remove the admin user as it is used internally for resources created by Odoo (updates, module installation, ...)' )) db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) return super(Users, self).unlink() @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): if args is None: args = [] users = self.browse() if name and operator in ['=', 'ilike']: users = self.search([('login', '=', name)] + args, limit=limit) if not users: users = self.search([('name', operator, name)] + args, limit=limit) return users.name_get() @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}) if ('name' not in default) and ('partner_id' not in default): default['name'] = _("%s (copy)") % self.name if 'login' not in default: default['login'] = _("%s (copy)") % self.login return super(Users, self).copy(default) @api.model @tools.ormcache('self._uid') def context_get(self): user = self.env.user 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 @api.model @api.returns('ir.actions.act_window', lambda record: record.id) def action_get(self): return self.sudo().env.ref('base.action_res_users_my') def check_super(self, passwd): return check_super(passwd) @api.model def check_credentials(self, password): """ Override this method to plug additional authentication methods""" user = self.sudo().search([('id', '=', self._uid), ('password', '=', password)]) if not user: raise AccessDenied() @api.model def _update_last_login(self): # only create new records to avoid any side-effect on concurrent transactions # extra records will be deleted by the periodical garbage collection self.env['res.users.log'].create({}) # populated by defaults @classmethod def _login(cls, db, login, password): if not password: return False user_id = False try: with cls.pool.cursor() as cr: self = api.Environment(cr, SUPERUSER_ID, {})[cls._name] user = self.search([('login', '=', login)]) if user: user_id = user.id user.sudo(user_id).check_credentials(password) user.sudo(user_id)._update_last_login() except AccessDenied: _logger.info("Login failed for db:%s login:%s", db, login) user_id = False return user_id @classmethod def authenticate(cls, 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 = cls._login(db, login, password) if uid == 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'): try: with cls.pool.cursor() as cr: base = user_agent_env['base_location'] ICP = api.Environment(cr, uid, {})['ir.config_parameter'] if not ICP.get_param('web.base.url.freeze'): ICP.set_param('web.base.url', base) except Exception: _logger.exception( "Failed to update web.base.url configuration parameter" ) return uid @classmethod def check(cls, 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 AccessDenied() db = cls.pool.db_name if cls.__uid_cache[db].get(uid) == passwd: return cr = cls.pool.cursor() try: self = api.Environment(cr, uid, {})[cls._name] self.check_credentials(passwd) cls.__uid_cache[db][uid] = passwd finally: cr.close() @api.model def change_password(self, old_passwd, new_passwd): """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: odoo.exceptions.AccessDenied when old password is wrong :raise: odoo.exceptions.UserError when new password is not set or empty """ self.check(self._cr.dbname, self._uid, old_passwd) if new_passwd: # use self.env.user here, because it has uid=SUPERUSER_ID return self.env.user.write({'password': new_passwd}) raise UserError( _("Setting empty passwords is not allowed for security reasons!")) @api.multi def preference_save(self): return { 'type': 'ir.actions.client', 'tag': 'reload_context', } @api.multi def preference_change_password(self): return { 'type': 'ir.actions.client', 'tag': 'change_password', 'target': 'new', } @api.model def has_group(self, group_ext_id): # use singleton's id if called on a non-empty recordset, otherwise # context uid uid = self.id or self._uid return self.sudo(user=uid)._has_group(group_ext_id) @api.model @tools.ormcache('self._uid', 'group_ext_id') def _has_group(self, 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('.') self._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)""", (self._uid, module, ext_id)) return bool(self._cr.fetchone()) # for a few places explicitly clearing the has_group cache has_group.clear_cache = _has_group.clear_cache @api.multi def _is_admin(self): self.ensure_one() return self._is_superuser() or self.has_group('base.group_erp_manager') @api.multi def _is_superuser(self): self.ensure_one() return self.id == SUPERUSER_ID @api.model def get_company_currency_id(self): return self.env.user.company_id.currency_id.id
class SaleOrderOption(models.Model): _name = "sale.order.option" _description = "Sale Options" _order = 'sequence, id' order_id = fields.Many2one('sale.order', 'Sales Order Reference', ondelete='cascade', index=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', sanitize_attributes=False, translate=html_translate) price_unit = fields.Float('Unit Price', required=True, digits=dp.get_precision('Product Price')) discount = fields.Float('Discount (%)', digits=dp.get_precision('Discount')) uom_id = fields.Many2one('uom.uom', 'Unit of Measure ', required=True) quantity = fields.Float('Quantity', required=True, digits=dp.get_precision('Product UoS'), default=1) sequence = fields.Integer('Sequence', help="Gives the sequence order when displaying a list of suggested product.") @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).get_product_price(product, self.quantity, partner_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): self.ensure_one() order = self.order_id if order.state not in ['draft', 'sent']: return False order_line = order.order_line.filtered(lambda line: line.product_id == self.product_id) if order_line: order_line = order_line[0] order_line.product_uom_qty += 1 else: vals = { 'price_unit': self.price_unit, 'website_description': self.website_description, 'name': self.name, 'order_id': order.id, 'product_id': self.product_id.id, 'product_uom_qty': self.quantity, 'product_uom': self.uom_id.id, 'discount': self.discount, } order_line = self.env['sale.order.line'].create(vals) order_line._compute_tax_id() self.write({'line_id': order_line.id}) return {'type': 'ir.actions.client', 'tag': 'reload'}
class TransferRequest(models.Model): _name = 'transfer.request' _inherit = ['mail.thread', 'resource.mixin'] # old implementation # def _get_default_requester(self): # emp = self.env['hr.employee'].search([('user_id', '=', self.env.uid)]) # if emp: # return emp[0].id # else: # return False @api.model def _get_default_requested_by(self): return self.env['res.users'].browse(self.env.uid) @api.model def _default_dest_location(self): if self.env.user.dest_location_id: return self.env.user.dest_location_id.id else: return False name = fields.Char(string='Name', readonly=1) requested_by = fields.Many2one('res.users', 'Requested by', required=True, default=_get_default_requested_by) # requested_by_employee_id = fields.Many2one('hr.employee', string='Requester', default=_get_default_requester) # requested_for_employee_id = fields.Many2one('hr.employee', string='Requested For') analytic_account_id = fields.Many2one( 'account.analytic.account', string='Analytic Account', ) cancel_reason = fields.Html(string='Cancellation Reason') transfer_request_line_ids = fields.One2many('transfer.request.line', 'transfer_request_id', string='Transferred Products') source_stock_location_id = fields.Many2one('stock.location', string='Source Location', domain=[('usage', '=', 'internal')]) destination_stock_location_id = fields.Many2one( 'stock.location', string='Destination Location', domain=[('usage', '=', 'transit')], default=_default_dest_location) state = fields.Selection([('draft', 'Draft'), ('approve', 'Approve'), ('transferring', 'Transferring'), ('done', 'Done'), ('cancelled', 'Cancelled')], string='State', default='draft', track_visibility='onchange') picking_count = fields.Integer(string='Transfers', compute='get_request_picking_count') transfer_reason = fields.Many2one('transfer.request.reason', string='Transfer Reason') scheduled_date = fields.Datetime( 'Scheduled Date', help= "Scheduled time for the first part of the shipment to be processed.") @api.model def create(self, vals): if not self.env.user.dest_location_id: raise ValidationError( _('Please configure destination location in current user related employee.' )) vals['name'] = self.env['ir.sequence'].next_by_code('transfer.request') vals.update({ 'requested_by': self.env.uid, 'destination_stock_location_id': self.env.user.dest_location_id.id, }) return super(TransferRequest, self).create(vals) @api.multi def unlink(self): for rec in self: if rec.state == 'done': raise ValidationError( _('You cannot delete done transfer request.')) return super(TransferRequest, self).unlink() @api.multi def set_state_to_approve(self): for rec in self: rec.state = 'approve' @api.multi def set_state_to_cancelled(self): for rec in self: if not rec.cancel_reason or rec.cancel_reason == '<p><br></p>': raise ValidationError( _('Please add reason for canceling request first.')) rec.state = 'cancelled' @api.multi def set_state_to_draft(self): for rec in self: rec.state = 'draft' @api.multi def set_state_to_transferring(self): for rec in self: rec.state = 'transferring' @api.multi def set_state_to_done(self): for rec in self: rec.state = 'done' @api.multi def get_request_picking_count(self): for rec in self: stock_picking_objects = self.env['stock.picking'].search([ ('transfer_request_id', '=', rec.id) ]) rec.picking_count = len(stock_picking_objects) @api.multi def transfer_products(self): for rec in self: rec.create_transfer_for_products() rec.set_state_to_transferring() # old implementation # transfer_line_ids = [line.id for line in rec.transfer_request_line_ids if # rec.transfer_request_line_ids and line.transfer_created == False] # return { # 'name': _('Transfer Products'), # 'view_type': 'form', # 'view_mode': 'form', # 'target': 'new', # 'res_model': 'transfer.products.wizard', # 'view_id': self.env.ref('bi_transfer_request.transfer_products_wizard_form_view').id, # 'type': 'ir.actions.act_window', # 'context': { # 'default_source_stock_location_id': rec.source_stock_location_id.id, # 'default_destination_stock_location_id': rec.destination_stock_location_id.id, # 'default_transfer_request_line_ids': transfer_line_ids, # 'default_created_from': 'transfer_request', # }, # } def create_transfer_for_products(self): picking_line_vals = [] source_warehouse = self.source_stock_location_id.get_warehouse() internal_picking_type = self.env['stock.picking.type'].sudo().search( [('code', '=', 'internal'), ('warehouse_id', '=', source_warehouse.id)], limit=1) if not internal_picking_type: raise ValidationError(_('Please configure internal transfer.')) else: internal_picking_type_id = internal_picking_type.id for line in self.transfer_request_line_ids: if line.transfer_created == False: picking_line_vals.append((0, 0, { 'product_id': line.product_id.id, 'name': line.product_id.name, 'product_uom_qty': line.transferred_qty or line.qty, 'product_uom': line.product_uom_id.id, 'company_id': self.env.user.company_id.id, 'location_id': self.source_stock_location_id.id, 'location_dest_id': self.destination_stock_location_id.id, 'transfer_request_id': self.id })) if len(picking_line_vals): picking_vals = { 'origin': self.name, 'scheduled_date': self.scheduled_date or fields.Datetime.now(), 'picking_type_id': internal_picking_type_id, 'location_id': self.source_stock_location_id.id, 'location_dest_id': self.destination_stock_location_id.id, 'company_id': self.env.user.company_id.id, 'move_type': 'direct', 'state': 'draft', 'move_lines': picking_line_vals, 'transfer_request_id': self.id } created_picking = self.env['stock.picking'].sudo().create( picking_vals) if created_picking: created_picking.action_confirm() for line in self.transfer_request_line_ids: line.transfer_created = True
class SaleOrder(models.Model): _inherit = 'sale.order' def _get_default_template(self): template = self.env.ref('website_quote.website_quote_template_default', raise_if_not_found=False) return template and template.active and template or False def _get_default_require_signature(self): default_template = self._get_default_template() if default_template: return default_template.require_signature else: return False def _get_default_require_payment(self): default_template = self._get_default_template() if default_template: return default_template.require_payment else: return False template_id = fields.Many2one( 'sale.quote.template', 'Quotation Template', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, default=_get_default_template) website_description = fields.Html('Description', sanitize_attributes=False, translate=html_translate) options = fields.One2many( 'sale.order.option', 'order_id', 'Optional Products Lines', copy=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}) amount_undiscounted = fields.Float( 'Amount Before Discount', compute='_compute_amount_undiscounted', digits=0) quote_viewed = fields.Boolean('Quotation Viewed') require_signature = fields.Boolean('Digital Signature', default=_get_default_require_signature, states={'sale': [('readonly', True)], 'done': [('readonly', True)]}, help='Request a digital signature to the customer in order to confirm orders automatically.') require_payment = fields.Boolean('Electronic Payment', default=_get_default_require_payment, states={'sale': [('readonly', True)], 'done': [('readonly', True)]}, help='Request an electronic payment to the customer in order to confirm orders automatically.') @api.multi @api.returns('self', lambda value: value.id) def copy(self, default=None): if self.template_id and self.template_id.number_of_days > 0: default = dict(default or {}) default['validity_date'] = fields.Date.to_string(datetime.now() + timedelta(self.template_id.number_of_days)) return super(SaleOrder, self).copy(default=default) @api.one def _compute_amount_undiscounted(self): total = 0.0 for line in self.order_line: total += line.price_subtotal + line.price_unit * ((line.discount or 0.0) / 100.0) * line.product_uom_qty # why is there a discount in a field named amount_undiscounted ?? self.amount_undiscounted = total @api.onchange('partner_id') def onchange_partner_id(self): super(SaleOrder, self).onchange_partner_id() self.note = self.template_id.note or self.note @api.onchange('partner_id') def onchange_update_description_lang(self): if not self.template_id: return else: template = self.template_id.with_context(lang=self.partner_id.lang) self.website_description = template.website_description @api.onchange('template_id') def onchange_template_id(self): if not self.template_id: self.require_signature = False self.require_payment = False return template = self.template_id.with_context(lang=self.partner_id.lang) order_lines = [(5, 0, 0)] for line in template.quote_line: data = { 'display_type': line.display_type, 'name': line.name, 'website_description': line.website_description, 'state': 'draft', } if line.product_id: discount = 0 if self.pricelist_id: price = self.pricelist_id.with_context(uom=line.product_uom_id.id).get_product_price(line.product_id, 1, False) if self.pricelist_id.discount_policy == 'without_discount' and line.price_unit: discount = (line.price_unit - price) / line.price_unit * 100 price = line.price_unit else: price = line.price_unit data.update({ 'price_unit': price, 'discount': 100 - ((100 - discount) * (100 - line.discount)/100), 'product_uom_qty': line.product_uom_qty, 'product_id': line.product_id.id, 'product_uom': line.product_uom_id.id, 'customer_lead': self._get_customer_lead(line.product_id.product_tmpl_id), }) if self.pricelist_id: data.update(self.env['sale.order.line']._get_purchase_price(self.pricelist_id, line.product_id, line.product_uom_id, fields.Date.context_today(self))) order_lines.append((0, 0, data)) self.order_line = order_lines self.order_line._compute_tax_id() option_lines = [] for option in template.options: if self.pricelist_id: price = self.pricelist_id.with_context(uom=option.uom_id.id).get_product_price(option.product_id, 1, False) else: price = option.price_unit data = { '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, } option_lines.append((0, 0, data)) self.options = option_lines if template.number_of_days > 0: self.validity_date = fields.Date.to_string(datetime.now() + timedelta(template.number_of_days)) self.website_description = template.website_description self.require_signature = template.require_signature self.require_payment = template.require_payment if template.note: self.note = template.note @api.constrains('template_id', 'require_signature', 'require_payment') def _check_portal_confirmation(self): for order in self.sudo().filtered('template_id'): if not order.require_signature and not order.require_payment: raise ValidationError(_('Please select a confirmation mode in Other Information: Digital Signature, Electronic Payment or both.')) @api.multi def open_quotation(self): self.ensure_one() self.write({'quote_viewed': True}) return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/quote/%s/%s' % (self.id, self.access_token) } @api.multi def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to the online quote if it exists. """ self.ensure_one() user = access_uid and self.env['res.users'].sudo().browse(access_uid) or self.env.user if not self.template_id or (not user.share and not self.env.context.get('force_website')): return super(SaleOrder, self).get_access_action(access_uid) return { 'type': 'ir.actions.act_url', 'url': '/quote/%s/%s' % (self.id, self.access_token), 'target': 'self', 'res_id': self.id, } def _get_share_url(self, redirect=False, signup_partner=False): self.ensure_one() if self.state not in ['sale', 'done']: auth_param = url_encode(self.partner_id.signup_get_auth_param()[self.partner_id.id]) return '/quote/%s/%s?' % (self.id, self.access_token) + auth_param return super(SaleOrder, self)._get_share_url(redirect) def get_portal_confirmation_action(self): """ Template override default behavior of pay / sign chosen in sales settings """ if self.template_id: if self.require_signature and not self.signature: return 'sign' elif self.require_payment: return 'pay' else: return 'none' return super(SaleOrder, self).get_portal_confirmation_action() def has_to_be_signed(self): res = super(SaleOrder, self).has_to_be_signed() return self.require_signature if self.template_id else res def has_to_be_paid(self): res = super(SaleOrder, self).has_to_be_paid() return self.require_payment if self.template_id else res @api.multi def action_confirm(self): res = super(SaleOrder, self).action_confirm() for order in self: if order.template_id and order.template_id.mail_template_id: self.template_id.mail_template_id.send_mail(order.id) return res @api.multi def _get_payment_type(self): self.ensure_one() return 'form_save' if self.require_payment else 'form'
class AccountEdiDocument(models.Model): _name = 'account.edi.document' _description = 'Electronic Document for an account.move' # == Stored fields == move_id = fields.Many2one('account.move', required=True, ondelete='cascade') edi_format_id = fields.Many2one('account.edi.format', required=True) attachment_id = fields.Many2one( 'ir.attachment', help= 'The file generated by edi_format_id when the invoice is posted (and this document is processed).' ) state = fields.Selection([('to_send', 'To Send'), ('sent', 'Sent'), ('to_cancel', 'To Cancel'), ('cancelled', 'Cancelled')]) error = fields.Html() # == Not stored fields == name = fields.Char(related='attachment_id.name') edi_format_name = fields.Char(string='Format Name', related='edi_format_id.name') _sql_constraints = [ ( 'unique_edi_document_by_move_by_format', 'UNIQUE(edi_format_id, move_id)', 'Only one edi document by move by format', ), ] def _prepare_jobs(self): """Creates a list of jobs to be performed by '_process_jobs' for the documents in self. Each document represent a job, BUT if multiple documents have the same state, edi_format_id, doc_type (invoice or payment) and company_id AND the edi_format_id supports batching, they are grouped into a single job. :returns: A list of tuples (key, documents) * key: A tuple (edi_format_id, state, doc_type, company_id) ** edi_format_id: The format to perform the operation with ** state: The state of the documents of this job ** doc_type: Are the moves of this job invoice or payments ? ** company_id: The company the moves belong to * documents: The documents related to this job. If edi_format_id does not support batch, length must be one """ to_process = [] batches = {} for edi_doc in self.filtered(lambda d: d.state in ('to_send', 'to_cancel')): move = edi_doc.move_id edi_format = edi_doc.edi_format_id if move.is_invoice(include_receipts=True): doc_type = 'invoice' elif move.payment_id or move.statement_line_id: doc_type = 'payment' else: continue key = (edi_format, edi_doc.state, doc_type, move.company_id) if edi_format._support_batching(): if not batches.get(key, None): batches[key] = self.env['account.edi.document'] batches[key] |= edi_doc else: to_process.append((key, edi_doc)) to_process.extend(batches.items()) return to_process @api.model def _process_jobs(self, to_process): """Post or cancel move_id (invoice or payment) by calling the related methods on edi_format_id. Invoices are processed before payments. """ def _postprocess_post_edi_results(documents, edi_result): attachments_to_unlink = self.env['ir.attachment'] for document in documents: move = document.move_id move_result = edi_result.get(move, {}) if move_result.get('attachment'): old_attachment = document.attachment_id document.write({ 'attachment_id': move_result['attachment'].id, 'state': 'sent', 'error': False, }) if not old_attachment.res_model or not old_attachment.res_id: attachments_to_unlink |= old_attachment else: document.error = move_result.get( 'error', _("Error when processing the journal entry.")) # Attachments that are not explicitly linked to a business model could be removed because they are not # supposed to have any traceability from the user. attachments_to_unlink.unlink() def _postprocess_cancel_edi_results(documents, edi_result): invoice_ids_to_cancel = set() # Avoid duplicates attachments_to_unlink = self.env['ir.attachment'] for document in documents: move = document.move_id move_result = edi_result.get(move, {}) if move_result.get('success'): old_attachment = document.attachment_id document.write({ 'state': 'cancelled', 'error': False, 'attachment_id': False, }) if move.is_invoice( include_receipts=True) and move.state == 'posted': # The user requested a cancellation of the EDI and it has been approved. Then, the invoice # can be safely cancelled. invoice_ids_to_cancel.add(move.id) if not old_attachment.res_model or not old_attachment.res_id: attachments_to_unlink |= old_attachment else: document.error = move_result.get('error') or _( "Error when cancelling the journal entry.") if invoice_ids_to_cancel: invoices = self.env['account.move'].browse( list(invoice_ids_to_cancel)) invoices.button_draft() invoices.button_cancel() # Attachments that are not explicitly linked to a business model could be removed because they are not # supposed to have any traceability from the user. attachments_to_unlink.unlink() test_mode = self._context.get('edi_test_mode', False) # ==== Process invoices ==== payments = [] for key, batches in to_process: edi_format, state, doc_type, company_id = key if doc_type == 'payment': payments.append((key, batches)) continue # payments are processed after invoices for documents in batches: try: with self.env.cr.savepoint(): # Locks the documents in DB. Avoid sending an invoice twice (the documents can be processed by the CRON but also manually). self._cr.execute( 'SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(self.ids)]) if state == 'to_send': edi_result = edi_format._post_invoice_edi( documents.move_id, test_mode=test_mode) _postprocess_post_edi_results( documents, edi_result) elif state == 'to_cancel': edi_result = edi_format._cancel_invoice_edi( documents.move_id, test_mode=test_mode) _postprocess_cancel_edi_results( documents, edi_result) except OperationalError as e: if e.pgcode == '55P03': _logger.debug( 'Another transaction already locked documents rows. Cannot process documents.' ) else: raise e # ==== Process payments ==== for key, batches in payments: edi_format, state, doc_type, company_id = key for documents in batches: try: with self.env.cr.savepoint(): self._cr.execute( 'SELECT * FROM account_edi_document WHERE id IN %s FOR UPDATE NOWAIT', [tuple(self.ids)]) if state == 'to_send': edi_result = edi_format._post_payment_edi( documents.move_id, test_mode=test_mode) _postprocess_post_edi_results( documents, edi_result) elif state == 'to_cancel': edi_result = edi_format._cancel_payment_edi( documents.move_id, test_mode=test_mode) _postprocess_cancel_edi_results( documents, edi_result) except OperationalError as e: if e.pgcode == '55P03': _logger.debug( 'Another transaction already locked documents rows. Cannot process documents.' ) else: raise e def _process_documents_no_web_services(self): """ Post and cancel all the documents that don't need a web service. """ jobs = self.filtered(lambda d: not d.edi_format_id._needs_web_services( ))._prepare_jobs() self._process_jobs(jobs) def _process_documents_web_services(self, job_count=None): """ Post and cancel all the documents that need a web service. This is called by CRON. :param job_count: Limit to the number of jobs to process among the ones that are available for treatment. """ jobs = self.filtered( lambda d: d.edi_format_id._needs_web_services())._prepare_jobs() self._process_jobs(jobs[0:job_count or len(jobs)])
class Wizard(models.TransientModel): _name = 'mail_move_message.wizard' _description = 'Mail move message wizard' @api.model def _model_selection(self): selection = [] config_parameters = self.env['ir.config_parameter'] model_names = config_parameters.sudo().get_param( 'mail_relocation_models') model_names = model_names.split(',') if model_names else [] if 'default_message_id' in self.env.context: message = self.env['mail.message'].browse( self.env.context['default_message_id']) if message.model and message.model not in model_names: model_names.append(message.model) if message.moved_from_model and message.moved_from_model not in model_names: model_names.append(message.moved_from_model) if model_names: selection = [(m.model, m.display_name) for m in self.env['ir.model'].search([('model', 'in', model_names)])] return selection @api.model def default_get(self, fields_list): res = super(Wizard, self).default_get(fields_list) available_models = self._model_selection() if len(available_models): record = self.env[available_models[0][0]].search([], limit=1) res['model_record'] = len(record) and ( available_models[0][0] + ',' + str(record.id)) or False if 'message_id' in res: message = self.env['mail.message'].browse(res['message_id']) email_from = message.email_from parts = email_split(email_from.replace(' ', ',')) if parts: email = parts[0] name = email_from.find( email ) != -1 and email_from[:email_from.index(email)].replace( '"', '').replace('<', '').strip() or email_from else: name, email = email_from res['message_name_from'] = name res['message_email_from'] = email res['partner_id'] = message.author_id.id if message.author_id and self.env.uid not in [ u.id for u in message.author_id.user_ids ]: res['filter_by_partner'] = True if message.author_id and res.get('model'): res_id = self.env[res['model']].search([], order='id desc', limit=1) if res_id: res['res_id'] = res_id[0].id config_parameters = self.env['ir.config_parameter'] res['move_followers'] = config_parameters.sudo().get_param( 'mail_relocation_move_followers') res['uid'] = self.env.uid return res message_id = fields.Many2one('mail.message', string='Message') message_body = fields.Html(related='message_id.body', string='Message to move', readonly=True) message_from = fields.Char(related='message_id.email_from', string='From', readonly=True) message_subject = fields.Char(related='message_id.subject', string='Subject', readonly=True) message_moved_by_message_id = fields.Many2one( 'mail.message', related='message_id.moved_by_message_id', string='Moved with', readonly=True) message_moved_by_user_id = fields.Many2one( 'res.users', related='message_id.moved_by_user_id', string='Moved by', readonly=True) message_is_moved = fields.Boolean(string='Is Moved', related='message_id.is_moved', readonly=True) parent_id = fields.Many2one( 'mail.message', string='Search by name', ) model_record = fields.Reference(selection="_model_selection", string='Record') model = fields.Char(compute="_compute_model_res_id", string='Model') res_id = fields.Integer(compute="_compute_model_res_id", string='Record ID') can_move = fields.Boolean('Can move', compute='_compute_get_can_move') move_back = fields.Boolean( 'MOVE TO ORIGIN', help='Move message and submessages to original place') partner_id = fields.Many2one('res.partner', string='Author') filter_by_partner = fields.Boolean('Filter Records by partner') message_email_from = fields.Char() message_name_from = fields.Char() # FIXME message_to_read should be True even if current message or any his childs are unread message_to_read = fields.Boolean( compute='_compute_is_read', string="Unread message", help="Service field shows that this message were unread when moved") uid = fields.Integer() move_followers = fields.Boolean( 'Move Followers', help="Add followers of current record to a new record.\n" "You must use this option, if new record has restricted access.\n" "You can change default value for this option at Settings/System Parameters" ) @api.depends('model_record') def _compute_model_res_id(self): for rec in self: rec.model = rec.model_record and rec.model_record._name or False rec.res_id = rec.model_record and rec.model_record.id or False @api.depends('message_id') def _compute_get_can_move(self): for r in self: r.get_can_move_one() def _compute_is_read(self): messages = self.env['mail.message'].sudo().browse( self.message_id.all_child_ids.ids + [self.message_id.id]) self.message_to_read = True in [m.needaction for m in messages] def get_can_move_one(self): self.ensure_one() # message was not moved before OR message is a top message of previous move self.can_move = not self.message_id.moved_by_message_id or self.message_id.moved_by_message_id.id == self.message_id.id @api.onchange('move_back') def on_change_move_back(self): if not self.move_back: return self.parent_id = self.message_id.moved_from_parent_id message = self.message_id if message.is_moved: self.model_record = self.env[message.moved_from_model].browse( message.moved_from_res_id) @api.onchange('parent_id', 'model_record') def update_move_back(self): model = self.message_id.moved_from_model self.move_back = self.parent_id == self.message_id.moved_from_parent_id \ and self.res_id == self.message_id.moved_from_res_id \ and (self.model == model or (not self.model and not model)) @api.onchange('parent_id') def on_change_parent_id(self): if self.parent_id and self.parent_id.model: self.model = self.parent_id.model self.res_id = self.parent_id.res_id else: self.model = None self.res_id = None @api.onchange('model', 'filter_by_partner', 'partner_id') def on_change_partner(self): domain = {'res_id': [('id', '!=', self.message_id.res_id)]} if self.model and self.filter_by_partner and self.partner_id: fields = self.env[self.model].fields_get(False) contact_field = False for n, f in fields.items(): if f['type'] == 'many2one' and f['relation'] == 'res.partner': contact_field = n break if contact_field: domain['res_id'].append( (contact_field, '=', self.partner_id.id)) if self.model: res_id = self.env[self.model].search(domain['res_id'], order='id desc', limit=1) self.res_id = res_id and res_id[0].id else: self.res_id = None return {'domain': domain} def check_access(self): for r in self: r.check_access_one() def check_access_one(self): self.ensure_one() operation = 'write' if not (self.model and self.res_id): return True model_obj = self.env[self.model] mids = model_obj.browse(self.res_id).exists() if hasattr(model_obj, 'check_mail_message_access'): model_obj.check_mail_message_access(mids.ids, operation) else: self.env['mail.thread'].check_mail_message_access( mids.ids, operation, model_name=self.model) def open_moved_by_message_id(self): message_id = None for r in self: message_id = r.message_moved_by_message_id.id return { 'type': 'ir.actions.act_window', 'res_model': 'mail_move_message.wizard', 'view_mode': 'form', 'view_type': 'form', 'views': [[False, 'form']], 'target': 'new', 'context': { 'default_message_id': message_id }, } def move(self): for r in self: if not r.model: raise exceptions.except_orm( _('Record field is empty!'), _('Select a record for relocation first')) for r in self: r.check_access() if not r.parent_id or not (r.parent_id.model == r.model and r.parent_id.res_id == r.res_id): # link with the first message of record parent = self.env['mail.message'].search( [('model', '=', r.model), ('res_id', '=', r.res_id)], order='id', limit=1) r.parent_id = parent.id or None r.message_id.move(r.parent_id.id, r.res_id, r.model, r.move_back, r.move_followers, r.message_to_read, r.partner_id) if r.model in ['mail.message', 'mail.channel', False]: return { 'name': 'Chess game page', 'type': 'ir.actions.act_url', 'url': '/web', 'target': 'self', } return { 'name': _('Record'), 'view_type': 'form', 'view_mode': 'form', 'res_model': r.model, 'res_id': r.res_id, 'views': [(False, 'form')], 'type': 'ir.actions.act_window', } def delete(self): for r in self: r.delete_one() def delete_one(self): self.ensure_one() msg_id = self.message_id.id # Send notification notification = {'id': msg_id} self.env['bus.bus'].sendone( (self._cr.dbname, 'mail_move_message.delete_message'), notification) self.message_id.unlink() return {} def read_close(self): for r in self: r.read_close_one() def read_close_one(self): self.ensure_one() self.message_id.set_message_done() self.message_id.child_ids.set_message_done() return {'type': 'ir.actions.act_window_close'}
class EventEvent(models.Model): """Event""" _name = 'event.event' _description = 'Event' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_begin' name = fields.Char(string='Event', translate=True, required=True, readonly=False, states={'done': [('readonly', True)]}) active = fields.Boolean(default=True) user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user, track_visibility="onchange", readonly=False, states={'done': [('readonly', True)]}) company_id = fields.Many2one('res.company', string='Company', change_default=True, default=lambda self: self.env['res.company']. _company_default_get('event.event'), required=False, readonly=False, states={'done': [('readonly', True)]}) organizer_id = fields.Many2one( 'res.partner', string='Organizer', track_visibility="onchange", default=lambda self: self.env.user.company_id.partner_id) event_type_id = fields.Many2one('event.type', string='Category', readonly=False, states={'done': [('readonly', True)]}, oldname='type') color = fields.Integer('Kanban Color Index') event_mail_ids = fields.One2many('event.mail', 'event_id', string='Mail Schedule', copy=True) # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', oldname='register_max', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= "For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted." ) seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')], 'Maximum Attendees', required=True, default='unlimited') seats_min = fields.Integer( string='Minimum Attendees', oldname='register_min', help= "For each event you can define a minimum reserved seats (number of attendees), if it does not reach the mentioned registrations the event can not be confirmed (keep 0 to ignore this rule)" ) seats_reserved = fields.Integer(oldname='register_current', string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer(oldname='register_avail', string='Available Seats', store=True, readonly=True, compute='_compute_seats') seats_unconfirmed = fields.Integer(oldname='register_prospect', string='Unconfirmed Seat Reservations', store=True, readonly=True, compute='_compute_seats') seats_used = fields.Integer(oldname='register_attended', string='Number of Participants', store=True, readonly=True, compute='_compute_seats') seats_expected = fields.Integer(string='Number of Expected Attendees', readonly=True, compute='_compute_seats') # Registration fields registration_ids = fields.One2many('event.registration', 'event_id', string='Attendees', readonly=False, states={'done': [('readonly', True)]}) # Date fields date_tz = fields.Selection('_tz_get', string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC') date_begin = fields.Datetime(string='Start Date', required=True, track_visibility='onchange', states={'done': [('readonly', True)]}) date_end = fields.Datetime(string='End Date', required=True, track_visibility='onchange', states={'done': [('readonly', True)]}) date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') state = fields.Selection( [('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('confirm', 'Confirmed'), ('done', 'Done')], string='Status', default='draft', readonly=True, required=True, copy=False, help= "If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'." ) auto_confirm = fields.Boolean(string='Autoconfirm Registrations') is_online = fields.Boolean('Online Event') address_id = fields.Many2one( 'res.partner', string='Location', default=lambda self: self.env.user.company_id.partner_id, readonly=False, states={'done': [('readonly', True)]}, track_visibility="onchange") country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True, readonly=False) twitter_hashtag = fields.Char('Twitter Hashtag') description = fields.Html(string='Description', oldname='note', translate=html_translate, sanitize_attributes=False, readonly=False, states={'done': [('readonly', True)]}) # badge fields badge_front = fields.Html(string='Badge Front') badge_back = fields.Html(string='Badge Back') badge_innerleft = fields.Html(string='Badge Inner Left') badge_innerright = fields.Html(string='Badge Inner Right') event_logo = fields.Html(string='Event Logo') @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 for event in self: event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0 # aggregate registrations by event and by state if self.ids: state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT event_id, state, count(event_id) FROM event_registration WHERE event_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_id, state """ self._cr.execute(query, (tuple(self.ids), )) for event_id, state, num in self._cr.fetchall(): event = self.browse(event_id) event[state_field[state]] += num # compute seats_available for event in self: if event.seats_max > 0: event.seats_available = event.seats_max - ( event.seats_reserved + event.seats_used) event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used @api.model def _tz_get(self): return [(x, x) for x in pytz.all_timezones] @api.one @api.depends('date_tz', 'date_begin') def _compute_date_begin_tz(self): if self.date_begin: self.date_begin_located = format_tz( self.with_context(use_babel=True).env, self.date_begin, tz=self.date_tz) else: self.date_begin_located = False @api.one @api.depends('date_tz', 'date_end') def _compute_date_end_tz(self): if self.date_end: self.date_end_located = format_tz( self.with_context(use_babel=True).env, self.date_end, tz=self.date_tz) else: self.date_end_located = False @api.onchange('is_online') def _onchange_is_online(self): if self.is_online: self.address_id = False @api.onchange('event_type_id') def _onchange_type(self): if self.event_type_id: self.seats_min = self.event_type_id.default_registration_min self.seats_max = self.event_type_id.default_registration_max if self.event_type_id.default_registration_max: self.seats_availability = 'limited' if self.event_type_id.auto_confirm: self.auto_confirm = self.event_type_id.auto_confirm if self.event_type_id.use_hashtag: self.twitter_hashtag = self.event_type_id.default_hashtag if self.event_type_id.use_timezone: self.date_tz = self.event_type_id.default_timezone self.is_online = self.event_type_id.is_online if self.event_type_id.event_type_mail_ids: self.event_mail_ids = [ (5, 0, 0) ] + [{ 'template_id': line.template_id, 'interval_nbr': line.interval_nbr, 'interval_unit': line.interval_unit, 'interval_type': line.interval_type } for line in self.event_type_id.event_type_mail_ids] @api.constrains('seats_min', 'seats_max', 'seats_availability') def _check_seats_min_max(self): if any(event.seats_availability == 'limited' and event.seats_min > event.seats_max for event in self): raise ValidationError( _('Maximum attendees number should be greater than minimum attendees number.' )) @api.constrains('seats_max', 'seats_available') def _check_seats_limit(self): if any(event.seats_availability == 'limited' and event.seats_max and event.seats_available < 0 for event in self): raise ValidationError(_('No more available seats.')) @api.one @api.constrains('date_begin', 'date_end') def _check_closing_date(self): if self.date_end < self.date_begin: raise ValidationError( _('The closing date cannot be earlier than the beginning date.' )) @api.multi @api.depends('name', 'date_begin', 'date_end') def name_get(self): result = [] for event in self: date_begin = fields.Datetime.from_string(event.date_begin) date_end = fields.Datetime.from_string(event.date_end) dates = [ fields.Date.to_string( fields.Datetime.context_timestamp(event, dt)) for dt in [date_begin, date_end] if dt ] dates = sorted(set(dates)) result.append( (event.id, '%s (%s)' % (event.name, ' - '.join(dates)))) return result @api.model def create(self, vals): res = super(EventEvent, self).create(vals) if res.organizer_id: res.message_subscribe([res.organizer_id.id]) if res.auto_confirm: res.button_confirm() return res @api.multi def write(self, vals): res = super(EventEvent, self).write(vals) if vals.get('organizer_id'): self.message_subscribe([vals['organizer_id']]) return res @api.multi @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) @api.one def button_draft(self): self.state = 'draft' @api.multi def button_cancel(self): if any('done' in event.mapped('registration_ids.state') for event in self): raise UserError( _("There are already attendees who attended this event. Please reset it to draft if you want to cancel this event." )) self.registration_ids.write({'state': 'cancel'}) self.state = 'cancel' @api.one def button_done(self): self.state = 'done' @api.one def button_confirm(self): self.state = 'confirm' @api.one def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'): for attendee in self.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail( attendee.id, force_send=force_send) @api.multi def _is_event_registrable(self): return True
class Task(models.Model): _name = "project.task" _description = "Task" _date_name = "date_start" _inherit = ['mail.thread', 'ir.needaction_mixin'] _mail_post_access = 'read' _order = "priority desc, sequence, date_start, name, id" def _get_default_partner(self): if 'default_project_id' in self.env.context: default_project_id = self.env['project.project'].browse( self.env.context['default_project_id']) return default_project_id.exists().partner_id def _get_default_stage_id(self): """ Gives default stage_id """ project_id = self.env.context.get('default_project_id') if not project_id: return False return self.stage_find(project_id, [('fold', '=', False)]) @api.multi def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None): TaskType = self.env['project.task.type'] order = TaskType._order access_rights_uid = access_rights_uid or self.env.uid if read_group_order == 'stage_id desc': order = '%s desc' % order if 'default_project_id' in self.env.context: search_domain = [ '|', ('project_ids', '=', self.env.context['default_project_id']), ('id', 'in', self.ids) ] else: search_domain = [('id', 'in', self.ids)] stage_ids = TaskType._search(search_domain, order=order, access_rights_uid=access_rights_uid) stages = TaskType.sudo(access_rights_uid).browse(stage_ids) result = stages.name_get() # restore order of the search result.sort( lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0]))) return result, {stage.id: stage.fold for stage in stages} _group_by_full = { 'stage_id': _read_group_stage_ids, } active = fields.Boolean(default=True) name = fields.Char(string='Task Title', track_visibility='onchange', required=True, index=True) description = fields.Html(string='Description') priority = fields.Selection([('0', 'Normal'), ('1', 'High')], default='0', index=True) sequence = fields.Integer( string='Sequence', index=True, default=10, help="Gives the sequence order when displaying a list of tasks.") stage_id = fields.Many2one('project.task.type', string='Stage', track_visibility='onchange', index=True, default=_get_default_stage_id, 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')], string='Kanban State', default='normal', track_visibility='onchange', required=True, copy=False, 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" ) create_date = fields.Datetime(index=True) write_date = fields.Datetime( index=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(string='Starting Date', default=fields.Datetime.now, index=True, copy=False) date_end = fields.Datetime(string='Ending Date', index=True, copy=False) date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True) date_deadline = fields.Date(string='Deadline', index=True, copy=False) date_last_stage_update = fields.Datetime(string='Last Stage Update', default=fields.Datetime.now, index=True, copy=False, readonly=True) project_id = fields.Many2one( 'project.project', string='Project', default=lambda self: self.env.context.get('default_project_id'), index=True, track_visibility='onchange', change_default=True) notes = fields.Text(string='Notes') planned_hours = fields.Float( string='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( string='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', string='Assigned to', default=lambda self: self.env.uid, index=True, track_visibility='onchange') partner_id = fields.Many2one('res.partner', string='Customer', default=_get_default_partner) manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True) company_id = fields.Many2one( 'res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) color = fields.Integer(string='Color Index') user_email = fields.Char(related='user_id.email', 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') # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id displayed_image_id = fields.Many2one( 'ir.attachment', domain= "[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Displayed Image') legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True) legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True) legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True) @api.onchange('project_id') def _onchange_project(self): if self.project_id: self.partner_id = self.project_id.partner_id self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)]) else: self.partner_id = False self.stage_id = False @api.onchange('user_id') def _onchange_user(self): if self.user_id: self.date_start = fields.Datetime.now() @api.multi def copy(self, default=None): if default is None: default = {} if not default.get('name'): default['name'] = _("%s (copy)") % self.name if 'remaining_hours' not in default: default['remaining_hours'] = self.planned_hours return super(Task, self).copy(default) @api.constrains('date_start', 'date_end') def _check_dates(self): if any( self.filtered(lambda task: task.date_start and task.date_end and task.date_start > task.date_end)): return ValidationError( _('Error ! Task starting date must be lower than its ending date.' )) # Override view according to the company definition @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): # 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 = self.env.user.company_id.project_time_mode_id tm = obj_tm and obj_tm.name or 'Hours' res = super(Task, self).fields_view_get(view_id=view_id, view_type=view_type, 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 = self.env.user.company_id.project_time_mode_id # using get_object to get translation value uom_hour = self.env.ref('product.product_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 @api.model def get_empty_list_help(self, help): self = self.with_context( empty_list_help_id=self.env.context.get('default_project_id'), empty_list_help_model='project.project', empty_list_help_document_name=_("tasks")) return super(Task, self).get_empty_list_help(help) # ---------------------------------------- # Case management # ---------------------------------------- def stage_find(self, section_id, domain=[], order='sequence'): """ 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 """ # collect all section_ids section_ids = [] if section_id: section_ids.append(section_id) section_ids.extend(self.mapped('project_id').ids) 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 return self.env['project.task.type'].search(search_domain, order=order, limit=1).id # ------------------------------------------------ # CRUD overrides # ------------------------------------------------ @api.model def create(self, vals): # context: no_log, because subtype already handle this context = dict(self.env.context, mail_create_nolog=True) # 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() task = super(Task, self.with_context(context)).create(vals) return task @api.multi def write(self, vals): now = fields.Datetime.now() # stage change: update date_last_stage_update if 'stage_id' in vals: vals['date_last_stage_update'] = now # reset kanban state when changing stage if 'kanban_state' not in vals: vals['kanban_state'] = 'normal' # user_id change: update date_assign if vals.get('user_id'): vals['date_assign'] = now result = super(Task, self).write(vals) return result # --------------------------------------------------- # Mail gateway # --------------------------------------------------- @api.multi def _track_template(self, tracking): res = super(Task, self)._track_template(tracking) test_task = self[0] changes, tracking_value_ids = tracking[test_task.id] if 'stage_id' in changes and test_task.stage_id.mail_template_id: res['stage_id'] = (test_task.stage_id.mail_template_id, { 'composition_mode': 'mass_mail' }) return res @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'kanban_state' in init_values and self.kanban_state == 'blocked': return 'project.mt_task_blocked' elif 'kanban_state' in init_values and self.kanban_state == 'done': return 'project.mt_task_ready' elif 'user_id' in init_values and self.user_id: # assigned -> new return 'project.mt_task_new' elif 'stage_id' in init_values and self.stage_id and self.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(init_values) @api.multi def _notification_group_recipients(self, message, recipients, done_ids, group_data): """ Override the mail.thread method to handle project users and officers recipients. Indeed those will have specific action in their notification emails: creating tasks, assigning it. """ group_project_user = self.env.ref('project.group_project_user') for recipient in recipients.filtered( lambda recipient: recipient.id not in done_ids): if recipient.user_ids and group_project_user in recipient.user_ids[ 0].groups_id: group_data['group_project_user'] |= recipient done_ids.add(recipient.id) return super(Task, self)._notification_group_recipients( message, recipients, done_ids, group_data) @api.multi def _notification_get_recipient_groups(self, message, recipients): self.ensure_one() res = super(Task, self)._notification_get_recipient_groups( message, recipients) take_action = self._notification_link_helper('assign') new_action_id = self.env.ref('project.action_view_task').id new_action = self._notification_link_helper('new', action_id=new_action_id) actions = [] if not self.user_id: actions.append({'url': take_action, 'title': _('I take it')}) else: actions.append({'url': new_action, 'title': _('New Task')}) res['group_project_user'] = {'actions': actions} return res @api.model def message_get_reply_to(self, res_ids, default=None): """ Override to get the reply_to of the parent project. """ tasks = self.sudo().browse(res_ids) project_ids = tasks.mapped('project_id').ids aliases = self.env['project.project'].message_get_reply_to( project_ids, default=default) return { task.id: aliases.get(task.project_id.id, False) for task in tasks } @api.multi def email_split(self, msg): email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or '')) # check left-part is not already an alias aliases = self.mapped('project_id.alias_name') return filter(lambda x: x.split('@')[0] not in aliases, email_list) @api.model def message_new(self, msg, custom_values=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') } defaults.update(custom_values) res = super(Task, self).message_new(msg, custom_values=defaults) task = self.browse(res) email_list = task.email_split(msg) partner_ids = filter( None, task._find_partner_from_emails(email_list, force_create=False)) task.message_subscribe(partner_ids) return res @api.multi def message_update(self, msg, update_vals=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 email_list = self.email_split(msg) partner_ids = filter( None, self._find_partner_from_emails(email_list, force_create=False)) self.message_subscribe(partner_ids) return super(Task, self).message_update(msg, update_vals=update_vals) @api.multi def message_get_suggested_recipients(self): recipients = super(Task, self).message_get_suggested_recipients() for task in self.filtered('partner_id'): reason = _('Customer Email') if task.partner_id.email else _( 'Customer') task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason) return recipients @api.multi def message_get_email_values(self, notif_mail=None): res = super(Task, self).message_get_email_values(notif_mail=notif_mail) headers = {} if res.get('headers'): try: headers.update(safe_eval(res['headers'])) except Exception: pass if self.project_id: current_objects = filter( None, headers.get('X-Odoo-Objects', '').split(',')) current_objects.insert(0, 'project.project-%s, ' % self.project_id.id) headers['X-Odoo-Objects'] = ','.join(current_objects) if self.tag_ids: headers['X-Odoo-Tags'] = ','.join(self.tag_ids.mapped('name')) res['headers'] = repr(headers) return res
class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' invoice_terms = fields.Html(related='company_id.invoice_terms', string="Terms & Conditions", readonly=False)
class ItemCodeID(models.Model): _name = 'item.code' _description = 'Item Code' _rec_name = 'title' item_id = fields.Many2one( 'item.number', string='Item No', default=lambda self: self.env.context.get('item_id')) project_id = fields.Many2one( 'project.project', string='Project', default=lambda self: self.env.context.get('project_id')) title = fields.Char('Item Code', required=True) image_medium = fields.Binary(String='image', attachment=True) sequence = fields.Integer('Item Code number', default=10) description = fields.Html('Description', translate=True, oldname="note", help="An introductory text to your page") sub_ids = fields.One2many('item.sub', 'code_id', string='Item Subs', copy=True) Type = fields.Char('Type') Type_of_finish = fields.Char('Type of finish') Length = fields.Float('Length', required=True) Width = fields.Float('Width', required=True) Height = fields.Float('Height', required=True) Thick = fields.Float('Thick', required=True) Quantity = fields.Integer('Quantity', required=True) Unit = fields.Many2one('uom.uom', 'Unit Of Measure') @api.multi def _default_stage_id(self): """ Gives default stage_id """ project_id = self.env.context.get('default_project_id') if not project_id: return False return self.stage_find(project_id, [('fold', '=', False)]) active = fields.Boolean( default=True, help= "If the active field is set to False, it will allow you to hide the estimation without removing it." ) stage_id = fields.Many2one( 'project.item.type', string='Stage', ondelete='restrict', track_visibility='onchange', index=True, copy=False, domain= "['|', ('project_ids', '=', False), ('project_ids', '=', project_id)]", group_expand='_read_group_stage_ids', default=lambda self: self.env['project.item.type'].search([], limit=1)) kanban_state = fields.Selection([('White', 'White'), ('Blue', 'Blue'), ('Green', 'Green'), ('Orange', 'Orange'), ('Yellow', 'Yellow')], string='Kanban State', default='Blue', compute='set_state_kanban') kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', track_visibility='onchange') legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False) legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False) legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False) legend_delivery = fields.Char(related='stage_id.legend_delivery', string='Kanban Delivery Explanation', readonly=True, related_sudo=False) legend_erection = fields.Char(related='stage_id.legend_erection', string='Kanban Erection Explanation', readonly=True, related_sudo=False) color = fields.Integer(string='Color Index') user_id = fields.Many2one('res.users', string="User", default=lambda self: self.env.uid) kanbancolor = fields.Integer('Color Index', compute="set_kanban_color") @api.multi @api.depends('stage_id') def set_state_kanban(self): j = 0 k = 0 y = 0 f = 0 for record in self: if record.stage_id.sequence == 2: record.kanban_state = 'Blue' if record.stage_id.sequence == 5: record.kanban_state = 'Green' if record.stage_id.sequence == 4: record.kanban_state = 'Orange' if record.stage_id.sequence == 3: record.kanban_state = 'Yellow' if record.stage_id.sequence == 1: record.kanban_state = 'White' rech_item_fin = self.env['item.code'].sudo().search([ ('item_id', '=', record.item_id.id) ]) logging.info(rech_item_fin) if rech_item_fin: for i in rech_item_fin: if i.stage_id.sequence == 5: j += 1 if i.stage_id.sequence == 4: k += 1 if i.stage_id.sequence == 3: y += 1 if i.stage_id.sequence == 2: f += 1 if len(rech_item_fin) == j: rech_item_fin[0].item_id.sudo().write({'stage_id': 5}) elif len(rech_item_fin) == k: rech_item_fin[0].item_id.sudo().write({'stage_id': 4}) elif len(rech_item_fin) == y: rech_item_fin[0].item_id.sudo().write({'stage_id': 3}) elif len(rech_item_fin) == f: rech_item_fin[0].item_id.sudo().write({'stage_id': 2}) def set_kanban_color(self): for record in self: kanbancolor = 0 if record.kanban_state == 'Blue': kanbancolor = 4 elif record.kanban_state == 'Green': kanbancolor = 10 elif record.kanban_state == 'Orange': kanbancolor = 2 elif record.kanban_state == 'Yellow': kanbancolor = 3 else: kanbancolor = 0 record.kanbancolor = kanbancolor @api.depends('stage_id', 'kanban_state') def _compute_kanban_state_label(self): for task in self: if task.kanban_state == 'White': task.kanban_state_label = task.legend_normal if task.kanban_state == 'Blue': task.kanban_state_label = task.legend_delivery elif task.kanban_state == 'Orange': task.kanban_state_label = task.legend_erection elif task.kanban_state == 'Yellow': task.kanban_state_label = task.legend_blocked else: task.kanban_state_label = task.legend_done @api.model def _read_group_stage_ids(self, stages, domain, order): return self.env['project.item.type'].search([])
class MedicalDiagnosticReport(models.Model): _inherit = "medical.diagnostic.report" with_department = fields.Boolean(default=False) medical_department_header = fields.Html(readonly=True) signature_id = fields.Many2one("res.users.signature", readonly=True) occurrence_date = fields.Datetime(related="encounter_id.create_date") encounter_id = fields.Many2one(readonly=True) image_ids = fields.One2many( "medical.diagnostic.report.image", inverse_name="diagnostic_report_id", copy=True, readonly=True, ) def _generate_serializer(self): result = super(MedicalDiagnosticReport, self)._generate_serializer() if self.with_department: result.update( {"medical_department_header": self.medical_department_header}) if self.image_ids: result.update({ "images": [image._generate_serializer() for image in self.image_ids] }) if self.signature_id: result.update({"signature_id": self.signature_id.id}) return result def registered2final_change_state(self): res = super().registered2final_change_state() if not self.medical_department_id.without_practitioner: res["signature_id"] = self.env.user.current_signature_id.id return res def _is_editable(self): department = self.medical_department_id return super()._is_editable() and (not department or self.env.user in department.user_ids) @api.depends_context("uid") @api.depends("medical_department_id", "medical_department_id.user_ids") def _compute_is_editable(self): super()._compute_is_editable() def _is_cancellable(self): department = self.medical_department_id return super()._is_cancellable() and (not department or self.env.user in department.user_ids) @api.depends_context("uid") @api.depends("medical_department_id", "medical_department_id.user_ids") def _compute_is_cancellable(self): super()._compute_is_cancellable() def copy_action(self): self.ensure_one() result = self.copy() return result.get_formview_action() def _add_image_attachment_vals(self, name=None, datas=None, **kwargs): return { "diagnostic_report_id": self.id, "data": datas, "name": name, } def add_image_attachment(self, name=None, datas=None, **kwargs): self.ensure_one() if self.state != "registered": raise ValidationError(_("State must be registered")) self.env["medical.diagnostic.report.image"].create( self._add_image_attachment_vals(name=name, datas=datas, **kwargs)) return True def _get_image_grouped(self): self.ensure_one() lst = self.image_ids.ids n = 2 return [ self.env["medical.diagnostic.report.image"].browse(lst[i:i + n]) for i in range(0, len(lst), n) ]
class ItemNumber(models.Model): _name = 'item.number' _description = 'Items Records for Projects Lines' _inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin'] _rec_name = 'title' def _default_stage_id(self): """ Gives default stage_id """ project_id = self.env.context.get('default_project_id') if not project_id: return False return self.stage_find(project_id, [('fold', '=', False)]) title = fields.Char('Item No', required=True) drawing_id = fields.Many2one('construction.drawing', 'Drawing') project_id = fields.Many2one(related='drawing_id.project_id', store=True, string='Project', readonly=True) pricing_id = fields.Many2one( 'construction.pricing', String='Pricing', default=lambda self: self.env.context.get('drawing_id')) Type = fields.Char('Type') image_medium = fields.Binary(String='image', attachment=True) Type_of_finish = fields.Char('Type of finish') Length = fields.Float('Length', required=True) Width = fields.Float('Width', required=True) Height = fields.Float('Height', required=True) Thick = fields.Float('Thick', required=True) Quantity = fields.Integer('Quantity', required=True) Volume = fields.Float('Volume', compute='_compute_total', required=True) Unit = fields.Many2one('uom.uom', 'Unit Of Measure') # get the unit pricing for each department UR_production = fields.Float(String='UR Production', compute='onchange_pricing_id') UR_delivery = fields.Float(String='UR Delivery', compute='onchange_pricing_id') UR_erection = fields.Float(String='UR Erection', compute='onchange_pricing_id') #get the amount of departments Amount_prod = fields.Float(String='Amount Production', compute='_compute_total_production', required=True) Amount_deli = fields.Float(String='Amount Delivery', compute='_compute_total_delivery', required=True) Amount_erec = fields.Float(String='Amount Erection', compute='_compute_total_erection', required=True) # get the Total unit sum of three units UR_total = fields.Float(String='Unit Rate Total', compute='_compute_total_UR', required=True) Amount_total = fields.Float(String='Amount Total', compute='_compute_total_amount', required=True) # the unit rate of project with currency currency_id = fields.Many2one("res.currency", compute='get_currency_id', string="Currency") Unit_Production = fields.Float(String='Unit Production', compute='_compute_unit_production', required=True) Unit_Delivery = fields.Float(String='Unit Delivery', compute='_compute_unit_delivery', required=True) Unit_Erection = fields.Float(String='Unit Erection', compute='_compute_unit_erection', required=True) active = fields.Boolean( default=True, help= "If the active field is set to False, it will allow you to hide the estimation without removing it." ) stage_id = fields.Many2one( 'project.item.type', string='Stage', ondelete='restrict', track_visibility='onchange', index=True, copy=False, domain= "['|', ('project_ids', '=', False), ('project_ids', '=', project_id)]", group_expand='_read_group_stage_ids', default=lambda self: self.env['project.item.type'].search([], limit=1)) kanban_state = fields.Selection([('White', 'White'), ('Blue', 'Blue'), ('Green', 'Green'), ('Orange', 'Orange'), ('Yellow', 'Yellow')], string='Kanban State', default='Blue', compute='set_state_kanban') kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', track_visibility='onchange') legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False) legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False) legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False) legend_delivery = fields.Char(related='stage_id.legend_delivery', string='Kanban Delivery Explanation', readonly=True, related_sudo=False) legend_erection = fields.Char(related='stage_id.legend_erection', string='Kanban Erection Explanation', readonly=True, related_sudo=False) color = fields.Integer(string='Color Index') user_id = fields.Many2one('res.users', string="User", default=lambda self: self.env.uid) item_code_ids = fields.One2many('item.code', 'item_id', string='Code Items') sub_item_ids = fields.One2many('item.sub', 'item_id', string="Sub Items") sub_items_status = fields.Html(string="SUBItems", compute='get_all_sub_items') kanbancolor = fields.Integer('Color Index', compute="set_kanban_color") @api.multi @api.depends('stage_id') def set_state_kanban(self): j = 0 for record in self: if record.stage_id.sequence == 2: record.kanban_state = 'Blue' if record.stage_id.sequence == 5: record.kanban_state = 'Green' if record.stage_id.sequence == 4: record.kanban_state = 'Orange' if record.stage_id.sequence == 3: record.kanban_state = 'Yellow' if record.stage_id.sequence == 1: record.kanban_state = 'White' # for i in record.item_code_ids : # if i.stage_id.sequence == 4: # j += 1 # # if i.stage_id.sequence == 3: # # k += 1 # # if i.stage_id.sequence == 2: # # y += 1 # # if i.stage_id.sequence == 1: # # f += 1 # if len(record.item_code_ids): # if len(record.item_code_ids)==j: # record.sudo().write({'stage_id':4}) # record.kanban_state='Green' def set_kanban_color(self): for record in self: kanbancolor = 0 if record.kanban_state == 'Blue': kanbancolor = 4 elif record.kanban_state == 'Green': kanbancolor = 10 elif record.kanban_state == 'Orange': kanbancolor = 2 elif record.kanban_state == 'Yellow': kanbancolor = 3 else: kanbancolor = 0 record.kanbancolor = kanbancolor @api.multi @api.depends('project_id', 'sub_item_ids') def get_all_sub_items(self): for rec in self: if rec.sub_item_ids: sub_items = [] body = """<table style='width: 100%;'>""" body += """<tr>""" body += """<td style="width: 50%;border: 1px solid grey;">""" + 'Item Name' + """</td>""" body += """<td style="width: 50%;border: 1px solid grey;">""" + 'Stage' + """</td>""" body += """</tr>""" for data in rec.sub_item_ids: body += """<tr>""" body += """<td style="width: 50%;border: 1px solid grey;">""" + data.title + """</td>""" body += """<td style="width: 50%;border: 1px solid grey;">""" + data.stage_id.name + """</td>""" body += """</tr>""" body += "</table>" rec.sub_items_status = body @api.multi def action_open_sub_item(self): self.ensure_one() return { 'name': _('Sub Item'), 'domain': [('item_id', '=', self.id)], 'view_type': 'form', 'res_model': 'item.sub', 'view_id': False, 'view_mode': 'tree,form', 'type': 'ir.actions.act_window', 'context': "{'default_item_id': %d}" % (self.id) } @api.depends('stage_id', 'kanban_state') def _compute_kanban_state_label(self): for task in self: if task.kanban_state == 'White': task.kanban_state_label = task.legend_normal if task.kanban_state == 'Blue': task.kanban_state_label = task.legend_delivery elif task.kanban_state == 'Orange': task.kanban_state_label = task.legend_erection elif task.kanban_state == 'Yellow': task.kanban_state_label = task.legend_blocked else: task.kanban_state_label = task.legend_done @api.model def _read_group_stage_ids(self, stages, domain, order): return self.env['project.item.type'].search([]) ''' # get the unit rate price from pricing or this project @api.multi @api.onchange('pricing_id') def onchange_pricing_id(self): res = {} if not self.pricing_id: return res self.UR_production = self.pricing_id.UR_production self.UR_delivery = self.pricing_id.UR_delivery self.UR_erection = self.pricing_id.UR_erection ''' # get the unit rate price from pricing or this project @api.multi @api.depends('pricing_id') def onchange_pricing_id(self): res = {} for record in self: if not record.pricing_id: return res record.UR_production = record.pricing_id.UR_production record.UR_delivery = record.pricing_id.UR_delivery record.UR_erection = record.pricing_id.UR_erection #open the item codes @api.multi def open_bom(self): self.ensure_one() ctx = { 'default_item_id': self.id, 'default_project_id': self.project_id.id, 'default_title': self.title, } return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'item.code', 'target': 'new', 'context': ctx, } #Compute Volume of an item @api.multi @api.depends('Length', 'Width', 'Height') def _compute_total(self): for rec in self: rec.Volume = rec.Length * rec.Width * rec.Height #Compute the total units in the pricing @api.multi @api.depends('UR_production', 'UR_delivery', 'UR_erection') def _compute_total_UR(self): for rec in self: rec.UR_total = rec.UR_production + rec.UR_delivery + rec.UR_erection #Compute the total amounts of departments @api.multi @api.depends('Amount_prod', 'Amount_deli', 'Amount_erec') def _compute_total_amount(self): for rec in self: rec.Amount_total = rec.Amount_prod + rec.Amount_deli + rec.Amount_erec @api.multi @api.depends('Amount_prod', 'Quantity', 'Unit_Production') def _compute_total_production(self): for rec in self: rec.Amount_prod = rec.Quantity * rec.Unit_Production @api.multi @api.depends('Amount_deli', 'Unit_Delivery', 'Quantity') def _compute_total_delivery(self): for rec in self: rec.Amount_deli = rec.Quantity * rec.Unit_Delivery @api.multi @api.depends('Amount_erec', 'Unit_Erection', 'Quantity') def _compute_total_erection(self): for rec in self: rec.Amount_erec = rec.Quantity * rec.Unit_Erection # Compute Unit rates of amount @api.multi @api.depends('Unit_Production', 'UR_production', 'Volume') def _compute_unit_production(self): for rec in self: rec.Unit_Production = rec.UR_production * rec.Volume @api.multi @api.depends('Unit_Delivery', 'UR_delivery', 'Volume') def _compute_unit_delivery(self): for rec in self: rec.Unit_Delivery = rec.UR_delivery * rec.Volume @api.multi @api.depends('Unit_Erection', 'UR_erection', 'Volume') def _compute_unit_erection(self): for rec in self: rec.Unit_Erection = rec.UR_erection * rec.Volume @api.multi def get_currency_id(self): user_id = self.env.uid res_user_id = self.env['res.users'].browse(user_id) for line in self: line.currency_id = res_user_id.company_id.currency_id.id
class BeginBalanceCheck(models.TransientModel): '''启用期初试算平衡向导''' _name = 'accountcore.begin_balance_check' org_ids = fields.Many2many('accountcore.org', string='待检查机构', required=True, default=lambda s: s.env.user.currentOrg) result = fields.Html(string='检查结果') @api.multi def do_check(self, *args): '''对选中机构执行平衡检查''' self.ensure_one() check_result = {} result_htmlStr = '' for org in self.org_ids: check_result[org.name] = self._check(org) for (key, value) in check_result.items(): result_htmlStr = result_htmlStr+"<h6>" + \ key+"</h6>"+"".join([v[1] for v in value]) self.result = result_htmlStr return { 'name': '启用期初平衡检查', 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'target': 'new', 'res_model': 'accountcore.begin_balance_check', 'res_id': self.id, } def _check(self, org): '''对一个机构执行平衡检查''' rl = [] # 获得机构期初 balance_records = AccountsBalance.getBeginOfOrg(org) # 检查月初本年累计发生额借方合计=贷方合计 rl.append(self._checkCumulativeAmountBalance(balance_records)) # 检查月初余额借方合计=贷方合计 rl.append(self._checkBeginingAmountBalance(balance_records)) # 检查月已发生额借方合计=贷方合计 rl.append(self._checkAmountBalance(balance_records)) # 检查资产=负债+所有者权益+收入-理论 rl.append(self._checkBalance(balance_records)) return rl def _checkCumulativeAmountBalance(self, balance_records): '''检查月初本年累计发生额借方合计''' damount = AccountsBalance._sumFieldOf('cumulativeDamount', balance_records) camount = AccountsBalance._sumFieldOf('cumulativeCamount', balance_records) imbalanceAmount = damount - camount if imbalanceAmount == 0: rl_html = "<div><span class='text-success fa fa-check'></span>月初本年借方累计发生额=月初本年贷方累计发生额[" + \ str(damount) + "="+str(camount)+"]</div>" return (True, rl_html) else: rl_html = "<div><span class='text-danger fa fa-close'></span>月初本年借方累计发生额合计=月初本年贷方累计发生额合计[" + \ str(damount)+"-" + str(camount) + \ "="+str(imbalanceAmount)+"]</div>" return (False, rl_html) def _checkBeginingAmountBalance(self, balance_records): '''检查月初余额借方合计''' damount = AccountsBalance._sumFieldOf('beginingDamount', balance_records) camount = AccountsBalance._sumFieldOf('beginingCamount', balance_records) imbalanceAmount = damount - camount if imbalanceAmount == 0: rl_html = "<div><span class='text-success fa fa-check'></span>月初借方余额合计=月初贷方贷方余额合计[" + \ str(damount) + "=" + str(camount) + "]</div>" return (True, rl_html) else: rl_html = "<div><span class='text-danger fa fa-close'></span>月初借方余额合计=月初贷方余额合计[" + \ str(damount) + "-" + str(camount) + \ "="+str(imbalanceAmount)+"]</div>" return (False, rl_html) def _checkAmountBalance(self, balance_records): '''检查月已发生额借方合计''' damount = AccountsBalance._sumFieldOf('damount', balance_records) camount = AccountsBalance._sumFieldOf('camount', balance_records) imbalanceAmount = damount - camount if imbalanceAmount == 0: rl_html = "<div><span class='text-success fa fa-check'></span>月借方已发生额合计=月贷方已发生额合计[" + \ str(damount) + "=" + str(camount) + "]</div>" return (True, rl_html) else: rl_html = "<div><span class='text-danger fa fa-exclamation'></span>月借方已发生额合计=月贷方已发生额合计[" + \ str(damount) + "-" + str(camount) + \ "="+str(imbalanceAmount)+"]</div>" return (False, rl_html) def _checkBalance(self, balance_records): '''检查资产=负债+所有者权益+收入-成本''' return (True, ".....")
class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' has_accounting_entries = fields.Boolean( compute='_compute_has_chart_of_accounts') currency_id = fields.Many2one('res.currency', related="company_id.currency_id", required=True, readonly=False, string='Currency', help="Main currency of the company.") currency_exchange_journal_id = fields.Many2one( comodel_name='account.journal', related='company_id.currency_exchange_journal_id', readonly=False, string="Currency Exchange Journal", domain="[('company_id', '=', company_id), ('type', '=', 'general')]", help= 'The accounting journal where automatic exchange differences will be registered' ) income_currency_exchange_account_id = fields.Many2one( comodel_name="account.account", related="company_id.income_currency_exchange_account_id", string="Gain Account", readonly=False, domain=lambda self: "[('internal_type', '=', 'other'), ('deprecated', '=', False), ('company_id', '=', company_id),\ ('user_type_id', 'in', %s)]" % [ self.env.ref('account.data_account_type_revenue').id, self.env.ref('account.data_account_type_other_income').id ]) expense_currency_exchange_account_id = fields.Many2one( comodel_name="account.account", related="company_id.expense_currency_exchange_account_id", string="Loss Account", readonly=False, domain=lambda self: "[('internal_type', '=', 'other'), ('deprecated', '=', False), ('company_id', '=', company_id),\ ('user_type_id', '=', %s)]" % self.env.ref( 'account.data_account_type_expenses').id) has_chart_of_accounts = fields.Boolean( compute='_compute_has_chart_of_accounts', string='Company has a chart of accounts') chart_template_id = fields.Many2one( 'account.chart.template', string='Template', default=lambda self: self.env.company.chart_template_id, domain="[('visible','=', True)]") sale_tax_id = fields.Many2one('account.tax', string="Default Sale Tax", related='company_id.account_sale_tax_id', readonly=False) purchase_tax_id = fields.Many2one( 'account.tax', string="Default Purchase Tax", related='company_id.account_purchase_tax_id', readonly=False) tax_calculation_rounding_method = fields.Selection( related='company_id.tax_calculation_rounding_method', string='Tax calculation rounding method', readonly=False) module_account_accountant = fields.Boolean(string='Accounting') group_analytic_accounting = fields.Boolean( string='Analytic Accounting', implied_group='analytic.group_analytic_accounting') group_analytic_tags = fields.Boolean( string='Analytic Tags', implied_group='analytic.group_analytic_tags') group_warning_account = fields.Boolean( string="Warnings in Invoices", implied_group='account.group_warning_account') group_cash_rounding = fields.Boolean( string="Cash Rounding", implied_group='account.group_cash_rounding') # group_show_line_subtotals_tax_excluded and group_show_line_subtotals_tax_included are opposite, # so we can assume exactly one of them will be set, and not the other. # We need both of them to coexist so we can take advantage of automatic group assignation. group_show_line_subtotals_tax_excluded = fields.Boolean( "Show line subtotals without taxes (B2B)", implied_group='account.group_show_line_subtotals_tax_excluded', group='base.group_portal,base.group_user,base.group_public') group_show_line_subtotals_tax_included = fields.Boolean( "Show line subtotals with taxes (B2C)", implied_group='account.group_show_line_subtotals_tax_included', group='base.group_portal,base.group_user,base.group_public') group_show_sale_receipts = fields.Boolean( string='Sale Receipt', implied_group='account.group_sale_receipts') group_show_purchase_receipts = fields.Boolean( string='Purchase Receipt', implied_group='account.group_purchase_receipts') show_line_subtotals_tax_selection = fields.Selection( [('tax_excluded', 'Tax-Excluded'), ('tax_included', 'Tax-Included')], string="Line Subtotals Tax Display", required=True, default='tax_excluded', config_parameter='account.show_line_subtotals_tax_selection') module_account_budget = fields.Boolean(string='Budget Management') module_account_payment = fields.Boolean(string='Invoice Online Payment') module_account_reports = fields.Boolean("Dynamic Reports") module_account_check_printing = fields.Boolean( "Allow check printing and deposits") module_account_batch_payment = fields.Boolean( string='Use batch payments', help= 'This allows you grouping payments into a single batch and eases the reconciliation process.\n' '-This installs the account_batch_payment module.') module_account_sepa = fields.Boolean(string='SEPA Credit Transfer (SCT)') module_account_sepa_direct_debit = fields.Boolean( string='Use SEPA Direct Debit') module_account_plaid = fields.Boolean(string="Plaid Connector") module_account_yodlee = fields.Boolean( "Bank Interface - Sync your bank feeds automatically") module_account_bank_statement_import_qif = fields.Boolean( "Import .qif files") module_account_bank_statement_import_ofx = fields.Boolean( "Import in .ofx format") module_account_bank_statement_import_csv = fields.Boolean( "Import in .csv format") module_account_bank_statement_import_camt = fields.Boolean( "Import in CAMT.053 format") module_currency_rate_live = fields.Boolean( string="Automatic Currency Rates") module_account_intrastat = fields.Boolean(string='Intrastat') module_product_margin = fields.Boolean(string="Allow Product Margin") module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT") module_account_taxcloud = fields.Boolean(string="Account TaxCloud") module_account_invoice_extract = fields.Boolean( string="Bill Digitalization") module_snailmail_account = fields.Boolean(string="Snailmail") tax_exigibility = fields.Boolean(string='Cash Basis', related='company_id.tax_exigibility', readonly=False) tax_cash_basis_journal_id = fields.Many2one( 'account.journal', related='company_id.tax_cash_basis_journal_id', string="Tax Cash Basis Journal", readonly=False) account_cash_basis_base_account_id = fields.Many2one( comodel_name='account.account', string="Base Tax Received Account", readonly=False, related='company_id.account_cash_basis_base_account_id', domain=[('deprecated', '=', False)]) qr_code = fields.Boolean(string='Display SEPA QR-code', related='company_id.qr_code', readonly=False) invoice_is_print = fields.Boolean(string='Print', related='company_id.invoice_is_print', readonly=False) invoice_is_email = fields.Boolean(string='Send Email', related='company_id.invoice_is_email', readonly=False) incoterm_id = fields.Many2one( 'account.incoterms', string='Default incoterm', related='company_id.incoterm_id', help= 'International Commercial Terms are a series of predefined commercial terms used in international transactions.', readonly=False) invoice_terms = fields.Text(related='company_id.invoice_terms', string="Terms & Conditions", readonly=False) invoice_terms_html = fields.Html(related='company_id.invoice_terms_html', string="Terms & Conditions as a Web page", readonly=False) terms_type = fields.Selection(related='company_id.terms_type', readonly=False) preview_ready = fields.Boolean(string="Display preview button", compute='_compute_terms_preview') use_invoice_terms = fields.Boolean( string='Default Terms & Conditions', config_parameter='account.use_invoice_terms') # Technical field to hide country specific fields from accounting configuration country_code = fields.Char(related='company_id.country_id.code', readonly=True) def set_values(self): super(ResConfigSettings, self).set_values() if self.group_multi_currency: self.env.ref('base.group_user').write({ 'implied_ids': [(4, self.env.ref('product.group_sale_pricelist').id)] }) # install a chart of accounts for the given company (if required) if self.env.company == self.company_id and self.chart_template_id and self.chart_template_id != self.company_id.chart_template_id: self.chart_template_id._load(15.0, 15.0, self.env.company) @api.depends('company_id') def _compute_has_chart_of_accounts(self): self.has_chart_of_accounts = bool(self.company_id.chart_template_id) self.has_accounting_entries = self.env[ 'account.chart.template'].existing_accounting(self.company_id) @api.onchange('show_line_subtotals_tax_selection') def _onchange_sale_tax(self): if self.show_line_subtotals_tax_selection == "tax_excluded": self.update({ 'group_show_line_subtotals_tax_included': False, 'group_show_line_subtotals_tax_excluded': True, }) else: self.update({ 'group_show_line_subtotals_tax_included': True, 'group_show_line_subtotals_tax_excluded': False, }) @api.onchange('group_analytic_accounting') def onchange_analytic_accounting(self): if self.group_analytic_accounting: self.module_account_accountant = True @api.onchange('module_account_budget') def onchange_module_account_budget(self): if self.module_account_budget: self.group_analytic_accounting = True @api.onchange('module_account_yodlee') def onchange_account_yodlee(self): if self.module_account_yodlee: self.module_account_plaid = True @api.onchange('tax_exigibility') def _onchange_tax_exigibility(self): res = {} tax = self.env['account.tax'].search( [('company_id', '=', self.env.company.id), ('tax_exigibility', '=', 'on_payment')], limit=1) if not self.tax_exigibility and tax: self.tax_exigibility = True res['warning'] = { 'title': _('Error!'), 'message': _('You cannot disable this setting because some of your taxes are cash basis. ' 'Modify your taxes first before disabling this setting.') } return res @api.depends('terms_type') def _compute_terms_preview(self): for setting in self: # We display the preview button only if the terms_type is html in the setting but also on the company # to avoid landing on an error page (see terms.py controller) setting.preview_ready = self.env.company.terms_type == 'html' and setting.terms_type == 'html' @api.model def create(self, values): # Optimisation purpose, saving a res_config even without changing any values will trigger the write of all # related values, including the currency_id field on res_company. This in turn will trigger the recomputation # of account_move_line related field company_currency_id which can be slow depending on the number of entries # in the database. Thus, if we do not explicitly change the currency_id, we should not write it on the company if ('company_id' in values and 'currency_id' in values): company = self.env['res.company'].browse(values.get('company_id')) if company.currency_id.id == values.get('currency_id'): values.pop('currency_id') return super(ResConfigSettings, self).create(values)
class MassMailing(models.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' _inherits = {'utm.source': 'source_id'} _rec_name = "source_id" @api.model def _get_default_mail_server_id(self): server_id = self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mail_server_id') try: server_id = literal_eval(server_id) if server_id else False return self.env['ir.mail_server'].search([('id', '=', server_id)]).id except ValueError: return False @api.model def default_get(self, fields): res = super(MassMailing, self).default_get(fields) if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model_real'): if res['mailing_model_real'] in ['res.partner', 'mail.mass_mailing.contact']: res['reply_to_mode'] = 'email' else: res['reply_to_mode'] = 'thread' return res active = fields.Boolean(default=True) subject = fields.Char('Subject', help='Subject of emails to send', required=True) email_from = fields.Char(string='From', required=True, default=lambda self: self.env['mail.message']._get_default_from()) sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False) schedule_date = fields.Datetime(string='Schedule in the Future') # don't translate 'body_arch', the translations are only on 'body_html' body_arch = fields.Html(string='Body', translate=False) body_html = fields.Html(string='Body converted to be send by mail', sanitize_attributes=False) attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel', 'mass_mailing_id', 'attachment_id', string='Attachments') keep_archives = fields.Boolean(string='Keep Archives') mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') campaign_id = fields.Many2one('utm.campaign', string='Campaign', help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special") source_id = fields.Many2one('utm.source', string='Source', required=True, ondelete='cascade', help="This is the link source, e.g. Search Engine, another domain, or name of email list") medium_id = fields.Many2one('utm.medium', string='Medium', help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email')) clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of Clicks") state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'), ('sending', 'Sending'), ('done', 'Sent')], string='Status', required=True, copy=False, default='draft', group_expand='_group_expand_states') color = fields.Integer(string='Color Index') user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user) # mailing options reply_to_mode = fields.Selection( [('thread', 'Recipient Followers'), ('email', 'Specified Email Address')], string='Reply-To Mode', required=True) reply_to = fields.Char(string='Reply To', help='Preferred Reply-To Address', default=lambda self: self.env['mail.message']._get_default_from()) # recipients mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True) mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)], default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id) mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name', readonly=True, related_sudo=True) mailing_domain = fields.Char(string='Domain', oldname='domain', default=[]) mail_server_id = fields.Many2one('ir.mail_server', string='Mail Server', default=_get_default_mail_server_id, help="Use a specific mail server in priority. Otherwise Odoo relies on the first outgoing mail server available (based on their sequencing) as it does for normal mails.") contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel', string='Mailing Lists') contact_ab_pc = fields.Integer(string='A/B Testing percentage', help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.', default=100) # statistics data statistics_ids = fields.One2many('mail.mail.statistics', 'mass_mailing_id', string='Emails Statistics') total = fields.Integer(compute="_compute_total") scheduled = fields.Integer(compute="_compute_statistics") expected = fields.Integer(compute="_compute_statistics") ignored = fields.Integer(compute="_compute_statistics") sent = fields.Integer(compute="_compute_statistics") delivered = fields.Integer(compute="_compute_statistics") opened = fields.Integer(compute="_compute_statistics") clicked = fields.Integer(compute="_compute_statistics") replied = fields.Integer(compute="_compute_statistics") bounced = fields.Integer(compute="_compute_statistics") failed = fields.Integer(compute="_compute_statistics") received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio') opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio') replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio') bounced_ratio = fields.Integer(compute="_compute_statistics", String='Bounced Ratio') next_departure = fields.Datetime(compute="_compute_next_departure", string='Scheduled date') def _compute_total(self): for mass_mailing in self: mass_mailing.total = len(mass_mailing.sudo().get_recipients()) def _compute_clicks_ratio(self): self.env.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(self.ids), )) mass_mailing_data = self.env.cr.dictfetchall() mapped_data = dict([(m['id'], 100 * m['nb_clicks'] / m['nb_mails']) for m in mass_mailing_data]) for mass_mailing in self: mass_mailing.clicks_ratio = mapped_data.get(mass_mailing.id, 0) @api.depends('mailing_model_id') def _compute_model(self): for record in self: record.mailing_model_real = (record.mailing_model_name != 'mail.mass_mailing.list') and record.mailing_model_name or 'mail.mass_mailing.contact' def _compute_statistics(self): """ Compute statistics of the mass mailing """ self.env.cr.execute(""" SELECT m.id as mailing_id, COUNT(s.id) AS expected, 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 AND s.ignored 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.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored, 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.clicked is not null THEN 1 ELSE null END) AS clicked, 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(self.ids), )) for row in self.env.cr.dictfetchall(): total = row['expected'] = (row['expected'] - row['ignored']) or 1 row['received_ratio'] = 100.0 * row['delivered'] / total row['opened_ratio'] = 100.0 * row['opened'] / total row['clicks_ratio'] = 100.0 * row['clicked'] / total row['replied_ratio'] = 100.0 * row['replied'] / total row['bounced_ratio'] = 100.0 * row['bounced'] / total self.browse(row.pop('mailing_id')).update(row) @api.multi def _unsubscribe_token(self, res_id, email): """Generate a secure hash for this mailing list and parameters. This is appended to the unsubscription URL and then checked at unsubscription time to ensure no malicious unsubscriptions are performed. :param int res_id: ID of the resource that will be unsubscribed. :param str email: Email of the resource that will be unsubscribed. """ secret = self.env["ir.config_parameter"].sudo().get_param( "database.secret") token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email)) return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest() def _compute_next_departure(self): cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall str2dt = fields.Datetime.from_string cron_time = str2dt(cron_next_call) for mass_mailing in self: if mass_mailing.schedule_date: schedule_date = str2dt(mass_mailing.schedule_date) mass_mailing.next_departure = max(schedule_date, cron_time) else: mass_mailing.next_departure = cron_time @api.onchange('mass_mailing_campaign_id') def _onchange_mass_mailing_campaign_id(self): if self.mass_mailing_campaign_id: dic = {'campaign_id': self.mass_mailing_campaign_id.campaign_id, 'source_id': self.mass_mailing_campaign_id.source_id, 'medium_id': self.mass_mailing_campaign_id.medium_id} self.update(dic) @api.onchange('mailing_model_id', 'contact_list_ids') def _onchange_model_and_list(self): mailing_domain = [] if self.mailing_model_name: if self.mailing_model_name == 'mail.mass_mailing.list': if self.contact_list_ids: mailing_domain.append(('list_ids', 'in', self.contact_list_ids.ids)) else: mailing_domain.append((0, '=', 1)) elif self.mailing_model_name == 'res.partner': mailing_domain.append(('customer', '=', True)) elif 'opt_out' in self.env[self.mailing_model_name]._fields and not self.mailing_domain: mailing_domain.append(('opt_out', '=', False)) else: mailing_domain.append((0, '=', 1)) self.mailing_domain = repr(mailing_domain) @api.onchange('subject') def _onchange_subject(self): if self.subject and not self.name: self.name = self.subject #------------------------------------------------------ # Technical stuff #------------------------------------------------------ @api.model def name_create(self, name): """ _rec_name is source_id, creates a utm.source instead """ mass_mailing = self.create({'name': name, 'subject': name}) return mass_mailing.name_get()[0] @api.model def create(self, vals): if vals.get('name') and not vals.get('subject'): vals['subject'] = vals['name'] return super(MassMailing, self).create(vals) @api.multi @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_('%s (copy)') % self.name) return super(MassMailing, self).copy(default=default) def _group_expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] def update_opt_out(self, email, list_ids, value): if len(list_ids) > 0: model = self.env['mail.mass_mailing.contact'].with_context(active_test=False) records = model.search([('email_normalized', '=', tools.email_normalize(email))]) opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([ ('contact_id', 'in', records.ids), ('list_id', 'in', list_ids), ('opt_out', '!=', value) ]) opt_out_records.write({'opt_out': value}) message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \ if value else _('The recipient <strong>subscribed to %s</strong> mailing list(s)') for record in records: # filter the list_id by record record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id) if len(record_lists) > 0: record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id')))) #------------------------------------------------------ # Views & Actions #------------------------------------------------------ @api.multi def action_duplicate(self): self.ensure_one() mass_mailing_copy = self.copy() if mass_mailing_copy: context = dict(self.env.context) context['form_view_initial_mode'] = 'edit' return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.mass_mailing', 'res_id': mass_mailing_copy.id, 'context': context, } return False @api.multi def action_test_mailing(self): self.ensure_one() ctx = dict(self.env.context, default_mass_mailing_id=self.id) return { 'name': _('Test Mailing'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'mail.mass_mailing.test', 'target': 'new', 'context': ctx, } @api.multi def action_schedule_date(self): self.ensure_one() action = self.env.ref('mass_mailing.mass_mailing_schedule_date_action').read()[0] action['context'] = dict(self.env.context, default_mass_mailing_id=self.id) return action @api.multi def put_in_queue(self): self.write({'state': 'in_queue'}) @api.multi def cancel_mass_mailing(self): self.write({'state': 'draft', 'schedule_date': False}) @api.multi def retry_failed_mail(self): failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) failed_mails.mapped('statistics_ids').unlink() failed_mails.sudo().unlink() self.write({'state': 'in_queue'}) def action_view_sent(self): return self._action_view_documents_filtered('sent') def action_view_opened(self): return self._action_view_documents_filtered('opened') def action_view_replied(self): return self._action_view_documents_filtered('replied') def action_view_bounced(self): return self._action_view_documents_filtered('bounced') def action_view_clicked(self): return self._action_view_documents_filtered('clicked') def action_view_delivered(self): return self._action_view_documents_filtered('delivered') def _action_view_documents_filtered(self, view_filter): if view_filter in ('sent', 'opened', 'replied', 'bounced', 'clicked'): opened_stats = self.statistics_ids.filtered(lambda stat: stat[view_filter]) elif view_filter == ('delivered'): opened_stats = self.statistics_ids.filtered(lambda stat: stat.sent and not stat.bounced) else: opened_stats = self.env['mail.mail.statistics'] res_ids = opened_stats.mapped('res_id') model_name = self.env['ir.model']._get(self.mailing_model_real).display_name return { 'name': model_name, 'type': 'ir.actions.act_window', 'view_mode': 'tree', 'res_model': self.mailing_model_real, 'domain': [('id', 'in', res_ids)], } #------------------------------------------------------ # Email Sending #------------------------------------------------------ def _get_opt_out_list(self): """Returns a set of emails opted-out in target model""" self.ensure_one() opt_out = {} target = self.env[self.mailing_model_real] if self.mailing_model_real == "mail.mass_mailing.contact": # if user is opt_out on One list but not on another # or if two user with same email address, one opted in and the other one opted out, send the mail anyway # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in target_list_contacts = self.env['mail.mass_mailing.list_contact_rel'].search( [('list_id', 'in', self.contact_list_ids.ids)]) opt_out_contacts = target_list_contacts.filtered(lambda rel: rel.opt_out).mapped('contact_id.email_normalized') opt_in_contacts = target_list_contacts.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email_normalized') opt_out = set(c for c in opt_out_contacts if c not in opt_in_contacts) _logger.info( "Mass-mailing %s targets %s, blacklist: %s emails", self, target._name, len(opt_out)) else: _logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name) return opt_out def _get_convert_links(self): self.ensure_one() utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self vals = {'mass_mailing_id': self.id} if self.mass_mailing_campaign_id: vals['mass_mailing_campaign_id'] = self.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 return vals def _get_seen_list(self): """Returns a set of emails already targeted by current mailing/campaign (no duplicates)""" self.ensure_one() target = self.env[self.mailing_model_real] if set(['email', 'email_from']) & set(target._fields): mail_field = 'email' if 'email' in target._fields else 'email_from' # avoid loading a large number of records in memory # + use a basic heuristic for extracting emails query = """ SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) FROM mail_mail_statistics s JOIN %(target)s t ON (s.res_id = t.id) WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL """ elif 'partner_id' in target._fields: mail_field = 'email' query = """ SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) FROM mail_mail_statistics s JOIN %(target)s t ON (s.res_id = t.id) JOIN res_partner p ON (t.partner_id = p.id) WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL """ else: raise UserError(_("Unsupported mass mailing model %s") % self.mailing_model_id.name) if self.mass_mailing_campaign_id.unique_ab_testing: query +=""" AND s.mass_mailing_campaign_id = %%(mailing_campaign_id)s; """ else: query +=""" AND s.mass_mailing_id = %%(mailing_id)s AND s.model = %%(target_model)s; """ query = query % {'target': target._table, 'mail_field': mail_field} params = {'mailing_id': self.id, 'mailing_campaign_id': self.mass_mailing_campaign_id.id, 'target_model': self.mailing_model_real} self._cr.execute(query, params) seen_list = set(m[0] for m in self._cr.fetchall()) _logger.info( "Mass-mailing %s has already reached %s %s emails", self, len(seen_list), target._name) return seen_list def _get_mass_mailing_context(self): """Returns extra context items with pre-filled blacklist and seen list for massmailing""" return { 'mass_mailing_opt_out_list': self._get_opt_out_list(), 'mass_mailing_seen_list': self._get_seen_list(), 'post_convert_links': self._get_convert_links(), } def get_recipients(self): if self.mailing_domain: domain = safe_eval(self.mailing_domain) res_ids = self.env[self.mailing_model_real].search(domain).ids else: res_ids = [] domain = [('id', 'in', res_ids)] # randomly choose a fragment if self.contact_ab_pc < 100: contact_nbr = self.env[self.mailing_model_real].search_count(domain) topick = int(contact_nbr / 100.0 * self.contact_ab_pc) if self.mass_mailing_campaign_id and self.mass_mailing_campaign_id.unique_ab_testing: already_mailed = self.mass_mailing_campaign_id.get_recipients()[self.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): res_ids = self.get_recipients() already_mailed = self.env['mail.mail.statistics'].search_read([('model', '=', self.mailing_model_real), ('res_id', 'in', res_ids), ('mass_mailing_id', '=', self.id)], ['res_id']) 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, res_ids=None): author_id = self.env.user.partner_id.id for mailing in self: if not res_ids: res_ids = mailing.get_remaining_recipients() if not res_ids: raise UserError(_('There is no recipients selected.')) composer_values = { 'author_id': author_id, 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], 'body': mailing.body_html, 'subject': mailing.subject, 'model': mailing.mailing_model_real, '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', 'template_id': None, 'mail_server_id': mailing.mail_server_id.id, } if mailing.reply_to_mode == 'email': composer_values['reply_to'] = mailing.reply_to composer = self.env['mail.compose.message'].with_context(active_ids=res_ids).create(composer_values) extra_context = self._get_mass_mailing_context() composer = composer.with_context(active_ids=res_ids, **extra_context) # auto-commit except in testing mode auto_commit = not getattr(threading.currentThread(), 'testing', False) composer.send_mail(auto_commit=auto_commit) mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()}) return True def convert_links(self): res = {} for mass_mailing in self: 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.env['link.tracker'].convert_links(html, vals, blacklist=['/unsubscribe_from_list']) return res @api.model def _process_mass_mailing_queue(self): mass_mailings = self.search([('state', 'in', ('in_queue', 'sending')), '|', ('schedule_date', '<', fields.Datetime.now()), ('schedule_date', '=', False)]) for mass_mailing in mass_mailings: user = mass_mailing.write_uid or self.env.user mass_mailing = mass_mailing.with_context(**user.sudo(user=user).context_get()) if len(mass_mailing.get_remaining_recipients()) > 0: mass_mailing.state = 'sending' mass_mailing.send_mail() else: mass_mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
class MailActivity(models.Model): """ An actual activity to perform. Activities are linked to documents using res_id and res_model_id fields. Activities have a deadline that can be used in kanban view to display a status. Once done activities are unlinked and a message is posted. This message has a new activity_type_id field that indicates the activity linked to the message. """ _name = 'mail.activity' _description = 'Activity' _order = 'date_deadline ASC' _rec_name = 'summary' @api.model def default_get(self, fields): res = super(MailActivity, self).default_get(fields) if not fields or 'res_model_id' in fields and res.get('res_model'): res['res_model_id'] = self.env['ir.model']._get( res['res_model']).id return res # owner res_id = fields.Integer('Related Document ID', index=True, required=True) res_model_id = fields.Many2one('ir.model', 'Document Model', index=True, ondelete='cascade', required=True) res_model = fields.Char('Related Document Model', index=True, related='res_model_id.model', store=True, readonly=True) res_name = fields.Char('Document Name', compute='_compute_res_name', store=True, help="Display name of the related document.", readonly=True) # activity activity_type_id = fields.Many2one( 'mail.activity.type', 'Activity', domain= "['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]", ondelete='restrict') activity_category = fields.Selection(related='activity_type_id.category') icon = fields.Char('Icon', related='activity_type_id.icon') summary = fields.Char('Summary') note = fields.Html('Note') feedback = fields.Html('Feedback') date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.today) automated = fields.Boolean( 'Automated activity', readonly=True, help= 'Indicates this activity has been created automatically and not by any user.' ) # description user_id = fields.Many2one('res.users', 'Assigned to', default=lambda self: self.env.user, index=True, required=True) state = fields.Selection([('overdue', 'Overdue'), ('today', 'Today'), ('planned', 'Planned')], 'State', compute='_compute_state') recommended_activity_type_id = fields.Many2one( 'mail.activity.type', string="Recommended Activity Type") previous_activity_type_id = fields.Many2one( 'mail.activity.type', string='Previous Activity Type') has_recommended_activities = fields.Boolean( 'Next activities available', compute='_compute_has_recommended_activities', help='Technical field for UX purpose') @api.multi @api.onchange('previous_activity_type_id') def _compute_has_recommended_activities(self): for record in self: record.has_recommended_activities = bool( record.previous_activity_type_id.next_type_ids) @api.depends('res_model', 'res_id') def _compute_res_name(self): for activity in self: activity.res_name = self.env[activity.res_model].browse( activity.res_id).name_get()[0][1] @api.depends('date_deadline') def _compute_state(self): today_default = date.today() for record in self.filtered(lambda activity: activity.date_deadline): today = today_default if record.user_id.tz: today_utc = pytz.UTC.localize(datetime.utcnow()) today_tz = today_utc.astimezone( pytz.timezone(record.user_id.tz)) today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day) date_deadline = fields.Date.from_string(record.date_deadline) diff = (date_deadline - today) if diff.days == 0: record.state = 'today' elif diff.days < 0: record.state = 'overdue' else: record.state = 'planned' @api.onchange('activity_type_id') def _onchange_activity_type_id(self): if self.activity_type_id: self.summary = self.activity_type_id.summary self.date_deadline = (datetime.now() + timedelta(days=self.activity_type_id.days)) @api.onchange('previous_activity_type_id') def _onchange_previous_activity_type_id(self): if self.previous_activity_type_id.next_type_ids: self.recommended_activity_type_id = self.previous_activity_type_id.next_type_ids[ 0] @api.onchange('recommended_activity_type_id') def _onchange_recommended_activity_type_id(self): self.activity_type_id = self.recommended_activity_type_id @api.multi def _check_access(self, operation): """ Rule to access activities * create: check write rights on related document; * write: rule OR write rights on document; * unlink: rule OR write rights on document; """ self.check_access_rights( operation, raise_exception=True) # will raise an AccessError if operation in ('write', 'unlink'): try: self.check_access_rule(operation) except exceptions.AccessError: pass else: return doc_operation = 'read' if operation == 'read' else 'write' activity_to_documents = dict() for activity in self.sudo(): activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id) for model, res_ids in activity_to_documents.items(): self.env[model].check_access_rights(doc_operation, raise_exception=True) try: self.env[model].browse(res_ids).check_access_rule( doc_operation) except exceptions.AccessError: raise exceptions.AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)' ) % (self._description, operation)) @api.model def create(self, values): # already compute default values to be sure those are computed using the current user values_w_defaults = self.default_get(self._fields.keys()) values_w_defaults.update(values) # continue as sudo because activities are somewhat protected activity = super(MailActivity, self.sudo()).create(values_w_defaults) activity_user = activity.sudo(self.env.user) activity_user._check_access('create') self.env[activity_user.res_model].browse( activity_user.res_id).message_subscribe( partner_ids=[activity_user.user_id.partner_id.id]) if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), { 'type': 'activity_updated', 'activity_created': True }) return activity_user @api.multi def write(self, values): self._check_access('write') if values.get('user_id'): pre_responsibles = self.mapped('user_id.partner_id') res = super(MailActivity, self.sudo()).write(values) if values.get('user_id'): for activity in self: self.env[activity.res_model].browse( activity.res_id).message_subscribe( partner_ids=[activity.user_id.partner_id.id]) if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), { 'type': 'activity_updated', 'activity_created': True }) for activity in self: if activity.date_deadline <= fields.Date.today(): for partner in pre_responsibles: self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', partner.id), { 'type': 'activity_updated', 'activity_deleted': True }) return res @api.multi def unlink(self): self._check_access('unlink') for activity in self: if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), { 'type': 'activity_updated', 'activity_deleted': True }) return super(MailActivity, self.sudo()).unlink() @api.multi def action_done(self): """ Wrapper without feedback because web button add context as parameter, therefore setting context to feedback """ return self.action_feedback() def action_feedback(self, feedback=False): message = self.env['mail.message'] if feedback: self.write(dict(feedback=feedback)) for activity in self: record = self.env[activity.res_model].browse(activity.res_id) record.message_post_with_view( 'mail.message_activity_done', values={'activity': activity}, subtype_id=self.env['ir.model.data'].xmlid_to_res_id( 'mail.mt_activities'), mail_activity_type_id=activity.activity_type_id.id, ) message |= record.message_ids[0] self.unlink() return message.ids and message.ids[0] or False @api.multi def action_done_schedule_next(self): wizard_ctx = dict( self.env.context, default_previous_activity_type_id=self.activity_type_id.id, default_res_id=self.res_id, default_res_model=self.res_model, ) self.action_done() return { 'name': _('Schedule an Activity'), 'context': wizard_ctx, 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.activity', 'views': [(False, 'form')], 'type': 'ir.actions.act_window', 'target': 'new', } @api.multi def action_close_dialog(self): return {'type': 'ir.actions.act_window_close'}