コード例 #1
0
class AccountTaxTemplatePython(models.Model):
    _inherit = 'account.tax.template'

    amount_type = fields.Selection(selection_add=[('code', 'Python Code')])

    python_compute = fields.Text(
        string='Python Code',
        default="result = price_unit * 0.10",
        help=
        "Compute the amount of the tax by setting the variable 'result'.\n\n"
        ":param base_amount: float, actual amount on which the tax is applied\n"
        ":param price_unit: float\n"
        ":param quantity: float\n"
        ":param product: product.product recordset singleton or None\n"
        ":param partner: res.partner recordset singleton or None")
    python_applicable = fields.Text(
        string='Applicable Code',
        default="result = True",
        help=
        "Determine if the tax will be applied by setting the variable 'result' to True or False.\n\n"
        ":param price_unit: float\n"
        ":param quantity: float\n"
        ":param product: product.product recordset singleton or None\n"
        ":param partner: res.partner recordset singleton or None")

    def _get_tax_vals(self, company):
        """ This method generates a dictionnary of all the values for the tax that will be created.
        """
        self.ensure_one()
        res = super(AccountTaxTemplatePython, self)._get_tax_vals(company)
        res['python_compute'] = self.python_compute
        res['python_applicable'] = self.python_applicable
        return res
コード例 #2
0
class AccountingAssertTest(models.Model):
    _name = "accounting.assert.test"
    _order = "sequence"

    name = fields.Char(string='Test Name',
                       required=True,
                       index=True,
                       translate=True)
    desc = fields.Text(string='Test Description', index=True, translate=True)
    code_exec = fields.Text(string='Python code',
                            required=True,
                            default=CODE_EXEC_DEFAULT)
    active = fields.Boolean(default=True)
    sequence = fields.Integer(default=10)
コード例 #3
0
ファイル: document_page_history.py プロジェクト: ecoreos/hz
class DocumentPageHistory(models.Model):
    """This model is necessary to manage a document history."""

    _name = "document.page.history"
    _description = "Document Page History"
    _order = 'id DESC'
    _rec_name = "create_date"

    page_id = fields.Many2one('document.page', 'Page')
    summary = fields.Char('Summary', select=True)
    content = fields.Text("Content")
    create_date = fields.Datetime("Date")
    create_uid = fields.Many2one('res.users', "Modified By")

    def getDiff(self, v1, v2):
        """Return the difference between two version of document version."""
        text1 = self.browse(v1).content
        text2 = self.browse(v2).content
        line1 = line2 = ''
        if text1:
            line1 = text1.splitlines(1)
        if text2:
            line2 = text2.splitlines(1)
        if (not line1 and not line2) or (line1 == line2):
            return _('There are no changes in revisions.')
        else:
            diff = difflib.HtmlDiff()
            return diff.make_table(
                line1, line2,
                "Revision-{}".format(v1),
                "Revision-{}".format(v2),
                context=True
            )
コード例 #4
0
class SaaSPortalModule(models.Model):
    _name = 'saas_portal.module'

    name = fields.Char('Name')
    technical_name = fields.Char('Technical Name')
    summary = fields.Text('Summary')
    author = fields.Char('Author')
    url = fields.Char('URL')
    module_id = fields.Many2one('ir.module.module', required=False)

    @api.onchange('module_id')
    def onchange_module_id(self):
        if self.module_id:
            self.name = self.module_id.shortdesc
            self.technical_name = self.module_id.name
            self.summary = self.module_id.summary
            self.author = self.module_id.author
            self.url = self.module_id.url
        else:
            self.name, self.technical_name, self.summary, self.author, self.url = [
                False
            ] * 5

    _sql_constraints = [
        ('technical_name_uniq', 'unique(technical_name)',
         'The module already exists!'),
    ]
コード例 #5
0
class LunchCashMove(models.Model):
    """ Two types of cashmoves: payment (credit) or order (debit) """
    _name = 'lunch.cashmove'
    _description = 'lunch cashmove'

    user_id = fields.Many2one('res.users',
                              'User',
                              required=True,
                              default=lambda self: self.env.uid)
    date = fields.Date('Date',
                       required=True,
                       default=fields.Date.context_today)
    amount = fields.Float(
        'Amount',
        required=True,
        help=
        'Can be positive (payment) or negative (order or payment if user wants to get his money back)'
    )
    description = fields.Text('Description',
                              help='Can be an order or a payment')
    order_id = fields.Many2one('lunch.order.line', 'Order', ondelete='cascade')
    state = fields.Selection([('order', 'Order'), ('payment', 'Payment')],
                             'Is an order or a payment',
                             default='payment')

    @api.multi
    def name_get(self):
        return [(cashmove.id,
                 '%s %s' % (_('Lunch Cashmove'), '#%d' % cashmove.id))
                for cashmove in self]
コード例 #6
0
ファイル: hr_recruitment.py プロジェクト: LiberTang0/5
class RecruitmentStage(models.Model):
    _name = "hr.recruitment.stage"
    _description = "Stage of Recruitment"
    _order = 'sequence'

    name = fields.Char("Stage name", required=True, translate=True)
    sequence = fields.Integer(
        "Sequence",
        default=1,
        help="Gives the sequence order when displaying a list of stages.")
    job_ids = fields.Many2many(
        'hr.job',
        'job_stage_rel',
        'stage_id',
        'job_id',
        string='Job Stages',
        default=lambda self: [(4, self._context['default_job_id'])]
        if self._context.get('default_job_id') else None)
    requirements = fields.Text("Requirements")
    template_id = fields.Many2one(
        'mail.template',
        "Use template",
        help=
        "If set, a message is posted on the applicant using the template when the applicant is set to the stage."
    )
    fold = fields.Boolean(
        "Folded in Recruitment Pipe",
        help=
        "This stage is folded in the kanban view when there are no records in that stage to display."
    )
コード例 #7
0
class DocumentPageShowDiff(models.TransientModel):
    """Display Difference for History."""

    _name = 'wizard.document.page.history.show_diff'

    def get_diff(self):
        """Return the Difference between two document."""
        history = self.env["document.page.history"]
        ids = self.env.context.get('active_ids', [])

        diff = ""
        if len(ids) == 2:
            if ids[0] > ids[1]:
                diff = history.getDiff(ids[1], ids[0])
            else:
                diff = history.getDiff(ids[0], ids[1])
        elif len(ids) == 1:
            old = history.browse(ids[0])
            nids = history.search(
                [('page_id', '=', old.page_id.id)],
                order='id DESC',
                limit=1
            )
            diff = history.getDiff(ids[0], nids.id)
        else:
            raise exceptions.Warning(
                _("Select one or maximum two history revisions!")
            )
        return diff

    diff = fields.Text(
        'Diff',
        readonly=True,
        default=get_diff
    )
コード例 #8
0
ファイル: l10n_be_intrastat.py プロジェクト: ecoreos/hz
class intrastat_transaction(models.Model):
    _name = 'l10n_be_intrastat.transaction'
    _rec_name = 'code'

    code = fields.Char('Code', required=True, readonly=True)
    description = fields.Text('Description', readonly=True)

    _sql_constraints = [
        ('l10n_be_intrastat_trcodeunique', 'UNIQUE (code)', 'Code must be unique.'),
    ]
コード例 #9
0
ファイル: pos_cache.py プロジェクト: LiberTang0/5
class pos_cache(models.Model):
    _name = 'pos.cache'

    cache = fields.Binary()
    product_domain = fields.Text(required=True)
    product_fields = fields.Text(required=True)

    config_id = fields.Many2one('pos.config', ondelete='cascade', required=True)
    compute_user_id = fields.Many2one('res.users', 'Cache compute user', required=True)

    @api.model
    def refresh_all_caches(self):
        self.env['pos.cache'].search([]).refresh_cache()

    @api.one
    def refresh_cache(self):
        products = self.env['product.product'].search(self.get_product_domain())
        prod_ctx = products.with_context(pricelist=self.config_id.pricelist_id.id, display_default_code=False)
        prod_ctx = prod_ctx.sudo(self.compute_user_id.id)
        res = prod_ctx.read(self.get_product_fields())
        datas = {
            'cache': cPickle.dumps(res, protocol=cPickle.HIGHEST_PROTOCOL),
        }

        self.write(datas)

    @api.model
    def get_product_domain(self):
        return literal_eval(self.product_domain)

    @api.model
    def get_product_fields(self):
        return literal_eval(self.product_fields)

    @api.model
    def get_cache(self, domain, fields):
        if not self.cache or domain != self.get_product_domain() or fields != self.get_product_fields():
            self.product_domain = str(domain)
            self.product_fields = str(fields)
            self.refresh_cache()

        return cPickle.loads(self.cache)
コード例 #10
0
class MailMessageSubtype(models.Model):
    """ Class holding subtype definition for messages. Subtypes allow to tune
        the follower subscription, allowing only some subtypes to be pushed
        on the Wall. """
    _name = 'mail.message.subtype'
    _description = 'Message subtypes'
    _order = 'sequence, id'

    name = fields.Char(
        'Message Type',
        required=True,
        translate=True,
        help='Message subtype gives a more precise type on the message, '
        'especially for system notifications. For example, it can be '
        'a notification related to a new record (New), or to a stage '
        'change in a process (Stage change). Message subtypes allow to '
        'precisely tune the notifications the user want to receive on its wall.'
    )
    description = fields.Text(
        'Description',
        translate=True,
        help='Description that will be added in the message posted for this '
        'subtype. If void, the name will be added instead.')
    internal = fields.Boolean(
        'Internal Only',
        help=
        'Messages with internal subtypes will be visible only by employees, aka members of base_user group'
    )
    parent_id = fields.Many2one(
        'mail.message.subtype',
        string='Parent',
        ondelete='set null',
        help=
        'Parent subtype, used for automatic subscription. This field is not '
        'correctly named. For example on a project, the parent_id of project '
        'subtypes refers to task-related subtypes.')
    relation_field = fields.Char(
        'Relation field',
        help='Field used to link the related model to the subtype model when '
        'using automatic subscription on a related document. The field '
        'is used to compute getattr(related_document.relation_field).')
    res_model = fields.Char(
        'Model',
        help=
        "Model the subtype applies to. If False, this subtype applies to all models."
    )
    default = fields.Boolean('Default',
                             default=True,
                             help="Activated by default when subscribing.")
    sequence = fields.Integer('Sequence',
                              default=1,
                              help="Used to order subtypes.")
    hidden = fields.Boolean('Hidden',
                            help="Hide the subtype in the follower options")
コード例 #11
0
class LunchProduct(models.Model):
    """ Products available to order. A product is linked to a specific vendor. """
    _name = 'lunch.product'
    _description = 'lunch product'

    name = fields.Char('Product', required=True)
    category_id = fields.Many2one('lunch.product.category',
                                  'Category',
                                  required=True)
    description = fields.Text('Description')
    price = fields.Float('Price', digits=dp.get_precision('Account'))
    supplier = fields.Many2one('res.partner', 'Vendor')
コード例 #12
0
class HrEquipmentCategory(models.Model):
    _name = 'hr.equipment.category'
    _inherits = {"mail.alias": "alias_id"}
    _inherit = ['mail.thread']
    _description = 'Asset Category'

    @api.one
    @api.depends('equipment_ids')
    def _compute_fold(self):
        self.fold = False if self.equipment_count else True

    name = fields.Char('Category Name', required=True, translate=True)
    user_id = fields.Many2one('res.users', 'Responsible', track_visibility='onchange', default=lambda self: self.env.uid)
    color = fields.Integer('Color Index')
    note = fields.Text('Comments', translate=True)
    equipment_ids = fields.One2many('hr.equipment', 'category_id', string='Equipments', copy=False)
    equipment_count = fields.Integer(string="Equipment", compute='_compute_equipment_count')
    maintenance_ids = fields.One2many('hr.equipment.request', 'category_id', copy=False)
    maintenance_count = fields.Integer(string="Maintenance", compute='_compute_maintenance_count')
    alias_id = fields.Many2one(
        'mail.alias', 'Alias', ondelete='cascade', required=True,
        help="Email alias for this equipment category. New emails will automatically "
        "create new maintenance request for this equipment category.")
    fold = fields.Boolean(string='Folded in Maintenance Pipe', compute='_compute_fold', store=True)

    @api.multi
    def _compute_equipment_count(self):
        equipment_data = self.env['hr.equipment'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
        mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in equipment_data])
        for category in self:
            category.equipment_count = mapped_data.get(category.id, 0)

    @api.multi
    def _compute_maintenance_count(self):
        maintenance_data = self.env['hr.equipment.request'].read_group([('category_id', 'in', self.ids)], ['category_id'], ['category_id'])
        mapped_data = dict([(m['category_id'][0], m['category_id_count']) for m in maintenance_data])
        for category in self:
            category.maintenance_count = mapped_data.get(category.id, 0)

    @api.model
    def create(self, vals):
        self = self.with_context(alias_model_name='hr.equipment.request', alias_parent_model_name=self._name)
        category_id = super(HrEquipmentCategory, self).create(vals)
        category_id.alias_id.write({'alias_parent_thread_id': category_id.id, 'alias_defaults': {'category_id': category_id.id}})
        return category_id

    @api.multi
    def unlink(self):
        for category in self:
            if category.equipment_ids or category.maintenance_ids:
                raise UserError(_("You cannot delete an equipment category containing equipments or maintenance requests."))
        res = super(HrEquipmentCategory, self).unlink()
        return res
コード例 #13
0
class ir_model_fields(models.Model):
    """Addition of text fields to fields."""
    _inherit = "ir.model.fields"

    notes = fields.Text('Notes to developers.')
    helper = fields.Text('Helper')
    # TODO: Make column1 and 2 required if a model has a m2m to itself
    column1 = fields.Char(
        'Column1',
        help=_("name of the column referring to 'these' records in the "
               "relation table"),
    )
    column2 = fields.Char(
        'Column2',
        help=_("name of the column referring to 'those' records in the "
               "relation table"),
    )
    limit = fields.Integer('Read limit', help=_("Read limit"))
    client_context = fields.Char(
        'Context',
        help=_("Context to use on the client side when handling the field "
               "(python dictionary)"),
    )
コード例 #14
0
ファイル: res_partner_idtype.py プロジェクト: ecoreos/hz
class ResPartnerIDtype(models.Model):
    _name = 'res.partner.idtype'
    _description = 'Identificacion Tipo de Documento'
    _order = 'sequence'

    name = fields.Char(required=True)
    code = fields.Char(required=True)
    sequence = fields.Integer()
    active = fields.Boolean(default=True)
    note = fields.Text()
    on_company = fields.Boolean(string=u'On Company?',
                                default=True,
                                help="Id type for use on Company")
    on_contact = fields.Boolean(string=u'On Contact?',
                                default=True,
                                help="Id type for use on Contacts")
    on_merchant = fields.Boolean(string=u'On Merchants?',
                                 default=True,
                                 help="Id type for use on Merchants")
コード例 #15
0
class AccountAccountType(models.Model):
    _name = "account.account.type"
    _description = "Account Type"

    name = fields.Char(string='Account Type', required=True, translate=True)
    include_initial_balance = fields.Boolean(
        string="Bring Accounts Balance Forward",
        help=
        "Used in reports to know if we should consider journal items from the beginning of time instead of from the fiscal year only. Account types that should be reset to zero at each new fiscal year (like expenses, revenue..) should not have this option set."
    )
    type = fields.Selection([
        ('other', 'Regular'),
        ('receivable', 'Receivable'),
        ('payable', 'Payable'),
        ('liquidity', 'Liquidity'),
    ], required=True, default='other',
        help="The 'Internal Type' is used for features available on "\
        "different types of accounts: liquidity type is for cash or bank accounts"\
        ", payable/receivable is for vendor/customer accounts.")
    note = fields.Text(string='Description')
コード例 #16
0
ファイル: hr_recruitment.py プロジェクト: LiberTang0/5
class Applicant(models.Model):
    _name = "hr.applicant"
    _description = "Applicant"
    _order = "priority desc, id desc"
    _inherit = ['mail.thread', 'ir.needaction_mixin', 'utm.mixin']
    _mail_mass_mailing = _('Applicants')

    def _default_stage_id(self):
        if self._context.get('default_job_id'):
            ids = self.env['hr.recruitment.stage'].search(
                [('job_ids', '=', self._context['default_job_id']),
                 ('fold', '=', False)],
                order='sequence asc',
                limit=1).ids
            if ids:
                return ids[0]
        return False

    def _default_company_id(self):
        company_id = False
        if self._context.get('default_department_id'):
            department = self.env['hr.department'].browse(
                self._context['default_department_id'])
            company_id = department.company_id.id
        if not company_id:
            company_id = self.env['res.company']._company_default_get(
                'hr.applicant')
        return company_id

    name = fields.Char("Subject / Application Name", required=True)
    active = fields.Boolean(
        "Active",
        default=True,
        help=
        "If the active field is set to false, it will allow you to hide the case without removing it."
    )
    description = fields.Text("Description")
    email_from = fields.Char("Email",
                             size=128,
                             help="These people will receive email.")
    email_cc = fields.Text(
        "Watchers Emails",
        size=252,
        help=
        "These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"
    )
    probability = fields.Float("Probability")
    partner_id = fields.Many2one('res.partner', "Contact")
    create_date = fields.Datetime("Creation Date", readonly=True, select=True)
    write_date = fields.Datetime("Update Date", readonly=True)
    stage_id = fields.Many2one('hr.recruitment.stage',
                               'Stage',
                               track_visibility='onchange',
                               domain="[('job_ids', '=', job_id)]",
                               copy=False,
                               select=1,
                               default=_default_stage_id)
    last_stage_id = fields.Many2one(
        'hr.recruitment.stage',
        "Last Stage",
        help=
        "Stage of the applicant before being in the current stage. Used for lost cases analysis."
    )
    categ_ids = fields.Many2many('hr.applicant.category', string="Tags")
    company_id = fields.Many2one('res.company',
                                 "Company",
                                 default=_default_company_id)
    user_id = fields.Many2one('res.users',
                              "Responsible",
                              track_visibility="onchange",
                              default=lambda self: self.env.uid)
    date_closed = fields.Datetime("Closed", readonly=True, select=True)
    date_open = fields.Datetime("Assigned", readonly=True, select=True)
    date_last_stage_update = fields.Datetime("Last Stage Update",
                                             select=True,
                                             default=fields.Datetime.now)
    date_action = fields.Date("Next Action Date")
    title_action = fields.Char("Next Action", size=64)
    priority = fields.Selection(AVAILABLE_PRIORITIES,
                                "Appreciation",
                                default='0')
    job_id = fields.Many2one('hr.job', "Applied Job")
    salary_proposed_extra = fields.Char(
        "Proposed Salary Extra",
        help="Salary Proposed by the Organisation, extra advantages")
    salary_expected_extra = fields.Char(
        "Expected Salary Extra",
        help="Salary Expected by Applicant, extra advantages")
    salary_proposed = fields.Float("Proposed Salary",
                                   help="Salary Proposed by the Organisation")
    salary_expected = fields.Float("Expected Salary",
                                   help="Salary Expected by Applicant")
    availability = fields.Date(
        "Availability",
        help=
        "The date at which the applicant will be available to start working")
    partner_name = fields.Char("Applicant's Name")
    partner_phone = fields.Char("Phone", size=32)
    partner_mobile = fields.Char("Mobile", size=32)
    type_id = fields.Many2one('hr.recruitment.degree', "Degree")
    department_id = fields.Many2one('hr.department', "Department")
    survey = fields.Many2one('survey.survey',
                             related='job_id.survey_id',
                             string="Survey")  # TDE FIXME: rename to survey_id
    response_id = fields.Many2one('survey.user_input',
                                  "Response",
                                  ondelete="set null",
                                  oldname="response")
    reference = fields.Char("Referred By")
    day_open = fields.Float(compute='_compute_day', string="Days to Open")
    day_close = fields.Float(compute='_compute_day', string="Days to Close")
    color = fields.Integer("Color Index", default=0)
    emp_id = fields.Many2one('hr.employee',
                             string="Employee",
                             track_visibility="onchange",
                             help="Employee linked to the applicant.")
    user_email = fields.Char(related='user_id.email',
                             type="char",
                             string="User Email",
                             readonly=True)
    attachment_number = fields.Integer(compute='_get_attachment_number',
                                       string="Number of Attachments")
    employee_name = fields.Char(related='emp_id.name', string="Employee Name")
    attachment_ids = fields.One2many('ir.attachment',
                                     'res_id',
                                     domain=[('res_model', '=', 'hr.applicant')
                                             ],
                                     string='Attachments')

    @api.depends('date_open', 'date_closed')
    @api.one
    def _compute_day(self):
        if self.date_open:
            date_create = datetime.strptime(
                self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
            date_open = datetime.strptime(self.date_open,
                                          tools.DEFAULT_SERVER_DATETIME_FORMAT)
            self.day_open = (date_open - date_create).total_seconds() / (24.0 *
                                                                         3600)

        if self.date_closed:
            date_create = datetime.strptime(
                self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
            date_closed = datetime.strptime(
                self.date_closed, tools.DEFAULT_SERVER_DATETIME_FORMAT)
            self.day_close = (date_closed -
                              date_create).total_seconds() / (24.0 * 3600)

    @api.multi
    def _get_attachment_number(self):
        read_group_res = self.env['ir.attachment'].read_group(
            [('res_model', '=', 'hr.applicant'),
             ('res_id', 'in', self.ids)], ['res_id'], ['res_id'])
        attach_data = dict(
            (res['res_id'], res['res_id_count']) for res in read_group_res)
        for record in self:
            record.attachment_number = attach_data.get(record.id, 0)

    @api.model
    def _read_group_stage_ids(self,
                              ids,
                              domain,
                              read_group_order=None,
                              access_rights_uid=None):
        access_rights_uid = access_rights_uid or self.env.uid
        Stage = self.env['hr.recruitment.stage']
        order = Stage._order
        # lame hack to allow reverting search, should just work in the trivial case
        if read_group_order == 'stage_id desc':
            order = "%s desc" % order
        # retrieve job_id from the context and write the domain: ids + contextual columns (job or default)
        job_id = self._context.get('default_job_id')
        department_id = self._context.get('default_department_id')
        search_domain = []
        if job_id:
            search_domain = [('job_ids', '=', job_id)]
        if department_id:
            if search_domain:
                search_domain = [
                    '|', ('job_ids.department_id', '=', department_id)
                ] + search_domain
            else:
                search_domain = [('job_ids.department_id', '=', department_id)]
        if self.ids:
            if search_domain:
                search_domain = ['|', ('id', 'in', self.ids)] + search_domain
            else:
                search_domain = [('id', 'in', self.ids)]

        stage_ids = Stage._search(search_domain,
                                  order=order,
                                  access_rights_uid=access_rights_uid)
        stages = Stage.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])))

        fold = {}
        for stage in stages:
            fold[stage.id] = stage.fold or False
        return result, fold

    _group_by_full = {'stage_id': _read_group_stage_ids}

    @api.onchange('job_id')
    def onchange_job_id(self):
        vals = self._onchange_job_id_internal(self.job_id.id)
        self.department_id = vals['value']['department_id']
        self.user_id = vals['value']['user_id']
        self.stage_id = vals['value']['stage_id']

    def _onchange_job_id_internal(self, job_id):
        department_id = False
        user_id = False
        stage_id = self.stage_id.id
        if job_id:
            job = self.env['hr.job'].browse(job_id)
            department_id = job.department_id.id
            user_id = job.user_id.id
            if not self.stage_id:
                stage_ids = self.env['hr.recruitment.stage'].search(
                    [('job_ids', '=', job.id), ('fold', '=', False)],
                    order='sequence asc',
                    limit=1).ids
                stage_id = stage_ids[0] if stage_ids else False

        return {
            'value': {
                'department_id': department_id,
                'user_id': user_id,
                'stage_id': stage_id
            }
        }

    @api.onchange('partner_id')
    def onchange_partner_id(self):
        self.partner_phone = self.partner_id.phone
        self.partner_mobile = self.partner_id.mobile
        self.email_from = self.partner_id.email

    @api.onchange('stage_id')
    def onchange_stage_id(self):
        vals = self._onchange_stage_id_internal(self.stage_id.id)
        if vals['value'].get('date_closed'):
            self.date_closed = vals['value']['date_closed']

    def _onchange_stage_id_internal(self, stage_id):
        if not stage_id:
            return {'value': {}}
        stage = self.env['hr.recruitment.stage'].browse(stage_id)
        if stage.fold:
            return {'value': {'date_closed': fields.datetime.now()}}
        return {'value': {'date_closed': False}}

    @api.model
    def create(self, vals):
        if vals.get('department_id'
                    ) and not self._context.get('default_department_id'):
            self = self.with_context(
                default_department_id=vals.get('department_id'))
        if vals.get('job_id') or self._context.get('default_job_id'):
            job_id = vals.get('job_id') or self._context.get('default_job_id')
            for key, value in self._onchange_job_id_internal(
                    job_id)['value'].iteritems():
                if key not in vals:
                    vals[key] = value
        if vals.get('user_id'):
            vals['date_open'] = fields.Datetime.now()
        if 'stage_id' in vals:
            vals.update(
                self._onchange_stage_id_internal(
                    vals.get('stage_id'))['value'])
        return super(Applicant,
                     self.with_context(mail_create_nolog=True)).create(vals)

    @api.multi
    def write(self, vals):
        # user_id change: update date_open
        if vals.get('user_id'):
            vals['date_open'] = fields.Datetime.now()
        # stage_id: track last stage before update
        if 'stage_id' in vals:
            vals['date_last_stage_update'] = fields.Datetime.now()
            vals.update(
                self._onchange_stage_id_internal(
                    vals.get('stage_id'))['value'])
            for applicant in self:
                vals['last_stage_id'] = applicant.stage_id.id
                res = super(Applicant, self).write(vals)
        else:
            res = super(Applicant, self).write(vals)

        # post processing: if stage changed, post a message in the chatter
        if vals.get('stage_id'):
            if self.stage_id.template_id:
                self.message_post_with_template(self.stage_id.template_id.id,
                                                notify=True,
                                                composition_mode='mass_mail')
        return res

    @api.model
    def get_empty_list_help(self, help):
        return super(
            Applicant,
            self.with_context(
                empty_list_help_model='hr.job',
                empty_list_help_id=self.env.context.get('default_job_id'),
                empty_list_help_document_name=_(
                    "job applicants"))).get_empty_list_help(help)

    @api.multi
    def action_get_created_employee(self):
        self.ensure_one()
        action = self.env['ir.actions.act_window'].for_xml_id(
            'hr', 'open_view_employee_list')
        action['res_id'] = self.mapped('emp_id').ids[0]
        return action

    @api.multi
    def action_makeMeeting(self):
        """ This opens Meeting's calendar view to schedule meeting on current applicant
            @return: Dictionary value for created Meeting view
        """
        self.ensure_one()
        partners = self.partner_id | self.user_id.partner_id | self.department_id.manager_id.user_id.partner_id

        category = self.env.ref('hr_recruitment.categ_meet_interview')
        res = self.env['ir.actions.act_window'].for_xml_id(
            'calendar', 'action_calendar_event')
        res['context'] = {
            'search_default_partner_ids': self.partner_id.name,
            'default_partner_ids': partners.ids,
            'default_user_id': self.env.uid,
            'default_name': self.name,
            'default_categ_ids': category and [category.id] or False,
        }
        return res

    @api.multi
    def action_start_survey(self):
        self.ensure_one()
        # create a response and link it to this applicant
        if not self.response_id:
            response = self.env['survey.user_input'].create({
                'survey_id':
                self.survey.id,
                'partner_id':
                self.partner_id.id
            })
            self.response_id = response.id
        else:
            response = self.response_id
        # grab the token of the response and start surveying
        return self.survey.with_context(
            survey_token=response.token).action_start_survey()

    @api.multi
    def action_print_survey(self):
        """ If response is available then print this response otherwise print survey form (print template of the survey) """
        self.ensure_one()
        if not self.response_id:
            return self.survey.action_print_survey()
        else:
            response = self.response_id
            return self.survey.with_context(
                survey_token=response.token).action_print_survey()

    @api.multi
    def action_get_attachment_tree_view(self):
        attachment_action = self.env.ref('base.action_attachment')
        action = attachment_action.read()[0]
        action['context'] = {
            'default_res_model': self._name,
            'default_res_id': self.ids[0]
        }
        action['domain'] = str(
            ['&', ('res_model', '=', self._name), ('res_id', 'in', self.ids)])
        return action

    @api.multi
    def _track_subtype(self, init_values):
        record = self[0]
        if 'emp_id' in init_values and record.emp_id:
            return 'hr_recruitment.mt_applicant_hired'
        elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence <= 1:
            return 'hr_recruitment.mt_applicant_new'
        elif 'stage_id' in init_values and record.stage_id and record.stage_id.sequence > 1:
            return 'hr_recruitment.mt_applicant_stage_changed'
        return super(Applicant, self)._track_subtype(init_values)

    @api.model
    def message_get_reply_to(self, ids, default=None):
        """ Override to get the reply_to of the parent project. """
        applicants = self.sudo().browse(ids)
        aliases = self.env['hr.job'].message_get_reply_to(
            applicants.mapped('job_id').ids, default=default)
        return dict(
            (applicant.id,
             aliases.get(applicant.job_id and applicant.job_id.id or 0, False))
            for applicant in applicants)

    @api.multi
    def message_get_suggested_recipients(self):
        recipients = super(Applicant, self).message_get_suggested_recipients()
        for applicant in self:
            if applicant.partner_id:
                applicant._message_add_suggested_recipient(
                    recipients,
                    partner=applicant.partner_id,
                    reason=_('Contact'))
            elif applicant.email_from:
                applicant._message_add_suggested_recipient(
                    recipients,
                    email=applicant.email_from,
                    reason=_('Contact Email'))
        return recipients

    @api.model
    def message_new(self, msg, custom_values=None):
        """ Overrides mail_thread message_new that is called by the mailgateway
            through message_process.
            This override updates the document according to the email.
        """
        val = msg.get('from').split('<')[0]
        defaults = {
            'name': msg.get('subject') or _("No Subject"),
            'partner_name': val,
            'email_from': msg.get('from'),
            'email_cc': msg.get('cc'),
            'user_id': False,
            'partner_id': msg.get('author_id', False),
        }
        if msg.get('priority'):
            defaults['priority'] = msg.get('priority')
        if custom_values:
            defaults.update(custom_values)
        return super(Applicant, self).message_new(msg, custom_values=defaults)

    @api.multi
    def create_employee_from_applicant(self):
        """ Create an hr.employee from the hr.applicants """
        employee = False
        for applicant in self:
            address_id = contact_name = False
            if applicant.partner_id:
                address_id = applicant.partner_id.address_get(['contact'
                                                               ])['contact']
                contact_name = applicant.partner_id.name_get()[0][1]
            if applicant.job_id and (applicant.partner_name or contact_name):
                applicant.job_id.write({
                    'no_of_hired_employee':
                    applicant.job_id.no_of_hired_employee + 1
                })
                employee = self.env['hr.employee'].create({
                    'name':
                    applicant.partner_name or contact_name,
                    'job_id':
                    applicant.job_id.id,
                    'address_home_id':
                    address_id,
                    'department_id':
                    applicant.department_id.id or False,
                    'address_id':
                    applicant.company_id and applicant.company_id.partner_id
                    and applicant.company_id.partner_id.id or False,
                    'work_email':
                    applicant.department_id
                    and applicant.department_id.company_id
                    and applicant.department_id.company_id.email or False,
                    'work_phone':
                    applicant.department_id
                    and applicant.department_id.company_id
                    and applicant.department_id.company_id.phone or False
                })
                applicant.write({'emp_id': employee.id})
                applicant.job_id.message_post(
                    body=_('New Employee %s Hired') % applicant.partner_name
                    if applicant.partner_name else applicant.name,
                    subtype="hr_recruitment.mt_job_applicant_hired")
                employee._broadcast_welcome()
            else:
                raise UserError(
                    _('You must define an Applied Job and a Contact Name for this applicant.'
                      ))

        employee_action = self.env.ref('hr.open_view_employee_list')
        dict_act_window = employee_action.read([])[0]
        if employee:
            dict_act_window['res_id'] = employee.id
        dict_act_window['view_mode'] = 'form,tree'
        return dict_act_window

    @api.multi
    def archive_applicant(self):
        self.write({'active': False})

    @api.multi
    def reset_applicant(self):
        """ Reinsert the applicant into the recruitment pipe in the first stage"""
        for applicant in self:
            first_stage_obj = self.env['hr.recruitment.stage'].search(
                [('job_ids', 'in', applicant.job_id.id)],
                order="sequence asc",
                limit=1)
            applicant.write({'active': True, 'stage_id': first_stage_obj.id})
コード例 #17
0
class AccountAssetAsset(models.Model):
    _name = 'account.asset.asset'
    _description = 'Asset/Revenue Recognition'
    _inherit = ['mail.thread', 'ir.needaction_mixin']

    account_move_ids = fields.One2many('account.move', 'asset_id', string='Entries', readonly=True, states={'draft': [('readonly', False)]})
    entry_count = fields.Integer(compute='_entry_count', string='# Asset Entries')
    name = fields.Char(string='Asset Name', required=True, readonly=True, states={'draft': [('readonly', False)]})
    code = fields.Char(string='Reference', size=32, readonly=True, states={'draft': [('readonly', False)]})
    value = fields.Float(string='Gross Value', required=True, readonly=True, digits=0, states={'draft': [('readonly', False)]}, oldname='purchase_value')
    currency_id = fields.Many2one('res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]},
        default=lambda self: self.env.user.company_id.currency_id.id)
    company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]},
        default=lambda self: self.env['res.company']._company_default_get('account.asset.asset'))
    note = fields.Text()
    category_id = fields.Many2one('account.asset.category', string='Category', required=True, change_default=True, readonly=True, states={'draft': [('readonly', False)]})
    date = fields.Date(string='Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, oldname="purchase_date")
    state = fields.Selection([('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], 'Status', required=True, copy=False, default='draft',
        help="When an asset is created, the status is 'Draft'.\n"
            "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n"
            "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status.")
    active = fields.Boolean(default=True)
    partner_id = fields.Many2one('res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]})
    method = fields.Selection([('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='linear',
        help="Choose the method to use to compute the amount of depreciation lines.\n  * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n"
            "  * Degressive: Calculated on basis of: Residual Value * Degressive Factor")
    method_number = fields.Integer(string='Number of Depreciations', readonly=True, states={'draft': [('readonly', False)]}, default=5, help="The number of depreciations needed to depreciate your asset")
    method_period = fields.Integer(string='Number of Months in a Period', required=True, readonly=True, default=12, states={'draft': [('readonly', False)]},
        help="The amount of time between two depreciations, in months")
    method_end = fields.Date(string='Ending Date', readonly=True, states={'draft': [('readonly', False)]})
    method_progress_factor = fields.Float(string='Degressive Factor', readonly=True, default=0.3, states={'draft': [('readonly', False)]})
    value_residual = fields.Float(compute='_amount_residual', method=True, digits=0, string='Residual Value')
    method_time = fields.Selection([('number', 'Number of Depreciations'), ('end', 'Ending Date')], string='Time Method', required=True, readonly=True, default='number', states={'draft': [('readonly', False)]},
        help="Choose the method to use to compute the dates and number of depreciation lines.\n"
             "  * Number of Depreciations: Fix the number of depreciation lines and the time between 2 depreciations.\n"
             "  * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond.")
    prorata = fields.Boolean(string='Prorata Temporis', readonly=True, states={'draft': [('readonly', False)]},
        help='Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January / Start date of fiscal year')
    depreciation_line_ids = fields.One2many('account.asset.depreciation.line', 'asset_id', string='Depreciation Lines', readonly=True, states={'draft': [('readonly', False)], 'open': [('readonly', False)]})
    salvage_value = fields.Float(string='Salvage Value', digits=0, readonly=True, states={'draft': [('readonly', False)]},
        help="It is the amount you plan to have that you cannot depreciate.")
    invoice_id = fields.Many2one('account.invoice', string='Invoice', states={'draft': [('readonly', False)]}, copy=False)
    type = fields.Selection(related="category_id.type", string='Type', required=True)

    @api.multi
    def unlink(self):
        for asset in self:
            if asset.state in ['open', 'close']:
                raise UserError(_('You cannot delete a document is in %s state.') % (asset.state,))
            if asset.account_move_ids:
                raise UserError(_('You cannot delete a document that contains posted entries.'))
        return super(AccountAssetAsset, self).unlink()

    @api.multi
    def _get_last_depreciation_date(self):
        """
        @param id: ids of a account.asset.asset objects
        @return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset
        """
        self.env.cr.execute("""
            SELECT a.id as id, COALESCE(MAX(m.date),a.date) AS date
            FROM account_asset_asset a
            LEFT JOIN account_move m ON (m.asset_id = a.id)
            WHERE a.id IN %s
            GROUP BY a.id, a.date """, (tuple(self.ids),))
        result = dict(self.env.cr.fetchall())
        return result

    @api.model
    def _cron_generate_entries(self):
        assets = self.env['account.asset.asset'].search([('state', '=', 'open')])
        assets._compute_entries(datetime.today())

    def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date):
        amount = 0
        if sequence == undone_dotation_number:
            amount = residual_amount
        else:
            if self.method == 'linear':
                amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids))
                if self.prorata and self.category_id.type == 'purchase':
                    amount = amount_to_depr / self.method_number
                    days = total_days - float(depreciation_date.strftime('%j'))
                    if sequence == 1:
                        amount = (amount_to_depr / self.method_number) / total_days * days
                    elif sequence == undone_dotation_number:
                        amount = (amount_to_depr / self.method_number) / total_days * (total_days - days)
            elif self.method == 'degressive':
                amount = residual_amount * self.method_progress_factor
                if self.prorata:
                    days = total_days - float(depreciation_date.strftime('%j'))
                    if sequence == 1:
                        amount = (residual_amount * self.method_progress_factor) / total_days * days
                    elif sequence == undone_dotation_number:
                        amount = (residual_amount * self.method_progress_factor) / total_days * (total_days - days)
        return amount

    def _compute_board_undone_dotation_nb(self, depreciation_date, total_days):
        undone_dotation_number = self.method_number
        if self.method_time == 'end':
            end_date = datetime.strptime(self.method_end, DF).date()
            undone_dotation_number = 0
            while depreciation_date <= end_date:
                depreciation_date = date(depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta(months=+self.method_period)
                undone_dotation_number += 1
        if self.prorata and self.category_id.type == 'purchase':
            undone_dotation_number += 1
        return undone_dotation_number

    @api.multi
    def compute_depreciation_board(self):
        self.ensure_one()

        posted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: x.move_check)
        unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: not x.move_check)

        # Remove old unposted depreciation lines. We cannot use unlink() with One2many field
        commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids]

        if self.value_residual != 0.0:
            amount_to_depr = residual_amount = self.value_residual
            if self.prorata:
                depreciation_date = datetime.strptime(self._get_last_depreciation_date()[self.id], DF).date()
            else:
                # depreciation_date = 1st of January of purchase year
                asset_date = datetime.strptime(self.date, DF).date()
                # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period
                if posted_depreciation_line_ids and posted_depreciation_line_ids[0].depreciation_date:
                    last_depreciation_date = datetime.strptime(posted_depreciation_line_ids[0].depreciation_date, DF).date()
                    depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period)
                else:
                    depreciation_date = asset_date
            day = depreciation_date.day
            month = depreciation_date.month
            year = depreciation_date.year
            total_days = (year % 4) and 365 or 366

            undone_dotation_number = self._compute_board_undone_dotation_nb(depreciation_date, total_days)
            for x in range(len(posted_depreciation_line_ids), undone_dotation_number):
                sequence = x + 1
                amount = self._compute_board_amount(sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date)
                amount = self.currency_id.round(amount)
                residual_amount -= amount
                vals = {
                    'amount': amount,
                    'asset_id': self.id,
                    'sequence': sequence,
                    'name': (self.code or '') + '/' + str(sequence),
                    'remaining_value': residual_amount,
                    'depreciated_value': self.value - (self.salvage_value + residual_amount),
                    'depreciation_date': depreciation_date.strftime(DF),
                }
                commands.append((0, False, vals))
                # Considering Depr. Period as months
                depreciation_date = date(year, month, day) + relativedelta(months=+self.method_period)
                day = depreciation_date.day
                month = depreciation_date.month
                year = depreciation_date.year

        self.write({'depreciation_line_ids': commands})

        return True

    @api.multi
    def validate(self):
        self.write({'state': 'open'})
        fields = [
            'method',
            'method_number',
            'method_period',
            'method_end',
            'method_progress_factor',
            'method_time',
            'salvage_value',
            'invoice_id',
        ]
        ref_tracked_fields = self.env['account.asset.asset'].fields_get(fields)
        for asset in self:
            tracked_fields = ref_tracked_fields.copy()
            if asset.method == 'linear':
                del(tracked_fields['method_progress_factor'])
            if asset.method_time != 'end':
                del(tracked_fields['method_end'])
            else:
                del(tracked_fields['method_number'])
            dummy, tracking_value_ids = asset._message_track(tracked_fields, dict.fromkeys(fields))
            asset.message_post(subject=_('Asset created'), tracking_value_ids=tracking_value_ids)

    @api.multi
    def set_to_close(self):
        move_ids = []
        for asset in self:
            unposted_depreciation_line_ids = asset.depreciation_line_ids.filtered(lambda x: not x.move_check)
            if unposted_depreciation_line_ids:
                old_values = {
                    'method_end': asset.method_end,
                    'method_number': asset.method_number,
                }

                # Remove all unposted depr. lines
                commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids]

                # Create a new depr. line with the residual amount and post it
                sequence = len(asset.depreciation_line_ids) - len(unposted_depreciation_line_ids) + 1
                today = datetime.today().strftime(DF)
                vals = {
                    'amount': asset.value_residual,
                    'asset_id': asset.id,
                    'sequence': sequence,
                    'name': (asset.code or '') + '/' + str(sequence),
                    'remaining_value': 0,
                    'depreciated_value': asset.value - asset.salvage_value,  # the asset is completely depreciated
                    'depreciation_date': today,
                }
                commands.append((0, False, vals))
                asset.write({'depreciation_line_ids': commands, 'method_end': today, 'method_number': sequence})
                tracked_fields = self.env['account.asset.asset'].fields_get(['method_number', 'method_end'])
                changes, tracking_value_ids = asset._message_track(tracked_fields, old_values)
                if changes:
                    asset.message_post(subject=_('Asset sold or disposed. Accounting entry awaiting for validation.'), tracking_value_ids=tracking_value_ids)
                move_ids += asset.depreciation_line_ids[-1].create_move(post_move=False)
        if move_ids:
            name = _('Disposal Move')
            view_mode = 'form'
            if len(move_ids) > 1:
                name = _('Disposal Moves')
                view_mode = 'tree,form'
            return {
                'name': name,
                'view_type': 'form',
                'view_mode': view_mode,
                'res_model': 'account.move',
                'type': 'ir.actions.act_window',
                'target': 'current',
                'res_id': move_ids[0],
            }

    @api.multi
    def set_to_draft(self):
        self.write({'state': 'draft'})

    @api.one
    @api.depends('value', 'salvage_value', 'depreciation_line_ids')
    def _amount_residual(self):
        total_amount = 0.0
        for line in self.depreciation_line_ids:
            if line.move_check:
                total_amount += line.amount
        self.value_residual = self.value - total_amount - self.salvage_value

    @api.onchange('company_id')
    def onchange_company_id(self):
        self.currency_id = self.company_id.currency_id.id

    @api.multi
    @api.depends('account_move_ids')
    def _entry_count(self):
        for asset in self:
            asset.entry_count = self.env['account.move'].search_count([('asset_id', '=', asset.id)])

    @api.one
    @api.constrains('prorata', 'method_time')
    def _check_prorata(self):
        if self.prorata and self.method_time != 'number':
            raise ValidationError(_('Prorata temporis can be applied only for time method "number of depreciations".'))

    @api.onchange('category_id')
    def onchange_category_id(self):
        vals = self.onchange_category_id_values(self.category_id.id)
        # We cannot use 'write' on an object that doesn't exist yet
        if vals:
            for k, v in vals['value'].iteritems():
                setattr(self, k, v)

    def onchange_category_id_values(self, category_id):
        if category_id:
            category = self.env['account.asset.category'].browse(category_id)
            return {
                'value': {
                    'method': category.method,
                    'method_number': category.method_number,
                    'method_time': category.method_time,
                    'method_period': category.method_period,
                    'method_progress_factor': category.method_progress_factor,
                    'method_end': category.method_end,
                    'prorata': category.prorata,
                }
            }

    @api.onchange('method_time')
    def onchange_method_time(self):
        if self.method_time != 'number':
            self.prorata = False

    @api.multi
    def copy_data(self, default=None):
        if default is None:
            default = {}
        default['name'] = self.name + _(' (copy)')
        return super(AccountAssetAsset, self).copy_data(default)[0]

    @api.multi
    def _compute_entries(self, date):
        depreciation_ids = self.env['account.asset.depreciation.line'].search([
            ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date),
            ('move_check', '=', False)])
        return depreciation_ids.create_move()

    @api.model
    def create(self, vals):
        asset = super(AccountAssetAsset, self.with_context(mail_create_nolog=True)).create(vals)
        asset.compute_depreciation_board()
        return asset

    @api.multi
    def write(self, vals):
        res = super(AccountAssetAsset, self).write(vals)
        if 'depreciation_line_ids' not in vals:
            self.compute_depreciation_board()
        return res

    @api.multi
    def open_entries(self):
        return {
            'name': _('Journal Entries'),
            'view_type': 'form',
            'view_mode': 'tree,form',
            'res_model': 'account.move',
            'view_id': False,
            'type': 'ir.actions.act_window',
            'context': dict(self.env.context or {}, search_default_asset_id=self.id, default_asset_id=self.id),
        }
コード例 #18
0
class Slide(models.Model):
    """ This model represents actual presentations. Those must be one of four
    types:

     - Presentation
     - Document
     - Infographic
     - Video

    Slide has various statistics like view count, embed count, like, dislikes """

    _name = 'slide.slide'
    _inherit = [
        'mail.thread', 'website.seo.metadata', 'website.published.mixin'
    ]
    _description = 'Slides'

    _PROMOTIONAL_FIELDS = [
        '__last_update', 'name', 'image_thumb', 'image_medium', 'slide_type',
        'total_views', 'category_id', 'channel_id', 'description', 'tag_ids',
        'write_date', 'create_date', 'website_published', 'website_url',
        'website_meta_title', 'website_meta_description',
        'website_meta_keywords'
    ]

    _sql_constraints = [('name_uniq', 'UNIQUE(channel_id, name)',
                         'The slide name must be unique within a channel')]

    # description
    name = fields.Char('Title', required=True, translate=True)
    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')
    download_security = fields.Selection([('none', 'No One'),
                                          ('user', 'Authentified Users Only'),
                                          ('public', 'Everyone')],
                                         string='Download Security',
                                         required=True,
                                         default='user')
    image = fields.Binary('Image', attachment=True)
    image_medium = fields.Binary('Medium',
                                 compute="_get_image",
                                 store=True,
                                 attachment=True)
    image_thumb = fields.Binary('Thumbnail',
                                compute="_get_image",
                                store=True,
                                attachment=True)

    @api.depends('image')
    def _get_image(self):
        for record in self:
            if record.image:
                record.image_medium = image.crop_image(record.image,
                                                       type='top',
                                                       ratio=(4, 3),
                                                       thumbnail_ratio=4)
                record.image_thumb = image.crop_image(record.image,
                                                      type='top',
                                                      ratio=(4, 3),
                                                      thumbnail_ratio=6)
            else:
                record.image_medium = False
                record.iamge_thumb = False

    # content
    slide_type = fields.Selection(
        [('infographic', 'Infographic'), ('presentation', 'Presentation'),
         ('document', 'Document'), ('video', 'Video')],
        string='Type',
        required=True,
        default='document',
        help=
        "Document type will be set automatically depending on file type, height and width."
    )
    index_content = fields.Text('Transcript')
    datas = fields.Binary('Content')
    url = fields.Char('Document URL', help="Youtube or Google Document URL")
    document_id = fields.Char('Document ID',
                              help="Youtube or Google Document ID")
    mime_type = fields.Char('Mime-type')

    @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.iteritems():
                setattr(self, key, value)

    # website
    date_published = fields.Datetime('Publish Date')
    website_message_ids = fields.One2many(
        'mail.message',
        'res_id',
        domain=lambda self: [('model', '=', self._name),
                             ('message_type', '=', 'comment')],
        string='Website Messages',
        help="Website communication history")
    likes = fields.Integer('Likes')
    dislikes = fields.Integer('Dislikes')
    # views
    embedcount_ids = fields.One2many('slide.embed',
                                     'slide_id',
                                     string="Embed Count")
    slide_views = fields.Integer('# of Website Views')
    embed_views = fields.Integer('# of Embedded Views')
    total_views = fields.Integer("Total # Views",
                                 default="0",
                                 compute='_compute_total',
                                 store=True)

    @api.depends('slide_views', 'embed_views')
    def _compute_total(self):
        for record in self:
            record.total_views = record.slide_views + record.embed_views

    embed_code = fields.Text('Embed Code',
                             readonly=True,
                             compute='_get_embed_code')

    def _get_embed_code(self):
        base_url = self.env['ir.config_parameter'].get_param('web.base.url')
        for record in self:
            if record.datas and not record.document_id:
                record.embed_code = '<iframe src="%s/slides/embed/%s?page=1" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % (
                    base_url, record.id, 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 = '<embed src="https://video.google.com/get_player?ps=docs&partnerid=30&docid=%s" type="application/x-shockwave-flash"></embed>' % (
                        record.document_id)
            else:
                record.embed_code = False

    @api.multi
    @api.depends('name')
    def _website_url(self, name, arg):
        res = super(Slide, self)._website_url(name, arg)
        base_url = self.env['ir.config_parameter'].get_param('web.base.url')
        #link_tracker is not in dependencies, so use it to shorten url only if installed.
        if self.env.registry.get('link.tracker'):
            LinkTracker = self.env['link.tracker']
            res.update({(slide.id, LinkTracker.sudo().create({
                'url':
                '%s/slides/slide/%s' % (base_url, slug(slide))
            }).short_url)
                        for slide in self})
        else:
            res.update({(slide.id,
                         '%s/slides/slide/%s' % (base_url, slug(slide)))
                        for slide in self})
        return res

    @api.model
    def create(self, values):
        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'):
            doc_data = self._parse_document_url(values['url']).get(
                'values', dict())
            for key, value in doc_data.iteritems():
                values.setdefault(key, value)
        # Do not publish slide if user has not publisher rights
        if not self.user_has_groups('base.group_website_publisher'):
            values['website_published'] = False
        slide = super(Slide, self).create(values)
        slide.channel_id.message_subscribe_users()
        slide._post_publication()
        return slide

    @api.multi
    def write(self, values):
        if values.get('url'):
            doc_data = self._parse_document_url(values['url']).get(
                'values', dict())
            for key, value in doc_data.iteritems():
                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

    @api.model
    def check_field_access_rights(self, operation, fields):
        """ As per channel access configuration (visibility)
         - public  ==> no restriction on slides access
         - private ==> restrict all slides of channel based on access group defined on channel group_ids field
         - partial ==> show channel, but presentations based on groups means any user can see channel but not slide's content.
        For private: implement using record rule
        For partial: user can see channel, but channel gridview have slide detail so we have to implement
        partial field access mechanism for public user so he can have access of promotional field (name, view_count) of slides,
        but not all fields like data (actual pdf content)
        all fields should be accessible only for user group defined on channel group_ids
        """
        if self.env.uid == SUPERUSER_ID:
            return fields or list(self._fields)
        fields = super(Slide,
                       self).check_field_access_rights(operation, fields)
        # still read not perform so we can not access self.channel_id
        if self.ids:
            self.env.cr.execute(
                'SELECT DISTINCT channel_id FROM ' + self._table +
                ' WHERE id IN %s', (tuple(self.ids), ))
            channel_ids = [x[0] for x in self.env.cr.fetchall()]
            channels = self.env['slide.channel'].sudo().browse(channel_ids)
            limited_access = all(
                channel.visibility == 'partial'
                and not len(channel.group_ids & self.env.user.groups_id)
                for channel in channels)
            if limited_access:
                fields = [
                    field for field in fields
                    if field in self._PROMOTIONAL_FIELDS
                ]
        return fields

    def get_related_slides(self, limit=20):
        domain = [('website_published', '=', True),
                  ('channel_id.visibility', '!=', 'private'),
                  ('id', '!=', self.id)]
        if self.category_id:
            domain += [('category_id', '=', self.category_id.id)]
        for record in self.search(domain, limit=limit):
            yield record

    def get_most_viewed_slides(self, limit=20):
        for record in self.search([('website_published', '=', True),
                                   ('channel_id.visibility', '!=', 'private'),
                                   ('id', '!=', self.id)],
                                  limit=limit,
                                  order='total_views desc'):
            yield record

    def _post_publication(self):
        base_url = self.env['ir.config_parameter'].get_param('web.base.url')
        for slide in self.filtered(lambda slide: slide.website_published):
            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)
            slide.channel_id.message_post(
                body=html_body,
                subtype='website_slides.mt_channel_slide_published')
        return True

    @api.one
    def send_share_email(self, email):
        base_url = self.env['ir.config_parameter'].get_param('web.base.url')
        return self.channel_id.share_template_id.with_context({
            'email':
            email,
            'base_url':
            base_url
        }).send_mail(self.id)

    # --------------------------------------------------
    # Parsing methods
    # --------------------------------------------------

    @api.model
    def _fetch_data(self, base_url, data, content_type=False):
        result = {'values': dict()}
        try:
            if data:
                base_url = base_url + '?%s' % urlencode(data)
            req = urllib2.Request(base_url)
            content = urllib2.urlopen(req).read()
            if content_type == 'json':
                result['values'] = json.loads(content)
            elif content_type in ('image', 'pdf'):
                result['values'] = content.encode('base64')
            else:
                result['values'] = content
        except urllib2.HTTPError as e:
            result['error'] = e.read()
            e.close()
        except urllib2.URLError as e:
            result['error'] = e.reason
        return result

    def _find_document_data_from_url(self, url):
        expr = re.compile(
            r'^.*((youtu.be/)|(v/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#\&\?]*).*'
        )
        arg = expr.match(url)
        document_id = arg and arg.group(7) or False
        if document_id:
            return ('youtube', document_id)

        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['ir.config_parameter'].sudo().get_param(
            'website_slides.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}
        youtube_values = fetch_res['values'].get('items', list(dict()))[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'],
            })
        return {'values': values}

    @api.model
    def _parse_google_document(self, document_id, only_preview_fields):
        def get_slide_type(vals):
            # TDE FIXME: WTF ??
            image = Image.open(io.BytesIO(vals['image'].decode('base64')))
            width, height = image.size
            if height > width:
                return 'document'
            else:
                return 'presentation'

        key = self.env['ir.config_parameter'].sudo().get_param(
            'website_slides.google_app_key')
        fetch_res = self._fetch_data(
            'https://www.googleapis.com/drive/v2/files/%s' % document_id, {
                'projection': 'BASIC',
                'key': key
            }, "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['datas'] = self._fetch_data(
                google_values['exportLinks']['application/pdf'], {},
                'pdf')['values']
            values['slide_type'] = get_slide_type(values)
            if google_values['exportLinks'].get('text/plain'):
                values['index_content'] = self._fetch_data(
                    google_values['exportLinks']['text/plain'], {})['values']
            if google_values['exportLinks'].get('text/csv'):
                values['index_content'] = self._fetch_data(
                    google_values['exportLinks']['text/csv'], {})['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}
コード例 #19
0
class AssetModify(models.TransientModel):
    _name = 'asset.modify'
    _description = 'Modify Asset'

    name = fields.Text(string='Reason', required=True)
    method_number = fields.Integer(string='Number of Depreciations', required=True)
    method_period = fields.Integer(string='Period Length')
    method_end = fields.Date(string='Ending date')
    asset_method_time = fields.Char(compute='_get_asset_method_time', string='Asset Method Time', readonly=True)

    @api.one
    def _get_asset_method_time(self):
        if self.env.context.get('active_id'):
            asset = self.env['account.asset.asset'].browse(self.env.context.get('active_id'))
            self.asset_method_time = asset.method_time

    @api.model
    def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
        result = super(AssetModify, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=submenu)
        asset_id = self.env.context.get('active_id')
        active_model = self.env.context.get('active_model')
        if active_model == 'account.asset.asset' and asset_id:
            asset = self.env['account.asset.asset'].browse(asset_id)
            doc = etree.XML(result['arch'])
            if asset.method_time == 'number' and doc.xpath("//field[@name='method_end']"):
                node = doc.xpath("//field[@name='method_end']")[0]
                node.set('invisible', '1')
                setup_modifiers(node, result['fields']['method_end'])
            elif asset.method_time == 'end' and doc.xpath("//field[@name='method_number']"):
                node = doc.xpath("//field[@name='method_number']")[0]
                node.set('invisible', '1')
                setup_modifiers(node, result['fields']['method_number'])
            result['arch'] = etree.tostring(doc)
        return result

    @api.model
    def default_get(self, fields):
        res = super(AssetModify, self).default_get(fields)
        asset_id = self.env.context.get('active_id')
        asset = self.env['account.asset.asset'].browse(asset_id)
        if 'name' in fields:
            res.update({'name': asset.name})
        if 'method_number' in fields and asset.method_time == 'number':
            res.update({'method_number': asset.method_number})
        if 'method_period' in fields:
            res.update({'method_period': asset.method_period})
        if 'method_end' in fields and asset.method_time == 'end':
            res.update({'method_end': asset.method_end})
        if self.env.context.get('active_id'):
            res['asset_method_time'] = self._get_asset_method_time()
        return res

    @api.multi
    def modify(self):
        """ Modifies the duration of asset for calculating depreciation
        and maintains the history of old values, in the chatter.
        """
        asset_id = self.env.context.get('active_id', False)
        asset = self.env['account.asset.asset'].browse(asset_id)
        old_values = {
            'method_number': asset.method_number,
            'method_period': asset.method_period,
            'method_end': asset.method_end,
        }
        asset_vals = {
            'method_number': self.method_number,
            'method_period': self.method_period,
            'method_end': self.method_end,
        }
        asset.write(asset_vals)
        asset.compute_depreciation_board()
        tracked_fields = self.env['account.asset.asset'].fields_get(['method_number', 'method_period', 'method_end'])
        changes, tracking_value_ids = asset._message_track(tracked_fields, old_values)
        if changes:
            asset.message_post(subject=_('Depreciation board modified'), body=self.name, tracking_value_ids=tracking_value_ids)
        return {'type': 'ir.actions.act_window_close'}
コード例 #20
0
ファイル: l10n_be_intrastat.py プロジェクト: ecoreos/hz
class report_intrastat_code(models.Model):
    _inherit = "report.intrastat.code"

    description = fields.Text('Description', translate=True)
コード例 #21
0
class Post(models.Model):

    _name = 'forum.post'
    _description = 'Forum Post'
    _inherit = ['mail.thread', 'website.seo.metadata']
    _order = "is_correct DESC, vote_count DESC, write_date DESC"

    name = fields.Char('Title')
    forum_id = fields.Many2one('forum.forum', string='Forum', required=True)
    content = fields.Html('Content', strip_style=True)
    plain_content = fields.Text('Plain Content',
                                compute='_get_plain_content',
                                store=True)
    content_link = fields.Char('URL', help="URL of Link Articles")
    tag_ids = fields.Many2many('forum.tag',
                               'forum_tag_rel',
                               'forum_id',
                               'forum_tag_id',
                               string='Tags')
    state = fields.Selection([('active', 'Active'),
                              ('pending', 'Waiting Validation'),
                              ('close', 'Close'), ('offensive', 'Offensive'),
                              ('flagged', 'Flagged')],
                             string='Status',
                             default='active')
    views = fields.Integer('Number of Views', default=0)
    active = fields.Boolean('Active', default=True)
    post_type = fields.Selection([('question', 'Question'),
                                  ('link', 'Article'),
                                  ('discussion', 'Discussion')],
                                 string='Type',
                                 default='question',
                                 required=True)
    website_message_ids = fields.One2many(
        'mail.message',
        'res_id',
        domain=lambda self: [
            '&', ('model', '=', self._name),
            ('message_type', 'in', ['email', 'comment'])
        ],
        string='Post Messages',
        help="Comments on forum post",
    )

    # history
    create_date = fields.Datetime('Asked on', select=True, readonly=True)
    create_uid = fields.Many2one('res.users',
                                 string='Created by',
                                 select=True,
                                 readonly=True)
    write_date = fields.Datetime('Update on', select=True, readonly=True)
    bump_date = fields.Datetime(
        'Bumped on',
        readonly=True,
        help=
        "Technical field allowing to bump a question. Writing on this field will trigger "
        "a write on write_date and therefore bump the post. Directly writing on write_date "
        "is currently not supported and this field is a workaround.")
    write_uid = fields.Many2one('res.users',
                                string='Updated by',
                                select=True,
                                readonly=True)
    relevancy = fields.Float('Relevance',
                             compute="_compute_relevancy",
                             store=True)

    # vote
    vote_ids = fields.One2many('forum.post.vote', 'post_id', string='Votes')
    user_vote = fields.Integer('My Vote', compute='_get_user_vote')
    vote_count = fields.Integer('Votes', compute='_get_vote_count', store=True)

    # favorite
    favourite_ids = fields.Many2many('res.users', string='Favourite')
    user_favourite = fields.Boolean('Is Favourite',
                                    compute='_get_user_favourite')
    favourite_count = fields.Integer('Favorite Count',
                                     compute='_get_favorite_count',
                                     store=True)

    # hierarchy
    is_correct = fields.Boolean('Correct',
                                help='Correct answer or answer accepted')
    parent_id = fields.Many2one('forum.post',
                                string='Question',
                                ondelete='cascade')
    self_reply = fields.Boolean('Reply to own question',
                                compute='_is_self_reply',
                                store=True)
    child_ids = fields.One2many('forum.post', 'parent_id', string='Answers')
    child_count = fields.Integer('Number of answers',
                                 compute='_get_child_count',
                                 store=True)
    uid_has_answered = fields.Boolean('Has Answered',
                                      compute='_get_uid_has_answered')
    has_validated_answer = fields.Boolean('Is answered',
                                          compute='_get_has_validated_answer',
                                          store=True)

    # offensive moderation tools
    flag_user_id = fields.Many2one('res.users', string='Flagged by')
    moderator_id = fields.Many2one('res.users',
                                   string='Reviewed by',
                                   readonly=True)

    # closing
    closed_reason_id = fields.Many2one('forum.post.reason', string='Reason')
    closed_uid = fields.Many2one('res.users', string='Closed by', select=1)
    closed_date = fields.Datetime('Closed on', readonly=True)

    # karma calculation and access
    karma_accept = fields.Integer('Convert comment to answer',
                                  compute='_get_post_karma_rights')
    karma_edit = fields.Integer('Karma to edit',
                                compute='_get_post_karma_rights')
    karma_close = fields.Integer('Karma to close',
                                 compute='_get_post_karma_rights')
    karma_unlink = fields.Integer('Karma to unlink',
                                  compute='_get_post_karma_rights')
    karma_comment = fields.Integer('Karma to comment',
                                   compute='_get_post_karma_rights')
    karma_comment_convert = fields.Integer(
        'Karma to convert comment to answer', compute='_get_post_karma_rights')
    karma_flag = fields.Integer('Flag a post as offensive',
                                compute='_get_post_karma_rights')
    can_ask = fields.Boolean('Can Ask', compute='_get_post_karma_rights')
    can_answer = fields.Boolean('Can Answer', compute='_get_post_karma_rights')
    can_accept = fields.Boolean('Can Accept', compute='_get_post_karma_rights')
    can_edit = fields.Boolean('Can Edit', compute='_get_post_karma_rights')
    can_close = fields.Boolean('Can Close', compute='_get_post_karma_rights')
    can_unlink = fields.Boolean('Can Unlink', compute='_get_post_karma_rights')
    can_upvote = fields.Boolean('Can Upvote', compute='_get_post_karma_rights')
    can_downvote = fields.Boolean('Can Downvote',
                                  compute='_get_post_karma_rights')
    can_comment = fields.Boolean('Can Comment',
                                 compute='_get_post_karma_rights')
    can_comment_convert = fields.Boolean('Can Convert to Comment',
                                         compute='_get_post_karma_rights')
    can_view = fields.Boolean('Can View', compute='_get_post_karma_rights')
    can_display_biography = fields.Boolean(
        "Is the author's biography visible from his post",
        compute='_get_post_karma_rights')
    can_post = fields.Boolean('Can Automatically be Validated',
                              compute='_get_post_karma_rights')
    can_flag = fields.Boolean('Can Flag', compute='_get_post_karma_rights')
    can_moderate = fields.Boolean('Can Moderate',
                                  compute='_get_post_karma_rights')

    @api.one
    @api.depends('content')
    def _get_plain_content(self):
        self.plain_content = tools.html2plaintext(
            self.content)[0:500] if self.content else False

    @api.one
    @api.depends('vote_count', 'forum_id.relevancy_post_vote',
                 'forum_id.relevancy_time_decay')
    def _compute_relevancy(self):
        if self.create_date:
            days = (datetime.today() - datetime.strptime(
                self.create_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)).days
            self.relevancy = math.copysign(1, self.vote_count) * (
                abs(self.vote_count - 1)**self.forum_id.relevancy_post_vote /
                (days + 2)**self.forum_id.relevancy_time_decay)
        else:
            self.relevancy = 0

    @api.multi
    def _get_user_vote(self):
        votes = self.env['forum.post.vote'].search_read(
            [('post_id', 'in', self._ids),
             ('user_id', '=', self._uid)], ['vote', 'post_id'])
        mapped_vote = dict([(v['post_id'][0], v['vote']) for v in votes])
        for vote in self:
            vote.user_vote = mapped_vote.get(vote.id, 0)

    @api.multi
    @api.depends('vote_ids.vote')
    def _get_vote_count(self):
        read_group_res = self.env['forum.post.vote'].read_group(
            [('post_id', 'in', self._ids)], ['post_id', 'vote'],
            ['post_id', 'vote'],
            lazy=False)
        result = dict.fromkeys(self._ids, 0)
        for data in read_group_res:
            result[data['post_id'][0]] += data['__count'] * int(data['vote'])
        for post in self:
            post.vote_count = result[post.id]

    @api.one
    def _get_user_favourite(self):
        self.user_favourite = self._uid in self.favourite_ids.ids

    @api.one
    @api.depends('favourite_ids')
    def _get_favorite_count(self):
        self.favourite_count = len(self.favourite_ids)

    @api.one
    @api.depends('create_uid', 'parent_id')
    def _is_self_reply(self):
        self.self_reply = self.parent_id.create_uid.id == self._uid

    @api.one
    @api.depends('child_ids.create_uid', 'website_message_ids')
    def _get_child_count(self):
        def process(node):
            total = len(node.website_message_ids) + len(node.child_ids)
            for child in node.child_ids:
                total += process(child)
            return total

        self.child_count = process(self)

    @api.one
    def _get_uid_has_answered(self):
        self.uid_has_answered = any(answer.create_uid.id == self._uid
                                    for answer in self.child_ids)

    @api.one
    @api.depends('child_ids.is_correct')
    def _get_has_validated_answer(self):
        self.has_validated_answer = any(answer.is_correct
                                        for answer in self.child_ids)

    @api.multi
    def _get_post_karma_rights(self):
        user = self.env.user
        is_admin = user.id == SUPERUSER_ID
        # sudoed recordset instead of individual posts so values can be
        # prefetched in bulk
        for post, post_sudo in itertools.izip(self, self.sudo()):
            is_creator = post.create_uid == user

            post.karma_accept = post.forum_id.karma_answer_accept_own if post.parent_id.create_uid == user else post.forum_id.karma_answer_accept_all
            post.karma_edit = post.forum_id.karma_edit_own if is_creator else post.forum_id.karma_edit_all
            post.karma_close = post.forum_id.karma_close_own if is_creator else post.forum_id.karma_close_all
            post.karma_unlink = post.forum_id.karma_unlink_own if is_creator else post.forum_id.karma_unlink_all
            post.karma_comment = post.forum_id.karma_comment_own if is_creator else post.forum_id.karma_comment_all
            post.karma_comment_convert = post.forum_id.karma_comment_convert_own if is_creator else post.forum_id.karma_comment_convert_all

            post.can_ask = is_admin or user.karma >= post.forum_id.karma_ask
            post.can_answer = is_admin or user.karma >= post.forum_id.karma_answer
            post.can_accept = is_admin or user.karma >= post.karma_accept
            post.can_edit = is_admin or user.karma >= post.karma_edit
            post.can_close = is_admin or user.karma >= post.karma_close
            post.can_unlink = is_admin or user.karma >= post.karma_unlink
            post.can_upvote = is_admin or user.karma >= post.forum_id.karma_upvote
            post.can_downvote = is_admin or user.karma >= post.forum_id.karma_downvote
            post.can_comment = is_admin or user.karma >= post.karma_comment
            post.can_comment_convert = is_admin or user.karma >= post.karma_comment_convert
            post.can_view = is_admin or user.karma >= post.karma_close or post_sudo.create_uid.karma > 0
            post.can_display_biography = is_admin or post_sudo.create_uid.karma >= post.forum_id.karma_user_bio
            post.can_post = is_admin or user.karma >= post.forum_id.karma_post
            post.can_flag = is_admin or user.karma >= post.forum_id.karma_flag
            post.can_moderate = is_admin or user.karma >= post.forum_id.karma_moderate

    @api.one
    @api.constrains('post_type', 'forum_id')
    def _check_post_type(self):
        if (self.post_type == 'question' and not self.forum_id.allow_question) \
                or (self.post_type == 'discussion' and not self.forum_id.allow_discussion) \
                or (self.post_type == 'link' and not self.forum_id.allow_link):
            raise UserError(_('This forum does not allow %s' % self.post_type))

    def _update_content(self, content, forum_id):
        forum = self.env['forum.forum'].browse(forum_id)
        if content and self.env.user.karma < forum.karma_dofollow:
            for match in re.findall(r'<a\s.*href=".*?">', content):
                content = re.sub(match,
                                 match[:3] + 'rel="nofollow" ' + match[3:],
                                 content)

        if self.env.user.karma <= forum.karma_editor:
            filter_regexp = r'(<img.*?>)|(<a[^>]*?href[^>]*?>)|(<[a-z|A-Z]+[^>]*style\s*=\s*[\'"][^\'"]*\s*background[^:]*:[^url;]*url)'
            content_match = re.search(filter_regexp, content, re.I)
            if content_match:
                raise KarmaError(
                    'User karma not sufficient to post an image or link.')
        return content

    @api.model
    def create(self, vals):
        if 'content' in vals and vals.get('forum_id'):
            vals['content'] = self._update_content(vals['content'],
                                                   vals['forum_id'])

        post = super(Post,
                     self.with_context(mail_create_nolog=True)).create(vals)
        # deleted or closed questions
        if post.parent_id and (post.parent_id.state == 'close'
                               or post.parent_id.active is False):
            raise UserError(
                _('Posting answer on a [Deleted] or [Closed] question is not possible'
                  ))
        # karma-based access
        if not post.parent_id and not post.can_ask:
            raise KarmaError('Not enough karma to create a new question')
        elif post.parent_id and not post.can_answer:
            raise KarmaError('Not enough karma to answer to a question')
        if not post.parent_id and not post.can_post:
            post.state = 'pending'

        # add karma for posting new questions
        if not post.parent_id and post.state == 'active':
            self.env.user.sudo().add_karma(
                post.forum_id.karma_gen_question_new)

        post.post_notification()
        return post

    @api.model
    def check_mail_message_access(self, res_ids, operation, model_name=None):
        if operation in ('write', 'unlink') and (not model_name or model_name
                                                 == 'forum.post'):
            # Make sure only author or moderator can edit/delete messages
            if any(not post.can_edit for post in self.browse(res_ids)):
                raise KarmaError('Not enough karma to edit a post.')
        return super(Post,
                     self).check_mail_message_access(res_ids,
                                                     operation,
                                                     model_name=model_name)

    @api.multi
    @api.depends('name', 'post_type')
    def name_get(self):
        result = []
        for post in self:
            if post.post_type == 'discussion' and post.parent_id and not post.name:
                result.append(
                    (post.id, '%s (%s)' % (post.parent_id.name, post.id)))
            else:
                result.append((post.id, '%s' % (post.name)))
        return result

    @api.multi
    def write(self, vals):
        if 'content' in vals:
            vals['content'] = self._update_content(vals['content'],
                                                   self.forum_id.id)
        if 'state' in vals:
            if vals['state'] in ['active', 'close'] and any(not post.can_close
                                                            for post in self):
                raise KarmaError('Not enough karma to close or reopen a post.')
        if 'active' in vals:
            if any(not post.can_unlink for post in self):
                raise KarmaError(
                    'Not enough karma to delete or reactivate a post')
        if 'is_correct' in vals:
            if any(not post.can_accept for post in self):
                raise KarmaError(
                    'Not enough karma to accept or refuse an answer')
            # update karma except for self-acceptance
            mult = 1 if vals['is_correct'] else -1
            for post in self:
                if vals['is_correct'] != post.is_correct and post.create_uid.id != self._uid:
                    post.create_uid.sudo().add_karma(
                        post.forum_id.karma_gen_answer_accepted * mult)
                    self.env.user.sudo().add_karma(
                        post.forum_id.karma_gen_answer_accept * mult)
        if any(key not in [
                'state', 'active', 'is_correct', 'closed_uid', 'closed_date',
                'closed_reason_id'
        ] for key in vals.keys()) and any(not post.can_edit for post in self):
            raise KarmaError('Not enough karma to edit a post.')

        res = super(Post, self).write(vals)
        # if post content modify, notify followers
        if 'content' in vals or 'name' in vals:
            for post in self:
                if post.parent_id:
                    body, subtype = _(
                        'Answer Edited'), 'website_forum.mt_answer_edit'
                    obj_id = post.parent_id
                else:
                    body, subtype = _(
                        'Question Edited'), 'website_forum.mt_question_edit'
                    obj_id = post
                obj_id.message_post(body=body, subtype=subtype)
        return res

    @api.multi
    def post_notification(self):
        base_url = self.env['ir.config_parameter'].get_param('web.base.url')
        for post in self:
            if post.state == 'active' and post.parent_id:
                body = _(
                    '<p>A new answer for <i>%s</i> has been posted. <a href="%s/forum/%s/question/%s">Click here to access the post.</a></p>'
                    % (post.parent_id.name, base_url,
                       slug(post.parent_id.forum_id), slug(post.parent_id)))
                post.parent_id.message_post(
                    subject=_('Re: %s') % post.parent_id.name,
                    body=body,
                    subtype='website_forum.mt_answer_new')
            elif post.state == 'active' and not post.parent_id:
                body = _(
                    '<p>A new question <i>%s</i> has been asked on %s. <a href="%s/forum/%s/question/%s">Click here to access the question.</a></p>'
                    % (post.name, post.forum_id.name, base_url,
                       slug(post.forum_id), slug(post)))
                post.message_post(subject=post.name,
                                  body=body,
                                  subtype='website_forum.mt_question_new')
            elif post.state == 'pending' and not post.parent_id:
                # TDE FIXME: in master, you should probably use a subtype;
                # however here we remove subtype but set partner_ids
                partners = post.sudo().message_partner_ids.filtered(
                    lambda partner: partner.user_ids and partner.user_ids.karma
                    >= post.forum_id.karma_moderate)
                note_subtype = self.sudo().env.ref('mail.mt_note')
                body = _(
                    '<p>A new question <i>%s</i> has been asked on %s and require your validation. <a href="%s/forum/%s/question/%s">Click here to access the question.</a></p>'
                    % (post.name, post.forum_id.name, base_url,
                       slug(post.forum_id), slug(post)))
                post.message_post(subject=post.name,
                                  body=body,
                                  subtype_id=note_subtype.id,
                                  partner_ids=partners.ids)
        return True

    @api.multi
    def reopen(self):
        if any(post.parent_id or post.state != 'close' for post in self):
            return False

        reason_offensive = self.env.ref('website_forum.reason_7')
        reason_spam = self.env.ref('website_forum.reason_8')
        for post in self:
            if post.closed_reason_id in (reason_offensive, reason_spam):
                _logger.info(
                    'Upvoting user <%s>, reopening spam/offensive question',
                    post.create_uid)
                post.create_uid.sudo().add_karma(
                    post.forum_id.karma_gen_answer_flagged * -1)

        self.sudo().write({'state': 'active'})

    @api.multi
    def close(self, reason_id):
        if any(post.parent_id for post in self):
            return False

        reason_offensive = self.env.ref('website_forum.reason_7').id
        reason_spam = self.env.ref('website_forum.reason_8').id
        if reason_id in (reason_offensive, reason_spam):
            for post in self:
                _logger.info(
                    'Downvoting user <%s> for posting spam/offensive contents',
                    post.create_uid)
                post.create_uid.sudo().add_karma(
                    post.forum_id.karma_gen_answer_flagged)

        self.write({
            'state':
            'close',
            'closed_uid':
            self._uid,
            'closed_date':
            datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT),
            'closed_reason_id':
            reason_id,
        })
        return True

    @api.one
    def validate(self):
        if not self.can_moderate:
            raise KarmaError('Not enough karma to validate a post')

        # if state == pending, no karma previously added for the new question
        if self.state == 'pending':
            self.create_uid.sudo().add_karma(
                self.forum_id.karma_gen_question_new)

        self.write({
            'state': 'active',
            'active': True,
            'moderator_id': self.env.user.id,
        })
        self.post_notification()
        return True

    @api.one
    def refuse(self):
        if not self.can_moderate:
            raise KarmaError('Not enough karma to refuse a post')

        self.moderator_id = self.env.user
        return True

    @api.one
    def flag(self):
        if not self.can_flag:
            raise KarmaError('Not enough karma to flag a post')

        if (self.state == 'flagged'):
            return {'error': 'post_already_flagged'}
        elif (self.state == 'active'):
            self.write({
                'state': 'flagged',
                'flag_user_id': self.env.user.id,
            })
            return self.can_moderate and {
                'success': 'post_flagged_moderator'
            } or {
                'success': 'post_flagged_non_moderator'
            }
        else:
            return {'error': 'post_non_flaggable'}

    @api.one
    def mark_as_offensive(self, reason_id):
        if not self.can_moderate:
            raise KarmaError('Not enough karma to mark a post as offensive')

        # remove some karma
        _logger.info(
            'Downvoting user <%s> for posting spam/offensive contents',
            self.create_uid)
        self.create_uid.sudo().add_karma(
            self.forum_id.karma_gen_answer_flagged)

        self.write({
            'state':
            'offensive',
            'moderator_id':
            self.env.user.id,
            'closed_date':
            datetime.today().strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT),
            'closed_reason_id':
            reason_id,
            'active':
            False,
        })
        return True

    @api.multi
    def unlink(self):
        if any(not post.can_unlink for post in self):
            raise KarmaError('Not enough karma to unlink a post')
        # if unlinking an answer with accepted answer: remove provided karma
        for post in self:
            if post.is_correct:
                post.create_uid.sudo().add_karma(
                    post.forum_id.karma_gen_answer_accepted * -1)
                self.env.user.sudo().add_karma(
                    post.forum_id.karma_gen_answer_accepted * -1)
        return super(Post, self).unlink()

    @api.multi
    def bump(self):
        """ Bump a question: trigger a write_date by writing on a dummy bump_date
        field. One cannot bump a question more than once every 10 days. """
        self.ensure_one()
        if self.forum_id.allow_bump and not self.child_ids and (datetime.today(
        ) - datetime.strptime(self.write_date,
                              tools.DEFAULT_SERVER_DATETIME_FORMAT)).days > 9:
            # write through super to bypass karma; sudo to allow public user to bump any post
            return self.sudo().write({'bump_date': fields.Datetime.now()})
        return False

    @api.multi
    def vote(self, upvote=True):
        Vote = self.env['forum.post.vote']
        vote_ids = Vote.search([('post_id', 'in', self._ids),
                                ('user_id', '=', self._uid)])
        new_vote = '1' if upvote else '-1'
        voted_forum_ids = set()
        if vote_ids:
            for vote in vote_ids:
                if upvote:
                    new_vote = '0' if vote.vote == '-1' else '1'
                else:
                    new_vote = '0' if vote.vote == '1' else '-1'
                vote.vote = new_vote
                voted_forum_ids.add(vote.post_id.id)
        for post_id in set(self._ids) - voted_forum_ids:
            for post_id in self._ids:
                Vote.create({'post_id': post_id, 'vote': new_vote})
        return {'vote_count': self.vote_count, 'user_vote': new_vote}

    @api.one
    def convert_answer_to_comment(self):
        """ Tools to convert an answer (forum.post) to a comment (mail.message).
        The original post is unlinked and a new comment is posted on the question
        using the post create_uid as the comment's author. """
        if not self.parent_id:
            return False

        # karma-based action check: use the post field that computed own/all value
        if not self.can_comment_convert:
            raise KarmaError(
                'Not enough karma to convert an answer to a comment')

        # post the message
        question = self.parent_id
        values = {
            'author_id':
            self.sudo().create_uid.partner_id.
            id,  # use sudo here because of access to res.users model
            'body':
            tools.html_sanitize(self.content,
                                strict=True,
                                strip_style=True,
                                strip_classes=True),
            'message_type':
            'comment',
            'subtype':
            'mail.mt_comment',
            'date':
            self.create_date,
        }
        new_message = self.browse(question.id).with_context(
            mail_create_nosubscribe=True).message_post(**values)

        # unlink the original answer, using SUPERUSER_ID to avoid karma issues
        self.sudo().unlink()

        return new_message

    @api.model
    def convert_comment_to_answer(self, message_id, default=None):
        """ Tool to convert a comment (mail.message) into an answer (forum.post).
        The original comment is unlinked and a new answer from the comment's author
        is created. Nothing is done if the comment's author already answered the
        question. """
        comment = self.env['mail.message'].sudo().browse(message_id)
        post = self.browse(comment.res_id)
        if not comment.author_id or not comment.author_id.user_ids:  # only comment posted by users can be converted
            return False

        # karma-based action check: must check the message's author to know if own / all
        karma_convert = comment.author_id.id == self.env.user.partner_id.id and post.forum_id.karma_comment_convert_own or post.forum_id.karma_comment_convert_all
        can_convert = self.env.user.karma >= karma_convert
        if not can_convert:
            raise KarmaError(
                'Not enough karma to convert a comment to an answer')

        # check the message's author has not already an answer
        question = post.parent_id if post.parent_id else post
        post_create_uid = comment.author_id.user_ids[0]
        if any(answer.create_uid.id == post_create_uid.id
               for answer in question.child_ids):
            return False

        # create the new post
        post_values = {
            'forum_id': question.forum_id.id,
            'content': comment.body,
            'parent_id': question.id,
        }
        # done with the author user to have create_uid correctly set
        new_post = self.sudo(post_create_uid.id).create(post_values)

        # delete comment
        comment.unlink()

        return new_post

    @api.one
    def unlink_comment(self, message_id):
        user = self.env.user
        comment = self.env['mail.message'].sudo().browse(message_id)
        if not comment.model == 'forum.post' or not comment.res_id == self.id:
            return False
        # karma-based action check: must check the message's author to know if own or all
        karma_unlink = comment.author_id.id == user.partner_id.id and self.forum_id.karma_comment_unlink_own or self.forum_id.karma_comment_unlink_all
        can_unlink = user.karma >= karma_unlink
        if not can_unlink:
            raise KarmaError('Not enough karma to unlink a comment')
        return comment.unlink()

    @api.multi
    def set_viewed(self):
        self._cr.execute(
            """UPDATE forum_post SET views = views+1 WHERE id IN %s""",
            (self._ids, ))
        return True

    @api.multi
    def get_access_action(self):
        """ Override method that generated the link to access the document. Instead
        of the classic form view, redirect to the post on the website directly """
        self.ensure_one()
        return {
            'type': 'ir.actions.act_url',
            'url': '/forum/%s/question/%s' % (self.forum_id.id, self.id),
            'target': 'self',
            'res_id': self.id,
        }

    @api.multi
    def _notification_get_recipient_groups(self, message, recipients):
        """ Override to set the access button: everyone can see an access button
        on their notification email. It will lead on the website view of the
        post. """
        res = super(Post, self)._notification_get_recipient_groups(
            message, recipients)
        access_action = self._notification_link_helper('view',
                                                       model=message.model,
                                                       res_id=message.res_id)
        for category, data in res.iteritems():
            res[category]['button_access'] = {
                'url': access_action,
                'title': '%s %s' % (_('View'), self.post_type)
            }
        return res

    @api.cr_uid_ids_context
    def message_post(self,
                     cr,
                     uid,
                     thread_id,
                     message_type='notification',
                     subtype=None,
                     context=None,
                     **kwargs):
        if thread_id and message_type == 'comment':  # user comments have a restriction on karma
            if isinstance(thread_id, (list, tuple)):
                post_id = thread_id[0]
            else:
                post_id = thread_id
            post = self.browse(cr, uid, post_id, context=context)
            # TDE FIXME: trigger browse because otherwise the function field is not compted - check with RCO
            tmp1, tmp2 = post.karma_comment, post.can_comment
            user = self.pool['res.users'].browse(cr, uid, uid)
            tmp3 = user.karma
            # TDE END FIXME
            if not post.can_comment:
                raise KarmaError('Not enough karma to comment')
        return super(Post, self).message_post(cr,
                                              uid,
                                              thread_id,
                                              message_type=message_type,
                                              subtype=subtype,
                                              context=context,
                                              **kwargs)
コード例 #22
0
class ModulePrototyper(models.Model):
    """Module Prototyper gathers different information from all over the
    database to build a prototype of module.
    We are calling it a prototype as it will most likely need to be reviewed
    by a developer to fix glitch that would sneak it during the generation of
    files but also to add not supported features.
    """
    _name = "module_prototyper"
    _description = "Module Prototyper"

    def get_default_description(self):
        """
        Extract the content of default description
        """
        filepath = '{}/../data/README.rst'.format(os.path.dirname(__file__))
        with open(filepath, 'r') as content_file:
            content = content_file.read()
        return content

    license = fields.Selection(
        [(licenses.GPL3, 'GPL Version 3'),
         (licenses.GPL3_L, 'GPL-3 or later version'),
         (licenses.LGPL3, 'LGPL-3'),
         (licenses.LGPL3_L, 'LGPL-3 or later version'),
         (licenses.AGPL3, 'Affero GPL-3'),
         (licenses.OSI, 'Other OSI Approved Licence'),
         ('Other proprietary', 'Other Proprietary')],
        string='License',
        default=licenses.AGPL3,
    )
    name = fields.Char(
        'Technical Name',
        required=True,
        help=('The technical name will be used to define the name of '
              'the exported module, the name of the model.'))
    category_id = fields.Many2one('ir.module.category', 'Category')
    human_name = fields.Char(
        'Module Name',
        required=True,
        help=('The Module Name will be used as the displayed name of the '
              'exported module.'))
    summary = fields.Char('Summary',
                          required=True,
                          help=('Enter a summary of your module'))
    description = fields.Text(
        'Description',
        required=True,
        help=('Enter the description of your module, what it does, how to '
              'install, configure and use it, the roadmap or known issues. '
              'The description will be exported in README.rst'),
        default=get_default_description)
    author = fields.Char('Author', required=True, help=('Enter your name'))
    maintainer = fields.Char(
        'Maintainer',
        help=('Enter the name of the person or organization who will '
              'maintain this module'))
    website = fields.Char('Website', help=('Enter the URL of your website'))
    icon_image = fields.Binary(
        'Icon',
        help=('The icon set up here will be used as the icon '
              'for the exported module also'))
    version = fields.Char(
        'Version',
        size=9,
        default='5.0.1.0.0',
        help=('Enter the version of your module with 5 digits'))
    auto_install = fields.Boolean(
        'Auto Install',
        default=False,
        help='Check if the module should be install by default.')
    application = fields.Boolean(
        'Application',
        default=False,
        help='Check if the module is an eCore application.')
    # Relations
    dependency_ids = fields.Many2many(
        'ir.module.module',
        'module_prototyper_module_rel',
        'module_prototyper_id',
        'module_id',
        'Dependencies',
        help=('Enter the list of required modules that need to be installed '
              'for your module to work properly'))
    data_ids = fields.Many2many(
        'ir.filters',
        'prototype_data_rel',
        'module_prototyper_id',
        'filter_id',
        'Data filters',
        help="The records matching the filters will be added as data.")
    demo_ids = fields.Many2many(
        'ir.filters',
        'prototype_demo_rel',
        'module_prototyper_id',
        'filter_id',
        'Demo filters',
        help="The records matching the filters will be added as demo data.")
    field_ids = fields.Many2many(
        'ir.model.fields',
        'prototype_fields_rel',
        'module_prototyper_id',
        'field_id',
        'Fields',
        help=('Enter the list of fields that you have created or modified '
              'and want to export in this module. New models will be '
              'exported as long as you choose one of his fields.'))
    menu_ids = fields.Many2many(
        'ir.ui.menu',
        'prototype_menu_rel',
        'module_prototyper_id',
        'menu_id',
        'Menu Items',
        help=('Enter the list of menu items that you have created and want '
              'to export in this module. Related windows actions will be '
              'exported as well.'))
    view_ids = fields.Many2many(
        'ir.ui.view',
        'prototype_view_rel',
        'module_prototyper_id',
        'view_id',
        'Views',
        help=('Enter the list of views that you have created and want to '
              'export in this module.'))
    group_ids = fields.Many2many(
        'res.groups',
        'prototype_groups_rel',
        'module_prototyper_id',
        'group_id',
        'Groups',
        help=('Enter the list of groups that you have created and want to '
              'export in this module.'))
    right_ids = fields.Many2many(
        'ir.model.access',
        'prototype_rights_rel',
        'module_prototyper_id',
        'right_id',
        'Access Rights',
        help=('Enter the list of access rights that you have created and '
              'want to export in this module.'))
    rule_ids = fields.Many2many(
        'ir.rule',
        'prototype_rule_rel',
        'module_prototyper_id',
        'rule_id',
        'Record Rules',
        help=('Enter the list of record rules that you have created and '
              'want to export in this module.'))
    report_ids = fields.Many2many(
        'ir.actions.report.xml',
        'prototype_report_rel',
        'module_prototyper_id',
        'report_id',
        'Reports',
        help=('Enter the list of reports that you have created and '
              'want to export in this module.'))
    activity_ids = fields.Many2many(
        'workflow.activity',
        'prototype_wf_activity_rel',
        'module_prototyper_id',
        'activity_id',
        'Activities',
        help=('Enter the list of workflow activities that you have created '
              'and want to export in this module'))
    transition_ids = fields.Many2many(
        'workflow.transition',
        'prototype_wf_transition_rel',
        'module_prototyper_id',
        'transition_id',
        'Transitions',
        help=('Enter the list of workflow transitions that you have created '
              'and want to export in this module'))

    _env = None
    _data_files = ()
    _demo_files = ()
    _field_descriptions = None
    File_details = namedtuple('file_details', ['filename', 'filecontent'])
    template_path = '{}/../templates/'.format(os.path.dirname(__file__))

    @api.model
    def set_jinja_env(self, api_version):
        """Set the Jinja2 environment.
        The environment will helps the system to find the templates to render.
        :param api_version: string, ecore api
        :return: jinja2.Environment instance.
        """
        if self._env is None:
            self._env = Environment(lstrip_blocks=True,
                                    trim_blocks=True,
                                    loader=FileSystemLoader(
                                        os.path.join(self.template_path,
                                                     api_version)))
        return self._env

    def set_field_descriptions(self):
        """Mock the list of fields into dictionary.
        It allows us to add or change attributes of the fields.

        :return: None
        """
        for field in self.field_ids:
            field_description = {}
            # This will mock a field record.
            # the mock will allow us to add data or modify the data
            # of the field (like for the name) with keeping all the
            # attributes of the record.
            field_description.update({
                attr_name: getattr(field, attr_name)
                for attr_name in dir(field) if not attr_name[0] == '_'
            })
            field_description['name'] = self.unprefix(field.name)
            self._field_descriptions[field] = field_description

    @api.model
    def generate_files(self):
        """ Generates the files from the details of the prototype.
        :return: tuple
        """
        assert self._env is not None, \
            'Run set_env(api_version) before to generate files.'

        # Avoid sharing these across instances
        self._data_files = []
        self._demo_files = []
        self._field_descriptions = {}
        self.set_field_descriptions()
        file_details = []
        file_details.extend(self.generate_models_details())
        file_details.extend(self.generate_views_details())
        file_details.extend(self.generate_menus_details())
        file_details.append(self.generate_module_init_file_details())
        file_details.extend(self.generate_data_files())
        # must be the last as the other generations might add information
        # to put in the __ecore__: additional dependencies, views files, etc.
        file_details.append(self.generate_module_ecore_file_details())
        if self.icon_image:
            file_details.append(self.save_icon())

        return file_details

    @api.model
    def save_icon(self):
        """Save the icon of the prototype as a image.
        The image is used afterwards as the icon of the exported module.

        :return: FileDetails instance
        """
        # TODO: The image is not always a jpg.
        # 2 ways to do it:
        #   * find a way to detect image type from the data
        #   * add document as a dependency.
        # The second options seems to be better, as Document is a base module.
        return self.File_details(
            os.path.join('static', 'description', 'icon.jpg'),
            base64.b64decode(self.icon_image))

    @api.model
    def generate_module_ecore_file_details(self):
        """Wrapper to generate the __ecore__.py file of the module."""
        return self.generate_file_details(
            '__ecore__.py',
            '__ecore__.py.template',
            prototype=self,
            data_files=self._data_files,
            demo_fiels=self._demo_files,
        )

    @api.model
    def generate_module_init_file_details(self):
        """Wrapper to generate the __init__.py file of the module."""
        return self.generate_file_details(
            '__init__.py',
            '__init__.py.template',
            # no import models if no work of fields in
            # the prototype
            models=bool(self.field_ids))

    @api.model
    def generate_models_details(self):
        """
        Finds the models from the list of fields and generates
        the __init__ file and each models files (one by class).
        """
        files = []
        # TODO: doesn't work as need to find the module to import
        # and it is not necessary the name of the model the fields
        # belongs to.
        # ie. field.cell_phone is defined in a model inheriting from
        # res.partner.
        # How do we find the module the field was defined in?
        # dependencies = set([dep.id for dep in self.dependencies])

        relations = {}
        field_descriptions = self._field_descriptions or {}
        for field in field_descriptions.itervalues():
            model = field.get('model_id')
            relations.setdefault(model, []).append(field)
            # dependencies.add(model.id)

        # blind update of dependencies.
        # self.write({
        #     'dependencies': [(6, 0, [id_ for id_ in dependencies])]
        # })

        files.append(self.generate_models_init_details(relations.keys()))
        for model, custom_fields in relations.iteritems():
            files.append(self.generate_model_details(model, custom_fields))

        return files

    @api.model
    def generate_models_init_details(self, ir_models):
        """Wrapper to generate the __init__.py file in models folder."""
        return self.generate_file_details(
            'models/__init__.py',
            'models/__init__.py.template',
            models=[
                self.friendly_name(ir_model.model) for ir_model in ir_models
            ])

    @api.model
    def generate_views_details(self):
        """Wrapper to generate the views files."""
        relations = {}
        for view in self.view_ids:
            relations.setdefault(view.model, []).append(view)

        views_details = []
        for model, views in relations.iteritems():
            filepath = 'views/{}_view.xml'.format(
                self.friendly_name(self.unprefix(model)))
            views_details.append(
                self.generate_file_details(filepath,
                                           'views/model_views.xml.template',
                                           views=views))
            self._data_files.append(filepath)

        return views_details

    @api.model
    def generate_menus_details(self):
        """Wrapper to generate the menus files."""
        relations = {}
        for menu in self.menu_ids:
            if menu.action and menu.action.res_model:
                model = self.unprefix(menu.action.res_model)
            else:
                model = 'ir_ui'
            relations.setdefault(model, []).append(menu)

        menus_details = []
        for model_name, menus in relations.iteritems():
            model_name = self.unprefix(model_name)
            filepath = 'views/{}_menus.xml'.format(
                self.friendly_name(model_name))
            menus_details.append(
                self.generate_file_details(
                    filepath,
                    'views/model_menus.xml.template',
                    menus=menus,
                ))
            self._data_files.append(filepath)

        return menus_details

    @api.model
    def generate_model_details(self, model, field_descriptions):
        """Wrapper to generate the python file for the model.

        :param model: ir.model record.
        :param field_descriptions: list of ir.model.fields records.
        :return: FileDetails instance.
        """
        python_friendly_name = self.friendly_name(self.unprefix(model.model))
        return self.generate_file_details(
            'models/{}.py'.format(python_friendly_name),
            'models/model_name.py.template',
            name=python_friendly_name,
            model=model,
            fields=field_descriptions,
        )

    @api.model
    def generate_data_files(self):
        """ Generate data and demo files """
        data, demo = {}, {}
        filters = [(data, ir_filter) for ir_filter in self.data_ids
                   ] + [(demo, ir_filter) for ir_filter in self.demo_ids]

        for target, ir_filter in filters:
            model = ir_filter.model_id
            model_obj = self.env[model]
            target.setdefault(model, model_obj.browse([]))
            target[model] |= model_obj.search(safe_eval(ir_filter.domain))

        res = []
        for prefix, model_data, file_list in [('data', data, self._data_files),
                                              ('demo', demo, self._demo_files)
                                              ]:
            for model_name, records in model_data.iteritems():
                fname = self.friendly_name(self.unprefix(model_name))
                filename = '{0}/{1}.xml'.format(prefix, fname)
                self._data_files.append(filename)

                res.append(
                    self.generate_file_details(
                        filename,
                        'data/model_name.xml.template',
                        model=model_name,
                        records=records,
                    ))

        return res

    @classmethod
    def unprefix(cls, name):
        if not name:
            return name
        return re.sub('^x_', '', name)

    @classmethod
    def is_prefixed(cls, name):
        return bool(re.match('^x_', name))

    @classmethod
    def friendly_name(cls, name):
        return name.replace('.', '_')

    @classmethod
    def fixup_domain(cls, domain):
        """ Fix a domain according to unprefixing of fields """
        res = []
        for elem in domain:
            if len(elem) == 3:
                elem = list(elem)
                elem[0] = cls.unprefix(elem[0])
            res.append(elem)
        return res

    @classmethod
    def fixup_arch(cls, archstr):
        doc = lxml.etree.fromstring(archstr)
        for elem in doc.xpath("//*[@name]"):
            elem.attrib["name"] = cls.unprefix(elem.attrib["name"])

        for elem in doc.xpath("//*[@attrs]"):
            try:
                attrs = safe_eval(elem.attrib["attrs"])
            except Exception:
                _logger.error("Unable to eval attribute: %s, skipping",
                              elem.attrib["attrs"])
                continue

            if isinstance(attrs, dict):
                for key, val in attrs.iteritems():
                    if isinstance(val, (list, tuple)):
                        attrs[key] = cls.fixup_domain(val)
                elem.attrib["attrs"] = repr(attrs)

        for elem in doc.xpath("//field"):
            # Make fields self-closed by removing useless whitespace
            if elem.text and not elem.text.strip():
                elem.text = None

        return lxml.etree.tostring(doc)

    @api.model
    def generate_file_details(self, filename, template, **kwargs):
        """ generate file details from jinja2 template.
        :param filename: name of the file the content is related to
        :param template: path to the file to render the content
        :param kwargs: arguments of the template
        :return: File_details instance
        """
        template = self._env.get_template(template)
        # keywords used in several templates.
        kwargs.update({
            'export_year': date.today().year,
            'author': self.author,
            'website': self.website,
            'license_text': licenses.get_license_text(self.license),
            'cr': self._cr,
            # Utility functions
            'fixup_arch': self.fixup_arch,
            'is_prefixed': self.is_prefixed,
            'unprefix': self.unprefix,
            'wrap': wrap,
        })
        return self.File_details(filename, template.render(kwargs))
コード例 #23
0
class Forum(models.Model):
    _name = 'forum.forum'
    _description = 'Forum'
    _inherit = ['mail.thread', 'website.seo.metadata']

    def init(self, cr):
        """ Add forum uuid for user email validation.

        TDE TODO: move me somewhere else, auto_init ? """
        forum_uuids = self.pool['ir.config_parameter'].search(
            cr, SUPERUSER_ID, [('key', '=', 'website_forum.uuid')])
        if not forum_uuids:
            self.pool['ir.config_parameter'].set_param(cr, SUPERUSER_ID,
                                                       'website_forum.uuid',
                                                       str(uuid.uuid4()),
                                                       ['base.group_system'])

    @api.model
    def _get_default_faq(self):
        fname = modules.get_module_resource('website_forum', 'data',
                                            'forum_default_faq.html')
        with open(fname, 'r') as f:
            return f.read()
        return False

    # description and use
    name = fields.Char('Forum Name', required=True, translate=True)
    faq = fields.Html('Guidelines', default=_get_default_faq, translate=True)
    description = fields.Text(
        'Description',
        translate=True,
        default=
        'This community is for professionals and enthusiasts of our products and services. '
        'Share and discuss the best content and new marketing ideas, '
        'build your professional profile and become a better marketer together.'
    )
    welcome_message = fields.Html(
        'Welcome Message',
        default=
        """<section class="bg-info" style="height: 168px;"><div class="container">
                        <div class="row">
                            <div class="col-md-12">
                                <h1 class="text-center" style="text-align: left;">Welcome!</h1>
                                <p class="text-muted text-center" style="text-align: left;">This community is for professionals and enthusiasts of our products and services. Share and discuss the best content and new marketing ideas, build your professional profile and become a better marketer together.</p>
                            </div>
                            <div class="col-md-12">
                                <a href="#" class="js_close_intro">Hide Intro</a>    <a class="btn btn-primary forum_register_url" href="/web/login">Register</a> </div>
                            </div>
                        </div>
                    </section>""")
    default_order = fields.Selection([('create_date desc', 'Newest'),
                                      ('write_date desc', 'Last Updated'),
                                      ('vote_count desc', 'Most Voted'),
                                      ('relevancy desc', 'Relevance'),
                                      ('child_count desc', 'Answered')],
                                     string='Default Order',
                                     required=True,
                                     default='write_date desc')
    relevancy_post_vote = fields.Float(
        'First Relevance Parameter',
        default=0.8,
        help=
        "This formula is used in order to sort by relevance. The variable 'votes' represents number of votes for a post, and 'days' is number of days since the post creation"
    )
    relevancy_time_decay = fields.Float('Second Relevance Parameter',
                                        default=1.8)
    default_post_type = fields.Selection([('question', 'Question'),
                                          ('discussion', 'Discussion'),
                                          ('link', 'Link')],
                                         string='Default Post',
                                         required=True,
                                         default='question')
    allow_question = fields.Boolean(
        'Questions',
        help=
        "Users can answer only once per question. Contributors can edit answers and mark the right ones.",
        default=True)
    allow_discussion = fields.Boolean('Discussions', default=True)
    allow_link = fields.Boolean(
        'Links',
        help="When clicking on the post, it redirects to an external link",
        default=True)
    allow_bump = fields.Boolean(
        'Allow Bump',
        default=True,
        help='Check this box to display a popup for posts older than 10 days '
        'without any given answer. The popup will offer to share it on social '
        'networks. When shared, a question is bumped at the top of the forum.')
    allow_share = fields.Boolean(
        'Sharing Options',
        default=True,
        help='After posting the user will be proposed to share its question '
        'or answer on social networks, enabling social network propagation '
        'of the forum content.')
    count_posts_waiting_validation = fields.Integer(
        string="Number of posts waiting for validation",
        compute='_compute_count_posts_waiting_validation')
    count_flagged_posts = fields.Integer(
        string='Number of flagged posts',
        compute='_compute_count_flagged_posts')
    # karma generation
    karma_gen_question_new = fields.Integer(string='Asking a question',
                                            default=2)
    karma_gen_question_upvote = fields.Integer(string='Question upvoted',
                                               default=5)
    karma_gen_question_downvote = fields.Integer(string='Question downvoted',
                                                 default=-2)
    karma_gen_answer_upvote = fields.Integer(string='Answer upvoted',
                                             default=10)
    karma_gen_answer_downvote = fields.Integer(string='Answer downvoted',
                                               default=-2)
    karma_gen_answer_accept = fields.Integer(string='Accepting an answer',
                                             default=2)
    karma_gen_answer_accepted = fields.Integer(string='Answer accepted',
                                               default=15)
    karma_gen_answer_flagged = fields.Integer(string='Answer flagged',
                                              default=-100)
    # karma-based actions
    karma_ask = fields.Integer(string='Ask questions', default=3)
    karma_answer = fields.Integer(string='Answer questions', default=3)
    karma_edit_own = fields.Integer(string='Edit own posts', default=1)
    karma_edit_all = fields.Integer(string='Edit all posts', default=300)
    karma_close_own = fields.Integer(string='Close own posts', default=100)
    karma_close_all = fields.Integer(string='Close all posts', default=500)
    karma_unlink_own = fields.Integer(string='Delete own posts', default=500)
    karma_unlink_all = fields.Integer(string='Delete all posts', default=1000)
    karma_upvote = fields.Integer(string='Upvote', default=5)
    karma_downvote = fields.Integer(string='Downvote', default=50)
    karma_answer_accept_own = fields.Integer(
        string='Accept an answer on own questions', default=20)
    karma_answer_accept_all = fields.Integer(
        string='Accept an answer to all questions', default=500)
    karma_comment_own = fields.Integer(string='Comment own posts', default=1)
    karma_comment_all = fields.Integer(string='Comment all posts', default=1)
    karma_comment_convert_own = fields.Integer(
        string='Convert own answers to comments and vice versa', default=50)
    karma_comment_convert_all = fields.Integer(
        string='Convert all answers to comments and vice versa', default=500)
    karma_comment_unlink_own = fields.Integer(string='Unlink own comments',
                                              default=50)
    karma_comment_unlink_all = fields.Integer(string='Unlink all comments',
                                              default=500)
    karma_retag = fields.Integer(string='Change question tags', default=75)
    karma_flag = fields.Integer(string='Flag a post as offensive', default=500)
    karma_dofollow = fields.Integer(
        string='Nofollow links',
        help=
        'If the author has not enough karma, a nofollow attribute is added to links',
        default=500)
    karma_editor = fields.Integer(string='Editor Features: image and links',
                                  default=30,
                                  oldname='karma_editor_link_files')
    karma_user_bio = fields.Integer(string='Display detailed user biography',
                                    default=750)
    karma_post = fields.Integer(string='Ask questions without validation',
                                default=100)
    karma_moderate = fields.Integer(string='Moderate posts', default=1000)

    @api.one
    @api.constrains('allow_question', 'allow_discussion', 'allow_link',
                    'default_post_type')
    def _check_default_post_type(self):
        if (self.default_post_type == 'question' and not self.allow_question) \
                or (self.default_post_type == 'discussion' and not self.allow_discussion) \
                or (self.default_post_type == 'link' and not self.allow_link):
            raise UserError(
                _('You cannot choose %s as default post since the forum does not allow it.'
                  % self.default_post_type))

    @api.one
    @api.constrains('allow_link', 'allow_question', 'allow_discussion',
                    'default_post_type')
    def _check_default_post_type(self):
        if self.default_post_type == 'link' and not self.allow_link or self.default_post_type == 'question' and not self.allow_question or self.default_post_type == 'discussion' and not self.allow_discussion:
            raise Warning(_('Post type in "Default post" must be activated'))

    @api.one
    def _compute_count_posts_waiting_validation(self):
        domain = [('forum_id', '=', self.id), ('state', '=', 'pending')]
        self.count_posts_waiting_validation = self.env[
            'forum.post'].search_count(domain)

    @api.one
    def _compute_count_flagged_posts(self):
        domain = [('forum_id', '=', self.id), ('state', '=', 'flagged')]
        self.count_flagged_posts = self.env['forum.post'].search_count(domain)

    @api.model
    def create(self, values):
        return super(
            Forum,
            self.with_context(mail_create_nolog=True,
                              mail_create_nosubscribe=True)).create(values)

    @api.model
    def _tag_to_write_vals(self, tags=''):
        User = self.env['res.users']
        Tag = self.env['forum.tag']
        post_tags = []
        existing_keep = []
        for tag in filter(None, tags.split(',')):
            if tag.startswith('_'):  # it's a new tag
                # check that not arleady created meanwhile or maybe excluded by the limit on the search
                tag_ids = Tag.search([('name', '=', tag[1:])])
                if tag_ids:
                    existing_keep.append(int(tag_ids[0]))
                else:
                    # check if user have Karma needed to create need tag
                    user = User.sudo().browse(self._uid)
                    if user.exists() and user.karma >= self.karma_retag:
                        post_tags.append((0, 0, {
                            'name': tag[1:],
                            'forum_id': self.id
                        }))
            else:
                existing_keep.append(int(tag))
        post_tags.insert(0, [6, 0, existing_keep])
        return post_tags

    def get_tags_first_char(self):
        """ get set of first letter of forum tags """
        tags = self.env['forum.tag'].search([('forum_id', '=', self.id),
                                             ('posts_count', '>', 0)])
        return sorted(set([tag.name[0].upper() for tag in tags]))
コード例 #24
0
ファイル: web_planner.py プロジェクト: ecoreos/hz
class Planner(models.Model):
    """Planner Model.
    Each Planner has link to an ir.ui.view record that is a template used
    to display the planner pages.
    Each Planner has link to ir.ui.menu record that is a top menu used to display the
    planner launcher(progressbar)

    Method _prepare_<planner_application>_data(self, cr, uid, context) that
    generate the values used to display in specific planner pages
    """

    _name = 'web.planner'
    _description = 'Planner'

    @api.model
    def _get_planner_application(self):
        return []

    name = fields.Char(string='Name', required=True)
    menu_id = fields.Many2one('ir.ui.menu', string='Menu', required=True)
    view_id = fields.Many2one('ir.ui.view', string='Template', required=True)
    progress = fields.Integer(string="Progress Percentage", default=5)
    # data field is used to store the data filled by user in planner(JSON Data)
    data = fields.Text(string='Data')
    tooltip_planner = fields.Html(string='Planner Tooltips', translate=True)
    planner_application = fields.Selection('_get_planner_application',
                                           string='Planner Application',
                                           required=True)
    active = fields.Boolean(
        string="Active",
        default=True,
        help=
        "If the active field is set to False, it will allow you to hide the planner. This change requires a refreshing a your page."
    )

    @api.model
    def render(self, template_id, planner_app):
        # prepare the planner data as per the planner application
        values = {
            'prepare_backend_url': self.prepare_backend_url,
            'is_module_installed': self.is_module_installed,
        }
        planner_find_method_name = '_prepare_%s_data' % planner_app
        if hasattr(self, planner_find_method_name):
            values.update(getattr(
                self, planner_find_method_name)())  # update the default value
        return self.env['ir.ui.view'].browse(template_id).render(values=values)

    @api.model
    def prepare_backend_url(self,
                            action_xml_id,
                            view_type='list',
                            module_name=None):
        """ prepare the backend url to the given action, or to the given module view.
            :param action_xml_id : the xml id of the action to redirect to
            :param view_type : the view type to display when redirecting (form, kanban, list, ...)
            :param module_name : the name of the module to display (if action_xml_id is 'open_module_tree'), or
                                 to redirect to if the action is not found.
            :returns url : the url to the correct page
        """
        params = dict(view_type=view_type)
        # setting the action
        action = self.env.ref(action_xml_id, False)
        if action:
            params['action'] = action.id
        else:
            params['model'] = 'ir.module.module'
        # setting the module
        if module_name:
            installed = self.env['ir.module.module']._installed()
            if module_name in installed:
                params['id'] = installed[module_name]
        return "/web#%s" % (urlencode(params), )

    @api.model
    def is_module_installed(self, module_name=None):
        return module_name in self.env['ir.module.module']._installed()
コード例 #25
0
class account_journal(models.Model):
    _inherit = "account.journal"

    @api.one
    def _kanban_dashboard(self):
        self.kanban_dashboard = json.dumps(self.get_journal_dashboard_datas())

    @api.one
    def _kanban_dashboard_graph(self):
        if (self.type in ['sale', 'purchase']):
            self.kanban_dashboard_graph = json.dumps(
                self.get_bar_graph_datas())
        elif (self.type in ['cash', 'bank']):
            self.kanban_dashboard_graph = json.dumps(
                self.get_line_graph_datas())

    kanban_dashboard = fields.Text(compute='_kanban_dashboard')
    kanban_dashboard_graph = fields.Text(compute='_kanban_dashboard_graph')
    show_on_dashboard = fields.Boolean(
        string='Show journal on dashboard',
        help="Whether this journal should be displayed on the dashboard or not",
        default=True)

    @api.multi
    def toggle_favorite(self):
        self.write(
            {'show_on_dashboard': False if self.show_on_dashboard else True})
        return False

    @api.multi
    def get_line_graph_datas(self):
        data = []
        today = datetime.today()
        last_month = today + timedelta(days=-30)
        bank_stmt = []
        # Query to optimize loading of data for bank statement graphs
        # Return a list containing the latest bank statement balance per day for the
        # last 30 days for current journal
        query = """SELECT a.date, a.balance_end 
                        FROM account_bank_statement AS a, 
                            (SELECT c.date, max(c.id) AS stmt_id 
                                FROM account_bank_statement AS c 
                                WHERE c.journal_id = %s 
                                    AND c.date > %s 
                                    AND c.date <= %s 
                                    GROUP BY date, id 
                                    ORDER BY date, id) AS b 
                        WHERE a.id = b.stmt_id;"""

        self.env.cr.execute(query, (self.id, last_month, today))
        bank_stmt = self.env.cr.dictfetchall()

        last_bank_stmt = self.env['account.bank.statement'].search(
            [('journal_id', 'in', self.ids),
             ('date', '<=', last_month.strftime(DF))],
            order="date desc, id desc",
            limit=1)
        start_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0

        locale = self._context.get('lang', 'en_US')
        show_date = last_month
        #get date in locale format
        name = format_date(show_date, 'd LLLL Y', locale=locale)
        short_name = format_date(show_date, 'd MMM', locale=locale)
        data.append({'x': short_name, 'y': start_balance, 'name': name})

        for stmt in bank_stmt:
            #fill the gap between last data and the new one
            number_day_to_add = (datetime.strptime(stmt.get('date'), DF) -
                                 show_date).days
            last_balance = data[len(data) - 1]['y']
            for day in range(0, number_day_to_add + 1):
                show_date = show_date + timedelta(days=1)
                #get date in locale format
                name = format_date(show_date, 'd LLLL Y', locale=locale)
                short_name = format_date(show_date, 'd MMM', locale=locale)
                data.append({'x': short_name, 'y': last_balance, 'name': name})
            #add new stmt value
            data[len(data) - 1]['y'] = stmt.get('balance_end')

        #continue the graph if the last statement isn't today
        if show_date != today:
            number_day_to_add = (today - show_date).days
            last_balance = data[len(data) - 1]['y']
            for day in range(0, number_day_to_add):
                show_date = show_date + timedelta(days=1)
                #get date in locale format
                name = format_date(show_date, 'd LLLL Y', locale=locale)
                short_name = format_date(show_date, 'd MMM', locale=locale)
                data.append({'x': short_name, 'y': last_balance, 'name': name})

        return [{'values': data, 'area': True}]

    @api.multi
    def get_bar_graph_datas(self):
        data = []
        today = datetime.strptime(fields.Date.context_today(self), DF)
        data.append({'label': _('Past'), 'value': 0.0, 'type': 'past'})
        day_of_week = int(
            format_datetime(today,
                            'e',
                            locale=self._context.get('lang', 'en_US')))
        first_day_of_week = today + timedelta(days=-day_of_week + 1)
        for i in range(-1, 4):
            if i == 0:
                label = _('This Week')
            elif i == 3:
                label = _('Future')
            else:
                start_week = first_day_of_week + timedelta(days=i * 7)
                end_week = start_week + timedelta(days=6)
                if start_week.month == end_week.month:
                    label = str(start_week.day) + '-' + str(
                        end_week.day) + ' ' + format_date(
                            end_week,
                            'MMM',
                            locale=self._context.get('lang', 'en_US'))
                else:
                    label = format_date(
                        start_week,
                        'd MMM',
                        locale=self._context.get(
                            'lang', 'en_US')) + '-' + format_date(
                                end_week,
                                'd MMM',
                                locale=self._context.get('lang', 'en_US'))
            data.append({
                'label': label,
                'value': 0.0,
                'type': 'past' if i < 0 else 'future'
            })

        # Build SQL query to find amount aggregated by week
        select_sql_clause = """SELECT sum(residual_company_signed) as total, min(date) as aggr_date from account_invoice where journal_id = %(journal_id)s and state = 'open'"""
        query = ''
        start_date = (first_day_of_week + timedelta(days=-7))
        for i in range(0, 6):
            if i == 0:
                query += "(" + select_sql_clause + " and date < '" + start_date.strftime(
                    DF) + "')"
            elif i == 6:
                query += " UNION ALL (" + select_sql_clause + " and date >= '" + start_date.strftime(
                    DF) + "')"
            else:
                next_date = start_date + timedelta(days=7)
                query += " UNION ALL (" + select_sql_clause + " and date >= '" + start_date.strftime(
                    DF) + "' and date < '" + next_date.strftime(DF) + "')"
                start_date = next_date

        self.env.cr.execute(query, {'journal_id': self.id})
        query_results = self.env.cr.dictfetchall()
        for index in range(0, len(query_results)):
            if query_results[index].get('aggr_date') != None:
                data[index]['value'] = query_results[index].get('total')

        return [{'values': data}]

    @api.multi
    def get_journal_dashboard_datas(self):
        number_to_reconcile = last_balance = account_sum = 0
        ac_bnk_stmt = []
        title = ''
        number_draft = number_waiting = number_late = sum_draft = sum_waiting = sum_late = 0
        if self.type in ['bank', 'cash']:
            last_bank_stmt = self.env['account.bank.statement'].search(
                [('journal_id', 'in', self.ids)],
                order="date desc, id desc",
                limit=1)
            last_balance = last_bank_stmt and last_bank_stmt[0].balance_end or 0
            ac_bnk_stmt = self.env['account.bank.statement'].search([
                ('journal_id', 'in', self.ids), ('state', '=', 'open')
            ])
            for ac_bnk in ac_bnk_stmt:
                for line in ac_bnk.line_ids:
                    if not line.journal_entry_ids:
                        number_to_reconcile += 1
            # optimization to read sum of balance from account_move_line
            account_ids = tuple(
                filter(None, [
                    self.default_debit_account_id.id,
                    self.default_credit_account_id.id
                ]))
            if account_ids:
                query = """SELECT sum(balance) FROM account_move_line WHERE account_id in %s;"""
                self.env.cr.execute(query, (account_ids, ))
                query_results = self.env.cr.dictfetchall()
                if query_results and query_results[0].get('sum') != None:
                    account_sum = query_results[0].get('sum')
        #TODO need to check if all invoices are in the same currency than the journal!!!!
        elif self.type in ['sale', 'purchase']:
            title = _('Bills to pay') if self.type == 'purchase' else _(
                'Invoices owed to you')
            # optimization to find total and sum of invoice that are in draft, open state
            query = """SELECT state, count(id) AS count, sum(amount_total) AS total FROM account_invoice WHERE journal_id = %s AND state NOT IN ('paid', 'cancel') GROUP BY state;"""
            self.env.cr.execute(query, (self.id, ))
            query_results = self.env.cr.dictfetchall()
            today = datetime.today()
            query = """SELECT count(id) AS count_late, sum(amount_total) AS total FROM account_invoice WHERE journal_id = %s AND date < %s AND state = 'open';"""
            self.env.cr.execute(query, (self.id, today))
            late_query_results = self.env.cr.dictfetchall()
            for result in query_results:
                if result.get('state') in ['draft', 'proforma', 'proforma2']:
                    number_draft = result.get('count')
                    sum_draft = result.get('total')
                elif result.get('state') == 'open':
                    number_waiting = result.get('count')
                    sum_waiting = result.get('total')
            if late_query_results and late_query_results[0].get(
                    'count_late') != None:
                number_late = late_query_results[0].get('count_late')
                sum_late = late_query_results[0].get('total')

        return {
            'number_to_reconcile':
            number_to_reconcile,
            'account_balance':
            formatLang(self.env,
                       account_sum,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'last_balance':
            formatLang(self.env,
                       last_balance,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'number_draft':
            number_draft,
            'number_waiting':
            number_waiting,
            'number_late':
            number_late,
            'sum_draft':
            formatLang(self.env,
                       sum_draft or 0.0,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'sum_waiting':
            formatLang(self.env,
                       sum_waiting or 0.0,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'sum_late':
            formatLang(self.env,
                       sum_late or 0.0,
                       currency_obj=self.currency_id
                       or self.company_id.currency_id),
            'currency_id':
            self.currency_id and self.currency_id.id
            or self.company_id.currency_id.id,
            'bank_statements_source':
            self.bank_statements_source,
            'title':
            title,
        }

    @api.multi
    def action_create_new(self):
        ctx = self._context.copy()
        model = 'account.invoice'
        if self.type == 'sale':
            ctx.update({
                'journal_type': self.type,
                'default_type': 'out_invoice',
                'type': 'out_invoice',
                'default_journal_id': self.id
            })
            if ctx.get('refund'):
                ctx.update({
                    'default_type': 'out_refund',
                    'type': 'out_refund'
                })
            view_id = self.env.ref('account.invoice_form').id
        elif self.type == 'purchase':
            ctx.update({
                'journal_type': self.type,
                'default_type': 'in_invoice',
                'type': 'in_invoice',
                'default_journal_id': self.id
            })
            if ctx.get('refund'):
                ctx.update({'default_type': 'in_refund', 'type': 'in_refund'})
            view_id = self.env.ref('account.invoice_supplier_form').id
        else:
            ctx.update({'default_journal_id': self.id})
            view_id = self.env.ref('account.view_move_form').id
            model = 'account.move'
        return {
            'name': _('Create invoice/bill'),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': model,
            'view_id': view_id,
            'context': ctx,
        }

    @api.multi
    def create_cash_statement(self):
        ctx = self._context.copy()
        ctx.update({
            'journal_id': self.id,
            'default_journal_id': self.id,
            'default_journal_type': 'cash'
        })
        return {
            'name': _('Create cash statement'),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'account.bank.statement',
            'context': ctx,
        }

    @api.multi
    def action_open_reconcile(self):
        if self.type in ['bank', 'cash']:
            # Open reconciliation view for bank statements belonging to this journal
            bank_stmt = self.env['account.bank.statement'].search([
                ('journal_id', 'in', self.ids)
            ])
            return {
                'type': 'ir.actions.client',
                'tag': 'bank_statement_reconciliation_view',
                'context': {
                    'statement_ids': bank_stmt.ids
                },
            }
        else:
            # Open reconciliation view for customers/suppliers
            action_context = {'show_mode_selector': False}
            if self.type == 'sale':
                action_context.update({'mode': 'customers'})
            elif self.type == 'purchase':
                action_context.update({'mode': 'suppliers'})
            return {
                'type': 'ir.actions.client',
                'tag': 'manual_reconciliation_view',
                'context': action_context,
            }

    @api.multi
    def open_action(self):
        """return action based on type for related journals"""
        action_name = self._context.get('action_name', False)
        if not action_name:
            if self.type == 'bank':
                action_name = 'action_bank_statement_tree'
            elif self.type == 'cash':
                action_name = 'action_view_bank_statement_tree'
            elif self.type == 'sale':
                action_name = 'action_invoice_tree1'
            elif self.type == 'purchase':
                action_name = 'action_invoice_tree2'
            else:
                action_name = 'action_move_journal_line'

        _journal_invoice_type_map = {
            'sale': 'out_invoice',
            'purchase': 'in_invoice',
            'bank': 'bank',
            'cash': 'cash',
            'general': 'general',
        }
        invoice_type = _journal_invoice_type_map[self.type]

        ctx = self._context.copy()
        ctx.update({
            'journal_type': self.type,
            'default_journal_id': self.id,
            'search_default_journal_id': self.id,
            'default_type': invoice_type,
            'type': invoice_type
        })
        ir_model_obj = self.pool['ir.model.data']
        model, action_id = ir_model_obj.get_object_reference(
            self._cr, self._uid, 'account', action_name)
        action = self.pool[model].read(self._cr,
                                       self._uid,
                                       action_id,
                                       context=self._context)
        action['context'] = ctx
        return action

    @api.multi
    def open_spend_money(self):
        return self.open_payments_action('outbound')

    @api.multi
    def open_collect_money(self):
        return self.open_payments_action('inbound')

    @api.multi
    def open_transfer_money(self):
        return self.open_payments_action('transfer')

    @api.multi
    def open_payments_action(self, payment_type):
        ctx = self._context.copy()
        ctx.update({
            'default_payment_type': payment_type,
            'default_journal_id': self.id
        })
        action_rec = self.env['ir.model.data'].xmlid_to_object(
            'account.action_account_payments')
        if action_rec:
            action = action_rec.read([])[0]
            action['context'] = ctx
            action['domain'] = [('journal_id', '=', self.id),
                                ('payment_type', '=', payment_type)]
            return action

    @api.multi
    def open_action_with_context(self):
        action_name = self.env.context.get('action_name', False)
        if not action_name:
            return False
        ctx = dict(self.env.context, default_journal_id=self.id)
        if ctx.get('search_default_journal', False):
            ctx.update(search_default_journal_id=self.id)
        ir_model_obj = self.pool['ir.model.data']
        model, action_id = ir_model_obj.get_object_reference(
            self._cr, self._uid, 'account', action_name)
        action = self.pool[model].read(self._cr,
                                       self._uid,
                                       action_id,
                                       context=self._context)
        action['context'] = ctx
        if ctx.get('use_domain', False):
            action['domain'] = [
                '|', ('journal_id', '=', self.id), ('journal_id', '=', False)
            ]
            action['name'] += ' for journal ' + self.name
        return action

    @api.multi
    def create_bank_statement(self):
        """return action to create a bank statements. This button should be called only on journals with type =='bank'"""
        self.bank_statements_source = 'manual'
        action = self.env.ref('account.action_bank_statement_tree').read()[0]
        action.update({
            'views': [[False, 'form']],
            'context':
            "{'default_journal_id': " + str(self.id) + "}",
        })
        return action
コード例 #26
0
ファイル: mail_channel.py プロジェクト: LiberTang0/5
class Channel(models.Model):
    """ A mail.channel is a discussion group that may behave like a listener
    on documents. """
    _description = 'Discussion channel'
    _name = 'mail.channel'
    _mail_flat_thread = False
    _mail_post_access = 'read'
    _inherit = ['mail.thread']
    _inherits = {'mail.alias': 'alias_id'}

    def _get_default_image(self):
        image_path = modules.get_module_resource('mail', 'static/src/img', 'groupdefault.png')
        return tools.image_resize_image_big(open(image_path, 'rb').read().encode('base64'))

    name = fields.Char('Name', required=True, translate=True)
    channel_type = fields.Selection([
        ('chat', 'Chat Discussion'),
        ('channel', 'Channel')],
        'Channel Type', default='channel')
    description = fields.Text('Description')
    uuid = fields.Char('UUID', size=50, select=True, default=lambda self: '%s' % uuid.uuid4())
    email_send = fields.Boolean('Send messages by email', default=False)
    # multi users channel
    channel_last_seen_partner_ids = fields.One2many('mail.channel.partner', 'channel_id', string='Last Seen')
    channel_partner_ids = fields.Many2many('res.partner', 'mail_channel_partner', 'channel_id', 'partner_id', string='Listeners')
    channel_message_ids = fields.Many2many('mail.message', 'mail_message_mail_channel_rel')
    message_is_follower = fields.Boolean('Is a member', compute='_compute_message_is_follower')
    # access
    public = fields.Selection([
        ('public', 'Everyone'),
        ('private', 'Invited people only'),
        ('groups', 'Selected group of users')],
        'Privacy', required=True, default='groups',
        help='This group is visible by non members. Invisible groups can add members through the invite button.')
    group_public_id = fields.Many2one('res.groups', string='Authorized Group',
                                      default=lambda self: self.env.ref('base.group_user'))
    group_ids = fields.Many2many(
        'res.groups', rel='mail_channel_res_group_rel',
        id1='mail_channel_id', id2='groups_id', string='Auto Subscription',
        help="Members of those groups will automatically added as followers. "
             "Note that they will be able to manage their subscription manually "
             "if necessary.")
    # image: all image fields are base64 encoded and PIL-supported
    image = fields.Binary("Photo", default=_get_default_image, attachment=True,
        help="This field holds the image used as photo for the group, limited to 1024x1024px.")
    image_medium = fields.Binary('Medium-sized photo',
        compute='_get_image', inverse='_set_image_medium', store=True, attachment=True,
        help="Medium-sized photo of the group. 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 photo',
        compute='_get_image', inverse='_set_image_small', store=True, attachment=True,
        help="Small-sized photo of the group. It is automatically "
             "resized as a 64x64px image, with aspect ratio preserved. "
             "Use this field anywhere a small image is required.")
    alias_id = fields.Many2one(
        'mail.alias', 'Alias', ondelete="restrict", required=True,
        help="The email address associated with this group. New emails received will automatically create new topics.")

    @api.multi
    def _compute_message_is_follower(self):
        memberships = self.env['mail.channel.partner'].sudo().search([
            ('channel_id', 'in', self.ids),
            ('partner_id', '=', self.env.user.partner_id.id),
            ])
        membership_ids = memberships.mapped('channel_id')
        for record in self:
            record.message_is_follower = record in membership_ids

    @api.one
    @api.depends('image')
    def _get_image(self):
        self.image_medium = tools.image_resize_image_medium(self.image)
        self.image_small = tools.image_resize_image_small(self.image)

    def _set_image_medium(self):
        self.image = tools.image_resize_image_big(self.image_medium)

    def _set_image_small(self):
        self.image = tools.image_resize_image_big(self.image_small)

    @api.model
    def create(self, vals):
        # Create channel and alias
        channel = super(Channel, self.with_context(
            alias_model_name=self._name, alias_parent_model_name=self._name, mail_create_nolog=True, mail_create_nosubscribe=True)
        ).create(vals)
        channel.alias_id.write({"alias_force_thread_id": channel.id, 'alias_parent_thread_id': channel.id})

        if vals.get('group_ids'):
            channel._subscribe_users()

        # make channel listen itself: posting on a channel notifies the channel
        if not self._context.get('mail_channel_noautofollow'):
            channel.message_subscribe(channel_ids=[channel.id])

        return channel

    @api.multi
    def unlink(self):
        aliases = self.mapped('alias_id')

        # Delete mail.channel
        try:
            all_emp_group = self.env.ref('mail.channel_all_employees')
        except ValueError:
            all_emp_group = None
        if all_emp_group and all_emp_group in self:
            raise UserError(_('You cannot delete those groups, as the Whole Company group is required by other modules.'))
        res = super(Channel, self).unlink()
        # Cascade-delete mail aliases as well, as they should not exist without the mail.channel.
        aliases.sudo().unlink()
        return res

    @api.multi
    def write(self, vals):
        result = super(Channel, self).write(vals)
        if vals.get('group_ids'):
            self._subscribe_users()
        return result

    def _subscribe_users(self):
        for mail_channel in self:
            mail_channel.write({'channel_partner_ids': [(4, pid) for pid in mail_channel.mapped('group_ids').mapped('users').mapped('partner_id').ids]})

    @api.multi
    def action_follow(self):
        self.ensure_one()
        channel_partner = self.mapped('channel_last_seen_partner_ids').filtered(lambda cp: cp.partner_id == self.env.user.partner_id)
        if not channel_partner:
            return self.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': self.env.user.partner_id.id})]})

    @api.multi
    def action_unfollow(self):
        partner_id = self.env.user.partner_id.id
        channel_info = self.channel_info('unsubscribe')[0]  # must be computed before leaving the channel (access rights)
        result = self.write({'channel_partner_ids': [(3, partner_id)]})
        self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', partner_id), channel_info)
        if not self.email_send:
            notification = _('<div class="o_mail_notification">left <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
            # post 'channel left' message as root since the partner just unsubscribed from the channel
            self.sudo().message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner_id)
        return result

    @api.multi
    def _notification_group_recipients(self, message, recipients, done_ids, group_data):
        """ All recipients of a message on a channel are considered as partners.
        This means they will receive a minimal email, without a link to access
        in the backend. Mailing lists should indeed send minimal emails to avoid
        the noise. """
        for recipient in recipients:
            group_data['partner'] |= recipient
            done_ids.add(recipient.id)
        return super(Channel, self)._notification_group_recipients(message, recipients, done_ids, group_data)

    @api.multi
    def message_get_email_values(self, notif_mail=None):
        self.ensure_one()
        res = super(Channel, self).message_get_email_values(notif_mail=notif_mail)
        headers = {}
        if res.get('headers'):
            try:
                headers.update(eval(res['headers']))
            except Exception:
                pass
        headers['Precedence'] = 'list'
        # avoid out-of-office replies from MS Exchange
        # http://blogs.technet.com/b/exchange/archive/2006/10/06/3395024.aspx
        headers['X-Auto-Response-Suppress'] = 'OOF'
        if self.alias_domain and self.alias_name:
            headers['List-Id'] = '%s.%s' % (self.alias_name, self.alias_domain)
            headers['List-Post'] = '<mailto:%s@%s>' % (self.alias_name, self.alias_domain)
            # Avoid users thinking it was a personal message
            # X-Forge-To: will replace To: after SMTP envelope is determined by ir.mail.server
            list_to = '"%s" <%s@%s>' % (self.name, self.alias_name, self.alias_domain)
            headers['X-Forge-To'] = list_to
        res['headers'] = repr(headers)
        return res

    @api.multi
    def message_get_recipient_values(self, notif_message=None, recipient_ids=None):
        # real mailing list: multiple recipients (hidden by X-Forge-To)
        if self.alias_domain and self.alias_name:
            return {
                'email_to': ','.join(formataddr((partner.name, partner.email)) for partner in self.env['res.partner'].sudo().browse(recipient_ids)),
                'recipient_ids': [],
            }
        return super(Channel, self).message_get_recipient_values(notif_message=notif_message, recipient_ids=recipient_ids)

    @api.multi
    @api.returns('self', lambda value: value.id)
    def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs):
        # auto pin 'direct_message' channel partner
        self.filtered(lambda channel: channel.channel_type == 'chat').mapped('channel_last_seen_partner_ids').write({'is_pinned': True})
        # apply shortcode (text only) subsitution
        body = self.env['mail.shortcode'].apply_shortcode(body, shortcode_type='text')
        message = super(Channel, self.with_context(mail_create_nosubscribe=True)).message_post(body=body, subject=subject, message_type=message_type, subtype=subtype, parent_id=parent_id, attachments=attachments, content_subtype=content_subtype, **kwargs)
        return message

    #------------------------------------------------------
    # Instant Messaging API
    #------------------------------------------------------
    # A channel header should be broadcasted:
    #   - when adding user to channel (only to the new added partners)
    #   - when folding/minimizing a channel (only to the user making the action)
    # A message should be broadcasted:
    #   - when a message is posted on a channel (to the channel, using _notify() method)

    # Anonymous method
    @api.multi
    def _broadcast(self, partner_ids):
        """ Broadcast the current channel header to the given partner ids
            :param partner_ids : the partner to notify
        """
        notifications = self._channel_channel_notifications(partner_ids)
        self.env['bus.bus'].sendmany(notifications)

    @api.multi
    def _channel_channel_notifications(self, partner_ids):
        """ Generate the bus notifications of current channel for the given partner ids
            :param partner_ids : the partner to send the current channel header
            :returns list of bus notifications (tuple (bus_channe, message_content))
        """
        notifications = []
        for partner in self.env['res.partner'].browse(partner_ids):
            user_id = partner.user_ids and partner.user_ids[0] or False
            if user_id:
                for channel_info in self.sudo(user_id).channel_info():
                    notifications.append([(self._cr.dbname, 'res.partner', partner.id), channel_info])
        return notifications

    @api.multi
    def _notify(self, message):
        """ Broadcast the given message on the current channels.
            Send the message on the Bus Channel (uuid for public mail.channel, and partner private bus channel (the tuple)).
            A partner will receive only on message on its bus channel, even if this message belongs to multiple mail channel. Then 'channel_ids' field
            of the received message indicates on wich mail channel the message should be displayed.
            :param : mail.message to broadcast
        """
        message.ensure_one()
        notifications = self._channel_message_notifications(message)
        self.env['bus.bus'].sendmany(notifications)

    @api.multi
    def _channel_message_notifications(self, message):
        """ Generate the bus notifications for the given message
            :param message : the mail.message to sent
            :returns list of bus notifications (tuple (bus_channe, message_content))
        """
        message_values = message.message_format()[0]
        notifications = []
        for channel in self:
            notifications.append([(self._cr.dbname, 'mail.channel', channel.id), dict(message_values)])
            # add uuid to allow anonymous to listen
            if channel.public == 'public':
                notifications.append([channel.uuid, dict(message_values)])
        return notifications

    @api.multi
    def channel_info(self, extra_info = False):
        """ Get the informations header for the current channels
            :returns a list of channels values
            :rtype : list(dict)
        """
        channel_infos = []
        partner_channels = self.env['mail.channel.partner']
        # find the channel partner state, if logged user
        if self.env.user and self.env.user.partner_id:
            partner_channels = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', 'in', self.ids)])
        # for each channel, build the information header and include the logged partner information
        for channel in self:
            info = {
                'id': channel.id,
                'name': channel.name,
                'uuid': channel.uuid,
                'state': 'open',
                'is_minimized': False,
                'channel_type': channel.channel_type,
                'public': channel.public,
                'mass_mailing': channel.email_send,
            }
            if extra_info:
                info['info'] = extra_info
            # add the partner for 'direct mesage' channel
            if channel.channel_type == 'chat':
                info['direct_partner'] = (channel.sudo()
                                          .with_context(active_test=False)
                                          .channel_partner_ids
                                          .filtered(lambda p: p.id != self.env.user.partner_id.id)
                                          .read(['id', 'name', 'im_status']))
            # add user session state, if available and if user is logged
            if partner_channels.ids:
                partner_channel = partner_channels.filtered(lambda c: channel.id == c.channel_id.id)
                if len(partner_channel) >= 1:
                    partner_channel = partner_channel[0]
                    info['state'] = partner_channel.fold_state or 'open'
                    info['is_minimized'] = partner_channel.is_minimized
                    info['seen_message_id'] = partner_channel.seen_message_id.id
                # add needaction and unread counter, since the user is logged
                info['message_needaction_counter'] = channel.message_needaction_counter
                info['message_unread_counter'] = channel.message_unread_counter
            channel_infos.append(info)
        return channel_infos

    @api.multi
    def channel_fetch_message(self, last_id=False, limit=20):
        """ Return message values of the current channel.
            :param last_id : last message id to start the research
            :param limit : maximum number of messages to fetch
            :returns list of messages values
            :rtype : list(dict)
        """
        self.ensure_one()
        domain = [("channel_ids", "in", self.ids)]
        if last_id:
            domain.append(("id", "<", last_id))
        return self.env['mail.message'].message_fetch(domain=domain, limit=limit)

    # User methods
    @api.model
    def channel_get(self, partners_to, pin=True):
        """ Get the canonical private channel between some partners, create it if needed.
            To reuse an old channel (conversation), this one must be private, and contains
            only the given partners.
            :param partners_to : list of res.partner ids to add to the conversation
            :param pin : True if getting the channel should pin it for the current user
            :returns a channel header, or False if the users_to was False
            :rtype : dict
        """
        if partners_to:
            partners_to.append(self.env.user.partner_id.id)
            # determine type according to the number of partner in the channel
            self.env.cr.execute("""
                SELECT P.channel_id as channel_id
                FROM mail_channel C, mail_channel_partner P
                WHERE P.channel_id = C.id
                    AND C.public LIKE 'private'
                    AND P.partner_id IN %s
                    AND channel_type LIKE 'chat'
                GROUP BY P.channel_id
                HAVING COUNT(P.partner_id) = %s
            """, (tuple(partners_to), len(partners_to),))
            result = self.env.cr.dictfetchall()
            if result:
                # get the existing channel between the given partners
                channel = self.browse(result[0].get('channel_id'))
                # pin up the channel for the current partner
                if pin:
                    self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)]).write({'is_pinned': True})
            else:
                # create a new one
                channel = self.create({
                    'channel_partner_ids': [(4, partner_id) for partner_id in partners_to],
                    'public': 'private',
                    'channel_type': 'chat',
                    'email_send': False,
                    'name': ', '.join(self.env['res.partner'].sudo().browse(partners_to).mapped('name')),
                })
                # broadcast the channel header to the other partner (not me)
                channel._broadcast(partners_to)
            return channel.channel_info()[0]
        return False

    @api.model
    def channel_get_and_minimize(self, partners_to):
        channel = self.channel_get(partners_to)
        if channel:
            self.channel_minimize(channel['uuid'])
        return channel

    @api.model
    def channel_fold(self, uuid, state=None):
        """ Update the fold_state of the given session. In order to syncronize web browser
            tabs, the change will be broadcast to himself (the current user channel).
            Note: the user need to be logged
            :param state : the new status of the session for the current user.
        """
        domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)]
        for session_state in self.env['mail.channel.partner'].search(domain):
            if not state:
                state = session_state.fold_state
                if session_state.fold_state == 'open':
                    state = 'folded'
                else:
                    state = 'open'
            session_state.write({
                'fold_state': state,
                'is_minimized': bool(state != 'closed'),
            })
            self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), session_state.channel_id.channel_info()[0])

    @api.model
    def channel_minimize(self, uuid, minimized=True):
        values = {
            'fold_state': minimized and 'open' or 'closed',
            'is_minimized': minimized
        }
        domain = [('partner_id', '=', self.env.user.partner_id.id), ('channel_id.uuid', '=', uuid)]
        channel_partners = self.env['mail.channel.partner'].search(domain)
        channel_partners.write(values)
        self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_partners.channel_id.channel_info()[0])

    @api.model
    def channel_pin(self, uuid, pinned=False):
        # add the person in the channel, and pin it (or unpin it)
        channel = self.search([('uuid', '=', uuid)])
        channel_partners = self.env['mail.channel.partner'].search([('partner_id', '=', self.env.user.partner_id.id), ('channel_id', '=', channel.id)])
        if not pinned:
            self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel.channel_info('unsubscribe')[0])
        if channel_partners:
            channel_partners.write({'is_pinned': pinned})

    @api.multi
    def channel_seen(self):
        self.ensure_one()
        if self.channel_message_ids.ids:
            last_message_id = self.channel_message_ids.ids[0] # zero is the index of the last message
            self.env['mail.channel.partner'].search([('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id)]).write({'seen_message_id': last_message_id})
            self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), {'info': 'channel_seen', 'id': self.id, 'last_message_id': last_message_id})
            return last_message_id

    @api.multi
    def channel_invite(self, partner_ids):
        """ Add the given partner_ids to the current channels and broadcast the channel header to them.
            :param partner_ids : list of partner id to add
        """
        partners = self.env['res.partner'].browse(partner_ids)
        # add the partner
        for channel in self:
            partners_to_add = partners - channel.channel_partner_ids
            channel.write({'channel_last_seen_partner_ids': [(0, 0, {'partner_id': partner_id}) for partner_id in partners_to_add.ids]})
            for partner in partners_to_add:
                notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
                self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment", author_id=partner.id)

        # broadcast the channel header to the added partner
        self._broadcast(partner_ids)

    #------------------------------------------------------
    # Instant Messaging View Specific (Slack Client Action)
    #------------------------------------------------------
    @api.model
    def get_init_notifications(self):
        """ Get unread messages and old messages received less than AWAY_TIMER
            ago of minimized channel ONLY. This aims to set the minimized channel
            when refreshing the page.
            Note : the user need to be logged
        """
        # get current user's minimzed channel
        minimized_channels = self.env['mail.channel.partner'].search([('is_minimized', '=', True), ('partner_id', '=', self.env.user.partner_id.id)]).mapped('channel_id')

        # get the message since the AWAY_TIMER
        threshold = datetime.datetime.now() - datetime.timedelta(seconds=AWAY_TIMER)
        threshold = threshold.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
        domain = [('channel_ids', 'in', minimized_channels.ids), ('create_date', '>', threshold)]

        # get the message since the last poll of the user
        presence = self.env['bus.presence'].search([('user_id', '=', self._uid)], limit=1)
        if presence:
            domain.append(('create_date', '>', presence.last_poll))

        # do the message search
        message_values = self.env['mail.message'].message_fetch(domain=domain)

        # create the notifications (channel infos first, then messages)
        notifications = []
        for channel_info in minimized_channels.channel_info():
            notifications.append([(self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info])
        for message_value in message_values:
            for channel_id in message_value['channel_ids']:
                if channel_id in minimized_channels.ids:
                    message_value['channel_ids'] = [channel_id]
                    notifications.append([(self._cr.dbname, 'mail.channel', channel_id), dict(message_value)])
        return notifications

    @api.model
    def channel_fetch_slot(self):
        """ Return the channels of the user grouped by 'slot' (channel, direct_message or private_group), and
            the mapping between partner_id/channel_id for direct_message channels.
            :returns dict : the grouped channels and the mapping
        """
        values = {}
        my_partner_id = self.env.user.partner_id.id
        pinned_channels = self.env['mail.channel.partner'].search([('partner_id', '=', my_partner_id), ('is_pinned', '=', True)]).mapped('channel_id')

        # get the group/public channels
        values['channel_channel'] = self.search([('channel_type', '=', 'channel'), ('public', 'in', ['public', 'groups']), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info()

        # get the pinned 'direct message' channel
        direct_message_channels = self.search([('channel_type', '=', 'chat'), ('id', 'in', pinned_channels.ids)])
        values['channel_direct_message'] = direct_message_channels.channel_info()

        # get the private group
        values['channel_private_group'] = self.search([('channel_type', '=', 'channel'), ('public', '=', 'private'), ('channel_partner_ids', 'in', [my_partner_id])]).channel_info()
        return values

    @api.model
    def channel_search_to_join(self, name=None, domain=None):
        """ Return the channel info of the channel the current partner can join
            :param name : the name of the researched channels
            :param domain : the base domain of the research
            :returns dict : channel dict
        """
        if not domain:
            domain = []
        domain = expression.AND([
            [('channel_type', '=', 'channel')],
            [('channel_partner_ids', 'not in', [self.env.user.partner_id.id])],
            [('public', '!=', 'private')],
            domain
        ])
        if name:
            domain = expression.AND([domain, [('name', 'ilike', '%'+name+'%')]])
        return self.search(domain).read(['name', 'public', 'uuid', 'channel_type'])

    @api.multi
    def channel_join_and_get_info(self):
        self.ensure_one()
        if self.channel_type == 'channel' and not self.email_send:
            notification = _('<div class="o_mail_notification">joined <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (self.id, self.name,)
            self.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
        self.action_follow()

        channel_info = self.channel_info()[0]
        self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
        return channel_info

    @api.model
    def channel_create(self, name, privacy='public'):
        """ Create a channel and add the current partner, broadcast it (to make the user directly
            listen to it when polling)
            :param name : the name of the channel to create
            :param privacy : privacy of the channel. Should be 'public' or 'private'.
            :return dict : channel header
        """
        # create the channel
        new_channel = self.create({
            'name': name,
            'public': privacy,
            'email_send': False,
            'channel_partner_ids': [(4, self.env.user.partner_id.id)]
        })
        channel_info = new_channel.channel_info('creation')[0]
        notification = _('<div class="o_mail_notification">created <a href="#" class="o_channel_redirect" data-oe-id="%s">#%s</a></div>') % (new_channel.id, new_channel.name,)
        new_channel.message_post(body=notification, message_type="notification", subtype="mail.mt_comment")
        self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', self.env.user.partner_id.id), channel_info)
        return channel_info

    @api.model
    def get_mention_suggestions(self, search, limit=8):
        """ Return 'limit'-first channels' id, name and public fields such that the name matches a
            'search' string. Exclude channels of type chat (DM), and private channels the current
            user isn't registered to. """
        domain = expression.AND([
                        [('name', 'ilike', search)],
                        [('channel_type', '=', 'channel')],
                        expression.OR([
                            [('public', '!=', 'private')],
                            [('channel_partner_ids', 'in', [self.env.user.partner_id.id])]
                        ])
                    ])
        return self.search_read(domain, ['id', 'name', 'public'], limit=limit)

    @api.model
    def channel_fetch_listeners(self, uuid):
        """ Return the id, name and email of partners listening to the given channel """
        self._cr.execute("""
            SELECT P.id, P.name, P.email
            FROM mail_channel_partner CP
                INNER JOIN res_partner P ON CP.partner_id = P.id
                INNER JOIN mail_channel C ON CP.channel_id = C.id
            WHERE C.uuid = %s""", (uuid,))
        return self._cr.dictfetchall()

    @api.multi
    def channel_fetch_preview(self):
        """ Return the last message of the given channels """
        self._cr.execute("""
            SELECT mail_channel_id AS id, MAX(mail_message_id) AS message_id
            FROM mail_message_mail_channel_rel
            WHERE mail_channel_id IN %s
            GROUP BY mail_channel_id
            """, (tuple(self.ids),))
        channels_preview = dict((r['message_id'], r) for r in self._cr.dictfetchall())
        last_messages = self.env['mail.message'].browse(channels_preview.keys()).message_format()
        for message in last_messages:
            channel = channels_preview[message['id']]
            del(channel['message_id'])
            channel['last_message'] = message
        return channels_preview.values()
コード例 #27
0
class MailComposer(models.TransientModel):
    """ Generic message composition wizard. You may inherit from this wizard
        at model and view levels to provide specific features.

        The behavior of the wizard depends on the composition_mode field:
        - 'comment': post on a record. The wizard is pre-populated via ``get_record_data``
        - 'mass_mail': wizard in mass mailing mode where the mail details can
            contain template placeholders that will be merged with actual data
            before being sent to each recipient.
    """
    _name = 'mail.compose.message'
    _inherit = 'mail.message'
    _description = 'Email composition wizard'
    _log_access = True
    _batch_size = 500

    @api.model
    def default_get(self, fields):
        """ Handle composition mode. Some details about context keys:
            - comment: default mode, model and ID of a record the user comments
                - default_model or active_model
                - default_res_id or active_id
            - reply: active_id of a message the user replies to
                - default_parent_id or message_id or active_id: ID of the
                    mail.message we reply to
                - message.res_model or default_model
                - message.res_id or default_res_id
            - mass_mail: model and IDs of records the user mass-mails
                - active_ids: record IDs
                - default_model or active_model
        """
        result = super(MailComposer, self).default_get(fields)

        # v6.1 compatibility mode
        result['composition_mode'] = result.get(
            'composition_mode',
            self._context.get('mail.compose.message.mode', 'comment'))
        result['model'] = result.get('model',
                                     self._context.get('active_model'))
        result['res_id'] = result.get('res_id', self._context.get('active_id'))
        result['parent_id'] = result.get('parent_id',
                                         self._context.get('message_id'))

        if not result[
                'model'] or not result['model'] in self.pool or not hasattr(
                    self.env[result['model']], 'message_post'):
            result['no_auto_thread'] = True

        # default values according to composition mode - NOTE: reply is deprecated, fall back on comment
        if result['composition_mode'] == 'reply':
            result['composition_mode'] = 'comment'
        vals = {}
        if 'active_domain' in self._context:  # not context.get() because we want to keep global [] domains
            vals['use_active_domain'] = True
            vals['active_domain'] = '%s' % self._context.get('active_domain')
        if result['composition_mode'] == 'comment':
            vals.update(self.get_record_data(result))

        for field in vals:
            if field in fields:
                result[field] = vals[field]

        # TDE HACK: as mailboxes used default_model='res.users' and default_res_id=uid
        # (because of lack of an accessible pid), creating a message on its own
        # profile may crash (res_users does not allow writing on it)
        # Posting on its own profile works (res_users redirect to res_partner)
        # but when creating the mail.message to create the mail.compose.message
        # access rights issues may rise
        # We therefore directly change the model and res_id
        if result['model'] == 'res.users' and result['res_id'] == self._uid:
            result['model'] = 'res.partner'
            result['res_id'] = self.env.user.partner_id.id

        if fields is not None:
            [
                result.pop(field, None) for field in result.keys()
                if field not in fields
            ]
        return result

    @api.model
    def _get_composition_mode_selection(self):
        return [('comment', 'Post on a document'),
                ('mass_mail', 'Email Mass Mailing'),
                ('mass_post', 'Post on Multiple Documents')]

    composition_mode = fields.Selection(
        selection=_get_composition_mode_selection,
        string='Composition mode',
        default='comment')
    partner_ids = fields.Many2many('res.partner',
                                   'mail_compose_message_res_partner_rel',
                                   'wizard_id', 'partner_id',
                                   'Additional Contacts')
    use_active_domain = fields.Boolean('Use active domain')
    active_domain = fields.Text('Active domain', readonly=True)
    attachment_ids = fields.Many2many(
        'ir.attachment', 'mail_compose_message_ir_attachments_rel',
        'wizard_id', 'attachment_id', 'Attachments')
    is_log = fields.Boolean(
        'Log an Internal Note',
        help='Whether the message is an internal note (comment mode only)')
    subject = fields.Char(default=False)
    # mass mode options
    notify = fields.Boolean(
        'Notify followers',
        help='Notify followers of the document (mass post only)')
    template_id = fields.Many2one('mail.template',
                                  'Use template',
                                  select=True,
                                  domain="[('model', '=', model)]")

    @api.multi
    def check_access_rule(self, operation):
        """ Access rules of mail.compose.message:
            - create: if
                - model, no res_id, I create a message in mass mail mode
            - then: fall back on mail.message acces rules
        """
        # Author condition (CREATE (mass_mail))
        if operation == 'create' and self._uid != SUPERUSER_ID:
            # read mail_compose_message.ids to have their values
            message_values = {}
            self._cr.execute(
                'SELECT DISTINCT id, model, res_id FROM "%s" WHERE id = ANY (%%s) AND res_id = 0'
                % self._table, (self.ids, ))
            for mid, rmod, rid in self._cr.fetchall():
                message_values[mid] = {'model': rmod, 'res_id': rid}
            # remove from the set to check the ids that mail_compose_message accepts
            author_ids = [
                mid for mid, message in message_values.iteritems()
                if message.get('model') and not message.get('res_id')
            ]
            self = self.browse(list(set(self.ids) -
                                    set(author_ids)))  # not sure slef = ...

        return super(MailComposer, self).check_access_rule(operation)

    @api.multi
    def _notify(self, force_send=False, user_signature=True):
        """ Override specific notify method of mail.message, because we do
            not want that feature in the wizard. """
        return

    @api.model
    def get_record_data(self, values):
        """ Returns a defaults-like dict with initial values for the composition
        wizard when sending an email related a previous email (parent_id) or
        a document (model, res_id). This is based on previously computed default
        values. """
        result, subject = {}, False
        if values.get('parent_id'):
            parent = self.env['mail.message'].browse(values.get('parent_id'))
            result['record_name'] = parent.record_name,
            subject = tools.ustr(parent.subject or parent.record_name or '')
            if not values.get('model'):
                result['model'] = parent.model
            if not values.get('res_id'):
                result['res_id'] = parent.res_id
            partner_ids = values.get('partner_ids', list()) + [
                (4, id) for id in parent.partner_ids.ids
            ]
            if self._context.get(
                    'is_private'
            ) and parent.author_id:  # check message is private then add author also in partner list.
                partner_ids += [(4, parent.author_id.id)]
            result['partner_ids'] = partner_ids
        elif values.get('model') and values.get('res_id'):
            doc_name_get = self.env[values.get('model')].browse(
                values.get('res_id')).name_get()
            result['record_name'] = doc_name_get and doc_name_get[0][1] or ''
            subject = tools.ustr(result['record_name'])

        re_prefix = _('Re:')
        if subject and not (subject.startswith('Re:')
                            or subject.startswith(re_prefix)):
            subject = "%s %s" % (re_prefix, subject)
        result['subject'] = subject

        return result

    #------------------------------------------------------
    # Wizard validation and send
    #------------------------------------------------------
    # action buttons call with positionnal arguments only, so we need an intermediary function
    # to ensure the context is passed correctly
    @api.multi
    def send_mail_action(self):
        # TDE/ ???
        return self.with_context(
            report_template_in_attachment=True).send_mail()

    @api.multi
    def send_mail(self, auto_commit=False):
        """ Process the wizard content and proceed with sending the related
            email(s), rendering any template patterns on the fly if needed. """
        for wizard in self:
            # Duplicate attachments linked to the email.template.
            # Indeed, basic mail.compose.message wizard duplicates attachments in mass
            # mailing mode. But in 'single post' mode, attachments of an email template
            # also have to be duplicated to avoid changing their ownership.
            if wizard.attachment_ids and wizard.composition_mode != 'mass_mail' and wizard.template_id:
                new_attachment_ids = []
                for attachment in wizard.attachment_ids:
                    if attachment in wizard.template_id.attachment_ids:
                        new_attachment_ids.append(
                            attachment.copy({
                                'res_model': 'mail.compose.message',
                                'res_id': wizard.id
                            }).id)
                    else:
                        new_attachment_ids.append(attachment.id)
                    wizard.write(
                        {'attachment_ids': [(6, 0, new_attachment_ids)]})

            # Mass Mailing
            mass_mode = wizard.composition_mode in ('mass_mail', 'mass_post')

            Mail = self.env['mail.mail']
            ActiveModel = self.env[wizard.model if wizard.
                                   model else 'mail.thread']
            if wizard.template_id:
                # template user_signature is added when generating body_html
                # mass mailing: use template auto_delete value -> note, for emails mass mailing only
                Mail = Mail.with_context(
                    mail_notify_user_signature=False,
                    mail_server_id=wizard.template_id.mail_server_id.id)
                ActiveModel = ActiveModel.with_context(
                    mail_notify_user_signature=False,
                    mail_auto_delete=wizard.template_id.auto_delete)
            if not hasattr(ActiveModel, 'message_post'):
                ActiveModel = self.env['mail.thread'].with_context(
                    thread_model=wizard.model)
            if wizard.composition_mode == 'mass_post':
                # do not send emails directly but use the queue instead
                # add context key to avoid subscribing the author
                ActiveModel = ActiveModel.with_context(
                    mail_notify_force_send=False, mail_create_nosubscribe=True)
            # wizard works in batch mode: [res_id] or active_ids or active_domain
            if mass_mode and wizard.use_active_domain and wizard.model:
                res_ids = self.env[wizard.model].search(
                    eval(wizard.active_domain)).ids
            elif mass_mode and wizard.model and self._context.get(
                    'active_ids'):
                res_ids = self._context['active_ids']
            else:
                res_ids = [wizard.res_id]

            batch_size = int(self.env['ir.config_parameter'].sudo().get_param(
                'mail.batch_size')) or self._batch_size
            sliced_res_ids = [
                res_ids[i:i + batch_size]
                for i in range(0, len(res_ids), batch_size)
            ]

            for res_ids in sliced_res_ids:
                batch_mails = Mail
                all_mail_values = wizard.get_mail_values(res_ids)
                for res_id, mail_values in all_mail_values.iteritems():
                    if wizard.composition_mode == 'mass_mail':
                        batch_mails |= Mail.create(mail_values)
                    else:
                        subtype = 'mail.mt_comment'
                        if wizard.is_log or (wizard.composition_mode
                                             == 'mass_post'
                                             and not wizard.notify
                                             ):  # log a note: subtype is False
                            subtype = False
                        ActiveModel.browse(res_id).message_post(
                            message_type='comment',
                            subtype=subtype,
                            **mail_values)

                if wizard.composition_mode == 'mass_mail':
                    batch_mails.send(auto_commit=auto_commit)

        return {'type': 'ir.actions.act_window_close'}

    @api.multi
    def get_mail_values(self, res_ids):
        """Generate the values that will be used by send_mail to create mail_messages
        or mail_mails. """
        self.ensure_one()
        results = dict.fromkeys(res_ids, False)
        rendered_values = {}
        mass_mail_mode = self.composition_mode == 'mass_mail'

        # render all template-based value at once
        if mass_mail_mode and self.model:
            rendered_values = self.render_message(res_ids)
        # compute alias-based reply-to in batch
        reply_to_value = dict.fromkeys(res_ids, None)
        if mass_mail_mode and not self.no_auto_thread:
            # reply_to_value = self.env['mail.thread'].with_context(thread_model=self.model).browse(res_ids).message_get_reply_to(default=self.email_from)
            reply_to_value = self.env['mail.thread'].with_context(
                thread_model=self.model).message_get_reply_to(
                    res_ids, default=self.email_from)

        for res_id in res_ids:
            # static wizard (mail.message) values
            mail_values = {
                'subject': self.subject,
                'body': self.body or '',
                'parent_id': self.parent_id and self.parent_id.id,
                'partner_ids': [partner.id for partner in self.partner_ids],
                'attachment_ids':
                [attach.id for attach in self.attachment_ids],
                'author_id': self.author_id.id,
                'email_from': self.email_from,
                'record_name': self.record_name,
                'no_auto_thread': self.no_auto_thread,
            }
            # mass mailing: rendering override wizard static values
            if mass_mail_mode and self.model:
                # always keep a copy, reset record name (avoid browsing records)
                mail_values.update(notification=True,
                                   model=self.model,
                                   res_id=res_id,
                                   record_name=False)
                # auto deletion of mail_mail
                if self.template_id and self.template_id.auto_delete:
                    mail_values['auto_delete'] = True
                # rendered values using template
                email_dict = rendered_values[res_id]
                mail_values['partner_ids'] += email_dict.pop('partner_ids', [])
                mail_values.update(email_dict)
                if not self.no_auto_thread:
                    mail_values.pop('reply_to')
                    if reply_to_value.get(res_id):
                        mail_values['reply_to'] = reply_to_value[res_id]
                if self.no_auto_thread and not mail_values.get('reply_to'):
                    mail_values['reply_to'] = mail_values['email_from']
                # mail_mail values: body -> body_html, partner_ids -> recipient_ids
                mail_values['body_html'] = mail_values.get('body', '')
                mail_values['recipient_ids'] = [
                    (4, id) for id in mail_values.pop('partner_ids', [])
                ]

                # process attachments: should not be encoded before being processed by message_post / mail_mail create
                mail_values['attachments'] = [
                    (name, base64.b64decode(enc_cont))
                    for name, enc_cont in email_dict.pop(
                        'attachments', list())
                ]
                attachment_ids = []
                for attach_id in mail_values.pop('attachment_ids'):
                    new_attach_id = self.env['ir.attachment'].browse(
                        attach_id).copy({
                            'res_model': self._name,
                            'res_id': self.id
                        })
                    attachment_ids.append(new_attach_id.id)
                mail_values['attachment_ids'] = self.env[
                    'mail.thread']._message_preprocess_attachments(
                        mail_values.pop('attachments', []), attachment_ids,
                        'mail.message', 0)

            results[res_id] = mail_values
        return results

    #------------------------------------------------------
    # Template methods
    #------------------------------------------------------

    @api.multi
    @api.onchange('template_id')
    def onchange_template_id_wrapper(self):
        self.ensure_one()
        values = self.onchange_template_id(self.template_id.id,
                                           self.composition_mode, self.model,
                                           self.res_id)['value']
        for fname, value in values.iteritems():
            setattr(self, fname, value)

    @api.multi
    def onchange_template_id(self, template_id, composition_mode, model,
                             res_id):
        """ - mass_mailing: we cannot render, so return the template values
            - normal mode: return rendered values
            /!\ for x2many field, this onchange return command instead of ids
        """
        if template_id and composition_mode == 'mass_mail':
            template = self.env['mail.template'].browse(template_id)
            fields = [
                'subject', 'body_html', 'email_from', 'reply_to',
                'mail_server_id'
            ]
            values = dict((field, getattr(template, field)) for field in fields
                          if getattr(template, field))
            if template.attachment_ids:
                values['attachment_ids'] = [
                    att.id for att in template.attachment_ids
                ]
            if template.mail_server_id:
                values['mail_server_id'] = template.mail_server_id.id
            if template.user_signature and 'body_html' in values:
                signature = self.env.user.signature
                values['body_html'] = tools.append_content_to_html(
                    values['body_html'], signature, plaintext=False)
            if template.report_template:
                attachment = self.env['ir.attachment']
                attach = self.generate_attachment_from_report(
                    template_id, res_id)
                for attach_fname, attach_datas in attach[res_id].pop(
                        'attachments', []):
                    data_attach = {
                        'name': attach_fname,
                        'datas': attach_datas,
                        'datas_fname': attach_fname,
                        'res_model': 'mail.compose.message',
                        'res_id': 0,
                        'type': 'binary',
                    }
                values.setdefault('attachment_ids', list()).append(
                    attachment.create(data_attach).id)

        elif template_id:
            values = self.generate_email_for_composer(template_id,
                                                      [res_id])[res_id]
            # transform attachments into attachment_ids; not attached to the document because this will
            # be done further in the posting process, allowing to clean database if email not send
            Attachment = self.env['ir.attachment']
            for attach_fname, attach_datas in values.pop('attachments', []):
                data_attach = {
                    'name': attach_fname,
                    'datas': attach_datas,
                    'datas_fname': attach_fname,
                    'res_model': 'mail.compose.message',
                    'res_id': 0,
                    'type':
                    'binary',  # override default_type from context, possibly meant for another model!
                }
                values.setdefault('attachment_ids', list()).append(
                    Attachment.create(data_attach).id)
        else:
            default_values = self.with_context(
                default_composition_mode=composition_mode,
                default_model=model,
                default_res_id=res_id).default_get([
                    'composition_mode', 'model', 'res_id', 'parent_id',
                    'partner_ids', 'subject', 'body', 'email_from', 'reply_to',
                    'attachment_ids', 'mail_server_id'
                ])
            values = dict((key, default_values[key]) for key in [
                'subject', 'body', 'partner_ids', 'email_from', 'reply_to',
                'attachment_ids', 'mail_server_id'
            ] if key in default_values)

        if values.get('body_html'):
            values['body'] = values.pop('body_html')

        # This onchange should return command instead of ids for x2many field.
        # ORM handle the assignation of command list on new onchange (api.v8),
        # this force the complete replacement of x2many field with
        # command and is compatible with onchange api.v7
        values = self._convert_to_write(self._convert_to_cache(values))

        return {'value': values}

    @api.multi
    def generate_attachment_from_report(self, template_id, res_id):
        fields = ['attachment_ids']
        result = self.env['mail.template'].with_context(
            tpl_partners_only=True).browse(template_id).generate_email(
                [res_id], fields=fields)
        return result

    @api.multi
    def save_as_template(self):
        """ hit save as template button: current form value will be a new
            template attached to the current document. """
        for record in self:
            models = self.env['ir.model'].search([('model', '=', record.model
                                                   or 'mail.message')])
            model_name = ''
            if models:
                model_name = models.name
            template_name = "%s: %s" % (model_name, tools.ustr(record.subject))
            values = {
                'name':
                template_name,
                'subject':
                record.subject or False,
                'body_html':
                record.body or False,
                'model_id':
                models.id or False,
                'attachment_ids':
                [(6, 0, [att.id for att in record.attachment_ids])],
            }
            template = self.env['mail.template'].create(values)
            # generate the saved template
            record.write({'template_id': template.id})
            record.onchange_template_id_wrapper()
            return _reopen(self, record.id, record.model)

    #------------------------------------------------------
    # Template rendering
    #------------------------------------------------------

    @api.multi
    def render_message(self, res_ids):
        """Generate template-based values of wizard, for the document records given
        by res_ids. This method is meant to be inherited by email_template that
        will produce a more complete dictionary, using Jinja2 templates.

        Each template is generated for all res_ids, allowing to parse the template
        once, and render it multiple times. This is useful for mass mailing where
        template rendering represent a significant part of the process.

        Default recipients are also computed, based on mail_thread method
        message_get_default_recipients. This allows to ensure a mass mailing has
        always some recipients specified.

        :param browse wizard: current mail.compose.message browse record
        :param list res_ids: list of record ids

        :return dict results: for each res_id, the generated template values for
                              subject, body, email_from and reply_to
        """
        self.ensure_one()
        multi_mode = True
        if isinstance(res_ids, (int, long)):
            multi_mode = False
            res_ids = [res_ids]

        subjects = self.render_template(self.subject, self.model, res_ids)
        bodies = self.render_template(self.body,
                                      self.model,
                                      res_ids,
                                      post_process=True)
        emails_from = self.render_template(self.email_from, self.model,
                                           res_ids)
        replies_to = self.render_template(self.reply_to, self.model, res_ids)

        default_recipients = self.env[
            'mail.thread'].message_get_default_recipients(res_model=self.model,
                                                          res_ids=res_ids)

        results = dict.fromkeys(res_ids, False)
        for res_id in res_ids:
            results[res_id] = {
                'subject': subjects[res_id],
                'body': bodies[res_id],
                'email_from': emails_from[res_id],
                'reply_to': replies_to[res_id],
            }
            results[res_id].update(default_recipients.get(res_id, dict()))

        # generate template-based values
        if self.template_id:
            template_values = self.generate_email_for_composer(
                self.template_id.id,
                res_ids,
                fields=[
                    'email_to', 'partner_to', 'email_cc', 'attachment_ids',
                    'mail_server_id'
                ])
        else:
            template_values = {}

        for res_id in res_ids:
            if template_values.get(res_id):
                # recipients are managed by the template
                results[res_id].pop('partner_ids')
                results[res_id].pop('email_to')
                results[res_id].pop('email_cc')
                # remove attachments from template values as they should not be rendered
                template_values[res_id].pop('attachment_ids', None)
            else:
                template_values[res_id] = dict()
            # update template values by composer values
            template_values[res_id].update(results[res_id])

        return multi_mode and template_values or template_values[res_ids[0]]

    @api.model
    def generate_email_for_composer(self, template_id, res_ids, fields=None):
        """ Call email_template.generate_email(), get fields relevant for
            mail.compose.message, transform email_cc and email_to into partner_ids """
        multi_mode = True
        if isinstance(res_ids, (int, long)):
            multi_mode = False
            res_ids = [res_ids]

        if fields is None:
            fields = [
                'subject', 'body_html', 'email_from', 'email_to', 'partner_to',
                'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id'
            ]
        returned_fields = fields + ['partner_ids', 'attachments']
        values = dict.fromkeys(res_ids, False)

        template_values = self.env['mail.template'].with_context(
            tpl_partners_only=True).browse(template_id).generate_email(
                res_ids, fields=fields)
        for res_id in res_ids:
            res_id_values = dict((field, template_values[res_id][field])
                                 for field in returned_fields
                                 if template_values[res_id].get(field))
            res_id_values['body'] = res_id_values.pop('body_html', '')
            values[res_id] = res_id_values

        return multi_mode and values or values[res_ids[0]]

    @api.model
    def render_template(self, template, model, res_ids, post_process=False):
        return self.env['mail.template'].render_template(
            template, model, res_ids, post_process=post_process)
コード例 #28
0
class HrEquipmentRequest(models.Model):
    _name = 'hr.equipment.request'
    _inherit = ['mail.thread']
    _description = 'Maintenance Requests'

    @api.returns('self')
    def _default_employee_get(self):
        return self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)

    @api.returns('self')
    def _default_stage(self):
        return self.env['hr.equipment.stage'].search([], limit=1)

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'stage_id' in init_values and self.stage_id.sequence <= 1:
            return 'hr_equipment.mt_req_created'
        elif 'stage_id' in init_values and self.stage_id.sequence > 1:
            return 'hr_equipment.mt_req_status'
        return super(HrEquipmentRequest, self)._track_subtype(init_values)

    name = fields.Char('Subjects', required=True)
    description = fields.Text('Description')
    request_date = fields.Date('Request Date', track_visibility='onchange', default=fields.Date.context_today)
    employee_id = fields.Many2one('hr.employee', string='Employee', default=_default_employee_get)
    department_id = fields.Many2one('hr.department', string='Department')
    category_id = fields.Many2one('hr.equipment.category', string='Category')
    equipment_id = fields.Many2one('hr.equipment', string='Asset', select=True)
    user_id = fields.Many2one('res.users', string='Assigned to', track_visibility='onchange')
    stage_id = fields.Many2one('hr.equipment.stage', string='Stage', track_visibility='onchange', default=_default_stage)
    priority = fields.Selection([('0', 'Very Low'), ('1', 'Low'), ('2', 'Normal'), ('3', 'High')], string='Priority')
    color = fields.Integer('Color Index')
    close_date = fields.Date('Close Date')
    kanban_state = fields.Selection([('normal', 'In Progress'), ('blocked', 'Blocked'), ('done', 'Ready for next stage')],
                                    string='Kanban State', required=True, default='normal', track_visibility='onchange')
    active = fields.Boolean(default=True, help="Set active to false to hide the maintenance request without deleting it.")


    @api.multi
    def archive_equipment_request(self):
        self.write({'active': False})

    @api.multi
    def reset_equipment_request(self):
        """ Reinsert the equipment request into the maintenance pipe in the first stage"""
        first_stage_obj = self.env['hr.equipment.stage'].search([], order="sequence asc", limit=1)
        self.write({'active': True, 'stage_id': first_stage_obj.id})

    @api.onchange('employee_id', 'department_id')
    def onchange_department_or_employee_id(self):
        domain = []
        if self.department_id:
            domain = [('department_id', '=', self.department_id.id)]
        if self.employee_id and self.department_id:
            domain = ['|'] + domain
        if self.employee_id:
            domain = domain + ['|', ('employee_id', '=', self.employee_id.id), ('employee_id', '=', None)]
        equipment = self.env['hr.equipment'].search(domain, limit=2)
        if len(equipment) == 1:
            self.equipment_id = equipment
        return {'domain': {'equipment_id': domain}}

    @api.onchange('equipment_id')
    def onchange_equipment_id(self):
        self.user_id = self.equipment_id.user_id if self.equipment_id.user_id else self.equipment_id.category_id.user_id
        self.category_id = self.equipment_id.category_id

    @api.onchange('category_id')
    def onchange_category_id(self):
        if not self.user_id or not self.equipment_id or (self.user_id and not self.equipment_id.user_id):
            self.user_id = self.category_id.user_id

    @api.model
    def create(self, vals):
        # context: no_log, because subtype already handle this
        self = self.with_context(mail_create_nolog=True)
        result = super(HrEquipmentRequest, self).create(vals)
        if result.employee_id.user_id:
            result.message_subscribe_users(user_ids=[result.employee_id.user_id.id])
        return result

    @api.multi
    def write(self, vals):
        # Overridden to reset the kanban_state to normal whenever
        # the stage (stage_id) of the Maintenance Request changes.
        if vals and 'kanban_state' not in vals and 'stage_id' in vals:
            vals['kanban_state'] = 'normal'
        if vals.get('employee_id'):
            employee = self.env['hr.employee'].browse(vals['employee_id'])
            if employee and employee.user_id:
                self.message_subscribe_users(user_ids=[employee.user_id.id])
        return super(HrEquipmentRequest, self).write(vals)

    @api.multi
    def _read_group_stage_ids(self, domain, read_group_order=None, access_rights_uid=None):
        """ Read group customization in order to display all the stages in the
            kanban view, even if they are empty
        """
        stage_obj = self.env['hr.equipment.stage']
        order = stage_obj._order
        access_rights_uid = access_rights_uid or self._uid

        if read_group_order == 'stage_id desc':
            order = '%s desc' % order

        stage_ids = stage_obj._search([], order=order, access_rights_uid=access_rights_uid)
        result = [stage.name_get()[0] for stage in stage_obj.browse(stage_ids)]

        # restore order of the search
        result.sort(lambda x, y: cmp(stage_ids.index(x[0]), stage_ids.index(y[0])))

        fold = {}
        for stage in stage_obj.browse(stage_ids):
            fold[stage.id] = stage.fold or False
        return result, fold

    _group_by_full = {
        'stage_id': _read_group_stage_ids
    }

    @api.model
    def message_new(self, msg, custom_values=None):
        """ Overrides mail_thread message_new that is called by the mailgateway
            through message_process.
            This override updates the document according to the email.
        """
        if custom_values is None:
            custom_values = {}
        email = tools.email_split(msg.get('from')) and tools.email_split(msg.get('from'))[0] or False
        user = self.env['res.users'].search([('login', '=', email)], limit=1)
        if user:
            employee = self.env['hr.employee'].search([('user_id', '=', user.id)], limit=1)
            if employee:
                custom_values['employee_id'] = employee and employee[0].id
        return super(HrEquipmentRequest, self).message_new(msg, custom_values=custom_values)
コード例 #29
0
class AccountTaxPython(models.Model):
    _inherit = "account.tax"

    amount_type = fields.Selection(selection_add=[('code', 'Python Code')])

    python_compute = fields.Text(
        string='Python Code',
        default="result = price_unit * 0.10",
        help=
        "Compute the amount of the tax by setting the variable 'result'.\n\n"
        ":param base_amount: float, actual amount on which the tax is applied\n"
        ":param price_unit: float\n"
        ":param quantity: float\n"
        ":param company: res.company recordset singleton\n"
        ":param product: product.product recordset singleton or None\n"
        ":param partner: res.partner recordset singleton or None")
    python_applicable = fields.Text(
        string='Applicable Code',
        default="result = True",
        help=
        "Determine if the tax will be applied by setting the variable 'result' to True or False.\n\n"
        ":param price_unit: float\n"
        ":param quantity: float\n"
        ":param company: res.company recordset singleton\n"
        ":param product: product.product recordset singleton or None\n"
        ":param partner: res.partner recordset singleton or None")

    def _compute_amount(self,
                        base_amount,
                        price_unit,
                        quantity=1.0,
                        product=None,
                        partner=None):
        self.ensure_one()
        if self.amount_type == 'code':
            company = self.env.user.company_id
            localdict = {
                'base_amount': base_amount,
                'price_unit': price_unit,
                'quantity': quantity,
                'product': product,
                'partner': partner,
                'company': company
            }
            safe_eval(self.python_compute, localdict, mode="exec", nocopy=True)
            return localdict['result']
        return super(AccountTaxPython,
                     self)._compute_amount(base_amount, price_unit, quantity,
                                           product, partner)

    @api.v8
    def compute_all(self,
                    price_unit,
                    currency=None,
                    quantity=1.0,
                    product=None,
                    partner=None):
        taxes = self.env['account.tax']
        company = self.env.user.company_id
        for tax in self:
            localdict = {
                'price_unit': price_unit,
                'quantity': quantity,
                'product': product,
                'partner': partner,
                'company': company
            }
            safe_eval(tax.python_applicable,
                      localdict,
                      mode="exec",
                      nocopy=True)
            if localdict.get('result', False):
                taxes += tax
        return super(AccountTaxPython,
                     taxes).compute_all(price_unit, currency, quantity,
                                        product, partner)

    @api.v7
    def compute_all(self,
                    cr,
                    uid,
                    ids,
                    price_unit,
                    currency_id=None,
                    quantity=1.0,
                    product_id=None,
                    partner_id=None,
                    context=None):
        currency = currency_id and self.pool.get('res.currency').browse(
            cr, uid, currency_id, context=context) or None
        product = product_id and self.pool.get('product.product').browse(
            cr, uid, product_id, context=context) or None
        partner = partner_id and self.pool.get('res.partner').browse(
            cr, uid, partner_id, context=context) or None
        ids = isinstance(ids, (int, long)) and [ids] or ids
        recs = self.browse(cr, uid, ids, context=context)
        return recs.compute_all(price_unit, currency, quantity, product,
                                partner)
コード例 #30
0
class HrEquipment(models.Model):
    _name = 'hr.equipment'
    _inherit = ['mail.thread']
    _description = 'Equipment'

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if ('employee_id' in init_values and self.employee_id) or ('department_id' in init_values and self.department_id):
            return 'hr_equipment.mt_mat_assign'
        return super(HrEquipment, self)._track_subtype(init_values)

    @api.multi
    def name_get(self):
        result = []
        for record in self:
            if record.name and record.serial_no:
                result.append((record.id, record.name + '/' + record.serial_no))
            if record.name and not record.serial_no:
                result.append((record.id, record.name))
        return result

    @api.model
    def name_search(self, name, args=None, operator='ilike', limit=100):
        args = args or []
        recs = self.browse()
        if name:
            recs = self.search([('name', '=', name)] + args, limit=limit)
        if not recs:
            recs = self.search([('name', operator, name)] + args, limit=limit)
        return recs.name_get()

    name = fields.Char('Asset Name', required=True, translate=True)
    user_id = fields.Many2one('res.users', string='Technician', track_visibility='onchange')
    employee_id = fields.Many2one('hr.employee', string='Assigned to Employee', track_visibility='onchange')
    department_id = fields.Many2one('hr.department', string='Assigned to Department', track_visibility='onchange')
    category_id = fields.Many2one('hr.equipment.category', string='Asset Category', track_visibility='onchange')
    partner_id = fields.Many2one('res.partner', string='Vendor', domain="[('supplier', '=', 1)]")
    partner_ref = fields.Char('Vendor Reference')
    location = fields.Char('Location')
    model = fields.Char('Model')
    serial_no = fields.Char('Serial Number', copy=False)
    assign_date = fields.Date('Assigned Date', track_visibility='onchange')
    cost = fields.Float('Cost')
    note = fields.Text('Note')
    warranty = fields.Date('Warranty')
    color = fields.Integer('Color Index')
    scrap_date = fields.Date('Scrap Date')
    equipment_assign_to = fields.Selection(
        [('department', 'Department'), ('employee', 'Employee')],
        string='Used By',
        required=True,
        default='employee')
    maintenance_ids = fields.One2many('hr.equipment.request', 'equipment_id')
    maintenance_count = fields.Integer(compute='_compute_maintenance_count', string="Maintenance", store=True)
    maintenance_open_count = fields.Integer(compute='_compute_maintenance_count', string="Current Maintenance", store=True)

    @api.one
    @api.depends('maintenance_ids.stage_id.done')
    def _compute_maintenance_count(self):
        self.maintenance_count = len(self.maintenance_ids)
        self.maintenance_open_count = len(self.maintenance_ids.filtered(lambda x: not x.stage_id.done))


    @api.onchange('equipment_assign_to')
    def _onchange_equipment_assign_to(self):
        if self.equipment_assign_to == 'employee':
            self.department_id = False
        if self.equipment_assign_to == 'department':
            self.employee_id = False
        self.assign_date = fields.Date.context_today(self)

    @api.onchange('category_id')
    def _onchange_category_id(self):
        self.user_id = self.category_id.user_id

    _sql_constraints = [
        ('serial_no', 'unique(serial_no)', "Another asset already exists with this serial number!"),
    ]

    @api.model
    def create(self, vals):
        equipment = super(HrEquipment, self).create(vals)
        # subscribe employee or department manager when equipment assign to him.
        user_ids = []
        if equipment.employee_id and equipment.employee_id.user_id:
            user_ids.append(equipment.employee_id.user_id.id)
        if equipment.department_id and equipment.department_id.manager_id and equipment.department_id.manager_id.user_id:
            user_ids.append(equipment.department_id.manager_id.user_id.id)
        if user_ids:
            equipment.message_subscribe_users(user_ids=user_ids)
        return equipment

    @api.multi
    def write(self, vals):
        user_ids = []
        # subscribe employee or department manager when equipment assign to employee or department.
        if vals.get('employee_id'):
            user_id = self.env['hr.employee'].browse(vals['employee_id'])['user_id']
            if user_id:
                user_ids.append(user_id.id)
        if vals.get('department_id'):
            department = self.env['hr.department'].browse(vals['department_id'])
            if department and department.manager_id and department.manager_id.user_id:
                user_ids.append(department.manager_id.user_id.id)
        if user_ids:
            self.message_subscribe_users(user_ids=user_ids)
        return super(HrEquipment, self).write(vals)

    @api.multi
    def _read_group_category_ids(self, domain, read_group_order=None, access_rights_uid=None):
        """ Read group customization in order to display all the category in the
            kanban view, even if they are empty
        """
        category_obj = self.env['hr.equipment.category']
        order = category_obj._order
        access_rights_uid = access_rights_uid or self._uid
        if read_group_order == 'category_id desc':
            order = '%s desc' % order

        category_ids = category_obj._search([], order=order, access_rights_uid=access_rights_uid)
        result = [category.name_get()[0] for category in category_obj.browse(category_ids)]
        # restore order of the search
        result.sort(lambda x, y: cmp(category_ids.index(x[0]), category_ids.index(y[0])))

        fold = {}
        for category in category_obj.browse(category_ids):
            fold[category.id] = category.fold
        return result, fold

    _group_by_full = {
        'category_id': _read_group_category_ids
    }