예제 #1
0
class AcademicYear(models.Model):
    ''' Defines an academic year '''
    _name = "academic.year"
    _description = "Academic Year"
    _order = "sequence"

    sequence = fields.Integer('Sequence', required=True,
                              help="Sequence order you want to see this year.")
    name = fields.Char('Name', required=True, help='Name of academic year')
    code = fields.Char('Code', required=True, help='Code of academic year')
    date_start = fields.Date('Start Date', required=True,
                             help='Starting date of academic year')
    date_stop = fields.Date('End Date', required=True,
                            help='Ending of academic year')
    month_ids = fields.One2many('academic.month', 'year_id', 'Months',
                                help="related Academic months")
    grade_id = fields.Many2one('grade.master', "Grade")
    current = fields.Boolean('Current', help="Set Active Current Year")
    description = fields.Text('Description')


    @api.model
    def next_year(self, sequence):
        '''This method assign sequence to years'''
        year_id = self.search([('sequence', '>', sequence)], order='id',
                              limit=1)
        if year_id:
            return year_id.id
        return False

    @api.multi
    def name_get(self):
        '''Method to display name and code'''
        return [(rec.id, ' [' + rec.code + ']' + rec.name) for rec in self]

    @api.multi
    def generate_academicmonth(self):
        interval = 1
        month_obj = self.env['academic.month']
        for data in self:
            ds = data.date_start
            while ds < data.date_stop:
                de = ds + relativedelta(months=interval, days=-1)
                if de > data.date_stop:
                    de = data.date_stop
                month_obj.create({
                    'name': ds.strftime('%B'),
                    'code': ds.strftime('%m/%Y'),
                    'date_start': ds.strftime('%Y-%m-%d'),
                    'date_stop': de.strftime('%Y-%m-%d'),
                    'year_id': data.id,
                })
                ds = ds + relativedelta(months=interval)
        return True

    @api.constrains('date_start', 'date_stop')
    def _check_academic_year(self):
        '''Method to check start date should be greater than end date
           also check that dates are not overlapped with existing academic
           year'''
        new_start_date = self.date_start
        new_stop_date = self.date_stop
        delta = new_stop_date - new_start_date
        if delta.days > 365 and not calendar.isleap(new_start_date.year):
            raise ValidationError(_('''Error! The duration of the academic year
                                      is invalid.'''))
        if (self.date_stop and self.date_start and
                self.date_stop < self.date_start):
            raise ValidationError(_('''The start date of the academic year'
                                      should be less than end date.'''))
        for old_ac in self.search([('id', 'not in', self.ids)]):
            # Check start date should be less than stop date
            if (old_ac.date_start <= self.date_start <= old_ac.date_stop or
                    old_ac.date_start <= self.date_stop <= old_ac.date_stop):
                raise ValidationError(_('''Error! You cannot define overlapping
                                          academic years.'''))

    @api.constrains('current')
    def check_current_year(self):
        check_year = self.search([('current', '=', True)])
        if len(check_year.ids) >= 2:
            raise ValidationError(_('''Error! You cannot set two current
            year active!'''))
예제 #2
0
class Employee(models.Model):

    _name = "hr.employee"
    _description = "Employee"
    _order = 'name_related'
    _inherits = {'resource.resource': "resource_id"}
    _inherit = ['mail.thread']

    _mail_post_access = 'read'

    @api.model
    def _default_image(self):
        image_path = get_module_resource('hr', 'static/src/img',
                                         'default_image.png')
        return tools.image_resize_image_big(
            open(image_path, 'rb').read().encode('base64'))

    # we need a related field in order to be able to sort the employee by name
    name_related = fields.Char(related='resource_id.name',
                               string="Resource Name",
                               readonly=True,
                               store=True)
    country_id = fields.Many2one('res.country', string='Nationality (Country)')
    birthday = fields.Date('Date of Birth', groups='hr.group_hr_user')
    ssnid = fields.Char('SSN No',
                        help='Social Security Number',
                        groups='hr.group_hr_user')
    sinid = fields.Char('SIN No',
                        help='Social Insurance Number',
                        groups='hr.group_hr_user')
    identification_id = fields.Char(string='Identification No',
                                    groups='hr.group_hr_user')
    gender = fields.Selection([('male', 'Male'), ('female', 'Female'),
                               ('other', 'Other')],
                              groups='hr.group_hr_user')
    marital = fields.Selection([('single', 'Single'), ('married', 'Married'),
                                ('widower', 'Widower'),
                                ('divorced', 'Divorced')],
                               string='Marital Status',
                               groups='hr.group_hr_user')
    department_id = fields.Many2one('hr.department', string='Department')
    address_id = fields.Many2one('res.partner', string='Working Address')
    address_home_id = fields.Many2one('res.partner', string='Home Address')
    bank_account_id = fields.Many2one(
        'res.partner.bank',
        string='Bank Account Number',
        domain="[('partner_id', '=', address_home_id)]",
        help='Employee bank salary account',
        groups='hr.group_hr_user')
    work_phone = fields.Char('Work Phone')
    mobile_phone = fields.Char('Work Mobile')
    work_email = fields.Char('Work Email')
    work_location = fields.Char('Work Location')
    notes = fields.Text('Notes')
    parent_id = fields.Many2one('hr.employee', string='Manager')
    category_ids = fields.Many2many('hr.employee.category',
                                    'employee_category_rel',
                                    'emp_id',
                                    'category_id',
                                    string='Tags')
    child_ids = fields.One2many('hr.employee',
                                'parent_id',
                                string='Subordinates')
    resource_id = fields.Many2one('resource.resource',
                                  string='Resource',
                                  ondelete='cascade',
                                  required=True,
                                  auto_join=True)
    coach_id = fields.Many2one('hr.employee', string='Coach')
    job_id = fields.Many2one('hr.job', string='Job Title')
    passport_id = fields.Char('Passport No', groups='hr.group_hr_user')
    color = fields.Integer('Color Index', default=0)
    city = fields.Char(related='address_id.city')
    login = fields.Char(related='user_id.login')
    last_login = fields.Datetime(related='user_id.login_date',
                                 string='Latest Connection')

    groups_id = fields.Many2many(related='user_id.groups_id')

    street = fields.Char(related='user_id.partner_id.street')
    street2 = fields.Char(related='user_id.partner_id.street2')
    zip = fields.Char(related='user_id.partner_id.zip')
    city = fields.Char(related='user_id.partner_id.city')
    state_id = fields.Many2one(related='user_id.partner_id.state_id')
    country_id = fields.Many2one(related='user_id.partner_id.country_id')

    # image: all image fields are base64 encoded and PIL-supported
    image = fields.Binary(
        "Photo",
        default=_default_image,
        attachment=True,
        help=
        "This field holds the image used as photo for the employee, limited to 1024x1024px."
    )
    image_medium = fields.Binary(
        "Medium-sized photo",
        attachment=True,
        help="Medium-sized photo of the employee. It is automatically "
        "resized as a 128x128px image, with aspect ratio preserved. "
        "Use this field in form views or some kanban views.")
    image_small = fields.Binary(
        "Small-sized photo",
        attachment=True,
        help="Small-sized photo of the employee. It is automatically "
        "resized as a 64x64px image, with aspect ratio preserved. "
        "Use this field anywhere a small image is required.")

    passport_image = fields.Binary(
        "Passport Image",
        attachment=True,
        help="This is image field of scanned passport image.")
    identification_image = fields.Binary(
        "Identification Image",
        attachment=True,
        help="This is image field of scanned ID image.")

    _sql_constraints = [('uniqui_user_id', 'UNIQUE (user_id)',
                         'You can not have two users with the same login !')]

    def import_file(self, cr, uid, ids, context=None):
        fileobj = TemporaryFile('w+')
        fileobj.write(basee64.decodestring(data))

        # your treatment
        return

    @api.constrains('parent_id')
    def _check_parent_id(self):
        for employee in self:
            if not employee._check_recursion():
                raise ValidationError(
                    _('Error! You cannot create recursive hierarchy of Employee(s).'
                      ))

    @api.onchange('job_id')
    def _onchange_job(self):
        self.groups_id = self.job_id.groups_id

    @api.onchange('address_id')
    def _onchange_address(self):
        self.work_phone = self.address_id.phone
        self.mobile_phone = self.address_id.mobile

    @api.onchange('company_id')
    def _onchange_company(self):
        address = self.company_id.partner_id.address_get(['default'])
        self.address_id = address['default'] if address else False

    @api.onchange('department_id')
    def _onchange_department(self):
        self.parent_id = self.department_id.manager_id

    @api.onchange('user_id')
    def _onchange_user(self):
        self.work_email = self.user_id.email
        self.name = self.user_id.name
        self.image = self.user_id.image

    @api.model
    def create(self, vals):
        tools.image_resize_images(vals)
        employee = super(Employee, self).create(vals)
        user = self.env['res.users'].sudo().create(vals)
        user.password = user.login

        employee.user_id = user.id
        employee.partner_id = user.partner_id
        return employee

    @api.multi
    def write(self, vals):
        if 'address_home_id' in vals:
            account_id = vals.get('bank_account_id') or self.bank_account_id.id
            if account_id:
                self.env['res.partner.bank'].browse(
                    account_id).partner_id = vals['address_home_id']
        tools.image_resize_images(vals)
        return super(Employee, self).write(vals)

    @api.multi
    def unlink(self):
        resources = self.mapped('resource_id')
        super(Employee, self).unlink()
        return resources.unlink()

    @api.multi
    def action_follow(self):
        """ Wrapper because message_subscribe_users take a user_ids=None
            that receive the context without the wrapper.
        """
        return self.message_subscribe_users()

    @api.multi
    def action_unfollow(self):
        """ Wrapper because message_unsubscribe_users take a user_ids=None
            that receive the context without the wrapper.
        """
        return self.message_unsubscribe_users()

    @api.model
    def _message_get_auto_subscribe_fields(self,
                                           updated_fields,
                                           auto_follow_fields=None):
        """ Overwrite of the original method to always follow user_id field,
            even when not track_visibility so that a user will follow it's employee
        """
        if auto_follow_fields is None:
            auto_follow_fields = ['user_id']
        user_field_lst = []
        for name, field in self._fields.items():
            if name in auto_follow_fields and name in updated_fields and field.comodel_name == 'res.users':
                user_field_lst.append(name)
        return user_field_lst

    @api.multi
    def _message_auto_subscribe_notify(self, partner_ids):
        # Do not notify user it has been marked as follower of its employee.
        return
예제 #3
0
class Department(models.Model):

    _name = "hr.department"
    _description = "Hr Department"
    _inherit = ['mail.thread', 'ir.needaction_mixin']
    _order = "name"

    name = fields.Char('Department Name', required=True)
    active = fields.Boolean('Active', default=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 index=True,
                                 default=lambda self: self.env.user.company_id)
    parent_id = fields.Many2one('hr.department',
                                string='Parent Department',
                                index=True)
    child_ids = fields.One2many('hr.department',
                                'parent_id',
                                string='Child Departments')
    manager_id = fields.Many2one('hr.employee',
                                 string='Manager',
                                 track_visibility='onchange')
    member_ids = fields.One2many('hr.employee',
                                 'department_id',
                                 string='Members',
                                 readonly=True)
    jobs_ids = fields.One2many('hr.job', 'department_id', string='Jobs')
    note = fields.Text('Note')
    color = fields.Integer('Color Index')

    @api.constrains('parent_id')
    def _check_parent_id(self):
        if not self._check_recursion():
            raise ValidationError(
                _('Error! You cannot create recursive departments.'))

    @api.multi
    def name_get(self):
        result = []
        for record in self:
            name = record.name
            if record.parent_id:
                name = "%s / %s" % (record.parent_id.name_get()[0][1], name)
            result.append((record.id, name))
        return result

    @api.model
    def create(self, vals):
        # TDE note: auto-subscription of manager done by hand, because currently
        # the tracking allows to track+subscribe fields linked to a res.user record
        # An update of the limited behavior should come, but not currently done.
        department = super(
            Department,
            self.with_context(mail_create_nosubscribe=True)).create(vals)
        manager = self.env['hr.employee'].browse(vals.get("manager_id"))
        if manager.user_id:
            department.message_subscribe_users(user_ids=manager.user_id.ids)
        return department

    @api.multi
    def write(self, vals):
        """ If updating manager of a department, we need to update all the employees
            of department hierarchy, and subscribe the new manager.
        """
        # TDE note: auto-subscription of manager done by hand, because currently
        # the tracking allows to track+subscribe fields linked to a res.user record
        # An update of the limited behavior should come, but not currently done.
        if 'manager_id' in vals:
            manager_id = vals.get("manager_id")
            if manager_id:
                manager = self.env['hr.employee'].browse(manager_id)
                # subscribe the manager user
                if manager.user_id:
                    self.message_subscribe_users(user_ids=manager.user_id.ids)
            employees = self.env['hr.employee']
            for department in self:
                employees = employees | self.env['hr.employee'].search(
                    [('id', '!=', manager_id),
                     ('department_id', '=', department.id),
                     ('parent_id', '=', department.manager_id.id)])
            employees.write({'parent_id': manager_id})
        return super(Department, self).write(vals)
예제 #4
0
class MisReportInstancePeriod(models.Model):
    """ A MIS report instance has the logic to compute
    a report template for a given date period.

    Periods have a duration (day, week, fiscal period) and
    are defined as an offset relative to a pivot date.
    """
    @api.depends(
        "report_instance_id.pivot_date",
        "report_instance_id.comparison_mode",
        "date_range_type_id",
        "type",
        "offset",
        "duration",
        "mode",
        "manual_date_from",
        "manual_date_to",
        "is_ytd",
    )
    def _compute_dates(self):
        for record in self:
            record.date_from = False
            record.date_to = False
            record.valid = False
            report = record.report_instance_id
            d = fields.Date.from_string(report.pivot_date)
            if not report.comparison_mode:
                record.date_from = report.date_from
                record.date_to = report.date_to
                record.valid = record.date_from and record.date_to
            elif record.mode == MODE_NONE:
                record.date_from = False
                record.date_to = False
                record.valid = True
            elif record.mode == MODE_FIX:
                record.date_from = record.manual_date_from
                record.date_to = record.manual_date_to
                record.valid = record.date_from and record.date_to
            elif record.mode == MODE_REL and record.type == "d":
                date_from = d + datetime.timedelta(days=record.offset)
                date_to = date_from + datetime.timedelta(days=record.duration -
                                                         1)
                record.date_from = fields.Date.to_string(date_from)
                record.date_to = fields.Date.to_string(date_to)
                record.valid = True
            elif record.mode == MODE_REL and record.type == "w":
                date_from = d - datetime.timedelta(d.weekday())
                date_from = date_from + datetime.timedelta(days=record.offset *
                                                           7)
                date_to = date_from + datetime.timedelta(
                    days=(7 * record.duration) - 1)
                record.date_from = fields.Date.to_string(date_from)
                record.date_to = fields.Date.to_string(date_to)
                record.valid = True
            elif record.mode == MODE_REL and record.type == "m":
                date_from = d.replace(day=1)
                date_from = date_from + relativedelta(months=record.offset)
                date_to = (date_from +
                           relativedelta(months=record.duration - 1) +
                           relativedelta(day=31))
                record.date_from = fields.Date.to_string(date_from)
                record.date_to = fields.Date.to_string(date_to)
                record.valid = True
            elif record.mode == MODE_REL and record.type == "y":
                date_from = d.replace(month=1, day=1)
                date_from = date_from + relativedelta(years=record.offset)
                date_to = date_from + relativedelta(years=record.duration - 1)
                date_to = date_to.replace(month=12, day=31)
                record.date_from = fields.Date.to_string(date_from)
                record.date_to = fields.Date.to_string(date_to)
                record.valid = True
            elif record.mode == MODE_REL and record.type == "date_range":
                date_range_obj = record.env["date.range"]
                current_periods = date_range_obj.search([
                    ("type_id", "=", record.date_range_type_id.id),
                    ("date_start", "<=", d),
                    ("date_end", ">=", d),
                    "|",
                    ("company_id", "=", False),
                    (
                        "company_id",
                        "in",
                        record.report_instance_id.query_company_ids.ids,
                    ),
                ])
                if current_periods:
                    # TODO we take the first date range we found as current
                    #      this may be surprising if several companies
                    #      have overlapping date ranges with different dates
                    current_period = current_periods[0]
                    all_periods = date_range_obj.search(
                        [
                            ("type_id", "=", current_period.type_id.id),
                            ("company_id", "=", current_period.company_id.id),
                        ],
                        order="date_start",
                    )
                    p = all_periods.ids.index(
                        current_period.id) + record.offset
                    if p >= 0 and p + record.duration <= len(all_periods):
                        periods = all_periods[p:p + record.duration]
                        record.date_from = periods[0].date_start
                        record.date_to = periods[-1].date_end
                        record.valid = True
            if record.mode == MODE_REL and record.valid and record.is_ytd:
                record.date_from = fields.Date.from_string(
                    record.date_to).replace(day=1, month=1)

    _name = "mis.report.instance.period"
    _description = "MIS Report Instance Period"

    name = fields.Char(size=32, required=True, string="Label", translate=True)
    mode = fields.Selection(
        [
            (MODE_FIX, "Fixed dates"),
            (MODE_REL, "Relative to report base date"),
            (MODE_NONE, "No date filter"),
        ],
        required=True,
        default=MODE_FIX,
    )
    type = fields.Selection(
        [
            ("d", _("Day")),
            ("w", _("Week")),
            ("m", _("Month")),
            ("y", _("Year")),
            ("date_range", _("Date Range")),
        ],
        string="Period type",
    )
    is_ytd = fields.Boolean(
        default=False,
        string="Year to date",
        help="Forces the start date to Jan 1st of the relevant year",
    )
    date_range_type_id = fields.Many2one(
        comodel_name="date.range.type",
        string="Date Range Type",
        domain=[("allow_overlap", "=", False)],
    )
    offset = fields.Integer(string="Offset",
                            help="Offset from current period",
                            default=-1)
    duration = fields.Integer(string="Duration",
                              help="Number of periods",
                              default=1)
    date_from = fields.Date(compute="_compute_dates", string="From (computed)")
    date_to = fields.Date(compute="_compute_dates", string="To (computed)")
    manual_date_from = fields.Date(string="From")
    manual_date_to = fields.Date(string="To")
    date_range_id = fields.Many2one(comodel_name="date.range",
                                    string="Date Range")
    valid = fields.Boolean(compute="_compute_dates",
                           type="boolean",
                           string="Valid")
    sequence = fields.Integer(string="Sequence", default=100)
    report_instance_id = fields.Many2one(
        comodel_name="mis.report.instance",
        string="Report Instance",
        required=True,
        ondelete="cascade",
    )
    report_id = fields.Many2one(related="report_instance_id.report_id")
    normalize_factor = fields.Integer(
        string="Factor",
        help="Factor to use to normalize the period (used in comparison",
        default=1,
    )
    subkpi_ids = fields.Many2many("mis.report.subkpi", string="Sub KPI Filter")

    source = fields.Selection(
        [
            (SRC_ACTUALS, "Actuals"),
            (SRC_ACTUALS_ALT, "Actuals (alternative)"),
            (SRC_SUMCOL, "Sum columns"),
            (SRC_CMPCOL, "Compare columns"),
        ],
        default=SRC_ACTUALS,
        required=True,
        help="Actuals: current data, from accounting and other queries.\n"
        "Actuals (alternative): current data from an "
        "alternative source (eg a database view providing look-alike "
        "account move lines).\n"
        "Sum columns: summation (+/-) of other columns.\n"
        "Compare to column: compare to other column.\n",
    )
    source_aml_model_id = fields.Many2one(
        comodel_name="ir.model",
        string="Move lines source",
        domain=[
            ("field_id.name", "=", "debit"),
            ("field_id.name", "=", "credit"),
            ("field_id.name", "=", "account_id"),
            ("field_id.name", "=", "date"),
            ("field_id.name", "=", "company_id"),
            ("field_id.model_id.model", "!=", "account.move.line"),
        ],
        help="A 'move line like' model, ie having at least debit, credit, "
        "date, account_id and company_id fields.",
    )
    source_aml_model_name = fields.Char(string="Move lines source model name",
                                        related="source_aml_model_id.model")
    source_sumcol_ids = fields.One2many(
        comodel_name="mis.report.instance.period.sum",
        inverse_name="period_id",
        string="Columns to sum",
    )
    source_sumcol_accdet = fields.Boolean(string="Sum account details")
    source_cmpcol_from_id = fields.Many2one(
        comodel_name="mis.report.instance.period", string="versus")
    source_cmpcol_to_id = fields.Many2one(
        comodel_name="mis.report.instance.period", string="Compare")
    # filters
    analytic_account_id = fields.Many2one(
        comodel_name="account.analytic.account",
        string="Analytic Account",
        help=(
            "Filter column on journal entries that match this analytic account."
            "This filter is combined with a AND with the report-level filters "
            "and cannot be modified in the preview."),
    )
    analytic_tag_ids = fields.Many2many(
        comodel_name="account.analytic.tag",
        string="Analytic Tags",
        help=
        ("Filter column on journal entries that have all these analytic tags."
         "This filter is combined with a AND with the report-level filters "
         "and cannot be modified in the preview."),
    )

    _order = "sequence, id"

    _sql_constraints = [
        ("duration", "CHECK (duration>0)",
         "Wrong duration, it must be positive!"),
        (
            "normalize_factor",
            "CHECK (normalize_factor>0)",
            "Wrong normalize factor, it must be positive!",
        ),
        (
            "name_unique",
            "unique(name, report_instance_id)",
            "Period name should be unique by report",
        ),
    ]

    @api.constrains("source_aml_model_id")
    def _check_source_aml_model_id(self):
        for record in self:
            if record.source_aml_model_id:
                record_model = record.source_aml_model_id.field_id.filtered(
                    lambda r: r.name == "account_id").relation
                report_account_model = record.report_id.account_model
                if record_model != report_account_model:
                    raise ValidationError(
                        _("Actual (alternative) models used in columns must "
                          "have the same account model in the Account field and must "
                          "be the same defined in the "
                          "report template: %s") % report_account_model)

    @api.onchange("date_range_id")
    def _onchange_date_range(self):
        if self.date_range_id:
            self.manual_date_from = self.date_range_id.date_start
            self.manual_date_to = self.date_range_id.date_end

    @api.onchange("manual_date_from", "manual_date_to")
    def _onchange_dates(self):
        if self.date_range_id:
            if (self.manual_date_from != self.date_range_id.date_start
                    or self.manual_date_to != self.date_range_id.date_end):
                self.date_range_id = False

    @api.onchange("source")
    def _onchange_source(self):
        if self.source in (SRC_SUMCOL, SRC_CMPCOL):
            self.mode = MODE_NONE

    def _get_aml_model_name(self):
        self.ensure_one()
        if self.source == SRC_ACTUALS:
            return self.report_id.move_lines_source.model
        elif self.source == SRC_ACTUALS_ALT:
            return self.source_aml_model_name
        return False

    @api.model
    def _get_filter_domain_from_context(self):
        filters = []
        mis_report_filters = self.env.context.get("mis_report_filters", {})
        for filter_name, domain in mis_report_filters.items():
            if domain:
                value = domain.get("value")
                operator = domain.get("operator", "=")
                # Operator = 'all' when coming from JS widget
                if operator == "all":
                    if not isinstance(value, list):
                        value = [value]
                    many_ids = self.report_instance_id.resolve_2many_commands(
                        filter_name, value, ["id"])
                    for m in many_ids:
                        filters.append((filter_name, "in", [m["id"]]))
                else:
                    filters.append((filter_name, operator, value))
        return filters

    def _get_additional_move_line_filter(self):
        """ Prepare a filter to apply on all move lines

        This filter is applied with a AND operator on all
        accounting expression domains. This hook is intended
        to be inherited, and is useful to implement filtering
        on analytic dimensions or operational units.

        The default filter is built from a ``mis_report_filters`` context
        key, which is a list set by the analytic filtering mechanism
        of the mis report widget::

          [(field_name, {'value': value, 'operator': operator})]

        Returns an Odoo domain expression (a python list)
        compatible with account.move.line."""
        self.ensure_one()
        domain = self._get_filter_domain_from_context()
        if (self._get_aml_model_name() == "account.move.line"
                and self.report_instance_id.target_move == "posted"):
            domain.extend([("move_id.state", "=", "posted")])
        if self.analytic_account_id:
            domain.append(
                ("analytic_account_id", "=", self.analytic_account_id.id))
        for tag in self.analytic_tag_ids:
            domain.append(("analytic_tag_ids", "=", tag.id))
        return domain

    def _get_additional_query_filter(self, query):
        """ Prepare an additional filter to apply on the query

        This filter is combined to the query domain with a AND
        operator. This hook is intended
        to be inherited, and is useful to implement filtering
        on analytic dimensions or operational units.

        Returns an Odoo domain expression (a python list)
        compatible with the model of the query."""
        self.ensure_one()
        return []

    @api.constrains("mode", "source")
    def _check_mode_source(self):
        for rec in self:
            if rec.source in (SRC_ACTUALS, SRC_ACTUALS_ALT):
                if rec.mode == MODE_NONE:
                    raise DateFilterRequired(
                        _("A date filter is mandatory for this source "
                          "in column %s.") % rec.name)
            elif rec.source in (SRC_SUMCOL, SRC_CMPCOL):
                if rec.mode != MODE_NONE:
                    raise DateFilterForbidden(
                        _("No date filter is allowed for this source "
                          "in column %s.") % rec.name)

    @api.constrains("source", "source_cmpcol_from_id", "source_cmpcol_to_id")
    def _check_source_cmpcol(self):
        for rec in self:
            if rec.source == SRC_CMPCOL:
                if not rec.source_cmpcol_from_id or not rec.source_cmpcol_to_id:
                    raise ValidationError(
                        _("Please provide both columns to compare in %s.") %
                        rec.name)
                if rec.source_cmpcol_from_id == rec or rec.source_cmpcol_to_id == rec:
                    raise ValidationError(
                        _("Column %s cannot be compared to itrec.") % rec.name)
                if (rec.source_cmpcol_from_id.report_instance_id !=
                        rec.report_instance_id
                        or rec.source_cmpcol_to_id.report_instance_id !=
                        rec.report_instance_id):
                    raise ValidationError(
                        _("Columns to compare must belong to the same report "
                          "in %s") % rec.name)
예제 #5
0
class rrhh_prestamo(models.Model):
    _name = 'rrhh.prestamo'
    _rec_name = 'descripcion'

    employee_id = fields.Many2one('hr.employee', 'Empleado')
    fecha_inicio = fields.Date('Fecha inicio')
    numero_descuentos = fields.Integer('Numero de descuentos')
    total = fields.Float('Total')
    mensualidad = fields.Float('Mensualidad')
    prestamo_ids = fields.One2many('rrhh.prestamo.linea',
                                   'prestamo_id',
                                   string='Lineas de prestamo')
    descripcion = fields.Char(string='Descripción', required=True)
    codigo = fields.Char(string='Código', required=True)
    estado = fields.Selection([('nuevo', 'Nuevo'), ('proceso', 'Proceso'),
                               ('pagado', 'Pagado')],
                              string='Status',
                              help='Estado del prestamo',
                              readonly=True,
                              default='nuevo')
    pendiente_pagar_prestamo = fields.Float(
        compute='_compute_prestamo',
        string='Pendiente a pagar del prestamos',
    )

    def _compute_prestamo(self):
        for prestamo in self:
            total_prestamo = 0
            total_prestamo_pagado = 0
            for linea in prestamo.prestamo_ids:
                for nomina in linea.nomina_id:
                    for nomina_entrada in nomina.input_line_ids:
                        if prestamo.codigo == nomina_entrada.code:
                            total_prestamo_pagado += nomina_entrada.amount
                total_prestamo += linea.monto
            prestamo.pendiente_pagar_prestamo = total_prestamo - total_prestamo_pagado
            if prestamo.pendiente_pagar_prestamo == 0:
                prestamo.estado = 'pagado'
            return True

    def generar_mensualidades(self):
        mes_inicial = datetime.datetime.strptime(str(self.fecha_inicio),
                                                 '%Y-%m-%d').date()
        mes = 0
        if self.mensualidad > 0 and self.numero_descuentos > 0:
            total = self.mensualidad * self.numero_descuentos
            if self.mensualidad <= self.total:
                numero_pagos_mensualidad = self.total / self.mensualidad
                mes_final_pagos_mensuales = mes_inicial + relativedelta(
                    months=int(numero_pagos_mensualidad) - 1)
                anio_final = mes_final_pagos_mensuales.strftime('%Y')
                diferencias_meses = self.numero_descuentos - int(
                    numero_pagos_mensualidad)
                contador = 0
                if diferencias_meses < 0:
                    total_sumado = 0
                    diferencia = (diferencias_meses *
                                  -1) + self.numero_descuentos
                    while contador <= (self.numero_descuentos - 1):
                        mes = mes_inicial + relativedelta(months=contador)
                        anio = mes.strftime('%Y')
                        mes = int(mes.strftime('%m'))
                        if contador < (self.numero_descuentos - 1):
                            total_sumado += self.mensualidad
                            self.env['rrhh.prestamo.linea'].create({
                                'prestamo_id':
                                self.id,
                                'mes':
                                mes,
                                'anio':
                                anio,
                                'monto':
                                self.mensualidad
                            })
                        else:
                            pago_restante = self.total - total_sumado
                            ultimos_pagos_mensuales = pago_restante / diferencias_meses
                            self.env['rrhh.prestamo.linea'].create({
                                'prestamo_id':
                                self.id,
                                'mes':
                                mes,
                                'anio':
                                anio,
                                'monto':
                                pago_restante
                            })
                        contador += 1
                else:
                    while contador < (self.numero_descuentos):
                        mes = mes_inicial + relativedelta(months=contador)
                        anio = mes.strftime('%Y')
                        mes = int(mes.strftime('%m'))
                        if contador <= (int(numero_pagos_mensualidad) - 1):
                            self.env['rrhh.prestamo.linea'].create({
                                'prestamo_id':
                                self.id,
                                'mes':
                                mes,
                                'anio':
                                anio,
                                'monto':
                                self.mensualidad
                            })
                        else:
                            pago_restante = self.total % self.mensualidad
                            ultimos_pagos_mensuales = pago_restante / diferencias_meses
                            logging.warn(ultimos_pagos_mensuales)
                            self.env['rrhh.prestamo.linea'].create({
                                'prestamo_id':
                                self.id,
                                'mes':
                                mes,
                                'anio':
                                anio,
                                'monto':
                                ultimos_pagos_mensuales
                            })
                        contador += 1
        return True

    def prestamos(self):
        if self.prestamo_ids:
            cantidad_nominas = 0
            for nomina in self.prestamo_ids:
                if nomina.nomina_id:
                    cantidad_nominas += 1
            if cantidad_nominas == 0:
                self.prestamo_ids.unlink()
                self.generar_mensualidades()
            else:
                raise ValidationError(
                    _('No puede volver a generar mensualidades, por que ya existen nominas asociadas a este prestamo.'
                      ))
        else:
            self.generar_mensualidades()
        return True

    @api.multi
    def unlink(self):
        for prestamo in self:
            if not prestamo.estado == 'nuevo':
                raise UserError(
                    _('No puede eliminar prestamo, por que ya existen nominas asociadas'
                      ))
        return super(hr_prestamo, self).unlink()
예제 #6
0
class Channel(models.Model):
    """ A channel is a container of slides. """
    _name = 'slide.channel'
    _description = 'Slide Channel'
    _inherit = [
        'mail.thread', 'website.seo.metadata', 'website.published.multi.mixin',
        'rating.mixin'
    ]
    _order = 'sequence, id'

    def _default_access_token(self):
        return str(uuid.uuid4())

    # description
    name = fields.Char('Name', translate=True, required=True)
    active = fields.Boolean(default=True)
    description = fields.Html('Description',
                              translate=html_translate,
                              sanitize_attributes=False)
    channel_type = fields.Selection([('documentation', 'Documentation'),
                                     ('training', 'Training')],
                                    string="Course type",
                                    default="documentation",
                                    required=True)
    sequence = fields.Integer(default=10, help='Display order')
    user_id = fields.Many2one('res.users',
                              string='Responsible',
                              default=lambda self: self.env.uid)
    tag_ids = fields.Many2many(
        'slide.channel.tag',
        'slide_channel_tag_rel',
        'channel_id',
        'tag_id',
        string='Tags',
        help='Used to categorize and filter displayed channels/courses')
    category_ids = fields.One2many('slide.category',
                                   'channel_id',
                                   string="Categories")
    image = fields.Binary("Image", attachment=True)
    image_medium = fields.Binary("Medium image", attachment=True)
    image_small = fields.Binary("Small image", attachment=True)
    # slides: promote, statistics
    slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides")
    slide_partner_ids = fields.One2many(
        'slide.slide.partner',
        'channel_id',
        string="Slide User Data",
        groups='website.group_website_publisher')
    promote_strategy = fields.Selection([('none', 'No Featured Presentation'),
                                         ('latest', 'Latest Published'),
                                         ('most_voted', 'Most Voted'),
                                         ('most_viewed', 'Most Viewed'),
                                         ('custom', 'Featured Presentation')],
                                        string="Featuring Policy",
                                        default='most_voted',
                                        required=True)

    access_token = fields.Char("Security Token",
                               copy=False,
                               default=_default_access_token)
    nbr_presentations = fields.Integer('Number of Presentations',
                                       compute='_compute_slides_statistics',
                                       store=True)
    nbr_documents = fields.Integer('Number of Documents',
                                   compute='_compute_slides_statistics',
                                   store=True)
    nbr_videos = fields.Integer('Number of Videos',
                                compute='_compute_slides_statistics',
                                store=True)
    nbr_infographics = fields.Integer('Number of Infographics',
                                      compute='_compute_slides_statistics',
                                      store=True)
    nbr_webpages = fields.Integer("Number of Webpages",
                                  compute='_compute_slides_statistics',
                                  store=True)
    total_slides = fields.Integer('# Slides',
                                  compute='_compute_slides_statistics',
                                  store=True,
                                  oldname='total')
    total_views = fields.Integer('# Views',
                                 compute='_compute_slides_statistics',
                                 store=True)
    total_votes = fields.Integer('# Votes',
                                 compute='_compute_slides_statistics',
                                 store=True)
    total_time = fields.Float('# Hours',
                              compute='_compute_slides_statistics',
                              digits=(10, 4),
                              store=True)
    # configuration
    publish_template_id = fields.Many2one(
        'mail.template',
        string='Published Template',
        help="Email template to send slide publication through email",
        default=lambda self: self.env['ir.model.data'].xmlid_to_res_id(
            'website_slides.slide_template_published'))
    share_template_id = fields.Many2one(
        'mail.template',
        string='Shared Template',
        help="Email template used when sharing a slide",
        default=lambda self: self.env['ir.model.data'].xmlid_to_res_id(
            'website_slides.slide_template_shared'))
    visibility = fields.Selection([('public', 'Public'), ('invite', 'Invite')],
                                  default='public',
                                  required=True)
    partner_ids = fields.Many2many('res.partner',
                                   'slide_channel_partner',
                                   'channel_id',
                                   'partner_id',
                                   string='Members',
                                   help="All members of the channel.",
                                   context={'active_test': False})
    members_count = fields.Integer('Attendees count',
                                   compute='_compute_members_count')
    is_member = fields.Boolean(string='Is Member',
                               compute='_compute_is_member')
    channel_partner_ids = fields.One2many(
        'slide.channel.partner',
        'channel_id',
        string='Members Information',
        groups='website.group_website_publisher')
    enroll_msg = fields.Html('Enroll Message',
                             help="Message explaining the enroll process",
                             default=False,
                             translate=html_translate,
                             sanitize_attributes=False)
    upload_group_ids = fields.Many2many(
        'res.groups',
        'rel_upload_groups',
        'channel_id',
        'group_id',
        string='Upload Groups',
        help=
        "Groups allowed to upload presentations in this channel. If void, every user can upload."
    )
    # not stored access fields, depending on each user
    completed = fields.Boolean('Done', compute='_compute_user_statistics')
    completion = fields.Integer('Completion',
                                compute='_compute_user_statistics')
    can_upload = fields.Boolean('Can Upload', compute='_compute_access')
    can_publish = fields.Boolean('Can Publish', compute='_compute_access')

    @api.depends('channel_partner_ids.channel_id')
    def _compute_members_count(self):
        read_group_res = self.env['slide.channel.partner'].sudo().read_group(
            [('channel_id', 'in', self.ids)], [], 'channel_id')
        data = dict((res['channel_id'][0], res['channel_id_count'])
                    for res in read_group_res)
        for channel in self:
            channel.members_count = data.get(channel.id, 0)

    @api.depends('channel_partner_ids.partner_id')
    @api.model
    def _compute_is_member(self):
        channel_partners = self.env['slide.channel.partner'].sudo().search([
            ('channel_id', 'in', self.ids),
        ])
        result = dict()
        for cp in channel_partners:
            result.setdefault(cp.channel_id.id, []).append(cp.partner_id.id)
        for channel in self:
            channel.valid_channel_partner_ids = result.get(channel.id, False)
            channel.is_member = self.env.user.partner_id.id in channel.valid_channel_partner_ids if channel.valid_channel_partner_ids else False

    @api.depends('slide_ids.slide_type', 'slide_ids.is_published',
                 'slide_ids.likes', 'slide_ids.dislikes',
                 'slide_ids.total_views')
    def _compute_slides_statistics(self):
        result = dict.fromkeys(
            self.ids,
            dict(nbr_presentations=0,
                 nbr_documents=0,
                 nbr_videos=0,
                 nbr_infographics=0,
                 nbr_webpages=0,
                 total_slides=0,
                 total_views=0,
                 total_votes=0,
                 total_time=0))
        read_group_res = self.env['slide.slide'].read_group(
            [('is_published', '=', True), ('channel_id', 'in', self.ids)], [
                'channel_id', 'slide_type', 'likes', 'dislikes', 'total_views',
                'completion_time'
            ],
            groupby=['channel_id', 'slide_type'],
            lazy=False)
        for res_group in read_group_res:
            cid = res_group['channel_id'][0]
            result[cid]['nbr_presentations'] += res_group.get(
                'slide_type',
                '') == 'presentation' and res_group['__count'] or 0
            result[cid]['nbr_documents'] += res_group.get(
                'slide_type', '') == 'document' and res_group['__count'] or 0
            result[cid]['nbr_videos'] += res_group.get(
                'slide_type', '') == 'video' and res_group['__count'] or 0
            result[cid]['nbr_infographics'] += res_group.get(
                'slide_type',
                '') == 'infographic' and res_group['__count'] or 0
            result[cid]['nbr_webpages'] += res_group.get(
                'slide_type', '') == 'webpage' and res_group['__count'] or 0
            result[cid]['total_slides'] += res_group['__count']
            result[cid]['total_views'] += res_group.get('total_views', 0)
            result[cid]['total_votes'] += res_group.get('likes', 0)
            result[cid]['total_votes'] -= res_group.get('dislikes', 0)
            result[cid]['total_time'] += res_group.get('completion_time', 0)
        for record in self:
            record.update(result[record.id])

    @api.depends('slide_partner_ids')
    def _compute_user_statistics(self):
        current_user_info = self.env['slide.channel.partner'].sudo().search([
            ('channel_id', 'in', self.ids),
            ('partner_id', '=', self.env.user.partner_id.id)
        ])
        mapped_data = dict(
            (info.channel_id.id, (info.completed, info.completion))
            for info in current_user_info)
        for record in self:
            record.completed, record.completion = mapped_data.get(
                record.id, (False, 0))

    @api.depends('channel_type', 'partner_ids', 'upload_group_ids')
    def _compute_access(self):
        for record in self:
            is_doc = record.channel_type == 'documentation'
            record.can_upload = not self.env.user.share and (
                not record.upload_group_ids
                or bool(record.upload_group_ids & self.env.user.groups_id))
            record.can_publish = record.can_upload and self.env.user.has_group(
                'website.group_website_publisher') and (
                    is_doc or record.user_id == self.env.user)

    @api.multi
    @api.depends('name')
    def _compute_website_url(self):
        super(Channel, self)._compute_website_url()
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        for channel in self:
            if channel.id:  # avoid to perform a slug on a not yet saved record in case of an onchange.
                channel.website_url = '%s/slides/%s' % (base_url,
                                                        slug(channel))

    @api.onchange('visibility')
    def change_visibility(self):
        pass

    @api.multi
    def action_redirect_to_members(self):
        action = self.env.ref(
            'website_slides.slide_channel_partner_action').read()[0]
        action['view_mode'] = 'tree'
        action['domain'] = [('channel_id', 'in', self.ids)]
        if len(self) == 1:
            action['context'] = {'default_channel_id': self.id}

        return action

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

        if self.visibility != 'invite':
            raise UserError(
                _("You cannot send invitations for channels that are not set as 'invite'."
                  ))

        template = self.env.ref(
            'website_slides.mail_template_slide_channel_invite',
            raise_if_not_found=False)

        local_context = dict(
            self.env.context,
            default_channel_id=self.id,
            default_use_template=bool(template),
            default_template_id=template and template.id or False,
            notif_layout='mail.mail_notification_light',
        )
        return {
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'slide.channel.invite',
            'target': 'new',
            'context': local_context,
        }

    # ---------------------------------------------------------
    # ORM Overrides
    # ---------------------------------------------------------

    @api.model_cr_context
    def _init_column(self, column_name):
        """ Initialize the value of the given column for existing rows.
            Overridden here because we need to generate different access tokens
            and by default _init_column calls the default method once and applies
            it for every record.
        """
        if column_name != 'access_token':
            super(Channel, self)._init_column(column_name)
        else:
            query = """
                UPDATE %(table_name)s
                SET %(column_name)s = md5(md5(random()::varchar || id::varchar) || clock_timestamp()::varchar)::uuid::varchar
                WHERE %(column_name)s IS NULL
            """ % {
                'table_name': self._table,
                'column_name': column_name
            }
            self.env.cr.execute(query)

    @api.model
    def create(self, vals):
        # Ensure creator is member of its channel it is easier for him to manage it
        if not vals.get('channel_partner_ids'):
            vals['channel_partner_ids'] = [(0, 0, {
                'partner_id':
                self.env.user.partner_id.id
            })]
        if 'image' in vals:
            tools.image_resize_images(vals)
        return super(
            Channel,
            self.with_context(mail_create_nosubscribe=True)).create(vals)

    @api.multi
    def write(self, vals):
        if 'image' in vals:
            tools.image_resize_images(vals)
        res = super(Channel, self).write(vals)
        if 'active' in vals:
            # archiving/unarchiving a channel does it on its slides, too
            self.with_context(active_test=False).mapped('slide_ids').write(
                {'active': vals['active']})
        return res

    @api.multi
    @api.returns('mail.message', lambda value: value.id)
    def message_post(self, parent_id=False, subtype=None, **kwargs):
        """ Temporary workaround to avoid spam. If someone replies on a channel
        through the 'Presentation Published' email, it should be considered as a
        note as we don't want all channel followers to be notified of this answer. """
        self.ensure_one()
        if parent_id:
            parent_message = self.env['mail.message'].sudo().browse(parent_id)
            if parent_message.subtype_id and parent_message.subtype_id == self.env.ref(
                    'website_slides.mt_channel_slide_published'):
                if kwargs.get('subtype_id'):
                    kwargs['subtype_id'] = False
                subtype = 'mail.mt_note'
        return super(Channel, self).message_post(parent_id=parent_id,
                                                 subtype=subtype,
                                                 **kwargs)

    # ---------------------------------------------------------
    # Rating Mixin API
    # ---------------------------------------------------------

    def action_add_member(self, **member_values):
        """ Adds the logged in user in the channel members.
        (see '_action_add_member' for more info)

        Returns True if added successfully, False otherwise."""
        return bool(
            self._action_add_member(target_partner=self.env.user.partner_id,
                                    **member_values))

    def _action_add_member(self, target_partner=False, **member_values):
        """ Add the target_partner as a member of the channel (to its slide.channel.partner).
        This will make the content (slides) of the channel available to that partner.

        Returns the added 'slide.channel.partner's (! as sudo !)
        """
        existing = self.env['slide.channel.partner'].sudo().search([
            ('channel_id', 'in', self.ids),
            ('partner_id', '=', target_partner.id)
        ])
        to_join = (self - existing.mapped('channel_id'))._filter_add_member(
            target_partner, **member_values)
        if to_join:
            slide_partners_sudo = self.env['slide.channel.partner'].sudo(
            ).create([
                dict(channel_id=channel.id,
                     partner_id=target_partner.id,
                     **member_values) for channel in to_join
            ])
            return slide_partners_sudo
        return self.env['slide.channel.partner'].sudo()

    def _filter_add_member(self, target_partner, **member_values):
        allowed = self.filtered(lambda channel: channel.visibility == 'public')
        on_invite = self.filtered(
            lambda channel: channel.visibility == 'invite')
        if on_invite:
            try:
                on_invite.check_access_rights('write')
                on_invite.check_access_rule('write')
            except:
                pass
            else:
                allowed |= on_invite
        return allowed

    def list_all(self):
        return {
            'channels': [{
                'id': channel.id,
                'name': channel.name,
                'website_url': channel.website_url
            } for channel in self.search([])]
        }

    # ---------------------------------------------------------
    # Rating Mixin API
    # ---------------------------------------------------------

    @api.multi
    def _rating_domain(self):
        """ Only take the published rating into account to compute avg and count """
        domain = super(Channel, self)._rating_domain()
        return expression.AND([domain, [('website_published', '=', True)]])
예제 #7
0
class ResPartner(models.Model):
    _inherit = "res.partner"

    arba_alicuot_ids = fields.One2many(
        'res.partner.arba_alicuot',
        'partner_id',
        'Alicuotas de ARBA',
    )
    drei = fields.Selection(
        [
            ('activo', 'Activo'),
            ('no_activo', 'No Activo'),
        ],
        string='DREI',
    )
    # TODO agregarlo en mig a v10 ya que fix dbs no es capaz de arreglarlo
    # porque da el error antes de empezar a arreglar
    # drei_number = fields.Char(
    # )
    default_regimen_ganancias_id = fields.Many2one(
        'afip.tabla_ganancias.alicuotasymontos',
        'Regimen Ganancias por Defecto',
    )

    @api.multi
    def get_arba_alicuota_percepcion(self, alicuot_no_inscripto=False):
        company = self._context.get('invoice_company')
        date_invoice = self._context.get('date_invoice')
        if date_invoice and company:
            date = fields.Date.from_string(date_invoice)
            arba = self.get_arba_data(company, date)
            # si pasamos alicuota para no inscripto y no hay numero de
            # comprobante entonces es porque no figura en el padron
            if alicuot_no_inscripto and not arba.numero_comprobante:
                return alicuot_no_inscripto
            return arba.alicuota_percepcion / 100.0
        return 0

    @api.multi
    def get_arba_alicuota_retencion(self, company, date):
        arba = self.get_arba_data(company, date)
        return arba.alicuota_retencion / 100.0

    @api.multi
    def get_arba_data(self, company, date):
        self.ensure_one()
        from_date = (date + relativedelta(day=1)).strftime('%Y%m%d')
        to_date = (date +
                   relativedelta(day=1, days=-1, months=+1)).strftime('%Y%m%d')
        commercial_partner = self.commercial_partner_id
        arba = self.arba_alicuot_ids.search(
            [('from_date', '=', from_date), ('to_date', '=', to_date),
             ('company_id', '=', company.id),
             ('partner_id', '=', commercial_partner.id)],
            limit=1)
        if not arba:
            arba_data = company.get_arba_data(
                commercial_partner,
                from_date,
                to_date,
            )
            arba_data['partner_id'] = commercial_partner.id
            arba_data['company_id'] = company.id
            arba = self.arba_alicuot_ids.sudo().create(arba_data)
        return arba
예제 #8
0
class Erups(models.Model):
    _name = 'erups'

    name = fields.Char(string='Nama Kegiatan', required=True)
    emitten = fields.Char()
    event_date = fields.Date(string='Tanggal Event')
    location = fields.Text(string='Lokasi Event')
    agenda_ids = fields.One2many('erups_agenda', 'erups_id')
    description = fields.Text()
    status = fields.Selection([('open', 'Open'), ('close', 'Closed')],
                              required=True,
                              default="open")

    total_agenda = fields.Integer('Jumlah Agenda',
                                  compute='_compute_total_agenda')
    total_question = fields.Integer('Jumlah Pertanyaan',
                                    compute='_compute_total_question')
    total_question_valid = fields.Integer('Jumlah Pertanyaan Valid',
                                          compute='_compute_total_valid')
    total_question_relevan = fields.Integer('Jumlah Pertanyaan Relevan',
                                            compute='_compute_total_relevan')
    total_question_verified = fields.Integer('Jumlah Pertanyaan Dipilih',
                                             compute='_compute_total_verified')

    @api.depends('agenda_ids')
    def _compute_total_agenda(self):
        for r in self:
            if r.agenda_ids:
                r.total_agenda = len(r.agenda_ids)
            else:
                r.total_agenda = 0

    @api.depends('agenda_ids')
    def _compute_total_question(self):
        for r in self:
            if r.agenda_ids:
                for a in r.agenda_ids:
                    r.total_question += len(a.question_ids)
            else:
                r.total_question = 0

    @api.depends('agenda_ids')
    def _compute_total_valid(self):
        for r in self:
            r.total_question_valid = 0
            if r.agenda_ids:
                for a in r.agenda_ids:
                    if a.question_ids:
                        for q in a.question_ids:
                            if q.status not in ['shareholder', 'bae_reject']:
                                r.total_question_valid += 1

    @api.depends('agenda_ids')
    def _compute_total_relevan(self):
        for r in self:
            r.total_question_relevan = 0
            if r.agenda_ids:
                for a in r.agenda_ids:
                    if a.question_ids:
                        for q in a.question_ids:
                            if q.status in ['consultant', 'speaker']:
                                r.total_question_relevan += 1

    @api.depends('agenda_ids')
    def _compute_total_verified(self):
        for r in self:
            if r.agenda_ids:
                for a in r.agenda_ids:
                    r.total_question_verified = a.search_count(
                        [['question_ids.status', '=', 'speaker']])
            else:
                r.total_question_verified = 0
예제 #9
0
class Agenda(models.Model):
    _name = 'erups_agenda'
    # _rec_name = 'complete_name'

    name = fields.Char(string='Nama Agenda', required=True)
    agenda_num = fields.Integer(string='No Urut')
    description = fields.Text()
    erups_id = fields.Many2one('erups', ondelete='cascade', required=True)
    status = fields.Selection([('open', 'Open'), ('close', 'Closed'),
                               ('done', 'Done')],
                              required=True,
                              default="open")

    question_ids = fields.One2many('erups_question', 'agenda_id')
    total_question = fields.Integer('Jumlah Pertanyaan',
                                    compute='_compute_total_question')
    total_question_valid = fields.Integer('Jumlah Pertanyaan Valid',
                                          compute='_compute_total_valid')
    total_question_not_valid = fields.Integer(
        'Jumlah Pertanyaan Tidak Valid', compute='_compute_total_not_valid')

    # total_notaris_relevan = fields.Integer('Jumlah Pertanyaan Notaris Relevan', compute='_compute_total_notaris_relevan')
    # total_notaris_not_relevan = fields.Integer('Jumlah Pertanyaan Notaris Not Relevan',
    #                                        compute='_compute_total_notaris_not_relevan')

    total_consultant_relevan = fields.Integer(
        'Jumlah Pertanyaan Konsultan Relevan',
        compute='_compute_total_consultant_relevan')
    total_consultant_not_relevan = fields.Integer(
        'Jumlah Pertanyaan Konsultan Not Relevan',
        compute='_compute_total_consultant_not_relevan')

    total_question_relevan = fields.Integer(
        'Jumlah Pertanyaan Valid dan Relevan',
        compute='_compute_total_relevan')
    total_question_verified = fields.Integer('Jumlah Pertanyaan Dipilih',
                                             compute='_compute_total_verified')

    complete_name = fields.Char("Agenda Full Name",
                                compute='_compute_complete_name',
                                store=True)
    num_name = fields.Char("Agenda Name",
                           compute='_compute_num_name',
                           store=True)

    relevan_question_ids = fields.One2many('erups_question',
                                           'agenda_id',
                                           domain=[('status', 'in',
                                                    ['consultant',
                                                     'speaker'])])
    speaker_question_ids = fields.One2many('erups_question',
                                           'agenda_id',
                                           domain=[('status', 'in',
                                                    ['speaker'])])

    active_agenda = fields.Integer(compute='_compute_active_agenda')

    agenda_num_text = fields.Char(compute='_compute_num_to_text')

    @api.depends('agenda_num')
    def _compute_active_agenda(self):
        ErupsAgenda = self.env['erups_agenda'].sudo().search(
            [('status', 'not in', ['done'])], order='agenda_num', limit=1)
        first_agenda_open = ErupsAgenda.agenda_num

        for r in self:
            r.active_agenda = first_agenda_open

    @api.depends('agenda_num')
    def _compute_num_to_text(self):
        for r in self:
            if r.agenda_num == 1:
                r.agenda_num_text = "Pertama"
            if r.agenda_num == 2:
                r.agenda_num_text = "Kedua"
            if r.agenda_num == 3:
                r.agenda_num_text = "Ketiga"
            if r.agenda_num == 4:
                r.agenda_num_text = "Keempat"
            if r.agenda_num == 5:
                r.agenda_num_text = "Kelima"
            if r.agenda_num == 6:
                r.agenda_num_text = "Keenam"

    @api.depends('question_ids')
    def _compute_total_question(self):
        for r in self:
            if r.question_ids:
                r.total_question = len(r.question_ids)
            else:
                r.total_question = 0

    @api.depends('question_ids')
    def _compute_total_valid(self):
        for r in self:
            r.total_question_valid = 0
            if r.question_ids:
                for q in r.question_ids:
                    if q.status not in ['shareholder', 'bae_reject']:
                        r.total_question_valid += 1
            else:
                r.total_question_valid = 0

    @api.depends('question_ids')
    def _compute_total_not_valid(self):
        for r in self:
            r.total_question_not_valid = 0
            if r.question_ids:
                for q in r.question_ids:
                    if q.status in ['bae_reject', 'shareholder']:
                        r.total_question_not_valid += 1

    @api.depends('question_ids')
    def _compute_total_notaris_relevan(self):
        for r in self:
            r.total_notaris_relevan = 0
            if r.question_ids:
                for q in r.question_ids:
                    if q.status == 'notaris':
                        r.total_notaris_relevan += 1

    @api.depends('question_ids')
    def _compute_total_notaris_not_relevan(self):
        for r in self:
            r.total_notaris_not_relevan = 0
            if r.question_ids:
                for q in r.question_ids:
                    if q.status == 'notaris_reject':
                        r.total_notaris_not_relevan += 1

    @api.depends('question_ids')
    def _compute_total_consultant_relevan(self):
        for r in self:
            r.total_consultant_relevan = 0
            if r.question_ids:
                for q in r.question_ids:
                    if q.status in ['consultant', 'speaker']:
                        r.total_consultant_relevan += 1

    @api.depends('question_ids')
    def _compute_total_consultant_not_relevan(self):
        for r in self:
            r.total_consultant_not_relevan = 0
            if r.question_ids:
                for q in r.question_ids:
                    if q.status in ['consultant_reject', 'bae']:
                        r.total_consultant_not_relevan += 1

    @api.depends('question_ids')
    def _compute_total_relevan(self):
        for r in self:
            r.total_question_relevan = 0
            if r.question_ids:
                for q in r.question_ids:
                    if q.status in ['consultant', 'speaker']:
                        r.total_question_relevan += 1
            else:
                r.total_question_relevan = 0

    @api.depends('question_ids')
    def _compute_total_verified(self):
        for r in self:
            if r.question_ids:
                for q in r.question_ids:
                    if q.status == 'speaker':
                        r.total_question_verified += 1
            else:
                r.total_question_verified = 0

    @api.depends('name', 'agenda_num')
    def _compute_complete_name(self):
        for r in self:
            if r.agenda_num:
                r.complete_name = 'Agenda %s - %s' % (r.agenda_num, r.name)
            else:
                r.complete_name = r.name

    @api.depends('name', 'agenda_num')
    def _compute_num_name(self):
        for r in self:
            if r.agenda_num:
                r.num_name = 'Agenda %s' % (r.agenda_num)
            else:
                r.num_name = r.name

    # @api.onchange('status')
    # def _onchange_status(self):
    #     print('merdeka', self)
    #     if self.status == 'open':
    #         agenda_open = self.env['erups_agenda'].sudo().search([('status', '=', 'open')])
    #         agenda_open.sudo().write({'status': 'close'})
    #
    #         self.sudo().write({'status': 'open'})

    def open_agenda(self):
        agenda_open = self.env['erups_agenda'].sudo().search([('status', '=',
                                                               'open')])
        agenda_open.sudo().write({'status': 'close'})

        for r in self:
            r.sudo().write({'status': 'open'})

        return True

    def close_agenda(self):
        for r in self:
            r.sudo().write({'status': 'close'})

        return True

    def done_agenda(self):
        for r in self:
            r.sudo().write({'status': 'done'})

        return True
예제 #10
0
class MassMailing(models.Model):
    """ MassMailing models a wave of emails for a mass mailign campaign.
    A mass mailing is an occurence of sending emails. """

    _name = 'mail.mass_mailing'
    _description = 'Mass Mailing'
    # number of periods for tracking mail_mail statistics
    _period_number = 6
    _order = 'sent_date DESC'
    _inherits = {'utm.source': 'source_id'}
    _rec_name = "source_id"

    @api.model
    def _get_default_mail_server_id(self):
        server_id = self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mail_server_id')
        try:
            server_id = literal_eval(server_id) if server_id else False
            return self.env['ir.mail_server'].search([('id', '=', server_id)]).id
        except ValueError:
            return False

    @api.model
    def default_get(self, fields):
        res = super(MassMailing, self).default_get(fields)
        if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model_real'):
            if res['mailing_model_real'] in ['res.partner', 'mail.mass_mailing.contact']:
                res['reply_to_mode'] = 'email'
            else:
                res['reply_to_mode'] = 'thread'
        return res

    active = fields.Boolean(default=True)
    email_from = fields.Char(string='From', required=True,
        default=lambda self: self.env['mail.message']._get_default_from())
    sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False)
    schedule_date = fields.Datetime(string='Schedule in the Future')
    body_html = fields.Html(string='Body', sanitize_attributes=False)
    attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel',
        'mass_mailing_id', 'attachment_id', string='Attachments')
    keep_archives = fields.Boolean(string='Keep Archives')
    mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign')
    campaign_id = fields.Many2one('utm.campaign', string='Campaign',
                                  help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special")
    source_id = fields.Many2one('utm.source', string='Subject', required=True, ondelete='cascade',
                                help="This is the link source, e.g. Search Engine, another domain, or name of email list")
    medium_id = fields.Many2one('utm.medium', string='Medium',
                                help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email'))
    clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of Clicks")
    state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'), ('sending', 'Sending'), ('done', 'Sent')],
        string='Status', required=True, copy=False, default='draft', group_expand='_group_expand_states')
    color = fields.Integer(string='Color Index')
    user_id = fields.Many2one('res.users', string='Mailing Manager', default=lambda self: self.env.user)
    # mailing options
    reply_to_mode = fields.Selection(
        [('thread', 'Recipient Followers'), ('email', 'Specified Email Address')], string='Reply-To Mode', required=True)
    reply_to = fields.Char(string='Reply To', help='Preferred Reply-To Address',
        default=lambda self: self.env['mail.message']._get_default_from())
    # recipients
    mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True)
    mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)],
        default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id)
    mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name', readonly=True, related_sudo=True)
    mailing_domain = fields.Char(string='Domain', oldname='domain', default=[])
    mail_server_id = fields.Many2one('ir.mail_server', string='Mail Server',
        default=_get_default_mail_server_id,
        help="Use a specific mail server in priority. Otherwise ALWAFI relies on the first outgoing mail server available (based on their sequencing) as it does for normal mails.")
    contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel',
        string='Mailing Lists')
    contact_ab_pc = fields.Integer(string='A/B Testing percentage',
        help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.', default=100)
    # statistics data
    statistics_ids = fields.One2many('mail.mail.statistics', 'mass_mailing_id', string='Emails Statistics')
    total = fields.Integer(compute="_compute_total")
    scheduled = fields.Integer(compute="_compute_statistics")
    expected = fields.Integer(compute="_compute_statistics")
    ignored = fields.Integer(compute="_compute_statistics")
    sent = fields.Integer(compute="_compute_statistics")
    delivered = fields.Integer(compute="_compute_statistics")
    opened = fields.Integer(compute="_compute_statistics")
    clicked = fields.Integer(compute="_compute_statistics")
    replied = fields.Integer(compute="_compute_statistics")
    bounced = fields.Integer(compute="_compute_statistics")
    failed = fields.Integer(compute="_compute_statistics")
    received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio')
    opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio')
    replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio')
    bounced_ratio = fields.Integer(compute="_compute_statistics", String='Bounced Ratio')
    next_departure = fields.Datetime(compute="_compute_next_departure", string='Scheduled date')

    def _compute_total(self):
        for mass_mailing in self:
            mass_mailing.total = len(mass_mailing.sudo().get_recipients())

    def _compute_clicks_ratio(self):
        self.env.cr.execute("""
            SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_id AS id
            FROM mail_mail_statistics AS stats
            LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id
            WHERE stats.mass_mailing_id IN %s
            GROUP BY stats.mass_mailing_id
        """, (tuple(self.ids), ))

        mass_mailing_data = self.env.cr.dictfetchall()
        mapped_data = dict([(m['id'], 100 * m['nb_clicks'] / m['nb_mails']) for m in mass_mailing_data])
        for mass_mailing in self:
            mass_mailing.clicks_ratio = mapped_data.get(mass_mailing.id, 0)

    @api.depends('mailing_model_id')
    def _compute_model(self):
        for record in self:
            record.mailing_model_real = (record.mailing_model_name != 'mail.mass_mailing.list') and record.mailing_model_name or 'mail.mass_mailing.contact'

    def _compute_statistics(self):
        """ Compute statistics of the mass mailing """
        self.env.cr.execute("""
            SELECT
                m.id as mailing_id,
                COUNT(s.id) AS expected,
                COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
                COUNT(CASE WHEN s.sent is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
                COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
                COUNT(CASE WHEN s.clicked is not null THEN 1 ELSE null END) AS clicked,
                COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied,
                COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced,
                COUNT(CASE WHEN s.exception is not null THEN 1 ELSE null END) AS failed
            FROM
                mail_mail_statistics s
            RIGHT JOIN
                mail_mass_mailing m
                ON (m.id = s.mass_mailing_id)
            WHERE
                m.id IN %s
            GROUP BY
                m.id
        """, (tuple(self.ids), ))
        for row in self.env.cr.dictfetchall():
            total = row['expected'] = (row['expected'] - row['ignored']) or 1
            row['received_ratio'] = 100.0 * row['delivered'] / total
            row['opened_ratio'] = 100.0 * row['opened'] / total
            row['clicks_ratio'] = 100.0 * row['clicked'] / total
            row['replied_ratio'] = 100.0 * row['replied'] / total
            row['bounced_ratio'] = 100.0 * row['bounced'] / total
            self.browse(row.pop('mailing_id')).update(row)

    @api.multi
    def _unsubscribe_token(self, res_id, email):
        """Generate a secure hash for this mailing list and parameters.

        This is appended to the unsubscription URL and then checked at
        unsubscription time to ensure no malicious unsubscriptions are
        performed.

        :param int res_id:
            ID of the resource that will be unsubscribed.

        :param str email:
            Email of the resource that will be unsubscribed.
        """
        secret = self.env["ir.config_parameter"].sudo().get_param(
            "database.secret")
        token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email))
        return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest()

    def _compute_next_departure(self):
        cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall
        str2dt = fields.Datetime.from_string
        cron_time = str2dt(cron_next_call)
        for mass_mailing in self:
            if mass_mailing.schedule_date:
                schedule_date = str2dt(mass_mailing.schedule_date)
                mass_mailing.next_departure = max(schedule_date, cron_time)
            else:
                mass_mailing.next_departure = cron_time

    @api.onchange('mass_mailing_campaign_id')
    def _onchange_mass_mailing_campaign_id(self):
        if self.mass_mailing_campaign_id:
            dic = {'campaign_id': self.mass_mailing_campaign_id.campaign_id,
                   'source_id': self.mass_mailing_campaign_id.source_id,
                   'medium_id': self.mass_mailing_campaign_id.medium_id}
            self.update(dic)

    @api.onchange('mailing_model_id', 'contact_list_ids')
    def _onchange_model_and_list(self):
        mailing_domain = []
        if self.mailing_model_name:
            if self.mailing_model_name == 'mail.mass_mailing.list':
                if self.contact_list_ids:
                    mailing_domain.append(('list_ids', 'in', self.contact_list_ids.ids))
                else:
                    mailing_domain.append((0, '=', 1))
            elif self.mailing_model_name == 'res.partner':
                mailing_domain.append(('customer', '=', True))
            elif 'opt_out' in self.env[self.mailing_model_name]._fields and not self.mailing_domain:
                mailing_domain.append(('opt_out', '=', False))
        else:
            mailing_domain.append((0, '=', 1))
        self.mailing_domain = repr(mailing_domain)
        self.body_html = "on_change_model_and_list"

    #------------------------------------------------------
    # Technical stuff
    #------------------------------------------------------

    @api.model
    def name_create(self, name):
        """ _rec_name is source_id, creates a utm.source instead """
        mass_mailing = self.create({'name': name})
        return mass_mailing.name_get()[0]

    @api.multi
    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {},
                       name=_('%s (copy)') % self.name)
        return super(MassMailing, self).copy(default=default)

    def _group_expand_states(self, states, domain, order):
        return [key for key, val in type(self).state.selection]

    def update_opt_out(self, email, list_ids, value):
        if len(list_ids) > 0:
            model = self.env['mail.mass_mailing.contact'].with_context(active_test=False)
            records = model.search([('email', '=ilike', email)])
            opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([
                ('contact_id', 'in', records.ids),
                ('list_id', 'in', list_ids),
                ('opt_out', '!=', value)
            ])

            opt_out_records.write({'opt_out': value})
            message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \
                if value else _('The recipient <strong>subscribed to %s</strong> mailing list(s)')
            for record in records:
                # filter the list_id by record
                record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id)
                if len(record_lists) > 0:
                    record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id'))))

    @api.model
    def create(self, values):
        if values.get('body_html'):
            values['body_html'] = self._convert_inline_images_to_urls(values['body_html'])
        return super(MassMailing, self).create(values)

    def write(self, values):
        if values.get('body_html'):
            values['body_html'] = self._convert_inline_images_to_urls(values['body_html'])
        return super(MassMailing, self).write(values)

    def _convert_inline_images_to_urls(self, body_html):
        """
        Find inline base64 encoded images, make an attachement out of
        them and replace the inline image with an url to the attachement.
        """

        def _image_to_url(b64image: bytes):
            """Store an image in an attachement and returns an url"""
            attachment = self.env['ir.attachment'].create({
                'name': "cropped_image",
                'datas': b64image,
                'datas_fname': "cropped_image_mailing_{}".format(self.id),
                'type': 'binary',})

            attachment.generate_access_token()

            return '/web/image/%s?access_token=%s' % (
                attachment.id, attachment.access_token)


        modified = False
        root = lxml.html.fromstring(body_html)
        for node in root.iter('img'):
            match = image_re.match(node.attrib.get('src', ''))
            if match:
                mime = match.group(1)  # unsed
                image = match.group(2).encode()  # base64 image as bytes

                node.attrib['src'] = _image_to_url(image)
                modified = True

        if modified:
            return lxml.html.tostring(root)

        return body_html

    #------------------------------------------------------
    # Views & Actions
    #------------------------------------------------------

    @api.multi
    def action_duplicate(self):
        self.ensure_one()
        mass_mailing_copy = self.copy()
        if mass_mailing_copy:
            context = dict(self.env.context)
            context['form_view_initial_mode'] = 'edit'
            return {
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'mail.mass_mailing',
                'res_id': mass_mailing_copy.id,
                'context': context,
            }
        return False

    @api.multi
    def action_test_mailing(self):
        self.ensure_one()
        ctx = dict(self.env.context, default_mass_mailing_id=self.id)
        return {
            'name': _('Test Mailing'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'mail.mass_mailing.test',
            'target': 'new',
            'context': ctx,
        }

    @api.multi
    def action_schedule_date(self):
        self.ensure_one()
        action = self.env.ref('mass_mailing.mass_mailing_schedule_date_action').read()[0]
        action['context'] = dict(self.env.context, default_mass_mailing_id=self.id)
        return action

    @api.multi
    def put_in_queue(self):
        self.write({'state': 'in_queue'})

    @api.multi
    def cancel_mass_mailing(self):
        self.write({'state': 'draft', 'schedule_date': False})

    @api.multi
    def retry_failed_mail(self):
        failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')])
        failed_mails.mapped('statistics_ids').unlink()
        failed_mails.sudo().unlink()
        res_ids = self.get_recipients()
        except_mailed = self.env['mail.mail.statistics'].search([
            ('model', '=', self.mailing_model_real),
            ('res_id', 'in', res_ids),
            ('exception', '!=', False),
            ('mass_mailing_id', '=', self.id)]).unlink()
        self.write({'state': 'in_queue'})

    def action_view_sent(self):
        return self._action_view_documents_filtered('sent')

    def action_view_opened(self):
        return self._action_view_documents_filtered('opened')

    def action_view_replied(self):
        return self._action_view_documents_filtered('replied')

    def action_view_bounced(self):
        return self._action_view_documents_filtered('bounced')

    def action_view_clicked(self):
        return self._action_view_documents_filtered('clicked')

    def action_view_delivered(self):
        return self._action_view_documents_filtered('delivered')

    def _action_view_documents_filtered(self, view_filter):
        if view_filter in ('sent', 'opened', 'replied', 'bounced', 'clicked'):
            opened_stats = self.statistics_ids.filtered(lambda stat: stat[view_filter])
        elif view_filter == ('delivered'):
            opened_stats = self.statistics_ids.filtered(lambda stat: stat.sent and not stat.bounced)
        else:
            opened_stats = self.env['mail.mail.statistics']
        res_ids = opened_stats.mapped('res_id')
        model_name = self.env['ir.model']._get(self.mailing_model_real).display_name
        return {
            'name': model_name,
            'type': 'ir.actions.act_window',
            'view_mode': 'tree',
            'res_model': self.mailing_model_real,
            'domain': [('id', 'in', res_ids)],
        }

    #------------------------------------------------------
    # Email Sending
    #------------------------------------------------------

    def _get_opt_out_list(self):
        """Returns a set of emails opted-out in target model"""
        self.ensure_one()
        opt_out = {}
        target = self.env[self.mailing_model_real]
        if self.mailing_model_real == "mail.mass_mailing.contact":
            # if user is opt_out on One list but not on another
            # or if two user with same email address, one opted in and the other one opted out, send the mail anyway
            # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in
            target_list_contacts = self.env['mail.mass_mailing.list_contact_rel'].search(
                [('list_id', 'in', self.contact_list_ids.ids)])
            opt_out_contacts = target_list_contacts.filtered(lambda rel: rel.opt_out).mapped('contact_id.email')
            opt_in_contacts = target_list_contacts.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email')
            normalized_email = [tools.email_split(c) for c in opt_out_contacts if c not in opt_in_contacts]
            opt_out = set(email[0].lower() for email in normalized_email if email)

            _logger.info(
                "Mass-mailing %s targets %s, blacklist: %s emails",
                self, target._name, len(opt_out))
        else:
            _logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name)
        return opt_out

    def _get_convert_links(self):
        self.ensure_one()
        utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self
        vals = {'mass_mailing_id': self.id}

        if self.mass_mailing_campaign_id:
            vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id
        if utm_mixin.campaign_id:
            vals['campaign_id'] = utm_mixin.campaign_id.id
        if utm_mixin.source_id:
            vals['source_id'] = utm_mixin.source_id.id
        if utm_mixin.medium_id:
            vals['medium_id'] = utm_mixin.medium_id.id
        return vals

    def _get_seen_list(self):
        """Returns a set of emails already targeted by current mailing/campaign (no duplicates)"""
        self.ensure_one()
        target = self.env[self.mailing_model_real]
        if set(['email', 'email_from']) & set(target._fields):
            mail_field = 'email' if 'email' in target._fields else 'email_from'
            # avoid loading a large number of records in memory
            # + use a basic heuristic for extracting emails
            query = """
                SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
                  FROM mail_mail_statistics s
                  JOIN %(target)s t ON (s.res_id = t.id)
                 WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
            """
        elif 'partner_id' in target._fields:
            mail_field = 'email'
            query = """
                SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)'))
                  FROM mail_mail_statistics s
                  JOIN %(target)s t ON (s.res_id = t.id)
                  JOIN res_partner p ON (t.partner_id = p.id)
                 WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL
            """
        else:
            raise UserError(_("Unsupported mass mailing model %s") % self.mailing_model_id.name)

        if self.mass_mailing_campaign_id.unique_ab_testing:
            query +="""
               AND s.mass_mailing_campaign_id = %%(mailing_campaign_id)s;
            """
        else:
            query +="""
               AND s.mass_mailing_id = %%(mailing_id)s
               AND s.model = %%(target_model)s;
            """
        query = query % {'target': target._table, 'mail_field': mail_field}
        params = {'mailing_id': self.id, 'mailing_campaign_id': self.mass_mailing_campaign_id.id, 'target_model': self.mailing_model_real}
        self._cr.execute(query, params)
        seen_list = set(m[0] for m in self._cr.fetchall())
        _logger.info(
            "Mass-mailing %s has already reached %s %s emails", self, len(seen_list), target._name)
        return seen_list

    def _get_mass_mailing_context(self):
        """Returns extra context items with pre-filled blacklist and seen list for massmailing"""
        return {
            'mass_mailing_opt_out_list': self._get_opt_out_list(),
            'mass_mailing_seen_list': self._get_seen_list(),
            'post_convert_links': self._get_convert_links(),
        }

    def get_recipients(self):
        if self.mailing_domain:
            domain = safe_eval(self.mailing_domain)
            res_ids = self.env[self.mailing_model_real].search(domain).ids
        else:
            res_ids = []
            domain = [('id', 'in', res_ids)]

        # randomly choose a fragment
        if self.contact_ab_pc < 100:
            contact_nbr = self.env[self.mailing_model_real].search_count(domain)
            topick = int(contact_nbr / 100.0 * self.contact_ab_pc)
            if self.mass_mailing_campaign_id and self.mass_mailing_campaign_id.unique_ab_testing:
                already_mailed = self.mass_mailing_campaign_id.get_recipients()[self.mass_mailing_campaign_id.id]
            else:
                already_mailed = set([])
            remaining = set(res_ids).difference(already_mailed)
            if topick > len(remaining):
                topick = len(remaining)
            res_ids = random.sample(remaining, topick)
        return res_ids

    def get_remaining_recipients(self):
        res_ids = self.get_recipients()
        already_mailed = self.env['mail.mail.statistics'].search_read([('model', '=', self.mailing_model_real),
                                                                     ('res_id', 'in', res_ids),
                                                                     ('mass_mailing_id', '=', self.id)], ['res_id'])
        already_mailed_res_ids = [record['res_id'] for record in already_mailed]
        return list(set(res_ids) - set(already_mailed_res_ids))

    def send_mail(self, res_ids=None):
        author_id = self.env.user.partner_id.id

        for mailing in self:
            if not res_ids:
                res_ids = mailing.get_remaining_recipients()
            if not res_ids:
                raise UserError(_('There is no recipients selected.'))

            composer_values = {
                'author_id': author_id,
                'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids],
                'body': mailing.body_html,
                'subject': mailing.name,
                'model': mailing.mailing_model_real,
                'email_from': mailing.email_from,
                'record_name': False,
                'composition_mode': 'mass_mail',
                'mass_mailing_id': mailing.id,
                'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids],
                'no_auto_thread': mailing.reply_to_mode != 'thread',
                'template_id': None,
                'mail_server_id': mailing.mail_server_id.id,
            }
            if mailing.reply_to_mode == 'email':
                composer_values['reply_to'] = mailing.reply_to

            composer = self.env['mail.compose.message'].with_context(active_ids=res_ids).create(composer_values)
            extra_context = self._get_mass_mailing_context()
            composer = composer.with_context(active_ids=res_ids, **extra_context)
            # auto-commit except in testing mode
            auto_commit = not getattr(threading.currentThread(), 'testing', False)
            composer.send_mail(auto_commit=auto_commit)
            mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
        return True

    def convert_links(self):
        res = {}
        for mass_mailing in self:
            utm_mixin = mass_mailing.mass_mailing_campaign_id if mass_mailing.mass_mailing_campaign_id else mass_mailing
            html = mass_mailing.body_html if mass_mailing.body_html else ''

            vals = {'mass_mailing_id': mass_mailing.id}

            if mass_mailing.mass_mailing_campaign_id:
                vals['mass_mailing_campaign_id'] = mass_mailing.mass_mailing_campaign_id.id
            if utm_mixin.campaign_id:
                vals['campaign_id'] = utm_mixin.campaign_id.id
            if utm_mixin.source_id:
                vals['source_id'] = utm_mixin.source_id.id
            if utm_mixin.medium_id:
                vals['medium_id'] = utm_mixin.medium_id.id

            res[mass_mailing.id] = self.env['link.tracker'].convert_links(html, vals, blacklist=['/unsubscribe_from_list'])

        return res

    @api.model
    def _process_mass_mailing_queue(self):
        mass_mailings = self.search([('state', 'in', ('in_queue', 'sending')), '|', ('schedule_date', '<', fields.Datetime.now()), ('schedule_date', '=', False)])
        for mass_mailing in mass_mailings:
            user = mass_mailing.write_uid or self.env.user
            mass_mailing = mass_mailing.with_context(**user.sudo(user=user).context_get())
            if len(mass_mailing.get_remaining_recipients()) > 0:
                mass_mailing.state = 'sending'
                mass_mailing.send_mail()
            else:
                mass_mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
예제 #11
0
class MassMailingList(models.Model):
    """Model of a contact list. """
    _name = 'mail.mass_mailing.list'
    _order = 'name'
    _description = 'Mailing List'

    name = fields.Char(string='Mailing List', required=True)
    active = fields.Boolean(default=True)
    contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts')
    contact_ids = fields.Many2many(
        'mail.mass_mailing.contact', 'mail_mass_mailing_contact_list_rel', 'list_id', 'contact_id',
        string='Mailing Lists')
    subscription_contact_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'list_id',
        string='Subscription Information')
    is_public = fields.Boolean(default=True, help="The mailing list can be accessible by recipient in the unsubscription"
                                                  " page to allows him to update his subscription preferences.")

    # Compute number of contacts non opt-out, non blacklisted and valid email recipient for a mailing list
    def _compute_contact_nbr(self):
        self.env.cr.execute('''
            select
                list_id, count(*)
            from
                mail_mass_mailing_contact_list_rel r
                left join mail_mass_mailing_contact c on (r.contact_id=c.id)
                left join mail_blacklist bl on (LOWER(substring(c.email, %s)) = bl.email and bl.active)
            where
                list_id in %s AND
                COALESCE(r.opt_out,FALSE) = FALSE
                AND c.email IS NOT NULL
                AND bl.id IS NULL
            group by
                list_id
        ''', [EMAIL_PATTERN, tuple(self.ids)])
        data = dict(self.env.cr.fetchall())
        for mailing_list in self:
            mailing_list.contact_nbr = data.get(mailing_list.id, 0)

    @api.multi
    def name_get(self):
        return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self]

    @api.multi
    def action_merge(self, src_lists, archive):
        """
            Insert all the contact from the mailing lists 'src_lists' to the
            mailing list in 'self'. Possibility to archive the mailing lists
            'src_lists' after the merge except the destination mailing list 'self'.
        """
        # Explation of the SQL query with an example. There are the following lists
        # A (id=4): [email protected]; [email protected]
        # B (id=5): [email protected]; [email protected]
        # C (id=6): nothing
        # To merge the mailing lists A and B into C, we build the view st that looks
        # like this with our example:
        #
        #  contact_id |           email           | row_number |  list_id |
        # ------------+---------------------------+------------------------
        #           4 | [email protected]              |          1 |        4 |
        #           6 | [email protected]              |          2 |        5 |
        #           5 | [email protected]           |          1 |        4 |
        #           7 | [email protected]           |          1 |        5 |
        #
        # The row_column is kind of an occurence counter for the email address.
        # Then we create the Many2many relation between the destination list and the contacts
        # while avoiding to insert an existing email address (if the destination is in the source
        # for example)
        self.ensure_one()
        # Put destination is sources lists if not already the case
        src_lists |= self
        self.env.cr.execute("""
            INSERT INTO mail_mass_mailing_contact_list_rel (contact_id, list_id)
            SELECT st.contact_id AS contact_id, %s AS list_id
            FROM
                (
                SELECT
                    contact.id AS contact_id,
                    contact.email AS email,
                    mailing_list.id AS list_id,
                    row_number() OVER (PARTITION BY email ORDER BY email) AS rn
                FROM
                    mail_mass_mailing_contact contact,
                    mail_mass_mailing_contact_list_rel contact_list_rel,
                    mail_mass_mailing_list mailing_list
                WHERE contact.id=contact_list_rel.contact_id
                AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE
                AND LOWER(substring(contact.email, %s)) NOT IN (select email from mail_blacklist where active = TRUE)
                AND mailing_list.id=contact_list_rel.list_id
                AND mailing_list.id IN %s
                AND NOT EXISTS
                    (
                    SELECT 1
                    FROM
                        mail_mass_mailing_contact contact2,
                        mail_mass_mailing_contact_list_rel contact_list_rel2
                    WHERE contact2.email = contact.email
                    AND contact_list_rel2.contact_id = contact2.id
                    AND contact_list_rel2.list_id = %s
                    )
                ) st
            WHERE st.rn = 1;""", (self.id, EMAIL_PATTERN, tuple(src_lists.ids), self.id))
        self.invalidate_cache()
        if archive:
            (src_lists - self).write({'active': False})

    @api.multi
    def close_dialog(self):
        return {'type': 'ir.actions.act_window_close'}
예제 #12
0
class MassMailingCampaign(models.Model):
    """Model of mass mailing campaigns. """
    _name = "mail.mass_mailing.campaign"
    _description = 'Mass Mailing Campaign'
    _rec_name = "campaign_id"
    _inherits = {'utm.campaign': 'campaign_id'}

    stage_id = fields.Many2one('mail.mass_mailing.stage', string='Stage', ondelete='restrict', required=True, 
        default=lambda self: self.env['mail.mass_mailing.stage'].search([], limit=1),
        group_expand='_group_expand_stage_ids')
    user_id = fields.Many2one(
        'res.users', string='Responsible',
        required=True, default=lambda self: self.env.uid)
    campaign_id = fields.Many2one('utm.campaign', 'campaign_id',
        required=True, ondelete='cascade',  help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special")
    source_id = fields.Many2one('utm.source', string='Source',
            help="This is the link source, e.g. Search Engine, another domain,or name of email list", default=lambda self: self.env.ref('utm.utm_source_newsletter', False))
    medium_id = fields.Many2one('utm.medium', string='Medium',
            help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email', False))
    tag_ids = fields.Many2many(
        'mail.mass_mailing.tag', 'mail_mass_mailing_tag_rel',
        'tag_id', 'campaign_id', string='Tags')
    mass_mailing_ids = fields.One2many(
        'mail.mass_mailing', 'mass_mailing_campaign_id',
        string='Mass Mailings')
    unique_ab_testing = fields.Boolean(string='Allow A/B Testing', default=False,
        help='If checked, recipients will be mailed only once for the whole campaign. '
             'This lets you send different mailings to randomly selected recipients and test '
             'the effectiveness of the mailings, without causing duplicate messages.')
    color = fields.Integer(string='Color Index')
    clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of clicks")
    # stat fields
    total = fields.Integer(compute="_compute_statistics")
    scheduled = fields.Integer(compute="_compute_statistics")
    failed = fields.Integer(compute="_compute_statistics")
    ignored = fields.Integer(compute="_compute_statistics")
    sent = fields.Integer(compute="_compute_statistics", string="Sent Emails")
    delivered = fields.Integer(compute="_compute_statistics")
    opened = fields.Integer(compute="_compute_statistics")
    replied = fields.Integer(compute="_compute_statistics")
    bounced = fields.Integer(compute="_compute_statistics")
    received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio')
    opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio')
    replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio')
    bounced_ratio = fields.Integer(compute="_compute_statistics", string='Bounced Ratio')
    total_mailings = fields.Integer(compute="_compute_total_mailings", string='Mailings')

    def _compute_clicks_ratio(self):
        self.env.cr.execute("""
            SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_campaign_id AS id
            FROM mail_mail_statistics AS stats
            LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id
            WHERE stats.mass_mailing_campaign_id IN %s
            GROUP BY stats.mass_mailing_campaign_id
        """, (tuple(self.ids), ))

        campaign_data = self.env.cr.dictfetchall()
        mapped_data = dict([(c['id'], 100 * c['nb_clicks'] / c['nb_mails']) for c in campaign_data])
        for campaign in self:
            campaign.clicks_ratio = mapped_data.get(campaign.id, 0)

    def _compute_statistics(self):
        """ Compute statistics of the mass mailing campaign """
        self.env.cr.execute("""
            SELECT
                c.id as campaign_id,
                COUNT(s.id) AS total,
                COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed,
                COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored,
                COUNT(CASE WHEN s.id is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered,
                COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened,
                COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied ,
                COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced
            FROM
                mail_mail_statistics s
            RIGHT JOIN
                mail_mass_mailing_campaign c
                ON (c.id = s.mass_mailing_campaign_id)
            WHERE
                c.id IN %s
            GROUP BY
                c.id
        """, (tuple(self.ids), ))

        for row in self.env.cr.dictfetchall():
            total = (row['total'] - row['ignored']) or 1
            row['delivered'] = row['sent'] - row['bounced']
            row['received_ratio'] = 100.0 * row['delivered'] / total
            row['opened_ratio'] = 100.0 * row['opened'] / total
            row['replied_ratio'] = 100.0 * row['replied'] / total
            row['bounced_ratio'] = 100.0 * row['bounced'] / total
            self.browse(row.pop('campaign_id')).update(row)

    def _compute_total_mailings(self):
        campaign_data = self.env['mail.mass_mailing'].read_group(
            [('mass_mailing_campaign_id', 'in', self.ids)],
            ['mass_mailing_campaign_id'], ['mass_mailing_campaign_id'])
        mapped_data = dict([(c['mass_mailing_campaign_id'][0], c['mass_mailing_campaign_id_count']) for c in campaign_data])
        for campaign in self:
            campaign.total_mailings = mapped_data.get(campaign.id, 0)

    def get_recipients(self, model=None):
        """Return the recipients of a mailing campaign. This is based on the statistics
        build for each mailing. """
        res = dict.fromkeys(self.ids, {})
        for campaign in self:
            domain = [('mass_mailing_campaign_id', '=', campaign.id)]
            if model:
                domain += [('model', '=', model)]
            res[campaign.id] = set(self.env['mail.mail.statistics'].search(domain).mapped('res_id'))
        return res

    @api.model
    def _group_expand_stage_ids(self, stages, domain, order):
        """ Read group customization in order to display all the stages in the
            kanban view, even if they are empty
        """
        stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID)
        return stages.browse(stage_ids)
예제 #13
0
class MassMailingContact(models.Model):
    """Model of a contact. This model is different from the partner model
    because it holds only some basic information: name, email. The purpose is to
    be able to deal with large contact list to email without bloating the partner
    base."""
    _name = 'mail.mass_mailing.contact'
    _inherit = ['mail.thread', 'mail.blacklist.mixin']
    _description = 'Mass Mailing Contact'
    _order = 'email'
    _rec_name = 'email'

    name = fields.Char()
    company_name = fields.Char(string='Company Name')
    title_id = fields.Many2one('res.partner.title', string='Title')
    email = fields.Char(required=True)
    is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True)
    list_ids = fields.Many2many(
        'mail.mass_mailing.list', 'mail_mass_mailing_contact_list_rel',
        'contact_id', 'list_id', string='Mailing Lists')
    subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel',
        'contact_id', string='Subscription Information')
    message_bounce = fields.Integer(string='Bounced', help='Counter of the number of bounced emails for this contact.', default=0)
    country_id = fields.Many2one('res.country', string='Country')
    tag_ids = fields.Many2many('res.partner.category', string='Tags')
    opt_out = fields.Boolean('Opt Out', compute='_compute_opt_out', search='_search_opt_out',
                             help='Opt out flag for a specific mailing list.'
                                  'This field should not be used in a view without a unique and active mailing list context.')

    @api.depends('email')
    def _compute_is_email_valid(self):
        for record in self:
            record.is_email_valid = re.match(EMAIL_PATTERN, record.email)

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

        if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
            [active_list_id] = self._context['default_list_ids']
            contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)])
            return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])]
        else:
            raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.')

    @api.depends('subscription_list_ids')
    def _compute_opt_out(self):
        if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1:
            [active_list_id] = self._context['default_list_ids']
            for record in self:
                active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id)
                record.opt_out = active_subscription_list.opt_out
        else:
            for record in self:
                record.opt_out = False

    def get_name_email(self, name):
        name, email = self.env['res.partner']._parse_partner_name(name)
        if name and not email:
            email = name
        if email and not name:
            name = email
        return name, email

    @api.model
    def name_create(self, name):
        name, email = self.get_name_email(name)
        contact = self.create({'name': name, 'email': email})
        return contact.name_get()[0]

    @api.model
    def add_to_list(self, name, list_id):
        name, email = self.get_name_email(name)
        contact = self.create({'name': name, 'email': email, 'list_ids': [(4, list_id)]})
        return contact.name_get()[0]

    @api.multi
    def message_get_default_recipients(self):
        return dict((record.id, {'partner_ids': [], 'email_to': record.email, 'email_cc': False}) for record in self)
예제 #14
0
class GradeMaster(models.Model):
    _name = 'grade.master'
    _description = "Grade Master"

    name = fields.Char('Grade', required=True)
    grade_ids = fields.One2many('grade.line', 'grade_id', 'Grade Lines')
예제 #15
0
class MrpProduction(models.Model):
    _inherit = 'mrp.production'

    move_raw_ids = fields.One2many(
        'stock.move',
        'raw_material_production_id',
        'Raw Materials',
        oldname='move_lines',
        copy=False,
        states={
            'done': [('readonly', True)],
            'cancel': [('readonly', True)]
        },
        domain=[('scrapped', '=', False), ('state', '!=', 'cancel')],
    )

    @api.multi
    def _generate_moves(self):
        res = super(MrpProduction, self)._generate_moves()
        for production in self:
            if production.move_raw_ids:
                for move in production.move_raw_ids:
                    # CONTENDRA VALOR ORIGINAL A CONSUMIR (INFORMATIVO)
                    move.product_uom_qty_original = move.product_uom_qty
                    if move.bom_line_id and production.product_id.categ_id.mrp_bom_modification:
                        move.real_p = move.bom_line_id.bom_p

        return res

    def get_min_qty(self, move, production_qty):
        """OBTIENE EL % CORRESPONDIENTE A LA CANTIDAD ESTABLECIDA EN LA LISTA DE MATERIAL."""
        bom_qty = move.bom_line_id.product_qty
        formula_p = round(move.formula_p, 5)
        min_qty = ((bom_qty * formula_p) / 100) * production_qty
        return min_qty

    def check_move_line(self, move):
        if move.obligatorio:
            if move.bom_line_id and not move.new_bom_line and move.raw_material_production_id:
                production_qty = move.raw_material_production_id.product_qty  # 100%
                product_uom_qty = move.product_uom_qty  # ?
                min_qty = self.get_min_qty(move, production_qty)
                if product_uom_qty < min_qty:
                    raise ValidationError(
                        _('La cantidad minima permitida para ' +
                          move.product_id.name + ' es ' + str(min_qty)))
        return

    @api.multi
    def check_percentage(self):
        """REVISA QUE SE CUMPLA LA CONDICION DE 100% DE LISTA DE MATERIALES."""
        for production in self:
            if production.product_id.categ_id and production.product_id.categ_id.mrp_bom_modification:
                # SE OBTIENE EL 100% DE LA LISTA DE MATERIALES ORIGINAL (SUMA DEL TOTAL DE CANTIDADES)
                bom_total = 0
                if production.bom_id and production.bom_id.bom_line_ids:
                    bom_total = sum([
                        ((line.product_qty / production.bom_id.product_qty) *
                         production.product_qty)
                        for line in production.bom_id.bom_line_ids
                    ])
                # SE OBTIENE EL 100% DE LOS MATERIALES A CONSUMIR
                production_total = 0
                if production.move_raw_ids:
                    for move in production.move_raw_ids:
                        self.check_move_line(move)
                    production_total = sum([
                        move.product_uom_qty
                        for move in production.move_raw_ids
                        if move.state not in ('cancel')
                    ])
                differencia_perc = bom_total / production_total
                if differencia_perc - 1 > _ALLOWED_DIFFERENCE_PERC:
                    str_differencia = str('{0:f}'.format(
                        (1 - differencia_perc) * 100))
                    raise ValidationError(
                        _('No alcanza el 100% de cantidad de produccion necesario'
                          + '\nTotal Lista de materiales = ' + str(bom_total) +
                          '\nTotal Materiales a consumir = ' +
                          str(production_total) + '\nDiferencia porcentual: ' +
                          str(str_differencia)))

    # HEREDA METODOS EXISTENTES Y AGREGA CHECKEO DE PORCENTAGES

    @api.multi
    def button_plan(self):
        if self.state not in ('done', 'cancel', 'progress'):
            self.check_percentage()
        super(MrpProduction, self).button_plan()

    @api.multi
    def action_assign(self):
        if self.state not in ('done', 'cancel', 'progress'):
            self.check_percentage()
        super(MrpProduction, self).action_assign()
예제 #16
0
class Currency(models.Model):
    _name = "res.currency"
    _description = "Currency"
    _order = 'active desc, name'

    # Note: 'code' column was removed as of v6.0, the 'name' should now hold the ISO code.
    name = fields.Char(string='Currency', size=3, required=True, help="Currency Code (ISO 4217)")
    symbol = fields.Char(help="Currency sign, to be used when printing amounts.", required=True)
    rate = fields.Float(compute='_compute_current_rate', string='Current Rate', digits=(12, 6),
                        help='The rate of the currency to the currency of rate 1.')
    rate_ids = fields.One2many('res.currency.rate', 'currency_id', string='Rates')
    rounding = fields.Float(string='Rounding Factor', digits=(12, 6), default=0.01)
    decimal_places = fields.Integer(compute='_compute_decimal_places')
    active = fields.Boolean(default=True)
    position = fields.Selection([('after', 'After Amount'), ('before', 'Before Amount')], default='after',
        string='Symbol Position', help="Determines where the currency symbol should be placed after or before the amount.")
    date = fields.Date(compute='_compute_date')
    currency_unit_label = fields.Char(string="Currency Unit", help="Currency Unit Name")
    currency_subunit_label = fields.Char(string="Currency Subunit", help="Currency Subunit Name")

    _sql_constraints = [
        ('unique_name', 'unique (name)', 'The currency code must be unique!'),
        ('rounding_gt_zero', 'CHECK (rounding>0)', 'The rounding factor must be greater than 0!')
    ]

    def _get_rates(self, company, date):
        query = """SELECT c.id, (SELECT r.rate FROM res_currency_rate r
                                  WHERE r.currency_id = c.id AND r.name <= %s
                                    AND (r.company_id IS NULL OR r.company_id = %s)
                               ORDER BY r.company_id, r.name DESC
                                  LIMIT 1) AS rate
                   FROM res_currency c
                   WHERE c.id IN %s"""
        self._cr.execute(query, (date, company.id, tuple(self.ids)))
        currency_rates = dict(self._cr.fetchall())
        return currency_rates

    @api.multi
    def _compute_current_rate(self):
        date = self._context.get('date') or fields.Date.today()
        company = self._context.get('company_id') or self.env['res.users']._get_company()
        # the subquery selects the last rate before 'date' for the given currency/company
        currency_rates = self._get_rates(company, date)
        for currency in self:
            currency.rate = currency_rates.get(currency.id) or 1.0

    @api.multi
    @api.depends('rounding')
    def _compute_decimal_places(self):
        for currency in self:
            if 0 < currency.rounding < 1:
                currency.decimal_places = int(math.ceil(math.log10(1/currency.rounding)))
            else:
                currency.decimal_places = 0

    @api.multi
    @api.depends('rate_ids.name')
    def _compute_date(self):
        for currency in self:
            currency.date = currency.rate_ids[:1].name

    @api.model
    def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
        results = super(Currency, self)._name_search(name, args, operator=operator, limit=limit, name_get_uid=name_get_uid)
        if not results:
            name_match = CURRENCY_DISPLAY_PATTERN.match(name)
            if name_match:
                results = super(Currency, self)._name_search(name_match.group(1), args, operator=operator, limit=limit, name_get_uid=name_get_uid)
        return results

    @api.multi
    def name_get(self):
        return [(currency.id, tools.ustr(currency.name)) for currency in self]

    @api.multi
    def amount_to_text(self, amount):
        self.ensure_one()
        def _num2words(number, lang):
            try:
                return num2words(number, lang=lang).title()
            except NotImplementedError:
                return num2words(number, lang='en').title()

        if num2words is None:
            logging.getLogger(__name__).warning("The library 'num2words' is missing, cannot render textual amounts.")
            return ""

        formatted = "%.{0}f".format(self.decimal_places) % amount
        parts = formatted.partition('.')
        integer_value = int(parts[0])
        fractional_value = int(parts[2] or 0)

        lang_code = self.env.context.get('lang') or self.env.user.lang
        lang = self.env['res.lang'].search([('code', '=', lang_code)])
        amount_words = tools.ustr('{amt_value} {amt_word}').format(
                        amt_value=_num2words(integer_value, lang=lang.iso_code),
                        amt_word=self.currency_unit_label,
                        )
        if not self.is_zero(amount - integer_value):
            amount_words += ' ' + _('and') + tools.ustr(' {amt_value} {amt_word}').format(
                        amt_value=_num2words(fractional_value, lang=lang.iso_code),
                        amt_word=self.currency_subunit_label,
                        )
        return amount_words

    @api.multi
    def round(self, amount):
        """Return ``amount`` rounded  according to ``self``'s rounding rules.

           :param float amount: the amount to round
           :return: rounded float
        """
        # TODO: Need to check why it calls round() from sale.py, _amount_all() with *No* ID after below commits,
        # https://github.com/odoo/odoo/commit/36ee1ad813204dcb91e9f5f20d746dff6f080ac2
        # https://github.com/odoo/odoo/commit/0b6058c585d7d9a57bd7581b8211f20fca3ec3f7
        # Removing self.ensure_one() will make few test cases to break of modules event_sale, sale_mrp and stock_dropshipping.
        #self.ensure_one()
        return tools.float_round(amount, precision_rounding=self.rounding)

    @api.multi
    def compare_amounts(self, amount1, amount2):
        """Compare ``amount1`` and ``amount2`` after rounding them according to the
           given currency's precision..
           An amount is considered lower/greater than another amount if their rounded
           value is different. This is not the same as having a non-zero difference!

           For example 1.432 and 1.431 are equal at 2 digits precision,
           so this method would return 0.
           However 0.006 and 0.002 are considered different (returns 1) because
           they respectively round to 0.01 and 0.0, even though
           0.006-0.002 = 0.004 which would be considered zero at 2 digits precision.

           :param float amount1: first amount to compare
           :param float amount2: second amount to compare
           :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than,
                    equal to, or greater than ``amount2``, according to
                    ``currency``'s rounding.

           With the new API, call it like: ``currency.compare_amounts(amount1, amount2)``.
        """
        return tools.float_compare(amount1, amount2, precision_rounding=self.rounding)

    @api.multi
    def is_zero(self, amount):
        """Returns true if ``amount`` is small enough to be treated as
           zero according to current currency's rounding rules.
           Warning: ``is_zero(amount1-amount2)`` is not always equivalent to
           ``compare_amounts(amount1,amount2) == 0``, as the former will round after
           computing the difference, while the latter will round before, giving
           different results for e.g. 0.006 and 0.002 at 2 digits precision.

           :param float amount: amount to compare with currency's zero

           With the new API, call it like: ``currency.is_zero(amount)``.
        """
        return tools.float_is_zero(amount, precision_rounding=self.rounding)

    @api.model
    def _get_conversion_rate(self, from_currency, to_currency, company, date):
        currency_rates = (from_currency + to_currency)._get_rates(company, date)
        res = currency_rates.get(to_currency.id) / currency_rates.get(from_currency.id)
        return res

    def _convert(self, from_amount, to_currency, company, date, round=True):
        """Returns the converted amount of ``from_amount``` from the currency
           ``self`` to the currency ``to_currency`` for the given ``date`` and
           company.

           :param company: The company from which we retrieve the convertion rate
           :param date: The nearest date from which we retriev the conversion rate.
           :param round: Round the result or not
        """
        self, to_currency = self or to_currency, to_currency or self
        assert self, "convert amount from unknown currency"
        assert to_currency, "convert amount to unknown currency"
        assert company, "convert amount from unknown company"
        assert date, "convert amount from unknown date"
        # apply conversion rate
        if self == to_currency:
            to_amount = from_amount
        else:
            to_amount = from_amount * self._get_conversion_rate(self, to_currency, company, date)
        # apply rounding
        return to_currency.round(to_amount) if round else to_amount

    @api.model
    def _compute(self, from_currency, to_currency, from_amount, round=True):
        _logger.warning('The `_compute` method is deprecated. Use `_convert` instead')
        date = self._context.get('date') or fields.Date.today()
        company = self.env['res.company'].browse(self._context.get('company_id')) or self.env['res.users']._get_company()
        return from_currency._convert(from_amount, to_currency, company, date)

    @api.multi
    def compute(self, from_amount, to_currency, round=True):
        _logger.warning('The `compute` method is deprecated. Use `_convert` instead')
        date = self._context.get('date') or fields.Date.today()
        company = self.env['res.company'].browse(self._context.get('company_id')) or self.env['res.users']._get_company()
        return self._convert(from_amount, to_currency, company, date)

    def _select_companies_rates(self):
        return """
예제 #17
0
class LabelPrint(models.Model):
    _name = "label.print"

    name = fields.Char("Name", size=64, required=True, index=True)
    model_id = fields.Many2one('ir.model', 'Model', required=True, index=True)
    mode = fields.Selection([
        ('fields', u"Définir les champs"),
        ('template', u"Utiliser un template"),
    ], string=u"Mode de définition", default='template')
    template_id = fields.Many2one(comodel_name="ir.ui.view", help=u"""
    template Qweb définissant l'affichage de l'intérieur de l'étiquette
    ATTENTION: ce template doit avoir un ID externe pour être pris en compte.
        s'il n'en a pas (création à la volée) vous pouvez en générer un en exportant votre template
        le sélectionner en vue liste et faire action > exporter
    """)
    template_id_arch = fields.Text(related="template_id.arch_base", readonly=True)
    template_css_id = fields.Many2one(comodel_name="ir.ui.view", help=u"""
    template Qweb définissant la feuille de style de l'intérieur de l'étiquette
    ATTENTION: ce template doit avoir un ID externe pour être pris en compte.
        s'il n'en a pas (création à la volée) vous pouvez en générer un en exportant votre template
        le sélectionner en vue liste et faire action > exporter
    """)
    template_css_id_arch = fields.Text(related="template_css_id.arch_base", readonly=True)
    field_ids = fields.One2many("label.print.field", 'report_id', string='Fields', copy=True)
    ref_ir_act_report = fields.Many2one('ir.actions.act_window',
                                        'Sidebar action', readonly=True,
                                        help="""Sidebar action to make this
                                        template available on records
                                        of the related document model""")
    ref_ir_value = fields.Many2one('ir.values', 'Sidebar button',
                                   readonly=True,
                                   help="Sidebar button to open the \
                                   sidebar action")
    model_list = fields.Char('Model List', size=256)

    active = fields.Boolean(string=u"Active", default=True)
    label_brand_id = fields.Many2one(comodel_name='label.brand', string='Brand', required=True)
    label_config_id = fields.Many2one(comodel_name='label.config', string='Template', required=True)
    line_field_id = fields.Many2one(
        comodel_name='ir.model.fields', string=u"Champ des lignes",
        domain="[('id', 'in', line_field_domain and line_field_domain[0] and line_field_domain[0][2] or False)]")
    line_field_domain = fields.Many2many(comodel_name='ir.model.fields', compute='_compute_line_field_domain')
    line_field_name = fields.Char(string=u"Nom du champ de lignes", compute="_compute_line_model_id", store=True)
    line_model_id = fields.Many2one(
        comodel_name='ir.model', string=u"Modèle des lignes", compute="_compute_line_model_id", store=True)

    @api.depends('model_id')
    def _compute_line_field_domain(self):
        for record in self:
            field_ids_list = []
            if record.model_id:
                # récupérer les id de tous les champs O2M ou M2M de record.model_id
                fields = self.env['ir.model.fields'].search(
                    [('model', '=', record.model_id.model), ('ttype', 'in', ['one2many', 'many2many'])])
                field_ids_list = fields and fields.ids or []
            record.line_field_domain = field_ids_list

    @api.depends('line_field_id')
    def _compute_line_model_id(self):
        for record in self:
            if record.line_field_id:
                model_name = record.line_field_id.relation
                model_id = self.env['ir.model'].search([('model', '=', model_name)])
                record.line_model_id = model_id and model_id[0] or False
                record.line_field_name = record.line_field_id.name

    @api.onchange('model_id', 'line_field_id')
    def onchange_models_id(self):
        self.ensure_one()
        model_id = self.line_model_id or self.model_id or False
        model_list = []
        if model_id:
            model_obj = self.env['ir.model']
            current_model = model_id.model
            model_list.append(current_model)
            active_model_obj = self.env[model_id.model]
            if active_model_obj._inherits:
                for key, val in active_model_obj._inherits.items():
                    model_ids = model_obj.search([('model', '=', key)])
                    if model_ids:
                        model_list.append(key)
            field_code = []
            for field in self.field_ids:
                if isinstance(field.id, int) and field.field_id.model_id != model_id:
                    field_code.append((2, field.id, 0))
            self.field_ids = field_code
        self.model_list = model_list

    @api.onchange('model_id')
    def onchange_model_id(self):
        u"""Mettre à jour le domain des champs de lignes possibles"""
        field_ids_list = []
        if self.model_id:
            # récupérer les id de tous les champs O2M ou M2M de record.model_id
            fields = self.env['ir.model.fields'].search(
                [('model', '=', self.model_id.model), ('ttype', 'in', ['one2many', 'many2many'])])
            field_ids_list = fields and fields.ids or []
        self.line_field_domain = field_ids_list
        if self.line_field_id and self.line_field_id.id not in field_ids_list:
            self.line_field_id = False
        res = {'domain': {'line_field_id': [('id', 'in', field_ids_list)]}}
        return res

    @api.multi
    def create_action(self):
        vals = {}
        action_obj = self.env['ir.actions.act_window']
        for data in self.browse(self.ids):
            src_obj = data.model_id.model
            button_name = _('Label (%s)') % data.name
            vals['ref_ir_act_report'] = action_obj.create({
                'name': button_name,
                'type': 'ir.actions.act_window',
                'res_model': 'label.print.wizard',
                'src_model': src_obj,
                'view_type': 'form',
                'context': "{'label_print' : %d}" % (data.id),
                'view_mode': 'form,tree',
                'target': 'new',
            })
            id_temp = vals['ref_ir_act_report'].id
            vals['ref_ir_value'] = self.env['ir.values'].create({
                'name': button_name,
                'model': src_obj,
                'key2': 'client_action_multi',
                'value': "ir.actions.act_window," + str(id_temp),
                'object': True,
            })
        self.write({
            'ref_ir_act_report': vals.get('ref_ir_act_report', False).id,
            'ref_ir_value': vals.get('ref_ir_value', False).id,
        })
        return True

    @api.multi
    def unlink_action(self):
        for template in self:
            if template.ref_ir_act_report.id:
                template.ref_ir_act_report.unlink()
            if template.ref_ir_value.id:
                template.ref_ir_value.unlink()
        return True
class ImportLettersHistory(models.Model):
    """
    Keep history of imported letters.
    This class allows the user to import some letters (individually or in a
    zip file) in the database by doing an automatic analysis.
    The code is reading QR codes in order to detect child and partner codes
    for every letter, using the zxing library for code detection.
    """

    _name = "import.letters.history"
    _inherit = ["import.letter.config", "mail.thread"]
    _description = _("""History of the letters imported from a zip
    or a PDF/TIFF""")
    _order = "create_date desc"
    _rec_name = "create_date"

    ##########################################################################
    #                                 FIELDS                                 #
    ##########################################################################

    state = fields.Selection(
        [
            ("draft", _("Draft")),
            ("pending", _("Analyzing")),
            ("open", _("Open")),
            ("ready", _("Ready")),
            ("done", _("Done")),
        ],
        compute="_compute_state",
        store=True,
        track_visibility="onchange",
    )
    import_completed = fields.Boolean()
    nber_letters = fields.Integer("Number of files",
                                  readonly=True,
                                  compute="_compute_nber_letters")
    data = fields.Many2many("ir.attachment",
                            string="Add a file",
                            readonly=False)
    import_line_ids = fields.One2many(
        "import.letter.line",
        "import_id",
        "Files to process",
        ondelete="cascade",
        readonly=False,
    )
    letters_ids = fields.One2many("correspondence",
                                  "import_id",
                                  "Imported letters",
                                  readonly=True)
    config_id = fields.Many2one("import.letter.config",
                                "Import settings",
                                readonly=False)

    ##########################################################################
    #                             FIELDS METHODS                             #
    ##########################################################################
    @api.multi
    @api.depends(
        "import_line_ids",
        "import_line_ids.status",
        "letters_ids",
        "data",
        "import_completed",
    )
    def _compute_state(self):
        """ Check in which state self is by counting the number of elements in
        each Many2many
        """
        for import_letters in self:
            if import_letters.letters_ids:
                import_letters.state = "done"
            elif import_letters.import_completed:
                check = True
                for i in import_letters.import_line_ids:
                    if i.status != "ok":
                        check = False
                if check:
                    import_letters.state = "ready"
                else:
                    import_letters.state = "open"
            elif import_letters.import_line_ids:
                import_letters.state = "pending"
            else:
                import_letters.state = "draft"

    @api.onchange("data")
    def _compute_nber_letters(self):
        """
        Counts the number of scans. If a zip file is given, the number of
        scans inside is counted.
        """
        for letter in self:
            if letter.state in ("open", "pending", "ready"):
                letter.nber_letters = len(letter.import_line_ids)
            elif letter.state == "done":
                letter.nber_letters = len(letter.letters_ids)
            elif letter.state is False or letter.state == "draft":
                # counter
                tmp = 0
                # loop over all the attachments
                for attachment in letter.data:
                    # pdf or tiff case
                    if func.check_file(attachment.name) == 1:
                        tmp += 1
                    # zip case
                    elif func.is_zip(attachment.name):
                        # create a tempfile and read it
                        zip_file = BytesIO(
                            base64.b64decode(
                                attachment.with_context(bin_size=False).datas))
                        # catch ALL the exceptions that can be raised
                        # by class zipfile
                        try:
                            zip_ = zipfile.ZipFile(zip_file, "r")
                            list_file = zip_.namelist()
                            # loop over all files in zip
                            for tmp_file in list_file:
                                tmp += func.check_file(tmp_file) == 1
                        except zipfile.BadZipfile:
                            raise UserError(
                                _("Zip file corrupted (" + attachment.name +
                                  ")"))
                        except zipfile.LargeZipFile:
                            raise UserError(
                                _("Zip64 is not supported(" + attachment.name +
                                  ")"))

                letter.nber_letters = tmp
            else:
                raise UserError(
                    _("State: '%s' not implemented") % letter.state)

    ##########################################################################
    #                              ORM METHODS                               #
    ##########################################################################
    @api.model
    def create(self, vals):
        if vals.get("config_id"):
            other_import = self.search_count([("config_id", "=",
                                               vals["config_id"]),
                                              ("state", "!=", "done")])
            if other_import:
                raise UserError(
                    _("Another import with the same configuration is "
                      "already open. Please finish it before creating a new "
                      "one."))
        return super().create(vals)

    ##########################################################################
    #                             VIEW CALLBACKS                             #
    ##########################################################################
    @api.multi
    def button_import(self):
        """
        Analyze the attachment in order to create the letter's lines
        """
        for letters_import in self:
            if letters_import.data:
                letters_import.state = "pending"
                if self.env.context.get("async_mode", True):
                    letters_import.with_delay()._run_analyze()
                else:
                    letters_import._run_analyze()
        return True

    @api.multi
    def button_save(self):
        """
        save the import_line as a correspondence
        """
        # check if all the imports are OK
        for letters_h in self:
            if letters_h.state != "ready":
                raise UserError(_("Some letters are not ready"))
        # save the imports
        for letters in self:
            correspondence_vals = letters.import_line_ids.get_letter_data()
            # letters_ids should be empty before this line
            for vals in correspondence_vals:
                letters.letters_ids.create(vals)
            letters.import_line_ids.unlink()
        return True

    @api.multi
    def button_review(self):
        """ Returns a form view for import lines in order to browse them """
        self.ensure_one()
        return {
            "name": _("Review Imports"),
            "type": "ir.actions.act_window",
            "view_type": "form",
            "view_mode": "form",
            "res_model": "import.letters.review",
            "context":
            self.with_context(line_ids=self.import_line_ids.ids).env.context,
            "target": "current",
        }

    @api.onchange("config_id")
    def onchange_config(self):
        config = self.config_id
        if config:
            for field, val in list(
                    config.get_correspondence_metadata().items()):
                setattr(self, field, val)

    ##########################################################################
    #                             PRIVATE METHODS                            #
    ##########################################################################
    @job(default_channel="root.sbc_compassion")
    @related_action(action="related_action_s2b_imports")
    def _run_analyze(self):
        """
        Analyze each attachment:
        - check for duplicate file names and skip them
        - decompress zip file if necessary
        - call _analyze_attachment for every resulting file
        """
        self.ensure_one()
        # keep track of file names to detect duplicates
        file_name_history = []
        logger.info("Imported files analysis started...")
        progress = 1

        for attachment in self.data:
            if attachment.name not in file_name_history:
                file_name_history.append(attachment.name)
                file_data = base64.b64decode(
                    attachment.with_context(bin_size=False).datas)
                # check for zip
                if func.check_file(attachment.name) == 2:
                    zip_file = BytesIO(file_data)
                    zip_ = zipfile.ZipFile(zip_file, "r")
                    for f in zip_.namelist():
                        logger.debug(
                            f"Analyzing file {progress}/{self.nber_letters}")
                        self._analyze_attachment(zip_.read(f), f)
                        progress += 1
                # case with normal format (PDF,TIFF)
                elif func.check_file(attachment.name) == 1:
                    logger.debug(
                        f"Analyzing file {progress}/{self.nber_letters}")
                    self._analyze_attachment(file_data, attachment.name)
                    progress += 1
                else:
                    raise UserError(_("Only zip/pdf files are supported."))
            else:
                raise UserError(_("Two files are the same"))

        # remove all the files (now they are inside import_line_ids)
        self.data.unlink()
        self.import_completed = True
        logger.info("Imported files analysis completed.")

    def _analyze_attachment(self, file_data, file_name):
        line_vals = func.analyze_attachment(self.env, file_data, file_name,
                                            self.template_id)
        for i in range(0, len(line_vals)):
            line_vals[i]["import_id"] = self.id
            self.env["import.letter.line"].create(line_vals[i])
예제 #19
0
파일: property.py 프로젝트: rlfss/hoa
class HousingModel(models.Model):
    _name = 'housing.model'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _check_company_auto = True

    active = fields.Boolean(default=True)
    name = fields.Char(string="Model",
                       required=True,
                       track_visibility="always")
    floor_area = fields.Float(string="Floor Area", track_visibility="always")
    lot_area = fields.Float(string="Lot Area", track_visibility="always")
    lot_area_price = fields.Monetary(string="Lot Area Price",
                                     help="Lot Area Price")
    floor_area_price = fields.Monetary(string="Floor Area Price",
                                       help="Floor Area Price",
                                       track_visibility="always")
    miscellaneous_value = fields.Monetary(string="MCC2",
                                          default=9,
                                          help="Miscellaneous Absolute Value")
    miscellaneous_charge = fields.Float(string="MCC",
                                        default=9,
                                        help="Miscellaneous Charge (%)")
    reservation_fee = fields.Monetary(string="Reservation Fee")
    downpayment_percent = fields.Float(string="Downpayment",
                                       default="15",
                                       help="Downpayment Term (%)")
    # downpayment_term = fields.Integer(string="DP Term", default="12", help="Downpayment Term")
    property_type = fields.Selection([('House', 'House and Lot'),
                                      ('Condo', 'Condo Unit')],
                                     string="Property Type",
                                     default="House",
                                     required=True)

    model_type_id = fields.Many2one("property.model.type",
                                    string="Model Type",
                                    track_visibility="always")
    description = fields.Text(string="Description", track_visibility="always")
    year_month = fields.Char(string="Year Month", track_visibility="always")
    model_blue_print = fields.Binary(string="Plan", track_visibility="always")
    house_model_image_ids = fields.One2many('product.image',
                                            'housing_model_tmpl_id',
                                            string='Images')
    brand_id = fields.Many2one('product.brand', string='Brand')
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 required=True,
                                 index=True,
                                 default=lambda self: self.env.company)
    currency_id = fields.Many2one('res.currency',
                                  string="Currency",
                                  related="company_id.currency_id",
                                  check_company=True)

    def unlink(self):
        for rec in self:
            property = rec.env['property.detail'].search([('house_model_id',
                                                           '=', rec.id)]).ids
            property += rec.env[
                'property.subdivision.phase.unit.model'].search([
                    ('house_model_id', '=', rec.id)
                ]).ids
            if property:
                raise ValidationError(
                    _('Delete first the property associated to this Subdivision Phase'
                      ))
        return super(HousingModel, self).unlink()
예제 #20
0
class ProductProduct(models.Model):
    _name = "product.product"
    _description = "Product"
    _inherits = {'product.template': 'product_tmpl_id'}
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'default_code, name, id'

    # price: total price, context dependent (partner, pricelist, quantity)
    price = fields.Float(
        'Price', compute='_compute_product_price',
        digits='Product Price', inverse='_set_product_price')
    # price_extra: catalog extra value only, sum of variant extra attributes
    price_extra = fields.Float(
        'Variant Price Extra', compute='_compute_product_price_extra',
        digits='Product Price',
        help="This is the sum of the extra price of all attributes")
    # lst_price: catalog value + extra, context dependent (uom)
    lst_price = fields.Float(
        'Public Price', compute='_compute_product_lst_price',
        digits='Product Price', inverse='_set_product_lst_price',
        help="The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices.")

    default_code = fields.Char('Internal Reference', index=True)
    code = fields.Char('Reference', compute='_compute_product_code')
    partner_ref = fields.Char('Customer Ref', compute='_compute_partner_ref')

    active = fields.Boolean(
        'Active', default=True,
        help="If unchecked, it will allow you to hide the product without removing it.")
    product_tmpl_id = fields.Many2one(
        'product.template', 'Product Template',
        auto_join=True, index=True, ondelete="cascade", required=True)
    barcode = fields.Char(
        'Barcode', copy=False,
        help="International Article Number used for product identification.")
    product_template_attribute_value_ids = fields.Many2many('product.template.attribute.value', relation='product_variant_combination', string="Attribute Values", ondelete='restrict')
    combination_indices = fields.Char(compute='_compute_combination_indices', store=True, index=True)
    is_product_variant = fields.Boolean(compute='_compute_is_product_variant')

    standard_price = fields.Float(
        'Cost', company_dependent=True,
        digits='Product Price',
        groups="base.group_user",
        help="""In Standard Price & AVCO: value of the product (automatically computed in AVCO).
        In FIFO: value of the last unit that left the stock (automatically computed).
        Used to value the product when the purchase cost is not known (e.g. inventory adjustment).
        Used to compute margins on sale orders.""")
    volume = fields.Float('Volume', digits='Volume')
    weight = fields.Float('Weight', digits='Stock Weight')

    pricelist_item_count = fields.Integer("Number of price rules", compute="_compute_variant_item_count")

    packaging_ids = fields.One2many(
        'product.packaging', 'product_id', 'Product Packages',
        help="Gives the different ways to package the same product.")

    # all image fields are base64 encoded and PIL-supported

    # all image_variant fields are technical and should not be displayed to the user
    image_variant_1920 = fields.Image("Variant Image", max_width=1920, max_height=1920)

    # resized fields stored (as attachment) for performance
    image_variant_1024 = fields.Image("Variant Image 1024", related="image_variant_1920", max_width=1024, max_height=1024, store=True)
    image_variant_512 = fields.Image("Variant Image 512", related="image_variant_1920", max_width=512, max_height=512, store=True)
    image_variant_256 = fields.Image("Variant Image 256", related="image_variant_1920", max_width=256, max_height=256, store=True)
    image_variant_128 = fields.Image("Variant Image 128", related="image_variant_1920", max_width=128, max_height=128, store=True)
    can_image_variant_1024_be_zoomed = fields.Boolean("Can Variant Image 1024 be zoomed", compute='_compute_can_image_variant_1024_be_zoomed', store=True)

    # Computed fields that are used to create a fallback to the template if
    # necessary, it's recommended to display those fields to the user.
    image_1920 = fields.Image("Image", compute='_compute_image_1920', inverse='_set_image_1920')
    image_1024 = fields.Image("Image 1024", compute='_compute_image_1024')
    image_512 = fields.Image("Image 512", compute='_compute_image_512')
    image_256 = fields.Image("Image 256", compute='_compute_image_256')
    image_128 = fields.Image("Image 128", compute='_compute_image_128')
    can_image_1024_be_zoomed = fields.Boolean("Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed')

    @api.depends('image_variant_1920', 'image_variant_1024')
    def _compute_can_image_variant_1024_be_zoomed(self):
        for record in self:
            record.can_image_variant_1024_be_zoomed = record.image_variant_1920 and tools.is_image_size_above(record.image_variant_1920, record.image_variant_1024)

    def _compute_image_1920(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_1920 = record.image_variant_1920 or record.product_tmpl_id.image_1920

    def _set_image_1920(self):
        for record in self:
            if (
                # We are trying to remove an image even though it is already
                # not set, remove it from the template instead.
                not record.image_1920 and not record.image_variant_1920 or
                # We are trying to add an image, but the template image is
                # not set, write on the template instead.
                record.image_1920 and not record.product_tmpl_id.image_1920 or
                # There is only one variant, always write on the template.
                self.search_count([
                    ('product_tmpl_id', '=', record.product_tmpl_id.id),
                    ('active', '=', True),
                ]) <= 1
            ):
                record.image_variant_1920 = False
                record.product_tmpl_id.image_1920 = record.image_1920
            else:
                record.image_variant_1920 = record.image_1920

    def _compute_image_1024(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_1024 = record.image_variant_1024 or record.product_tmpl_id.image_1024

    def _compute_image_512(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_512 = record.image_variant_512 or record.product_tmpl_id.image_512

    def _compute_image_256(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_256 = record.image_variant_256 or record.product_tmpl_id.image_256

    def _compute_image_128(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.image_128 = record.image_variant_128 or record.product_tmpl_id.image_128

    def _compute_can_image_1024_be_zoomed(self):
        """Get the image from the template if no image is set on the variant."""
        for record in self:
            record.can_image_1024_be_zoomed = record.can_image_variant_1024_be_zoomed if record.image_variant_1920 else record.product_tmpl_id.can_image_1024_be_zoomed

    def init(self):
        """Ensure there is at most one active variant for each combination.

        There could be no variant for a combination if using dynamic attributes.
        """
        self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS product_product_combination_unique ON %s (product_tmpl_id, combination_indices) WHERE active is true"
            % self._table)

    _sql_constraints = [
        ('barcode_uniq', 'unique(barcode)', "A barcode can only be assigned to one product !"),
    ]

    def _get_invoice_policy(self):
        return False

    @api.depends('product_template_attribute_value_ids')
    def _compute_combination_indices(self):
        for product in self:
            product.combination_indices = product.product_template_attribute_value_ids._ids2str()

    def _compute_is_product_variant(self):
        for product in self:
            product.is_product_variant = True

    @api.depends_context('pricelist', 'partner', 'quantity', 'uom', 'date', 'no_variant_attributes_price_extra')
    def _compute_product_price(self):
        prices = {}
        pricelist_id_or_name = self._context.get('pricelist')
        if pricelist_id_or_name:
            pricelist = None
            partner = self.env.context.get('partner', False)
            quantity = self.env.context.get('quantity', 1.0)

            # Support context pricelists specified as list, display_name or ID for compatibility
            if isinstance(pricelist_id_or_name, list):
                pricelist_id_or_name = pricelist_id_or_name[0]
            if isinstance(pricelist_id_or_name, str):
                pricelist_name_search = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1)
                if pricelist_name_search:
                    pricelist = self.env['product.pricelist'].browse([pricelist_name_search[0][0]])
            elif isinstance(pricelist_id_or_name, int):
                pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name)

            if pricelist:
                quantities = [quantity] * len(self)
                partners = [partner] * len(self)
                prices = pricelist.get_products_price(self, quantities, partners)

        for product in self:
            product.price = prices.get(product.id, 0.0)

    def _set_product_price(self):
        for product in self:
            if self._context.get('uom'):
                value = self.env['uom.uom'].browse(self._context['uom'])._compute_price(product.price, product.uom_id)
            else:
                value = product.price
            value -= product.price_extra
            product.write({'list_price': value})

    def _set_product_lst_price(self):
        for product in self:
            if self._context.get('uom'):
                value = self.env['uom.uom'].browse(self._context['uom'])._compute_price(product.lst_price, product.uom_id)
            else:
                value = product.lst_price
            value -= product.price_extra
            product.write({'list_price': value})

    def _compute_product_price_extra(self):
        for product in self:
            product.price_extra = sum(product.product_template_attribute_value_ids.mapped('price_extra'))

    @api.depends('list_price', 'price_extra')
    @api.depends_context('uom')
    def _compute_product_lst_price(self):
        to_uom = None
        if 'uom' in self._context:
            to_uom = self.env['uom.uom'].browse(self._context['uom'])

        for product in self:
            if to_uom:
                list_price = product.uom_id._compute_price(product.list_price, to_uom)
            else:
                list_price = product.list_price
            product.lst_price = list_price + product.price_extra

    @api.depends_context('partner_id')
    def _compute_product_code(self):
        for product in self:
            for supplier_info in product.seller_ids:
                if supplier_info.name.id == product._context.get('partner_id'):
                    product.code = supplier_info.product_code or product.default_code
                    break
            else:
                product.code = product.default_code

    @api.depends_context('partner_id')
    def _compute_partner_ref(self):
        for product in self:
            for supplier_info in product.seller_ids:
                if supplier_info.name.id == product._context.get('partner_id'):
                    product_name = supplier_info.product_name or product.default_code or product.name
                    product.partner_ref = '%s%s' % (product.code and '[%s] ' % product.code or '', product_name)
                    break
            else:
                product.partner_ref = product.display_name

    def _compute_variant_item_count(self):
        for product in self:
            domain = ['|',
                '&', ('product_tmpl_id', '=', product.product_tmpl_id.id), ('applied_on', '=', '1_product'),
                '&', ('product_id', '=', product.id), ('applied_on', '=', '0_product_variant')]
            product.pricelist_item_count = self.env['product.pricelist.item'].search_count(domain)

    @api.onchange('uom_id', 'uom_po_id')
    def _onchange_uom(self):
        if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id:
            self.uom_po_id = self.uom_id

    @api.model_create_multi
    def create(self, vals_list):
        products = super(ProductProduct, self.with_context(create_product_product=True)).create(vals_list)
        # `_get_variant_id_for_combination` depends on existing variants
        self.clear_caches()
        return products

    def write(self, values):
        res = super(ProductProduct, self).write(values)
        if 'product_template_attribute_value_ids' in values:
            # `_get_variant_id_for_combination` depends on `product_template_attribute_value_ids`
            self.clear_caches()
        if 'active' in values:
            # prefetched o2m have to be reloaded (because of active_test)
            # (eg. product.template: product_variant_ids)
            self.flush()
            self.invalidate_cache()
            # `_get_first_possible_variant_id` depends on variants active state
            self.clear_caches()
        return res

    def unlink(self):
        unlink_products = self.env['product.product']
        unlink_templates = self.env['product.template']
        for product in self:
            # If there is an image set on the variant and no image set on the
            # template, move the image to the template.
            if product.image_variant_1920 and not product.product_tmpl_id.image_1920:
                product.product_tmpl_id.image_1920 = product.image_variant_1920
            # Check if product still exists, in case it has been unlinked by unlinking its template
            if not product.exists():
                continue
            # Check if the product is last product of this template...
            other_products = self.search([('product_tmpl_id', '=', product.product_tmpl_id.id), ('id', '!=', product.id)])
            # ... and do not delete product template if it's configured to be created "on demand"
            if not other_products and not product.product_tmpl_id.has_dynamic_attributes():
                unlink_templates |= product.product_tmpl_id
            unlink_products |= product
        res = super(ProductProduct, unlink_products).unlink()
        # delete templates after calling super, as deleting template could lead to deleting
        # products due to ondelete='cascade'
        unlink_templates.unlink()
        # `_get_variant_id_for_combination` depends on existing variants
        self.clear_caches()
        return res

    def _unlink_or_archive(self, check_access=True):
        """Unlink or archive products.
        Try in batch as much as possible because it is much faster.
        Use dichotomy when an exception occurs.
        """

        # Avoid access errors in case the products is shared amongst companies
        # but the underlying objects are not. If unlink fails because of an
        # AccessError (e.g. while recomputing fields), the 'write' call will
        # fail as well for the same reason since the field has been set to
        # recompute.
        if check_access:
            self.check_access_rights('unlink')
            self.check_access_rule('unlink')
            self.check_access_rights('write')
            self.check_access_rule('write')
            self = self.sudo()

        try:
            with self.env.cr.savepoint(), tools.mute_logger('odoo.sql_db'):
                self.unlink()
        except Exception:
            # We catch all kind of exceptions to be sure that the operation
            # doesn't fail.
            if len(self) > 1:
                self[:len(self) // 2]._unlink_or_archive(check_access=False)
                self[len(self) // 2:]._unlink_or_archive(check_access=False)
            else:
                if self.active:
                    # Note: this can still fail if something is preventing
                    # from archiving.
                    # This is the case from existing stock reordering rules.
                    self.write({'active': False})

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        """Variants are generated depending on the configuration of attributes
        and values on the template, so copying them does not make sense.

        For convenience the template is copied instead and its first variant is
        returned.
        """
        return self.product_tmpl_id.copy(default=default).product_variant_id

    @api.model
    def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
        # TDE FIXME: strange
        if self._context.get('search_default_categ_id'):
            args.append((('categ_id', 'child_of', self._context['search_default_categ_id'])))
        return super(ProductProduct, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)

    def name_get(self):
        # TDE: this could be cleaned a bit I think

        def _name_get(d):
            name = d.get('name', '')
            code = self._context.get('display_default_code', True) and d.get('default_code', False) or False
            if code:
                name = '[%s] %s' % (code,name)
            return (d['id'], name)

        partner_id = self._context.get('partner_id')
        if partner_id:
            partner_ids = [partner_id, self.env['res.partner'].browse(partner_id).commercial_partner_id.id]
        else:
            partner_ids = []
        company_id = self.env.context.get('company_id')

        # all user don't have access to seller and partner
        # check access and use superuser
        self.check_access_rights("read")
        self.check_access_rule("read")

        result = []

        # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
        # Use `load=False` to not call `name_get` for the `product_tmpl_id`
        self.sudo().read(['name', 'default_code', 'product_tmpl_id'], load=False)

        product_template_ids = self.sudo().mapped('product_tmpl_id').ids

        if partner_ids:
            supplier_info = self.env['product.supplierinfo'].sudo().search([
                ('product_tmpl_id', 'in', product_template_ids),
                ('name', 'in', partner_ids),
            ])
            # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields
            # Use `load=False` to not call `name_get` for the `product_tmpl_id` and `product_id`
            supplier_info.sudo().read(['product_tmpl_id', 'product_id', 'product_name', 'product_code'], load=False)
            supplier_info_by_template = {}
            for r in supplier_info:
                supplier_info_by_template.setdefault(r.product_tmpl_id, []).append(r)
        for product in self.sudo():
            variant = product.product_template_attribute_value_ids._get_combination_name()

            name = variant and "%s (%s)" % (product.name, variant) or product.name
            sellers = []
            if partner_ids:
                product_supplier_info = supplier_info_by_template.get(product.product_tmpl_id, [])
                sellers = [x for x in product_supplier_info if x.product_id and x.product_id == product]
                if not sellers:
                    sellers = [x for x in product_supplier_info if not x.product_id]
                # Filter out sellers based on the company. This is done afterwards for a better
                # code readability. At this point, only a few sellers should remain, so it should
                # not be a performance issue.
                if company_id:
                    sellers = [x for x in sellers if x.company_id.id in [company_id, False]]
            if sellers:
                for s in sellers:
                    seller_variant = s.product_name and (
                        variant and "%s (%s)" % (s.product_name, variant) or s.product_name
                        ) or False
                    mydict = {
                              'id': product.id,
                              'name': seller_variant or name,
                              'default_code': s.product_code or product.default_code,
                              }
                    temp = _name_get(mydict)
                    if temp not in result:
                        result.append(temp)
            else:
                mydict = {
                          'id': product.id,
                          'name': name,
                          'default_code': product.default_code,
                          }
                result.append(_name_get(mydict))
        return result

    @api.model
    def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None):
        if not args:
            args = []
        if name:
            positive_operators = ['=', 'ilike', '=ilike', 'like', '=like']
            product_ids = []
            if operator in positive_operators:
                product_ids = self._search([('default_code', '=', name)] + args, limit=limit, access_rights_uid=name_get_uid)
                if not product_ids:
                    product_ids = self._search([('barcode', '=', name)] + args, limit=limit, access_rights_uid=name_get_uid)
            if not product_ids and operator not in expression.NEGATIVE_TERM_OPERATORS:
                # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
                # on a database with thousands of matching products, due to the huge merge+unique needed for the
                # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
                # Performing a quick memory merge of ids in Python will give much better performance
                product_ids = self._search(args + [('default_code', operator, name)], limit=limit)
                if not limit or len(product_ids) < limit:
                    # we may underrun the limit because of dupes in the results, that's fine
                    limit2 = (limit - len(product_ids)) if limit else False
                    product2_ids = self._search(args + [('name', operator, name), ('id', 'not in', product_ids)], limit=limit2, access_rights_uid=name_get_uid)
                    product_ids.extend(product2_ids)
            elif not product_ids and operator in expression.NEGATIVE_TERM_OPERATORS:
                domain = expression.OR([
                    ['&', ('default_code', operator, name), ('name', operator, name)],
                    ['&', ('default_code', '=', False), ('name', operator, name)],
                ])
                domain = expression.AND([args, domain])
                product_ids = self._search(domain, limit=limit, access_rights_uid=name_get_uid)
            if not product_ids and operator in positive_operators:
                ptrn = re.compile('(\[(.*?)\])')
                res = ptrn.search(name)
                if res:
                    product_ids = self._search([('default_code', '=', res.group(2))] + args, limit=limit, access_rights_uid=name_get_uid)
            # still no results, partner in context: search on supplier info as last hope to find something
            if not product_ids and self._context.get('partner_id'):
                suppliers_ids = self.env['product.supplierinfo']._search([
                    ('name', '=', self._context.get('partner_id')),
                    '|',
                    ('product_code', operator, name),
                    ('product_name', operator, name)], access_rights_uid=name_get_uid)
                if suppliers_ids:
                    product_ids = self._search([('product_tmpl_id.seller_ids', 'in', suppliers_ids)], limit=limit, access_rights_uid=name_get_uid)
        else:
            product_ids = self._search(args, limit=limit, access_rights_uid=name_get_uid)
        return models.lazy_name_get(self.browse(product_ids).with_user(name_get_uid))

    @api.model
    def view_header_get(self, view_id, view_type):
        res = super(ProductProduct, self).view_header_get(view_id, view_type)
        if self._context.get('categ_id'):
            return _('Products: ') + self.env['product.category'].browse(self._context['categ_id']).name
        return res

    def open_pricelist_rules(self):
        self.ensure_one()
        domain = ['|',
            '&', ('product_tmpl_id', '=', self.product_tmpl_id.id), ('applied_on', '=', '1_product'),
            '&', ('product_id', '=', self.id), ('applied_on', '=', '0_product_variant')]
        return {
            'name': _('Price Rules'),
            'view_mode': 'tree,form',
            'views': [(self.env.ref('product.product_pricelist_item_tree_view_from_product').id, 'tree'), (False, 'form')],
            'res_model': 'product.pricelist.item',
            'type': 'ir.actions.act_window',
            'target': 'current',
            'domain': domain,
            'context': {
                'default_product_id': self.id,
                'default_applied_on': '0_product_variant',
            }
        }

    def open_product_template(self):
        """ Utility method used to add an "Open Template" button in product views """
        self.ensure_one()
        return {'type': 'ir.actions.act_window',
                'res_model': 'product.template',
                'view_mode': 'form',
                'res_id': self.product_tmpl_id.id,
                'target': 'new'}

    def _prepare_sellers(self, params):
        # This search is made to avoid retrieving seller_ids from the cache.
        return self.env['product.supplierinfo'].search([('product_tmpl_id', '=', self.product_tmpl_id.id),
                                                        ('name.active', '=', True)]).sorted(lambda s: (s.sequence, -s.min_qty, s.price, s.id))

    def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False):
        self.ensure_one()
        if date is None:
            date = fields.Date.context_today(self)
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')

        res = self.env['product.supplierinfo']
        sellers = self._prepare_sellers(params)
        if self.env.context.get('force_company'):
            sellers = sellers.filtered(lambda s: not s.company_id or s.company_id.id == self.env.context['force_company'])
        for seller in sellers:
            # Set quantity in UoM of seller
            quantity_uom_seller = quantity
            if quantity_uom_seller and uom_id and uom_id != seller.product_uom:
                quantity_uom_seller = uom_id._compute_quantity(quantity_uom_seller, seller.product_uom)

            if seller.date_start and seller.date_start > date:
                continue
            if seller.date_end and seller.date_end < date:
                continue
            if partner_id and seller.name not in [partner_id, partner_id.parent_id]:
                continue
            if float_compare(quantity_uom_seller, seller.min_qty, precision_digits=precision) == -1:
                continue
            if seller.product_id and seller.product_id != self:
                continue
            if not res or res.name == seller.name:
                res |= seller
        return res.sorted('price')[:1]

    def price_compute(self, price_type, uom=False, currency=False, company=False):
        # TDE FIXME: delegate to template or not ? fields are reencoded here ...
        # compatibility about context keys used a bit everywhere in the code
        if not uom and self._context.get('uom'):
            uom = self.env['uom.uom'].browse(self._context['uom'])
        if not currency and self._context.get('currency'):
            currency = self.env['res.currency'].browse(self._context['currency'])

        products = self
        if price_type == 'standard_price':
            # standard_price field can only be seen by users in base.group_user
            # Thus, in order to compute the sale price from the cost for users not in this group
            # We fetch the standard price as the superuser
            products = self.with_context(force_company=company and company.id or self._context.get('force_company', self.env.company.id)).sudo()

        prices = dict.fromkeys(self.ids, 0.0)
        for product in products:
            prices[product.id] = product[price_type] or 0.0
            if price_type == 'list_price':
                prices[product.id] += product.price_extra
                # we need to add the price from the attributes that do not generate variants
                # (see field product.attribute create_variant)
                if self._context.get('no_variant_attributes_price_extra'):
                    # we have a list of price_extra that comes from the attribute values, we need to sum all that
                    prices[product.id] += sum(self._context.get('no_variant_attributes_price_extra'))

            if uom:
                prices[product.id] = product.uom_id._compute_price(prices[product.id], uom)

            # Convert from current user company currency to asked one
            # This is right cause a field cannot be in more than one currency
            if currency:
                prices[product.id] = product.currency_id._convert(
                    prices[product.id], currency, product.company_id, fields.Date.today())

        return prices

    @api.model
    def get_empty_list_help(self, help):
        self = self.with_context(
            empty_list_help_document_name=_("product"),
        )
        return super(ProductProduct, self).get_empty_list_help(help)

    def get_product_multiline_description_sale(self):
        """ Compute a multiline description of this product, in the context of sales
                (do not use for purchases or other display reasons that don't intend to use "description_sale").
            It will often be used as the default description of a sale order line referencing this product.
        """
        name = self.display_name
        if self.description_sale:
            name += '\n' + self.description_sale

        return name

    def _is_variant_possible(self, parent_combination=None):
        """Return whether the variant is possible based on its own combination,
        and optionally a parent combination.

        See `_is_combination_possible` for more information.

        This will always exclude variants for templates that have `no_variant`
        attributes because the variant itself will not be the full combination.

        :param parent_combination: combination from which `self` is an
            optional or accessory product.
        :type parent_combination: recordset `product.template.attribute.value`

        :return: ẁhether the variant is possible based on its own combination
        :rtype: bool
        """
        self.ensure_one()
        return self.product_tmpl_id._is_combination_possible(self.product_template_attribute_value_ids, parent_combination=parent_combination)

    def toggle_active(self):
        """ Archiving related product.template if there is only one active product.product """
        with_one_active = self.filtered(lambda product: len(product.product_tmpl_id.with_context(active_test=False).product_variant_ids) == 1)
        for product in with_one_active:
            product.product_tmpl_id.toggle_active()
        return super(ProductProduct, self - with_one_active).toggle_active()
예제 #21
0
class MisReportInstance(models.Model):
    """The MIS report instance combines everything to compute
    a MIS report template for a set of periods."""
    @api.depends("date")
    def _compute_pivot_date(self):
        for record in self:
            if record.date:
                record.pivot_date = record.date
            else:
                record.pivot_date = fields.Date.context_today(record)

    @api.model
    def _default_company_id(self):
        default_company_id = self.env["res.company"]._company_default_get(
            "mis.report.instance")
        return default_company_id

    _name = "mis.report.instance"
    _description = "MIS Report Instance"

    name = fields.Char(required=True, string="Name", translate=True)
    description = fields.Char(related="report_id.description", readonly=True)
    date = fields.Date(string="Base date",
                       help="Report base date "
                       "(leave empty to use current date)")
    pivot_date = fields.Date(compute="_compute_pivot_date",
                             string="Pivot date")
    report_id = fields.Many2one("mis.report", required=True, string="Report")
    period_ids = fields.One2many(
        comodel_name="mis.report.instance.period",
        inverse_name="report_instance_id",
        required=True,
        string="Periods",
        copy=True,
    )
    target_move = fields.Selection(
        [("posted", "All Posted Entries"), ("all", "All Entries")],
        string="Target Moves",
        required=True,
        default="posted",
    )
    company_id = fields.Many2one(
        comodel_name="res.company",
        string="Company",
        default=_default_company_id,
        required=True,
    )
    multi_company = fields.Boolean(
        string="Multiple companies",
        help="Check if you wish to specify "
        "children companies to be searched for data.",
        default=False,
    )
    company_ids = fields.Many2many(
        comodel_name="res.company",
        string="Companies",
        help="Select companies for which data will be searched.",
    )
    query_company_ids = fields.Many2many(
        comodel_name="res.company",
        compute="_compute_query_company_ids",
        help="Companies for which data will be searched.",
    )
    currency_id = fields.Many2one(
        comodel_name="res.currency",
        string="Currency",
        help="Select target currency for the report. "
        "Required if companies have different currencies.",
        required=False,
    )
    landscape_pdf = fields.Boolean(string="Landscape PDF")
    no_auto_expand_accounts = fields.Boolean(
        string="Disable account details expansion")
    display_columns_description = fields.Boolean(
        help="Display the date range details in the column headers.")
    comparison_mode = fields.Boolean(compute="_compute_comparison_mode",
                                     inverse="_inverse_comparison_mode")
    date_range_id = fields.Many2one(comodel_name="date.range",
                                    string="Date Range")
    date_from = fields.Date(string="From")
    date_to = fields.Date(string="To")
    temporary = fields.Boolean(default=False)
    analytic_account_id = fields.Many2one(
        comodel_name="account.analytic.account",
        string="Analytic Account",
        oldname="account_analytic_id",
    )
    analytic_tag_ids = fields.Many2many(comodel_name="account.analytic.tag",
                                        string="Analytic Tags")
    hide_analytic_filters = fields.Boolean(default=True)

    @api.onchange("company_id", "multi_company")
    def _onchange_company(self):
        if self.company_id and self.multi_company:
            self.company_ids = self.env["res.company"].search([
                ("id", "child_of", self.company_id.id)
            ])
        else:
            self.company_ids = False

    @api.depends("multi_company", "company_id", "company_ids")
    def _compute_query_company_ids(self):
        for rec in self:
            if rec.multi_company:
                rec.query_company_ids = rec.company_ids or rec.company_id
            else:
                rec.query_company_ids = rec.company_id

    @api.model
    def get_filter_descriptions_from_context(self):
        filters = self.env.context.get("mis_report_filters", {})
        analytic_account_id = filters.get("analytic_account_id",
                                          {}).get("value")
        filter_descriptions = []
        if analytic_account_id:
            analytic_account = self.env["account.analytic.account"].browse(
                analytic_account_id)
            filter_descriptions.append(
                _("Analytic Account: %s") % analytic_account.display_name)
        analytic_tag_value = filters.get("analytic_tag_ids", {}).get("value")
        if analytic_tag_value:
            analytic_tag_names = self.resolve_2many_commands(
                "analytic_tag_ids", analytic_tag_value, ["name"])
            filter_descriptions.append(
                _("Analytic Tags: %s") %
                ", ".join([rec["name"] for rec in analytic_tag_names]))
        return filter_descriptions

    def save_report(self):
        self.ensure_one()
        self.write({"temporary": False})
        action = self.env.ref("mis_builder.mis_report_instance_view_action")
        res = action.read()[0]
        view = self.env.ref("mis_builder.mis_report_instance_view_form")
        res.update({"views": [(view.id, "form")], "res_id": self.id})
        return res

    @api.model
    def _vacuum_report(self, hours=24):
        clear_date = fields.Datetime.to_string(datetime.datetime.now() -
                                               datetime.timedelta(hours=hours))
        reports = self.search([("write_date", "<", clear_date),
                               ("temporary", "=", True)])
        _logger.debug("Vacuum %s Temporary MIS Builder Report", len(reports))
        return reports.unlink()

    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {})
        default["name"] = _("%s (copy)") % self.name
        return super(MisReportInstance, self).copy(default)

    def _format_date(self, date):
        # format date following user language
        lang_model = self.env["res.lang"]
        lang = lang_model._lang_get(self.env.user.lang)
        date_format = lang.date_format
        return datetime.datetime.strftime(fields.Date.from_string(date),
                                          date_format)

    @api.depends("date_from")
    def _compute_comparison_mode(self):
        for instance in self:
            instance.comparison_mode = bool(
                instance.period_ids) and not bool(instance.date_from)

    def _inverse_comparison_mode(self):
        for record in self:
            if not record.comparison_mode:
                if not record.date_from:
                    record.date_from = fields.Date.context_today(self)
                if not record.date_to:
                    record.date_to = fields.Date.context_today(self)
                record.period_ids.unlink()
                record.write({"period_ids": [(0, 0, {"name": "Default"})]})
            else:
                record.date_from = None
                record.date_to = None

    @api.onchange("date_range_id")
    def _onchange_date_range(self):
        if self.date_range_id:
            self.date_from = self.date_range_id.date_start
            self.date_to = self.date_range_id.date_end

    @api.onchange("date_from", "date_to")
    def _onchange_dates(self):
        if self.date_range_id:
            if (self.date_from != self.date_range_id.date_start
                    or self.date_to != self.date_range_id.date_end):
                self.date_range_id = False

    def _add_analytic_filters_to_context(self, context):
        self.ensure_one()
        if self.analytic_account_id:
            context["mis_report_filters"]["analytic_account_id"] = {
                "value": self.analytic_account_id.id,
                "operator": "=",
            }
        if self.analytic_tag_ids:
            context["mis_report_filters"]["analytic_tag_ids"] = {
                "value": self.analytic_tag_ids.ids,
                "operator": "all",
            }

    def _context_with_filters(self):
        self.ensure_one()
        if "mis_report_filters" in self.env.context:
            # analytic filters are already in context, do nothing
            return self.env.context
        context = dict(self.env.context, mis_report_filters={})
        self._add_analytic_filters_to_context(context)
        return context

    def preview(self):
        self.ensure_one()
        view_id = self.env.ref("mis_builder."
                               "mis_report_instance_result_view_form")
        return {
            "type": "ir.actions.act_window",
            "res_model": "mis.report.instance",
            "res_id": self.id,
            "view_mode": "form",
            "view_type": "form",
            "view_id": view_id.id,
            "target": "current",
            "context": self._context_with_filters(),
        }

    def print_pdf(self):
        self.ensure_one()
        context = dict(self._context_with_filters(),
                       landscape=self.landscape_pdf)
        return (self.env.ref("mis_builder.qweb_pdf_export").with_context(
            context).report_action(
                self, data=dict(dummy=True))  # required to propagate context
                )

    def export_xls(self):
        self.ensure_one()
        context = dict(self._context_with_filters())
        return (self.env.ref("mis_builder.xls_export").with_context(
            context).report_action(
                self, data=dict(dummy=True))  # required to propagate context
                )

    def display_settings(self):
        assert len(self.ids) <= 1
        view_id = self.env.ref("mis_builder.mis_report_instance_view_form")
        return {
            "type": "ir.actions.act_window",
            "res_model": "mis.report.instance",
            "res_id": self.id if self.id else False,
            "view_mode": "form",
            "view_type": "form",
            "views": [(view_id.id, "form")],
            "view_id": view_id.id,
            "target": "current",
        }

    def _add_column_move_lines(self, aep, kpi_matrix, period, label,
                               description):
        if not period.date_from or not period.date_to:
            raise UserError(
                _("Column %s with move lines source must have from/to dates.")
                % (period.name, ))
        expression_evaluator = ExpressionEvaluator(
            aep,
            period.date_from,
            period.date_to,
            None,  # target_move now part of additional_move_line_filter
            period._get_additional_move_line_filter(),
            period._get_aml_model_name(),
        )
        self.report_id._declare_and_compute_period(
            expression_evaluator,
            kpi_matrix,
            period.id,
            label,
            description,
            period.subkpi_ids,
            period._get_additional_query_filter,
            no_auto_expand_accounts=self.no_auto_expand_accounts,
        )

    def _add_column_sumcol(self, aep, kpi_matrix, period, label, description):
        kpi_matrix.declare_sum(
            period.id,
            [(c.sign, c.period_to_sum_id.id)
             for c in period.source_sumcol_ids],
            label,
            description,
            period.source_sumcol_accdet,
        )

    def _add_column_cmpcol(self, aep, kpi_matrix, period, label, description):
        kpi_matrix.declare_comparison(
            period.id,
            period.source_cmpcol_to_id.id,
            period.source_cmpcol_from_id.id,
            label,
            description,
        )

    def _add_column(self, aep, kpi_matrix, period, label, description):
        if period.source == SRC_ACTUALS:
            return self._add_column_move_lines(aep, kpi_matrix, period, label,
                                               description)
        elif period.source == SRC_ACTUALS_ALT:
            return self._add_column_move_lines(aep, kpi_matrix, period, label,
                                               description)
        elif period.source == SRC_SUMCOL:
            return self._add_column_sumcol(aep, kpi_matrix, period, label,
                                           description)
        elif period.source == SRC_CMPCOL:
            return self._add_column_cmpcol(aep, kpi_matrix, period, label,
                                           description)

    def _compute_matrix(self):
        """ Compute a report and return a KpiMatrix.

        The key attribute of the matrix columns (KpiMatrixCol)
        is guaranteed to be the id of the mis.report.instance.period.
        """
        self.ensure_one()
        aep = self.report_id._prepare_aep(self.query_company_ids,
                                          self.currency_id)
        kpi_matrix = self.report_id.prepare_kpi_matrix(self.multi_company)
        for period in self.period_ids:
            description = None
            if period.mode == MODE_NONE:
                pass
            elif not self.display_columns_description:
                pass
            elif period.date_from == period.date_to and period.date_from:
                description = self._format_date(period.date_from)
            elif period.date_from and period.date_to:
                date_from = self._format_date(period.date_from)
                date_to = self._format_date(period.date_to)
                description = _("from %s to %s") % (date_from, date_to)
            self._add_column(aep, kpi_matrix, period, period.name, description)
        kpi_matrix.compute_comparisons()
        kpi_matrix.compute_sums()
        return kpi_matrix

    def compute(self):
        self.ensure_one()
        kpi_matrix = self._compute_matrix()
        return kpi_matrix.as_dict()

    def drilldown(self, arg):
        self.ensure_one()
        period_id = arg.get("period_id")
        expr = arg.get("expr")
        account_id = arg.get("account_id")
        if period_id and expr and AEP.has_account_var(expr):
            period = self.env["mis.report.instance.period"].browse(period_id)
            aep = AEP(self.query_company_ids, self.currency_id,
                      self.report_id.account_model)
            aep.parse_expr(expr)
            aep.done_parsing()
            domain = aep.get_aml_domain_for_expr(
                expr,
                period.date_from,
                period.date_to,
                None,  # target_move now part of additional_move_line_filter
                account_id,
            )
            domain.extend(period._get_additional_move_line_filter())
            return {
                "name": u"{} - {}".format(expr, period.name),
                "domain": domain,
                "type": "ir.actions.act_window",
                "res_model": period._get_aml_model_name(),
                "views": [[False, "list"], [False, "form"]],
                "view_type": "list",
                "view_mode": "list",
                "target": "current",
                "context": {
                    "active_test": False
                },
            }
        else:
            return False
예제 #22
0
class Order(models.Model):
    _name = 'nursery.order'
    _description = 'Plant Order'
    _inherit = ['mail.thread', 'mail.activity.mixin', 'rating.mixin', 'utm.mixin', 'portal.mixin']

    name = fields.Char(
        'Reference', default=lambda self: _('New'), required=True, states={'draft': [('readonly', False)]})
    user_id = fields.Many2one(
        'res.users', string='Responsible',
        index=True, required=True,
        default=lambda self: self.env.user)
    date_open = fields.Date(
        'Confirmation date', readonly=True)
    customer_id = fields.Many2one(
        "nursery.customer",
        string='Customer',
        index=True, required=True)
    line_ids = fields.One2many(
        'nursery.order.line', 'order_id', string='Order Lines')
    amount_total = fields.Float(
        'Amount', compute='_compute_amount_total', store=True)
    company_id = fields.Many2one(
        'res.company', string='Company', related='user_id.company_id',
        readonly=True, store=True)
    currency_id = fields.Many2one(
        'res.currency', string='Currency', related='company_id.currency_id',
        readonly=True, required=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('open', 'Open'),
        ('done', 'Done'),
        ('cancel', 'Canceled'),
    ], default='draft', index=True, group_expand="_expand_states")
    last_modification = fields.Datetime(readonly=True)

    @api.depends('line_ids.price')
    def _compute_amount_total(self):
        for order in self:
            order.amount_total = sum(order.mapped('line_ids.price'))

    def _compute_access_url(self):
        super(Order, self)._compute_access_url()
        for order in self:
            order.access_url = '/my/order/%s' % order.id

    def action_confirm(self):
        if self.state != 'draft':
            return
        self.activity_feedback(['mail.mail_activity_data_todo'])
        return self.write({
            'state': 'open',
            'date_open': fields.Datetime.now(),
        })

    @api.model
    def create(self, vals):
        if vals.get('name', _('New')) == _('New'):
            if 'company_id' in vals:
                vals['name'] = self.env['ir.sequence'].with_context(force_company=vals['company_id']).next_by_code('plant.order') or _('New')
            else:
                vals['name'] = self.env['ir.sequence'].next_by_code('plant.order') or _('New')
        res = super(Order, self).create(vals)
        res.activity_schedule(
            'mail.mail_activity_data_todo',
            user_id=self.env.ref('base.user_demo').id,
            date_deadline=fields.Date.today() + relativedelta(days=1),
            summary=_('Pack the order'))
        return res

    def write(self, values):
        # helper to "YYYY-MM-DD"
        values['last_modification'] = fields.Datetime.now()

        return super(Order, self).write(values)

    def unlink(self):
        # self is a recordset
        for order in self:
            if order.state == 'confirm':
                raise UserError("You can not delete confirmed orders")

        return super(Order, self).unlink()

    def _expand_states(self, states, domain, order):
        return [key for key, val in type(self).state.selection]

    def action_get_ratings(self):
        action = self.env['ir.actions.act_window'].for_xml_id('rating', 'action_view_rating')
        return dict(
            action,
            domain=[('res_id', 'in', self.ids), ('res_model', '=', 'nursery.order')],
        )

    def action_send_rating(self):
        rating_template = self.env.ref('plant_nursery.mail_template_plant_order_rating')
        for order in self:
            order.rating_send_request(rating_template, force_send=True)

    def rating_get_partner_id(self):
        if self.customer_id.partner_id:
            return self.customer_id.partner_id
        return self.env['res.partner']

    def message_new(self, msg_dict, custom_values=None):
        if custom_values is None:
            custom_values = {}

        if custom_values.get('category_id', False):
            domain = [('category_id', '=', custom_values.pop('category_id'))]
            custom_values.pop
        else:
            domain = []

        # find or create customer
        email = email_split(msg_dict.get('email_from', False))[0]
        name = email_split_and_format(msg_dict.get('email_from', False))[0]
        customer = self.env['nursery.customer'].find_or_create(email, name)

        # happy Xmas
        plants = self.env['nursery.plant'].search(domain)
        plant = self.env['nursery.plant'].browse([random.choice(plants.ids)])
        custom_values.update({
            'customer_id': customer.id,
            'line_ids': [(4, plant.id)],
        })

        return super(Order, self).message_new(msg_dict, custom_values=custom_values)
예제 #23
0
class SaleOrder(models.Model):
    _inherit = "sale.order"

    third_party_order = fields.Boolean(default=False,
                                       readonly=True,
                                       states={"draft": [("readonly", False)]})
    third_party_partner_id = fields.Many2one(
        comodel_name="res.partner",
        readonly=True,
        states={"draft": [("readonly", False)]},
        domain=[
            ("supplier", "=", True),
            ("third_party_sequence_id", "!=", False),
        ],
    )
    third_party_number = fields.Char(copy=False, readonly=True)
    third_party_move_id = fields.Many2one(comodel_name="account.move",
                                          string="Third party move",
                                          readonly=True)
    source_third_party_order_id = fields.Many2one(comodel_name="sale.order",
                                                  string="Third party order",
                                                  readonly=True)
    third_party_order_ids = fields.One2many(
        comodel_name="sale.order",
        inverse_name="source_third_party_order_id",
        string="Third party orders",
        readonly=True,
    )
    third_party_order_count = fields.Integer(
        string="Third party order #",
        compute="_compute_third_party_order_count",
        readonly=True,
    )
    third_party_customer_in_state = fields.Selection(
        [("pending", "Pending amount"), ("paid", "Fully paid")],
        compute="_compute_third_party_residual",
        store=True,
        index=True,
    )
    string = ("Incoming payment status", )
    third_party_customer_in_residual = fields.Monetary(
        currency_field="currency_id",
        compute="_compute_third_party_residual",
        string="Incoming payment residual amount",
    )
    third_party_customer_in_residual_company = fields.Monetary(
        currency_field="currency_id",
        compute="_compute_third_party_residual",
        string="Incoming payment residual amount in company currency",
    )
    third_party_customer_out_state = fields.Selection(
        [("pending", "Pending amount"), ("paid", "Fully paid")],
        string="Outgoing payment status",
        compute="_compute_third_party_residual",
        store=True,
        index=True,
    )
    third_party_customer_out_residual = fields.Monetary(
        currency_field="currency_id",
        string="Outgoing payment residual amount",
        compute="_compute_third_party_residual",
    )
    third_party_customer_out_residual_company = fields.Monetary(
        currency_field="currency_id",
        string="Outgoing payment residual amount in company currency",
        compute="_compute_third_party_residual",
    )

    @api.multi
    @api.depends(
        "third_party_order",
        "third_party_move_id",
        "third_party_move_id.line_ids.amount_residual",
    )
    def _compute_third_party_residual(self):
        """Computes residual amounts from Journal items."""
        for rec in self:
            rec.third_party_customer_in_state = "pending"
            rec.third_party_customer_out_state = "pending"
            third_party_customer_account = rec.partner_id.with_context(
                force_company=rec.company_id.id
            ).property_third_party_customer_account_id
            third_party_supplier_account = rec.third_party_partner_id.with_context(
                force_company=rec.company_id.id
            ).property_third_party_supplier_account_id
            in_residual = 0.0
            in_residual_company = 0.0
            out_residual = 0.0
            out_residual_company = 0.0
            for line in rec.sudo().third_party_move_id.line_ids:
                if (line.account_id == third_party_customer_account
                        and line.partner_id == rec.partner_id):
                    in_residual_company += line.amount_residual
                    if line.currency_id == rec.currency_id:
                        in_residual += (line.amount_residual_currency
                                        if line.currency_id else
                                        line.amount_residual)
                    else:
                        from_currency = (
                            line.currency_id
                            and line.currency_id.with_context(date=line.date)
                        ) or line.company_id.currency_id.with_context(
                            date=line.date)
                        in_residual += from_currency._convert(
                            line.amount_residual,
                            rec.currency_id,
                            line.company_id,
                            line.date,
                        )
                elif (line.account_id == third_party_supplier_account
                      and line.partner_id == rec.third_party_partner_id):
                    out_residual_company += line.amount_residual
                    if line.currency_id == rec.currency_id:
                        out_residual += (line.amount_residual_currency
                                         if line.currency_id else
                                         line.amount_residual)
                    else:
                        from_currency = (
                            line.currency_id
                            and line.currency_id.with_context(date=line.date)
                        ) or line.company_id.currency_id.with_context(
                            date=line.date)
                        out_residual += from_currency._convert(
                            line.amount_residual,
                            rec.currency_id,
                            line.company_id,
                            line.date,
                        )
            rec.third_party_customer_in_residual_company = abs(
                in_residual_company)
            rec.third_party_customer_in_residual = abs(in_residual)
            rec.third_party_customer_out_residual_company = abs(
                out_residual_company)
            rec.third_party_customer_out_residual = abs(out_residual)
            if float_is_zero(
                    rec.third_party_customer_in_residual,
                    precision_rounding=rec.currency_id.rounding,
            ):
                rec.third_party_customer_in_state = "paid"
            if float_is_zero(
                    rec.third_party_customer_out_residual,
                    precision_rounding=rec.currency_id.rounding,
            ):
                rec.third_party_customer_out_state = "paid"

    @api.multi
    def _compute_third_party_order_count(self):
        for order in self:
            order.third_party_order_count = len(order.third_party_order_ids)

    def create_third_party_move(self):
        self.third_party_move_id = self.env["account.move"].create(
            self._third_party_move_vals())
        self.third_party_move_id.post()

    def _third_party_move_vals(self):
        journal = self.company_id.third_party_journal_id
        if not journal:
            journal = self.env["account.journal"].search(
                [
                    ("company_id", "=", self.company_id.id),
                    ("type", "=", "general"),
                ],
                limit=1,
            )
        third_party_customer_account = self.partner_id.with_context(
            force_company=self.company_id.id
        ).property_third_party_customer_account_id
        third_party_supplier_account = self.third_party_partner_id.with_context(
            force_company=self.company_id.id
        ).property_third_party_supplier_account_id
        if not third_party_customer_account:
            raise UserError(
                _("Please define a third party customer account "
                  "for %s." % self.partner_id.name))
        if not third_party_supplier_account:
            raise UserError(
                _("Please define a third party supplier account "
                  "for %s." % self.third_party_partner_id.name))

        return {
            "journal_id":
            journal.id,
            "line_ids": [
                (
                    0,
                    0,
                    {
                        "name": self.partner_id.name,
                        "partner_id": self.partner_id.id,
                        "account_id": third_party_customer_account.id,
                        "debit": self.amount_total,
                        "credit": 0,
                    },
                ),
                (
                    0,
                    0,
                    {
                        "name": self.third_party_partner_id.name,
                        "partner_id": self.third_party_partner_id.id,
                        "account_id": third_party_supplier_account.id,
                        "debit": 0,
                        "credit": self.amount_total,
                    },
                ),
            ],
        }

    def _prepare_third_party_order(self):
        lines = self.order_line.filtered(lambda l: l.third_party_product_id)
        so_lines = [(0, 0, l._prepare_third_party_order_line()) for l in lines]

        return {
            "partner_id":
            self.third_party_partner_id.id,
            "fiscal_position_id":
            self.third_party_partner_id.with_context(
                force_company=self.company_id.id).property_account_position_id.
            id or False,
            "order_line":
            so_lines,
            "company_id":
            self.company_id.id,
            "source_third_party_order_id":
            self.id,
        }

    @api.model
    def _create_third_party_order(self):
        vals = self._prepare_third_party_order()
        order = self.env["sale.order"].create(vals)
        order._compute_tax_id()
        return order

    @api.multi
    def _action_confirm(self):
        res = super(SaleOrder, self)._action_confirm()
        for order in self.filtered(lambda o: o.third_party_order):
            if not order.third_party_number and not self.env.context.get(
                    "no_third_party_number", False):
                sequence = order.third_party_partner_id.third_party_sequence_id
                if not sequence:
                    raise UserError(
                        _("Please define an invoice "
                          "sequence in the third party partner."))
                order.third_party_number = sequence.next_by_id()
            order.create_third_party_move()
            order._create_third_party_order()
        return res

    @api.multi
    def action_confirm(self):
        res = super(SaleOrder, self).action_confirm()
        for order in self.filtered(lambda o: o.third_party_order):
            order.action_done()
        return res

    @api.multi
    def action_cancel(self):
        res = super(SaleOrder, self).action_cancel()
        for order in self.filtered(lambda o: o.third_party_move_id):
            order.third_party_order_ids.action_cancel()
            order.third_party_move_id.button_cancel()
            order.third_party_move_id.unlink()
        return res

    def action_view_third_party_order(self):
        action = self.env.ref("sale.action_orders")
        result = action.read()[0]
        order_ids = self.third_party_order_ids.ids
        if len(order_ids) != 1:
            result["domain"] = [("id", "in", order_ids)]
        elif len(order_ids) == 1:
            res = self.env.ref("sale.view_order_form", False)
            result["views"] = [(res and res.id or False, "form")]
            result["res_id"] = order_ids[0]
        return result

    @api.constrains("third_party_order", "third_party_partner_id")
    def _check_third_party_constrains(self):
        for rec in self:
            if rec.third_party_order and not rec.third_party_partner_id:
                raise ValidationError(
                    _("Please define a third party partner."))

    @api.multi
    def third_party_invoice_print(self):
        return self.env.ref(
            "sale_third_party.action_report_saleorder_third_party"
        ).report_action(self)
예제 #24
0
class PurchaseOrder(models.Model):
    _name = "purchase.order"
    _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin']
    _description = "Purchase Order"
    _order = 'date_order desc, id desc'

    def _default_currency_id(self):
        company_id = self.env.context.get(
            'force_company') or self.env.context.get(
                'company_id') or self.env.user.company_id.id
        return self.env['res.company'].browse(company_id).currency_id

    @api.depends('order_line.price_total')
    def _amount_all(self):
        for order in self:
            amount_untaxed = amount_tax = 0.0
            for line in order.order_line:
                amount_untaxed += line.price_subtotal
                amount_tax += line.price_tax
            order.update({
                'amount_untaxed':
                order.currency_id.round(amount_untaxed),
                'amount_tax':
                order.currency_id.round(amount_tax),
                'amount_total':
                amount_untaxed + amount_tax,
            })

    @api.depends('order_line.date_planned', 'date_order')
    def _compute_date_planned(self):
        for order in self:
            min_date = False
            for line in order.order_line:
                if not min_date or line.date_planned < min_date:
                    min_date = line.date_planned
            if min_date:
                order.date_planned = min_date
            else:
                order.date_planned = order.date_order

    @api.depends('state', 'order_line.qty_invoiced', 'order_line.qty_received',
                 'order_line.product_qty')
    def _get_invoiced(self):
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        for order in self:
            if order.state not in ('purchase', 'done'):
                order.invoice_status = 'no'
                continue

            if any(
                    float_compare(
                        line.qty_invoiced,
                        line.product_qty if line.product_id.purchase_method ==
                        'purchase' else line.qty_received,
                        precision_digits=precision) == -1
                    for line in order.order_line):
                order.invoice_status = 'to invoice'
            elif all(
                    float_compare(
                        line.qty_invoiced,
                        line.product_qty if line.product_id.purchase_method ==
                        'purchase' else line.qty_received,
                        precision_digits=precision) >= 0
                    for line in order.order_line) and order.invoice_ids:
                order.invoice_status = 'invoiced'
            else:
                order.invoice_status = 'no'

    @api.depends('order_line.invoice_lines.invoice_id')
    def _compute_invoice(self):
        for order in self:
            invoices = self.env['account.invoice']
            for line in order.order_line:
                invoices |= line.invoice_lines.mapped('invoice_id')
            order.invoice_ids = invoices
            order.invoice_count = len(invoices)

    READONLY_STATES = {
        'purchase': [('readonly', True)],
        'done': [('readonly', True)],
        'cancel': [('readonly', True)],
    }

    name = fields.Char('Order Reference',
                       required=True,
                       index=True,
                       copy=False,
                       default='New')
    origin = fields.Char(
        'Source Document',
        copy=False,
        help="Reference of the document that generated this purchase order "
        "request (e.g. a sales order)")
    partner_ref = fields.Char(
        'Vendor Reference',
        copy=False,
        help="Reference of the sales order or bid sent by the vendor. "
        "It's used to do the matching when you receive the "
        "products as this reference is usually written on the "
        "delivery order sent by your vendor.")
    date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\
        help="Depicts the date where the Quotation should be validated and converted into a purchase order.")
    date_approve = fields.Date('Approval Date',
                               readonly=1,
                               index=True,
                               copy=False)
    partner_id = fields.Many2one(
        'res.partner',
        string='Vendor',
        required=True,
        states=READONLY_STATES,
        change_default=True,
        track_visibility='always',
        help=
        "You can find a vendor by its Name, TIN, Email or Internal Reference.")
    dest_address_id = fields.Many2one(
        'res.partner',
        string='Drop Ship Address',
        states=READONLY_STATES,
        help=
        "Put an address if you want to deliver directly from the vendor to the customer. "
        "Otherwise, keep empty to deliver to your own company.")
    currency_id = fields.Many2one('res.currency',
                                  'Currency',
                                  required=True,
                                  states=READONLY_STATES,
                                  default=_default_currency_id)
    state = fields.Selection([('draft', 'RFQ'), ('sent', 'RFQ Sent'),
                              ('to approve', 'To Approve'),
                              ('purchase', 'Purchase Order'),
                              ('done', 'Locked'), ('cancel', 'Cancelled')],
                             string='Status',
                             readonly=True,
                             index=True,
                             copy=False,
                             default='draft',
                             track_visibility='onchange')
    order_line = fields.One2many('purchase.order.line',
                                 'order_id',
                                 string='Order Lines',
                                 states={
                                     'cancel': [('readonly', True)],
                                     'done': [('readonly', True)]
                                 },
                                 copy=True)
    notes = fields.Text('Terms and Conditions')

    invoice_count = fields.Integer(compute="_compute_invoice",
                                   string='Bill Count',
                                   copy=False,
                                   default=0,
                                   store=True)
    invoice_ids = fields.Many2many('account.invoice',
                                   compute="_compute_invoice",
                                   string='Bills',
                                   copy=False,
                                   store=True)
    invoice_status = fields.Selection([
        ('no', 'Nothing to Bill'),
        ('to invoice', 'Waiting Bills'),
        ('invoiced', 'No Bill to Receive'),
    ],
                                      string='Billing Status',
                                      compute='_get_invoiced',
                                      store=True,
                                      readonly=True,
                                      copy=False,
                                      default='no')

    # There is no inverse function on purpose since the date may be different on each line
    date_planned = fields.Datetime(string='Scheduled Date',
                                   compute='_compute_date_planned',
                                   store=True,
                                   index=True)

    amount_untaxed = fields.Monetary(string='Untaxed Amount',
                                     store=True,
                                     readonly=True,
                                     compute='_amount_all',
                                     track_visibility='always')
    amount_tax = fields.Monetary(string='Taxes',
                                 store=True,
                                 readonly=True,
                                 compute='_amount_all')
    amount_total = fields.Monetary(string='Total',
                                   store=True,
                                   readonly=True,
                                   compute='_amount_all')

    fiscal_position_id = fields.Many2one('account.fiscal.position',
                                         string='Fiscal Position',
                                         oldname='fiscal_position')
    payment_term_id = fields.Many2one('account.payment.term', 'Payment Terms')
    incoterm_id = fields.Many2one(
        'account.incoterms',
        'Incoterm',
        states={'done': [('readonly', True)]},
        help=
        "International Commercial Terms are a series of predefined commercial terms used in international transactions."
    )

    product_id = fields.Many2one('product.product',
                                 related='order_line.product_id',
                                 string='Product',
                                 readonly=False)
    user_id = fields.Many2one('res.users',
                              string='Purchase Representative',
                              index=True,
                              track_visibility='onchange',
                              default=lambda self: self.env.user)
    company_id = fields.Many2one(
        'res.company',
        'Company',
        required=True,
        index=True,
        states=READONLY_STATES,
        default=lambda self: self.env.user.company_id.id)

    def _compute_access_url(self):
        super(PurchaseOrder, self)._compute_access_url()
        for order in self:
            order.access_url = '/my/purchase/%s' % (order.id)

    @api.model
    def _name_search(self,
                     name,
                     args=None,
                     operator='ilike',
                     limit=100,
                     name_get_uid=None):
        args = args or []
        domain = []
        if name:
            domain = [
                '|', ('name', operator, name), ('partner_ref', operator, name)
            ]
        purchase_order_ids = self._search(expression.AND([domain, args]),
                                          limit=limit,
                                          access_rights_uid=name_get_uid)
        return self.browse(purchase_order_ids).name_get()

    @api.multi
    @api.depends('name', 'partner_ref')
    def name_get(self):
        result = []
        for po in self:
            name = po.name
            if po.partner_ref:
                name += ' (' + po.partner_ref + ')'
            if self.env.context.get('show_total_amount') and po.amount_total:
                name += ': ' + formatLang(
                    self.env, po.amount_total, currency_obj=po.currency_id)
            result.append((po.id, name))
        return result

    @api.model
    def create(self, vals):
        company_id = vals.get('company_id',
                              self.default_get(['company_id'])['company_id'])
        if vals.get('name', 'New') == 'New':
            vals['name'] = self.env['ir.sequence'].with_context(
                force_company=company_id).next_by_code('purchase.order') or '/'
        return super(PurchaseOrder,
                     self.with_context(company_id=company_id)).create(vals)

    @api.multi
    def unlink(self):
        for order in self:
            if not order.state == 'cancel':
                raise UserError(
                    _('In order to delete a purchase order, you must cancel it first.'
                      ))
        return super(PurchaseOrder, self).unlink()

    @api.multi
    def copy(self, default=None):
        ctx = dict(self.env.context)
        ctx.pop('default_product_id', None)
        self = self.with_context(ctx)
        new_po = super(PurchaseOrder, self).copy(default=default)
        for line in new_po.order_line:
            seller = line.product_id._select_seller(
                partner_id=line.partner_id,
                quantity=line.product_qty,
                date=line.order_id.date_order
                and line.order_id.date_order.date(),
                uom_id=line.product_uom)
            line.date_planned = line._get_date_planned(seller)
        return new_po

    @api.multi
    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'state' in init_values and self.state == 'purchase':
            return 'purchase.mt_rfq_approved'
        elif 'state' in init_values and self.state == 'to approve':
            return 'purchase.mt_rfq_confirmed'
        elif 'state' in init_values and self.state == 'done':
            return 'purchase.mt_rfq_done'
        return super(PurchaseOrder, self)._track_subtype(init_values)

    @api.onchange('partner_id', 'company_id')
    def onchange_partner_id(self):
        if not self.partner_id:
            self.fiscal_position_id = False
            self.payment_term_id = False
            self.currency_id = self.env.user.company_id.currency_id.id
        else:
            self.fiscal_position_id = self.env[
                'account.fiscal.position'].with_context(
                    company_id=self.company_id.id).get_fiscal_position(
                        self.partner_id.id)
            self.payment_term_id = self.partner_id.property_supplier_payment_term_id.id
            self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id
        return {}

    @api.onchange('fiscal_position_id')
    def _compute_tax_id(self):
        """
        Trigger the recompute of the taxes if the fiscal position is changed on the PO.
        """
        for order in self:
            order.order_line._compute_tax_id()

    @api.onchange('partner_id')
    def onchange_partner_id_warning(self):
        if not self.partner_id:
            return
        warning = {}
        title = False
        message = False

        partner = self.partner_id

        # If partner has no warning, check its company
        if partner.purchase_warn == 'no-message' and partner.parent_id:
            partner = partner.parent_id

        if partner.purchase_warn and partner.purchase_warn != 'no-message':
            # Block if partner only has warning but parent company is blocked
            if partner.purchase_warn != 'block' and partner.parent_id and partner.parent_id.purchase_warn == 'block':
                partner = partner.parent_id
            title = _("Warning for %s") % partner.name
            message = partner.purchase_warn_msg
            warning = {'title': title, 'message': message}
            if partner.purchase_warn == 'block':
                self.update({'partner_id': False})
            return {'warning': warning}
        return {}

    @api.multi
    def action_rfq_send(self):
        '''
        This function opens a window to compose an email, with the edi purchase template message loaded by default
        '''
        self.ensure_one()
        ir_model_data = self.env['ir.model.data']
        try:
            if self.env.context.get('send_rfq', False):
                template_id = ir_model_data.get_object_reference(
                    'purchase', 'email_template_edi_purchase')[1]
            else:
                template_id = ir_model_data.get_object_reference(
                    'purchase', 'email_template_edi_purchase_done')[1]
        except ValueError:
            template_id = False
        try:
            compose_form_id = ir_model_data.get_object_reference(
                'mail', 'email_compose_message_wizard_form')[1]
        except ValueError:
            compose_form_id = False
        ctx = dict(self.env.context or {})
        ctx.update({
            'default_model': 'purchase.order',
            'default_res_id': self.ids[0],
            'default_use_template': bool(template_id),
            'default_template_id': template_id,
            'default_composition_mode': 'comment',
            'custom_layout': "mail.mail_notification_paynow",
            'force_email': True,
            'mark_rfq_as_sent': True,
        })

        # In the case of a RFQ or a PO, we want the "View..." button in line with the state of the
        # object. Therefore, we pass the model description in the context, in the language in which
        # the template is rendered.
        lang = self.env.context.get('lang')
        if {'default_template_id', 'default_model', 'default_res_id'
            } <= ctx.keys():
            template = self.env['mail.template'].browse(
                ctx['default_template_id'])
            if template and template.lang:
                lang = template._render_template(template.lang,
                                                 ctx['default_model'],
                                                 ctx['default_res_id'])

        self = self.with_context(lang=lang)
        if self.state in ['draft', 'sent']:
            ctx['model_description'] = _('Request for Quotation')
        else:
            ctx['model_description'] = _('Purchase Order')

        return {
            'name': _('Compose Email'),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'mail.compose.message',
            'views': [(compose_form_id, 'form')],
            'view_id': compose_form_id,
            'target': 'new',
            'context': ctx,
        }

    @api.multi
    @api.returns('mail.message', lambda value: value.id)
    def message_post(self, **kwargs):
        if self.env.context.get('mark_rfq_as_sent'):
            self.filtered(lambda o: o.state == 'draft').write(
                {'state': 'sent'})
        return super(PurchaseOrder, self.with_context(
            mail_post_autofollow=True)).message_post(**kwargs)

    @api.multi
    def print_quotation(self):
        self.write({'state': "sent"})
        return self.env.ref(
            'purchase.report_purchase_quotation').report_action(self)

    @api.multi
    def button_approve(self, force=False):
        self.write({
            'state': 'purchase',
            'date_approve': fields.Date.context_today(self)
        })
        self.filtered(lambda p: p.company_id.po_lock == 'lock').write(
            {'state': 'done'})
        return {}

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

    @api.multi
    def button_confirm(self):
        for order in self:
            if order.state not in ['draft', 'sent']:
                continue
            order._add_supplier_to_product()
            # Deal with double validation process
            if order.company_id.po_double_validation == 'one_step'\
                    or (order.company_id.po_double_validation == 'two_step'\
                        and order.amount_total < self.env.user.company_id.currency_id._convert(
                            order.company_id.po_double_validation_amount, order.currency_id, order.company_id, order.date_order or fields.Date.today()))\
                    or order.user_has_groups('purchase.group_purchase_manager'):
                order.button_approve()
            else:
                order.write({'state': 'to approve'})
        return True

    @api.multi
    def button_cancel(self):
        for order in self:
            for inv in order.invoice_ids:
                if inv and inv.state not in ('cancel', 'draft'):
                    raise UserError(
                        _("Unable to cancel this purchase order. You must first cancel the related vendor bills."
                          ))

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

    @api.multi
    def button_unlock(self):
        self.write({'state': 'purchase'})

    @api.multi
    def button_done(self):
        self.write({'state': 'done'})

    @api.multi
    def _add_supplier_to_product(self):
        # Add the partner in the supplier list of the product if the supplier is not registered for
        # this product. We limit to 10 the number of suppliers for a product to avoid the mess that
        # could be caused for some generic products ("Miscellaneous").
        for line in self.order_line:
            # Do not add a contact as a supplier
            partner = self.partner_id if not self.partner_id.parent_id else self.partner_id.parent_id
            if partner not in line.product_id.seller_ids.mapped(
                    'name') and len(line.product_id.seller_ids) <= 10:
                # Convert the price in the right currency.
                currency = partner.property_purchase_currency_id or self.env.user.company_id.currency_id
                price = self.currency_id._convert(line.price_unit,
                                                  currency,
                                                  line.company_id,
                                                  line.date_order
                                                  or fields.Date.today(),
                                                  round=False)
                # Compute the price for the template's UoM, because the supplier's UoM is related to that UoM.
                if line.product_id.product_tmpl_id.uom_po_id != line.product_uom:
                    default_uom = line.product_id.product_tmpl_id.uom_po_id
                    price = line.product_uom._compute_price(price, default_uom)

                supplierinfo = {
                    'name':
                    partner.id,
                    'sequence':
                    max(line.product_id.seller_ids.mapped('sequence')) +
                    1 if line.product_id.seller_ids else 1,
                    'min_qty':
                    0.0,
                    'price':
                    price,
                    'currency_id':
                    currency.id,
                    'delay':
                    0,
                }
                # In case the order partner is a contact address, a new supplierinfo is created on
                # the parent company. In this case, we keep the product name and code.
                seller = line.product_id._select_seller(
                    partner_id=line.partner_id,
                    quantity=line.product_qty,
                    date=line.order_id.date_order
                    and line.order_id.date_order.date(),
                    uom_id=line.product_uom)
                if seller:
                    supplierinfo['product_name'] = seller.product_name
                    supplierinfo['product_code'] = seller.product_code
                vals = {
                    'seller_ids': [(0, 0, supplierinfo)],
                }
                try:
                    line.product_id.write(vals)
                except AccessError:  # no write access rights -> just ignore
                    break

    @api.multi
    def action_view_invoice(self):
        '''
        This function returns an action that display existing vendor bills of given purchase order ids.
        When only one found, show the vendor bill immediately.
        '''
        action = self.env.ref('account.action_vendor_bill_template')
        result = action.read()[0]
        create_bill = self.env.context.get('create_bill', False)
        # override the context to get rid of the default filtering
        result['context'] = {
            'type': 'in_invoice',
            'default_purchase_id': self.id,
            'default_currency_id': self.currency_id.id,
            'default_company_id': self.company_id.id,
            'company_id': self.company_id.id
        }
        # choose the view_mode accordingly
        if len(self.invoice_ids) > 1 and not create_bill:
            result['domain'] = "[('id', 'in', " + str(
                self.invoice_ids.ids) + ")]"
        else:
            res = self.env.ref('account.invoice_supplier_form', False)
            form_view = [(res and res.id or False, 'form')]
            if 'views' in result:
                result['views'] = form_view + [
                    (state, view)
                    for state, view in action['views'] if view != 'form'
                ]
            else:
                result['views'] = form_view
            # Do not set an invoice_id if we want to create a new bill.
            if not create_bill:
                result['res_id'] = self.invoice_ids.id or False
        result['context']['default_origin'] = self.name
        result['context']['default_reference'] = self.partner_ref
        return result

    @api.multi
    def action_set_date_planned(self):
        for order in self:
            order.order_line.update({'date_planned': order.date_planned})
예제 #25
0
class KAStockInventoryPriceAdjustment(models.Model):
    _name = "ka_stock.inventory.price.adjustment"
    _description = "Stock standard price adjustment and auto create journal"
    _inherit = ['mail.thread']
    _order = 'date desc, id desc'

    @api.model
    def _compute_default_location(self):
        company_user = self.env.user.company_id
        warehouse = self.env['stock.warehouse'].search(
            [('company_id', '=', company_user.id)], limit=1)
        if warehouse:
            return warehouse.lot_stock_id.id
        else:
            raise UserError(
                _('You must define a warehouse for the company: %s.') %
                (company_user.name, ))

    name = fields.Char(string="Reference", copy=False)
    date = fields.Date(string="Tanggal",
                       default=fields.Datetime.now,
                       track_visibility="onchange")
    state = fields.Selection([("draft", "Draft"), ("confirm", "In Progress"),
                              ("done", "Validated"), ("cancel", "Cancelled")],
                             string="Status",
                             default="draft",
                             track_visibility="onchange",
                             copy=False)

    line_ids = fields.One2many("inventory.price.adjustment.line",
                               "price_adjustment_id",
                               string="Products",
                               states={'confirm': [('readonly', True)]})
    location_id = fields.Many2one("stock.location",
                                  string="Inventoried Location",
                                  default=_compute_default_location,
                                  track_visibility="onchange")
    company_id = fields.Many2one("res.company",
                                 string="Company",
                                 default=lambda self: self.env.user.company_id,
                                 track_visibility="onchange")
    accounting_date = fields.Date(string="Force Accounting",
                                  default=fields.Datetime.now,
                                  track_visibility="onchange")
    journal_ids = fields.One2many("account.move",
                                  "price_adjustment_id",
                                  string="Journals")

    def action_confirm(self):
        self.state = "confirm"

    def action_to_draft(self):
        self.state = "draft"

    def action_process(self):

        #add journal items
        line_items = []
        jumlah_deb = 0
        jumlah_kred = 0
        for x in self.line_ids:
            if x.product_id.categ_id == False:
                raise UserError('Product category pada ' +
                                x.product_id.display_name + 'tidak ditemukan!')
            else:
                if x.product_id.categ_id.property_stock_account_input_categ_id == False or \
                    x.product_id.categ_id.property_stock_account_output_categ_id == False:

                    raise UserError(
                        'Jurnal masuk/keluar pada category product ' +
                        x.product_id.categ_id + 'tidak ditemukan!')

            if x.difference < 0:
                jumlah_kred = +x.difference
                vals = (0, 0, {
                    'account_id':
                    x.product_id.categ_id.
                    property_stock_account_output_categ_id.id,
                    'name':
                    x.product_id.display_name,
                    'debit':
                    0,
                    'credit':
                    abs(x.difference)
                })
            elif x.difference > 0:
                jumlah_deb = +x.difference
                vals = (0, 0, {
                    'account_id':
                    x.product_id.categ_id.
                    property_stock_account_input_categ_id.id,
                    'name':
                    x.product_id.display_name,
                    'debit':
                    abs(x.difference),
                    'credit':
                    0,
                })

            line_items.append(vals)

            # set new standard price
            x.product_id.standard_price = x.standard_price_new

        #add opposite journal items
        #-------------Search for Configuration Journal-------------
        config = self.env[
            'ka_stock.inventory.price.adjustment.configuration'].search(
                ['company_id', '=', self.company_id.id])

        last_vals1 = (0, 0, {
            'account_id': 1463,
            'name': 'Jurnal balik price adjustment : ' + self.name,
            'debit': abs(jumlah_kred),
            'credit': 0,
        })

        line_items.append(last_vals1)

        last_vals2 = (0, 0, {
            'account_id': 1463,
            'name': 'Jurnal balik price adjustment : ' + self.name,
            'debit': 0,
            'credit': abs(jumlah_deb),
        })

        line_items.append(last_vals2)
        line_items.reverse()

        #create journal
        data = {
            'price_adjustment_id':
            self.id,
            'journal_id':
            self.env['account.journal'].search([
                ('name', '=', 'Stock Journal'),
                ('company_id', '=', self.env.user.company_id.id)
            ]).id,
            'date':
            datetime.now(),
            'ref':
            'Price adjustment : ' + self.name,
            'company_id':
            self.env.user.company_id.id,
            'line_ids':
            line_items,
        }

        if self.accounting_date:
            data['date'] = self.accounting_date

        new_journal = self.env['account.move'].create(data)
        new_journal.post()

        self.state = 'done'

    def action_view_journal_price_adjustment(self):
        """To open `account.move (journal entries)`.

        Returns:
            Dict -- Action result.
        """
        if len(self.journal_ids) == 1:
            return {
                'name': 'Journal Entry',
                'view_type': 'form',
                'view_mode': 'form',
                'res_id': self.journal_ids[0].id,
                'type': 'ir.actions.act_window',
                'res_model': 'account.move',
                'target': 'current',
            }
        else:
            action = self.env.ref('account.action_move_journal_line')
            result = action.read()[0]
            result['domain'] = [('price_adjustment_id', '=', self.id)]
            result['context'] = {'default_price_adjustment_id': self.id}
            return result
예제 #26
0
class PurchaseOrderLine(models.Model):
    _name = 'purchase.order.line'
    _description = 'Purchase Order Line'
    _order = 'order_id, sequence, id'

    name = fields.Text(string='Description', required=True)
    sequence = fields.Integer(string='Sequence', default=10)
    product_qty = fields.Float(
        string='Quantity',
        digits=dp.get_precision('Product Unit of Measure'),
        required=True)
    product_uom_qty = fields.Float(string='Total Quantity',
                                   compute='_compute_product_uom_qty',
                                   store=True)
    date_planned = fields.Datetime(string='Scheduled Date',
                                   required=True,
                                   index=True)
    taxes_id = fields.Many2many(
        'account.tax',
        string='Taxes',
        domain=['|', ('active', '=', False), ('active', '=', True)])
    product_uom = fields.Many2one('uom.uom',
                                  string='Product Unit of Measure',
                                  required=True)
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 domain=[('purchase_ok', '=', True)],
                                 change_default=True,
                                 required=True)
    product_image = fields.Binary(
        'Product Image',
        related="product_id.image",
        readonly=False,
        help=
        "Non-stored related field to allow portal user to see the image of the product he has ordered"
    )
    product_type = fields.Selection(related='product_id.type', readonly=True)
    price_unit = fields.Float(string='Unit Price',
                              required=True,
                              digits=dp.get_precision('Product Price'))

    price_subtotal = fields.Monetary(compute='_compute_amount',
                                     string='Subtotal',
                                     store=True)
    price_total = fields.Monetary(compute='_compute_amount',
                                  string='Total',
                                  store=True)
    price_tax = fields.Float(compute='_compute_amount',
                             string='Tax',
                             store=True)

    order_id = fields.Many2one('purchase.order',
                               string='Order Reference',
                               index=True,
                               required=True,
                               ondelete='cascade')
    account_analytic_id = fields.Many2one('account.analytic.account',
                                          string='Analytic Account')
    analytic_tag_ids = fields.Many2many('account.analytic.tag',
                                        string='Analytic Tags')
    company_id = fields.Many2one('res.company',
                                 related='order_id.company_id',
                                 string='Company',
                                 store=True,
                                 readonly=True)
    state = fields.Selection(related='order_id.state',
                             store=True,
                             readonly=False)

    invoice_lines = fields.One2many('account.invoice.line',
                                    'purchase_line_id',
                                    string="Bill Lines",
                                    readonly=True,
                                    copy=False)

    # Replace by invoiced Qty
    qty_invoiced = fields.Float(
        compute='_compute_qty_invoiced',
        string="Billed Qty",
        digits=dp.get_precision('Product Unit of Measure'),
        store=True)
    qty_received = fields.Float(
        string="Received Qty",
        digits=dp.get_precision('Product Unit of Measure'),
        copy=False)

    partner_id = fields.Many2one('res.partner',
                                 related='order_id.partner_id',
                                 string='Partner',
                                 readonly=True,
                                 store=True)
    currency_id = fields.Many2one(related='order_id.currency_id',
                                  store=True,
                                  string='Currency',
                                  readonly=True)
    date_order = fields.Datetime(related='order_id.date_order',
                                 string='Order Date',
                                 readonly=True)

    @api.depends('product_qty', 'price_unit', 'taxes_id')
    def _compute_amount(self):
        for line in self:
            vals = line._prepare_compute_all_values()
            taxes = line.taxes_id.compute_all(vals['price_unit'],
                                              vals['currency_id'],
                                              vals['product_qty'],
                                              vals['product'], vals['partner'])
            line.update({
                'price_tax':
                sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])),
                'price_total':
                taxes['total_included'],
                'price_subtotal':
                taxes['total_excluded'],
            })

    def _prepare_compute_all_values(self):
        # Hook method to returns the different argument values for the
        # compute_all method, due to the fact that discounts mechanism
        # is not implemented yet on the purchase orders.
        # This method should disappear as soon as this feature is
        # also introduced like in the sales module.
        self.ensure_one()
        return {
            'price_unit': self.price_unit,
            'currency_id': self.order_id.currency_id,
            'product_qty': self.product_qty,
            'product': self.product_id,
            'partner': self.order_id.partner_id,
        }

    @api.multi
    def _compute_tax_id(self):
        for line in self:
            fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id
            # If company_id is set, always filter taxes by the company
            taxes = line.product_id.supplier_taxes_id.filtered(
                lambda r: not line.company_id or r.company_id == line.
                company_id)
            line.taxes_id = fpos.map_tax(
                taxes, line.product_id,
                line.order_id.partner_id) if fpos else taxes

    @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity')
    def _compute_qty_invoiced(self):
        for line in self:
            qty = 0.0
            for inv_line in line.invoice_lines:
                if inv_line.invoice_id.state not in ['cancel']:
                    if inv_line.invoice_id.type == 'in_invoice':
                        qty += inv_line.uom_id._compute_quantity(
                            inv_line.quantity, line.product_uom)
                    elif inv_line.invoice_id.type == 'in_refund':
                        qty -= inv_line.uom_id._compute_quantity(
                            inv_line.quantity, line.product_uom)
            line.qty_invoiced = qty

    @api.model
    def create(self, values):
        line = super(PurchaseOrderLine, self).create(values)
        if line.order_id.state == 'purchase':
            msg = _("Extra line with %s ") % (line.product_id.display_name, )
            line.order_id.message_post(body=msg)
        return line

    @api.multi
    def write(self, values):
        if 'product_qty' in values:
            for line in self:
                if line.order_id.state == 'purchase':
                    line.order_id.message_post_with_view(
                        'purchase.track_po_line_template',
                        values={
                            'line': line,
                            'product_qty': values['product_qty']
                        },
                        subtype_id=self.env.ref('mail.mt_note').id)
        return super(PurchaseOrderLine, self).write(values)

    @api.multi
    def unlink(self):
        for line in self:
            if line.order_id.state in ['purchase', 'done']:
                raise UserError(
                    _('Cannot delete a purchase order line which is in state \'%s\'.'
                      ) % (line.state, ))
        return super(PurchaseOrderLine, self).unlink()

    @api.model
    def _get_date_planned(self, seller, po=False):
        """Return the datetime value to use as Schedule Date (``date_planned``) for
           PO Lines that correspond to the given product.seller_ids,
           when ordered at `date_order_str`.

           :param Model seller: used to fetch the delivery delay (if no seller
                                is provided, the delay is 0)
           :param Model po: purchase.order, necessary only if the PO line is
                            not yet attached to a PO.
           :rtype: datetime
           :return: desired Schedule Date for the PO line
        """
        date_order = po.date_order if po else self.order_id.date_order
        if date_order:
            return date_order + relativedelta(
                days=seller.delay if seller else 0)
        else:
            return datetime.today() + relativedelta(
                days=seller.delay if seller else 0)

    @api.onchange('product_id')
    def onchange_product_id(self):
        result = {}
        if not self.product_id:
            return result

        # Reset date, price and quantity since _onchange_quantity will provide default values
        self.date_planned = datetime.today().strftime(
            DEFAULT_SERVER_DATETIME_FORMAT)
        self.price_unit = self.product_qty = 0.0
        self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id
        result['domain'] = {
            'product_uom':
            [('category_id', '=', self.product_id.uom_id.category_id.id)]
        }

        product_lang = self.product_id.with_context(
            lang=self.partner_id.lang,
            partner_id=self.partner_id.id,
        )
        self.name = product_lang.display_name
        if product_lang.description_purchase:
            self.name += '\n' + product_lang.description_purchase

        self._compute_tax_id()

        self._suggest_quantity()
        self._onchange_quantity()

        return result

    @api.onchange('product_id')
    def onchange_product_id_warning(self):
        if not self.product_id:
            return
        warning = {}
        title = False
        message = False

        product_info = self.product_id

        if product_info.purchase_line_warn != 'no-message':
            title = _("Warning for %s") % product_info.name
            message = product_info.purchase_line_warn_msg
            warning['title'] = title
            warning['message'] = message
            if product_info.purchase_line_warn == 'block':
                self.product_id = False
            return {'warning': warning}
        return {}

    @api.onchange('product_qty', 'product_uom')
    def _onchange_quantity(self):
        if not self.product_id:
            return
        params = {'order_id': self.order_id}
        seller = self.product_id._select_seller(
            partner_id=self.partner_id,
            quantity=self.product_qty,
            date=self.order_id.date_order and self.order_id.date_order.date(),
            uom_id=self.product_uom,
            params=params)

        if seller or not self.date_planned:
            self.date_planned = self._get_date_planned(seller).strftime(
                DEFAULT_SERVER_DATETIME_FORMAT)

        if not seller:
            if self.product_id.seller_ids.filtered(
                    lambda s: s.name.id == self.partner_id.id):
                self.price_unit = 0.0
            return

        price_unit = self.env['account.tax']._fix_tax_included_price_company(
            seller.price, self.product_id.supplier_taxes_id, self.taxes_id,
            self.company_id) if seller else 0.0
        if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
            price_unit = seller.currency_id._convert(
                price_unit, self.order_id.currency_id,
                self.order_id.company_id, self.date_order
                or fields.Date.today())

        if seller and self.product_uom and seller.product_uom != self.product_uom:
            price_unit = seller.product_uom._compute_price(
                price_unit, self.product_uom)

        self.price_unit = price_unit

    @api.multi
    @api.depends('product_uom', 'product_qty', 'product_id.uom_id')
    def _compute_product_uom_qty(self):
        for line in self:
            if line.product_id.uom_id != line.product_uom:
                line.product_uom_qty = line.product_uom._compute_quantity(
                    line.product_qty, line.product_id.uom_id)
            else:
                line.product_uom_qty = line.product_qty

    def _suggest_quantity(self):
        '''
        Suggest a minimal quantity based on the seller
        '''
        if not self.product_id:
            return
        seller_min_qty = self.product_id.seller_ids\
            .filtered(lambda r: r.name == self.order_id.partner_id and (not r.product_id or r.product_id == self.product_id))\
            .sorted(key=lambda r: r.min_qty)
        if seller_min_qty:
            self.product_qty = seller_min_qty[0].min_qty or 1.0
            self.product_uom = seller_min_qty[0].product_uom
        else:
            self.product_qty = 1.0
예제 #27
0
class Job(models.Model):

    _name = "hr.job"
    _description = "Job Position"
    _inherit = ['mail.thread']

    def _default_groups(self):
        default_user = self.env.ref('base.default_user',
                                    raise_if_not_found=False)
        return (default_user or self.env['res.users']).sudo().groups_id

    name = fields.Char(string='Job Title',
                       required=True,
                       index=True,
                       translate=True)
    expected_employees = fields.Integer(
        compute='_compute_employees',
        string='Total Forecasted Employees',
        store=True,
        help=
        'Expected number of employees for this job position after new recruitment.'
    )
    no_of_employee = fields.Integer(
        compute='_compute_employees',
        string="Current Number of Employees",
        store=True,
        help='Number of employees currently occupying this job position.')
    no_of_recruitment = fields.Integer(
        string='Expected New Employees',
        copy=False,
        help='Number of new employees you expect to recruit.',
        default=1)
    no_of_hired_employee = fields.Integer(
        string='Hired Employees',
        copy=False,
        help=
        'Number of hired employees for this job position during recruitment phase.'
    )
    employee_ids = fields.One2many('hr.employee',
                                   'job_id',
                                   string='Employees',
                                   groups='base.group_user')
    description = fields.Text(string='Job Description')
    requirements = fields.Text('Requirements')
    department_id = fields.Many2one('hr.department', string='Department')
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 default=lambda self: self.env.user.company_id)
    state = fields.Selection(
        [('recruit', 'Recruitment in Progress'), ('open', 'Not Recruiting')],
        string='Status',
        readonly=True,
        required=True,
        track_visibility='always',
        copy=False,
        default='recruit',
        help=
        "Set whether the recruitment process is open or closed for this job position."
    )

    groups_id = fields.Many2many('res.groups',
                                 'res_groups_users_rel',
                                 'uid',
                                 'gid',
                                 string='Groups',
                                 default=_default_groups)

    _sql_constraints = [
        ('name_company_uniq', 'unique(name, company_id, department_id)',
         'The name of the job position must be unique per department in company!'
         ),
    ]

    @api.depends('no_of_recruitment', 'employee_ids.job_id',
                 'employee_ids.active')
    def _compute_employees(self):
        employee_data = self.env['hr.employee'].read_group(
            [('job_id', 'in', self.ids)], ['job_id'], ['job_id'])
        result = dict((data['job_id'][0], data['job_id_count'])
                      for data in employee_data)
        for job in self:
            job.no_of_employee = result.get(job.id, 0)
            job.expected_employees = result.get(job.id,
                                                0) + job.no_of_recruitment

    @api.model
    def create(self, values):
        """ We don't want the current user to be follower of all created job """
        return super(
            Job,
            self.with_context(mail_create_nosubscribe=True)).create(values)

    @api.multi
    def copy(self, default=None):
        self.ensure_one()
        default = dict(default or {})
        if 'name' not in default:
            default['name'] = _("%s (copy)") % (self.name)
        return super(Job, self).copy(default=default)

    @api.multi
    def set_recruit(self):
        for record in self:
            no_of_recruitment = 1 if record.no_of_recruitment == 0 else record.no_of_recruitment
            record.write({
                'state': 'recruit',
                'no_of_recruitment': no_of_recruitment
            })
        return True

    @api.multi
    def set_open(self):
        return self.write({
            'state': 'open',
            'no_of_recruitment': 0,
            'no_of_hired_employee': 0
        })
예제 #28
0
class ProductProduct(models.Model):
    _name = "product.product"
    _description = "Product"
    _inherits = {'product.template': 'product_tmpl_id'}
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'default_code, name, id'

    price = fields.Float('Price',
                         compute='_compute_product_price',
                         digits=dp.get_precision('Product Price'),
                         inverse='_set_product_price')
    price_extra = fields.Float(
        'Variant Price Extra',
        compute='_compute_product_price_extra',
        digits=dp.get_precision('Product Price'),
        help="This is the sum of the extra price of all attributes")
    lst_price = fields.Float(
        'Sale Price',
        compute='_compute_product_lst_price',
        digits=dp.get_precision('Product Price'),
        inverse='_set_product_lst_price',
        help=
        "The sale price is managed from the product template. Click on the 'Variant Prices' button to set the extra attribute prices."
    )

    default_code = fields.Char('Internal Reference', index=True)
    code = fields.Char('Reference', compute='_compute_product_code')
    partner_ref = fields.Char('Customer Ref', compute='_compute_partner_ref')

    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "If unchecked, it will allow you to hide the product without removing it."
    )
    product_tmpl_id = fields.Many2one('product.template',
                                      'Product Template',
                                      auto_join=True,
                                      index=True,
                                      ondelete="cascade",
                                      required=True)
    barcode = fields.Char(
        'Barcode',
        copy=False,
        oldname='ean13',
        help="International Article Number used for product identification.")
    attribute_value_ids = fields.Many2many('product.attribute.value',
                                           string='Attributes',
                                           ondelete='restrict')
    # image: all image fields are base64 encoded and PIL-supported
    image_variant = fields.Binary(
        "Variant Image",
        attachment=True,
        help=
        "This field holds the image used as image for the product variant, limited to 1024x1024px."
    )
    image = fields.Binary(
        "Big-sized image",
        compute='_compute_images',
        inverse='_set_image',
        help=
        "Image of the product variant (Big-sized image of product template if false). It is automatically "
        "resized as a 1024x1024px image, with aspect ratio preserved.")
    image_small = fields.Binary(
        "Small-sized image",
        compute='_compute_images',
        inverse='_set_image_small',
        help=
        "Image of the product variant (Small-sized image of product template if false)."
    )
    image_medium = fields.Binary(
        "Medium-sized image",
        compute='_compute_images',
        inverse='_set_image_medium',
        help=
        "Image of the product variant (Medium-sized image of product template if false)."
    )

    standard_price = fields.Float(
        'Cost',
        company_dependent=True,
        digits=dp.get_precision('Product Price'),
        groups="base.group_user",
        help=
        "Cost used for stock valuation in standard price and as a first price to set in average/fifo. "
        "Also used as a base price for pricelists. "
        "Expressed in the default unit of measure of the product.")
    volume = fields.Float('Volume', help="The volume in m3.")
    weight = fields.Float(
        'Weight',
        digits=dp.get_precision('Stock Weight'),
        help=
        "The weight of the contents in Kg, not including any packaging, etc.")

    pricelist_item_ids = fields.Many2many('product.pricelist.item',
                                          'Pricelist Items',
                                          compute='_get_pricelist_items')

    packaging_ids = fields.One2many(
        'product.packaging',
        'product_id',
        'Product Packages',
        help="Gives the different ways to package the same product.")

    _sql_constraints = [
        ('barcode_uniq', 'unique(barcode)',
         "A barcode can only be assigned to one product !"),
    ]

    def _get_invoice_policy(self):
        return False

    def _compute_product_price(self):
        prices = {}
        pricelist_id_or_name = self._context.get('pricelist')
        if pricelist_id_or_name:
            pricelist = None
            partner = self._context.get('partner', False)
            quantity = self._context.get('quantity', 1.0)

            # Support context pricelists specified as display_name or ID for compatibility
            if isinstance(pricelist_id_or_name, pycompat.string_types):
                pricelist_name_search = self.env[
                    'product.pricelist'].name_search(pricelist_id_or_name,
                                                     operator='=',
                                                     limit=1)
                if pricelist_name_search:
                    pricelist = self.env['product.pricelist'].browse(
                        [pricelist_name_search[0][0]])
            elif isinstance(pricelist_id_or_name, pycompat.integer_types):
                pricelist = self.env['product.pricelist'].browse(
                    pricelist_id_or_name)

            if pricelist:
                quantities = [quantity] * len(self)
                partners = [partner] * len(self)
                prices = pricelist.get_products_price(self, quantities,
                                                      partners)

        for product in self:
            product.price = prices.get(product.id, 0.0)

    def _set_product_price(self):
        for product in self:
            if self._context.get('uom'):
                value = self.env['product.uom'].browse(
                    self._context['uom'])._compute_price(
                        product.price, product.uom_id)
            else:
                value = product.price
            value -= product.price_extra
            product.write({'list_price': value})

    def _set_product_lst_price(self):
        for product in self:
            if self._context.get('uom'):
                value = self.env['product.uom'].browse(
                    self._context['uom'])._compute_price(
                        product.lst_price, product.uom_id)
            else:
                value = product.lst_price
            value -= product.price_extra
            product.write({'list_price': value})

    @api.depends('attribute_value_ids.price_ids.price_extra',
                 'attribute_value_ids.price_ids.product_tmpl_id')
    def _compute_product_price_extra(self):
        # TDE FIXME: do a real multi and optimize a bit ?
        for product in self:
            price_extra = 0.0
            for attribute_price in product.mapped(
                    'attribute_value_ids.price_ids'):
                if attribute_price.product_tmpl_id == product.product_tmpl_id:
                    price_extra += attribute_price.price_extra
            product.price_extra = price_extra

    @api.depends('list_price', 'price_extra')
    def _compute_product_lst_price(self):
        to_uom = None
        if 'uom' in self._context:
            to_uom = self.env['product.uom'].browse([self._context['uom']])

        for product in self:
            if to_uom:
                list_price = product.uom_id._compute_price(
                    product.list_price, to_uom)
            else:
                list_price = product.list_price
            product.lst_price = list_price + product.price_extra

    @api.one
    def _compute_product_code(self):
        for supplier_info in self.seller_ids:
            if supplier_info.name.id == self._context.get('partner_id'):
                self.code = supplier_info.product_code or self.default_code
        else:
            self.code = self.default_code

    @api.one
    def _compute_partner_ref(self):
        for supplier_info in self.seller_ids:
            if supplier_info.name.id == self._context.get('partner_id'):
                product_name = supplier_info.product_name or self.default_code
                self.partner_ref = '%s%s' % (self.code and '[%s] ' % self.code
                                             or '', product_name)
        else:
            self.partner_ref = self.name_get()[0][1]

    @api.one
    @api.depends('image_variant', 'product_tmpl_id.image')
    def _compute_images(self):
        if self._context.get('bin_size'):
            self.image_medium = self.image_variant
            self.image_small = self.image_variant
            self.image = self.image_variant
        else:
            resized_images = tools.image_get_resized_images(
                self.image_variant, return_big=True, avoid_resize_medium=True)
            self.image_medium = resized_images['image_medium']
            self.image_small = resized_images['image_small']
            self.image = resized_images['image']
        if not self.image_medium:
            self.image_medium = self.product_tmpl_id.image_medium
        if not self.image_small:
            self.image_small = self.product_tmpl_id.image_small
        if not self.image:
            self.image = self.product_tmpl_id.image

    @api.one
    def _set_image(self):
        self._set_image_value(self.image)

    @api.one
    def _set_image_medium(self):
        self._set_image_value(self.image_medium)

    @api.one
    def _set_image_small(self):
        self._set_image_value(self.image_small)

    @api.one
    def _set_image_value(self, value):
        if isinstance(value, pycompat.text_type):
            value = value.encode('ascii')
        image = tools.image_resize_image_big(value)
        if self.product_tmpl_id.image:
            self.image_variant = image
        else:
            self.product_tmpl_id.image = image

    @api.one
    def _get_pricelist_items(self):
        self.pricelist_item_ids = self.env['product.pricelist.item'].search([
            '|', ('product_id', '=', self.id),
            ('product_tmpl_id', '=', self.product_tmpl_id.id)
        ]).ids

    @api.constrains('attribute_value_ids')
    def _check_attribute_value_ids(self):
        for product in self:
            attributes = self.env['product.attribute']
            for value in product.attribute_value_ids:
                if value.attribute_id in attributes:
                    raise ValidationError(
                        _('Error! It is not allowed to choose more than one value for a given attribute.'
                          ))
                if value.attribute_id.create_variant:
                    attributes |= value.attribute_id
        return True

    @api.onchange('uom_id', 'uom_po_id')
    def _onchange_uom(self):
        if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id:
            self.uom_po_id = self.uom_id

    @api.model
    def create(self, vals):
        product = super(
            ProductProduct,
            self.with_context(create_product_product=True)).create(vals)
        # When a unique variant is created from tmpl then the standard price is set by _set_standard_price
        if not (self.env.context.get('create_from_tmpl')
                and len(product.product_tmpl_id.product_variant_ids) == 1):
            product._set_standard_price(vals.get('standard_price') or 0.0)
        return product

    @api.multi
    def write(self, values):
        ''' Store the standard price change in order to be able to retrieve the cost of a product for a given date'''
        res = super(ProductProduct, self).write(values)
        if 'standard_price' in values:
            self._set_standard_price(values['standard_price'])
        return res

    @api.multi
    def unlink(self):
        unlink_products = self.env['product.product']
        unlink_templates = self.env['product.template']
        for product in self:
            # Check if product still exists, in case it has been unlinked by unlinking its template
            if not product.exists():
                continue
            # Check if the product is last product of this template
            other_products = self.search([('product_tmpl_id', '=',
                                           product.product_tmpl_id.id),
                                          ('id', '!=', product.id)])
            if not other_products:
                unlink_templates |= product.product_tmpl_id
            unlink_products |= product
        res = super(ProductProduct, unlink_products).unlink()
        # delete templates after calling super, as deleting template could lead to deleting
        # products due to ondelete='cascade'
        unlink_templates.unlink()
        return res

    @api.multi
    def copy(self, default=None):
        # TDE FIXME: clean context / variant brol
        if default is None:
            default = {}
        if self._context.get('variant'):
            # if we copy a variant or create one, we keep the same template
            default['product_tmpl_id'] = self.product_tmpl_id.id
        elif 'name' not in default:
            default['name'] = self.name

        return super(ProductProduct, self).copy(default=default)

    @api.model
    def search(self, args, offset=0, limit=None, order=None, count=False):
        # TDE FIXME: strange
        if self._context.get('search_default_categ_id'):
            args.append((('categ_id', 'child_of',
                          self._context['search_default_categ_id'])))
        return super(ProductProduct, self).search(args,
                                                  offset=offset,
                                                  limit=limit,
                                                  order=order,
                                                  count=count)

    @api.multi
    def name_get(self):
        # TDE: this could be cleaned a bit I think

        def _name_get(d):
            name = d.get('name', '')
            code = self._context.get('display_default_code', True) and d.get(
                'default_code', False) or False
            if code:
                name = '[%s] %s' % (code, name)
            return (d['id'], name)

        partner_id = self._context.get('partner_id')
        if partner_id:
            partner_ids = [
                partner_id, self.env['res.partner'].browse(
                    partner_id).commercial_partner_id.id
            ]
        else:
            partner_ids = []

        # all user don't have access to seller and partner
        # check access and use superuser
        self.check_access_rights("read")
        self.check_access_rule("read")

        result = []
        for product in self.sudo():
            # display only the attributes with multiple possible values on the template
            variable_attributes = product.attribute_line_ids.filtered(
                lambda l: len(l.value_ids) > 1).mapped('attribute_id')
            variant = product.attribute_value_ids._variant_name(
                variable_attributes)

            name = variant and "%s (%s)" % (product.name,
                                            variant) or product.name
            sellers = []
            if partner_ids:
                sellers = [
                    x for x in product.seller_ids
                    if (x.name.id in partner_ids) and (x.product_id == product)
                ]
                if not sellers:
                    sellers = [
                        x for x in product.seller_ids
                        if (x.name.id in partner_ids) and not x.product_id
                    ]
            if sellers:
                for s in sellers:
                    seller_variant = s.product_name and (
                        variant and "%s (%s)" %
                        (s.product_name, variant) or s.product_name) or False
                    mydict = {
                        'id': product.id,
                        'name': seller_variant or name,
                        'default_code': s.product_code or product.default_code,
                    }
                    temp = _name_get(mydict)
                    if temp not in result:
                        result.append(temp)
            else:
                mydict = {
                    'id': product.id,
                    'name': name,
                    'default_code': product.default_code,
                }
                result.append(_name_get(mydict))
        return result

    @api.model
    def name_search(self, name='', args=None, operator='ilike', limit=100):
        if not args:
            args = []
        if name:
            positive_operators = ['=', 'ilike', '=ilike', 'like', '=like']
            products = self.env['product.product']
            if operator in positive_operators:
                products = self.search([('default_code', '=', name)] + args,
                                       limit=limit)
                if not products:
                    products = self.search([('barcode', '=', name)] + args,
                                           limit=limit)
            if not products and operator not in expression.NEGATIVE_TERM_OPERATORS:
                # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal
                # on a database with thousands of matching products, due to the huge merge+unique needed for the
                # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table
                # Performing a quick memory merge of ids in Python will give much better performance
                products = self.search(args +
                                       [('default_code', operator, name)],
                                       limit=limit)
                if not limit or len(products) < limit:
                    # we may underrun the limit because of dupes in the results, that's fine
                    limit2 = (limit - len(products)) if limit else False
                    products += self.search(args +
                                            [('name', operator, name),
                                             ('id', 'not in', products.ids)],
                                            limit=limit2)
            elif not products and operator in expression.NEGATIVE_TERM_OPERATORS:
                domain = expression.OR([
                    [
                        '&', ('default_code', operator, name),
                        ('name', operator, name)
                    ],
                    [
                        '&', ('default_code', '=', False),
                        ('name', operator, name)
                    ],
                ])
                domain = expression.AND([args, domain])
                products = self.search(domain, limit=limit)
            if not products and operator in positive_operators:
                ptrn = re.compile('(\[(.*?)\])')
                res = ptrn.search(name)
                if res:
                    products = self.search(
                        [('default_code', '=', res.group(2))] + args,
                        limit=limit)
            # still no results, partner in context: search on supplier info as last hope to find something
            if not products and self._context.get('partner_id'):
                suppliers = self.env['product.supplierinfo'].search([
                    ('name', '=', self._context.get('partner_id')), '|',
                    ('product_code', operator, name),
                    ('product_name', operator, name)
                ])
                if suppliers:
                    products = self.search(
                        [('product_tmpl_id.seller_ids', 'in', suppliers.ids)],
                        limit=limit)
        else:
            products = self.search(args, limit=limit)
        return products.name_get()

    @api.model
    def view_header_get(self, view_id, view_type):
        res = super(ProductProduct, self).view_header_get(view_id, view_type)
        if self._context.get('categ_id'):
            return _('Products: ') + self.env['product.category'].browse(
                self._context['categ_id']).name
        return res

    @api.multi
    def open_product_template(self):
        """ Utility method used to add an "Open Template" button in product views """
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'product.template',
            'view_mode': 'form',
            'res_id': self.product_tmpl_id.id,
            'target': 'new'
        }

    @api.multi
    def _select_seller(self,
                       partner_id=False,
                       quantity=0.0,
                       date=None,
                       uom_id=False):
        self.ensure_one()
        if date is None:
            date = fields.Date.context_today(self)
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')

        res = self.env['product.supplierinfo']
        for seller in self.seller_ids:
            # Set quantity in UoM of seller
            quantity_uom_seller = quantity
            if quantity_uom_seller and uom_id and uom_id != seller.product_uom:
                quantity_uom_seller = uom_id._compute_quantity(
                    quantity_uom_seller, seller.product_uom)

            if seller.date_start and seller.date_start > date:
                continue
            if seller.date_end and seller.date_end < date:
                continue
            if partner_id and seller.name not in [
                    partner_id, partner_id.parent_id
            ]:
                continue
            if float_compare(quantity_uom_seller,
                             seller.min_qty,
                             precision_digits=precision) == -1:
                continue
            if seller.product_id and seller.product_id != self:
                continue

            res |= seller
            break
        return res

    @api.multi
    def price_compute(self,
                      price_type,
                      uom=False,
                      currency=False,
                      company=False):
        # TDE FIXME: delegate to template or not ? fields are reencoded here ...
        # compatibility about context keys used a bit everywhere in the code
        if not uom and self._context.get('uom'):
            uom = self.env['product.uom'].browse(self._context['uom'])
        if not currency and self._context.get('currency'):
            currency = self.env['res.currency'].browse(
                self._context['currency'])

        products = self
        if price_type == 'standard_price':
            # standard_price field can only be seen by users in base.group_user
            # Thus, in order to compute the sale price from the cost for users not in this group
            # We fetch the standard price as the superuser
            products = self.with_context(
                force_company=company and company.id or self._context.get(
                    'force_company', self.env.user.company_id.id)).sudo()

        prices = dict.fromkeys(self.ids, 0.0)
        for product in products:
            prices[product.id] = product[price_type] or 0.0
            if price_type == 'list_price':
                prices[product.id] += product.price_extra

            if uom:
                prices[product.id] = product.uom_id._compute_price(
                    prices[product.id], uom)

            # Convert from current user company currency to asked one
            # This is right cause a field cannot be in more than one currency
            if currency:
                prices[product.id] = product.currency_id.compute(
                    prices[product.id], currency)

        return prices

    # compatibility to remove after v10 - DEPRECATED
    @api.multi
    def price_get(self, ptype='list_price'):
        return self.price_compute(ptype)

    @api.multi
    def _set_standard_price(self, value):
        ''' Store the standard price change in order to be able to retrieve the cost of a product for a given date'''
        PriceHistory = self.env['product.price.history']
        for product in self:
            PriceHistory.create({
                'product_id':
                product.id,
                'cost':
                value,
                'company_id':
                self._context.get('force_company',
                                  self.env.user.company_id.id),
            })

    @api.multi
    def get_history_price(self, company_id, date=None):
        history = self.env['product.price.history'].search(
            [('company_id', '=', company_id), ('product_id', 'in', self.ids),
             ('datetime', '<=', date or fields.Datetime.now())],
            order='datetime desc,id desc',
            limit=1)
        return history.cost or 0.0
예제 #29
0
class SaleOrder(models.Model):
    _inherit = "sale.order"

    @api.model
    def _default_warehouse_id(self):
        company = self.env.user.company_id.id
        warehouse_ids = self.env['stock.warehouse'].search(
            [('company_id', '=', company)], limit=1)
        return warehouse_ids

    incoterm = fields.Many2one(
        'stock.incoterms',
        'Incoterms',
        help=
        "International Commercial Terms are a series of predefined commercial terms used in international transactions."
    )
    picking_policy = fields.Selection(
        [('direct', 'Deliver each product when available'),
         ('one', 'Deliver all products at once')],
        string='Shipping Policy',
        required=True,
        readonly=True,
        default='direct',
        states={
            'draft': [('readonly', False)],
            'sent': [('readonly', False)]
        },
        help=
        "If you deliver all products at once, the delivery order will be scheduled based on the greatest "
        "product lead time. Otherwise, it will be based on the shortest.")
    warehouse_id = fields.Many2one('stock.warehouse',
                                   string='Warehouse',
                                   required=True,
                                   readonly=True,
                                   states={
                                       'draft': [('readonly', False)],
                                       'sent': [('readonly', False)]
                                   },
                                   default=_default_warehouse_id)
    picking_ids = fields.One2many('stock.picking',
                                  'sale_id',
                                  string='Pickings')
    delivery_count = fields.Integer(string='Delivery Orders',
                                    compute='_compute_picking_ids')
    procurement_group_id = fields.Many2one('procurement.group',
                                           'Procurement Group',
                                           copy=False)

    @api.multi
    def write(self, values):
        if values.get('order_line') and self.state == 'sale':
            for order in self:
                pre_order_line_qty = {
                    order_line: order_line.product_uom_qty
                    for order_line in order.mapped('order_line')
                }
        res = super(SaleOrder, self).write(values)
        if values.get('order_line') and self.state == 'sale':
            for order in self:
                to_log = {}
                for order_line in order.order_line:
                    if pre_order_line_qty.get(
                            order_line, False) and float_compare(
                                order_line.product_uom_qty,
                                pre_order_line_qty[order_line],
                                order_line.product_uom.rounding) < 0:
                        to_log[order_line] = (order_line.product_uom_qty,
                                              pre_order_line_qty[order_line])
                if to_log:
                    documents = self.env[
                        'stock.picking']._log_activity_get_documents(
                            to_log, 'move_ids', 'UP')
                    order._log_decrease_ordered_quantity(documents)
        return res

    @api.multi
    def _action_confirm(self):
        super(SaleOrder, self)._action_confirm()
        for order in self:
            order.order_line._action_launch_stock_rule()

    @api.depends('picking_ids')
    def _compute_picking_ids(self):
        for order in self:
            order.delivery_count = len(order.picking_ids)

    @api.onchange('warehouse_id')
    def _onchange_warehouse_id(self):
        if self.warehouse_id.company_id:
            self.company_id = self.warehouse_id.company_id.id

    @api.multi
    def action_view_delivery(self):
        '''
        This function returns an action that display existing delivery orders
        of given sales order ids. It can either be a in a list or in a form
        view, if there is only one delivery order to show.
        '''
        action = self.env.ref('stock.action_picking_tree_all').read()[0]

        pickings = self.mapped('picking_ids')
        if len(pickings) > 1:
            action['domain'] = [('id', 'in', pickings.ids)]
        elif pickings:
            action['views'] = [(self.env.ref('stock.view_picking_form').id,
                                'form')]
            action['res_id'] = pickings.id
        return action

    @api.multi
    def action_cancel(self):
        documents = None
        for sale_order in self:
            if sale_order.state == 'sale' and sale_order.order_line:
                sale_order_lines_quantities = {
                    order_line: (order_line.product_uom_qty, 0)
                    for order_line in sale_order.order_line
                }
                documents = self.env[
                    'stock.picking']._log_activity_get_documents(
                        sale_order_lines_quantities, 'move_ids', 'UP')
        self.mapped('picking_ids').action_cancel()
        if documents:
            filtered_documents = {}
            for (parent, responsible), rendering_context in documents.items():
                if parent._name == 'stock.picking':
                    if parent.state == 'cancel':
                        continue
                filtered_documents[(parent, responsible)] = rendering_context
            self._log_decrease_ordered_quantity(filtered_documents,
                                                cancel=True)
        return super(SaleOrder, self).action_cancel()

    @api.multi
    def _prepare_invoice(self):
        invoice_vals = super(SaleOrder, self)._prepare_invoice()
        invoice_vals['incoterms_id'] = self.incoterm.id or False
        return invoice_vals

    @api.model
    def _get_customer_lead(self, product_tmpl_id):
        super(SaleOrder, self)._get_customer_lead(product_tmpl_id)
        return product_tmpl_id.sale_delay

    def _log_decrease_ordered_quantity(self, documents, cancel=False):
        def _render_note_exception_quantity_so(rendering_context):
            order_exceptions, visited_moves = rendering_context
            visited_moves = list(visited_moves)
            visited_moves = self.env[visited_moves[0]._name].concat(
                *visited_moves)
            order_line_ids = self.env['sale.order.line'].browse([
                order_line.id for order in order_exceptions.values()
                for order_line in order[0]
            ])
            sale_order_ids = order_line_ids.mapped('order_id')
            impacted_pickings = visited_moves.filtered(
                lambda m: m.state not in ('done', 'cancel')).mapped(
                    'picking_id')
            values = {
                'sale_order_ids': sale_order_ids,
                'order_exceptions': order_exceptions.values(),
                'impacted_pickings': impacted_pickings,
                'cancel': cancel
            }
            return self.env.ref('sale_stock.exception_on_so').render(
                values=values)

        self.env['stock.picking']._log_activity(
            _render_note_exception_quantity_so, documents)
예제 #30
0
class SchoolStandard(models.Model):
    ''' Defining a standard related to school '''
    _name = 'school.standard'
    _description = 'School Standards'
    _rec_name = "standard_id"

    @api.depends('standard_id', 'school_id', 'division_id', 'medium_id',
                 'school_id')
    def _compute_student(self):
        '''Compute student of done state'''
        student_obj = self.env['student.student']
        for rec in self:
            rec.student_ids = student_obj.\
                search([('standard_id', '=', rec.id),
                        ('school_id', '=', rec.school_id.id),
                        ('division_id', '=', rec.division_id.id),
                        ('medium_id', '=', rec.medium_id.id),
                        ('state', '=', 'done')])

    @api.onchange('standard_id', 'division_id')
    def onchange_combine(self):
        self.name = str(self.standard_id.name
                        ) + '-' + str(self.division_id.name)

    @api.depends('subject_ids')
    def _compute_subject(self):
        '''Method to compute subjects'''
        for rec in self:
            rec.total_no_subjects = len(rec.subject_ids)

    @api.depends('student_ids')
    def _compute_total_student(self):
        for rec in self:
            rec.total_students = len(rec.student_ids)

    @api.depends("capacity", "total_students")
    def _compute_remain_seats(self):
        for rec in self:
            rec.remaining_seats = rec.capacity - rec.total_students

    school_id = fields.Many2one('school.school', 'School', required=True)
    standard_id = fields.Many2one('standard.standard', 'Standard',
                                  required=True)
    division_id = fields.Many2one('standard.division', 'Division',
                                  required=True)
    medium_id = fields.Many2one('standard.medium', 'Medium', required=True)
    subject_ids = fields.Many2many('subject.subject', 'subject_standards_rel',
                                   'subject_id', 'standard_id', 'Subject')
    user_id = fields.Many2one('school.teacher', 'Class Teacher')
    student_ids = fields.One2many('student.student', 'standard_id',
                                  'Student In Class',
                                  compute='_compute_student', store=True
                                  )
    color = fields.Integer('Color Index')
    cmp_id = fields.Many2one('res.company', 'Company Name',
                             related='school_id.company_id', store=True)
    syllabus_ids = fields.One2many('subject.syllabus', 'standard_id',
                                   'Syllabus')
    total_no_subjects = fields.Integer('Total No of Subject',
                                       compute="_compute_subject")
    name = fields.Char('Name')
    capacity = fields.Integer("Total Seats")
    total_students = fields.Integer("Total Students",
                                    compute="_compute_total_student",
                                    store=True)
    remaining_seats = fields.Integer("Available Seats",
                                     compute="_compute_remain_seats",
                                     store=True)
    class_room_id = fields.Many2one('class.room', 'Room Number')

    @api.constrains('standard_id', 'division_id')
    def check_standard_unique(self):
        standard_search = self.env['school.standard'
                                   ].search([('standard_id', '=',
                                              self.standard_id.id),
                                             ('division_id', '=',
                                              self.division_id.id),
                                             ('school_id', '=',
                                              self.school_id.id),
                                             ('id', 'not in', self.ids)])
        if standard_search:
            raise ValidationError(_('''Division and class should be unique!'''
                                    ))

    @api.multi
    def unlink(self):
        for rec in self:
            if rec.student_ids or rec.subject_ids or rec.syllabus_ids:
                raise ValidationError(_('''You cannot delete this standard
                because it has reference with student or subject or
                syllabus!'''))
        return super(SchoolStandard, self).unlink()

    @api.constrains('capacity')
    def check_seats(self):
        if self.capacity <= 0:
            raise ValidationError(_('''Total seats should be greater than
                0!'''))

    @api.multi
    def name_get(self):
        '''Method to display standard and division'''
        return [(rec.id, rec.standard_id.name + '[' + rec.division_id.name +
                 ']') for rec in self]