예제 #1
0
class ir_cron(models.Model):
    """ Model describing cron jobs (also called actions or tasks).
    """

    # TODO: perhaps in the future we could consider a flag on ir.cron jobs
    # that would cause database wake-up even if the database has not been
    # loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something)
    # See also eagle.cron

    _name = "ir.cron"
    _order = 'cron_name'
    _description = 'Scheduled Actions'

    ir_actions_server_id = fields.Many2one('ir.actions.server',
                                           'Server action',
                                           delegate=True,
                                           ondelete='restrict',
                                           required=True)
    cron_name = fields.Char('Name',
                            related='ir_actions_server_id.name',
                            store=True,
                            readonly=False)
    user_id = fields.Many2one('res.users',
                              string='Scheduler User',
                              default=lambda self: self.env.user,
                              required=True)
    active = fields.Boolean(default=True)
    interval_number = fields.Integer(default=1, help="Repeat every x.")
    interval_type = fields.Selection([('minutes', 'Minutes'),
                                      ('hours', 'Hours'), ('days', 'Days'),
                                      ('weeks', 'Weeks'),
                                      ('months', 'Months')],
                                     string='Interval Unit',
                                     default='months')
    numbercall = fields.Integer(
        string='Number of Calls',
        default=1,
        help=
        'How many times the method is called,\na negative number indicates no limit.'
    )
    doall = fields.Boolean(
        string='Repeat Missed',
        help=
        "Specify if missed occurrences should be executed when the server restarts."
    )
    nextcall = fields.Datetime(
        string='Next Execution Date',
        required=True,
        default=fields.Datetime.now,
        help="Next planned execution date for this job.")
    lastcall = fields.Datetime(
        string='Last Execution Date',
        help=
        "Previous time the cron ran successfully, provided to the job through the context on the `lastcall` key"
    )
    priority = fields.Integer(
        default=5,
        help=
        'The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.'
    )

    @api.model
    def create(self, values):
        values['usage'] = 'ir_cron'
        return super(ir_cron, self).create(values)

    def method_direct_trigger(self):
        self.check_access_rights('write')
        for cron in self:
            self.with_user(cron.user_id).ir_actions_server_id.run()
        return True

    @api.model
    def _handle_callback_exception(self, cron_name, server_action_id, job_id,
                                   job_exception):
        """ Method called when an exception is raised by a job.

        Simply logs the exception and rollback the transaction. """
        self._cr.rollback()

    @api.model
    def _callback(self, cron_name, server_action_id, job_id):
        """ Run the method associated to a given job. It takes care of logging
        and exception handling. Note that the user running the server action
        is the user calling this method. """
        try:
            if self.pool != self.pool.check_signaling():
                # the registry has changed, reload self in the new registry
                self.env.reset()
                self = self.env()[self._name]

            log_depth = (None if _logger.isEnabledFor(logging.DEBUG) else 1)
            eagle.netsvc.log(
                _logger,
                logging.DEBUG,
                'cron.object.execute',
                (self._cr.dbname, self._uid, '*', cron_name, server_action_id),
                depth=log_depth)
            start_time = False
            if _logger.isEnabledFor(logging.DEBUG):
                start_time = time.time()
            self.env['ir.actions.server'].browse(server_action_id).run()
            if start_time and _logger.isEnabledFor(logging.DEBUG):
                end_time = time.time()
                _logger.debug('%.3fs (cron %s, server action %d with uid %d)',
                              end_time - start_time, cron_name,
                              server_action_id, self.env.uid)
            self.pool.signal_changes()
        except Exception as e:
            self.pool.reset_changes()
            _logger.exception(
                "Call from cron %s for server action #%s failed in Job #%s",
                cron_name, server_action_id, job_id)
            self._handle_callback_exception(cron_name, server_action_id,
                                            job_id, e)

    @classmethod
    def _process_job(cls, job_cr, job, cron_cr):
        """ Run a given job taking care of the repetition.

        :param job_cr: cursor to use to execute the job, safe to commit/rollback
        :param job: job to be run (as a dictionary).
        :param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date,
            must not be committed/rolled back!
        """
        try:
            with api.Environment.manage():
                cron = api.Environment(
                    job_cr, job['user_id'],
                    {'lastcall': fields.Datetime.from_string(job['lastcall'])
                     })[cls._name]
                # Use the user's timezone to compare and compute datetimes,
                # otherwise unexpected results may appear. For instance, adding
                # 1 month in UTC to July 1st at midnight in GMT+2 gives July 30
                # instead of August 1st!
                now = fields.Datetime.context_timestamp(cron, datetime.now())
                nextcall = fields.Datetime.context_timestamp(
                    cron, fields.Datetime.from_string(job['nextcall']))
                numbercall = job['numbercall']

                ok = False
                while nextcall < now and numbercall:
                    if numbercall > 0:
                        numbercall -= 1
                    if not ok or job['doall']:
                        cron._callback(job['cron_name'],
                                       job['ir_actions_server_id'], job['id'])
                    if numbercall:
                        nextcall += _intervalTypes[job['interval_type']](
                            job['interval_number'])
                    ok = True
                addsql = ''
                if not numbercall:
                    addsql = ', active=False'
                cron_cr.execute(
                    "UPDATE ir_cron SET nextcall=%s, numbercall=%s, lastcall=%s"
                    + addsql + " WHERE id=%s",
                    (fields.Datetime.to_string(nextcall.astimezone(
                        pytz.UTC)), numbercall,
                     fields.Datetime.to_string(now.astimezone(
                         pytz.UTC)), job['id']))
                cron.flush()
                cron.invalidate_cache()

        finally:
            job_cr.commit()
            cron_cr.commit()

    @classmethod
    def _process_jobs(cls, db_name):
        """ Try to process all cron jobs.

        This selects in database all the jobs that should be processed. It then
        tries to lock each of them and, if it succeeds, run the cron job (if it
        doesn't succeed, it means the job was already locked to be taken care
        of by another thread) and return.

        :raise BadVersion: if the version is different from the worker's
        :raise BadModuleState: if modules are to install/upgrade/remove
        """
        db = eagle.sql_db.db_connect(db_name)
        threading.current_thread().dbname = db_name
        try:
            with db.cursor() as cr:
                # Make sure the database has the same version as the code of
                # base and that no module must be installed/upgraded/removed
                cr.execute(
                    "SELECT latest_version FROM ir_module_module WHERE name=%s",
                    ['base'])
                (version, ) = cr.fetchone()
                cr.execute(
                    "SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s",
                    ['to %'])
                (changes, ) = cr.fetchone()
                if version is None:
                    raise BadModuleState()
                elif version != BASE_VERSION:
                    raise BadVersion()
                # Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1.
                cr.execute("""SELECT * FROM ir_cron
                              WHERE numbercall != 0
                                  AND active AND nextcall <= (now() at time zone 'UTC')
                              ORDER BY priority""")
                jobs = cr.dictfetchall()

            if changes:
                if not jobs:
                    raise BadModuleState()
                # nextcall is never updated if the cron is not executed,
                # it is used as a sentinel value to check whether cron jobs
                # have been locked for a long time (stuck)
                parse = fields.Datetime.from_string
                oldest = min([parse(job['nextcall']) for job in jobs])
                if datetime.now() - oldest > MAX_FAIL_TIME:
                    eagle.modules.reset_modules_state(db_name)
                else:
                    raise BadModuleState()

            for job in jobs:
                lock_cr = db.cursor()
                try:
                    # Try to grab an exclusive lock on the job row from within the task transaction
                    # Restrict to the same conditions as for the search since the job may have already
                    # been run by an other thread when cron is running in multi thread
                    lock_cr.execute("""SELECT *
                                       FROM ir_cron
                                       WHERE numbercall != 0
                                          AND active
                                          AND nextcall <= (now() at time zone 'UTC')
                                          AND id=%s
                                       FOR UPDATE NOWAIT""", (job['id'], ),
                                    log_exceptions=False)

                    locked_job = lock_cr.fetchone()
                    if not locked_job:
                        _logger.debug(
                            "Job `%s` already executed by another process/thread. skipping it",
                            job['cron_name'])
                        continue
                    # Got the lock on the job row, run its code
                    _logger.info('Starting job `%s`.', job['cron_name'])
                    job_cr = db.cursor()
                    try:
                        registry = eagle.registry(db_name)
                        registry[cls._name]._process_job(job_cr, job, lock_cr)
                        _logger.info('Job `%s` done.', job['cron_name'])
                    except Exception:
                        _logger.exception(
                            'Unexpected exception while processing cron job %r',
                            job)
                    finally:
                        job_cr.close()

                except psycopg2.OperationalError as e:
                    if e.pgcode == '55P03':
                        # Class 55: Object not in prerequisite state; 55P03: lock_not_available
                        _logger.debug(
                            'Another process/thread is already busy executing job `%s`, skipping it.',
                            job['cron_name'])
                        continue
                    else:
                        # Unexpected OperationalError
                        raise
                finally:
                    # we're exiting due to an exception while acquiring the lock
                    lock_cr.close()

        finally:
            if hasattr(threading.current_thread(), 'dbname'):
                del threading.current_thread().dbname

    @classmethod
    def _acquire_job(cls, db_name):
        """ Try to process all cron jobs.

        This selects in database all the jobs that should be processed. It then
        tries to lock each of them and, if it succeeds, run the cron job (if it
        doesn't succeed, it means the job was already locked to be taken care
        of by another thread) and return.

        This method hides most exceptions related to the database's version, the
        modules' state, and such.
        """
        try:
            cls._process_jobs(db_name)
        except BadVersion:
            _logger.warning(
                'Skipping database %s as its base version is not %s.', db_name,
                BASE_VERSION)
        except BadModuleState:
            _logger.warning(
                'Skipping database %s because of modules to install/upgrade/remove.',
                db_name)
        except psycopg2.ProgrammingError as e:
            if e.pgcode == '42P01':
                # Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table
                # The table ir_cron does not exist; this is probably not an OpenERP database.
                _logger.warning(
                    'Tried to poll an undefined table on database %s.',
                    db_name)
            else:
                raise
        except Exception:
            _logger.warning('Exception in cron:', exc_info=True)

    def _try_lock(self):
        """Try to grab a dummy exclusive write-lock to the rows with the given ids,
           to make sure a following write() or unlink() will not block due
           to a process currently executing those cron tasks"""
        try:
            self._cr.execute(
                """SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" %
                self._table, [tuple(self.ids)],
                log_exceptions=False)
        except psycopg2.OperationalError:
            self._cr.rollback(
            )  # early rollback to allow translations to work for the user feedback
            raise UserError(
                _("Record cannot be modified right now: "
                  "This cron task is currently being executed and may not be modified "
                  "Please try again in a few minutes"))

    def write(self, vals):
        self._try_lock()
        return super(ir_cron, self).write(vals)

    def unlink(self):
        self._try_lock()
        return super(ir_cron, self).unlink()

    def try_write(self, values):
        try:
            with self._cr.savepoint():
                self._cr.execute(
                    """SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT"""
                    % self._table, [tuple(self.ids)],
                    log_exceptions=False)
        except psycopg2.OperationalError:
            pass
        else:
            return super(ir_cron, self).write(values)
        return False

    @api.model
    def toggle(self, model, domain):
        active = bool(self.env[model].search_count(domain))
        return self.try_write({'active': active})
예제 #2
0
class ProductImage(models.Model):
    _name = 'product.image'
    _description = "Product Image"
    _inherit = ['image.mixin']
    _order = 'sequence, id'

    name = fields.Char("Name", required=True)
    sequence = fields.Integer(default=10, index=True)

    image_1920 = fields.Image(required=True)

    product_tmpl_id = fields.Many2one('product.template',
                                      "Product Template",
                                      index=True,
                                      ondelete='cascade')
    product_variant_id = fields.Many2one('product.product',
                                         "Product Variant",
                                         index=True,
                                         ondelete='cascade')
    image_1920_id = fields.Many2one('res.partner',
                                    "Partner Image",
                                    index=True,
                                    ondelete='cascade')
    video_url = fields.Char('Video URL',
                            help='URL of a video for showcasing your product.')
    embed_code = fields.Char(compute="_compute_embed_code")

    can_image_1024_be_zoomed = fields.Boolean(
        "Can Image 1024 be zoomed",
        compute='_compute_can_image_1024_be_zoomed',
        store=True)

    @api.depends('image_1920', 'image_1024')
    def _compute_can_image_1024_be_zoomed(self):
        for image in self:
            image.can_image_1024_be_zoomed = image.image_1920 and tools.is_image_size_above(
                image.image_1920, image.image_1024)

    @api.depends('video_url')
    def _compute_embed_code(self):
        for image in self:
            image.embed_code = get_video_embed_code(image.video_url)

    @api.constrains('video_url')
    def _check_valid_video_url(self):
        for image in self:
            if image.video_url and not image.embed_code:
                raise ValidationError(
                    _("Provided video URL for '%s' is not valid. Please enter a valid video URL."
                      ) % image.name)

    @api.model_create_multi
    def create(self, vals_list):
        """
            We don't want the default_product_tmpl_id from the context
            to be applied if we have a product_variant_id set to avoid
            having the variant images to show also as template images.
            But we want it if we don't have a product_variant_id set.
        """
        context_without_template = self.with_context({
            k: v
            for k, v in self.env.context.items()
            if k != 'default_product_tmpl_id'
        })
        normal_vals = []
        variant_vals_list = []

        for vals in vals_list:
            if vals.get('product_variant_id'
                        ) and 'default_product_tmpl_id' in self.env.context:
                variant_vals_list.append(vals)
            else:
                normal_vals.append(vals)

        return super().create(normal_vals) + super(
            ProductImage, context_without_template).create(variant_vals_list)
예제 #3
0
class UoM(models.Model):
    _name = 'uom.uom'
    _description = 'Product Unit of Measure'
    _order = "name"

    name = fields.Char('Unit of Measure', required=True, translate=True)
    category_id = fields.Many2one(
        'uom.category',
        'Category',
        required=True,
        ondelete='cascade',
        help=
        "Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios."
    )
    factor = fields.Float(
        'Ratio',
        default=1.0,
        digits=0,
        required=True,  # force NUMERIC with unlimited precision
        help=
        'How much bigger or smaller this unit is compared to the reference Unit of Measure for this category: 1 * (reference unit) = ratio * (this unit)'
    )
    factor_inv = fields.Float(
        'Bigger Ratio',
        compute='_compute_factor_inv',
        digits=0,  # force NUMERIC with unlimited precision
        readonly=True,
        required=True,
        help=
        'How many times this Unit of Measure is bigger than the reference Unit of Measure in this category: 1 * (this unit) = ratio * (reference unit)'
    )
    rounding = fields.Float(
        'Rounding Precision',
        default=0.01,
        digits=0,
        required=True,
        help="The computed quantity will be a multiple of this value. "
        "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece."
    )
    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "Uncheck the active field to disable a unit of measure without deleting it."
    )
    uom_type = fields.Selection(
        [('bigger', 'Bigger than the reference Unit of Measure'),
         ('reference', 'Reference Unit of Measure for this category'),
         ('smaller', 'Smaller than the reference Unit of Measure')],
        'Type',
        default='reference',
        required=1)
    measure_type = fields.Selection(string="Type of measurement category",
                                    related='category_id.measure_type',
                                    store=True,
                                    readonly=True)

    _sql_constraints = [
        ('factor_gt_zero', 'CHECK (factor!=0)',
         'The conversion ratio for a unit of measure cannot be 0!'),
        ('rounding_gt_zero', 'CHECK (rounding>0)',
         'The rounding precision must be strictly positive.'),
        ('factor_reference_is_one',
         "CHECK((uom_type = 'reference' AND factor = 1.0) OR (uom_type != 'reference'))",
         "The reference unit must have a conversion factor equal to 1.")
    ]

    @api.one
    @api.depends('factor')
    def _compute_factor_inv(self):
        self.factor_inv = self.factor and (1.0 / self.factor) or 0.0

    @api.onchange('uom_type')
    def _onchange_uom_type(self):
        if self.uom_type == 'reference':
            self.factor = 1

    @api.constrains('category_id', 'uom_type', 'active')
    def _check_category_reference_uniqueness(self):
        """ Force the existence of only one UoM reference per category
            NOTE: this is a constraint on the all table. This might not be a good practice, but this is
            not possible to do it in SQL directly.
        """
        category_ids = self.mapped('category_id').ids
        self._cr.execute(
            """
            SELECT C.id AS category_id, count(U.id) AS uom_count
            FROM uom_category C
            LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference' AND U.active = 't'
            WHERE C.id IN %s
            GROUP BY C.id
        """, (tuple(category_ids), ))
        for uom_data in self._cr.dictfetchall():
            if uom_data['uom_count'] == 0:
                raise ValidationError(
                    _("UoM category %s should have a reference unit of measure. If you just created a new category, please record the 'reference' unit first."
                      ) % (self.env['uom.category'].browse(
                          uom_data['category_id']).name, ))
            if uom_data['uom_count'] > 1:
                raise ValidationError(
                    _("UoM category %s should only have one reference unit of measure."
                      ) % (self.env['uom.category'].browse(
                          uom_data['category_id']).name, ))

    @api.model_create_multi
    def create(self, vals_list):
        for values in vals_list:
            if 'factor_inv' in values:
                factor_inv = values.pop('factor_inv')
                values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0
        return super(UoM, self).create(vals_list)

    @api.multi
    def write(self, values):
        if 'factor_inv' in values:
            factor_inv = values.pop('factor_inv')
            values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0
        return super(UoM, self).write(values)

    @api.multi
    def unlink(self):
        if self.filtered(lambda uom: uom.measure_type == 'time'):
            raise UserError(
                _("You cannot delete this UoM as it is used by the system. You should rather archive it."
                  ))
        return super(UoM, self).unlink()

    @api.model
    def name_create(self, name):
        """ The UoM category and factor are required, so we'll have to add temporary values
        for imported UoMs """
        values = {self._rec_name: name, 'factor': 1}
        # look for the category based on the english name, i.e. no context on purpose!
        # TODO: should find a way to have it translated but not created until actually used
        if not self._context.get('default_category_id'):
            EnglishUoMCateg = self.env['uom.category'].with_context({})
            misc_category = EnglishUoMCateg.search([
                ('name', '=', 'Unsorted/Imported Units')
            ])
            if misc_category:
                values['category_id'] = misc_category.id
            else:
                values['category_id'] = EnglishUoMCateg.name_create(
                    'Unsorted/Imported Units')[0]
        new_uom = self.create(values)
        return new_uom.name_get()[0]

    @api.multi
    def _compute_quantity(self,
                          qty,
                          to_unit,
                          round=True,
                          rounding_method='UP',
                          raise_if_failure=True):
        """ Convert the given quantity from the current UoM `self` into a given one
            :param qty: the quantity to convert
            :param to_unit: the destination UoM record (uom.uom)
            :param raise_if_failure: only if the conversion is not possible
                - if true, raise an exception if the conversion is not possible (different UoM category),
                - otherwise, return the initial quantity
        """
        if not self:
            return qty
        self.ensure_one()
        if self.category_id.id != to_unit.category_id.id:
            if raise_if_failure:
                raise UserError(
                    _('The unit of measure %s defined on the order line doesn\'t belong to the same category than the unit of measure %s defined on the product. Please correct the unit of measure defined on the order line or on the product, they should belong to the same category.'
                      ) % (self.name, to_unit.name))
            else:
                return qty
        amount = qty / self.factor
        if to_unit:
            amount = amount * to_unit.factor
            if round:
                amount = tools.float_round(amount,
                                           precision_rounding=to_unit.rounding,
                                           rounding_method=rounding_method)
        return amount

    @api.multi
    def _compute_price(self, price, to_unit):
        self.ensure_one()
        if not self or not price or not to_unit or self == to_unit:
            return price
        if self.category_id.id != to_unit.category_id.id:
            return price
        amount = price * self.factor
        if to_unit:
            amount = amount / to_unit.factor
        return amount
예제 #4
0
class IrActionsActWindow(models.Model):
    _name = 'ir.actions.act_window'
    _description = 'Action Window'
    _table = 'ir_act_window'
    _inherit = 'ir.actions.actions'
    _sequence = 'ir_actions_id_seq'
    _order = 'name'

    @api.constrains('res_model', 'src_model')
    def _check_model(self):
        for action in self:
            if action.res_model not in self.env:
                raise ValidationError(
                    _('Invalid model name %r in action definition.') %
                    action.res_model)
            if action.src_model and action.src_model not in self.env:
                raise ValidationError(
                    _('Invalid model name %r in action definition.') %
                    action.src_model)

    @api.depends('view_ids.view_mode', 'view_mode', 'view_id.type')
    def _compute_views(self):
        """ Compute an ordered list of the specific view modes that should be
            enabled when displaying the result of this action, along with the
            ID of the specific view to use for each mode, if any were required.

            This function hides the logic of determining the precedence between
            the view_modes string, the view_ids o2m, and the view_id m2o that
            can be set on the action.
        """
        for act in self:
            act.views = [(view.view_id.id, view.view_mode)
                         for view in act.view_ids]
            got_modes = [view.view_mode for view in act.view_ids]
            all_modes = act.view_mode.split(',')
            missing_modes = [
                mode for mode in all_modes if mode not in got_modes
            ]
            if missing_modes:
                if act.view_id.type in missing_modes:
                    # reorder missing modes to put view_id first if present
                    missing_modes.remove(act.view_id.type)
                    act.views.append((act.view_id.id, act.view_id.type))
                act.views.extend([(False, mode) for mode in missing_modes])

    @api.depends('res_model', 'search_view_id')
    def _compute_search_view(self):
        for act in self:
            fvg = self.env[act.res_model].fields_view_get(
                act.search_view_id.id, 'search')
            act.search_view = str(fvg)

    name = fields.Char(string='Action Name', translate=True)
    type = fields.Char(default="ir.actions.act_window")
    view_id = fields.Many2one('ir.ui.view',
                              string='View Ref.',
                              ondelete='set null')
    domain = fields.Char(
        string='Domain Value',
        help=
        "Optional domain filtering of the destination data, as a Python expression"
    )
    context = fields.Char(
        string='Context Value',
        default={},
        required=True,
        help=
        "Context dictionary as Python expression, empty by default (Default: {})"
    )
    res_id = fields.Integer(
        string='Record ID',
        help=
        "Database ID of record to open in form view, when ``view_mode`` is set to 'form' only"
    )
    res_model = fields.Char(
        string='Destination Model',
        required=True,
        help="Model name of the object to open in the view window")
    src_model = fields.Char(
        string='Source Model',
        help=
        "Optional model name of the objects on which this action should be visible"
    )
    target = fields.Selection([('current', 'Current Window'),
                               ('new', 'New Window'),
                               ('inline', 'Inline Edit'),
                               ('fullscreen', 'Full Screen'),
                               ('main', 'Main action of Current Window')],
                              default="current",
                              string='Target Window')
    view_mode = fields.Char(
        required=True,
        default='tree,form',
        help=
        "Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)"
    )
    view_type = fields.Selection(
        [('tree', 'Tree'), ('form', 'Form')],
        default="form",
        string='View Type',
        required=True,
        help=
        "View type: Tree type to use for the tree view, set to 'tree' for a hierarchical tree view, or 'form' for a regular list view"
    )
    usage = fields.Char(
        string='Action Usage',
        help="Used to filter menu and home actions from the user form.")
    view_ids = fields.One2many('ir.actions.act_window.view',
                               'act_window_id',
                               string='No of Views')
    views = fields.Binary(compute='_compute_views',
                          help="This function field computes the ordered list of views that should be enabled " \
                               "when displaying the result of an action, federating view mode, views and " \
                               "reference view. The result is returned as an ordered list of pairs (view_id,view_mode).")
    limit = fields.Integer(default=80, help='Default limit for the list view')
    groups_id = fields.Many2many('res.groups',
                                 'ir_act_window_group_rel',
                                 'act_id',
                                 'gid',
                                 string='Groups')
    search_view_id = fields.Many2one('ir.ui.view', string='Search View Ref.')
    filter = fields.Boolean()
    auto_search = fields.Boolean(default=True)
    search_view = fields.Text(compute='_compute_search_view')
    multi = fields.Boolean(
        string='Restrict to lists',
        help=
        "If checked and the action is bound to a model, it will only appear in the More menu on list views"
    )

    @api.multi
    def read(self, fields=None, load='_classic_read'):
        """ call the method get_empty_list_help of the model and set the window action help message
        """
        result = super(IrActionsActWindow, self).read(fields, load=load)
        if not fields or 'help' in fields:
            for values in result:
                model = values.get('res_model')
                if model in self.env:
                    eval_ctx = dict(self.env.context)
                    try:
                        ctx = safe_eval(values.get('context', '{}'), eval_ctx)
                    except:
                        ctx = {}
                    values['help'] = self.with_context(
                        **ctx).env[model].get_empty_list_help(
                            values.get('help', ''))
        return result

    @api.model
    def for_xml_id(self, module, xml_id):
        """ Returns the act_window object created for the provided xml_id

        :param module: the module the act_window originates in
        :param xml_id: the namespace-less id of the action (the @id
                       attribute from the XML file)
        :return: A read() view of the ir.actions.act_window
        """
        record = self.env.ref("%s.%s" % (module, xml_id))
        return record.read()[0]

    @api.model_create_multi
    def create(self, vals_list):
        self.clear_caches()
        return super(IrActionsActWindow, self).create(vals_list)

    @api.multi
    def unlink(self):
        self.clear_caches()
        return super(IrActionsActWindow, self).unlink()

    @api.multi
    def exists(self):
        ids = self._existing()
        existing = self.filtered(lambda rec: rec.id in ids)
        if len(existing) < len(self):
            # mark missing records in cache with a failed value
            exc = MissingError(
                _("Record does not exist or has been deleted.") +
                '\n\n({} {}, {} {})'.format(_('Records:'), (
                    self - existing).ids[:6], _('User:'), self._uid))
            for record in (self - existing):
                record._cache.set_failed(self._fields, exc)
        return existing

    @api.model
    @tools.ormcache()
    def _existing(self):
        self._cr.execute("SELECT id FROM %s" % self._table)
        return set(row[0] for row in self._cr.fetchall())
예제 #5
0
class HolidaysType(models.Model):
    _inherit = "hr.leave.type"

    def _default_project_id(self):
        company = self.company_id if self.company_id else self.env.company
        return company.leave_timesheet_project_id.id

    def _default_task_id(self):
        company = self.company_id if self.company_id else self.env.company
        return company.leave_timesheet_task_id.id

    timesheet_generate = fields.Boolean(
        'Generate Timesheet',
        default=True,
        help=
        "If checked, when validating a time off, timesheet will be generated in the Vacation Project of the company."
    )
    timesheet_project_id = fields.Many2one(
        'project.project',
        string="Project",
        default=_default_project_id,
        domain="[('company_id', '=', company_id)]",
        help=
        "The project will contain the timesheet generated when a time off is validated."
    )
    timesheet_task_id = fields.Many2one(
        'project.task',
        string="Task for timesheet",
        default=_default_task_id,
        domain=
        "[('project_id', '=', timesheet_project_id), ('company_id', '=', company_id)]"
    )

    @api.onchange('timesheet_task_id')
    def _onchange_timesheet_generate(self):
        if self.timesheet_task_id or self.timesheet_project_id:
            self.timesheet_generate = True
        else:
            self.timesheet_generate = False

    @api.onchange('timesheet_project_id')
    def _onchange_timesheet_project(self):
        company = self.company_id if self.company_id else self.env.company
        default_task_id = company.leave_timesheet_task_id
        if default_task_id and default_task_id.project_id == self.timesheet_project_id:
            self.timesheet_task_id = default_task_id
        else:
            self.timesheet_task_id = False
        if self.timesheet_project_id:
            self.timesheet_generate = True
        else:
            self.timesheet_generate = False

    @api.constrains('timesheet_generate', 'timesheet_project_id',
                    'timesheet_task_id')
    def _check_timesheet_generate(self):
        for holiday_status in self:
            if holiday_status.timesheet_generate:
                if not holiday_status.timesheet_project_id or not holiday_status.timesheet_task_id:
                    raise ValidationError(
                        _("Both the internal project and task are required to "
                          "generate a timesheet for the time off. If you don't want a timesheet, you should "
                          "leave the internal project and task empty."))
예제 #6
0
class PhoneMixin(models.AbstractModel):
    """ Purpose of this mixin is to offer two services

      * compute a sanitized phone number based on ´´_sms_get_number_fields´´.
        It takes first sanitized value, trying each field returned by the
        method (see ``MailThread._sms_get_number_fields()´´ for more details
        about the usage of this method);
      * compute blacklist state of records. It is based on phone.blacklist
        model and give an easy-to-use field and API to manipulate blacklisted
        records;

    Main API methods

      * ``_phone_set_blacklisted``: set recordset as blacklisted;
      * ``_phone_reset_blacklisted``: reactivate recordset (even if not blacklisted
        this method can be called safely);
    """
    _name = 'mail.thread.phone'
    _description = 'Phone Blacklist Mixin'
    _inherit = ['mail.thread']

    phone_sanitized = fields.Char(
        string='Sanitized Number', compute="_compute_phone_sanitized", compute_sudo=True, store=True,
        help="Field used to store sanitized phone number. Helps speeding up searches and comparisons.")
    phone_blacklisted = fields.Boolean(
        string='Phone Blacklisted', compute="_compute_phone_blacklisted", compute_sudo=True, store=False,
        search="_search_phone_blacklisted", groups="base.group_user",
        help="If the email address is on the blacklist, the contact won't receive mass mailing anymore, from any list")

    @api.depends(lambda self: self._phone_get_number_fields())
    def _compute_phone_sanitized(self):
        self._assert_phone_field()
        number_fields = self._phone_get_number_fields()
        for record in self:
            for fname in number_fields:
                sanitized = record.phone_get_sanitized_number(number_fname=fname)
                if sanitized:
                    break
            record.phone_sanitized = sanitized

    @api.depends('phone_sanitized')
    def _compute_phone_blacklisted(self):
        # TODO : Should remove the sudo as compute_sudo defined on methods.
        # But if user doesn't have access to mail.blacklist, doen't work without sudo().
        blacklist = set(self.env['phone.blacklist'].sudo().search([
            ('number', 'in', self.mapped('phone_sanitized'))]).mapped('number'))
        for record in self:
            record.phone_blacklisted = record.phone_sanitized in blacklist

    @api.model
    def _search_phone_blacklisted(self, operator, value):
        # Assumes operator is '=' or '!=' and value is True or False
        self._assert_phone_field()
        if operator != '=':
            if operator == '!=' and isinstance(value, bool):
                value = not value
            else:
                raise NotImplementedError()

        if value:
            query = """
                SELECT m.id
                    FROM phone_blacklist bl
                    JOIN %s m
                    ON m.phone_sanitized = bl.number AND bl.active
            """
        else:
            query = """
                SELECT m.id
                    FROM %s m
                    LEFT JOIN phone_blacklist bl
                    ON m.phone_sanitized = bl.number AND bl.active
                    WHERE bl.id IS NULL
            """
        self._cr.execute(query % self._table)
        res = self._cr.fetchall()
        if not res:
            return [(0, '=', 1)]
        return [('id', 'in', [r[0] for r in res])]

    def _assert_phone_field(self):
        if not hasattr(self, "_phone_get_number_fields"):
            raise UserError(_('Invalid primary phone field on model %s') % self._name)
        if not any(fname in self and self._fields[fname].type == 'char' for fname in self._phone_get_number_fields()):
            raise UserError(_('Invalid primary phone field on model %s') % self._name)

    def _phone_get_number_fields(self):
        """ This method returns the fields to use to find the number to use to
        send an SMS on a record. """
        return []

    def _phone_get_country_field(self):
        if 'country_id' in self:
            return 'country_id'
        return False

    def phone_get_sanitized_numbers(self, number_fname='mobile', force_format='E164'):
        res = dict.fromkeys(self.ids, False)
        country_fname = self._phone_get_country_field()
        for record in self:
            number = record[number_fname]
            res[record.id] = phone_validation.phone_sanitize_numbers_w_record([number], record, record_country_fname=country_fname, force_format=force_format)[number]['sanitized']
        return res

    def phone_get_sanitized_number(self, number_fname='mobile', force_format='E164'):
        self.ensure_one()
        country_fname = self._phone_get_country_field()
        number = self[number_fname]
        return phone_validation.phone_sanitize_numbers_w_record([number], self, record_country_fname=country_fname, force_format=force_format)[number]['sanitized']

    def _phone_set_blacklisted(self):
        return self.env['phone.blacklist'].sudo()._add([r.phone_sanitized for r in self])

    def _phone_reset_blacklisted(self):
        return self.env['phone.blacklist'].sudo()._remove([r.phone_sanitized for r in self])
예제 #7
0
class PosOrderReport(models.Model):
    _name = "report.pos.order"
    _description = "Point of Sale Orders Report"
    _auto = False
    _order = 'date desc'

    date = fields.Datetime(string='Order Date', readonly=True)
    order_id = fields.Many2one('pos.order', string='Order', readonly=True)
    partner_id = fields.Many2one('res.partner',
                                 string='Customer',
                                 readonly=True)
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 readonly=True)
    product_tmpl_id = fields.Many2one('product.template',
                                      string='Product Template',
                                      readonly=True)
    state = fields.Selection([('draft', 'New'), ('paid', 'Paid'),
                              ('done', 'Posted'), ('invoiced', 'Invoiced'),
                              ('cancel', 'Cancelled')],
                             string='Status')
    user_id = fields.Many2one('res.users', string='Salesperson', readonly=True)
    price_total = fields.Float(string='Total Price', readonly=True)
    price_sub_total = fields.Float(string='Subtotal w/o discount',
                                   readonly=True)
    total_discount = fields.Float(string='Total Discount', readonly=True)
    average_price = fields.Float(string='Average Price',
                                 readonly=True,
                                 group_operator="avg")
    location_id = fields.Many2one('stock.location',
                                  string='Location',
                                  readonly=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True)
    nbr_lines = fields.Integer(string='Sale Line Count',
                               readonly=True,
                               oldname='nbr')
    product_qty = fields.Integer(string='Product Quantity', readonly=True)
    journal_id = fields.Many2one('account.journal', string='Journal')
    delay_validation = fields.Integer(string='Delay Validation')
    product_categ_id = fields.Many2one('product.category',
                                       string='Product Category',
                                       readonly=True)
    invoiced = fields.Boolean(readonly=True)
    config_id = fields.Many2one('pos.config',
                                string='Point of Sale',
                                readonly=True)
    pos_categ_id = fields.Many2one('pos.category',
                                   string='PoS Category',
                                   readonly=True)
    pricelist_id = fields.Many2one('product.pricelist',
                                   string='Pricelist',
                                   readonly=True)
    session_id = fields.Many2one('pos.session',
                                 string='Session',
                                 readonly=True)

    def _select(self):
        return """
            SELECT
                MIN(l.id) AS id,
                COUNT(*) AS nbr_lines,
                s.date_order AS date,
                SUM(l.qty) AS product_qty,
                SUM(l.qty * l.price_unit) AS price_sub_total,
                SUM((l.qty * l.price_unit) * (100 - l.discount) / 100) AS price_total,
                SUM((l.qty * l.price_unit) * (l.discount / 100)) AS total_discount,
                (SUM(l.qty*l.price_unit)/SUM(l.qty * u.factor))::decimal AS average_price,
                SUM(cast(to_char(date_trunc('day',s.date_order) - date_trunc('day',s.create_date),'DD') AS INT)) AS delay_validation,
                s.id as order_id,
                s.partner_id AS partner_id,
                s.state AS state,
                s.user_id AS user_id,
                s.location_id AS location_id,
                s.company_id AS company_id,
                s.sale_journal AS journal_id,
                l.product_id AS product_id,
                pt.categ_id AS product_categ_id,
                p.product_tmpl_id,
                ps.config_id,
                pt.pos_categ_id,
                s.pricelist_id,
                s.session_id,
                s.invoice_id IS NOT NULL AS invoiced
        """

    def _from(self):
        return """
            FROM pos_order_line AS l
                LEFT JOIN pos_order s ON (s.id=l.order_id)
                LEFT JOIN product_product p ON (l.product_id=p.id)
                LEFT JOIN product_template pt ON (p.product_tmpl_id=pt.id)
                LEFT JOIN uom_uom u ON (u.id=pt.uom_id)
                LEFT JOIN pos_session ps ON (s.session_id=ps.id)
        """

    def _group_by(self):
        return """
            GROUP BY
                s.id, s.date_order, s.partner_id,s.state, pt.categ_id,
                s.user_id, s.location_id, s.company_id, s.sale_journal,
                s.pricelist_id, s.invoice_id, s.create_date, s.session_id,
                l.product_id,
                pt.categ_id, pt.pos_categ_id,
                p.product_tmpl_id,
                ps.config_id
        """

    def _having(self):
        return """
            HAVING
                SUM(l.qty * u.factor) != 0
        """

    @api.model_cr
    def init(self):
        tools.drop_view_if_exists(self._cr, self._table)
        self._cr.execute("""
            CREATE OR REPLACE VIEW %s AS (
                %s
                %s
                %s
                %s
            )
        """ % (self._table, self._select(), self._from(), self._group_by(),
               self._having()))
예제 #8
0
class Users(models.Model):
    """ Update of res.users class
        - add a preference about sending emails about notifications
        - make a new user follow itself
        - add a welcome message
        - add suggestion preference
        - if adding groups to a user, check mail.channels linked to this user
          group, and the user. This is done by overriding the write method.
    """
    _name = 'res.users'
    _inherit = ['res.users']
    _description = 'Users'

    alias_id = fields.Many2one('mail.alias', 'Alias', ondelete="set null", required=False,
            help="Email address internally associated with this user. Incoming "\
                 "emails will appear in the user's notifications.", copy=False, auto_join=True)
    alias_contact = fields.Selection([('everyone', 'Everyone'),
                                      ('partners', 'Authenticated Partners'),
                                      ('followers', 'Followers only')],
                                     string='Alias Contact Security',
                                     related='alias_id.alias_contact',
                                     readonly=False)
    notification_type = fields.Selection(
        [('email', 'Handle by Emails'), ('inbox', 'Handle in Eagle')],
        'Notification Management',
        required=True,
        default='email',
        help="Policy on how to handle Chatter notifications:\n"
        "- Handle by Emails: notifications are sent to your email address\n"
        "- Handle in Eagle: notifications appear in your Eagle Inbox")
    # channel-specific: moderation
    is_moderator = fields.Boolean(string='Is moderator',
                                  compute='_compute_is_moderator')
    moderation_counter = fields.Integer(string='Moderation count',
                                        compute='_compute_moderation_counter')
    moderation_channel_ids = fields.Many2many('mail.channel',
                                              'mail_channel_moderator_rel',
                                              string='Moderated channels')

    @api.depends('moderation_channel_ids.moderation',
                 'moderation_channel_ids.moderator_ids')
    @api.multi
    def _compute_is_moderator(self):
        moderated = self.env['mail.channel'].search([
            ('id', 'in', self.mapped('moderation_channel_ids').ids),
            ('moderation', '=', True), ('moderator_ids', 'in', self.ids)
        ])
        user_ids = moderated.mapped('moderator_ids')
        for user in self:
            user.is_moderator = user in user_ids

    @api.multi
    def _compute_moderation_counter(self):
        self._cr.execute(
            """
SELECT channel_moderator.res_users_id, COUNT(msg.id)
FROM "mail_channel_moderator_rel" AS channel_moderator
JOIN "mail_message" AS msg
ON channel_moderator.mail_channel_id = msg.res_id
    AND channel_moderator.res_users_id IN %s
    AND msg.model = 'mail.channel'
    AND msg.moderation_status = 'pending_moderation'
GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)])
        result = dict(self._cr.fetchall())
        for user in self:
            user.moderation_counter = result.get(user.id, 0)

    def __init__(self, pool, cr):
        """ Override of __init__ to add access rights on notification_email_send
            and alias fields. Access rights are disabled by default, but allowed
            on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS.
        """
        init_res = super(Users, self).__init__(pool, cr)
        # duplicate list to avoid modifying the original reference
        type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS)
        type(self).SELF_WRITEABLE_FIELDS.extend(['notification_type'])
        # duplicate list to avoid modifying the original reference
        type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS)
        type(self).SELF_READABLE_FIELDS.extend(['notification_type'])
        return init_res

    @api.model
    def create(self, values):
        if not values.get('login', False):
            action = self.env.ref('base.action_res_users')
            msg = _(
                "You cannot create a new user from here.\n To create new user please go to configuration panel."
            )
            raise exceptions.RedirectWarning(
                msg, action.id, _('Go to the configuration panel'))

        user = super(Users, self).create(values)
        # Auto-subscribe to channels
        self.env['mail.channel'].search([
            ('group_ids', 'in', user.groups_id.ids)
        ])._subscribe_users()
        return user

    @api.multi
    def write(self, vals):
        write_res = super(Users, self).write(vals)
        sel_groups = [
            vals[k] for k in vals if is_selection_groups(k) and vals[k]
        ]
        if vals.get('groups_id'):
            # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]}
            user_group_ids = [
                command[1] for command in vals['groups_id'] if command[0] == 4
            ]
            user_group_ids += [
                id for command in vals['groups_id'] if command[0] == 6
                for id in command[2]
            ]
            self.env['mail.channel'].search([
                ('group_ids', 'in', user_group_ids)
            ])._subscribe_users()
        elif sel_groups:
            self.env['mail.channel'].search([('group_ids', 'in', sel_groups)
                                             ])._subscribe_users()
        return write_res

    @api.model
    def systray_get_activities(self):
        query = """SELECT m.id, count(*), act.res_model as model,
                        CASE
                            WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today'
                            WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue'
                            WHEN %(today)s::date - act.date_deadline::date < 0 Then 'planned'
                        END AS states
                    FROM mail_activity AS act
                    JOIN ir_model AS m ON act.res_model_id = m.id
                    WHERE user_id = %(user_id)s
                    GROUP BY m.id, states, act.res_model;
                    """
        self.env.cr.execute(query, {
            'today': fields.Date.context_today(self),
            'user_id': self.env.uid,
        })
        activity_data = self.env.cr.dictfetchall()
        model_ids = [a['id'] for a in activity_data]
        model_names = {
            n[0]: n[1]
            for n in self.env['ir.model'].browse(model_ids).name_get()
        }

        user_activities = {}
        for activity in activity_data:
            if not user_activities.get(activity['model']):
                user_activities[activity['model']] = {
                    'name':
                    model_names[activity['id']],
                    'model':
                    activity['model'],
                    'type':
                    'activity',
                    'icon':
                    modules.module.get_module_icon(
                        self.env[activity['model']]._original_module),
                    'total_count':
                    0,
                    'today_count':
                    0,
                    'overdue_count':
                    0,
                    'planned_count':
                    0,
                }
            user_activities[activity['model']][
                '%s_count' % activity['states']] += activity['count']
            if activity['states'] in ('today', 'overdue'):
                user_activities[
                    activity['model']]['total_count'] += activity['count']

        return list(user_activities.values())
예제 #9
0
class res_partner_has_image(models.Model):
    _inherit = "res.partner"

    @api.multi
    def _has_image(self):
        return dict((p.id, bool(p.image)) for p in self)

    has_image = fields.Boolean(compute='_has_image', string="Image Res")
    property_account_customer_advance = fields.Many2one(
        'account.account',
        string="Account Customer Advance",
        help="This account will be used for advance payment of custom")

    @api.multi
    def _get_re_reg_advance_account(self):
        re_reg_account_rec = self.env['account.account'].search([('code', '=',
                                                                  '210602')])
        for rec in self:
            if rec.is_student or rec.is_parent:
                if re_reg_account_rec.id:
                    rec.re_reg_advance_account = re_reg_account_rec.id
                else:
                    rec.re_reg_advance_account = False

    # @api.depends('advance_total_recivable')
    # def get_advance_total_recivable(self):
    # 	"""
    #    -----------------------------------------------------
    #    :return:
    #    """
    #
    # 	account_move_line_obj = self.env['account.move.line']
    # 	query = account_move_line_obj._query_get_get()
    # 	for record in self:
    # 		if record.property_account_customer_advance.id:
    # 			amount_difference = 0.00
    # 			# for account_move_line_rec in account_move_line_obj.search([('partner_id','=',record.id)]):
    # 			#     if account_move_line_rec.account_id.id == record.property_account_customer_advance.id:
    # 			#         amount_difference += account_move_line_rec.credit
    # 			#         amount_difference -= account_move_line_rec.debit
    # 			# record.re_reg_total_recivable = amount_difference
    # 			ctx = self._context.copy()
    # 			ctx['all_fiscalyear'] = True
    # 			query = self.env['account.move.line']._query_get_get()
    # 			self._cr.execute("""SELECT l.partner_id, SUM(l.debit),SUM(l.credit), SUM(l.debit-l.credit)
    #                              FROM account_move_line l
    #                              WHERE l.partner_id IN %s
    #                              AND l.account_id IN %s
    #                              AND l.reconcile_id IS NULL
    #                              AND """ + query + """
    #                              GROUP BY l.partner_id
    #                              """, (tuple(record.ids), tuple(record.property_account_customer_advance.ids),))
    # 			fetch = self._cr.fetchall()
    # 			for pid, total_debit, total_credit, val in fetch:
    # 				amount_difference += total_credit
    # 				amount_difference -= total_debit
    # 				self.advance_total_recivable = amount_difference

    @api.depends('re_reg_total_recivable')
    def get_re_registration_total_recivable(self):
        """
        -----------------------------------------------------
        :return:
        """
        account_move_line_obj = self.env['account.move.line']
        query = account_move_line_obj._query_get()
        for record in self:
            if record.re_reg_advance_account.id:
                amount_difference = 0.00
                # for account_move_line_rec in account_move_line_obj.search([('partner_id','=',record.id)]):
                #     if account_move_line_rec.account_id.id == record.property_account_customer_advance.id:
                #         amount_difference += account_move_line_rec.credit
                #         amount_difference -= account_move_line_rec.debit
                # record.re_reg_total_recivable = amount_difference
                ctx = self._context.copy()
                ctx['all_fiscalyear'] = True
                query = self.env['account.move.line']._query_get()
                self._cr.execute(
                    """SELECT l.partner_id, SUM(l.debit),SUM(l.credit), SUM(l.debit-l.credit)
	                             FROM account_move_line l
	                             WHERE l.partner_id IN %s
	                             AND l.account_id IN %s
	                             AND l.reconcile_id IS NULL
	                             AND """ + query + """
	                             GROUP BY l.partner_id
	                             """, (
                        tuple(record.ids),
                        tuple(record.re_reg_advance_account.ids),
                    ))
                fetch = self._cr.fetchall()
                for pid, total_debit, total_credit, val in fetch:
                    amount_difference += total_credit
                    amount_difference -= total_debit
                    self.re_reg_total_recivable = amount_difference

    @api.one
    @api.depends('tc_initiated')
    def _get_tc_initiad(self):
        obj_transfer_certificate = self.env['trensfer.certificate']
        for rec in self:
            if rec.is_student == True:
                if rec.id:
                    tc_rec = obj_transfer_certificate.search(
                        [('name', '=', rec.id)], limit=1)
                    if tc_rec.id:
                        if tc_rec.state in ('tc_requested',
                                            'fee_balance_review',
                                            'final_fee_awaited'):
                            rec.tc_initiated = 'yes'
                        if tc_rec.state in ('tc_complete', 'tc_cancel'):
                            rec.tc_initiated = 'no'
                    else:
                        rec.tc_initiated = 'no'

    student_state = fields.Selection(
        [('academic_fee_paid', 'Academic fee paid'),
         ('academic_fee_unpaid', 'Academic fee unpaid'),
         ('academic_fee_partially_paid', 'Academic fee partially paid'),
         ('tc_initiated', 'TC initiated'),
         ('confirmed_student', 'Confirmed Student'),
         ('ministry_approved_old', 'Ministry Approved')],
        string='Status')

    tc_initiated = fields.Selection([('yes', 'Yes'), ('no', 'No')],
                                    string='TC Initiated',
                                    compute='_get_tc_initiad')

    re_reg_next_academic_year = fields.Selection(
        [('yes', 'YES'), ('no', 'NO')],
        'Re-registered for next Academic year',
        default='no')
    re_reg_advance_account = fields.Many2one(
        'account.account',
        string="Account Re-Registration Advance",
        help=
        "This account will be used for Re-Registration fee advance payment of Student/Parent",
        compute=_get_re_reg_advance_account)
    re_reg_total_recivable = fields.Float(
        compute='get_re_registration_total_recivable',
        string='Re-Reg Advance Total Recivable')
    advance_total_recivable = fields.Float(
        compute='get_advance_total_recivable',
        string='Advance Total Recivable')
예제 #10
0
class Partner(models.Model):
    _inherit = 'res.partner'

    associate_member = fields.Many2one(
        'res.partner',
        string='Associate Member',
        help="A member with whom you want to associate your membership."
        "It will consider the membership state of the associated member.")
    member_lines = fields.One2many('membership.membership_line',
                                   'partner',
                                   string='Membership')
    free_member = fields.Boolean(
        string='Free Member',
        help="Select if you want to give free membership.")
    membership_amount = fields.Float(
        string='Membership Amount',
        digits=(16, 2),
        help='The price negotiated by the partner')
    membership_state = fields.Selection(
        membership.STATE,
        compute='_compute_membership_state',
        string='Current Membership Status',
        store=True,
        help='It indicates the membership state.\n'
        '-Non Member: A partner who has not applied for any membership.\n'
        '-Cancelled Member: A member who has cancelled his membership.\n'
        '-Old Member: A member whose membership date has expired.\n'
        '-Waiting Member: A member who has applied for the membership and whose invoice is going to be created.\n'
        '-Invoiced Member: A member whose invoice has been created.\n'
        '-Paying member: A member who has paid the membership fee.')
    membership_start = fields.Date(
        compute='_compute_membership_start',
        string='Membership Start Date',
        store=True,
        help="Date from which membership becomes active.")
    membership_stop = fields.Date(
        compute='_compute_membership_stop',
        string='Membership End Date',
        store=True,
        help="Date until which membership remains active.")
    membership_cancel = fields.Date(
        compute='_compute_membership_cancel',
        string='Cancel Membership Date',
        store=True,
        help="Date on which membership has been cancelled")

    @api.depends(
        'member_lines.account_invoice_line.invoice_id.state',
        'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
        'member_lines.account_invoice_line.invoice_id.payment_ids',
        'member_lines.account_invoice_line.invoice_id.payment_move_line_ids',
        'member_lines.account_invoice_line.invoice_id.partner_id',
        'free_member', 'member_lines.date_to', 'member_lines.date_from',
        'associate_member')
    def _compute_membership_state(self):
        values = self._membership_state()
        for partner in self:
            partner.membership_state = values[partner.id]

        # Do not depend directly on "associate_member.membership_state" or we might end up in an
        # infinite loop. Since we still need this dependency somehow, we explicitly search for the
        # "parent members" and trigger a recompute.
        parent_members = self.search([('associate_member', 'in', self.ids)
                                      ]) - self
        if parent_members:
            parent_members._recompute_todo(self._fields['membership_state'])

    @api.depends(
        'member_lines.account_invoice_line.invoice_id.state',
        'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
        'member_lines.account_invoice_line.invoice_id.payment_ids',
        'free_member', 'member_lines.date_to', 'member_lines.date_from',
        'member_lines.date_cancel', 'membership_state',
        'associate_member.membership_state')
    def _compute_membership_start(self):
        """Return  date of membership"""
        for partner in self:
            partner.membership_start = self.env[
                'membership.membership_line'].search(
                    [('partner', '=', partner.associate_member.id
                      or partner.id), ('date_cancel', '=', False)],
                    limit=1,
                    order='date_from').date_from

    @api.depends(
        'member_lines.account_invoice_line.invoice_id.state',
        'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
        'member_lines.account_invoice_line.invoice_id.payment_ids',
        'free_member', 'member_lines.date_to', 'member_lines.date_from',
        'member_lines.date_cancel', 'membership_state',
        'associate_member.membership_state')
    def _compute_membership_stop(self):
        MemberLine = self.env['membership.membership_line']
        for partner in self:
            partner.membership_stop = self.env[
                'membership.membership_line'].search(
                    [('partner', '=', partner.associate_member.id
                      or partner.id), ('date_cancel', '=', False)],
                    limit=1,
                    order='date_to desc').date_to

    @api.depends(
        'member_lines.account_invoice_line.invoice_id.state',
        'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
        'member_lines.account_invoice_line.invoice_id.payment_ids',
        'free_member', 'member_lines.date_to', 'member_lines.date_from',
        'member_lines.date_cancel', 'membership_state',
        'associate_member.membership_state')
    def _compute_membership_cancel(self):
        for partner in self:
            if partner.membership_state == 'canceled':
                partner.membership_cancel = self.env[
                    'membership.membership_line'].search(
                        [('partner', '=', partner.id)],
                        limit=1,
                        order='date_cancel').date_cancel
            else:
                partner.membership_cancel = False

    def _membership_state(self):
        """This Function return Membership State For Given Partner. """
        res = {}
        today = fields.Date.today()
        for partner in self:
            res[partner.id] = 'none'

            if partner.membership_cancel and today > partner.membership_cancel:
                res[partner.id] = 'free' if partner.free_member else 'canceled'
                continue
            if partner.membership_stop and today > partner.membership_stop:
                res[partner.id] = 'free' if partner.free_member else 'old'
                continue
            if partner.associate_member:
                res_state = partner.associate_member._membership_state()
                res[partner.id] = res_state[partner.associate_member.id]
                continue

            s = 4
            if partner.member_lines:
                for mline in partner.member_lines:
                    if (mline.date_to or date.min) >= today and (
                            mline.date_from or date.min) <= today:
                        if mline.account_invoice_line.invoice_id.partner_id == partner:
                            mstate = mline.account_invoice_line.invoice_id.state
                            if mstate == 'paid':
                                s = 0
                                inv = mline.account_invoice_line.invoice_id
                                for ml in inv.payment_move_line_ids:
                                    if any(
                                            ml.invoice_id.filtered(
                                                lambda inv: inv.type ==
                                                'out_refund')):
                                        s = 2
                                break
                            elif mstate == 'open' and s != 0:
                                s = 1
                            elif mstate == 'cancel' and s != 0 and s != 1:
                                s = 2
                            elif mstate == 'draft' and s != 0 and s != 1:
                                s = 3
                if s == 4:
                    for mline in partner.member_lines:
                        if (mline.date_from or date.min) < today and (
                                mline.date_to or date.min
                        ) < today and (mline.date_from or date.min) <= (
                                mline.date_to or date.min
                        ) and mline.account_invoice_line and mline.account_invoice_line.invoice_id.state == 'paid':
                            s = 5
                        else:
                            s = 6
                if s == 0:
                    res[partner.id] = 'paid'
                elif s == 1:
                    res[partner.id] = 'invoiced'
                elif s == 2:
                    res[partner.id] = 'canceled'
                elif s == 3:
                    res[partner.id] = 'waiting'
                elif s == 5:
                    res[partner.id] = 'old'
                elif s == 6:
                    res[partner.id] = 'none'
            if partner.free_member and s != 0:
                res[partner.id] = 'free'
        return res

    @api.one
    @api.constrains('associate_member')
    def _check_recursion_associate_member(self):
        level = 100
        while self:
            self = self.associate_member
            if not level:
                raise ValidationError(
                    _('You cannot create recursive associated members.'))
            level -= 1

    @api.model
    def _cron_update_membership(self):
        partners = self.search([('membership_state', 'in',
                                 ['invoiced', 'paid'])])
        # mark the field to be recomputed, and recompute it
        partners._recompute_todo(self._fields['membership_state'])
        self.recompute()

    @api.multi
    def create_membership_invoice(self, product_id=None, datas=None):
        """ Create Customer Invoice of Membership for partners.
        @param datas: datas has dictionary value which consist Id of Membership product and Cost Amount of Membership.
                      datas = {'membership_product_id': None, 'amount': None}
        """
        product_id = product_id or datas.get('membership_product_id')
        amount = datas.get('amount', 0.0)
        invoice_list = []
        for partner in self:
            addr = partner.address_get(['invoice'])
            if partner.free_member:
                raise UserError(_("Partner is a free Member."))
            if not addr.get('invoice', False):
                raise UserError(
                    _("Partner doesn't have an address to make the invoice."))
            invoice = self.env['account.invoice'].create({
                'partner_id':
                partner.id,
                'account_id':
                partner.property_account_receivable_id.id,
                'fiscal_position_id':
                partner.property_account_position_id.id
            })
            line_values = {
                'product_id': product_id,
                'price_unit': amount,
                'invoice_id': invoice.id,
            }
            # create a record in cache, apply onchange then revert back to a dictionnary
            invoice_line = self.env['account.invoice.line'].new(line_values)
            invoice_line._onchange_product_id()
            line_values = invoice_line._convert_to_write(
                {name: invoice_line[name]
                 for name in invoice_line._cache})
            line_values['price_unit'] = amount
            invoice.write({'invoice_line_ids': [(0, 0, line_values)]})
            invoice_list.append(invoice.id)
            invoice.compute_taxes()
        return invoice_list
예제 #11
0
class Company(models.Model):
    _inherit = "res.company"

    invoice_is_snailmail = fields.Boolean(string='Send by Post', default=False)
예제 #12
0
class StockWarehouse(models.Model):
    _inherit = 'stock.warehouse'

    manufacture_to_resupply = fields.Boolean(
        'Manufacture to Resupply',
        default=True,
        help=
        "When products are manufactured, they can be manufactured in this warehouse."
    )
    manufacture_pull_id = fields.Many2one('stock.rule', 'Manufacture Rule')
    manufacture_mto_pull_id = fields.Many2one('stock.rule',
                                              'Manufacture MTO Rule')
    pbm_mto_pull_id = fields.Many2one('stock.rule',
                                      'Picking Before Manufacturing MTO Rule')
    sam_rule_id = fields.Many2one('stock.rule',
                                  'Stock After Manufacturing Rule')
    manu_type_id = fields.Many2one(
        'stock.picking.type',
        'Manufacturing Operation Type',
        domain=
        "[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]",
        check_company=True)

    pbm_type_id = fields.Many2one(
        'stock.picking.type',
        'Picking Before Manufacturing Operation Type',
        check_company=True)
    sam_type_id = fields.Many2one('stock.picking.type',
                                  'Stock After Manufacturing Operation Type',
                                  check_company=True)

    manufacture_steps = fields.Selection(
        [('mrp_one_step', 'Manufacture (1 step)'),
         ('pbm', 'Pick components and then manufacture (2 steps)'),
         ('pbm_sam',
          'Pick components, manufacture and then store products (3 steps)')],
        'Manufacture',
        default='mrp_one_step',
        required=True,
        help="Produce : Move the components to the production location\
        directly and start the manufacturing process.\nPick / Produce : Unload\
        the components from the Stock to Input location first, and then\
        transfer it to the Production location.")

    pbm_route_id = fields.Many2one('stock.location.route',
                                   'Picking Before Manufacturing Route',
                                   ondelete='restrict')

    pbm_loc_id = fields.Many2one('stock.location',
                                 'Picking before Manufacturing Location',
                                 check_company=True)
    sam_loc_id = fields.Many2one('stock.location',
                                 'Stock after Manufacturing Location',
                                 check_company=True)

    def get_rules_dict(self):
        result = super(StockWarehouse, self).get_rules_dict()
        production_location_id = self._get_production_location()
        for warehouse in self:
            result[warehouse.id].update({
                'mrp_one_step': [],
                'pbm': [
                    self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id,
                                 warehouse.pbm_type_id, 'pull'),
                    self.Routing(warehouse.pbm_loc_id, production_location_id,
                                 warehouse.manu_type_id, 'pull'),
                ],
                'pbm_sam': [
                    self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id,
                                 warehouse.pbm_type_id, 'pull'),
                    self.Routing(warehouse.pbm_loc_id, production_location_id,
                                 warehouse.manu_type_id, 'pull'),
                    self.Routing(warehouse.sam_loc_id, warehouse.lot_stock_id,
                                 warehouse.sam_type_id, 'push'),
                ],
            })
        return result

    @api.model
    def _get_production_location(self):
        location = self.env['stock.location'].with_context(
            force_company=self.company_id.id).search(
                [('usage', '=', 'production'),
                 ('company_id', '=', self.company_id.id)],
                limit=1)
        if not location:
            raise UserError(_('Can\'t find any production location.'))
        return location

    def _get_routes_values(self):
        routes = super(StockWarehouse, self)._get_routes_values()
        routes.update({
            'pbm_route_id': {
                'routing_key': self.manufacture_steps,
                'depends': ['manufacture_steps', 'manufacture_to_resupply'],
                'route_update_values': {
                    'name':
                    self._format_routename(route_type=self.manufacture_steps),
                    'active':
                    self.manufacture_steps != 'mrp_one_step',
                },
                'route_create_values': {
                    'product_categ_selectable': True,
                    'warehouse_selectable': True,
                    'product_selectable': False,
                    'company_id': self.company_id.id,
                    'sequence': 10,
                },
                'rules_values': {
                    'active': True,
                }
            }
        })
        return routes

    def _get_route_name(self, route_type):
        names = {
            'mrp_one_step':
            _('Manufacture (1 step)'),
            'pbm':
            _('Pick components and then manufacture'),
            'pbm_sam':
            _('Pick components, manufacture and then store products (3 steps)'
              ),
        }
        if route_type in names:
            return names[route_type]
        else:
            return super(StockWarehouse, self)._get_route_name(route_type)

    def _get_global_route_rules_values(self):
        rules = super(StockWarehouse, self)._get_global_route_rules_values()
        location_src = self.manufacture_steps == 'mrp_one_step' and self.lot_stock_id or self.pbm_loc_id
        production_location = self._get_production_location()
        location_id = self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id
        rules.update({
            'manufacture_pull_id': {
                'depends': ['manufacture_steps', 'manufacture_to_resupply'],
                'create_values': {
                    'action':
                    'manufacture',
                    'procure_method':
                    'make_to_order',
                    'company_id':
                    self.company_id.id,
                    'picking_type_id':
                    self.manu_type_id.id,
                    'route_id':
                    self._find_global_route('mrp.route_warehouse0_manufacture',
                                            _('Manufacture')).id
                },
                'update_values': {
                    'active': self.manufacture_to_resupply,
                    'name': self._format_rulename(location_id, False,
                                                  'Production'),
                    'location_id': location_id.id,
                    'propagate_cancel': self.manufacture_steps == 'pbm_sam'
                },
            },
            'manufacture_mto_pull_id': {
                'depends': ['manufacture_steps', 'manufacture_to_resupply'],
                'create_values': {
                    'procure_method':
                    'make_to_order',
                    'company_id':
                    self.company_id.id,
                    'action':
                    'pull',
                    'auto':
                    'manual',
                    'route_id':
                    self._find_global_route('stock.route_warehouse0_mto',
                                            _('Make To Order')).id,
                    'location_id':
                    production_location.id,
                    'location_src_id':
                    location_src.id,
                    'picking_type_id':
                    self.manu_type_id.id
                },
                'update_values': {
                    'name':
                    self._format_rulename(location_src, production_location,
                                          'MTO'),
                    'active':
                    self.manufacture_to_resupply,
                },
            },
            'pbm_mto_pull_id': {
                'depends': ['manufacture_steps', 'manufacture_to_resupply'],
                'create_values': {
                    'procure_method':
                    'make_to_order',
                    'company_id':
                    self.company_id.id,
                    'action':
                    'pull',
                    'auto':
                    'manual',
                    'route_id':
                    self._find_global_route('stock.route_warehouse0_mto',
                                            _('Make To Order')).id,
                    'name':
                    self._format_rulename(self.lot_stock_id, self.pbm_loc_id,
                                          'MTO'),
                    'location_id':
                    self.pbm_loc_id.id,
                    'location_src_id':
                    self.lot_stock_id.id,
                    'picking_type_id':
                    self.pbm_type_id.id
                },
                'update_values': {
                    'active':
                    self.manufacture_steps != 'mrp_one_step'
                    and self.manufacture_to_resupply,
                }
            },
            # The purpose to move sam rule in the manufacture route instead of
            # pbm_route_id is to avoid conflict with receipt in multiple
            # step. For example if the product is manufacture and receipt in two
            # step it would conflict in WH/Stock since product could come from
            # WH/post-prod or WH/input. We do not have this conflict with
            # manufacture route since it is set on the product.
            'sam_rule_id': {
                'depends': ['manufacture_steps', 'manufacture_to_resupply'],
                'create_values': {
                    'procure_method':
                    'make_to_order',
                    'company_id':
                    self.company_id.id,
                    'action':
                    'pull',
                    'auto':
                    'manual',
                    'route_id':
                    self._find_global_route('mrp.route_warehouse0_manufacture',
                                            _('Manufacture')).id,
                    'name':
                    self._format_rulename(self.sam_loc_id, self.lot_stock_id,
                                          False),
                    'location_id':
                    self.lot_stock_id.id,
                    'location_src_id':
                    self.sam_loc_id.id,
                    'picking_type_id':
                    self.sam_type_id.id
                },
                'update_values': {
                    'active':
                    self.manufacture_steps == 'pbm_sam'
                    and self.manufacture_to_resupply,
                }
            }
        })
        return rules

    def _get_locations_values(self, vals, code=False):
        values = super(StockWarehouse, self)._get_locations_values(vals,
                                                                   code=code)
        def_values = self.default_get(['manufacture_steps'])
        manufacture_steps = vals.get('manufacture_steps',
                                     def_values['manufacture_steps'])
        code = vals.get('code') or code or ''
        code = code.replace(' ', '').upper()
        company_id = vals.get('company_id', self.company_id.id)
        values.update({
            'pbm_loc_id': {
                'name': _('Pre-Production'),
                'active': manufacture_steps in ('pbm', 'pbm_sam'),
                'usage': 'internal',
                'barcode': self._valid_barcode(code + '-PREPRODUCTION',
                                               company_id)
            },
            'sam_loc_id': {
                'name':
                _('Post-Production'),
                'active':
                manufacture_steps == 'pbm_sam',
                'usage':
                'internal',
                'barcode':
                self._valid_barcode(code + '-POSTPRODUCTION', company_id)
            },
        })
        return values

    def _get_sequence_values(self):
        values = super(StockWarehouse, self)._get_sequence_values()
        values.update({
            'pbm_type_id': {
                'name':
                self.name + ' ' + _('Sequence picking before manufacturing'),
                'prefix':
                self.code + '/PC/',
                'padding':
                5,
                'company_id':
                self.company_id.id
            },
            'sam_type_id': {
                'name':
                self.name + ' ' + _('Sequence stock after manufacturing'),
                'prefix': self.code + '/SFP/',
                'padding': 5,
                'company_id': self.company_id.id
            },
            'manu_type_id': {
                'name': self.name + ' ' + _('Sequence production'),
                'prefix': self.code + '/MO/',
                'padding': 5,
                'company_id': self.company_id.id
            },
        })
        return values

    def _get_picking_type_create_values(self, max_sequence):
        data, next_sequence = super(
            StockWarehouse, self)._get_picking_type_create_values(max_sequence)
        data.update({
            'pbm_type_id': {
                'name': _('Pick Components'),
                'code': 'internal',
                'use_create_lots': True,
                'use_existing_lots': True,
                'default_location_src_id': self.lot_stock_id.id,
                'default_location_dest_id': self.pbm_loc_id.id,
                'sequence': next_sequence + 1,
                'sequence_code': 'PC',
                'company_id': self.company_id.id,
            },
            'sam_type_id': {
                'name': _('Store Finished Product'),
                'code': 'internal',
                'use_create_lots': True,
                'use_existing_lots': True,
                'default_location_src_id': self.sam_loc_id.id,
                'default_location_dest_id': self.lot_stock_id.id,
                'sequence': next_sequence + 3,
                'sequence_code': 'SFP',
                'company_id': self.company_id.id,
            },
            'manu_type_id': {
                'name': _('Manufacturing'),
                'code': 'mrp_operation',
                'use_create_lots': True,
                'use_existing_lots': True,
                'sequence': next_sequence + 2,
                'sequence_code': 'MO',
                'company_id': self.company_id.id,
            },
        })
        return data, max_sequence + 4

    def _get_picking_type_update_values(self):
        data = super(StockWarehouse, self)._get_picking_type_update_values()
        data.update({
            'pbm_type_id': {
                'active':
                self.manufacture_to_resupply
                and self.manufacture_steps in ('pbm', 'pbm_sam')
            },
            'sam_type_id': {
                'active':
                self.manufacture_to_resupply
                and self.manufacture_steps == 'pbm_sam'
            },
            'manu_type_id': {
                'active':
                self.manufacture_to_resupply,
                'default_location_src_id':
                self.manufacture_steps in ('pbm', 'pbm_sam')
                and self.pbm_loc_id.id or self.lot_stock_id.id,
                'default_location_dest_id':
                self.manufacture_steps == 'pbm_sam' and self.sam_loc_id.id
                or self.lot_stock_id.id,
            },
        })
        return data

    def write(self, vals):
        if any(field in vals
               for field in ('manufacture_steps', 'manufacture_to_resupply')):
            for warehouse in self:
                warehouse._update_location_manufacture(
                    vals.get('manufacture_steps', warehouse.manufacture_steps))
        return super(StockWarehouse, self).write(vals)

    def _get_all_routes(self):
        routes = super(StockWarehouse, self)._get_all_routes()
        routes |= self.filtered(
            lambda self: self.manufacture_to_resupply and self.
            manufacture_pull_id and self.manufacture_pull_id.route_id).mapped(
                'manufacture_pull_id').mapped('route_id')
        return routes

    def _update_location_manufacture(self, new_manufacture_step):
        self.mapped('pbm_loc_id').write(
            {'active': new_manufacture_step != 'mrp_one_step'})
        self.mapped('sam_loc_id').write(
            {'active': new_manufacture_step == 'pbm_sam'})

    def _update_name_and_code(self, name=False, code=False):
        res = super(StockWarehouse, self)._update_name_and_code(name, code)
        # change the manufacture stock rule name
        for warehouse in self:
            if warehouse.manufacture_pull_id and name:
                warehouse.manufacture_pull_id.write({
                    'name':
                    warehouse.manufacture_pull_id.name.replace(
                        warehouse.name, name, 1)
                })
        return res
예제 #13
0
class MailResendMessage(models.TransientModel):
    _name = 'mail.resend.message'
    _description = 'Email resend wizard'

    mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True)
    partner_ids = fields.One2many('mail.resend.partner', 'resend_wizard_id', string='Recipients')
    notification_ids = fields.Many2many('mail.notification', string='Notifications', readonly=True)
    has_cancel = fields.Boolean(compute='_compute_has_cancel')
    partner_readonly = fields.Boolean(compute='_compute_partner_readonly')

    @api.depends("partner_ids")
    def _compute_has_cancel(self):
        self.has_cancel = self.partner_ids.filtered(lambda p: not p.resend)

    def _compute_partner_readonly(self):
        self.partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False)

    @api.model
    def default_get(self, fields):
        rec = super(MailResendMessage, self).default_get(fields)
        message_id = self._context.get('mail_message_to_resend')
        if message_id:
            mail_message_id = self.env['mail.message'].browse(message_id)
            notification_ids = mail_message_id.notification_ids.filtered(lambda notif: notif.notification_type == 'email' and notif.notification_status in ('exception', 'bounce'))
            partner_ids = [(0, 0, {
                "partner_id": notif.res_partner_id.id,
                "name": notif.res_partner_id.name,
                "email": notif.res_partner_id.email,
                "resend": True,
                "message": notif.format_failure_reason(),
            }) for notif in notification_ids]
            has_user = any([notif.res_partner_id.user_ids for notif in notification_ids])
            if has_user:
                partner_readonly = not self.env['res.users'].check_access_rights('write', raise_exception=False)
            else:
                partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False)
            rec['partner_readonly'] = partner_readonly
            rec['notification_ids'] = [(6, 0, notification_ids.ids)]
            rec['mail_message_id'] = mail_message_id.id
            rec['partner_ids'] = partner_ids
        else:
            raise UserError(_('No message_id found in context'))
        return rec

    def resend_mail_action(self):
        """ 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:
            "If a partner disappeared from partner list, we cancel the notification"
            to_cancel = wizard.partner_ids.filtered(lambda p: not p.resend).mapped("partner_id")
            to_send = wizard.partner_ids.filtered(lambda p: p.resend).mapped("partner_id")
            notif_to_cancel = wizard.notification_ids.filtered(lambda notif: notif.notification_type == 'email' and notif.res_partner_id in to_cancel and notif.notification_status in ('exception', 'bounce'))
            notif_to_cancel.sudo().write({'notification_status': 'canceled'})
            if to_send:
                message = wizard.mail_message_id
                record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread']

                email_partners_data = []
                for pid, cid, active, pshare, ctype, notif, groups in self.env['mail.followers']._get_recipient_data(None, 'comment', False, pids=to_send.ids):
                    if pid and notif == 'email' or not notif:
                        pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []}
                        if not pshare and notif:  # has an user and is not shared, is therefore user
                            email_partners_data.append(dict(pdata, type='user'))
                        elif pshare and notif:  # has an user and is shared, is therefore portal
                            email_partners_data.append(dict(pdata, type='portal'))
                        else:  # has no user, is therefore customer
                            email_partners_data.append(dict(pdata, type='customer'))

                record._notify_record_by_email(message, {'partners': email_partners_data}, check_existing=True, send_after_commit=False)

            self.mail_message_id._notify_mail_failure_update()
        return {'type': 'ir.actions.act_window_close'}

    def cancel_mail_action(self):
        for wizard in self:
            for notif in wizard.notification_ids:
                notif.filtered(lambda notif: notif.notification_type == 'email' and notif.notification_status in ('exception', 'bounce')).sudo().write({'notification_status': 'canceled'})
            wizard.mail_message_id._notify_mail_failure_update()
        return {'type': 'ir.actions.act_window_close'}
class EagleeduAssigningClass(models.Model):
    _name = 'eagleedu.assigning.class'
    _description = 'Assign the Students to Class'
    _inherit = ['mail.thread']
    # _rec_name = 'class_assign_name'
    name = fields.Char('Class Assign Register',
                       compute='get_class_assign_name')
    keep_roll_no = fields.Boolean("keep Roll No")
    class_id = fields.Many2one('eagleedu.class', string='Class')
    student_list = fields.One2many('eagleedu.student.list',
                                   'connect_id',
                                   string="Students")
    admitted_class = fields.Many2one('eagleedu.class.division',
                                     string="Admitted Class")
    assigned_by = fields.Many2one('res.users',
                                  string='Assigned By',
                                  default=lambda self: self.env.uid)
    state = fields.Selection([('draft', 'Draft'), ('done', 'Done')],
                             string='State',
                             required=True,
                             default='draft',
                             track_visibility='onchange')

    assign_date = fields.Datetime(string='Asigned Date',
                                  default=datetime.today())

    #assign_date = fields.Date(string='Assigned Date',default=fields.date.today())

    @api.model
    def get_class_assign_name(self):
        for rec in self:
            rec.name = str(rec.admitted_class.name) + '(Assign on ' + str(
                rec.assign_date) + ')'
            #rec.name = rec.admitted_class.name #+ '(assigned on '+ rec.assign_date +')'

    def assigning_class(self):
        max_roll = self.env['eagleedu.class.history'].search(
            [('class_id', '=', self.admitted_class.id)],
            order='roll_no desc',
            limit=1)
        if max_roll.roll_no:
            next_roll = max_roll.roll_no
        else:
            next_roll = 0

        for rec in self:

            if not self.student_list:
                raise ValidationError(_('No Student Lines'))
            com_sub = self.env['eagleedu.syllabus'].search([
                ('class_id', '=', rec.class_id.id),
                ('academic_year', '=', rec.admitted_class.academic_year_id.id),
                ('divisional', '=', False),
                ('selection_type', '=', 'compulsory')
            ])
            elect_sub = self.env['eagleedu.syllabus'].search([
                ('class_id', '=', rec.class_id.id),
                ('academic_year', '=', rec.admitted_class.academic_year_id.id),
                ('divisional', '=', True),
                ('division_id', '=', rec.admitted_class.id),
                ('selection_type', '=', 'compulsory')
            ])
            com_subjects = []  # compulsory Subject List
            el_subjects = []  # Elective Subject List
            for sub in com_sub:
                com_subjects.append(sub.id)
            for sub in elect_sub:
                el_subjects.append(sub.id)
            for line in self.student_list:
                st = self.env['eagleedu.student'].search([
                    ('id', '=', line.student_id.id)
                ])
                st.class_id = rec.admitted_class.id
                if self.keep_roll_no != True:
                    next_roll = next_roll + 1
                    line.roll_no = next_roll
                st.roll_no = line.roll_no

                # create student history

                self.env['eagleedu.class.history'].create({
                    'academic_year_id':
                    rec.admitted_class.academic_year_id.id,
                    'class_id':
                    rec.admitted_class.id,
                    'student_id':
                    line.student_id.id,
                    'roll_no':
                    line.roll_no,
                    'compulsory_subjects': [(6, 0, com_subjects)],
                    'selective_subjects': [(6, 0, el_subjects)]
                })

            self.write({'state': 'done'})

    def unlink(self):
        """Return warning if the Record is in done state"""
        for rec in self:
            if rec.state == 'done':
                raise ValidationError(_("Cannot delete Record in Done state"))

    def get_student_list(self):
        """returns the list of students applied to join the selected class"""
        for rec in self:
            for line in rec.student_list:
                line.unlink()
            # TODO apply filter not to get student assigned previously
            students = self.env['eagleedu.student'].search([
                ('class_id', '=', rec.admitted_class.id),
                ('assigned', '=', False)
            ])
            if not students:
                raise ValidationError(_('No Students Available.. !'))
            values = []
            for stud in students:
                stud_line = {
                    'class_id': rec.class_id.id,
                    'student_id': stud.id,
                    'connect_id': rec.id,
                    'roll_no': stud.application_id.roll_no
                }
                stud.assigned = True
                values.append(stud_line)
            for line in values:
                rec.student_line = self.env['eagleedu.student.list'].create(
                    line)
예제 #15
0
class PaymentAdviceReport(models.Model):
    _name = "payment.advice.report"
    _description = "Payment Advice Analysis"
    _auto = False

    name = fields.Char(readonly=True)
    date = fields.Date(readonly=True)
    year = fields.Char(readonly=True)
    month = fields.Selection([('01', 'January'), ('02', 'February'),
                              ('03', 'March'), ('04', 'April'), ('05', 'May'),
                              ('06', 'June'), ('07', 'July'), ('08', 'August'),
                              ('09', 'September'), ('10', 'October'),
                              ('11', 'November'), ('12', 'December')],
                             readonly=True)
    day = fields.Char(readonly=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('confirm', 'Confirmed'),
        ('cancel', 'Cancelled'),
    ],
                             string='Status',
                             index=True,
                             readonly=True)
    employee_id = fields.Many2one('hr.employee',
                                  string='Employee',
                                  readonly=True)
    nbr = fields.Integer(string='# Payment Lines', readonly=True)
    number = fields.Char(readonly=True)
    bysal = fields.Float(string='By Salary', readonly=True)
    bank_id = fields.Many2one('res.bank', string='Bank', readonly=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True)
    cheque_nos = fields.Char(string='Cheque Numbers', readonly=True)
    neft = fields.Boolean(string='NEFT Transaction', readonly=True)
    ifsc_code = fields.Char(string='IFSC Code', readonly=True)
    employee_bank_no = fields.Char(string='Employee Bank Account',
                                   required=True)

    @api.model_cr
    def init(self):
        drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute("""
            create or replace view payment_advice_report as (
                select
                    min(l.id) as id,
                    sum(l.bysal) as bysal,
                    p.name,
                    p.state,
                    p.date,
                    p.number,
                    p.company_id,
                    p.bank_id,
                    p.chaque_nos as cheque_nos,
                    p.neft,
                    l.employee_id,
                    l.ifsc_code,
                    l.name as employee_bank_no,
                    to_char(p.date, 'YYYY') as year,
                    to_char(p.date, 'MM') as month,
                    to_char(p.date, 'YYYY-MM-DD') as day,
                    1 as nbr
                from
                    hr_payroll_advice as p
                    left join hr_payroll_advice_line as l on (p.id=l.advice_id)
                where
                    l.employee_id IS NOT NULL
                group by
                    p.number,p.name,p.date,p.state,p.company_id,p.bank_id,p.chaque_nos,p.neft,
                    l.employee_id,l.advice_id,l.bysal,l.ifsc_code, l.name
            )
        """)
예제 #16
0
class PosSession(models.Model):
    _name = 'pos.session'
    _order = 'id desc'
    _description = 'Point of Sale Session'

    POS_SESSION_STATE = [
        ('opening_control',
         'Opening Control'),  # method action_pos_session_open
        ('opened', 'In Progress'),  # method action_pos_session_closing_control
        ('closing_control',
         'Closing Control'),  # method action_pos_session_close
        ('closed', 'Closed & Posted'),
    ]

    def _confirm_orders(self):
        for session in self:
            company_id = session.config_id.journal_id.company_id.id
            orders = session.order_ids.filtered(
                lambda order: order.state == 'paid')
            journal_id = self.env['ir.config_parameter'].sudo().get_param(
                'pos.closing.journal_id_%s' % company_id,
                default=session.config_id.journal_id.id)
            if not journal_id:
                raise UserError(
                    _("You have to set a Sale Journal for the POS:%s") %
                    (session.config_id.name, ))

            move = self.env['pos.order'].with_context(
                force_company=company_id)._create_account_move(
                    session.start_at, session.name, int(journal_id),
                    company_id)
            orders.with_context(
                force_company=company_id)._create_account_move_line(
                    session, move)
            for order in session.order_ids.filtered(
                    lambda o: o.state not in ['done', 'invoiced']):
                if order.state not in ('paid'):
                    raise UserError(
                        _("You cannot confirm all orders of this session, because they have not the 'paid' status.\n"
                          "{reference} is in state {state}, total amount: {total}, paid: {paid}"
                          ).format(
                              reference=order.pos_reference or order.name,
                              state=order.state,
                              total=order.amount_total,
                              paid=order.amount_paid,
                          ))
                order.action_pos_order_done()
            orders_to_reconcile = session.order_ids._filtered_for_reconciliation(
            )
            orders_to_reconcile.sudo()._reconcile_payments()

    config_id = fields.Many2one(
        'pos.config',
        string='Point of Sale',
        help="The physical point of sale you will use.",
        required=True,
        index=True)
    name = fields.Char(string='Session ID',
                       required=True,
                       readonly=True,
                       default='/')
    user_id = fields.Many2one(
        'res.users',
        string='Responsible',
        required=True,
        index=True,
        readonly=True,
        states={'opening_control': [('readonly', False)]},
        default=lambda self: self.env.uid)
    currency_id = fields.Many2one('res.currency',
                                  related='config_id.currency_id',
                                  string="Currency",
                                  readonly=False)
    start_at = fields.Datetime(string='Opening Date', readonly=True)
    stop_at = fields.Datetime(string='Closing Date', readonly=True, copy=False)

    state = fields.Selection(POS_SESSION_STATE,
                             string='Status',
                             required=True,
                             readonly=True,
                             index=True,
                             copy=False,
                             default='opening_control')

    sequence_number = fields.Integer(
        string='Order Sequence Number',
        help='A sequence number that is incremented with each order',
        default=1)
    login_number = fields.Integer(
        string='Login Sequence Number',
        help=
        'A sequence number that is incremented each time a user resumes the pos session',
        default=0)

    cash_control = fields.Boolean(compute='_compute_cash_all',
                                  string='Has Cash Control')
    cash_journal_id = fields.Many2one('account.journal',
                                      compute='_compute_cash_all',
                                      string='Cash Journal',
                                      store=True)
    cash_register_id = fields.Many2one('account.bank.statement',
                                       compute='_compute_cash_all',
                                       string='Cash Register',
                                       store=True)

    cash_register_balance_end_real = fields.Monetary(
        related='cash_register_id.balance_end_real',
        string="Ending Balance",
        help="Total of closing cash control lines.",
        readonly=True)
    cash_register_balance_start = fields.Monetary(
        related='cash_register_id.balance_start',
        string="Starting Balance",
        help="Total of opening cash control lines.",
        readonly=True)
    cash_register_total_entry_encoding = fields.Monetary(
        related='cash_register_id.total_entry_encoding',
        string='Total Cash Transaction',
        readonly=True,
        help="Total of all paid sales orders")
    cash_register_balance_end = fields.Monetary(
        related='cash_register_id.balance_end',
        digits=0,
        string="Theoretical Closing Balance",
        help="Sum of opening balance and transactions.",
        readonly=True)
    cash_register_difference = fields.Monetary(
        related='cash_register_id.difference',
        string='Difference',
        help=
        "Difference between the theoretical closing balance and the real closing balance.",
        readonly=True)

    journal_ids = fields.Many2many('account.journal',
                                   related='config_id.journal_ids',
                                   readonly=True,
                                   string='Available Payment Methods')
    order_ids = fields.One2many('pos.order', 'session_id', string='Orders')
    statement_ids = fields.One2many('account.bank.statement',
                                    'pos_session_id',
                                    string='Bank Statement',
                                    readonly=True)
    picking_count = fields.Integer(compute='_compute_picking_count')
    rescue = fields.Boolean(
        string='Recovery Session',
        help="Auto-generated session for orphan orders, ignored in constraints",
        readonly=True,
        copy=False)

    _sql_constraints = [('uniq_name', 'unique(name)',
                         "The name of this POS Session must be unique !")]

    @api.multi
    def _compute_picking_count(self):
        for pos in self:
            pickings = pos.order_ids.mapped('picking_id').filtered(
                lambda x: x.state != 'done')
            pos.picking_count = len(pickings.ids)

    @api.multi
    def action_stock_picking(self):
        pickings = self.order_ids.mapped('picking_id').filtered(
            lambda x: x.state != 'done')
        action_picking = self.env.ref('stock.action_picking_tree_ready')
        action = action_picking.read()[0]
        action['context'] = {}
        action['domain'] = [('id', 'in', pickings.ids)]
        return action

    @api.depends('config_id', 'statement_ids')
    def _compute_cash_all(self):
        for session in self:
            session.cash_journal_id = session.cash_register_id = session.cash_control = False
            if session.config_id.cash_control:
                for statement in session.statement_ids:
                    if statement.journal_id.type == 'cash':
                        session.cash_control = True
                        session.cash_journal_id = statement.journal_id.id
                        session.cash_register_id = statement.id
                if not session.cash_control and session.state != 'closed':
                    raise UserError(
                        _("Cash control can only be applied to cash journals.")
                    )

    @api.constrains('user_id', 'state')
    def _check_unicity(self):
        # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user
        if self.search_count([('state', 'not in',
                               ('closed', 'closing_control')),
                              ('user_id', '=', self.user_id.id),
                              ('rescue', '=', False)]) > 1:
            raise ValidationError(
                _("You cannot create two active sessions with the same responsible."
                  ))

    @api.constrains('config_id')
    def _check_pos_config(self):
        if self.search_count([('state', '!=', 'closed'),
                              ('config_id', '=', self.config_id.id),
                              ('rescue', '=', False)]) > 1:
            raise ValidationError(
                _("Another session is already opened for this point of sale."))

    @api.model
    def create(self, values):
        config_id = values.get('config_id') or self.env.context.get(
            'default_config_id')
        if not config_id:
            raise UserError(
                _("You should assign a Point of Sale to your session."))

        # journal_id is not required on the pos_config because it does not
        # exists at the installation. If nothing is configured at the
        # installation we do the minimal configuration. Impossible to do in
        # the .xml files as the CoA is not yet installed.
        pos_config = self.env['pos.config'].browse(config_id)
        ctx = dict(self.env.context, company_id=pos_config.company_id.id)
        if not pos_config.journal_id:
            default_journals = pos_config.with_context(ctx).default_get(
                ['journal_id', 'invoice_journal_id'])
            if (not default_journals.get('journal_id')
                    or not default_journals.get('invoice_journal_id')):
                raise UserError(
                    _("Unable to open the session. You have to assign a sales journal to your point of sale."
                      ))
            pos_config.with_context(ctx).sudo().write({
                'journal_id':
                default_journals['journal_id'],
                'invoice_journal_id':
                default_journals['invoice_journal_id']
            })
        # define some cash journal if no payment method exists
        if not pos_config.journal_ids:
            Journal = self.env['account.journal']
            journals = Journal.with_context(ctx).search([
                ('journal_user', '=', True), ('type', '=', 'cash')
            ])
            if not journals:
                journals = Journal.with_context(ctx).search([('type', '=',
                                                              'cash')])
                if not journals:
                    journals = Journal.with_context(ctx).search([
                        ('journal_user', '=', True)
                    ])
            if not journals:
                raise ValidationError(
                    _("No payment method configured! \nEither no Chart of Account is installed or no payment method is configured for this POS."
                      ))
            journals.sudo().write({'journal_user': True})
            pos_config.sudo().write({'journal_ids': [(6, 0, journals.ids)]})

        pos_name = self.env['ir.sequence'].with_context(ctx).next_by_code(
            'pos.session')
        if values.get('name'):
            pos_name += ' ' + values['name']

        statements = []
        ABS = self.env['account.bank.statement']
        uid = SUPERUSER_ID if self.env.user.has_group(
            'point_of_sale.group_pos_user') else self.env.user.id
        for journal in pos_config.journal_ids:
            # set the journal_id which should be used by
            # account.bank.statement to set the opening balance of the
            # newly created bank statement
            ctx['journal_id'] = journal.id if pos_config.cash_control and journal.type == 'cash' else False
            st_values = {
                'journal_id':
                journal.id,
                'user_id':
                self.env.user.id,
                'name':
                pos_name,
                'balance_start':
                self.env["account.bank.statement"]._get_opening_balance(
                    journal.id) if journal.type == 'cash' else 0
            }

            statements.append(
                ABS.with_context(ctx).sudo(uid).create(st_values).id)

        values.update({
            'name': pos_name,
            'statement_ids': [(6, 0, statements)],
            'config_id': config_id
        })

        res = super(PosSession,
                    self.with_context(ctx).sudo(uid)).create(values)
        if not pos_config.cash_control:
            res.action_pos_session_open()

        return res

    @api.multi
    def unlink(self):
        for session in self.filtered(lambda s: s.statement_ids):
            session.statement_ids.unlink()
        return super(PosSession, self).unlink()

    @api.multi
    def login(self):
        self.ensure_one()
        self.write({
            'login_number': self.login_number + 1,
        })

    @api.multi
    def action_pos_session_open(self):
        # second browse because we need to refetch the data from the DB for cash_register_id
        # we only open sessions that haven't already been opened
        for session in self.filtered(
                lambda session: session.state == 'opening_control'):
            values = {}
            if not session.start_at:
                values['start_at'] = fields.Datetime.now()
            values['state'] = 'opened'
            session.write(values)
            session.statement_ids.button_open()
        return True

    @api.multi
    def action_pos_session_closing_control(self):
        self._check_pos_session_balance()
        for session in self:
            session.write({
                'state': 'closing_control',
                'stop_at': fields.Datetime.now()
            })
            if not session.config_id.cash_control:
                session.action_pos_session_close()

    @api.multi
    def _check_pos_session_balance(self):
        for session in self:
            for statement in session.statement_ids:
                if (statement != session.cash_register_id) and (
                        statement.balance_end != statement.balance_end_real):
                    statement.write(
                        {'balance_end_real': statement.balance_end})

    @api.multi
    def action_pos_session_validate(self):
        self._check_pos_session_balance()
        self.action_pos_session_close()

    @api.multi
    def action_pos_session_close(self):
        # Close CashBox
        for session in self:
            company_id = session.config_id.company_id.id
            ctx = dict(self.env.context,
                       force_company=company_id,
                       company_id=company_id)
            ctx_notrack = dict(ctx, mail_notrack=True)
            for st in session.statement_ids:
                if abs(st.difference) > st.journal_id.amount_authorized_diff:
                    # The pos manager can close statements with maximums.
                    if not self.user_has_groups(
                            "point_of_sale.group_pos_manager"):
                        raise UserError(
                            _("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it."
                              ) % (st.difference,
                                   st.journal_id.amount_authorized_diff))
                if (st.journal_id.type not in ['bank', 'cash']):
                    raise UserError(
                        _("The journal type for your payment method should be bank or cash."
                          ))
                st.with_context(ctx_notrack).sudo().button_confirm_bank()
        self.with_context(ctx)._confirm_orders()
        self.write({'state': 'closed'})
        return {
            'type': 'ir.actions.client',
            'name': 'Point of Sale Menu',
            'tag': 'reload',
            'params': {
                'menu_id': self.env.ref('point_of_sale.menu_point_root').id
            },
        }

    @api.multi
    def open_frontend_cb(self):
        if not self.ids:
            return {}
        for session in self.filtered(lambda s: s.user_id.id != self.env.uid):
            raise UserError(
                _("You cannot use the session of another user. This session is owned by %s. "
                  "Please first close this one to use this point of sale.") %
                session.user_id.name)
        return {
            'type': 'ir.actions.act_url',
            'target': 'self',
            'url': '/pos/web/',
        }

    @api.multi
    def open_cashbox(self):
        self.ensure_one()
        context = dict(self._context)
        balance_type = context.get('balance') or 'start'
        context['bank_statement_id'] = self.cash_register_id.id
        context['balance'] = balance_type
        context['default_pos_id'] = self.config_id.id

        action = {
            'name': _('Cash Control'),
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'account.bank.statement.cashbox',
            'view_id':
            self.env.ref('account.view_account_bnk_stmt_cashbox').id,
            'type': 'ir.actions.act_window',
            'context': context,
            'target': 'new'
        }

        cashbox_id = None
        if balance_type == 'start':
            cashbox_id = self.cash_register_id.cashbox_start_id.id
        else:
            cashbox_id = self.cash_register_id.cashbox_end_id.id
        if cashbox_id:
            action['res_id'] = cashbox_id

        return action
예제 #17
0
class ResConfigSettings(models.TransientModel):
    _inherit = 'res.config.settings'

    app_system_name = fields.Char(
        'System Name', help=u"Setup System Name,which replace Eagle")
    app_show_lang = fields.Boolean(
        'Show Quick Language Switcher',
        help=u"When enable,User can quick switch language in user menu")
    app_show_debug = fields.Boolean(
        'Show Quick Debug',
        help=u"When enable,everyone login can see the debug menu")
    app_show_documentation = fields.Boolean(
        'Show Documentation', help=u"When enable,User can visit user manual")
    app_show_documentation_dev = fields.Boolean(
        'Show Developer Documentation',
        help=u"When enable,User can visit development documentation")
    app_show_support = fields.Boolean(
        'Show Support', help=u"When enable,User can vist your support site")
    app_show_account = fields.Boolean(
        'Show My Account', help=u"When enable,User can login to your website")
    app_show_enterprise = fields.Boolean(
        'Show Enterprise Tag', help=u"Uncheck to hide the Enterprise tag")
    app_show_share = fields.Boolean(
        'Show Share Dashboard',
        help=u"Uncheck to hide the Eagle Share Dashboard")
    app_show_poweredby = fields.Boolean(
        'Show Powered by Eagle', help=u"Uncheck to hide the Powered by text")
    app_stop_subscribe = fields.Boolean(
        'Stop Eagle Subscribe(Performance Improve)',
        help=u"Check to stop Eagle Subscribe function")
    group_show_author_in_apps = fields.Boolean(
        string="Show Author in Apps Dashboard",
        implied_group='app_eagle_customize.group_show_author_in_apps',
        help=u"Uncheck to Hide Author and Website in Apps Dashboard")

    app_documentation_url = fields.Char('Documentation Url')
    app_documentation_dev_url = fields.Char('Developer Documentation Url')
    app_support_url = fields.Char('Support Url')
    app_account_title = fields.Char('My Eagle.com Account Title')
    app_account_url = fields.Char('My Eagle.com Account Url')
    app_enterprise_url = fields.Char('Customize Module Url(eg. Enterprise)')

    @api.model
    def get_values(self):
        res = super(ResConfigSettings, self).get_values()
        ir_config = self.env['ir.config_parameter'].sudo()
        app_system_name = ir_config.get_param('app_system_name',
                                              default='eagleApp')

        app_show_lang = True if ir_config.get_param(
            'app_show_lang') == "True" else False
        app_show_debug = True if ir_config.get_param(
            'app_show_debug') == "True" else False
        app_show_documentation = True if ir_config.get_param(
            'app_show_documentation') == "True" else False
        app_show_documentation_dev = True if ir_config.get_param(
            'app_show_documentation_dev') == "True" else False
        app_show_support = True if ir_config.get_param(
            'app_show_support') == "True" else False
        app_show_account = True if ir_config.get_param(
            'app_show_account') == "True" else False
        app_show_enterprise = True if ir_config.get_param(
            'app_show_enterprise') == "True" else False
        app_show_share = True if ir_config.get_param(
            'app_show_share') == "True" else False
        app_show_poweredby = True if ir_config.get_param(
            'app_show_poweredby') == "True" else False
        app_stop_subscribe = True if ir_config.get_param(
            'app_stop_subscribe') == "True" else False

        app_documentation_url = ir_config.get_param(
            'app_documentation_url',
            default=
            'https://www.sunpop.cn/documentation/user/12.0/en/index.html')
        app_documentation_dev_url = ir_config.get_param(
            'app_documentation_dev_url',
            default='https://www.sunpop.cn/documentation/12.0/index.html')
        app_support_url = ir_config.get_param(
            'app_support_url', default='https://www.sunpop.cn/trial/')
        app_account_title = ir_config.get_param('app_account_title',
                                                default='My Online Account')
        app_account_url = ir_config.get_param(
            'app_account_url', default='https://www.sunpop.cn/my-account/')
        app_enterprise_url = ir_config.get_param(
            'app_enterprise_url', default='https://www.sunpop.cn')
        res.update(app_system_name=app_system_name,
                   app_show_lang=app_show_lang,
                   app_show_debug=app_show_debug,
                   app_show_documentation=app_show_documentation,
                   app_show_documentation_dev=app_show_documentation_dev,
                   app_show_support=app_show_support,
                   app_show_account=app_show_account,
                   app_show_enterprise=app_show_enterprise,
                   app_show_share=app_show_share,
                   app_show_poweredby=app_show_poweredby,
                   app_stop_subscribe=app_stop_subscribe,
                   app_documentation_url=app_documentation_url,
                   app_documentation_dev_url=app_documentation_dev_url,
                   app_support_url=app_support_url,
                   app_account_title=app_account_title,
                   app_account_url=app_account_url,
                   app_enterprise_url=app_enterprise_url)
        return res

    @api.multi
    def set_values(self):
        super(ResConfigSettings, self).set_values()
        ir_config = self.env['ir.config_parameter'].sudo()
        ir_config.set_param("app_system_name", self.app_system_name or "")
        ir_config.set_param("app_show_lang", self.app_show_lang or "False")
        ir_config.set_param("app_show_debug", self.app_show_debug or "False")
        ir_config.set_param("app_show_documentation",
                            self.app_show_documentation or "False")
        ir_config.set_param("app_show_documentation_dev",
                            self.app_show_documentation_dev or "False")
        ir_config.set_param("app_show_support", self.app_show_support
                            or "False")
        ir_config.set_param("app_show_account", self.app_show_account
                            or "False")
        ir_config.set_param("app_show_enterprise", self.app_show_enterprise
                            or "False")
        ir_config.set_param("app_show_share", self.app_show_share or "False")
        ir_config.set_param("app_show_poweredby", self.app_show_poweredby
                            or "False")
        ir_config.set_param("app_stop_subscribe", self.app_stop_subscribe
                            or "False")

        ir_config.set_param(
            "app_documentation_url", self.app_documentation_url
            or "https://www.sunpop.cn/documentation/user/12.0/en/index.html")
        ir_config.set_param(
            "app_documentation_dev_url", self.app_documentation_dev_url
            or "https://www.sunpop.cn/documentation/12.0/index.html")
        ir_config.set_param(
            "app_support_url", self.app_support_url
            or "https://www.sunpop.cn/trial/")
        ir_config.set_param("app_account_title", self.app_account_title
                            or "My Online Account")
        ir_config.set_param(
            "app_account_url", self.app_account_url
            or "https://www.sunpop.cn/my-account/")
        ir_config.set_param("app_enterprise_url", self.app_enterprise_url
                            or "https://www.sunpop.cn")

    def set_module_url(self):
        sql = "UPDATE ir_module_module SET website = '%s' WHERE license like '%s' and website <> ''" % (
            self.app_enterprise_url, 'OEEL%')
        try:
            self._cr.execute(sql)
        except Exception as e:
            pass

    def remove_sales(self):
        to_removes = [
            # 清除销售单据
            [
                'sale.order.line',
            ],
            [
                'sale.order',
            ],
            # 销售提成,自用
            [
                'sale.commission.line',
            ],
            # 不能删除报价单模板
            # ['sale.order.template.option', ],
            # ['sale.order.template.line', ],
            # ['sale.order.template', ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号
            seqs = self.env['ir.sequence'].search([
                '|', ('code', '=', 'sale.order'),
                ('code', '=', 'sale.commission.line')
            ])
            for seq in seqs:
                seq.write({
                    'number_next': 1,
                })
        except Exception as e:
            raise Warning(e)
        return True

    def remove_product(self):
        to_removes = [
            # 清除产品数据
            [
                'product.product',
            ],
            [
                'product.template',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号,针对自动产品编号
            seqs = self.env['ir.sequence'].search([('code', '=',
                                                    'product.product')])
            for seq in seqs:
                seq.write({
                    'number_next': 1,
                })
        except Exception as e:
            pass  # raise Warning(e)
        return True

    def remove_product_attribute(self):
        to_removes = [
            # 清除产品属性
            [
                'product.attribute.value',
            ],
            [
                'product.attribute',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_pos(self):
        to_removes = [
            # 清除POS单据
            [
                'pos.order.line',
            ],
            [
                'pos.order',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号
            seqs = self.env['ir.sequence'].search([('code', '=', 'pos.order')])
            for seq in seqs:
                seq.write({
                    'number_next': 1,
                })
            # 更新要关帐的值,因为 store=true 的计算字段要重置
            statement = self.env['account.bank.statement'].search([])
            for s in statement:
                s._end_balance()

        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_purchase(self):
        to_removes = [
            # 清除采购单据
            [
                'purchase.order.line',
            ],
            [
                'purchase.order',
            ],
            [
                'purchase.requisition.line',
            ],
            [
                'purchase.requisition',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号
            seqs = self.env['ir.sequence'].search([
                '|', ('code', '=', 'purchase.order'), '|',
                ('code', '=', 'purchase.requisition.purchase.tender'),
                ('code', '=', 'purchase.requisition.blanket.order')
            ])
            for seq in seqs:
                seq.write({
                    'number_next': 1,
                })
            self._cr.execute(sql)
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_expense(self):
        to_removes = [
            # 清除采购单据
            [
                'hr.expense.sheet',
            ],
            [
                'hr.expense',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号
            seqs = self.env['ir.sequence'].search([('code', '=',
                                                    'hr.expense.invoice')])
            for seq in seqs:
                seq.write({
                    'number_next': 1,
                })
            self._cr.execute(sql)
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_expense(self):
        to_removes = [
            # 清除
            [
                'hr.expense.sheet',
            ],
            [
                'hr.expense',
            ],
            [
                'hr.payslip',
            ],
            [
                'hr.payslip.run',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号
            seqs = self.env['ir.sequence'].search([('code', '=',
                                                    'hr.expense.invoice')])
            for seq in seqs:
                seq.write({
                    'number_next': 1,
                })
            self._cr.execute(sql)
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_mrp(self):
        to_removes = [
            # 清除生产单据
            [
                'mrp.workcenter.productivity',
            ],
            [
                'mrp.workorder',
            ],
            [
                'mrp.production.workcenter.line',
            ],
            [
                'change.production.qty',
            ],
            [
                'mrp.production',
            ],
            [
                'mrp.production.product.line',
            ],
            [
                'mrp.unbuild',
            ],
            [
                'change.production.qty',
            ],
            [
                'sale.forecast.indirect',
            ],
            [
                'sale.forecast',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号
            seqs = self.env['ir.sequence'].search([
                '|',
                ('code', '=', 'mrp.production'),
                ('code', '=', 'mrp.unbuild'),
            ])
            for seq in seqs:
                seq.write({
                    'number_next': 1,
                })
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_mrp_bom(self):
        to_removes = [
            # 清除生产BOM
            [
                'mrp.bom.line',
            ],
            [
                'mrp.bom',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_inventory(self):
        to_removes = [
            # 清除库存单据
            [
                'stock.quant',
            ],
            [
                'stock.move.line',
            ],
            [
                'stock.package.level',
            ],
            [
                'stock.quantity.history',
            ],
            [
                'stock.quant.package',
            ],
            [
                'stock.move',
            ],
            [
                'stock.pack.operation',
            ],
            [
                'stock.picking',
            ],
            [
                'stock.scrap',
            ],
            [
                'stock.picking.batch',
            ],
            [
                'stock.inventory.line',
            ],
            [
                'stock.inventory',
            ],
            [
                'stock.production.lot',
            ],
            [
                'stock.fixed.putaway.strat',
            ],
            [
                'procurement.group',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号
            seqs = self.env['ir.sequence'].search([
                '|', ('code', '=', 'stock.lot.serial'), '|',
                ('code', '=', 'stock.lot.tracking'), '|',
                ('code', '=', 'stock.orderpoint'), '|',
                ('code', '=', 'stock.picking'), '|',
                ('code', '=', 'picking.batch'), '|',
                ('code', '=', 'stock.quant.package'), '|',
                ('code', '=', 'stock.scrap'), '|',
                ('code', '=', 'stock.picking'), '|', ('prefix', '=', 'WH/IN/'),
                '|', ('prefix', '=', 'WH/INT/'), '|',
                ('prefix', '=', 'WH/OUT/'), '|', ('prefix', '=', 'WH/PACK/'),
                ('prefix', '=', 'WH/PICK/')
            ])
            for seq in seqs:
                seq.write({
                    'number_next': 1,
                })
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_account(self):
        to_removes = [
            # 清除财务会计单据
            [
                'account.voucher.line',
            ],
            [
                'account.voucher',
            ],
            [
                'account.bank.statement.line',
            ],
            [
                'account.payment',
            ],
            [
                'account.analytic.line',
            ],
            [
                'account.analytic.account',
            ],
            [
                'account.invoice.line',
            ],
            [
                'account.invoice.refund',
            ],
            [
                'account.invoice',
            ],
            [
                'account.partial.reconcile',
            ],
            [
                'account.move.line',
            ],
            [
                'hr.expense.sheet',
            ],
            [
                'account.move',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)

                    # 更新序号
                    seqs = self.env['ir.sequence'].search([
                        '|', ('code', '=', 'account.reconcile'), '|',
                        ('code', '=', 'account.payment.customer.invoice'), '|',
                        ('code', '=', 'account.payment.customer.refund'), '|',
                        ('code', '=', 'account.payment.supplier.invoice'), '|',
                        ('code', '=', 'account.payment.supplier.refund'), '|',
                        ('code', '=', 'account.payment.transfer'), '|',
                        ('prefix', 'like', 'BNK1/'), '|',
                        ('prefix', 'like', 'CSH1/'), '|',
                        ('prefix', 'like', 'INV/'), '|',
                        ('prefix', 'like', 'EXCH/'), '|',
                        ('prefix', 'like', 'MISC/'), '|',
                        ('prefix', 'like', '账单/'), ('prefix', 'like', '杂项/')
                    ])
                    for seq in seqs:
                        seq.write({
                            'number_next': 1,
                        })
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_account_chart(self):
        to_removes = [
            # 清除财务科目,用于重设
            [
                'res.partner.bank',
            ],
            [
                'res.bank',
            ],
            ['account.move.line'],
            ['account.invoice'],
            ['account.payment'],
            [
                'account.bank.statement',
            ],
            [
                'account.tax.account.tag',
            ],
            [
                'account.tax',
            ],
            [
                'account.tax',
            ],
            [
                'account.account.account.tag',
            ],
            ['wizard_multi_charts_accounts'],
            [
                'account.account',
            ],
            [
                'account.journal',
            ],
        ]
        # todo: 要做 remove_hr,因为工资表会用到 account
        # 更新account关联,很多是多公司字段,故只存在 ir_property,故在原模型,只能用update
        try:
            # reset default tax,不管多公司
            field1 = self.env['ir.model.fields']._get('product.template',
                                                      "taxes_id").id
            field2 = self.env['ir.model.fields']._get('product.template',
                                                      "supplier_taxes_id").id

            sql = (
                "delete from ir_default where field_id = %s or field_id = %s"
            ) % (field1, field2)
            self._cr.execute(sql)
        except Exception as e:
            pass  # raise Warning(e)
        try:
            rec = self.env['res.partner'].search([])
            for r in rec:
                r.write({
                    'property_account_receivable_id': None,
                    'property_account_payable_id': None,
                })
        except Exception as e:
            pass  # raise Warning(e)
        try:
            rec = self.env['product.category'].search([])
            for r in rec:
                r.write({
                    'property_account_income_categ_id': None,
                    'property_account_expense_categ_id': None,
                    'property_account_creditor_price_difference_categ': None,
                    'property_stock_account_input_categ_id': None,
                    'property_stock_account_output_categ_id': None,
                    'property_stock_valuation_account_id': None,
                })
        except Exception as e:
            pass  # raise Warning(e)
        try:
            rec = self.env['stock.location'].search([])
            for r in rec:
                r.write({
                    'valuation_in_account_id': None,
                    'valuation_out_account_id': None,
                })
        except Exception as e:
            pass  # raise Warning(e)

        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)

            sql = "update res_company set chart_template_id=null;"
            self._cr.execute(sql)
            # 更新序号
        except Exception as e:
            pass

        return True

    @api.multi
    def remove_project(self):
        to_removes = [
            # 清除项目
            [
                'account.analytic.line',
            ],
            [
                'project.task',
            ],
            [
                'project.forecast',
            ],
            [
                'project.project',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
            # 更新序号
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_website(self):
        to_removes = [
            # 清除网站数据,w, w_blog
            [
                'blog.tag.category',
            ],
            [
                'blog.tag',
            ],
            [
                'blog.post',
            ],
            [
                'blog.blog',
            ],
            [
                'website.published.multi.mixin',
            ],
            [
                'website.published.mixin',
            ],
            [
                'website.multi.mixin',
            ],
            [
                'website.redirect',
            ],
            [
                'website.seo.metadata',
            ],
            [
                'website.page',
            ],
            [
                'website.menu',
            ],
            [
                'website',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj and obj._table:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_message(self):
        to_removes = [
            # 清除消息数据
            [
                'mail.message',
            ],
            [
                'mail.followers',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj and obj._table:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)
        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_workflow(self):
        to_removes = [
            # 清除工作流
            [
                'wkf.workitem',
            ],
            [
                'wkf.instance',
            ],
        ]
        try:
            for line in to_removes:
                obj_name = line[0]
                obj = self.pool.get(obj_name)
                if obj and obj._table:
                    sql = "delete from %s" % obj._table
                    self._cr.execute(sql)

        except Exception as e:
            pass  # raise Warning(e)
        return True

    @api.multi
    def remove_all_biz(self):
        try:
            self.remove_account()
            self.remove_inventory()
            self.remove_mrp()
            self.remove_purchase()
            self.remove_sales()
            self.remove_project()
            self.remove_pos()
            self.remove_expense()
            self.remove_message()
        except Exception as e:
            pass  # raise Warning(e)
        return True
예제 #18
0
class FeatureProductSlider(models.Model):
    _name = 'feature.product.slider.config'
    _description = 'Featured Products Slider'

    name = fields.Char(string="Name",
                       default='My Products Slider',
                       required=True,
                       translate=True,
                       help="""Slider name will not be visible in website it 
                       is only for unique identification while dragging the snippet in website."""
                       )
    active = fields.Boolean(string="Active", default=True)
    feature_name = fields.Char(string="Featured Products Slider Label",
                               default='Featured Products',
                               required=True,
                               translate=True,
                               help="""Slider title to be displayed on website 
                               like Featured Products, Latest and etc...""")
    feature_products_collections = fields.Many2many(
        'product.template',
        'theme_eagleshop12_feature_pro_colle_slider_rel',
        'slider_id',
        'prod_id',
        required=True,
        string="Featured Products Collections")

    on_sale_name = fields.Char(string="On Sale Slider Label",
                               default='On Sale',
                               required=True,
                               translate=True,
                               help="""Slider title to be displayed on
                                website like On Sale, Latest and etc...""")
    on_sale_collections = fields.Many2many(
        'product.template',
        'theme_eagleshop12_on_sale_name_collections_slider_rel',
        'slider_id',
        'prod_id',
        required=True,
        string="Sale Products Collections")

    random_name = fields.Char(string="Random Products Slider Label",
                              default='Random Products',
                              required=True,
                              translate=True,
                              help="""Slider title to be displayed on website 
                              like Random Products, Latest and etc...""")
    random_products_collections = fields.Many2many(
        'product.template',
        'theme_eagleshop12_random_product_coll_slider_rel',
        'slider_id',
        'prod_id',
        required=True,
        string="Random Products Collections")

    low_price_name = fields.Char(
        string="Low Price Slider Label",
        default='Low Price',
        required=True,
        translate=True,
        help="""Slider title to be displayed on website 
                                 like Low Price, Latest and etc...""")
    low_price_collections = fields.Many2many(
        'product.template',
        'theme_eagleshop12_low_price_products_collec_slider_rel',
        'slider_id',
        'prod_id',
        required=True,
        string="Low Products Collections")
예제 #19
0
class SMSResend(models.TransientModel):
    _name = 'sms.resend'
    _description = 'SMS Resend'
    _rec_name = 'mail_message_id'

    @api.model
    def default_get(self, fields):
        result = super(SMSResend, self).default_get(fields)
        if result.get('mail_message_id'):
            mail_message_id = self.env['mail.message'].browse(
                result['mail_message_id'])
            result['recipient_ids'] = [
                (0, 0, {
                    'notification_id':
                    notif.id,
                    'resend':
                    True,
                    'failure_type':
                    notif.failure_type,
                    'partner_name':
                    notif.res_partner_id.display_name
                    or mail_message_id.record_name,
                    'sms_number':
                    notif.sms_number,
                }) for notif in mail_message_id.notification_ids
                if notif.notification_type == 'sms'
                and notif.notification_status in ('exception', 'bounce')
            ]
        return result

    mail_message_id = fields.Many2one('mail.message',
                                      'Message',
                                      readonly=True,
                                      required=True)
    recipient_ids = fields.One2many('sms.resend.recipient',
                                    'sms_resend_id',
                                    string='Recipients')
    has_cancel = fields.Boolean(compute='_compute_has_cancel')
    has_insufficient_credit = fields.Boolean(
        compute='_compute_has_insufficient_credit')

    @api.depends("recipient_ids.failure_type")
    def _compute_has_insufficient_credit(self):
        self.has_insufficient_credit = self.recipient_ids.filtered(
            lambda p: p.failure_type == 'sms_credit')

    @api.depends("recipient_ids.resend")
    def _compute_has_cancel(self):
        self.has_cancel = self.recipient_ids.filtered(lambda p: not p.resend)

    def _check_access(self):
        if not self.mail_message_id or not self.mail_message_id.model or not self.mail_message_id.res_id:
            raise exceptions.UserError(
                _('You do not have access to the message and/or related document.'
                  ))
        record = self.env[self.mail_message_id.model].browse(
            self.mail_message_id.res_id)
        record.check_access_rights('read')
        record.check_access_rule('read')

    def action_resend(self):
        self._check_access()

        all_notifications = self.env['mail.notification'].sudo().search([
            ('mail_message_id', '=', self.mail_message_id.id),
            ('notification_type', '=', 'sms'),
            ('notification_status', 'in', ('exception', 'bounce'))
        ])
        sudo_self = self.sudo()
        to_cancel_ids = [
            r.notification_id.id for r in sudo_self.recipient_ids
            if not r.resend
        ]
        to_resend_ids = [
            r.notification_id.id for r in sudo_self.recipient_ids if r.resend
        ]

        if to_cancel_ids:
            all_notifications.filtered(lambda n: n.id in to_cancel_ids).write(
                {'notification_status': 'canceled'})

        if to_resend_ids:
            record = self.env[self.mail_message_id.model].browse(
                self.mail_message_id.res_id)

            sms_pid_to_number = dict((r.partner_id.id, r.sms_number)
                                     for r in self.recipient_ids
                                     if r.resend and r.partner_id)
            pids = list(sms_pid_to_number.keys())
            numbers = [
                r.sms_number for r in self.recipient_ids
                if r.resend and not r.partner_id
            ]

            rdata = []
            for pid, cid, active, pshare, ctype, notif, groups in self.env[
                    'mail.followers']._get_recipient_data(record,
                                                          'sms',
                                                          False,
                                                          pids=pids):
                if pid and notif == 'sms':
                    rdata.append({
                        'id': pid,
                        'share': pshare,
                        'active': active,
                        'notif': notif,
                        'groups': groups or [],
                        'type': 'customer' if pshare else 'user'
                    })
            if rdata or numbers:
                record._notify_record_by_sms(
                    self.mail_message_id, {'partners': rdata},
                    check_existing=True,
                    sms_numbers=numbers,
                    sms_pid_to_number=sms_pid_to_number,
                    put_in_queue=False)

        self.mail_message_id._notify_sms_update()
        return {'type': 'ir.actions.act_window_close'}

    def action_cancel(self):
        self._check_access()

        sudo_self = self.sudo()
        sudo_self.mapped('recipient_ids.notification_id').write(
            {'notification_status': 'canceled'})
        self.mail_message_id._notify_sms_update()
        return {'type': 'ir.actions.act_window_close'}

    def action_buy_credits(self):
        url = self.env['iap.account'].get_credits_url(service_name='sms')
        return {
            'type': 'ir.actions.act_url',
            'url': url,
        }
예제 #20
0
class MultiSlider(models.Model):
    _name = 'multi.slider.config'
    _description = 'Product Multi Slider'

    name = fields.Char(string="Slider name",
                       default='Trending',
                       required=True,
                       translate=True,
                       help="""Slider title to be displayed on website 
                       like Best products, Latest and etc...""")
    active = fields.Boolean(string="Active", default=True)

    auto_rotate = fields.Boolean(string='Auto Rotate Slider', default=True)
    sliding_speed = fields.Integer(string="Slider sliding speed",
                                   default='5000',
                                   help='''Sliding speed of a slider can be set
                                    from here and it will be in milliseconds.'''
                                   )

    no_of_collection = fields.Selection(
        [('2', '2'), ('3', '3'), ('4', '4'), ('5', '5')],
        string="No. of collections to show",
        default='2',
        required=True,
        help="No of collections to be displayed on slider.")

    label_collection_1 = fields.Char(
        string="1st collection name",
        default='First collection',
        required=True,
        help="""Collection label to be displayed in
                                      website like Featured, Trending, Best Sellers, etc...""",
        translate=True)
    collection_1_ids = fields.Many2many('product.template',
                                        'product_slider_collection_1_rel',
                                        'slider_id',
                                        'prod_id',
                                        required=True,
                                        string="1st product collection")
    special_offer_1_product_tmpl_id = fields.Many2one(
        'product.template',
        required=True,
        string='''Special Offer product 
                                                      for 1st collection''')

    label_collection_2 = fields.Char(string="2nd collection name",
                                     default='Second collection',
                                     required=True,
                                     translate=True,
                                     help="""Collection label to be 
                                     displayed in website like Featured, Trending, Best Sellers, etc..."""
                                     )
    collection_2_ids = fields.Many2many('product.template',
                                        'product_slider_collection_2_rel',
                                        'slider_id',
                                        'prod_id',
                                        required=True,
                                        string="2nd product collection")
    special_offer_2_product_tmpl_id = fields.Many2one('product.template',
                                                      required=True,
                                                      string='''Special Offer
                                                       product for 2nd collection'''
                                                      )

    label_collection_3 = fields.Char(string="3rd collection name",
                                     default='Third collection',
                                     translate=True,
                                     help="""Collection label to be 
                                     displayed in website like Featured, Trending, Best Sellers, etc..."""
                                     )
    collection_3_ids = fields.Many2many('product.template',
                                        'product_slider_collection_3_rel',
                                        'slider_id',
                                        'prod_id',
                                        string="3rd product collection")
    special_offer_3_product_tmpl_id = fields.Many2one(
        'product.template', string='Special Offer product for 3rd collection')

    label_collection_4 = fields.Char(
        string="4th collection name",
        default='Fourth collection',
        translate=True,
        help="""Collection label to be displayed in 
                                     website like Featured, Trending, Best Sellers, etc..."""
    )
    collection_4_ids = fields.Many2many('product.template',
                                        'product_slider_collection_4_rel',
                                        'slider_id',
                                        'prod_id',
                                        string="4th product collection")
    special_offer_4_product_tmpl_id = fields.Many2one(
        'product.template', string='Special Offer product for 4th collection')

    label_collection_5 = fields.Char(string="5th collection name",
                                     default='Fifth collection',
                                     translate=True,
                                     help="""Collection label to be
                                      displayed in website like Featured,
                                       Trending, Best Sellers, etc...""")
    collection_5_ids = fields.Many2many('product.template',
                                        'product_slider_collection_5_rel',
                                        'slider_id',
                                        'prod_id',
                                        string="5th product collection")
    special_offer_5_product_tmpl_id = fields.Many2one(
        'product.template', string='Special Offer product for 5th collection')
예제 #21
0
class Challenge(models.Model):
    """Gamification challenge

    Set of predifined objectives assigned to people with rules for recurrence and
    rewards

    If 'user_ids' is defined and 'period' is different than 'one', the set will
    be assigned to the users for each period (eg: every 1st of each month if
    'monthly' is selected)
    """

    _name = 'gamification.challenge'
    _description = 'Gamification Challenge'
    _inherit = 'mail.thread'
    _order = 'end_date, start_date, name, id'

    name = fields.Char("Challenge Name", required=True, translate=True)
    description = fields.Text("Description", translate=True)
    state = fields.Selection([
            ('draft', "Draft"),
            ('inprogress', "In Progress"),
            ('done', "Done"),
        ], default='draft', copy=False,
        string="State", required=True, tracking=True)
    manager_id = fields.Many2one(
        'res.users', default=lambda self: self.env.uid,
        string="Responsible", help="The user responsible for the challenge.",)

    user_ids = fields.Many2many('res.users', 'gamification_challenge_users_rel', string="Users", help="List of users participating to the challenge")
    user_domain = fields.Char("User domain", help="Alternative to a list of users")

    period = fields.Selection([
            ('once', "Non recurring"),
            ('daily', "Daily"),
            ('weekly', "Weekly"),
            ('monthly', "Monthly"),
            ('yearly', "Yearly")
        ], default='once',
        string="Periodicity",
        help="Period of automatic goal assigment. If none is selected, should be launched manually.",
        required=True)
    start_date = fields.Date("Start Date", help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date.")
    end_date = fields.Date("End Date", help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date.")

    invited_user_ids = fields.Many2many('res.users', 'gamification_invited_user_ids_rel', string="Suggest to users")

    line_ids = fields.One2many('gamification.challenge.line', 'challenge_id',
                                  string="Lines",
                                  help="List of goals that will be set",
                                  required=True, copy=True)

    reward_id = fields.Many2one('gamification.badge', string="For Every Succeeding User")
    reward_first_id = fields.Many2one('gamification.badge', string="For 1st user")
    reward_second_id = fields.Many2one('gamification.badge', string="For 2nd user")
    reward_third_id = fields.Many2one('gamification.badge', string="For 3rd user")
    reward_failure = fields.Boolean("Reward Bests if not Succeeded?")
    reward_realtime = fields.Boolean("Reward as soon as every goal is reached", default=True, help="With this option enabled, a user can receive a badge only once. The top 3 badges are still rewarded only at the end of the challenge.")

    visibility_mode = fields.Selection([
            ('personal', "Individual Goals"),
            ('ranking', "Leader Board (Group Ranking)"),
        ], default='personal',
        string="Display Mode", required=True)

    report_message_frequency = fields.Selection([
            ('never', "Never"),
            ('onchange', "On change"),
            ('daily', "Daily"),
            ('weekly', "Weekly"),
            ('monthly', "Monthly"),
            ('yearly', "Yearly")
        ], default='never',
        string="Report Frequency", required=True)
    report_message_group_id = fields.Many2one('mail.channel', string="Send a copy to", help="Group that will receive a copy of the report in addition to the user")
    report_template_id = fields.Many2one('mail.template', default=lambda self: self._get_report_template(), string="Report Template", required=True)
    remind_update_delay = fields.Integer("Non-updated manual goals will be reminded after", help="Never reminded if no value or zero is specified.")
    last_report_date = fields.Date("Last Report Date", default=fields.Date.today)
    next_report_date = fields.Date("Next Report Date", compute='_get_next_report_date', store=True)

    category = fields.Selection([
        ('hr', 'Human Resources / Engagement'),
        ('other', 'Settings / Gamification Tools'),
    ], string="Appears in", required=True, default='hr',
       help="Define the visibility of the challenge through menus")

    REPORT_OFFSETS = {
        'daily': timedelta(days=1),
        'weekly': timedelta(days=7),
        'monthly': relativedelta(months=1),
        'yearly': relativedelta(years=1),
    }
    @api.depends('last_report_date', 'report_message_frequency')
    def _get_next_report_date(self):
        """ Return the next report date based on the last report date and
        report period.
        """
        for challenge in self:
            last = challenge.last_report_date
            offset = self.REPORT_OFFSETS.get(challenge.report_message_frequency)

            if offset:
                challenge.next_report_date = last + offset
            else:
                challenge.next_report_date = False

    def _get_report_template(self):
        template = self.env.ref('gamification.simple_report_template', raise_if_not_found=False)

        return template.id if template else False

    @api.model
    def create(self, vals):
        """Overwrite the create method to add the user of groups"""

        if vals.get('user_domain'):
            users = self._get_challenger_users(ustr(vals.get('user_domain')))

            if not vals.get('user_ids'):
                vals['user_ids'] = []
            vals['user_ids'].extend((4, user.id) for user in users)

        return super(Challenge, self).create(vals)

    def write(self, vals):
        if vals.get('user_domain'):
            users = self._get_challenger_users(ustr(vals.get('user_domain')))

            if not vals.get('user_ids'):
                vals['user_ids'] = []
            vals['user_ids'].extend((4, user.id) for user in users)

        write_res = super(Challenge, self).write(vals)

        if vals.get('report_message_frequency', 'never') != 'never':
            # _recompute_challenge_users do not set users for challenges with no reports, subscribing them now
            for challenge in self:
                challenge.message_subscribe([user.partner_id.id for user in challenge.user_ids])

        if vals.get('state') == 'inprogress':
            self._recompute_challenge_users()
            self._generate_goals_from_challenge()

        elif vals.get('state') == 'done':
            self._check_challenge_reward(force=True)

        elif vals.get('state') == 'draft':
            # resetting progress
            if self.env['gamification.goal'].search([('challenge_id', 'in', self.ids), ('state', '=', 'inprogress')], limit=1):
                raise exceptions.UserError(_("You can not reset a challenge with unfinished goals."))

        return write_res


    ##### Update #####

    @api.model # FIXME: check how cron functions are called to see if decorator necessary
    def _cron_update(self, ids=False, commit=True):
        """Daily cron check.

        - Start planned challenges (in draft and with start_date = today)
        - Create the missing goals (eg: modified the challenge to add lines)
        - Update every running challenge
        """
        # start scheduled challenges
        planned_challenges = self.search([
            ('state', '=', 'draft'),
            ('start_date', '<=', fields.Date.today())
        ])
        if planned_challenges:
            planned_challenges.write({'state': 'inprogress'})

        # close scheduled challenges
        scheduled_challenges = self.search([
            ('state', '=', 'inprogress'),
            ('end_date', '<', fields.Date.today())
        ])
        if scheduled_challenges:
            scheduled_challenges.write({'state': 'done'})

        records = self.browse(ids) if ids else self.search([('state', '=', 'inprogress')])

        # in cron mode, will do intermediate commits
        # FIXME: replace by parameter
        return records.with_context(commit_gamification=commit)._update_all()

    def _update_all(self):
        """Update the challenges and related goals

        :param list(int) ids: the ids of the challenges to update, if False will
        update only challenges in progress."""
        if not self:
            return True

        Goals = self.env['gamification.goal']

        # include yesterday goals to update the goals that just ended
        # exclude goals for users that did not connect since the last update
        yesterday = fields.Date.to_string(date.today() - timedelta(days=1))
        self.env.cr.execute("""SELECT gg.id
                        FROM gamification_goal as gg,
                             gamification_challenge as gc,
                             res_users as ru,
                             res_users_log as log
                       WHERE gg.challenge_id = gc.id
                         AND gg.user_id = ru.id
                         AND ru.id = log.create_uid
                         AND gg.write_date < log.create_date
                         AND gg.closed IS false
                         AND gc.id IN %s
                         AND (gg.state = 'inprogress'
                              OR (gg.state = 'reached'
                                  AND (gg.end_date >= %s OR gg.end_date IS NULL)))
                      GROUP BY gg.id
        """, [tuple(self.ids), yesterday])

        Goals.browse(goal_id for [goal_id] in self.env.cr.fetchall()).update_goal()

        self._recompute_challenge_users()
        self._generate_goals_from_challenge()

        for challenge in self:
            if challenge.last_report_date != fields.Date.today():
                # goals closed but still opened at the last report date
                closed_goals_to_report = Goals.search([
                    ('challenge_id', '=', challenge.id),
                    ('start_date', '>=', challenge.last_report_date),
                    ('end_date', '<=', challenge.last_report_date)
                ])

                if challenge.next_report_date and fields.Date.today() >= challenge.next_report_date:
                    challenge.report_progress()
                elif closed_goals_to_report:
                    # some goals need a final report
                    challenge.report_progress(subset_goals=closed_goals_to_report)

        self._check_challenge_reward()
        return True

    def _get_challenger_users(self, domain):
        # FIXME: literal_eval?
        user_domain = safe_eval(domain)
        return self.env['res.users'].search(user_domain)

    def _recompute_challenge_users(self):
        """Recompute the domain to add new users and remove the one no longer matching the domain"""
        for challenge in self.filtered(lambda c: c.user_domain):
            current_users = challenge.user_ids
            new_users = self._get_challenger_users(challenge.user_domain)

            if current_users != new_users:
                challenge.user_ids = new_users

        return True

    def action_start(self):
        """Start a challenge"""
        return self.write({'state': 'inprogress'})

    def action_check(self):
        """Check a challenge

        Create goals that haven't been created yet (eg: if added users)
        Recompute the current value for each goal related"""
        self.env['gamification.goal'].search([
            ('challenge_id', 'in', self.ids),
            ('state', '=', 'inprogress')
        ]).unlink()

        return self._update_all()

    def action_report_progress(self):
        """Manual report of a goal, does not influence automatic report frequency"""
        for challenge in self:
            challenge.report_progress()
        return True

    ##### Automatic actions #####

    def _generate_goals_from_challenge(self):
        """Generate the goals for each line and user.

        If goals already exist for this line and user, the line is skipped. This
        can be called after each change in the list of users or lines.
        :param list(int) ids: the list of challenge concerned"""

        Goals = self.env['gamification.goal']
        for challenge in self:
            (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
            to_update = Goals.browse(())

            for line in challenge.line_ids:
                # there is potentially a lot of users
                # detect the ones with no goal linked to this line
                date_clause = ""
                query_params = [line.id]
                if start_date:
                    date_clause += " AND g.start_date = %s"
                    query_params.append(start_date)
                if end_date:
                    date_clause += " AND g.end_date = %s"
                    query_params.append(end_date)

                query = """SELECT u.id AS user_id
                             FROM res_users u
                        LEFT JOIN gamification_goal g
                               ON (u.id = g.user_id)
                            WHERE line_id = %s
                              {date_clause}
                        """.format(date_clause=date_clause)
                self.env.cr.execute(query, query_params)
                user_with_goal_ids = {it for [it] in self.env.cr._obj}

                participant_user_ids = set(challenge.user_ids.ids)
                user_squating_challenge_ids = user_with_goal_ids - participant_user_ids
                if user_squating_challenge_ids:
                    # users that used to match the challenge 
                    Goals.search([
                        ('challenge_id', '=', challenge.id),
                        ('user_id', 'in', list(user_squating_challenge_ids))
                    ]).unlink()

                values = {
                    'definition_id': line.definition_id.id,
                    'line_id': line.id,
                    'target_goal': line.target_goal,
                    'state': 'inprogress',
                }

                if start_date:
                    values['start_date'] = start_date
                if end_date:
                    values['end_date'] = end_date

                # the goal is initialised over the limit to make sure we will compute it at least once
                if line.condition == 'higher':
                    values['current'] = min(line.target_goal - 1, 0)
                else:
                    values['current'] = max(line.target_goal + 1, 0)

                if challenge.remind_update_delay:
                    values['remind_update_delay'] = challenge.remind_update_delay

                for user_id in (participant_user_ids - user_with_goal_ids):
                    values['user_id'] = user_id
                    to_update |= Goals.create(values)

            to_update.update_goal()

        return True

    ##### JS utilities #####

    def _get_serialized_challenge_lines(self, user=(), restrict_goals=(), restrict_top=0):
        """Return a serialised version of the goals information if the user has not completed every goal

        :param user: user retrieving progress (False if no distinction,
                     only for ranking challenges)
        :param restrict_goals: compute only the results for this subset of
                               gamification.goal ids, if False retrieve every
                               goal of current running challenge
        :param int restrict_top: for challenge lines where visibility_mode is
                                 ``ranking``, retrieve only the best
                                 ``restrict_top`` results and itself, if 0
                                 retrieve all restrict_goal_ids has priority
                                 over restrict_top

        format list
        # if visibility_mode == 'ranking'
        {
            'name': <gamification.goal.description name>,
            'description': <gamification.goal.description description>,
            'condition': <reach condition {lower,higher}>,
            'computation_mode': <target computation {manually,count,sum,python}>,
            'monetary': <{True,False}>,
            'suffix': <value suffix>,
            'action': <{True,False}>,
            'display_mode': <{progress,boolean}>,
            'target': <challenge line target>,
            'own_goal_id': <gamification.goal id where user_id == uid>,
            'goals': [
                {
                    'id': <gamification.goal id>,
                    'rank': <user ranking>,
                    'user_id': <res.users id>,
                    'name': <res.users name>,
                    'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,
                    'completeness': <percentage>,
                    'current': <current value>,
                }
            ]
        },
        # if visibility_mode == 'personal'
        {
            'id': <gamification.goal id>,
            'name': <gamification.goal.description name>,
            'description': <gamification.goal.description description>,
            'condition': <reach condition {lower,higher}>,
            'computation_mode': <target computation {manually,count,sum,python}>,
            'monetary': <{True,False}>,
            'suffix': <value suffix>,
            'action': <{True,False}>,
            'display_mode': <{progress,boolean}>,
            'target': <challenge line target>,
            'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>,                                
            'completeness': <percentage>,
            'current': <current value>,
        }
        """
        Goals = self.env['gamification.goal']
        (start_date, end_date) = start_end_date_for_period(self.period)

        res_lines = []
        for line in self.line_ids:
            line_data = {
                'name': line.definition_id.name,
                'description': line.definition_id.description,
                'condition': line.definition_id.condition,
                'computation_mode': line.definition_id.computation_mode,
                'monetary': line.definition_id.monetary,
                'suffix': line.definition_id.suffix,
                'action': True if line.definition_id.action_id else False,
                'display_mode': line.definition_id.display_mode,
                'target': line.target_goal,
            }
            domain = [
                ('line_id', '=', line.id),
                ('state', '!=', 'draft'),
            ]
            if restrict_goals:
                domain.append(('ids', 'in', restrict_goals.ids))
            else:
                # if no subset goals, use the dates for restriction
                if start_date:
                    domain.append(('start_date', '=', start_date))
                if end_date:
                    domain.append(('end_date', '=', end_date))

            if self.visibility_mode == 'personal':
                if not user:
                    raise exceptions.UserError(_("Retrieving progress for personal challenge without user information"))

                domain.append(('user_id', '=', user.id))

                goal = Goals.search(domain, limit=1)
                if not goal:
                    continue

                if goal.state != 'reached':
                    return []
                line_data.update(goal.read(['id', 'current', 'completeness', 'state'])[0])
                res_lines.append(line_data)
                continue

            line_data['own_goal_id'] = False,
            line_data['goals'] = []
            if line.condition=='higher':
                goals = Goals.search(domain, order="completeness desc, current desc")
            else:
                goals = Goals.search(domain, order="completeness desc, current asc")
            if not goals:
                continue

            for ranking, goal in enumerate(goals):
                if user and goal.user_id == user:
                    line_data['own_goal_id'] = goal.id
                elif restrict_top and ranking > restrict_top:
                    # not own goal and too low to be in top
                    continue

                line_data['goals'].append({
                    'id': goal.id,
                    'user_id': goal.user_id.id,
                    'name': goal.user_id.name,
                    'rank': ranking,
                    'current': goal.current,
                    'completeness': goal.completeness,
                    'state': goal.state,
                })
            if len(goals) < 3:
                # display at least the top 3 in the results
                missing = 3 - len(goals)
                for ranking, mock_goal in enumerate([{'id': False,
                                                      'user_id': False,
                                                      'name': '',
                                                      'current': 0,
                                                      'completeness': 0,
                                                      'state': False}] * missing,
                                                    start=len(goals)):
                    mock_goal['rank'] = ranking
                    line_data['goals'].append(mock_goal)

            res_lines.append(line_data)
        return res_lines

    ##### Reporting #####

    def report_progress(self, users=(), subset_goals=False):
        """Post report about the progress of the goals

        :param users: users that are concerned by the report. If False, will
                      send the report to every user concerned (goal users and
                      group that receive a copy). Only used for challenge with
                      a visibility mode set to 'personal'.
        :param subset_goals: goals to restrict the report
        """

        challenge = self

        MailTemplates = self.env['mail.template']
        if challenge.visibility_mode == 'ranking':
            lines_boards = challenge._get_serialized_challenge_lines(restrict_goals=subset_goals)

            body_html = MailTemplates.with_context(challenge_lines=lines_boards)._render_template(challenge.report_template_id.body_html, 'gamification.challenge', challenge.id)

            # send to every follower and participant of the challenge
            challenge.message_post(
                body=body_html,
                partner_ids=challenge.mapped('user_ids.partner_id.id'),
                subtype='mail.mt_comment',
                email_layout_xmlid='mail.mail_notification_light',
                )
            if challenge.report_message_group_id:
                challenge.report_message_group_id.message_post(
                    body=body_html,
                    subtype='mail.mt_comment')

        else:
            # generate individual reports
            for user in (users or challenge.user_ids):
                lines = challenge._get_serialized_challenge_lines(user, restrict_goals=subset_goals)
                if not lines:
                    continue

                body_html = MailTemplates.with_user(user).with_context(challenge_lines=lines)._render_template(
                    challenge.report_template_id.body_html,
                    'gamification.challenge',
                    challenge.id)

                # notify message only to users, do not post on the challenge
                challenge.message_notify(
                    body=body_html,
                    partner_ids=[user.partner_id.id],
                    subtype='mail.mt_comment',
                    email_layout_xmlid='mail.mail_notification_light',
                )
                if challenge.report_message_group_id:
                    challenge.report_message_group_id.message_post(
                        body=body_html,
                        subtype='mail.mt_comment',
                        email_layout_xmlid='mail.mail_notification_light',
                    )
        return challenge.write({'last_report_date': fields.Date.today()})

    ##### Challenges #####
    def accept_challenge(self):
        user = self.env.user
        sudoed = self.sudo()
        sudoed.message_post(body=_("%s has joined the challenge") % user.name)
        sudoed.write({'invited_user_ids': [(3, user.id)], 'user_ids': [(4, user.id)]})
        return sudoed._generate_goals_from_challenge()

    def discard_challenge(self):
        """The user discard the suggested challenge"""
        user = self.env.user
        sudoed = self.sudo()
        sudoed.message_post(body=_("%s has refused the challenge") % user.name)
        return sudoed.write({'invited_user_ids': (3, user.id)})

    def _check_challenge_reward(self, force=False):
        """Actions for the end of a challenge

        If a reward was selected, grant it to the correct users.
        Rewards granted at:
            - the end date for a challenge with no periodicity
            - the end of a period for challenge with periodicity
            - when a challenge is manually closed
        (if no end date, a running challenge is never rewarded)
        """
        commit = self.env.context.get('commit_gamification') and self.env.cr.commit

        for challenge in self:
            (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date)
            yesterday = date.today() - timedelta(days=1)

            rewarded_users = self.env['res.users']
            challenge_ended = force or end_date == fields.Date.to_string(yesterday)
            if challenge.reward_id and (challenge_ended or challenge.reward_realtime):
                # not using start_date as intemportal goals have a start date but no end_date
                reached_goals = self.env['gamification.goal'].read_group([
                    ('challenge_id', '=', challenge.id),
                    ('end_date', '=', end_date),
                    ('state', '=', 'reached')
                ], fields=['user_id'], groupby=['user_id'])
                for reach_goals_user in reached_goals:
                    if reach_goals_user['user_id_count'] == len(challenge.line_ids):
                        # the user has succeeded every assigned goal
                        user = self.env['res.users'].browse(reach_goals_user['user_id'][0])
                        if challenge.reward_realtime:
                            badges = self.env['gamification.badge.user'].search_count([
                                ('challenge_id', '=', challenge.id),
                                ('badge_id', '=', challenge.reward_id.id),
                                ('user_id', '=', user.id),
                            ])
                            if badges > 0:
                                # has already recieved the badge for this challenge
                                continue
                        challenge._reward_user(user, challenge.reward_id)
                        rewarded_users |= user
                        if commit:
                            commit()

            if challenge_ended:
                # open chatter message
                message_body = _("The challenge %s is finished.") % challenge.name

                if rewarded_users:
                    user_names = rewarded_users.name_get()
                    message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s.") % (challenge.reward_id.name, ", ".join(name for (user_id, name) in user_names))
                else:
                    message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewarded for this challenge.")

                # reward bests
                reward_message = _("<br/> %(rank)d. %(user_name)s - %(reward_name)s")
                if challenge.reward_first_id:
                    (first_user, second_user, third_user) = challenge._get_topN_users(MAX_VISIBILITY_RANKING)
                    if first_user:
                        challenge._reward_user(first_user, challenge.reward_first_id)
                        message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :")
                        message_body += reward_message % {
                            'rank': 1,
                            'user_name': first_user.name,
                            'reward_name': challenge.reward_first_id.name,
                        }
                    else:
                        message_body += _("Nobody reached the required conditions to receive special badges.")

                    if second_user and challenge.reward_second_id:
                        challenge._reward_user(second_user, challenge.reward_second_id)
                        message_body += reward_message % {
                            'rank': 2,
                            'user_name': second_user.name,
                            'reward_name': challenge.reward_second_id.name,
                        }
                    if third_user and challenge.reward_third_id:
                        challenge._reward_user(third_user, challenge.reward_third_id)
                        message_body += reward_message % {
                            'rank': 3,
                            'user_name': third_user.name,
                            'reward_name': challenge.reward_third_id.name,
                        }

                challenge.message_post(
                    partner_ids=[user.partner_id.id for user in challenge.user_ids],
                    body=message_body)
                if commit:
                    commit()

        return True

    def _get_topN_users(self, n):
        """Get the top N users for a defined challenge

        Ranking criterias:
            1. succeed every goal of the challenge
            2. total completeness of each goal (can be over 100)

        Only users having reached every goal of the challenge will be returned
        unless the challenge ``reward_failure`` is set, in which case any user
        may be considered.

        :returns: an iterable of exactly N records, either User objects or
                  False if there was no user for the rank. There can be no
                  False between two users (if users[k] = False then
                  users[k+1] = False
        """
        Goals = self.env['gamification.goal']
        (start_date, end_date) = start_end_date_for_period(self.period, self.start_date, self.end_date)
        challengers = []
        for user in self.user_ids:
            all_reached = True
            total_completeness = 0
            # every goal of the user for the running period
            goal_ids = Goals.search([
                ('challenge_id', '=', self.id),
                ('user_id', '=', user.id),
                ('start_date', '=', start_date),
                ('end_date', '=', end_date)
            ])
            for goal in goal_ids:
                if goal.state != 'reached':
                    all_reached = False
                if goal.definition_condition == 'higher':
                    # can be over 100
                    total_completeness += (100.0 * goal.current / goal.target_goal) if goal.target_goal else 0
                elif goal.state == 'reached':
                    # for lower goals, can not get percentage so 0 or 100
                    total_completeness += 100

            challengers.append({'user': user, 'all_reached': all_reached, 'total_completeness': total_completeness})

        challengers.sort(key=lambda k: (k['all_reached'], k['total_completeness']), reverse=True)
        if not self.reward_failure:
            # only keep the fully successful challengers at the front, could
            # probably use filter since the successful ones are at the front
            challengers = itertools.takewhile(lambda c: c['all_reached'], challengers)

        # append a tail of False, then keep the first N
        challengers = itertools.islice(
            itertools.chain(
                (c['user'] for c in challengers),
                itertools.repeat(False),
            ), 0, n
        )

        return tuple(challengers)

    def _reward_user(self, user, badge):
        """Create a badge user and send the badge to him

        :param user: the user to reward
        :param badge: the concerned badge
        """
        return self.env['gamification.badge.user'].create({
            'user_id': user.id,
            'badge_id': badge.id,
            'challenge_id': self.id
        })._send_badge()
예제 #22
0
class SurveyQuestion(models.Model):
    """ Questions that will be asked in a survey.

        Each question can have one of more suggested answers (eg. in case of
        dropdown choices, multi-answer checkboxes, radio buttons...).

        Technical note:

        survey.question is also the model used for the survey's pages (with the "is_page" field set to True).

        A page corresponds to a "section" in the interface, and the fact that it separates the survey in
        actual pages in the interface depends on the "questions_layout" parameter on the survey.survey model.
        Pages are also used when randomizing questions. The randomization can happen within a "page".

        Using the same model for questions and pages allows to put all the pages and questions together in a o2m field
        (see survey.survey.question_and_page_ids) on the view side and easily reorganize your survey by dragging the
        items around.

        It also removes on level of encoding by directly having 'Add a page' and 'Add a question'
        links on the tree view of questions, enabling a faster encoding.

        However, this has the downside of making the code reading a little bit more complicated.
        Efforts were made at the model level to create computed fields so that the use of these models
        still seems somewhat logical. That means:
        - A survey still has "page_ids" (question_and_page_ids filtered on is_page = True)
        - These "page_ids" still have question_ids (questions located between this page and the next)
        - These "question_ids" still have a "page_id"

        That makes the use and display of these information at view and controller levels easier to understand.
    """

    _name = 'survey.question'
    _description = 'Survey Question'
    _rec_name = 'question'
    _order = 'sequence,id'

    @api.model
    def default_get(self, fields):
        defaults = super(SurveyQuestion, self).default_get(fields)
        if (not fields or 'question_type' in fields):
            defaults['question_type'] = False if defaults.get(
                'is_page') == True else 'free_text'
        return defaults

    # Question metadata
    survey_id = fields.Many2one('survey.survey',
                                string='Survey',
                                ondelete='cascade')
    page_id = fields.Many2one('survey.question',
                              string='Page',
                              compute="_compute_page_id",
                              store=True)
    question_ids = fields.One2many('survey.question',
                                   string='Questions',
                                   compute="_compute_question_ids")
    scoring_type = fields.Selection(related='survey_id.scoring_type',
                                    string='Scoring Type',
                                    readonly=True)
    sequence = fields.Integer('Sequence', default=10)
    # Question
    is_page = fields.Boolean('Is a page?')
    questions_selection = fields.Selection(
        related='survey_id.questions_selection',
        readonly=True,
        help=
        "If randomized is selected, add the number of random questions next to the section."
    )
    random_questions_count = fields.Integer(
        'Random questions count',
        default=1,
        help=
        "Used on randomized sections to take X random questions from all the questions of that section."
    )
    title = fields.Char('Title', required=True, translate=True)
    question = fields.Char('Question', related="title")
    description = fields.Html(
        'Description',
        help=
        "Use this field to add additional explanations about your question",
        translate=True)
    question_type = fields.Selection(
        [('free_text', 'Multiple Lines Text Box'),
         ('textbox', 'Single Line Text Box'),
         ('numerical_box', 'Numerical Value'), ('date', 'Date'),
         ('datetime', 'Datetime'),
         ('simple_choice', 'Multiple choice: only one answer'),
         ('multiple_choice', 'Multiple choice: multiple answers allowed'),
         ('matrix', 'Matrix')],
        string='Question Type')
    # simple choice / multiple choice / matrix
    labels_ids = fields.One2many(
        'survey.label',
        'question_id',
        string='Types of answers',
        copy=True,
        help=
        'Labels used for proposed choices: simple choice, multiple choice and columns of matrix'
    )
    # matrix
    matrix_subtype = fields.Selection(
        [('simple', 'One choice per row'),
         ('multiple', 'Multiple choices per row')],
        string='Matrix Type',
        default='simple')
    labels_ids_2 = fields.One2many(
        'survey.label',
        'question_id_2',
        string='Rows of the Matrix',
        copy=True,
        help='Labels used for proposed choices: rows of matrix')
    # Display options
    column_nb = fields.Selection(
        [('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')],
        string='Number of columns',
        default='12',
        help=
        'These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap for dropdown-based simple and multiple choice questions.'
    )
    display_mode = fields.Selection(
        [('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')],
        string='Display Mode',
        default='columns',
        help='Display mode of simple choice questions.')
    # Comments
    comments_allowed = fields.Boolean('Show Comments Field')
    comments_message = fields.Char(
        'Comment Message',
        translate=True,
        default=lambda self: _("If other, please specify:"))
    comment_count_as_answer = fields.Boolean(
        'Comment Field is an Answer Choice')
    # Validation
    validation_required = fields.Boolean('Validate entry')
    validation_email = fields.Boolean('Input must be an email')
    validation_length_min = fields.Integer('Minimum Text Length')
    validation_length_max = fields.Integer('Maximum Text Length')
    validation_min_float_value = fields.Float('Minimum value')
    validation_max_float_value = fields.Float('Maximum value')
    validation_min_date = fields.Date('Minimum Date')
    validation_max_date = fields.Date('Maximum Date')
    validation_min_datetime = fields.Datetime('Minimum Datetime')
    validation_max_datetime = fields.Datetime('Maximum Datetime')
    validation_error_msg = fields.Char(
        'Validation Error message',
        translate=True,
        default=lambda self: _("The answer you entered is not valid."))
    # Constraints on number of answers (matrices)
    constr_mandatory = fields.Boolean('Mandatory Answer')
    constr_error_msg = fields.Char(
        'Error message',
        translate=True,
        default=lambda self: _("This question requires an answer."))
    # Answer
    user_input_line_ids = fields.One2many('survey.user_input_line',
                                          'question_id',
                                          string='Answers',
                                          domain=[('skipped', '=', False)],
                                          groups='survey.group_survey_user')

    _sql_constraints = [
        ('positive_len_min', 'CHECK (validation_length_min >= 0)',
         'A length must be positive!'),
        ('positive_len_max', 'CHECK (validation_length_max >= 0)',
         'A length must be positive!'),
        ('validation_length',
         'CHECK (validation_length_min <= validation_length_max)',
         'Max length cannot be smaller than min length!'),
        ('validation_float',
         'CHECK (validation_min_float_value <= validation_max_float_value)',
         'Max value cannot be smaller than min value!'),
        ('validation_date',
         'CHECK (validation_min_date <= validation_max_date)',
         'Max date cannot be smaller than min date!'),
        ('validation_datetime',
         'CHECK (validation_min_datetime <= validation_max_datetime)',
         'Max datetime cannot be smaller than min datetime!')
    ]

    @api.onchange('validation_email')
    def _onchange_validation_email(self):
        if self.validation_email:
            self.validation_required = False

    @api.onchange('is_page')
    def _onchange_is_page(self):
        if self.is_page:
            self.question_type = False

    # Validation methods

    def validate_question(self, post, answer_tag):
        """ Validate question, depending on question type and parameters """
        self.ensure_one()
        try:
            checker = getattr(self, 'validate_' + self.question_type)
        except AttributeError:
            _logger.warning(self.question_type +
                            ": This type of question has no validation method")
            return {}
        else:
            return checker(post, answer_tag)

    def validate_free_text(self, post, answer_tag):
        self.ensure_one()
        errors = {}
        answer = post[answer_tag].strip()
        # Empty answer to mandatory question
        if self.constr_mandatory and not answer:
            errors.update({answer_tag: self.constr_error_msg})
        return errors

    def validate_textbox(self, post, answer_tag):
        self.ensure_one()
        errors = {}
        answer = post[answer_tag].strip()
        # Empty answer to mandatory question
        if self.constr_mandatory and not answer:
            errors.update({answer_tag: self.constr_error_msg})
        # Email format validation
        # Note: this validation is very basic:
        #     all the strings of the form
        #     <something>@<anything>.<extension>
        #     will be accepted
        if answer and self.validation_email:
            if not email_validator.match(answer):
                errors.update(
                    {answer_tag: _('This answer must be an email address')})
        # Answer validation (if properly defined)
        # Length of the answer must be in a range
        if answer and self.validation_required:
            if not (self.validation_length_min <= len(answer) <=
                    self.validation_length_max):
                errors.update({answer_tag: self.validation_error_msg})
        return errors

    def validate_numerical_box(self, post, answer_tag):
        self.ensure_one()
        errors = {}
        answer = post[answer_tag].strip()
        # Empty answer to mandatory question
        if self.constr_mandatory and not answer:
            errors.update({answer_tag: self.constr_error_msg})
        # Checks if user input is a number
        if answer:
            try:
                floatanswer = float(answer)
            except ValueError:
                errors.update({answer_tag: _('This is not a number')})
        # Answer validation (if properly defined)
        if answer and self.validation_required:
            # Answer is not in the right range
            with tools.ignore(Exception):
                floatanswer = float(
                    answer)  # check that it is a float has been done hereunder
                if not (self.validation_min_float_value <= floatanswer <=
                        self.validation_max_float_value):
                    errors.update({answer_tag: self.validation_error_msg})
        return errors

    def date_validation(self, date_type, post, answer_tag, min_value,
                        max_value):
        self.ensure_one()
        errors = {}
        if date_type not in ('date', 'datetime'):
            raise ValueError("Unexpected date type value")
        answer = post[answer_tag].strip()
        # Empty answer to mandatory question
        if self.constr_mandatory and not answer:
            errors.update({answer_tag: self.constr_error_msg})
        # Checks if user input is a date
        if answer:
            try:
                if date_type == 'datetime':
                    dateanswer = fields.Datetime.from_string(answer)
                else:
                    dateanswer = fields.Date.from_string(answer)
            except ValueError:
                errors.update({answer_tag: _('This is not a date')})
                return errors
        # Answer validation (if properly defined)
        if answer and self.validation_required:
            # Answer is not in the right range
            try:
                if date_type == 'datetime':
                    date_from_string = fields.Datetime.from_string
                else:
                    date_from_string = fields.Date.from_string
                dateanswer = date_from_string(answer)
                min_date = date_from_string(min_value)
                max_date = date_from_string(max_value)

                if min_date and max_date and not (min_date <= dateanswer <=
                                                  max_date):
                    # If Minimum and Maximum Date are entered
                    errors.update({answer_tag: self.validation_error_msg})
                elif min_date and not min_date <= dateanswer:
                    # If only Minimum Date is entered and not Define Maximum Date
                    errors.update({answer_tag: self.validation_error_msg})
                elif max_date and not dateanswer <= max_date:
                    # If only Maximum Date is entered and not Define Minimum Date
                    errors.update({answer_tag: self.validation_error_msg})
            except ValueError:  # check that it is a date has been done hereunder
                pass
        return errors

    def validate_date(self, post, answer_tag):
        return self.date_validation('date', post, answer_tag,
                                    self.validation_min_date,
                                    self.validation_max_date)

    def validate_datetime(self, post, answer_tag):
        return self.date_validation('datetime', post, answer_tag,
                                    self.validation_min_datetime,
                                    self.validation_max_datetime)

    def validate_simple_choice(self, post, answer_tag):
        self.ensure_one()
        errors = {}
        if self.comments_allowed:
            comment_tag = "%s_%s" % (answer_tag, 'comment')
        # Empty answer to mandatory self
        if self.constr_mandatory and answer_tag not in post:
            errors.update({answer_tag: self.constr_error_msg})
        if self.constr_mandatory and answer_tag in post and not post[
                answer_tag].strip():
            errors.update({answer_tag: self.constr_error_msg})
        # Answer is a comment and is empty
        if self.constr_mandatory and answer_tag in post and post[
                answer_tag] == "-1" and self.comment_count_as_answer and comment_tag in post and not post[
                    comment_tag].strip():
            errors.update({answer_tag: self.constr_error_msg})
        return errors

    def validate_multiple_choice(self, post, answer_tag):
        self.ensure_one()
        errors = {}
        if self.constr_mandatory:
            answer_candidates = dict_keys_startswith(post, answer_tag)
            comment_flag = answer_candidates.pop(("%s_%s" % (answer_tag, -1)),
                                                 None)
            if self.comments_allowed:
                comment_answer = answer_candidates.pop(
                    ("%s_%s" % (answer_tag, 'comment')), '').strip()
            # Preventing answers with blank value
            if all(not answer.strip() for answer in
                   answer_candidates.values()) and answer_candidates:
                errors.update({answer_tag: self.constr_error_msg})
            # There is no answer neither comments (if comments count as answer)
            if not answer_candidates and self.comment_count_as_answer and (
                    not comment_flag or not comment_answer):
                errors.update({answer_tag: self.constr_error_msg})
            # There is no answer at all
            if not answer_candidates and not self.comment_count_as_answer:
                errors.update({answer_tag: self.constr_error_msg})
        return errors

    def validate_matrix(self, post, answer_tag):
        self.ensure_one()
        errors = {}
        if self.constr_mandatory:
            lines_number = len(self.labels_ids_2)
            answer_candidates = dict_keys_startswith(post, answer_tag)
            answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')),
                                  '').strip()
            # Number of lines that have been answered
            if self.matrix_subtype == 'simple':
                answer_number = len(answer_candidates)
            elif self.matrix_subtype == 'multiple':
                answer_number = len(
                    {sk.rsplit('_', 1)[0]
                     for sk in answer_candidates})
            else:
                raise RuntimeError("Invalid matrix subtype")
            # Validate that each line has been answered
            if answer_number != lines_number:
                errors.update({answer_tag: self.constr_error_msg})
        return errors

    @api.depends('survey_id.question_and_page_ids.is_page',
                 'survey_id.question_and_page_ids.sequence')
    def _compute_question_ids(self):
        """Will take all questions of the survey for which the index is higher than the index of this page
        and lower than the index of the next page."""
        for question in self:
            if question.is_page:
                next_page_index = False
                for page in question.survey_id.page_ids:
                    if page._index() > question._index():
                        next_page_index = page._index()
                        break

                question.question_ids = question.survey_id.question_ids.filtered(
                    lambda q: q._index() > question._index() and
                    (not next_page_index or q._index() < next_page_index))
            else:
                question.question_ids = self.env['survey.question']

    @api.depends('survey_id.question_and_page_ids.is_page',
                 'survey_id.question_and_page_ids.sequence')
    def _compute_page_id(self):
        """Will find the page to which this question belongs to by looking inside the corresponding survey"""
        for question in self:
            if question.is_page:
                question.page_id = None
            else:
                question.page_id = next((iter(
                    question.survey_id.question_and_page_ids.filtered(
                        lambda q: q.is_page and q.sequence < question.sequence
                    ).sorted(reverse=True))), None)

    def _index(self):
        """We would normally just use the 'sequence' field of questions BUT, if the pages and questions are
        created without ever moving records around, the sequence field can be set to 0 for all the questions.

        However, the order of the recordset is always correct so we can rely on the index method."""
        self.ensure_one()
        return list(self.survey_id.question_and_page_ids).index(self)

    def get_correct_answer_ids(self):
        self.ensure_one()

        return self.labels_ids.filtered(lambda label: label.is_correct)
예제 #23
0
class MrpProduction(models.Model):
    _inherit = 'mrp.production'

    extra_cost = fields.Float(copy=False, help='Extra cost per produced unit')
    show_valuation = fields.Boolean(compute='_compute_show_valuation')

    def _compute_show_valuation(self):
        for order in self:
            order.show_valuation = any(m.state == 'done'
                                       for m in order.move_finished_ids)

    def _cal_price(self, consumed_moves):
        """Set a price unit on the finished move according to `consumed_moves`.
        """
        super(MrpProduction, self)._cal_price(consumed_moves)
        work_center_cost = 0
        finished_move = self.move_finished_ids.filtered(
            lambda x: x.product_id == self.product_id and x.state not in
            ('done', 'cancel') and x.quantity_done > 0)
        if finished_move:
            finished_move.ensure_one()
            for work_order in self.workorder_ids:
                time_lines = work_order.time_ids.filtered(
                    lambda x: x.date_end and not x.cost_already_recorded)
                duration = sum(time_lines.mapped('duration'))
                time_lines.write({'cost_already_recorded': True})
                work_center_cost += (
                    duration / 60.0) * work_order.workcenter_id.costs_hour
            if finished_move.product_id.cost_method in ('fifo', 'average'):
                qty_done = finished_move.product_uom._compute_quantity(
                    finished_move.quantity_done,
                    finished_move.product_id.uom_id)
                extra_cost = self.extra_cost * qty_done
                finished_move.price_unit = (sum([
                    -m.stock_valuation_layer_ids.value for m in consumed_moves
                ]) + work_center_cost + extra_cost) / qty_done
        return True

    def _prepare_wc_analytic_line(self, wc_line):
        wc = wc_line.workcenter_id
        hours = wc_line.duration / 60.0
        value = hours * wc.costs_hour
        account = wc.costs_hour_account_id.id
        return {
            'name': wc_line.name + ' (H)',
            'amount': -value,
            'account_id': account,
            'ref': wc.code,
            'unit_amount': hours,
            'company_id': self.company_id.id,
        }

    def _costs_generate(self):
        """ Calculates total costs at the end of the production.
        """
        self.ensure_one()
        AccountAnalyticLine = self.env['account.analytic.line'].sudo()
        for wc_line in self.workorder_ids.filtered(
                'workcenter_id.costs_hour_account_id'):
            vals = self._prepare_wc_analytic_line(wc_line)
            precision_rounding = wc_line.workcenter_id.costs_hour_account_id.currency_id.rounding
            if not float_is_zero(vals.get('amount', 0.0),
                                 precision_rounding=precision_rounding):
                # we use SUPERUSER_ID as we do not guarantee an mrp user
                # has access to account analytic lines but still should be
                # able to produce orders
                AccountAnalyticLine.create(vals)

    def button_mark_done(self):
        self.ensure_one()
        res = super(MrpProduction, self).button_mark_done()
        self._costs_generate()
        return res

    def action_view_stock_valuation_layers(self):
        self.ensure_one()
        domain = [('id', 'in',
                   (self.move_raw_ids + self.move_finished_ids +
                    self.scrap_ids.move_id).stock_valuation_layer_ids.ids)]
        action = self.env.ref(
            'stock_account.stock_valuation_layer_action').read()[0]
        context = literal_eval(action['context'])
        context.update(self.env.context)
        context['no_at_date'] = True
        return dict(action, domain=domain, context=context)
예제 #24
0
class EventType(models.Model):
    _name = 'event.type'
    _inherit = ['event.type']

    website_menu = fields.Boolean('Display a dedicated menu on Website')
예제 #25
0
class SaasClient(models.Model):
    _name = 'saas.client'
    _order = 'id desc'
    _description = 'Class for managing SaaS Instances(Clients)'

    @api.depends('data_directory_path')
    def _compute_addons_path(self):
        for obj in self:
            if obj.data_directory_path and type(obj.id) != NewId:
                obj.addons_path = "{}/addons/13.0".format(
                    obj.data_directory_path)
            else:
                obj.addons_path = ""

    name = fields.Char(string="Name")
    client_url = fields.Char(string="URL")
    database_name = fields.Char(string="Database Name")
    saas_contract_id = fields.Many2one(comodel_name="saas.contract",
                                       string="SaaS Contract")
    partner_id = fields.Many2one(comodel_name="res.partner", string="Customer")
    containter_port = fields.Char(string="Port")
    containter_path = fields.Char(string="Path")
    container_name = fields.Char(string="Instance Name")
    container_id = fields.Char(string="Instance ID")
    data_directory_path = fields.Char(string="Data Directory Path")
    addons_path = fields.Char(compute='_compute_addons_path',
                              string="Extra Addons Path")
    saas_module_ids = fields.One2many(comodel_name="saas.module.status",
                                      inverse_name="client_id",
                                      string="Related Modules")
    server_id = fields.Many2one(comodel_name="saas.server",
                                string="SaaS Server")
    invitation_url = fields.Char("Invitation URL")
    state = fields.Selection(selection=CLIENT_STATE,
                             default="draft",
                             string="State")
    is_drop_db = fields.Boolean(string="Drop Db", default=False)
    is_drop_container = fields.Boolean(string="Drop Container", default=False)

    _sql_constraints = [
        ('database_name_uniq', 'unique(database_name)',
         'Database Name Must Be Unique !!!'),
    ]

    @api.model
    def create_docker_instance(self, domain_name=None):
        modules = [module.technical_name for module in self.saas_module_ids]
        host_server, db_server = self.saas_contract_id.plan_id.server_id.get_server_details(
        )
        response = None
        self.database_name = domain_name.replace("https://",
                                                 "").replace("http://", "")
        config_path = get_module_resource('eagle_saas_kit')
        response = saas.main(
            dict(db_template=self.saas_contract_id.db_template,
                 db_name=self.database_name,
                 modules=modules,
                 config_path=config_path,
                 host_domain=domain_name,
                 host_server=host_server,
                 db_server=db_server))
        return response

    @api.model
    def create_client_instance(self, domain_name=None):
        server_id = self.server_id
        if server_id.server_type == 'containerized':
            return self.create_docker_instance(domain_name)
        return False

    def fetch_client_url(self, domain_name=None):
        for obj in self:
            if type(domain_name) != str:
                if obj.saas_contract_id.use_separate_domain:
                    domain_name = obj.saas_contract_id.domain_name
                else:
                    domain_name = "{}.{}".format(
                        obj.saas_contract_id.domain_name,
                        obj.saas_contract_id.saas_domain_url)

            response = None
            try:
                response = obj.create_client_instance(domain_name)
            except Exception as e:
                raise UserError("Unable To Create Client\nERROR: {}".format(e))
            if response:
                obj.client_url = response.get("url", False)
                obj.containter_port = response.get("port", False)
                obj.containter_path = response.get("path", False)
                obj.container_name = response.get("name", False)
                obj.container_id = response.get("container_id", False)
                obj.state = "started"

                obj.data_directory_path = response.get("extra-addons", False)
                if response.get("modules_installation", False):
                    for module_status_id in obj.saas_module_ids:
                        module_status_id.status = 'installed'
                else:
                    for module_status_id in obj.saas_module_ids:
                        if module_status_id.technical_name not in response.get(
                                "modules_missed", []):
                            module_status_id.status = 'installed'
            else:
                raise UserError(
                    "Couldn't create the instance with the selected domain name. Please use some other domain name."
                )

    def login_to_client_instance(self):
        for obj in self:
            host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details(
            )
            response = query.get_credentials(obj.database_name,
                                             host_server=host_server,
                                             db_server=db_server)
            if response:
                login = response[0][0]
                password = response[0][1]
                login_url = "{}/saas/login?db={}&login={}&passwd={}".format(
                    obj.client_url, obj.database_name, login, password)
                return {
                    'type': 'ir.actions.act_url',
                    'url': login_url,
                    'target': 'new',
                }
            else:
                raise UserError("Unknown Error!")

    def stop_client(self):
        for obj in self:
            host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details(
            )
            response_flag = containers.action(operation="stop",
                                              container_id=obj.container_id,
                                              host_server=host_server,
                                              db_server=db_server)
            if response_flag:
                obj.state = "stopped"
            else:
                raise UserError("Operation Failed! Unknown Error!")

    def start_client(self):
        for obj in self:
            host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details(
            )
            response_flag = containers.action(operation="start",
                                              container_id=obj.container_id,
                                              host_server=host_server,
                                              db_server=db_server)
            if response_flag:
                obj.state = "started"
            else:
                raise UserError("Operation Failed! Unknown Error!")

    def restart_client(self):
        for obj in self:
            host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details(
            )
            response_flag = containers.action(operation="restart",
                                              container_id=obj.container_id,
                                              host_server=host_server,
                                              db_server=db_server)
            if response_flag:
                obj.state = "started"
            else:
                raise UserError("Operation Failed! Unknown Error!")

    @api.model
    def create(self, vals):
        vals['name'] = self.env['ir.sequence'].next_by_code('saas.client')
        return super(SaasClient, self).create(vals)

    def disable_client_wizard(self):
        raise UserError("Developement Under Process")
        res_wizard = self.env['saas.client.disable'].sudo().create(
            {'client_id': self.id})
        return {
            'name': 'Disable Client',
            'view_mode': 'form',
            'res_model': 'saas.client.disable',
            'type': 'ir.actions.act_window',
            'res_id': res_wizard.id,
        }

    def inactive_client(self):
        for obj in self:
            if obj.saas_contract_id.state != 'inactive':
                raise UserError('Please Inactive the Related Contract First')
            if obj.state in ['stopped', 'draft']:
                obj.state = 'inactive'
            else:
                raise UserError("Can't Inactive a Running Client")

    def unlink(self):
        for obj in self:
            raise Warning("Can't Delete Clients")

    def drop_db(self):
        for obj in self:
            if obj.state == "inactive":
                host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details(
                )
                _logger.info("HOST SERER %r   DB SERVER  %r" %
                             (host_server, db_server))
                response = client.main(obj.database_name,
                                       obj.containter_port,
                                       host_server,
                                       get_module_resource('eagle_saas_kit'),
                                       from_drop_db=True)
                if not response['db_drop']:
                    raise UserError(
                        "ERROR: Couldn't Drop Client Database. Please Try Again Later.\n\nOperation\tStatus\n\nDrop database: \t{}\n"
                        .format(response['db_drop']))
                else:
                    obj.is_drop_db = True

    def drop_container(self):
        for obj in self:
            if obj.state == "inactive":
                host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details(
                )
                _logger.info("HOST SERER %r   DB SERVER  %r" %
                             (host_server, db_server))
                response = client.main(obj.database_name,
                                       obj.containter_port,
                                       host_server,
                                       get_module_resource('eagle_saas_kit'),
                                       container_id=obj.container_id,
                                       db_server=db_server,
                                       from_drop_container=True)
                if not response['drop_container'] or not response[
                        'delete_nginx_vhost'] or not response[
                            'delete_data_dir']:
                    raise UserError(
                        "ERROR: Couldn't Drop Client Container. Please Try Again Later.\n\nOperation\tStatus\n\nDelete Domain Mapping: \t{}\nDelete Data Directory: \t{}"
                        .format(response['drop_container'],
                                response['delete_nginx_vhost']))
                else:
                    obj.is_drop_container = True
예제 #26
0
class Event(models.Model):
    _name = 'event.event'
    _inherit = [
        'event.event', 'website.seo.metadata', 'website.published.multi.mixin'
    ]

    is_published = fields.Boolean(track_visibility='onchange')

    is_participating = fields.Boolean("Is Participating",
                                      compute="_compute_is_participating")

    website_menu = fields.Boolean(
        'Dedicated Menu',
        help="Creates menus Introduction, Location and Register on the page "
        " of the event on the website.",
        copy=False)
    menu_id = fields.Many2one('website.menu', 'Event Menu', copy=False)

    def _compute_is_participating(self):
        # we don't allow public user to see participating label
        if self.env.user != self.env['website'].get_current_website().user_id:
            email = self.env.user.partner_id.email
            for event in self:
                domain = [
                    '&', '|', ('email', '=', email),
                    ('partner_id', '=', self.env.user.partner_id.id),
                    ('event_id', '=', event.id)
                ]
                event.is_participating = self.env[
                    'event.registration'].search_count(domain)

    @api.multi
    @api.depends('name')
    def _compute_website_url(self):
        super(Event, self)._compute_website_url()
        for event in self:
            if event.id:  # avoid to perform a slug on a not yet saved record in case of an onchange.
                event.website_url = '/event/%s' % slug(event)

    @api.onchange('event_type_id')
    def _onchange_type(self):
        super(Event, self)._onchange_type()
        if self.event_type_id:
            self.website_menu = self.event_type_id.website_menu

    def _get_menu_entries(self):
        """ Method returning menu entries to display on the website view of the
        event, possibly depending on some options in inheriting modules. """
        self.ensure_one()
        return [
            (_('Introduction'), False, 'website_event.template_intro'),
            (_('Location'), False, 'website_event.template_location'),
            (_('Register'), '/event/%s/register' % slug(self), False),
        ]

    def _toggle_create_website_menus(self, vals):
        for event in self:
            if 'website_menu' in vals:
                if event.menu_id and not event.website_menu:
                    event.menu_id.unlink()
                elif event.website_menu:
                    if not event.menu_id:
                        root_menu = self.env['website.menu'].create({
                            'name':
                            event.name,
                            'website_id':
                            event.website_id.id
                        })
                        event.menu_id = root_menu
                    for sequence, (name, url, xml_id) in enumerate(
                            event._get_menu_entries()):
                        event._create_menu(sequence, name, url, xml_id)

    @api.model
    def create(self, vals):
        res = super(Event, self).create(vals)
        res._toggle_create_website_menus(vals)
        return res

    @api.multi
    def write(self, vals):
        res = super(Event, self).write(vals)
        self._toggle_create_website_menus(vals)
        return res

    def _create_menu(self, sequence, name, url, xml_id):
        if not url:
            newpath = self.env['website'].new_page(name + ' ' + self.name,
                                                   template=xml_id,
                                                   ispage=False)['url']
            url = "/event/" + slug(self) + "/page/" + newpath[1:]
        menu = self.env['website.menu'].create({
            'name':
            name,
            'url':
            url,
            'parent_id':
            self.menu_id.id,
            'sequence':
            sequence,
            'website_id':
            self.website_id.id,
        })
        return menu

    @api.multi
    def google_map_img(self, zoom=8, width=298, height=298):
        self.ensure_one()
        if self.address_id:
            return self.sudo().address_id.google_map_img(zoom=zoom,
                                                         width=width,
                                                         height=height)
        return None

    @api.multi
    def google_map_link(self, zoom=8):
        self.ensure_one()
        if self.address_id:
            return self.sudo().address_id.google_map_link(zoom=zoom)
        return None

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'is_published' in init_values and self.is_published:
            return 'website_event.mt_event_published'
        elif 'is_published' in init_values and not self.is_published:
            return 'website_event.mt_event_unpublished'
        return super(Event, self)._track_subtype(init_values)

    @api.multi
    def action_open_badge_editor(self):
        """ open the event badge editor : redirect to the report page of event badge report """
        self.ensure_one()
        return {
            'type':
            'ir.actions.act_url',
            'target':
            'new',
            'url':
            '/report/html/%s/%s?enable_editor' %
            ('event.event_event_report_template_badge', self.id),
        }

    @api.multi
    def _get_ics_file(self):
        """ Returns iCalendar file for the event invitation.
            :returns a dict of .ics file content for each event
        """
        result = {}
        if not vobject:
            return result

        for event in self:
            cal = vobject.iCalendar()
            cal_event = cal.add('vevent')

            if not event.date_begin or not event.date_end:
                raise UserError(
                    _("No date has been specified for the event, no file will be generated."
                      ))
            cal_event.add('created').value = fields.Datetime.now().replace(
                tzinfo=pytz.timezone('UTC'))
            cal_event.add('dtstart').value = fields.Datetime.from_string(
                event.date_begin).replace(tzinfo=pytz.timezone('UTC'))
            cal_event.add('dtend').value = fields.Datetime.from_string(
                event.date_end).replace(tzinfo=pytz.timezone('UTC'))
            cal_event.add('summary').value = event.name
            if event.address_id:
                cal_event.add(
                    'location').value = event.sudo().address_id.contact_address

            result[event.id] = cal.serialize().encode('utf-8')
        return result

    def _get_event_resource_urls(self, attendees):
        url_date_start = self.date_begin.strftime('%Y%m%dT%H%M%SZ')
        url_date_stop = self.date_end.strftime('%Y%m%dT%H%M%SZ')
        params = {
            'action': 'TEMPLATE',
            'text': self.name,
            'dates': url_date_start + '/' + url_date_stop,
            'details': self.name,
        }
        if self.address_id:
            params.update(
                location=self.sudo().address_id.contact_address.replace(
                    '\n', ' '))
        encoded_params = werkzeug.url_encode(params)
        google_url = GOOGLE_CALENDAR_URL + encoded_params
        iCal_url = '/event/%s/ics?%s' % (slug(self), encoded_params)
        return {'google_url': google_url, 'iCal_url': iCal_url}

    def _default_website_meta(self):
        res = super(Event, self)._default_website_meta()
        res['default_opengraph']['og:title'] = res['default_twitter'][
            'twitter:title'] = self.name
        res['default_opengraph']['og:description'] = res['default_twitter'][
            'twitter:description'] = self.date_begin
        res['default_twitter']['twitter:card'] = 'summary'
        return res
예제 #27
0
class MrpBom(models.Model):
    """ Defines bills of material for a product or a product template """
    _name = 'mrp.bom'
    _description = 'Bill of Material'
    _inherit = ['mail.thread']
    _rec_name = 'product_tmpl_id'
    _order = "sequence"
    _check_company_auto = True

    def _get_default_product_uom_id(self):
        return self.env['uom.uom'].search([], limit=1, order='id').id

    code = fields.Char('Reference')
    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "If the active field is set to False, it will allow you to hide the bills of material without removing it."
    )
    type = fields.Selection([('normal', 'Manufacture this product'),
                             ('phantom', 'Kit')],
                            'BoM Type',
                            default='normal',
                            required=True)
    product_tmpl_id = fields.Many2one(
        'product.template',
        'Product',
        check_company=True,
        domain=
        "[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        required=True)
    product_id = fields.Many2one(
        'product.product',
        'Product Variant',
        check_company=True,
        domain=
        "['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu']),  '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        help=
        "If a product variant is defined the BOM is available only for this product."
    )
    bom_line_ids = fields.One2many('mrp.bom.line',
                                   'bom_id',
                                   'BoM Lines',
                                   copy=True)
    byproduct_ids = fields.One2many('mrp.bom.byproduct',
                                    'bom_id',
                                    'By-products',
                                    copy=True)
    product_qty = fields.Float('Quantity',
                               default=1.0,
                               digits='Unit of Measure',
                               required=True)
    product_uom_id = fields.Many2one(
        'uom.uom',
        'Unit of Measure',
        default=_get_default_product_uom_id,
        required=True,
        help=
        "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control",
        domain="[('category_id', '=', product_uom_category_id)]")
    product_uom_category_id = fields.Many2one(
        related='product_id.uom_id.category_id')
    sequence = fields.Integer(
        'Sequence',
        help=
        "Gives the sequence order when displaying a list of bills of material."
    )
    routing_id = fields.Many2one(
        'mrp.routing',
        'Routing',
        check_company=True,
        help=
        "The operations for producing this BoM.  When a routing is specified, the production orders will "
        " be executed through work orders, otherwise everything is processed in the production order itself. "
    )
    ready_to_produce = fields.Selection(
        [('all_available', ' When all components are available'),
         ('asap', 'When components for 1st operation are available')],
        string='Manufacturing Readiness',
        default='asap',
        help=
        "Defines when a Manufacturing Order is considered as ready to be started",
        required=True)
    picking_type_id = fields.Many2one(
        'stock.picking.type',
        'Operation Type',
        domain=
        "[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]",
        check_company=True,
        help=
        u"When a procurement has a ‘produce’ route with a operation type set, it will try to create "
        "a Manufacturing Order for that product using a BoM of the same operation type. That allows "
        "to define stock rules which trigger different manufacturing orders with different BoMs."
    )
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 index=True,
                                 default=lambda self: self.env.company)
    consumption = fields.Selection(
        [('strict', 'Strict'), ('flexible', 'Flexible')],
        help=
        "Defines if you can consume more or less components than the quantity defined on the BoM.",
        default='strict',
        string='Consumption')

    @api.onchange('product_id')
    def onchange_product_id(self):
        if self.product_id:
            for line in self.bom_line_ids:
                line.bom_product_template_attribute_value_ids = False

    @api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids')
    def _check_bom_lines(self):
        for bom in self:
            for bom_line in bom.bom_line_ids:
                if bom.product_id and bom_line.product_id == bom.product_id:
                    raise ValidationError(
                        _("BoM line product %s should not be the same as BoM product."
                          ) % bom.display_name)
                if bom_line.product_tmpl_id == bom.product_tmpl_id:
                    raise ValidationError(
                        _("BoM line product %s should not be the same as BoM product."
                          ) % bom.display_name)
                if bom.product_id and bom_line.bom_product_template_attribute_value_ids:
                    raise ValidationError(
                        _("BoM cannot concern product %s and have a line with attributes (%s) at the same time."
                          ) % (bom.product_id.display_name, ", ".join([
                              ptav.display_name for ptav in
                              bom_line.bom_product_template_attribute_value_ids
                          ])))
                for ptav in bom_line.bom_product_template_attribute_value_ids:
                    if ptav.product_tmpl_id != bom.product_tmpl_id:
                        raise ValidationError(
                            _("The attribute value %s set on product %s does not match the BoM product %s."
                              ) %
                            (ptav.display_name,
                             ptav.product_tmpl_id.display_name,
                             bom_line.parent_product_tmpl_id.display_name))

    @api.onchange('product_uom_id')
    def onchange_product_uom_id(self):
        res = {}
        if not self.product_uom_id or not self.product_tmpl_id:
            return
        if self.product_uom_id.category_id.id != self.product_tmpl_id.uom_id.category_id.id:
            self.product_uom_id = self.product_tmpl_id.uom_id.id
            res['warning'] = {
                'title':
                _('Warning'),
                'message':
                _('The Product Unit of Measure you chose has a different category than in the product form.'
                  )
            }
        return res

    @api.onchange('product_tmpl_id')
    def onchange_product_tmpl_id(self):
        if self.product_tmpl_id:
            self.product_uom_id = self.product_tmpl_id.uom_id.id
            if self.product_id.product_tmpl_id != self.product_tmpl_id:
                self.product_id = False
            for line in self.bom_line_ids:
                line.bom_product_template_attribute_value_ids = False

    @api.onchange('routing_id')
    def onchange_routing_id(self):
        for line in self.bom_line_ids:
            line.operation_id = False

    @api.model
    def name_create(self, name):
        # prevent to use string as product_tmpl_id
        if isinstance(name, str):
            raise UserError(
                _("You cannot create a new Bill of Material from here."))
        return super(MrpBom, self).name_create(name)

    def name_get(self):
        return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code
                                   or '', bom.product_tmpl_id.display_name))
                for bom in self]

    def unlink(self):
        if self.env['mrp.production'].search(
            [('bom_id', 'in', self.ids),
             ('state', 'not in', ['done', 'cancel'])],
                limit=1):
            raise UserError(
                _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.'
                  ))
        return super(MrpBom, self).unlink()

    @api.model
    def _bom_find_domain(self,
                         product_tmpl=None,
                         product=None,
                         picking_type=None,
                         company_id=False,
                         bom_type=False):
        if product:
            if not product_tmpl:
                product_tmpl = product.product_tmpl_id
            domain = [
                '|', ('product_id', '=', product.id), '&',
                ('product_id', '=', False),
                ('product_tmpl_id', '=', product_tmpl.id)
            ]
        elif product_tmpl:
            domain = [('product_tmpl_id', '=', product_tmpl.id)]
        else:
            # neither product nor template, makes no sense to search
            raise UserError(
                _('You should provide either a product or a product template to search a BoM'
                  ))
        if picking_type:
            domain += [
                '|', ('picking_type_id', '=', picking_type.id),
                ('picking_type_id', '=', False)
            ]
        if company_id or self.env.context.get('company_id'):
            domain = domain + [
                '|', ('company_id', '=', False),
                ('company_id', '=', company_id
                 or self.env.context.get('company_id'))
            ]
        if bom_type:
            domain += [('type', '=', bom_type)]
        # order to prioritize bom with product_id over the one without
        return domain

    @api.model
    def _bom_find(self,
                  product_tmpl=None,
                  product=None,
                  picking_type=None,
                  company_id=False,
                  bom_type=False):
        """ Finds BoM for particular product, picking and company """
        if product and product.type == 'service' or product_tmpl and product_tmpl.type == 'service':
            return False
        domain = self._bom_find_domain(product_tmpl=product_tmpl,
                                       product=product,
                                       picking_type=picking_type,
                                       company_id=company_id,
                                       bom_type=bom_type)
        if domain is False:
            return domain
        return self.search(domain, order='sequence, product_id', limit=1)

    def explode(self, product, quantity, picking_type=False):
        """
            Explodes the BoM and creates two lists with all the information you need: bom_done and line_done
            Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM
            and converted into its UoM
        """
        from collections import defaultdict

        graph = defaultdict(list)
        V = set()

        def check_cycle(v, visited, recStack, graph):
            visited[v] = True
            recStack[v] = True
            for neighbour in graph[v]:
                if visited[neighbour] == False:
                    if check_cycle(neighbour, visited, recStack,
                                   graph) == True:
                        return True
                elif recStack[neighbour] == True:
                    return True
            recStack[v] = False
            return False

        boms_done = [(self, {
            'qty': quantity,
            'product': product,
            'original_qty': quantity,
            'parent_line': False
        })]
        lines_done = []
        V |= set([product.product_tmpl_id.id])

        bom_lines = [(bom_line, product, quantity, False)
                     for bom_line in self.bom_line_ids]
        for bom_line in self.bom_line_ids:
            V |= set([bom_line.product_id.product_tmpl_id.id])
            graph[product.product_tmpl_id.id].append(
                bom_line.product_id.product_tmpl_id.id)
        while bom_lines:
            current_line, current_product, current_qty, parent_line = bom_lines[
                0]
            bom_lines = bom_lines[1:]

            if current_line._skip_bom_line(current_product):
                continue

            line_quantity = current_qty * current_line.product_qty
            bom = self._bom_find(product=current_line.product_id,
                                 picking_type=picking_type
                                 or self.picking_type_id,
                                 company_id=self.company_id.id,
                                 bom_type='phantom')
            if bom:
                converted_line_quantity = current_line.product_uom_id._compute_quantity(
                    line_quantity / bom.product_qty, bom.product_uom_id)
                bom_lines = [(line, current_line.product_id,
                              converted_line_quantity, current_line)
                             for line in bom.bom_line_ids] + bom_lines
                for bom_line in bom.bom_line_ids:
                    graph[current_line.product_id.product_tmpl_id.id].append(
                        bom_line.product_id.product_tmpl_id.id)
                    if bom_line.product_id.product_tmpl_id.id in V and check_cycle(
                            bom_line.product_id.product_tmpl_id.id,
                        {key: False
                         for key in V}, {key: False
                                         for key in V}, graph):
                        raise UserError(
                            _('Recursion error!  A product with a Bill of Material should not have itself in its BoM or child BoMs!'
                              ))
                    V |= set([bom_line.product_id.product_tmpl_id.id])
                boms_done.append((bom, {
                    'qty': converted_line_quantity,
                    'product': current_product,
                    'original_qty': quantity,
                    'parent_line': current_line
                }))
            else:
                # We round up here because the user expects that if he has to consume a little more, the whole UOM unit
                # should be consumed.
                rounding = current_line.product_uom_id.rounding
                line_quantity = float_round(line_quantity,
                                            precision_rounding=rounding,
                                            rounding_method='UP')
                lines_done.append((current_line, {
                    'qty': line_quantity,
                    'product': current_product,
                    'original_qty': quantity,
                    'parent_line': parent_line
                }))

        return boms_done, lines_done

    @api.model
    def get_import_templates(self):
        return [{
            'label': _('Import Template for Bills of Materials'),
            'template': '/mrp/static/xls/mrp_bom.xls'
        }]
예제 #28
0
class BaseModuleUninstall(models.TransientModel):
    _name = "base.module.uninstall"
    _description = "Module Uninstall"

    show_all = fields.Boolean()
    module_id = fields.Many2one(
        'ir.module.module',
        string="Module",
        required=True,
        domain=[('state', 'in', ['installed', 'to upgrade', 'to install'])],
        ondelete='cascade',
        readonly=True,
    )
    module_ids = fields.Many2many('ir.module.module',
                                  string="Impacted modules",
                                  compute='_compute_module_ids')
    model_ids = fields.Many2many('ir.model',
                                 string="Impacted data models",
                                 compute='_compute_model_ids')

    def _get_modules(self):
        """ Return all the modules impacted by self. """
        return self.module_id.downstream_dependencies(self.module_id)

    @api.depends('module_id', 'show_all')
    def _compute_module_ids(self):
        for wizard in self:
            modules = wizard._get_modules()
            wizard.module_ids = modules if wizard.show_all else modules.filtered(
                'application')

    def _get_models(self):
        """ Return the models (ir.model) to consider for the impact. """
        return self.env['ir.model'].search([('transient', '=', False)])

    @api.depends('module_ids')
    def _compute_model_ids(self):
        ir_models = self._get_models()
        ir_models_xids = ir_models._get_external_ids()
        for wizard in self:
            if wizard.module_id:
                module_names = set(wizard._get_modules().mapped('name'))

                def lost(model):
                    xids = ir_models_xids.get(model.id, ())
                    return xids and all(
                        xid.split('.')[0] in module_names for xid in xids)

                # find the models that have all their XIDs in the given modules
                self.model_ids = ir_models.filtered(lost).sorted('name')

    @api.onchange('module_id')
    def _onchange_module_id(self):
        # if we select a technical module, show technical modules by default
        if not self.module_id.application:
            self.show_all = True

    @api.multi
    def action_uninstall(self):
        modules = self.mapped('module_id')
        return modules.button_immediate_uninstall()
예제 #29
0
class StockMove(models.Model):
    _inherit = 'stock.move'

    is_subcontract = fields.Boolean('The move is a subcontract receipt')
    show_subcontracting_details_visible = fields.Boolean(
        compute='_compute_show_subcontracting_details_visible')

    def _compute_show_subcontracting_details_visible(self):
        """ Compute if the action button in order to see moves raw is visible """
        for move in self:
            if move.is_subcontract and move._has_tracked_subcontract_components() and\
                    not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding):
                move.show_subcontracting_details_visible = True
            else:
                move.show_subcontracting_details_visible = False

    def _compute_show_details_visible(self):
        """ If the move is subcontract and the components are tracked. Then the
        show details button is visible.
        """
        res = super(StockMove, self)._compute_show_details_visible()
        for move in self:
            if not move.is_subcontract:
                continue
            if not move._has_tracked_subcontract_components():
                continue
            move.show_details_visible = True
        return res

    def copy(self, default=None):
        self.ensure_one()
        if not self.is_subcontract or 'location_id' in default:
            return super(StockMove, self).copy(default=default)
        if not default:
            default = {}
        default['location_id'] = self.picking_id.location_id.id
        return super(StockMove, self).copy(default=default)

    def write(self, values):
        """ If the initial demand is updated then also update the linked
        subcontract order to the new quantity.
        """
        if 'product_uom_qty' in values:
            if self.env.context.get('cancel_backorder') is False:
                return super(StockMove, self).write(values)
            self.filtered(
                lambda m: m.is_subcontract and m.state not in
                ['draft', 'cancel', 'done'])._update_subcontract_order_qty(
                    values['product_uom_qty'])
        return super(StockMove, self).write(values)

    def action_show_details(self):
        """ Open the produce wizard in order to register tracked components for
        subcontracted product. Otherwise use standard behavior.
        """
        self.ensure_one()
        if self.is_subcontract:
            rounding = self.product_uom.rounding
            production = self.move_orig_ids.production_id
            if self._has_tracked_subcontract_components() and\
                    float_compare(production.qty_produced, production.product_uom_qty, precision_rounding=rounding) < 0 and\
                    float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) < 0:
                return self._action_record_components()
        action = super(StockMove, self).action_show_details()
        if self.is_subcontract:
            action['views'] = [
                (self.env.ref('stock.view_stock_move_operations').id, 'form')
            ]
            action['context'].update({
                'show_lots_m2o': self.has_tracking != 'none',
                'show_lots_text': False,
            })
        return action

    def action_show_subcontract_details(self):
        """ Display moves raw for subcontracted product self. """
        moves = self.move_orig_ids.production_id.move_raw_ids
        tree_view = self.env.ref(
            'mrp_subcontracting.mrp_subcontracting_move_tree_view')
        form_view = self.env.ref(
            'mrp_subcontracting.mrp_subcontracting_move_form_view')
        return {
            'name': _('Raw Materials for %s') % (self.product_id.display_name),
            'type': 'ir.actions.act_window',
            'res_model': 'stock.move',
            'views': [(tree_view.id, 'tree'), (form_view.id, 'form')],
            'target': 'current',
            'domain': [('id', 'in', moves.ids)],
        }

    def _action_confirm(self, merge=True, merge_into=False):
        subcontract_details_per_picking = defaultdict(list)
        for move in self:
            if move.location_id.usage != 'supplier' or move.location_dest_id.usage == 'supplier':
                continue
            if move.move_orig_ids.production_id:
                continue
            bom = move._get_subcontract_bom()
            if not bom:
                continue
            if float_is_zero(move.product_qty, precision_rounding=move.product_uom.rounding) and\
                    move.picking_id.immediate_transfer is True:
                raise UserError(_("To subcontract, use a planned transfer."))
            subcontract_details_per_picking[move.picking_id].append(
                (move, bom))
            move.write({
                'is_subcontract':
                True,
                'location_id':
                move.picking_id.partner_id.with_context(
                    force_company=move.company_id.id).
                property_stock_subcontractor.id
            })
        for picking, subcontract_details in subcontract_details_per_picking.items(
        ):
            picking._subcontracted_produce(subcontract_details)

        res = super(StockMove, self)._action_confirm(merge=merge,
                                                     merge_into=merge_into)
        if subcontract_details_per_picking:
            self.env['stock.picking'].concat(
                *list(subcontract_details_per_picking.keys())).action_assign()
        return res

    def _action_record_components(self):
        action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
        action['context'] = dict(
            default_production_id=self.move_orig_ids.production_id.id,
            default_subcontract_move_id=self.id)
        return action

    def _check_overprocessed_subcontract_qty(self):
        """ If a subcontracted move use tracked components. Do not allow to add
        quantity without the produce wizard. Instead update the initial demand
        and use the register component button. Split or correct a lot/sn is
        possible.
        """
        overprocessed_moves = self.env['stock.move']
        for move in self:
            if not move.is_subcontract:
                continue
            # Extra quantity is allowed when components do not need to be register
            if not move._has_tracked_subcontract_components():
                continue
            rounding = move.product_uom.rounding
            if float_compare(move.quantity_done,
                             move.move_orig_ids.production_id.qty_produced,
                             precision_rounding=rounding) > 0:
                overprocessed_moves |= move
        if overprocessed_moves:
            raise UserError(
                _("""
You have to use 'Records Components' button in order to register quantity for a
subcontracted product(s) with tracked component(s):
 %s.
If you want to process more than initially planned, you
can use the edit + unlock buttons in order to adapt the initial demand on the
operations.""") % ('\n'.join(
                    overprocessed_moves.mapped('product_id.display_name'))))

    def _get_subcontract_bom(self):
        self.ensure_one()
        bom = self.env['mrp.bom'].sudo()._bom_subcontract_find(
            product=self.product_id,
            picking_type=self.picking_type_id,
            company_id=self.company_id.id,
            bom_type='subcontract',
            subcontractor=self.picking_id.partner_id,
        )
        return bom

    def _has_tracked_subcontract_components(self):
        self.ensure_one()
        return any(m.has_tracking != 'none'
                   for m in self.move_orig_ids.production_id.move_raw_ids)

    def _prepare_extra_move_vals(self, qty):
        vals = super(StockMove, self)._prepare_extra_move_vals(qty)
        vals['location_id'] = self.location_id.id
        return vals

    def _prepare_move_split_vals(self, qty):
        vals = super(StockMove, self)._prepare_move_split_vals(qty)
        vals['location_id'] = self.location_id.id
        return vals

    def _should_bypass_reservation(self):
        """ If the move is subcontracted then ignore the reservation. """
        should_bypass_reservation = super(StockMove,
                                          self)._should_bypass_reservation()
        if not should_bypass_reservation and self.is_subcontract:
            return True
        return should_bypass_reservation

    def _update_subcontract_order_qty(self, quantity):
        for move in self:
            quantity_change = quantity - move.product_uom_qty
            production = move.move_orig_ids.production_id
            if production:
                self.env['change.production.qty'].with_context(
                    skip_activity=True).create({
                        'mo_id':
                        production.id,
                        'product_qty':
                        production.product_uom_qty + quantity_change
                    }).change_prod_qty()
예제 #30
0
class View(models.Model):

    _name = "ir.ui.view"
    _inherit = ["ir.ui.view", "website.seo.metadata"]

    customize_show = fields.Boolean("Show As Optional Inherit", default=False)
    website_id = fields.Many2one('website',
                                 ondelete='cascade',
                                 string="Website")
    page_ids = fields.One2many('website.page', 'view_id')
    first_page_id = fields.Many2one('website.page',
                                    string='Website Page',
                                    help='First page linked to this view',
                                    compute='_compute_first_page_id')

    @api.multi
    def _compute_first_page_id(self):
        for view in self:
            view.first_page_id = self.env['website.page'].search(
                [('view_id', '=', view.id)], limit=1)

    @api.multi
    def write(self, vals):
        '''COW for ir.ui.view. This way editing websites does not impact other
        websites. Also this way newly created websites will only
        contain the default views.
        '''
        current_website_id = self.env.context.get('website_id')
        if not current_website_id or self.env.context.get('no_cow'):
            return super(View, self).write(vals)

        # We need to consider inactive views when handling multi-website cow
        # feature (to copy inactive children views, to search for specific
        # views, ...)
        for view in self.with_context(active_test=False):
            # Make sure views which are written in a website context receive
            # a value for their 'key' field
            if not view.key and not vals.get('key'):
                view.with_context(
                    no_cow=True).key = 'website.key_%s' % str(uuid.uuid4())[:6]

            # No need of COW if the view is already specific
            if view.website_id:
                super(View, view).write(vals)
                continue

            # If already a specific view for this generic view, write on it
            website_specific_view = view.search(
                [('key', '=', view.key),
                 ('website_id', '=', current_website_id)],
                limit=1)
            if website_specific_view:
                super(View, website_specific_view).write(vals)
                continue

            # Set key to avoid copy() to generate an unique key as we want the
            # specific view to have the same key
            copy_vals = {'website_id': current_website_id, 'key': view.key}
            # Copy with the 'inherit_id' field value that will be written to
            # ensure the copied view's validation works
            if vals.get('inherit_id'):
                copy_vals['inherit_id'] = vals['inherit_id']
            website_specific_view = view.copy(copy_vals)

            view._create_website_specific_pages_for_view(
                website_specific_view,
                view.env['website'].browse(current_website_id))

            for inherit_child in view.inherit_children_ids.filter_duplicate(
            ).sorted(key=lambda v: (v.priority, v.id)):
                if inherit_child.website_id.id == current_website_id:
                    # In the case the child was already specific to the current
                    # website, we cannot just reattach it to the new specific
                    # parent: we have to copy it there and remove it from the
                    # original tree. Indeed, the order of children 'id' fields
                    # must remain the same so that the inheritance is applied
                    # in the same order in the copied tree.
                    child = inherit_child.copy({
                        'inherit_id':
                        website_specific_view.id,
                        'key':
                        inherit_child.key
                    })
                    inherit_child.inherit_children_ids.write(
                        {'inherit_id': child.id})
                    inherit_child.unlink()
                else:
                    # Trigger COW on inheriting views
                    inherit_child.write(
                        {'inherit_id': website_specific_view.id})

            super(View, website_specific_view).write(vals)

        return True

    @api.multi
    def _get_specific_views(self):
        """ Given a view, return a record set containing all the specific views
            for that view's key.
            If the given view is already specific, it will also return itself.
        """
        self.ensure_one()
        domain = [('key', '=', self.key), ('website_id', '!=', False)]
        return self.with_context(active_test=False).search(domain)

    def _load_records_write(self, values):
        """ During module update, when updating a generic view, we should also
            update its specific views (COW'd).
            Note that we will only update unmodified fields. That will mimic the
            noupdate behavior on views having an ir.model.data.
        """
        if self.type == 'qweb' and not self.website_id:
            # Update also specific views
            for cow_view in self._get_specific_views():
                authorized_vals = {}
                for key in values:
                    if cow_view[key] == self[key]:
                        authorized_vals[key] = values[key]
                cow_view.write(authorized_vals)
        super(View, self)._load_records_write(values)

    def _load_records_create(self, values):
        """ During module install, when creating a generic child view, we should
            also create that view under specific view trees (COW'd).
            Top level view (no inherit_id) do not need that behavior as they
            will be shared between websites since there is no specific yet.
        """
        records = super(View, self)._load_records_create(values)
        for record in records:
            if record.type == 'qweb' and record.inherit_id and not record.website_id and not record.inherit_id.website_id:
                specific_parent_views = record.with_context(
                    active_test=False).search([
                        ('key', '=', record.inherit_id.key),
                        ('website_id', '!=', None),
                    ])
                for specific_parent_view in specific_parent_views:
                    record.with_context(
                        website_id=specific_parent_view.website_id.id).write({
                            'inherit_id':
                            specific_parent_view.id,
                        })
        return records

    @api.multi
    def unlink(self):
        '''This implements COU (copy-on-unlink). When deleting a generic page
        website-specific pages will be created so only the current
        website is affected.
        '''
        current_website_id = self._context.get('website_id')

        if current_website_id and not self._context.get('no_cow'):
            for view in self.filtered(lambda view: not view.website_id):
                for website in self.env['website'].search([
                    ('id', '!=', current_website_id)
                ]):
                    # reuse the COW mechanism to create
                    # website-specific copies, it will take
                    # care of creating pages and menus.
                    view.with_context(website_id=website.id).write(
                        {'name': view.name})

        specific_views = self.env['ir.ui.view']
        if self and self.pool._init:
            for view in self:
                specific_views += view._get_specific_views()

        result = super(View, self + specific_views).unlink()
        self.clear_caches()
        return result

    def _create_website_specific_pages_for_view(self, new_view, website):
        for page in self.page_ids:
            # create new pages for this view
            page.copy({
                'view_id': new_view.id,
                'is_published': page.is_published,
            })

    @api.model
    def get_related_views(self, key, bundles=False):
        '''Make this only return most specific views for website.'''
        # get_related_views can be called through website=False routes
        # (e.g. /web_editor/get_assets_editor_resources), so website
        # dispatch_parameters may not be added. Manually set
        # website_id. (It will then always fallback on a website, this
        # method should never be called in a generic context, even for
        # tests)
        self = self.with_context(
            website_id=self.env['website'].get_current_website().id)
        return super(View, self).get_related_views(key, bundles=bundles)

    def filter_duplicate(self):
        """ Filter current recordset only keeping the most suitable view per distinct key.
            Every non-accessible view will be removed from the set:
              * In non website context, every view with a website will be removed
              * In a website context, every view from another website
        """
        current_website_id = self._context.get('website_id')
        most_specific_views = self.env['ir.ui.view']
        if not current_website_id:
            return self.filtered(lambda view: not view.website_id)

        for view in self:
            # specific view: add it if it's for the current website and ignore
            # it if it's for another website
            if view.website_id and view.website_id.id == current_website_id:
                most_specific_views |= view
            # generic view: add it only if, for the current website, there is no
            # specific view for this view (based on the same `key` attribute)
            elif not view.website_id and not any(
                    view.key == view2.key and view2.website_id
                    and view2.website_id.id == current_website_id
                    for view2 in self):
                most_specific_views |= view

        return most_specific_views

    @api.model
    def _view_get_inherited_children(self, view, options):
        extensions = super(View,
                           self)._view_get_inherited_children(view, options)
        return extensions.filter_duplicate()

    @api.model
    def _view_obj(self, view_id):
        ''' Given an xml_id or a view_id, return the corresponding view record.
            In case of website context, return the most specific one.
            :param view_id: either a string xml_id or an integer view_id
            :return: The view record or empty recordset
        '''
        if isinstance(view_id, pycompat.string_types) or isinstance(
                view_id, pycompat.integer_types):
            return self.env['website'].viewref(view_id)
        else:
            # It can already be a view object when called by '_views_get()' that is calling '_view_obj'
            # for it's inherit_children_ids, passing them directly as object record. (Note that it might
            # be a view_id from another website but it will be filtered in 'get_related_views()')
            return view_id if view_id._name == 'ir.ui.view' else self.env[
                'ir.ui.view']

    @api.model
    def _get_inheriting_views_arch_website(self, view_id):
        return self.env['website'].browse(self._context.get('website_id'))

    @api.model
    def _get_inheriting_views_arch_domain(self, view_id, model):
        domain = super(View,
                       self)._get_inheriting_views_arch_domain(view_id, model)
        current_website = self._get_inheriting_views_arch_website(view_id)
        website_views_domain = current_website.website_domain()
        # when rendering for the website we have to include inactive views
        # we will prefer inactive website-specific views over active generic ones
        if current_website:
            domain = [leaf for leaf in domain if 'active' not in leaf]

        return expression.AND([website_views_domain, domain])

    @api.model
    def get_inheriting_views_arch(self, view_id, model):
        if not self._context.get('website_id'):
            return super(View, self).get_inheriting_views_arch(view_id, model)

        inheriting_views = super(View, self.with_context(
            active_test=False)).get_inheriting_views_arch(view_id, model)

        # prefer inactive website-specific views over active generic ones
        inheriting_views = self.browse([
            view[1] for view in inheriting_views
        ]).filter_duplicate().filtered('active')

        return [(view.arch, view.id) for view in inheriting_views]

    @api.model
    @tools.ormcache_context('self._uid', 'xml_id', keys=('website_id', ))
    def get_view_id(self, xml_id):
        """If a website_id is in the context and the given xml_id is not an int
        then try to get the id of the specific view for that website, but
        fallback to the id of the generic view if there is no specific.

        If no website_id is in the context, it might randomly return the generic
        or the specific view, so it's probably not recommanded to use this
        method. `viewref` is probably more suitable.

        Archived views are ignored (unless the active_test context is set, but
        then the ormcache_context will not work as expected).
        """
        if 'website_id' in self._context and not isinstance(
                xml_id, pycompat.integer_types):
            current_website = self.env['website'].browse(
                self._context.get('website_id'))
            domain = ['&',
                      ('key', '=', xml_id)] + current_website.website_domain()

            view = self.search(domain, order='website_id', limit=1)
            if not view:
                _logger.warning("Could not find view object with xml_id '%s'",
                                xml_id)
                raise ValueError('View %r in website %r not found' %
                                 (xml_id, self._context['website_id']))
            return view.id
        return super(View, self).get_view_id(xml_id)

    @api.multi
    def _get_original_view(self):
        """Given a view, retrieve the original view it was COW'd from.
        The given view might already be the original one. In that case it will
        (and should) return itself.
        """
        self.ensure_one()
        domain = [('key', '=', self.key), ('model_data_id', '!=', None)]
        return self.with_context(active_test=False).search(
            domain,
            limit=1)  # Useless limit has multiple xmlid should not be possible

    @api.multi
    def render(self, values=None, engine='ir.qweb', minimal_qcontext=False):
        """ Render the template. If website is enabled on request, then extend rendering context with website values. """
        new_context = dict(self._context)
        if request and getattr(request, 'is_frontend', False):

            editable = request.website.is_publisher()
            translatable = editable and self._context.get(
                'lang') != request.website.default_lang_code
            editable = not translatable and editable

            # in edit mode ir.ui.view will tag nodes
            if not translatable and not self.env.context.get(
                    'rendering_bundle'):
                if editable:
                    new_context = dict(self._context, inherit_branding=True)
                elif request.env.user.has_group(
                        'website.group_website_publisher'):
                    new_context = dict(self._context,
                                       inherit_branding_auto=True)
            # Fallback incase main_object dont't inherit 'website.seo.metadata'
            if values and 'main_object' in values and not hasattr(
                    values['main_object'], 'get_website_meta'):
                values['main_object'].get_website_meta = lambda: {}

        if self._context != new_context:
            self = self.with_context(new_context)
        return super(View, self).render(values,
                                        engine=engine,
                                        minimal_qcontext=minimal_qcontext)

    @api.model
    def _prepare_qcontext(self):
        """ Returns the qcontext : rendering context with website specific value (required
            to render website layout template)
        """
        qcontext = super(View, self)._prepare_qcontext()

        if request and getattr(request, 'is_frontend', False):
            Website = self.env['website']
            editable = request.website.is_publisher()
            translatable = editable and self._context.get(
                'lang') != request.env['ir.http']._get_default_lang().code
            editable = not translatable and editable

            if 'main_object' not in qcontext:
                qcontext['main_object'] = self

            cur = Website.get_current_website()
            qcontext['multi_website_websites_current'] = {
                'website_id': cur.id,
                'name': cur.name,
                'domain': cur.domain
            }
            qcontext['multi_website_websites'] = [{
                'website_id': website.id,
                'name': website.name,
                'domain': website.domain
            } for website in Website.search([]) if website != cur]

            cur_company = self.env.user.company_id
            qcontext['multi_website_companies_current'] = {
                'company_id': cur_company.id,
                'name': cur_company.name
            }
            qcontext['multi_website_companies'] = [{
                'company_id': comp.id,
                'name': comp.name
            } for comp in self.env.user.company_ids if comp != cur_company]

            qcontext.update(
                dict(
                    self._context.copy(),
                    website=request.website,
                    url_for=url_for,
                    res_company=request.website.company_id.sudo(),
                    default_lang_code=request.env['ir.http']._get_default_lang(
                    ).code,
                    languages=request.env['ir.http']._get_language_codes(),
                    translatable=translatable,
                    editable=editable,
                    menu_data=self.env['ir.ui.menu'].load_menus_root()
                    if request.website.is_user() else None,
                ))

        return qcontext

    @api.model
    def get_default_lang_code(self):
        website_id = self.env.context.get('website_id')
        if website_id:
            lang_code = self.env['website'].browse(
                website_id).default_lang_code
            return lang_code
        else:
            return super(View, self).get_default_lang_code()

    @api.multi
    def redirect_to_page_manager(self):
        return {
            'type': 'ir.actions.act_url',
            'url': '/website/pages',
            'target': 'self',
        }

    def _read_template_keys(self):
        return super(View, self)._read_template_keys() + ['website_id']

    @api.model
    def _save_oe_structure_hook(self):
        res = super(View, self)._save_oe_structure_hook()
        res['website_id'] = self.env['website'].get_current_website().id
        return res

    @api.model
    def _set_noupdate(self):
        '''If website is installed, any call to `save` from the frontend will
        actually write on the specific view (or create it if not exist yet).
        In that case, we don't want to flag the generic view as noupdate.
        '''
        if not self._context.get('website_id'):
            super(View, self)._set_noupdate()

    @api.multi
    def save(self, value, xpath=None):
        self.ensure_one()
        current_website = self.env['website'].get_current_website()
        # xpath condition is important to be sure we are editing a view and not
        # a field as in that case `self` might not exist (check commit message)
        if xpath and self.key and current_website:
            # The first time a generic view is edited, if multiple editable parts
            # were edited at the same time, multiple call to this method will be
            # done but the first one may create a website specific view. So if there
            # already is a website specific view, we need to divert the super to it.
            website_specific_view = self.env['ir.ui.view'].search(
                [('key', '=', self.key),
                 ('website_id', '=', current_website.id)],
                limit=1)
            if website_specific_view:
                self = website_specific_view
        super(View, self).save(value, xpath=xpath)