Example #1
0
class ResourceResource(models.Model):
    _name = "resource.resource"
    _description = "Resources"

    @api.model
    def default_get(self, fields):
        res = super(ResourceResource, self).default_get(fields)
        if not res.get('calendar_id') and res.get('company_id'):
            company = self.env['res.company'].browse(res['company_id'])
            res['calendar_id'] = company.resource_calendar_id.id
        return res

    name = fields.Char(required=True)
    active = fields.Boolean(
        'Active',
        default=True,
        tracking=True,
        help=
        "If the active field is set to False, it will allow you to hide the resource record without removing it."
    )
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 default=lambda self: self.env.company)
    resource_type = fields.Selection([('user', 'Human'),
                                      ('material', 'Material')],
                                     string='Resource Type',
                                     default='user',
                                     required=True)
    user_id = fields.Many2one(
        'res.users',
        string='User',
        help='Related user name for the resource to manage its access.')
    time_efficiency = fields.Float(
        'Efficiency Factor',
        default=100,
        required=True,
        help=
        "This field is used to calculate the the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100%, then the expected duration will be one hour. If the efficiency factor is 200%, however the expected duration will be 30 minutes."
    )
    calendar_id = fields.Many2one(
        "resource.calendar",
        string='Working Time',
        default=lambda self: self.env.company.resource_calendar_id,
        required=True,
        help="Define the schedule of resource")
    tz = fields.Selection(
        _tz_get,
        string='Timezone',
        required=True,
        default=lambda self: self._context.get('tz'
                                               ) or self.env.user.tz or 'UTC',
        help=
        "This field is used in order to define in which timezone the resources will work."
    )

    _sql_constraints = [
        ('check_time_efficiency', 'CHECK(time_efficiency>0)',
         'Time efficiency must be strictly positive'),
    ]

    @api.constrains('time_efficiency')
    def _check_time_efficiency(self):
        for record in self:
            if record.time_efficiency == 0:
                raise ValidationError(
                    _('The efficiency factor cannot be equal to 0.'))

    @api.model
    def create(self, values):
        if values.get('company_id') and not values.get('calendar_id'):
            values['calendar_id'] = self.env['res.company'].browse(
                values['company_id']).resource_calendar_id.id
        if not values.get('tz'):
            # retrieve timezone on user or calendar
            tz = (self.env['res.users'].browse(values.get('user_id')).tz
                  or self.env['resource.calendar'].browse(
                      values.get('calendar_id')).tz)
            if tz:
                values['tz'] = tz
        return super(ResourceResource, self).create(values)

    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        self.ensure_one()
        if default is None:
            default = {}
        if not default.get('name'):
            default.update(name=_('%s (copy)') % (self.name))
        return super(ResourceResource, self).copy(default)

    @api.onchange('company_id')
    def _onchange_company_id(self):
        if self.company_id:
            self.calendar_id = self.company_id.resource_calendar_id.id

    @api.onchange('user_id')
    def _onchange_user_id(self):
        if self.user_id:
            self.tz = self.user_id.tz

    def _get_work_interval(self, start, end):
        """ Return interval's start datetime for interval closest to start. And interval's end datetime for interval closest to end.
            If none is found return None
            Note: this method is used in enterprise (forecast and planning)

            :start: datetime
            :end: datetime
            :return: (datetime|None, datetime|None)
        """
        start_datetime = timezone_datetime(start)
        end_datetime = timezone_datetime(end)
        resource_mapping = {}
        for resource in self:
            work_intervals = sorted(resource.calendar_id._work_intervals(
                start_datetime, end_datetime, resource),
                                    key=lambda x: x[0])
            if work_intervals:
                resource_mapping[resource.id] = (
                    work_intervals[0][0].astimezone(utc),
                    work_intervals[-1][1].astimezone(utc))
            else:
                resource_mapping[resource.id] = (None, None)
        return resource_mapping

    def _get_unavailable_intervals(self, start, end):
        """ Compute the intervals during which employee is unavailable with hour granularity between start and end
            Note: this method is used in enterprise (forecast and planning)

        """
        start_datetime = timezone_datetime(start)
        end_datetime = timezone_datetime(end)
        resource_mapping = {}
        for resource in self:
            calendar = resource.calendar_id
            resource_work_intervals = calendar._work_intervals(
                start_datetime, end_datetime, resource)
            resource_work_intervals = [
                (start, stop) for start, stop, meta in resource_work_intervals
            ]
            # start + flatten(intervals) + end
            resource_work_intervals = [start_datetime] + list(
                chain.from_iterable(resource_work_intervals)) + [end_datetime]
            # put it back to UTC
            resource_work_intervals = list(
                map(lambda dt: dt.astimezone(utc), resource_work_intervals))
            # pick groups of two
            resource_work_intervals = list(
                zip(resource_work_intervals[0::2],
                    resource_work_intervals[1::2]))
            resource_mapping[resource.id] = resource_work_intervals
        return resource_mapping
Example #2
0
class AccountFrFec(models.TransientModel):
    _name = 'account.fr.fec'
    _description = 'Ficher Echange Informatise'

    date_from = fields.Date(string='Start Date', required=True)
    date_to = fields.Date(string='End Date', required=True)
    fec_data = fields.Binary('FEC File', readonly=True)
    filename = fields.Char(string='Filename', size=256, readonly=True)
    export_type = fields.Selection([
        ('official', 'Official FEC report (posted entries only)'),
        ('nonofficial',
         'Non-official FEC report (posted and unposted entries)'),
    ],
                                   string='Export Type',
                                   required=True,
                                   default='official')

    def do_query_unaffected_earnings(self):
        ''' Compute the sum of ending balances for all accounts that are of a type that does not bring forward the balance in new fiscal years.
            This is needed because we have to display only one line for the initial balance of all expense/revenue accounts in the FEC.
        '''

        sql_query = '''
        SELECT
            'OUV' AS JournalCode,
            'Balance initiale' AS JournalLib,
            'OUVERTURE/' || %s AS EcritureNum,
            %s AS EcritureDate,
            '120/129' AS CompteNum,
            'Benefice (perte) reporte(e)' AS CompteLib,
            '' AS CompAuxNum,
            '' AS CompAuxLib,
            '-' AS PieceRef,
            %s AS PieceDate,
            '/' AS EcritureLib,
            replace(CASE WHEN COALESCE(sum(aml.balance), 0) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
            replace(CASE WHEN COALESCE(sum(aml.balance), 0) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
            '' AS EcritureLet,
            '' AS DateLet,
            %s AS ValidDate,
            '' AS Montantdevise,
            '' AS Idevise
        FROM
            account_move_line aml
            LEFT JOIN account_move am ON am.id=aml.move_id
            JOIN account_account aa ON aa.id = aml.account_id
            LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id
        WHERE
            am.date < %s
            AND am.company_id = %s
            AND aat.include_initial_balance = 'f'
            AND (aml.debit != 0 OR aml.credit != 0)
        '''
        # For official report: only use posted entries
        if self.export_type == "official":
            sql_query += '''
            AND am.state = 'posted'
            '''
        company = self.env.user.company_id
        formatted_date_from = fields.Date.to_string(self.date_from).replace(
            '-', '')
        date_from = self.date_from
        formatted_date_year = date_from.year
        self._cr.execute(
            sql_query,
            (formatted_date_year, formatted_date_from, formatted_date_from,
             formatted_date_from, self.date_from, company.id))
        listrow = []
        row = self._cr.fetchone()
        listrow = list(row)
        return listrow

    def _get_company_legal_data(self, company):
        """
        Dom-Tom are excluded from the EU's fiscal territory
        Those regions do not have SIREN
        sources:
            https://www.service-public.fr/professionnels-entreprises/vosdroits/F23570
            http://www.douane.gouv.fr/articles/a11024-tva-dans-les-dom
        """
        dom_tom_group = self.env.ref('l10n_fr.dom-tom')
        is_dom_tom = company.country_id.code in dom_tom_group.country_ids.mapped(
            'code')
        if not is_dom_tom and not company.vat:
            raise Warning(
                _("Missing VAT number for company %s") % company.name)
        if not is_dom_tom and company.vat[0:2] != 'FR':
            raise Warning(_("FEC is for French companies only !"))

        return {
            'siren': company.vat[4:13] if not is_dom_tom else '',
        }

    @api.multi
    def generate_fec(self):
        self.ensure_one()
        # We choose to implement the flat file instead of the XML
        # file for 2 reasons :
        # 1) the XSD file impose to have the label on the account.move
        # but Eagle has the label on the account.move.line, so that's a
        # problem !
        # 2) CSV files are easier to read/use for a regular accountant.
        # So it will be easier for the accountant to check the file before
        # sending it to the fiscal administration
        company = self.env.user.company_id
        company_legal_data = self._get_company_legal_data(company)

        header = [
            u'JournalCode',  # 0
            u'JournalLib',  # 1
            u'EcritureNum',  # 2
            u'EcritureDate',  # 3
            u'CompteNum',  # 4
            u'CompteLib',  # 5
            u'CompAuxNum',  # 6  We use partner.id
            u'CompAuxLib',  # 7
            u'PieceRef',  # 8
            u'PieceDate',  # 9
            u'EcritureLib',  # 10
            u'Debit',  # 11
            u'Credit',  # 12
            u'EcritureLet',  # 13
            u'DateLet',  # 14
            u'ValidDate',  # 15
            u'Montantdevise',  # 16
            u'Idevise',  # 17
        ]

        rows_to_write = [header]
        # INITIAL BALANCE
        unaffected_earnings_xml_ref = self.env.ref(
            'account.data_unaffected_earnings')
        unaffected_earnings_line = True  # used to make sure that we add the unaffected earning initial balance only once
        if unaffected_earnings_xml_ref:
            #compute the benefit/loss of last year to add in the initial balance of the current year earnings account
            unaffected_earnings_results = self.do_query_unaffected_earnings()
            unaffected_earnings_line = False

        sql_query = '''
        SELECT
            'OUV' AS JournalCode,
            'Balance initiale' AS JournalLib,
            'OUVERTURE/' || %s AS EcritureNum,
            %s AS EcritureDate,
            MIN(aa.code) AS CompteNum,
            replace(replace(MIN(aa.name), '|', '/'), '\t', '') AS CompteLib,
            '' AS CompAuxNum,
            '' AS CompAuxLib,
            '-' AS PieceRef,
            %s AS PieceDate,
            '/' AS EcritureLib,
            replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
            replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
            '' AS EcritureLet,
            '' AS DateLet,
            %s AS ValidDate,
            '' AS Montantdevise,
            '' AS Idevise,
            MIN(aa.id) AS CompteID
        FROM
            account_move_line aml
            LEFT JOIN account_move am ON am.id=aml.move_id
            JOIN account_account aa ON aa.id = aml.account_id
            LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id
        WHERE
            am.date < %s
            AND am.company_id = %s
            AND aat.include_initial_balance = 't'
            AND (aml.debit != 0 OR aml.credit != 0)
        '''

        # For official report: only use posted entries
        if self.export_type == "official":
            sql_query += '''
            AND am.state = 'posted'
            '''

        sql_query += '''
        GROUP BY aml.account_id, aat.type
        HAVING round(sum(aml.balance), %s) != 0
        AND aat.type not in ('receivable', 'payable')
        '''
        formatted_date_from = fields.Date.to_string(self.date_from).replace(
            '-', '')
        date_from = self.date_from
        formatted_date_year = date_from.year
        currency_digits = 2

        self._cr.execute(
            sql_query,
            (formatted_date_year, formatted_date_from, formatted_date_from,
             formatted_date_from, self.date_from, company.id, currency_digits))

        for row in self._cr.fetchall():
            listrow = list(row)
            account_id = listrow.pop()
            if not unaffected_earnings_line:
                account = self.env['account.account'].browse(account_id)
                if account.user_type_id.id == self.env.ref(
                        'account.data_unaffected_earnings').id:
                    #add the benefit/loss of previous fiscal year to the first unaffected earnings account found.
                    unaffected_earnings_line = True
                    current_amount = float(listrow[11].replace(
                        ',', '.')) - float(listrow[12].replace(',', '.'))
                    unaffected_earnings_amount = float(
                        unaffected_earnings_results[11].replace(
                            ',', '.')) - float(
                                unaffected_earnings_results[12].replace(
                                    ',', '.'))
                    listrow_amount = current_amount + unaffected_earnings_amount
                    if float_is_zero(listrow_amount,
                                     precision_digits=currency_digits):
                        continue
                    if listrow_amount > 0:
                        listrow[11] = str(listrow_amount).replace('.', ',')
                        listrow[12] = '0,00'
                    else:
                        listrow[11] = '0,00'
                        listrow[12] = str(-listrow_amount).replace('.', ',')
            rows_to_write.append(listrow)

        #if the unaffected earnings account wasn't in the selection yet: add it manually
        if (not unaffected_earnings_line and unaffected_earnings_results
                and (unaffected_earnings_results[11] != '0,00'
                     or unaffected_earnings_results[12] != '0,00')):
            #search an unaffected earnings account
            unaffected_earnings_account = self.env['account.account'].search(
                [('user_type_id', '=',
                  self.env.ref('account.data_unaffected_earnings').id)],
                limit=1)
            if unaffected_earnings_account:
                unaffected_earnings_results[
                    4] = unaffected_earnings_account.code
                unaffected_earnings_results[
                    5] = unaffected_earnings_account.name
            rows_to_write.append(unaffected_earnings_results)

        # INITIAL BALANCE - receivable/payable
        sql_query = '''
        SELECT
            'OUV' AS JournalCode,
            'Balance initiale' AS JournalLib,
            'OUVERTURE/' || %s AS EcritureNum,
            %s AS EcritureDate,
            MIN(aa.code) AS CompteNum,
            replace(MIN(aa.name), '|', '/') AS CompteLib,
            CASE WHEN rp.ref IS null OR rp.ref = ''
            THEN COALESCE('ID ' || rp.id, '')
            ELSE replace(rp.ref, '|', '/')
            END
            AS CompAuxNum,
            COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib,
            '-' AS PieceRef,
            %s AS PieceDate,
            '/' AS EcritureLib,
            replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit,
            replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit,
            '' AS EcritureLet,
            '' AS DateLet,
            %s AS ValidDate,
            '' AS Montantdevise,
            '' AS Idevise,
            MIN(aa.id) AS CompteID
        FROM
            account_move_line aml
            LEFT JOIN account_move am ON am.id=aml.move_id
            LEFT JOIN res_partner rp ON rp.id=aml.partner_id
            JOIN account_account aa ON aa.id = aml.account_id
            LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id
        WHERE
            am.date < %s
            AND am.company_id = %s
            AND aat.include_initial_balance = 't'
            AND (aml.debit != 0 OR aml.credit != 0)
        '''

        # For official report: only use posted entries
        if self.export_type == "official":
            sql_query += '''
            AND am.state = 'posted'
            '''

        sql_query += '''
        GROUP BY aml.account_id, aat.type, rp.ref, rp.id
        HAVING round(sum(aml.balance), %s) != 0
        AND aat.type in ('receivable', 'payable')
        '''
        self._cr.execute(
            sql_query,
            (formatted_date_year, formatted_date_from, formatted_date_from,
             formatted_date_from, self.date_from, company.id, currency_digits))

        for row in self._cr.fetchall():
            listrow = list(row)
            account_id = listrow.pop()
            rows_to_write.append(listrow)

        # LINES
        sql_query = '''
        SELECT
            replace(replace(aj.code, '|', '/'), '\t', '') AS JournalCode,
            replace(replace(aj.name, '|', '/'), '\t', '') AS JournalLib,
            replace(replace(am.name, '|', '/'), '\t', '') AS EcritureNum,
            TO_CHAR(am.date, 'YYYYMMDD') AS EcritureDate,
            aa.code AS CompteNum,
            replace(replace(aa.name, '|', '/'), '\t', '') AS CompteLib,
            CASE WHEN rp.ref IS null OR rp.ref = ''
            THEN COALESCE('ID ' || rp.id, '')
            ELSE replace(rp.ref, '|', '/')
            END
            AS CompAuxNum,
            COALESCE(replace(replace(rp.name, '|', '/'), '\t', ''), '') AS CompAuxLib,
            CASE WHEN am.ref IS null OR am.ref = ''
            THEN '-'
            ELSE replace(replace(am.ref, '|', '/'), '\t', '')
            END
            AS PieceRef,
            TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
            CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/'
                WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/'
                ELSE replace(replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', ''), '\r', '') END AS EcritureLib,
            replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
            replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
            CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
            CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet,
            TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate,
            CASE
                WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN ''
                ELSE replace(to_char(aml.amount_currency, '000000000000000D99'), '.', ',')
            END AS Montantdevise,
            CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise
        FROM
            account_move_line aml
            LEFT JOIN account_move am ON am.id=aml.move_id
            LEFT JOIN res_partner rp ON rp.id=aml.partner_id
            JOIN account_journal aj ON aj.id = am.journal_id
            JOIN account_account aa ON aa.id = aml.account_id
            LEFT JOIN res_currency rc ON rc.id = aml.currency_id
            LEFT JOIN account_full_reconcile rec ON rec.id = aml.full_reconcile_id
        WHERE
            am.date >= %s
            AND am.date <= %s
            AND am.company_id = %s
            AND (aml.debit != 0 OR aml.credit != 0)
        '''

        # For official report: only use posted entries
        if self.export_type == "official":
            sql_query += '''
            AND am.state = 'posted'
            '''

        sql_query += '''
        ORDER BY
            am.date,
            am.name,
            aml.id
        '''
        self._cr.execute(sql_query, (self.date_from, self.date_to, company.id))

        for row in self._cr.fetchall():
            rows_to_write.append(list(row))

        fecvalue = self._csv_write_rows(rows_to_write)
        end_date = fields.Date.to_string(self.date_to).replace('-', '')
        suffix = ''
        if self.export_type == "nonofficial":
            suffix = '-NONOFFICIAL'

        self.write({
            'fec_data':
            base64.encodestring(fecvalue),
            # Filename = <siren>FECYYYYMMDD where YYYMMDD is the closing date
            'filename':
            '%sFEC%s%s.csv' % (company_legal_data['siren'], end_date, suffix),
        })

        action = {
            'name':
            'FEC',
            'type':
            'ir.actions.act_url',
            'url':
            "web/content/?model=account.fr.fec&id=" + str(self.id) +
            "&filename_field=filename&field=fec_data&download=true&filename=" +
            self.filename,
            'target':
            'self',
        }
        return action

    def _csv_write_rows(self, rows, lineterminator=u'\r\n'):
        """
        Write FEC rows into a file
        It seems that Bercy's bureaucracy is not too happy about the
        empty new line at the End Of File.

        @param {list(list)} rows: the list of rows. Each row is a list of strings
        @param {unicode string} [optional] lineterminator: effective line terminator
            Has nothing to do with the csv writer parameter
            The last line written won't be terminated with it

        @return the value of the file
        """
        fecfile = io.BytesIO()
        writer = pycompat.csv_writer(fecfile, delimiter='|', lineterminator='')

        rows_length = len(rows)
        for i, row in enumerate(rows):
            if not i == rows_length - 1:
                row[-1] += lineterminator
            writer.writerow(row)

        fecvalue = fecfile.getvalue()
        fecfile.close()
        return fecvalue
Example #3
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'

    @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('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.move_id')
    def _compute_invoice(self):
        for order in self:
            invoices = order.mapped('order_line.invoice_lines.move_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.Datetime('Confirmation Date', readonly=1, index=True, copy=False)
    partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="You can find a vendor by its Name, TIN, Email or Internal Reference.")
    dest_address_id = fields.Many2one('res.partner', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", 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=lambda self: self.env.company.currency_id.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', tracking=True)
    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.move', compute="_compute_invoice", string='Bills', copy=False, store=True)
    invoice_status = fields.Selection([
        ('no', 'Nothing to Bill'),
        ('to invoice', 'Waiting Bills'),
        ('invoiced', 'Fully Billed'),
    ], 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='Receipt Date', index=True)

    amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', tracking=True)
    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', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    payment_term_id = fields.Many2one('account.payment.term', 'Payment Terms', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    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, tracking=True,
        default=lambda self: self.env.user, check_company=True)
    company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.company.id)
    currency_rate = fields.Float("Currency Rate", compute='_compute_currency_rate', compute_sudo=True, store=True, readonly=True, help='Ratio between the purchase order currency and the company currency')

    @api.constrains('company_id', 'order_line')
    def _check_order_line_company_id(self):
        for order in self:
            companies = order.order_line.product_id.company_id
            if companies and companies != order.company_id:
                bad_products = order.order_line.product_id.filtered(lambda p: p.company_id and p.company_id != order.company_id)
                raise ValidationError((_("Your quotation contains products from company %s whereas your quotation belongs to company %s. \n Please change the company of your quotation or remove the products from other companies (%s).") % (', '.join(companies.mapped('display_name')), order.company_id.display_name, ', '.join(bad_products.mapped('display_name')))))

    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 models.lazy_name_get(self.browse(purchase_order_ids).with_user(name_get_uid))

    @api.depends('date_order', 'currency_id', 'company_id', 'company_id.currency_id')
    def _compute_currency_rate(self):
        for order in self:
            order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order)

    @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):
        if vals.get('name', 'New') == 'New':
            seq_date = None
            if 'date_order' in vals:
                seq_date = fields.Datetime.context_timestamp(self, fields.Datetime.to_datetime(vals['date_order']))
            vals['name'] = self.env['ir.sequence'].next_by_code('purchase.order', sequence_date=seq_date) or '/'
        return super(PurchaseOrder, self).create(vals)

    def write(self, vals):
        res = super(PurchaseOrder, self).write(vals)
        if vals.get('date_planned'):
            self.order_line.filtered(lambda line: not line.display_type).date_planned = vals['date_planned']
        return res

    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()

    def copy(self, default=None):
        new_po = super(PurchaseOrder, self).copy(default=default)
        for line in new_po.order_line:
            if new_po.date_planned:
                line.date_planned = new_po.date_planned
            elif line.product_id:
                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

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

    @api.onchange('partner_id', 'company_id')
    def onchange_partner_id(self):
        # Ensures all properties and fiscal positions
        # are taken with the company of the order
        # if not defined, force_company doesn't change anything.
        self = self.with_context(force_company=self.company_id.id)
        if not self.partner_id:
            self.fiscal_position_id = False
            self.payment_term_id = False
            self.currency_id = self.env.company.currency_id.id
        else:
            self.fiscal_position_id = self.env['account.fiscal.position'].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.company.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 or not self.env.user.has_group('purchase.group_warning_purchase'):
            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 {}

    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',
            'active_model': 'purchase.order',
            'active_id': self.ids[0],
            '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_mode': 'form',
            'res_model': 'mail.compose.message',
            'views': [(compose_form_id, 'form')],
            'view_id': compose_form_id,
            'target': 'new',
            'context': ctx,
        }

    @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)

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

    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 {}

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

    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.company.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

    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'})

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

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

    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 line.product_id and 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.company.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

    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_move_in_invoice_type')
        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'] = {
            'default_type': 'in_invoice',
            'default_company_id': self.company_id.id,
            'default_purchase_id': self.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.view_move_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
Example #4
0
class EventRegistration(models.Model):
    _name = 'event.registration'
    _description = 'Event Registration'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'name, create_date desc'

    # event
    origin = fields.Char(
        string='Source Document',
        readonly=True,
        help=
        "Reference of the document that created the registration, for example a sales order"
    )
    event_id = fields.Many2one('event.event',
                               string='Event',
                               required=True,
                               readonly=True,
                               states={'draft': [('readonly', False)]})
    # attendee
    partner_id = fields.Many2one('res.partner',
                                 string='Contact',
                                 states={'done': [('readonly', True)]})
    name = fields.Char(string='Attendee Name', index=True)
    email = fields.Char(string='Email')
    phone = fields.Char(string='Phone')
    mobile = fields.Char(string='Mobile')
    # organization
    date_open = fields.Datetime(string='Registration Date',
                                readonly=True,
                                default=lambda self: fields.Datetime.now()
                                )  # weird crash is directly now
    date_closed = fields.Datetime(string='Attended Date', readonly=True)
    event_begin_date = fields.Datetime(string="Event Start Date",
                                       related='event_id.date_begin',
                                       readonly=True)
    event_end_date = fields.Datetime(string="Event End Date",
                                     related='event_id.date_end',
                                     readonly=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 related='event_id.company_id',
                                 store=True,
                                 readonly=True,
                                 states={'draft': [('readonly', False)]})
    state = fields.Selection([('draft', 'Unconfirmed'),
                              ('cancel', 'Cancelled'), ('open', 'Confirmed'),
                              ('done', 'Attended')],
                             string='Status',
                             default='draft',
                             readonly=True,
                             copy=False,
                             tracking=True)

    @api.constrains('event_id', 'state')
    def _check_seats_limit(self):
        for registration in self:
            if registration.event_id.seats_availability == 'limited' and registration.event_id.seats_max and registration.event_id.seats_available < (
                    1 if registration.state == 'draft' else 0):
                raise ValidationError(
                    _('No more seats available for this event.'))

    def _check_auto_confirmation(self):
        if self._context.get('registration_force_draft'):
            return False
        if any(registration.event_id.state != 'confirm'
               or not registration.event_id.auto_confirm or (
                   not registration.event_id.seats_available
                   and registration.event_id.seats_availability == 'limited')
               for registration in self):
            return False
        return True

    @api.model
    def create(self, vals):
        registration = super(EventRegistration, self).create(vals)
        if registration._check_auto_confirmation():
            registration.sudo().confirm_registration()
        return registration

    @api.model
    def _prepare_attendee_values(self, registration):
        """ Method preparing the values to create new attendees based on a
        sales order line. It takes some registration data (dict-based) that are
        optional values coming from an external input like a web page. This method
        is meant to be inherited in various addons that sell events. """
        partner_id = registration.pop('partner_id', self.env.user.partner_id)
        event_id = registration.pop('event_id', False)
        data = {
            'name': registration.get('name', partner_id.name),
            'phone': registration.get('phone', partner_id.phone),
            'mobile': registration.get('mobile', partner_id.mobile),
            'email': registration.get('email', partner_id.email),
            'partner_id': partner_id.id,
            'event_id': event_id and event_id.id or False,
        }
        data.update({
            key: value
            for key, value in registration.items() if key in self._fields
        })
        return data

    def do_draft(self):
        self.write({'state': 'draft'})

    def confirm_registration(self):
        self.write({'state': 'open'})

        # auto-trigger after_sub (on subscribe) mail schedulers, if needed
        onsubscribe_schedulers = self.event_id.event_mail_ids.filtered(
            lambda s: s.interval_type == 'after_sub')
        onsubscribe_schedulers.execute()

    def button_reg_close(self):
        """ Close Registration """
        for registration in self:
            today = fields.Datetime.now()
            if registration.event_id.date_begin <= today and registration.event_id.state == 'confirm':
                registration.write({'state': 'done', 'date_closed': today})
            elif registration.event_id.state == 'draft':
                raise UserError(
                    _("You must wait the event confirmation before doing this action."
                      ))
            else:
                raise UserError(
                    _("You must wait the event starting day before doing this action."
                      ))

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

    @api.onchange('partner_id')
    def _onchange_partner(self):
        if self.partner_id:
            contact_id = self.partner_id.address_get().get('contact', False)
            if contact_id:
                contact = self.env['res.partner'].browse(contact_id)
                self.name = contact.name or self.name
                self.email = contact.email or self.email
                self.phone = contact.phone or self.phone
                self.mobile = contact.mobile or self.mobile

    def _message_get_suggested_recipients(self):
        recipients = super(EventRegistration,
                           self)._message_get_suggested_recipients()
        public_users = self.env['res.users'].sudo()
        public_groups = self.env.ref("base.group_public",
                                     raise_if_not_found=False)
        if public_groups:
            public_users = public_groups.sudo().with_context(
                active_test=False).mapped("users")
        try:
            for attendee in self:
                is_public = attendee.sudo().with_context(
                    active_test=False
                ).partner_id.user_ids in public_users if public_users else False
                if attendee.partner_id and not is_public:
                    attendee._message_add_suggested_recipient(
                        recipients,
                        partner=attendee.partner_id,
                        reason=_('Customer'))
                elif attendee.email:
                    attendee._message_add_suggested_recipient(
                        recipients,
                        email=attendee.email,
                        reason=_('Customer Email'))
        except AccessError:  # no read access rights -> ignore suggested recipients
            pass
        return recipients

    def _message_get_default_recipients(self):
        # Prioritize registration email over partner_id, which may be shared when a single
        # partner booked multiple seats
        return {
            r.id: {
                'partner_ids': [],
                'email_to': r.email,
                'email_cc': False
            }
            for r in self
        }

    def _message_post_after_hook(self, message, msg_vals):
        if self.email and not self.partner_id:
            # we consider that posting a message with a specified recipient (not a follower, a specific one)
            # on a document without customer means that it was created through the chatter using
            # suggested recipients. This heuristic allows to avoid ugly hacks in JS.
            new_partner = message.partner_ids.filtered(
                lambda partner: partner.email == self.email)
            if new_partner:
                self.search([
                    ('partner_id', '=', False),
                    ('email', '=', new_partner.email),
                    ('state', 'not in', ['cancel']),
                ]).write({'partner_id': new_partner.id})
        return super(EventRegistration,
                     self)._message_post_after_hook(message, msg_vals)

    def action_send_badge_email(self):
        """ Open a window to compose an email, with the template - 'event_badge'
            message loaded by default
        """
        self.ensure_one()
        template = self.env.ref('event.event_registration_mail_template_badge')
        compose_form = self.env.ref('mail.email_compose_message_wizard_form')
        ctx = dict(
            default_model='event.registration',
            default_res_id=self.id,
            default_use_template=bool(template),
            default_template_id=template.id,
            default_composition_mode='comment',
            custom_layout="mail.mail_notification_light",
        )
        return {
            'name': _('Compose Email'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'mail.compose.message',
            'views': [(compose_form.id, 'form')],
            'view_id': compose_form.id,
            'target': 'new',
            'context': ctx,
        }

    def get_date_range_str(self):
        self.ensure_one()
        today = fields.Datetime.now()
        event_date = self.event_begin_date
        diff = (event_date.date() - today.date())
        if diff.days <= 0:
            return _('today')
        elif diff.days == 1:
            return _('tomorrow')
        elif (diff.days < 7):
            return _('in %d days') % (diff.days, )
        elif (diff.days < 14):
            return _('next week')
        elif event_date.month == (today + relativedelta(months=+1)).month:
            return _('next month')
        else:
            return _('on ') + format_datetime(self.env,
                                              self.event_begin_date,
                                              tz=self.event_id.date_tz,
                                              dt_format='medium')

    def summary(self):
        self.ensure_one()
        return {'information': []}
Example #5
0
class User(models.Model):
    _inherit = ['res.users']

    # note: a user can only be linked to one employee per company (see sql constraint in ´hr.employee´)
    employee_ids = fields.One2many('hr.employee',
                                   'user_id',
                                   string='Related employee')
    employee_id = fields.Many2one('hr.employee',
                                  string="Company employee",
                                  compute='_compute_company_employee',
                                  search='_search_company_employee',
                                  store=False)

    job_title = fields.Char(related='employee_id.job_title', readonly=False)
    work_phone = fields.Char(related='employee_id.work_phone', readonly=False)
    mobile_phone = fields.Char(related='employee_id.mobile_phone',
                               readonly=False)
    employee_phone = fields.Char(related='employee_id.phone',
                                 readonly=False,
                                 related_sudo=False)
    work_email = fields.Char(related='employee_id.work_email',
                             readonly=False,
                             related_sudo=False)
    category_ids = fields.Many2many(related='employee_id.category_ids',
                                    string="Employee Tags",
                                    readonly=False,
                                    related_sudo=False)
    department_id = fields.Many2one(related='employee_id.department_id',
                                    readonly=False,
                                    related_sudo=False)
    address_id = fields.Many2one(related='employee_id.address_id',
                                 readonly=False,
                                 related_sudo=False)
    work_location = fields.Char(related='employee_id.work_location',
                                readonly=False,
                                related_sudo=False)
    employee_parent_id = fields.Many2one(related='employee_id.parent_id',
                                         related_sudo=False)
    coach_id = fields.Many2one(related='employee_id.coach_id',
                               readonly=False,
                               related_sudo=False)
    address_home_id = fields.Many2one(related='employee_id.address_home_id',
                                      readonly=False,
                                      related_sudo=False)
    is_address_home_a_company = fields.Boolean(
        related='employee_id.is_address_home_a_company',
        readonly=False,
        related_sudo=False)
    private_email = fields.Char(related='address_home_id.email',
                                string="Private Email",
                                readonly=False)
    km_home_work = fields.Integer(related='employee_id.km_home_work',
                                  readonly=False,
                                  related_sudo=False)
    # res.users already have a field bank_account_id and country_id from the res.partner inheritance: don't redefine them
    employee_bank_account_id = fields.Many2one(
        related='employee_id.bank_account_id',
        string="Employee's Bank Account Number",
        related_sudo=False,
        readonly=False)
    employee_country_id = fields.Many2one(related='employee_id.country_id',
                                          string="Employee's Country",
                                          readonly=False,
                                          related_sudo=False)
    identification_id = fields.Char(related='employee_id.identification_id',
                                    readonly=False,
                                    related_sudo=False)
    passport_id = fields.Char(related='employee_id.passport_id',
                              readonly=False,
                              related_sudo=False)
    gender = fields.Selection(related='employee_id.gender',
                              readonly=False,
                              related_sudo=False)
    birthday = fields.Date(related='employee_id.birthday',
                           readonly=False,
                           related_sudo=False)
    place_of_birth = fields.Char(related='employee_id.place_of_birth',
                                 readonly=False,
                                 related_sudo=False)
    country_of_birth = fields.Many2one(related='employee_id.country_of_birth',
                                       readonly=False,
                                       related_sudo=False)
    marital = fields.Selection(related='employee_id.marital',
                               readonly=False,
                               related_sudo=False)
    spouse_complete_name = fields.Char(
        related='employee_id.spouse_complete_name',
        readonly=False,
        related_sudo=False)
    spouse_birthdate = fields.Date(related='employee_id.spouse_birthdate',
                                   readonly=False,
                                   related_sudo=False)
    children = fields.Integer(related='employee_id.children',
                              readonly=False,
                              related_sudo=False)
    emergency_contact = fields.Char(related='employee_id.emergency_contact',
                                    readonly=False,
                                    related_sudo=False)
    emergency_phone = fields.Char(related='employee_id.emergency_phone',
                                  readonly=False,
                                  related_sudo=False)
    visa_no = fields.Char(related='employee_id.visa_no',
                          readonly=False,
                          related_sudo=False)
    permit_no = fields.Char(related='employee_id.permit_no',
                            readonly=False,
                            related_sudo=False)
    visa_expire = fields.Date(related='employee_id.visa_expire',
                              readonly=False,
                              related_sudo=False)
    additional_note = fields.Text(related='employee_id.additional_note',
                                  readonly=False,
                                  related_sudo=False)
    barcode = fields.Char(related='employee_id.barcode',
                          readonly=False,
                          related_sudo=False)
    pin = fields.Char(related='employee_id.pin',
                      readonly=False,
                      related_sudo=False)
    certificate = fields.Selection(related='employee_id.certificate',
                                   readonly=False,
                                   related_sudo=False)
    study_field = fields.Char(related='employee_id.study_field',
                              readonly=False,
                              related_sudo=False)
    study_school = fields.Char(related='employee_id.study_school',
                               readonly=False,
                               related_sudo=False)
    employee_count = fields.Integer(compute='_compute_employee_count')
    hr_presence_state = fields.Selection(
        related='employee_id.hr_presence_state')
    last_activity = fields.Date(related='employee_id.last_activity')
    last_activity_time = fields.Char(related='employee_id.last_activity_time')

    can_edit = fields.Boolean(compute='_compute_can_edit')

    def _compute_can_edit(self):
        can_edit = self.env['ir.config_parameter'].sudo().get_param(
            'hr.hr_employee_self_edit') or self.env.user.has_group(
                'hr.group_hr_user')
        for user in self:
            user.can_edit = can_edit

    @api.depends('employee_ids')
    def _compute_employee_count(self):
        for user in self.with_context(active_test=False):
            user.employee_count = len(user.employee_ids)

    def __init__(self, pool, cr):
        """ Override of __init__ to add access rights.
            Access rights are disabled by default, but allowed
            on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS.
        """
        hr_readable_fields = [
            'active',
            'child_ids',
            'employee_id',
            'employee_ids',
            'employee_parent_id',
            'hr_presence_state',
            'last_activity',
            'last_activity_time',
            'can_edit',
        ]

        hr_writable_fields = [
            'additional_note',
            'address_home_id',
            'address_id',
            'barcode',
            'birthday',
            'category_ids',
            'children',
            'coach_id',
            'country_of_birth',
            'department_id',
            'display_name',
            'emergency_contact',
            'emergency_phone',
            'employee_bank_account_id',
            'employee_country_id',
            'gender',
            'identification_id',
            'is_address_home_a_company',
            'job_title',
            'private_email',
            'km_home_work',
            'marital',
            'mobile_phone',
            'notes',
            'employee_parent_id',
            'passport_id',
            'permit_no',
            'employee_phone',
            'pin',
            'place_of_birth',
            'spouse_birthdate',
            'spouse_complete_name',
            'visa_expire',
            'visa_no',
            'work_email',
            'work_location',
            'work_phone',
            'certificate',
            'study_field',
            'study_school',
        ]

        init_res = super(User, self).__init__(pool, cr)
        # duplicate list to avoid modifying the original reference
        type(self).SELF_READABLE_FIELDS = type(
            self
        ).SELF_READABLE_FIELDS + hr_readable_fields + hr_writable_fields
        type(self).SELF_WRITEABLE_FIELDS = type(
            self).SELF_WRITEABLE_FIELDS + hr_writable_fields
        return init_res

    @api.model
    def fields_view_get(self,
                        view_id=None,
                        view_type='form',
                        toolbar=False,
                        submenu=False):
        # When the front-end loads the views it gets the list of available fields
        # for the user (according to its access rights). Later, when the front-end wants to
        # populate the view with data, it only asks to read those available fields.
        # However, in this case, we want the user to be able to read/write its own data,
        # even if they are protected by groups.
        # We make the front-end aware of those fields by sending all field definitions.
        # Note: limit the `sudo` to the only action of "editing own profile" action in order to
        # avoid breaking `groups` mecanism on res.users form view.
        profile_view = self.env.ref("hr.res_users_view_form_profile")
        if profile_view and view_id == profile_view.id:
            self = self.with_user(SUPERUSER_ID)
        return super(User, self).fields_view_get(view_id=view_id,
                                                 view_type=view_type,
                                                 toolbar=toolbar,
                                                 submenu=submenu)

    def write(self, vals):
        """
        Synchronize user and its related employee
        and check access rights if employees are not allowed to update
        their own data (otherwise sudo is applied for self data).
        """
        hr_fields = {
            field
            for field_name, field in self._fields.items()
            if field.related_field and field.related_field.model_name ==
            'hr.employee' and field_name in vals
        }
        can_edit_self = self.env['ir.config_parameter'].sudo().get_param(
            'hr.hr_employee_self_edit') or self.env.user.has_group(
                'hr.group_hr_user')
        if hr_fields and not can_edit_self:
            # Raise meaningful error message
            raise AccessError(
                _("You are only allowed to update your preferences. Please contact a HR officer to update other informations."
                  ))

        result = super(User, self).write(vals)

        employee_values = {}
        for fname in [
                f for f in ['name', 'email', 'image_1920', 'tz'] if f in vals
        ]:
            employee_values[fname] = vals[fname]
        if employee_values:
            if 'email' in employee_values:
                employee_values['work_email'] = employee_values.pop('email')
            if 'image_1920' in vals:
                without_image = self.env['hr.employee'].sudo().search([
                    ('user_id', 'in', self.ids), ('image_1920', '=', False)
                ])
                with_image = self.env['hr.employee'].sudo().search([
                    ('user_id', 'in', self.ids), ('image_1920', '!=', False)
                ])
                without_image.write(employee_values)
                if not can_edit_self:
                    employee_values.pop('image_1920')
                with_image.write(employee_values)
            else:
                self.env['hr.employee'].sudo().search([
                    ('user_id', 'in', self.ids)
                ]).write(employee_values)
        return result

    @api.model
    def action_get(self):
        if self.env.user.employee_id:
            return self.sudo().env.ref('hr.res_users_action_my').read()[0]
        return super(User, self).action_get()

    @api.depends('employee_ids')
    @api.depends_context('force_company')
    def _compute_company_employee(self):
        for user in self:
            user.employee_id = self.env['hr.employee'].search(
                [('id', 'in', user.employee_ids.ids),
                 ('company_id', '=', self.env.company.id)],
                limit=1)

    def _search_company_employee(self, operator, value):
        employees = self.env['hr.employee'].search(
            [('name', operator, value), '|',
             ('company_id', '=', self.env.company.id),
             ('company_id', '=', False)],
            order='company_id ASC')
        return [('id', 'in', employees.mapped('user_id').ids)]

    def action_create_employee(self):
        self.ensure_one()
        self.env['hr.employee'].create(
            dict(user_id=self.id,
                 name=self.name,
                 **self.env['hr.employee']._sync_user(self)))
Example #6
0
class Lead2OpportunityMassConvert(models.TransientModel):

    _name = 'crm.lead2opportunity.partner.mass'
    _description = 'Convert Lead to Opportunity (in mass)'
    _inherit = 'crm.lead2opportunity.partner'

    @api.model
    def default_get(self, fields):
        res = super(Lead2OpportunityMassConvert, self).default_get(fields)
        if 'partner_id' in fields:  # avoid forcing the partner of the first lead as default
            res['partner_id'] = False
        if 'action' in fields:
            res['action'] = 'each_exist_or_create'
        if 'name' in fields:
            res['name'] = 'convert'
        if 'opportunity_ids' in fields:
            res['opportunity_ids'] = False
        return res

    user_ids = fields.Many2many('res.users', string='Salesmen')
    team_id = fields.Many2one('crm.team',
                              'Sales Team',
                              index=True,
                              oldname='section_id')
    deduplicate = fields.Boolean(
        'Apply deduplication',
        default=True,
        help='Merge with existing leads/opportunities of each partner')
    action = fields.Selection(
        [('each_exist_or_create', 'Use existing partner or create'),
         ('nothing', 'Do not link to a customer')],
        'Related Customer',
        required=True)
    force_assignation = fields.Boolean(
        'Force assignation',
        help=
        'If unchecked, this will leave the salesman of duplicated opportunities'
    )

    @api.onchange('action')
    def _onchange_action(self):
        if self.action != 'exist':
            self.partner_id = False

    @api.onchange('deduplicate')
    def _onchange_deduplicate(self):
        active_leads = self.env['crm.lead'].browse(self._context['active_ids'])
        partner_ids = [
            (lead.partner_id.id, lead.partner_id and lead.partner_id.email
             or lead.email_from) for lead in active_leads
        ]
        partners_duplicated_leads = {}
        for partner_id, email in partner_ids:
            duplicated_leads = self._get_duplicated_leads(partner_id, email)
            if len(duplicated_leads) > 1:
                partners_duplicated_leads.setdefault(
                    (partner_id, email), []).extend(duplicated_leads)

        leads_with_duplicates = []
        for lead in active_leads:
            lead_tuple = (lead.partner_id.id, lead.partner_id.email
                          if lead.partner_id else lead.email_from)
            if len(partners_duplicated_leads.get(lead_tuple, [])) > 1:
                leads_with_duplicates.append(lead.id)

        self.opportunity_ids = self.env['crm.lead'].browse(
            leads_with_duplicates)

    @api.multi
    def _convert_opportunity(self, vals):
        """ When "massively" (more than one at a time) converting leads to
            opportunities, check the salesteam_id and salesmen_ids and update
            the values before calling super.
        """
        self.ensure_one()
        salesteam_id = self.team_id.id if self.team_id else False
        salesmen_ids = []
        if self.user_ids:
            salesmen_ids = self.user_ids.ids
        vals.update({'user_ids': salesmen_ids, 'team_id': salesteam_id})
        return super(Lead2OpportunityMassConvert,
                     self)._convert_opportunity(vals)

    @api.multi
    def mass_convert(self):
        self.ensure_one()
        if self.name == 'convert' and self.deduplicate:
            merged_lead_ids = set()
            remaining_lead_ids = set()
            lead_selected = self._context.get('active_ids', [])
            for lead_id in lead_selected:
                if lead_id not in merged_lead_ids:
                    lead = self.env['crm.lead'].browse(lead_id)
                    duplicated_leads = self._get_duplicated_leads(
                        lead.partner_id.id, lead.partner_id.email
                        if lead.partner_id else lead.email_from)
                    if len(duplicated_leads) > 1:
                        lead = duplicated_leads.merge_opportunity()
                        merged_lead_ids.update(duplicated_leads.ids)
                        remaining_lead_ids.add(lead.id)
            active_ids = set(self._context.get('active_ids', {}))
            active_ids = (active_ids - merged_lead_ids) | remaining_lead_ids

            self = self.with_context(active_ids=list(
                active_ids))  # only update active_ids when there are set
        no_force_assignation = self._context.get('no_force_assignation',
                                                 not self.force_assignation)
        return self.with_context(
            no_force_assignation=no_force_assignation).action_apply()
Example #7
0
class EventEvent(models.Model):
    """Event"""
    _name = 'event.event'
    _description = 'Event'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'date_begin'

    name = fields.Char(string='Event',
                       translate=True,
                       required=True,
                       readonly=False,
                       states={'done': [('readonly', True)]})
    active = fields.Boolean(default=True)
    user_id = fields.Many2one('res.users',
                              string='Responsible',
                              default=lambda self: self.env.user,
                              tracking=True,
                              readonly=False,
                              states={'done': [('readonly', True)]})
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 change_default=True,
                                 default=lambda self: self.env.company,
                                 required=False,
                                 readonly=False,
                                 states={'done': [('readonly', True)]})
    organizer_id = fields.Many2one(
        'res.partner',
        string='Organizer',
        tracking=True,
        default=lambda self: self.env.company.partner_id,
        domain=
        "['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    event_type_id = fields.Many2one('event.type',
                                    string='Category',
                                    readonly=False,
                                    states={'done': [('readonly', True)]})
    color = fields.Integer('Kanban Color Index')
    event_mail_ids = fields.One2many('event.mail',
                                     'event_id',
                                     string='Mail Schedule',
                                     copy=True)

    # Seats and computation
    seats_max = fields.Integer(
        string='Maximum Attendees Number',
        readonly=True,
        states={
            'draft': [('readonly', False)],
            'confirm': [('readonly', False)]
        },
        help=
        "For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted."
    )
    seats_availability = fields.Selection([('limited', 'Limited'),
                                           ('unlimited', 'Unlimited')],
                                          'Maximum Attendees',
                                          required=True,
                                          default='unlimited')
    seats_min = fields.Integer(
        string='Minimum Attendees',
        help=
        "For each event you can define a minimum reserved seats (number of attendees), if it does not reach the mentioned registrations the event can not be confirmed (keep 0 to ignore this rule)"
    )
    seats_reserved = fields.Integer(string='Reserved Seats',
                                    store=True,
                                    readonly=True,
                                    compute='_compute_seats')
    seats_available = fields.Integer(string='Available Seats',
                                     store=True,
                                     readonly=True,
                                     compute='_compute_seats')
    seats_unconfirmed = fields.Integer(string='Unconfirmed Seat Reservations',
                                       store=True,
                                       readonly=True,
                                       compute='_compute_seats')
    seats_used = fields.Integer(string='Number of Participants',
                                store=True,
                                readonly=True,
                                compute='_compute_seats')
    seats_expected = fields.Integer(string='Number of Expected Attendees',
                                    compute_sudo=True,
                                    readonly=True,
                                    compute='_compute_seats')

    # Registration fields
    registration_ids = fields.One2many('event.registration',
                                       'event_id',
                                       string='Attendees',
                                       readonly=False,
                                       states={'done': [('readonly', True)]})
    # Date fields
    date_tz = fields.Selection('_tz_get',
                               string='Timezone',
                               required=True,
                               default=lambda self: self.env.user.tz or 'UTC')
    date_begin = fields.Datetime(string='Start Date',
                                 required=True,
                                 tracking=True,
                                 states={'done': [('readonly', True)]})
    date_end = fields.Datetime(string='End Date',
                               required=True,
                               tracking=True,
                               states={'done': [('readonly', True)]})
    date_begin_located = fields.Char(string='Start Date Located',
                                     compute='_compute_date_begin_tz')
    date_end_located = fields.Char(string='End Date Located',
                                   compute='_compute_date_end_tz')
    is_one_day = fields.Boolean(compute='_compute_field_is_one_day')

    state = fields.Selection(
        [('draft', 'Unconfirmed'), ('cancel', 'Cancelled'),
         ('confirm', 'Confirmed'), ('done', 'Done')],
        string='Status',
        default='draft',
        readonly=True,
        required=True,
        copy=False,
        help=
        "If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'."
    )
    auto_confirm = fields.Boolean(string='Autoconfirm Registrations')
    is_online = fields.Boolean('Online Event')
    address_id = fields.Many2one(
        'res.partner',
        string='Location',
        default=lambda self: self.env.company.partner_id,
        readonly=False,
        states={'done': [('readonly', True)]},
        domain=
        "['|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        tracking=True)
    country_id = fields.Many2one('res.country',
                                 'Country',
                                 related='address_id.country_id',
                                 store=True,
                                 readonly=False)
    twitter_hashtag = fields.Char('Twitter Hashtag')
    description = fields.Html(string='Description',
                              translate=html_translate,
                              sanitize_attributes=False,
                              readonly=False,
                              states={'done': [('readonly', True)]})
    # badge fields
    badge_front = fields.Html(string='Badge Front')
    badge_back = fields.Html(string='Badge Back')
    badge_innerleft = fields.Html(string='Badge Inner Left')
    badge_innerright = fields.Html(string='Badge Inner Right')
    event_logo = fields.Html(string='Event Logo')

    @api.depends('seats_max', 'registration_ids.state')
    def _compute_seats(self):
        """ Determine reserved, available, reserved but unconfirmed and used seats. """
        # initialize fields to 0
        for event in self:
            event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0
        # aggregate registrations by event and by state
        if self.ids:
            state_field = {
                'draft': 'seats_unconfirmed',
                'open': 'seats_reserved',
                'done': 'seats_used',
            }
            query = """ SELECT event_id, state, count(event_id)
                        FROM event_registration
                        WHERE event_id IN %s AND state IN ('draft', 'open', 'done')
                        GROUP BY event_id, state
                    """
            self.env['event.registration'].flush(['event_id', 'state'])
            self._cr.execute(query, (tuple(self.ids), ))
            for event_id, state, num in self._cr.fetchall():
                event = self.browse(event_id)
                event[state_field[state]] += num
        # compute seats_available
        for event in self:
            if event.seats_max > 0:
                event.seats_available = event.seats_max - (
                    event.seats_reserved + event.seats_used)
            event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used

    @api.model
    def _tz_get(self):
        return [(x, x) for x in pytz.all_timezones]

    @api.depends('date_tz', 'date_begin')
    def _compute_date_begin_tz(self):
        for event in self:
            if event.date_begin:
                event.date_begin_located = format_datetime(self.env,
                                                           event.date_begin,
                                                           tz=event.date_tz,
                                                           dt_format='medium')
            else:
                event.date_begin_located = False

    @api.depends('date_tz', 'date_end')
    def _compute_date_end_tz(self):
        for event in self:
            if event.date_end:
                event.date_end_located = format_datetime(self.env,
                                                         event.date_end,
                                                         tz=event.date_tz,
                                                         dt_format='medium')
            else:
                event.date_end_located = False

    @api.depends('date_begin', 'date_end', 'date_tz')
    def _compute_field_is_one_day(self):
        for event in self:
            # Need to localize because it could begin late and finish early in
            # another timezone
            event = event.with_context(tz=event.date_tz)
            begin_tz = fields.Datetime.context_timestamp(
                event, event.date_begin)
            end_tz = fields.Datetime.context_timestamp(event, event.date_end)
            event.is_one_day = (begin_tz.date() == end_tz.date())

    @api.onchange('is_online')
    def _onchange_is_online(self):
        if self.is_online:
            self.address_id = False

    @api.onchange('event_type_id')
    def _onchange_type(self):
        if self.event_type_id:
            self.seats_min = self.event_type_id.default_registration_min
            self.seats_max = self.event_type_id.default_registration_max
            if self.event_type_id.default_registration_max:
                self.seats_availability = 'limited'

            if self.event_type_id.auto_confirm:
                self.auto_confirm = self.event_type_id.auto_confirm

            if self.event_type_id.use_hashtag:
                self.twitter_hashtag = self.event_type_id.default_hashtag

            if self.event_type_id.use_timezone:
                self.date_tz = self.event_type_id.default_timezone

            self.is_online = self.event_type_id.is_online

            if self.event_type_id.event_type_mail_ids:
                self.event_mail_ids = [(5, 0, 0)] + [(0, 0, {
                    attribute_name: line[attribute_name]
                    for attribute_name in self.env['event.type.mail'].
                    _get_event_mail_fields_whitelist()
                }) for line in self.event_type_id.event_type_mail_ids]

    @api.constrains('seats_min', 'seats_max', 'seats_availability')
    def _check_seats_min_max(self):
        if any(event.seats_availability == 'limited'
               and event.seats_min > event.seats_max for event in self):
            raise ValidationError(
                _('Maximum attendees number should be greater than minimum attendees number.'
                  ))

    @api.constrains('seats_max', 'seats_available')
    def _check_seats_limit(self):
        if any(event.seats_availability == 'limited' and event.seats_max
               and event.seats_available < 0 for event in self):
            raise ValidationError(_('No more available seats.'))

    @api.constrains('date_begin', 'date_end')
    def _check_closing_date(self):
        for event in self:
            if event.date_end < event.date_begin:
                raise ValidationError(
                    _('The closing date cannot be earlier than the beginning date.'
                      ))

    @api.depends('name', 'date_begin', 'date_end')
    def name_get(self):
        result = []
        for event in self:
            date_begin = fields.Datetime.from_string(event.date_begin)
            date_end = fields.Datetime.from_string(event.date_end)
            dates = [
                fields.Date.to_string(
                    fields.Datetime.context_timestamp(event, dt))
                for dt in [date_begin, date_end] if dt
            ]
            dates = sorted(set(dates))
            result.append(
                (event.id, '%s (%s)' % (event.name, ' - '.join(dates))))
        return result

    @api.model
    def create(self, vals):
        res = super(EventEvent, self).create(vals)
        if res.organizer_id:
            res.message_subscribe([res.organizer_id.id])
        if res.auto_confirm:
            res.button_confirm()
        return res

    def write(self, vals):
        res = super(EventEvent, self).write(vals)
        if vals.get('organizer_id'):
            self.message_subscribe([vals['organizer_id']])
        return res

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

    def button_draft(self):
        self.write({'state': 'draft'})

    def button_cancel(self):
        if any('done' in event.mapped('registration_ids.state')
               for event in self):
            raise UserError(
                _("There are already attendees who attended this event. Please reset it to draft if you want to cancel this event."
                  ))
        self.registration_ids.write({'state': 'cancel'})
        self.state = 'cancel'

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

    def button_confirm(self):
        self.write({'state': 'confirm'})

    def mail_attendees(self,
                       template_id,
                       force_send=False,
                       filter_func=lambda self: self.state != 'cancel'):
        for event in self:
            for attendee in event.registration_ids.filtered(filter_func):
                self.env['mail.template'].browse(template_id).send_mail(
                    attendee.id, force_send=force_send)

    def _is_event_registrable(self):
        return self.date_end > fields.Datetime.now()

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

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

            cal_event.add('created').value = fields.Datetime.now().replace(
                tzinfo=pytz.timezone('UTC'))
            cal_event.add('dtstart').value = fields.Datetime.from_string(
                event.date_begin).replace(tzinfo=pytz.timezone('UTC'))
            cal_event.add('dtend').value = fields.Datetime.from_string(
                event.date_end).replace(tzinfo=pytz.timezone('UTC'))
            cal_event.add('summary').value = event.name
            if event.address_id:
                cal_event.add(
                    'location').value = event.sudo().address_id.contact_address

            result[event.id] = cal.serialize().encode('utf-8')
        return result
Example #8
0
class OpMediaQueue(models.Model):
    _name = 'op.media.queue'
    _inherit = 'mail.thread'
    _rec_name = 'user_id'
    _description = 'Media Queue Request'

    name = fields.Char("Sequence No", readonly=True, copy=False, default='/')
    partner_id = fields.Many2one('res.partner', 'Student/Faculty')
    media_id = fields.Many2one('op.media',
                               'Media',
                               required=True,
                               track_visibility='onchange')
    date_from = fields.Date('From Date',
                            required=True,
                            default=fields.Date.today())
    date_to = fields.Date('To Date', required=True)
    user_id = fields.Many2one('res.users',
                              'User',
                              readonly=True,
                              default=lambda self: self.env.uid)
    state = fields.Selection([('request', 'Request'), ('accept', 'Accepted'),
                              ('reject', 'Rejected')],
                             'Status',
                             copy=False,
                             default='request',
                             track_visibility='onchange')

    @api.onchange('user_id')
    def onchange_user(self):
        self.partner_id = self.user_id.partner_id.id

    @api.constrains('date_from', 'date_to')
    def _check_date(self):
        if self.date_from > self.date_to:
            raise ValidationError(_('To Date cannot be set before From Date.'))

    @api.model
    def create(self, vals):
        if self.env.user.child_ids:
            raise Warning(
                _('Invalid Action!\n Parent can not create \
            Media Queue Requests!'))
        if vals.get('name', '/') == '/':
            vals['name'] = self.env['ir.sequence'].next_by_code(
                'op.media.queue') or '/'
        return super(OpMediaQueue, self).create(vals)

    @api.multi
    def write(self, vals):
        if self.env.user.child_ids:
            raise Warning(
                _('Invalid Action!\n Parent can not edit \
            Media Queue Requests!'))
        return super(OpMediaQueue, self).write(vals)

    @api.multi
    def do_reject(self):
        self.state = 'reject'

    @api.multi
    def do_accept(self):
        self.state = 'accept'

    @api.multi
    def do_request_again(self):
        self.state = 'request'
Example #9
0
class StockScrap(models.Model):
    _name = 'stock.scrap'
    _order = 'id desc'
    _description = 'Scrap'

    def _get_default_scrap_location_id(self):
        return self.env['stock.location'].search(
            [('scrap_location', '=', True),
             ('company_id', 'in', [self.env.user.company_id.id, False])],
            limit=1).id

    def _get_default_location_id(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
        return None

    name = fields.Char('Reference',
                       default=lambda self: _('New'),
                       copy=False,
                       readonly=True,
                       required=True,
                       states={'done': [('readonly', True)]})
    origin = fields.Char(string='Source Document')
    product_id = fields.Many2one('product.product',
                                 'Product',
                                 domain=[('type', 'in', ['product', 'consu'])],
                                 required=True,
                                 states={'done': [('readonly', True)]})
    product_uom_id = fields.Many2one('uom.uom',
                                     'Unit of Measure',
                                     required=True,
                                     states={'done': [('readonly', True)]})
    tracking = fields.Selection('Product Tracking',
                                readonly=True,
                                related="product_id.tracking")
    lot_id = fields.Many2one('stock.production.lot',
                             'Lot',
                             states={'done': [('readonly', True)]},
                             domain="[('product_id', '=', product_id)]")
    package_id = fields.Many2one('stock.quant.package',
                                 'Package',
                                 states={'done': [('readonly', True)]})
    owner_id = fields.Many2one('res.partner',
                               'Owner',
                               states={'done': [('readonly', True)]})
    move_id = fields.Many2one('stock.move', 'Scrap Move', readonly=True)
    picking_id = fields.Many2one('stock.picking',
                                 'Picking',
                                 states={'done': [('readonly', True)]})
    location_id = fields.Many2one('stock.location',
                                  'Location',
                                  domain="[('usage', '=', 'internal')]",
                                  required=True,
                                  states={'done': [('readonly', True)]},
                                  default=_get_default_location_id)
    scrap_location_id = fields.Many2one(
        'stock.location',
        'Scrap Location',
        default=_get_default_scrap_location_id,
        domain="[('scrap_location', '=', True)]",
        required=True,
        states={'done': [('readonly', True)]})
    scrap_qty = fields.Float('Quantity',
                             default=1.0,
                             required=True,
                             states={'done': [('readonly', True)]})
    state = fields.Selection([('draft', 'Draft'), ('done', 'Done')],
                             string='Status',
                             default="draft")
    date_expected = fields.Datetime('Expected Date',
                                    default=fields.Datetime.now)

    @api.onchange('picking_id')
    def _onchange_picking_id(self):
        if self.picking_id:
            self.location_id = (
                self.picking_id.state == 'done'
            ) and self.picking_id.location_dest_id.id or self.picking_id.location_id.id

    @api.onchange('product_id')
    def onchange_product_id(self):
        if self.product_id:
            self.product_uom_id = self.product_id.uom_id.id

    @api.model
    def create(self, vals):
        if 'name' not in vals or vals['name'] == _('New'):
            vals['name'] = self.env['ir.sequence'].next_by_code(
                'stock.scrap') or _('New')
        scrap = super(StockScrap, self).create(vals)
        return scrap

    def unlink(self):
        if 'done' in self.mapped('state'):
            raise UserError(_('You cannot delete a scrap which is done.'))
        return super(StockScrap, self).unlink()

    def _get_origin_moves(self):
        return self.picking_id and self.picking_id.move_lines.filtered(
            lambda x: x.product_id == self.product_id)

    def _prepare_move_values(self):
        self.ensure_one()
        return {
            'name':
            self.name,
            'origin':
            self.origin or self.picking_id.name or self.name,
            'product_id':
            self.product_id.id,
            'product_uom':
            self.product_uom_id.id,
            'product_uom_qty':
            self.scrap_qty,
            'location_id':
            self.location_id.id,
            'scrapped':
            True,
            'location_dest_id':
            self.scrap_location_id.id,
            'move_line_ids': [(0, 0, {
                'product_id': self.product_id.id,
                'product_uom_id': self.product_uom_id.id,
                'qty_done': self.scrap_qty,
                'location_id': self.location_id.id,
                'location_dest_id': self.scrap_location_id.id,
                'package_id': self.package_id.id,
                'owner_id': self.owner_id.id,
                'lot_id': self.lot_id.id,
            })],
            #             'restrict_partner_id': self.owner_id.id,
            'picking_id':
            self.picking_id.id
        }

    @api.multi
    def do_scrap(self):
        for scrap in self:
            move = self.env['stock.move'].create(scrap._prepare_move_values())
            # master: replace context by cancel_backorder
            move.with_context(is_scrap=True)._action_done()
            scrap.write({'move_id': move.id, 'state': 'done'})
        return True

    def action_get_stock_picking(self):
        action = self.env.ref('stock.action_picking_tree_all').read([])[0]
        action['domain'] = [('id', '=', self.picking_id.id)]
        return action

    def action_get_stock_move_lines(self):
        action = self.env.ref('stock.stock_move_line_action').read([])[0]
        action['domain'] = [('move_id', '=', self.move_id.id)]
        return action

    def action_validate(self):
        self.ensure_one()
        if self.product_id.type != 'product':
            return self.do_scrap()
        precision = self.env['decimal.precision'].precision_get(
            'Product Unit of Measure')
        available_qty = sum(self.env['stock.quant']._gather(
            self.product_id,
            self.location_id,
            self.lot_id,
            self.package_id,
            self.owner_id,
            strict=True).mapped('quantity'))
        scrap_qty = self.product_uom_id._compute_quantity(
            self.scrap_qty, self.product_id.uom_id)
        if float_compare(available_qty, scrap_qty,
                         precision_digits=precision) >= 0:
            return self.do_scrap()
        else:
            return {
                'name':
                _('Insufficient Quantity'),
                'view_type':
                'form',
                'view_mode':
                'form',
                'res_model':
                'stock.warn.insufficient.qty.scrap',
                'view_id':
                self.env.ref(
                    'stock.stock_warn_insufficient_qty_scrap_form_view').id,
                'type':
                'ir.actions.act_window',
                'context': {
                    'default_product_id': self.product_id.id,
                    'default_location_id': self.location_id.id,
                    'default_scrap_id': self.id
                },
                'target':
                'new'
            }
Example #10
0
class ResourceResource(models.Model):
    _name = "resource.resource"
    _description = "Resources"

    @api.model
    def default_get(self, fields):
        res = super(ResourceResource, self).default_get(fields)
        if not res.get('calendar_id') and res.get('company_id'):
            company = self.env['res.company'].browse(res['company_id'])
            res['calendar_id'] = company.resource_calendar_id.id
        return res

    name = fields.Char(required=True)
    active = fields.Boolean(
        'Active',
        default=True,
        track_visibility='onchange',
        help=
        "If the active field is set to False, it will allow you to hide the resource record without removing it."
    )
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        default=lambda self: self.env['res.company']._company_default_get())
    resource_type = fields.Selection([('user', 'Human'),
                                      ('material', 'Material')],
                                     string='Resource Type',
                                     default='user',
                                     required=True)
    user_id = fields.Many2one(
        'res.users',
        string='User',
        help='Related user name for the resource to manage its access.')
    time_efficiency = fields.Float(
        'Efficiency Factor',
        default=100,
        required=True,
        help=
        "This field is used to calculate the the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100%, then the expected duration will be one hour. If the efficiency factor is 200%, however the expected duration will be 30 minutes."
    )
    calendar_id = fields.Many2one("resource.calendar",
                                  string='Working Time',
                                  default=lambda self: self.env['res.company'].
                                  _company_default_get().resource_calendar_id,
                                  required=True,
                                  help="Define the schedule of resource")
    tz = fields.Selection(
        _tz_get,
        string='Timezone',
        required=True,
        default=lambda self: self._context.get('tz'
                                               ) or self.env.user.tz or 'UTC',
        help=
        "This field is used in order to define in which timezone the resources will work."
    )

    _sql_constraints = [
        ('check_time_efficiency', 'CHECK(time_efficiency>0)',
         'Time efficiency must be strictly positive'),
    ]

    @api.multi
    @api.constrains('time_efficiency')
    def _check_time_efficiency(self):
        for record in self:
            if record.time_efficiency == 0:
                raise ValidationError(
                    _('The efficiency factor cannot be equal to 0.'))

    @api.model
    def create(self, values):
        if values.get('company_id') and not values.get('calendar_id'):
            values['calendar_id'] = self.env['res.company'].browse(
                values['company_id']).resource_calendar_id.id
        if not values.get('tz'):
            # retrieve timezone on user or calendar
            tz = (self.env['res.users'].browse(values.get('user_id')).tz
                  or self.env['resource.calendar'].browse(
                      values.get('calendar_id')).tz)
            if tz:
                values['tz'] = tz
        return super(ResourceResource, self).create(values)

    @api.multi
    @api.returns('self', lambda value: value.id)
    def copy(self, default=None):
        self.ensure_one()
        if default is None:
            default = {}
        if not default.get('name'):
            default.update(name=_('%s (copy)') % (self.name))
        return super(ResourceResource, self).copy(default)

    @api.onchange('company_id')
    def _onchange_company_id(self):
        if self.company_id:
            self.calendar_id = self.company_id.resource_calendar_id.id

    @api.onchange('user_id')
    def _onchange_user_id(self):
        if self.user_id:
            self.tz = self.user_id.tz
Example #11
0
class StockRule(models.Model):
    _inherit = 'stock.rule'
    action = fields.Selection(selection_add=[('manufacture', 'Manufacture')])

    def _get_message_dict(self):
        message_dict = super(StockRule, self)._get_message_dict()
        source, destination, operation = self._get_message_values()
        manufacture_message = _(
            'When products are needed in <b>%s</b>, <br/> a manufacturing order is created to fulfill the need.'
        ) % (destination)
        if self.location_src_id:
            manufacture_message += _(
                ' <br/><br/> The components will be taken from <b>%s</b>.') % (
                    source)
        message_dict.update({'manufacture': manufacture_message})
        return message_dict

    @api.onchange('action')
    def _onchange_action_operation(self):
        domain = {'picking_type_id': []}
        if self.action == 'manufacture':
            domain = {'picking_type_id': [('code', '=', 'mrp_operation')]}
        return {'domain': domain}

    @api.multi
    def _run_manufacture(self, product_id, product_qty, product_uom,
                         location_id, name, origin, values):
        Production = self.env['mrp.production']
        ProductionSudo = Production.sudo().with_context(
            force_company=values['company_id'].id)
        bom = self._get_matching_bom(product_id, values)
        if not bom:
            msg = _(
                'There is no Bill of Material found for the product %s. Please define a Bill of Material for this product.'
            ) % (product_id.display_name, )
            raise UserError(msg)

        # create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example)
        production = ProductionSudo.create(
            self._prepare_mo_vals(product_id, product_qty, product_uom,
                                  location_id, name, origin, values, bom))
        origin_production = values.get('move_dest_ids') and values[
            'move_dest_ids'][0].raw_material_production_id or False
        orderpoint = values.get('orderpoint_id')
        if orderpoint:
            production.message_post_with_view(
                'mail.message_origin_link',
                values={
                    'self': production,
                    'origin': orderpoint
                },
                subtype_id=self.env.ref('mail.mt_note').id)
        if origin_production:
            production.message_post_with_view(
                'mail.message_origin_link',
                values={
                    'self': production,
                    'origin': origin_production
                },
                subtype_id=self.env.ref('mail.mt_note').id)
        return True

    @api.multi
    def _get_matching_bom(self, product_id, values):
        if values.get('bom_id', False):
            return values['bom_id']
        return self.env['mrp.bom'].with_context(
            company_id=values['company_id'].id,
            force_company=values['company_id'].id)._bom_find(
                product=product_id, picking_type=self.picking_type_id
            )  # TDE FIXME: context bullshit

    def _prepare_mo_vals(self, product_id, product_qty, product_uom,
                         location_id, name, origin, values, bom):
        return {
            'origin':
            origin,
            'product_id':
            product_id.id,
            'product_qty':
            product_qty,
            'product_uom_id':
            product_uom.id,
            'location_src_id':
            self.location_src_id.id
            or self.picking_type_id.default_location_src_id.id
            or location_id.id,
            'location_dest_id':
            location_id.id,
            'bom_id':
            bom.id,
            'date_planned_start':
            fields.Datetime.to_string(
                self._get_date_planned(product_id, values)),
            'date_planned_finished':
            values['date_planned'],
            'procurement_group_id':
            False,
            'propagate':
            self.propagate,
            'picking_type_id':
            self.picking_type_id.id or values['warehouse_id'].manu_type_id.id,
            'company_id':
            values['company_id'].id,
            'move_dest_ids':
            values.get('move_dest_ids') and [(4, x.id)
                                             for x in values['move_dest_ids']]
            or False,
        }

    def _get_date_planned(self, product_id, values):
        format_date_planned = fields.Datetime.from_string(
            values['date_planned'])
        date_planned = format_date_planned - relativedelta(
            days=product_id.produce_delay or 0.0)
        date_planned = date_planned - relativedelta(
            days=values['company_id'].manufacturing_lead)
        return date_planned

    def _push_prepare_move_copy_values(self, move_to_copy, new_date):
        new_move_vals = super(StockRule, self)._push_prepare_move_copy_values(
            move_to_copy, new_date)
        new_move_vals['production_id'] = False
        return new_move_vals
Example #12
0
class ResourceCalendar(models.Model):
    """ Calendar model for a resource. It has

     - attendance_ids: list of resource.calendar.attendance that are a working
                       interval in a given weekday.
     - leave_ids: list of leaves linked to this calendar. A leave can be general
                  or linked to a specific resource, depending on its resource_id.

    All methods in this class use intervals. An interval is a tuple holding
    (begin_datetime, end_datetime). A list of intervals is therefore a list of
    tuples, holding several intervals of work or leaves. """
    _name = "resource.calendar"
    _description = "Resource Working Time"

    @api.model
    def default_get(self, fields):
        res = super(ResourceCalendar, self).default_get(fields)
        if not res.get('name') and res.get('company_id'):
            res['name'] = _(
                'Working Hours of %s') % self.env['res.company'].browse(
                    res['company_id']).name
        return res

    def _get_default_attendance_ids(self):
        return [(0, 0, {
            'name': _('Monday Morning'),
            'dayofweek': '0',
            'hour_from': 8,
            'hour_to': 12,
            'day_period': 'morning'
        }),
                (0, 0, {
                    'name': _('Monday Evening'),
                    'dayofweek': '0',
                    'hour_from': 13,
                    'hour_to': 17,
                    'day_period': 'afternoon'
                }),
                (0, 0, {
                    'name': _('Tuesday Morning'),
                    'dayofweek': '1',
                    'hour_from': 8,
                    'hour_to': 12,
                    'day_period': 'morning'
                }),
                (0, 0, {
                    'name': _('Tuesday Evening'),
                    'dayofweek': '1',
                    'hour_from': 13,
                    'hour_to': 17,
                    'day_period': 'afternoon'
                }),
                (0, 0, {
                    'name': _('Wednesday Morning'),
                    'dayofweek': '2',
                    'hour_from': 8,
                    'hour_to': 12,
                    'day_period': 'morning'
                }),
                (0, 0, {
                    'name': _('Wednesday Evening'),
                    'dayofweek': '2',
                    'hour_from': 13,
                    'hour_to': 17,
                    'day_period': 'afternoon'
                }),
                (0, 0, {
                    'name': _('Thursday Morning'),
                    'dayofweek': '3',
                    'hour_from': 8,
                    'hour_to': 12,
                    'day_period': 'morning'
                }),
                (0, 0, {
                    'name': _('Thursday Evening'),
                    'dayofweek': '3',
                    'hour_from': 13,
                    'hour_to': 17,
                    'day_period': 'afternoon'
                }),
                (0, 0, {
                    'name': _('Friday Morning'),
                    'dayofweek': '4',
                    'hour_from': 8,
                    'hour_to': 12,
                    'day_period': 'morning'
                }),
                (0, 0, {
                    'name': _('Friday Evening'),
                    'dayofweek': '4',
                    'hour_from': 13,
                    'hour_to': 17,
                    'day_period': 'afternoon'
                })]

    name = fields.Char(required=True)
    company_id = fields.Many2one(
        'res.company',
        'Company',
        default=lambda self: self.env['res.company']._company_default_get())
    attendance_ids = fields.One2many('resource.calendar.attendance',
                                     'calendar_id',
                                     'Working Time',
                                     copy=True,
                                     default=_get_default_attendance_ids)
    leave_ids = fields.One2many('resource.calendar.leaves', 'calendar_id',
                                'Leaves')
    global_leave_ids = fields.One2many('resource.calendar.leaves',
                                       'calendar_id',
                                       'Global Leaves',
                                       domain=[('resource_id', '=', False)])
    hours_per_day = fields.Float(
        "Average hour per day",
        default=HOURS_PER_DAY,
        help=
        "Average hours per day a resource is supposed to work with this calendar."
    )
    tz = fields.Selection(
        _tz_get,
        string='Timezone',
        required=True,
        default=lambda self: self._context.get('tz'
                                               ) or self.env.user.tz or 'UTC',
        help=
        "This field is used in order to define in which timezone the resources will work."
    )

    @api.onchange('attendance_ids')
    def _onchange_hours_per_day(self):
        attendances = self.attendance_ids.filtered(
            lambda attendance: not attendance.date_from and not attendance.
            date_to)
        hour_count = 0.0
        for attendance in attendances:
            hour_count += attendance.hour_to - attendance.hour_from
        if attendances:
            self.hours_per_day = float_round(
                hour_count / float(len(set(attendances.mapped('dayofweek')))),
                precision_digits=2)

    # --------------------------------------------------
    # Computation API
    # --------------------------------------------------
    def _attendance_intervals(self, start_dt, end_dt, resource=None):
        """ Return the attendance intervals in the given datetime range.
            The returned intervals are expressed in the resource's timezone.
        """
        assert start_dt.tzinfo and end_dt.tzinfo
        combine = datetime.combine

        # express all dates and times in the resource's timezone
        tz = timezone((resource or self).tz)
        start_dt = start_dt.astimezone(tz)
        end_dt = end_dt.astimezone(tz)

        # for each attendance spec, generate the intervals in the date range
        result = []
        for attendance in self.attendance_ids:
            start = start_dt.date()
            if attendance.date_from:
                start = max(start, attendance.date_from)
            until = end_dt.date()
            if attendance.date_to:
                until = min(until, attendance.date_to)
            weekday = int(attendance.dayofweek)

            for day in rrule(DAILY, start, until=until, byweekday=weekday):
                # attendance hours are interpreted in the resource's timezone
                dt0 = tz.localize(
                    combine(day, float_to_time(attendance.hour_from)))
                dt1 = tz.localize(
                    combine(day, float_to_time(attendance.hour_to)))
                result.append((max(start_dt, dt0), min(end_dt,
                                                       dt1), attendance))

        return Intervals(result)

    def _leave_intervals(self, start_dt, end_dt, resource=None, domain=None):
        """ Return the leave intervals in the given datetime range.
            The returned intervals are expressed in the calendar's timezone.
        """
        assert start_dt.tzinfo and end_dt.tzinfo
        self.ensure_one()

        # for the computation, express all datetimes in UTC
        resource_ids = [resource.id, False] if resource else [False]
        if domain is None:
            domain = [('time_type', '=', 'leave')]
        domain = domain + [
            ('calendar_id', '=', self.id),
            ('resource_id', 'in', resource_ids),
            ('date_from', '<=', datetime_to_string(end_dt)),
            ('date_to', '>=', datetime_to_string(start_dt)),
        ]

        # retrieve leave intervals in (start_dt, end_dt)
        tz = timezone((resource or self).tz)
        start_dt = start_dt.astimezone(tz)
        end_dt = end_dt.astimezone(tz)
        result = []
        for leave in self.env['resource.calendar.leaves'].search(domain):
            dt0 = string_to_datetime(leave.date_from).astimezone(tz)
            dt1 = string_to_datetime(leave.date_to).astimezone(tz)
            result.append((max(start_dt, dt0), min(end_dt, dt1), leave))

        return Intervals(result)

    def _work_intervals(self, start_dt, end_dt, resource=None, domain=None):
        """ Return the effective work intervals between the given datetimes. """
        return (self._attendance_intervals(start_dt, end_dt, resource) -
                self._leave_intervals(start_dt, end_dt, resource, domain))

    # --------------------------------------------------
    # External API
    # --------------------------------------------------

    @api.multi
    def get_work_hours_count(self,
                             start_dt,
                             end_dt,
                             compute_leaves=True,
                             domain=None):
        """
            `compute_leaves` controls whether or not this method is taking into
            account the global leaves.

            `domain` controls the way leaves are recognized.
            None means default value ('time_type', '=', 'leave')

            Counts the number of work hours between two datetimes.
        """
        # Set timezone in UTC if no timezone is explicitly given
        if not start_dt.tzinfo:
            start_dt = start_dt.replace(tzinfo=utc)
        if not end_dt.tzinfo:
            end_dt = end_dt.replace(tzinfo=utc)

        if compute_leaves:
            intervals = self._work_intervals(start_dt, end_dt, domain=domain)
        else:
            intervals = self._attendance_intervals(start_dt, end_dt)

        return sum((stop - start).total_seconds() / 3600
                   for start, stop, meta in intervals)

    @api.multi
    def plan_hours(self,
                   hours,
                   day_dt,
                   compute_leaves=False,
                   domain=None,
                   resource=None):
        """
        `compute_leaves` controls whether or not this method is taking into
        account the global leaves.

        `domain` controls the way leaves are recognized.
        None means default value ('time_type', '=', 'leave')

        Return datetime after having planned hours
        """
        day_dt, revert = make_aware(day_dt)

        # which method to use for retrieving intervals
        if compute_leaves:
            get_intervals = partial(self._work_intervals,
                                    domain=domain,
                                    resource=resource)
        else:
            get_intervals = self._attendance_intervals

        if hours >= 0:
            delta = timedelta(days=14)
            for n in range(100):
                dt = day_dt + delta * n
                for start, stop, meta in get_intervals(dt, dt + delta):
                    interval_hours = (stop - start).total_seconds() / 3600
                    if hours <= interval_hours:
                        return revert(start + timedelta(hours=hours))
                    hours -= interval_hours
            return False
        else:
            hours = abs(hours)
            delta = timedelta(days=14)
            for n in range(100):
                dt = day_dt - delta * n
                for start, stop, meta in reversed(get_intervals(
                        dt - delta, dt)):
                    interval_hours = (stop - start).total_seconds() / 3600
                    if hours <= interval_hours:
                        return revert(stop - timedelta(hours=hours))
                    hours -= interval_hours
            return False

    @api.multi
    def plan_days(self, days, day_dt, compute_leaves=False, domain=None):
        """
        `compute_leaves` controls whether or not this method is taking into
        account the global leaves.

        `domain` controls the way leaves are recognized.
        None means default value ('time_type', '=', 'leave')

        Returns the datetime of a days scheduling.
        """
        day_dt, revert = make_aware(day_dt)

        # which method to use for retrieving intervals
        if compute_leaves:
            get_intervals = partial(self._work_intervals, domain=domain)
        else:
            get_intervals = self._attendance_intervals

        if days > 0:
            found = set()
            delta = timedelta(days=14)
            for n in range(100):
                dt = day_dt + delta * n
                for start, stop, meta in get_intervals(dt, dt + delta):
                    found.add(start.date())
                    if len(found) == days:
                        return revert(stop)
            return False

        elif days < 0:
            days = abs(days)
            found = set()
            delta = timedelta(days=14)
            for n in range(100):
                dt = day_dt - delta * n
                for start, stop, meta in reversed(get_intervals(
                        dt - delta, dt)):
                    found.add(start.date())
                    if len(found) == days:
                        return revert(start)
            return False

        else:
            return revert(day_dt)
class OpAssignmentSubLine(models.Model):
    _name = "op.assignment.sub.line"
    _inherit = "mail.thread"
    _rec_name = "assignment_id"
    _description = "Assignment Submission"
    _order = "submission_date DESC"

    def _compute_get_user_group(self):
        for user in self:
            if self.env.user.has_group(
                    'openeagleedu_core.group_op_back_office_admin') or \
                    self.env.user.has_group(
                        'openeagleedu_core.group_op_back_office') or \
                    self.env.user.has_group(
                        'openeagleedu_core.group_op_faculty'):
                user.user_boolean = True
            else:
                user.user_boolean = False

    assignment_id = fields.Many2one(
        'op.assignment', 'Assignment', required=True)
    student_id = fields.Many2one(
        'op.student', 'Student',
        default=lambda self: self.env['op.student'].search(
            [('user_id', '=', self.env.uid)]), required=True)
    description = fields.Text('Description', track_visibility='onchange')
    state = fields.Selection([
        ('draft', 'Draft'), ('submit', 'Submitted'), ('reject', 'Rejected'),
        ('change', 'Change Req.'), ('accept', 'Accepted')], basestring='State',
        default='draft', track_visibility='onchange')
    submission_date = fields.Datetime(
        'Submission Date', readonly=True,
        default=lambda self: fields.Datetime.now(), required=True)
    marks = fields.Float('Marks', track_visibility='onchange')
    note = fields.Text('Note')
    user_id = fields.Many2one(
        'res.users', related='student_id.user_id', string='User')
    faculty_user_id = fields.Many2one(
        'res.users', related='assignment_id.faculty_id.user_id',
        string='Faculty User')
    user_boolean = fields.Boolean(string='Check user',
                                  compute='_compute_get_user_group')

    def act_draft(self):
        result = self.state = 'draft'
        return result and result or False

    def act_submit(self):
        result = self.state = 'submit'
        return result and result or False

    def act_accept(self):
        result = self.state = 'accept'
        return result and result or False

    def act_change_req(self):
        result = self.state = 'change'
        return result and result or False

    def act_reject(self):
        result = self.state = 'reject'
        return result and result or False

    def unlink(self):
        for record in self:
            if not record.state == 'draft' and not self.env.user.has_group(
                    'openeagleedu_core.group_op_faculty'):
                raise ValidationError(
                    _("You can't delete none draft submissions!"))
        res = super(OpAssignmentSubLine, self).unlink()
        return res

    @api.model
    def create(self, vals):
        if self.env.user.child_ids:
            raise Warning(_('Invalid Action!\n Parent can not \
            create Assignment Submissions!'))
        return super(OpAssignmentSubLine, self).create(vals)

    def write(self, vals):
        if self.env.user.child_ids:
            raise Warning(_('Invalid Action!\n Parent can not edit \
            Assignment Submissions!'))
        return super(OpAssignmentSubLine, self).write(vals)
Example #14
0
class AcquirerAdyen(models.Model):
    _inherit = 'payment.acquirer'

    provider = fields.Selection(selection_add=[('adyen', 'Adyen')])
    adyen_merchant_account = fields.Char('Merchant Account',
                                         required_if_provider='adyen',
                                         groups='base.group_user')
    adyen_skin_code = fields.Char('Skin Code',
                                  required_if_provider='adyen',
                                  groups='base.group_user')
    adyen_skin_hmac_key = fields.Char('Skin HMAC Key',
                                      required_if_provider='adyen',
                                      groups='base.group_user')

    @api.model
    def _adyen_convert_amount(self, amount, currency):
        """
        Adyen requires the amount to be multiplied by 10^k,
        where k depends on the currency code.
        """
        k = CURRENCY_CODE_MAPS.get(currency.name, 2)
        paymentAmount = int(tools.float_round(amount, k) * (10**k))
        return paymentAmount

    @api.model
    def _get_adyen_urls(self, environment):
        """ Adyen URLs: yhpp: hosted payment page: pay.shtml for single, select.shtml for multiple """
        return {
            'adyen_form_url':
            'https://%s.adyen.com/hpp/pay.shtml' %
            ('live' if environment == 'prod' else environment),
        }

    @api.multi
    def _adyen_generate_merchant_sig_sha256(self, inout, values):
        """ Generate the shasign for incoming or outgoing communications., when using the SHA-256
        signature.

        :param string inout: 'in' (eagle contacting ogone) or 'out' (adyen
                             contacting eagle). In this last case only some
                             fields should be contained (see e-Commerce basic)
        :param dict values: transaction values
        :return string: shasign
        """
        def escapeVal(val):
            return val.replace('\\', '\\\\').replace(':', '\\:')

        def signParams(parms):
            signing_string = ':'.join(
                escapeVal(v) for v in chain(parms.keys(), parms.values()))
            hm = hmac.new(hmac_key, signing_string.encode('utf-8'),
                          hashlib.sha256)
            return base64.b64encode(hm.digest())

        assert inout in ('in', 'out')
        assert self.provider == 'adyen'

        if inout == 'in':
            # All the fields sent to Adyen must be included in the signature. ALL the f*****g
            # fields, despite what is claimed in the documentation. For example, in
            # https://docs.adyen.com/developers/hpp-manual, it is stated: "The resURL parameter does
            # not need to be included in the signature." It's a trap, it must be included as well!
            keys = [
                'merchantReference',
                'paymentAmount',
                'currencyCode',
                'shipBeforeDate',
                'skinCode',
                'merchantAccount',
                'sessionValidity',
                'merchantReturnData',
                'shopperEmail',
                'shopperReference',
                'allowedMethods',
                'blockedMethods',
                'offset',
                'shopperStatement',
                'recurringContract',
                'billingAddressType',
                'deliveryAddressType',
                'brandCode',
                'countryCode',
                'shopperLocale',
                'orderData',
                'offerEmail',
                'resURL',
            ]
        else:
            keys = [
                'authResult',
                'merchantReference',
                'merchantReturnData',
                'paymentMethod',
                'pspReference',
                'shopperLocale',
                'skinCode',
            ]

        hmac_key = binascii.a2b_hex(self.adyen_skin_hmac_key.encode('ascii'))
        raw_values = {k: values.get(k, '') for k in keys if k in values}
        raw_values_ordered = OrderedDict(
            sorted(raw_values.items(), key=lambda t: t[0]))

        return signParams(raw_values_ordered)

    @api.multi
    def _adyen_generate_merchant_sig(self, inout, values):
        """ Generate the shasign for incoming or outgoing communications, when using the SHA-1
        signature (deprecated by Adyen).

        :param string inout: 'in' (eagle contacting ogone) or 'out' (adyen
                             contacting eagle). In this last case only some
                             fields should be contained (see e-Commerce basic)
        :param dict values: transaction values

        :return string: shasign
        """
        assert inout in ('in', 'out')
        assert self.provider == 'adyen'

        if inout == 'in':
            keys = "paymentAmount currencyCode shipBeforeDate merchantReference skinCode merchantAccount sessionValidity shopperEmail shopperReference recurringContract allowedMethods blockedMethods shopperStatement merchantReturnData billingAddressType deliveryAddressType offset".split(
            )
        else:
            keys = "authResult pspReference merchantReference skinCode merchantReturnData".split(
            )

        def get_value(key):
            if values.get(key):
                return values[key]
            return ''

        sign = ''.join('%s' % get_value(k) for k in keys).encode('ascii')
        key = self.adyen_skin_hmac_key.encode('ascii')
        return base64.b64encode(hmac.new(key, sign, hashlib.sha1).digest())

    @api.multi
    def adyen_form_generate_values(self, values):
        base_url = self.env['ir.config_parameter'].sudo().get_param(
            'web.base.url')
        # tmp
        import datetime
        from dateutil import relativedelta

        paymentAmount = self._adyen_convert_amount(values['amount'],
                                                   values['currency'])
        if self.provider == 'adyen' and len(self.adyen_skin_hmac_key) == 64:
            tmp_date = datetime.datetime.today() + relativedelta.relativedelta(
                days=1)

            values.update({
                'merchantReference':
                values['reference'],
                'paymentAmount':
                '%d' % paymentAmount,
                'currencyCode':
                values['currency'] and values['currency'].name or '',
                'shipBeforeDate':
                tmp_date.strftime('%Y-%m-%d'),
                'skinCode':
                self.adyen_skin_code,
                'merchantAccount':
                self.adyen_merchant_account,
                'shopperLocale':
                values.get('partner_lang', ''),
                'sessionValidity':
                tmp_date.isoformat('T')[:19] + "Z",
                'resURL':
                urls.url_join(base_url, AdyenController._return_url),
                'merchantReturnData':
                json.dumps({'return_url': '%s' % values.pop('return_url')})
                if values.get('return_url', '') else False,
                'shopperEmail':
                values.get('partner_email')
                or values.get('billing_partner_email') or '',
            })
            values['merchantSig'] = self._adyen_generate_merchant_sig_sha256(
                'in', values)

        else:
            tmp_date = datetime.date.today() + relativedelta.relativedelta(
                days=1)

            values.update({
                'merchantReference':
                values['reference'],
                'paymentAmount':
                '%d' % paymentAmount,
                'currencyCode':
                values['currency'] and values['currency'].name or '',
                'shipBeforeDate':
                tmp_date,
                'skinCode':
                self.adyen_skin_code,
                'merchantAccount':
                self.adyen_merchant_account,
                'shopperLocale':
                values.get('partner_lang'),
                'sessionValidity':
                tmp_date,
                'resURL':
                urls.url_join(base_url, AdyenController._return_url),
                'merchantReturnData':
                json.dumps({'return_url': '%s' % values.pop('return_url')})
                if values.get('return_url') else False,
            })
            values['merchantSig'] = self._adyen_generate_merchant_sig(
                'in', values)

        return values

    @api.multi
    def adyen_get_form_action_url(self):
        return self._get_adyen_urls(self.environment)['adyen_form_url']
Example #15
0
class MailMessage(models.Model):
    """ Override MailMessage class in order to add a new type: SMS messages.
    Those messages comes with their own notification method, using SMS
    gateway. """
    _inherit = 'mail.message'

    message_type = fields.Selection(selection_add=[('sms', 'SMS')])
    has_sms_error = fields.Boolean(
        'Has SMS error', compute='_compute_has_sms_error', search='_search_has_sms_error',
        help='Has error')

    def _compute_has_sms_error(self):
        sms_error_from_notification = self.env['mail.notification'].sudo().search([
            ('notification_type', '=', 'sms'),
            ('mail_message_id', 'in', self.ids),
            ('notification_status', '=', 'exception')]).mapped('mail_message_id')
        for message in self:
            message.has_sms_error = message in sms_error_from_notification

    def _search_has_sms_error(self, operator, operand):
        if operator == '=' and operand:
            return ['&', ('notification_ids.notification_status', '=', 'exception'), ('notification_ids.notification_type', '=', 'sms')]
        raise NotImplementedError()

    def _format_mail_failures(self):
        """ A shorter message to notify a SMS delivery failure update

        TDE FIXME: should be cleaned
        """
        res = super(MailMessage, self)._format_mail_failures()

        # prepare notifications computation in batch
        all_notifications = self.env['mail.notification'].sudo().search([
            ('mail_message_id', 'in', self.ids)
        ])
        msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo())
        for notif in all_notifications:
            msgid_to_notif[notif.mail_message_id.id] += notif

        for message in self:
            notifications = msgid_to_notif[message.id]
            if not any(notification.notification_type == 'sms' for notification in notifications):
                continue
            info = dict(message._get_mail_failure_dict(),
                        failure_type='sms',
                        notifications=dict((notif.res_partner_id.id, (notif.notification_status, notif.res_partner_id.name)) for notif in notifications if notif.notification_type == 'sms'),
                        module_icon='/sms/static/img/sms_failure.svg'
                        )
            res.append(info)
        return res

    def _notify_sms_update(self):
        """ Send bus notifications to update status of notifications in chatter.
        Purpose is to send the updated status per author.

        TDE FIXME: author_id strategy seems curious, check with JS """
        messages = self.env['mail.message']
        for message in self:
            # YTI FIXME: check allowed_company_ids if necessary
            if message.model and message.res_id:
                record = self.env[message.model].browse(message.res_id)
                try:
                    record.check_access_rights('read')
                    record.check_access_rule('read')
                except exceptions.AccessError:
                    continue
                else:
                    messages |= message

        """ Notify channels after update of SMS status """
        updates = [[
            (self._cr.dbname, 'res.partner', author.id),
            {'type': 'sms_update', 'elements': self.env['mail.message'].concat(*author_messages)._format_mail_failures()}
        ] for author, author_messages in groupby(messages, itemgetter('author_id'))]
        self.env['bus.bus'].sendmany(updates)

    def message_format(self):
        """ Override in order to retrieves data about SMS (recipient name and
            SMS status)

        TDE FIXME: clean the overall message_format thingy
        """
        message_values = super(MailMessage, self).message_format()
        all_sms_notifications = self.env['mail.notification'].sudo().search([
            ('mail_message_id', 'in', [r['id'] for r in message_values]),
            ('notification_type', '=', 'sms')
        ])
        msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo())
        for notif in all_sms_notifications:
            msgid_to_notif[notif.mail_message_id.id] += notif

        for message in message_values:
            customer_sms_data = [(notif.id, notif.res_partner_id.display_name or notif.sms_number, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])]
            message['sms_ids'] = customer_sms_data
        return message_values
Example #16
0
def inverse_fn(records):
    pass

MODELS = [
    ('boolean', fields.Boolean()),
    ('integer', fields.Integer()),
    ('float', fields.Float()),
    ('decimal', fields.Float(digits=(16, 3))),
    ('string.bounded', fields.Char(size=16)),
    ('string.required', fields.Char(size=None, required=True)),
    ('string', fields.Char(size=None)),
    ('date', fields.Date()),
    ('datetime', fields.Datetime()),
    ('text', fields.Text()),
    ('selection', fields.Selection([(1, "Foo"), (2, "Bar"), (3, "Qux"), (4, '')])),
    ('selection.function', fields.Selection(selection_fn)),
    # just relate to an integer
    ('many2one', fields.Many2one('export.integer')),
    ('one2many', fields.One2many('export.one2many.child', 'parent_id')),
    ('many2many', fields.Many2many('export.many2many.other')),
    ('function', fields.Integer(compute=compute_fn, inverse=inverse_fn)),
    # related: specialization of fields.function, should work the same way
    # TODO: reference
]

for name, field in MODELS:
    class NewModel(models.Model):
        _name = 'export.%s' % name
        _description = 'Export: %s' % name
        _rec_name = 'value'
Example #17
0
class AccountClosing(models.Model):
    """
    This object holds an interval total and a grand total of the accounts of type receivable for a company,
    as well as the last account_move that has been counted in a previous object
    It takes its earliest brother to infer from when the computation needs to be done
    in order to compute its own data.
    """
    _name = 'account.sale.closing'
    _order = 'date_closing_stop desc, sequence_number desc'
    _description = "Sale Closing"

    name = fields.Char(help="Frequency and unique sequence number",
                       required=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True,
                                 required=True)
    date_closing_stop = fields.Datetime(
        string="Closing Date",
        help='Date to which the values are computed',
        readonly=True,
        required=True)
    date_closing_start = fields.Datetime(
        string="Starting Date",
        help='Date from which the total interval is computed',
        readonly=True,
        required=True)
    frequency = fields.Selection(string='Closing Type',
                                 selection=[('daily', 'Daily'),
                                            ('monthly', 'Monthly'),
                                            ('annually', 'Annual')],
                                 readonly=True,
                                 required=True)
    total_interval = fields.Monetary(
        string="Period Total",
        help=
        'Total in receivable accounts during the interval, excluding overlapping periods',
        readonly=True,
        required=True)
    cumulative_total = fields.Monetary(
        string="Cumulative Grand Total",
        help='Total in receivable accounts since the beginnig of times',
        readonly=True,
        required=True)
    sequence_number = fields.Integer('Sequence #',
                                     readonly=True,
                                     required=True)
    last_order_id = fields.Many2one(
        'pos.order',
        string='Last Pos Order',
        help='Last Pos order included in the grand total',
        readonly=True)
    last_order_hash = fields.Char(
        string='Last Order entry\'s inalteralbility hash', readonly=True)
    currency_id = fields.Many2one('res.currency',
                                  string='Currency',
                                  help="The company's currency",
                                  readonly=True,
                                  related='company_id.currency_id',
                                  store=True)

    def _query_for_aml(self, company, first_move_sequence_number, date_start):
        params = {'company_id': company.id}
        query = '''WITH aggregate AS (SELECT m.id AS move_id,
                    aml.balance AS balance,
                    aml.id as line_id
            FROM account_move_line aml
            JOIN account_journal j ON aml.journal_id = j.id
            JOIN account_account acc ON acc.id = aml.account_id
            JOIN account_account_type t ON (t.id = acc.user_type_id AND t.type = 'receivable')
            JOIN account_move m ON m.id = aml.move_id
            WHERE j.type = 'sale'
                AND aml.company_id = %(company_id)s
                AND m.state = 'posted' '''

        if first_move_sequence_number is not False and first_move_sequence_number is not None:
            params['first_move_sequence_number'] = first_move_sequence_number
            query += '''AND m.secure_sequence_number > %(first_move_sequence_number)s'''
        elif date_start:
            #the first time we compute the closing, we consider only from the installation of the module
            params['date_start'] = date_start
            query += '''AND m.date >= %(date_start)s'''

        query += " ORDER BY m.secure_sequence_number DESC) "
        query += '''SELECT array_agg(move_id) AS move_ids,
                           array_agg(line_id) AS line_ids,
                           sum(balance) AS balance
                    FROM aggregate'''

        self.env.cr.execute(query, params)
        return self.env.cr.dictfetchall()[0]

    def _compute_amounts(self, frequency, company):
        """
        Method used to compute all the business data of the new object.
        It will search for previous closings of the same frequency to infer the move from which
        account move lines should be fetched.
        @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
            frequencies are literal (daily means 24 hours and so on)
        @param {recordset} company: the company for which the closing is done
        @return {dict} containing {field: value} for each business field of the object
        """
        interval_dates = self._interval_dates(frequency, company)
        previous_closing = self.search([('frequency', '=', frequency),
                                        ('company_id', '=', company.id)],
                                       limit=1,
                                       order='sequence_number desc')

        first_order = self.env['pos.order']
        date_start = interval_dates['interval_from']
        cumulative_total = 0
        if previous_closing:
            first_order = previous_closing.last_order_id
            date_start = previous_closing.create_date
            cumulative_total += previous_closing.cumulative_total

        domain = [('company_id', '=', company.id),
                  ('state', 'in', ('paid', 'done', 'invoiced'))]
        if first_order.l10n_fr_secure_sequence_number is not False and first_order.l10n_fr_secure_sequence_number is not None:
            domain = AND([
                domain,
                [('l10n_fr_secure_sequence_number', '>',
                  first_order.l10n_fr_secure_sequence_number)]
            ])
        elif date_start:
            #the first time we compute the closing, we consider only from the installation of the module
            domain = AND([domain, [('date_order', '>=', date_start)]])

        orders = self.env['pos.order'].search(domain, order='date_order desc')

        total_interval = sum(orders.mapped('amount_total'))
        cumulative_total += total_interval

        # We keep the reference to avoid gaps (like daily object during the weekend)
        last_order = first_order
        if orders:
            last_order = orders[0]

        return {
            'total_interval':
            total_interval,
            'cumulative_total':
            cumulative_total,
            'last_order_id':
            last_order.id,
            'last_order_hash':
            last_order.l10n_fr_secure_sequence_number,
            'date_closing_stop':
            interval_dates['date_stop'],
            'date_closing_start':
            date_start,
            'name':
            interval_dates['name_interval'] + ' - ' +
            interval_dates['date_stop'][:10]
        }

    def _interval_dates(self, frequency, company):
        """
        Method used to compute the theoretical date from which account move lines should be fetched
        @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
            frequencies are literal (daily means 24 hours and so on)
        @param {recordset} company: the company for which the closing is done
        @return {dict} the theoretical date from which account move lines are fetched.
            date_stop date to which the move lines are fetched, always now()
            the dates are in their Eagle Database string representation
        """
        date_stop = datetime.utcnow()
        interval_from = None
        name_interval = ''
        if frequency == 'daily':
            interval_from = date_stop - timedelta(days=1)
            name_interval = _('Daily Closing')
        elif frequency == 'monthly':
            month_target = date_stop.month > 1 and date_stop.month - 1 or 12
            year_target = month_target < 12 and date_stop.year or date_stop.year - 1
            interval_from = date_stop.replace(year=year_target,
                                              month=month_target)
            name_interval = _('Monthly Closing')
        elif frequency == 'annually':
            year_target = date_stop.year - 1
            interval_from = date_stop.replace(year=year_target)
            name_interval = _('Annual Closing')

        return {
            'interval_from': FieldDateTime.to_string(interval_from),
            'date_stop': FieldDateTime.to_string(date_stop),
            'name_interval': name_interval
        }

    def write(self, vals):
        raise UserError(
            _('Sale Closings are not meant to be written or deleted under any circumstances.'
              ))

    def unlink(self):
        raise UserError(
            _('Sale Closings are not meant to be written or deleted under any circumstances.'
              ))

    @api.model
    def _automated_closing(self, frequency='daily'):
        """To be executed by the CRON to create an object of the given frequency for each company that needs it
        @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
            frequencies are literal (daily means 24 hours and so on)
        @return {recordset} all the objects created for the given frequency
        """
        res_company = self.env['res.company'].search([])
        account_closings = self.env['account.sale.closing']
        for company in res_company.filtered(
                lambda c: c._is_accounting_unalterable()):
            new_sequence_number = company.l10n_fr_closing_sequence_id.next_by_id(
            )
            values = self._compute_amounts(frequency, company)
            values['frequency'] = frequency
            values['company_id'] = company.id
            values['sequence_number'] = new_sequence_number
            account_closings |= account_closings.create(values)

        return account_closings
Example #18
0
class SelectionWithDefault(models.Model):
    _name = 'export.selection.withdefault'
    _description = 'Export Selection With Default'

    const = fields.Integer(default=4)
    value = fields.Selection([(1, "Foo"), (2, "Bar")], default=2)
Example #19
0
class Lead2OpportunityPartner(models.TransientModel):

    _name = 'crm.lead2opportunity.partner'
    _description = 'Convert Lead to Opportunity (not in mass)'
    _inherit = 'crm.partner.binding'

    @api.model
    def default_get(self, fields):
        """ Default get for name, opportunity_ids.
            If there is an exisitng partner link to the lead, find all existing
            opportunities links with this partner to merge all information together
        """
        result = super(Lead2OpportunityPartner, self).default_get(fields)
        if self._context.get('active_id'):
            tomerge = {int(self._context['active_id'])}

            partner_id = result.get('partner_id')
            lead = self.env['crm.lead'].browse(self._context['active_id'])
            email = lead.partner_id.email if lead.partner_id else lead.email_from

            tomerge.update(
                self._get_duplicated_leads(partner_id,
                                           email,
                                           include_lost=True).ids)

            if 'action' in fields and not result.get('action'):
                result['action'] = 'exist' if partner_id else 'create'
            if 'partner_id' in fields:
                result['partner_id'] = partner_id
            if 'name' in fields:
                result['name'] = 'merge' if len(tomerge) >= 2 else 'convert'
            if 'opportunity_ids' in fields and len(tomerge) >= 2:
                result['opportunity_ids'] = list(tomerge)
            if lead.user_id:
                result['user_id'] = lead.user_id.id
            if lead.team_id:
                result['team_id'] = lead.team_id.id
            if not partner_id and not lead.contact_name:
                result['action'] = 'nothing'
        return result

    name = fields.Selection([('convert', 'Convert to opportunity'),
                             ('merge', 'Merge with existing opportunities')],
                            'Conversion Action',
                            required=True)
    opportunity_ids = fields.Many2many('crm.lead', string='Opportunities')
    user_id = fields.Many2one('res.users', 'Salesperson', index=True)
    team_id = fields.Many2one('crm.team',
                              'Sales Team',
                              oldname='section_id',
                              index=True)

    @api.onchange('action')
    def onchange_action(self):
        if self.action == 'exist':
            self.partner_id = self._find_matching_partner()
        else:
            self.partner_id = False

    @api.onchange('user_id')
    def _onchange_user(self):
        """ When changing the user, also set a team_id or restrict team id
            to the ones user_id is member of.
        """
        if self.user_id:
            if self.team_id:
                user_in_team = self.env['crm.team'].search_count([
                    ('id', '=', self.team_id.id), '|',
                    ('user_id', '=', self.user_id.id),
                    ('member_ids', '=', self.user_id.id)
                ])
            else:
                user_in_team = False
            if not user_in_team:
                values = self.env['crm.lead']._onchange_user_values(
                    self.user_id.id if self.user_id else False)
                self.team_id = values.get('team_id', False)

    @api.model
    def _get_duplicated_leads(self, partner_id, email, include_lost=False):
        """ Search for opportunities that have the same partner and that arent done or cancelled """
        return self.env['crm.lead']._get_duplicated_leads_by_emails(
            partner_id, email, include_lost=include_lost)

    # NOTE JEM : is it the good place to test this ?
    @api.model
    def view_init(self, fields):
        """ Check some preconditions before the wizard executes. """
        for lead in self.env['crm.lead'].browse(
                self._context.get('active_ids', [])):
            if lead.probability == 100:
                raise UserError(
                    _("Closed/Dead leads cannot be converted into opportunities."
                      ))
        return False

    @api.multi
    def _convert_opportunity(self, vals):
        self.ensure_one()

        res = False

        leads = self.env['crm.lead'].browse(vals.get('lead_ids'))
        for lead in leads:
            self_def_user = self.with_context(default_user_id=self.user_id.id)
            partner_id = self_def_user._create_partner(
                lead.id, self.action,
                vals.get('partner_id') or lead.partner_id.id)
            res = lead.convert_opportunity(partner_id, [], False)
        user_ids = vals.get('user_ids')

        leads_to_allocate = leads
        if self._context.get('no_force_assignation'):
            leads_to_allocate = leads_to_allocate.filtered(
                lambda lead: not lead.user_id)

        if user_ids:
            leads_to_allocate.allocate_salesman(user_ids,
                                                team_id=(vals.get('team_id')))

        return res

    @api.multi
    def action_apply(self):
        """ Convert lead to opportunity or merge lead and opportunity and open
            the freshly created opportunity view.
        """
        self.ensure_one()
        values = {
            'team_id': self.team_id.id,
        }

        if self.partner_id:
            values['partner_id'] = self.partner_id.id

        if self.name == 'merge':
            leads = self.with_context(
                active_test=False).opportunity_ids.merge_opportunity()
            if not leads.active:
                leads.write({
                    'active': True,
                    'activity_type_id': False,
                    'lost_reason': False
                })
            if leads.type == "lead":
                values.update({
                    'lead_ids': leads.ids,
                    'user_ids': [self.user_id.id]
                })
                self.with_context(
                    active_ids=leads.ids)._convert_opportunity(values)
            elif not self._context.get(
                    'no_force_assignation') or not leads.user_id:
                values['user_id'] = self.user_id.id
                leads.write(values)
        else:
            leads = self.env['crm.lead'].browse(
                self._context.get('active_ids', []))
            values.update({
                'lead_ids': leads.ids,
                'user_ids': [self.user_id.id]
            })
            self._convert_opportunity(values)

        return leads[0].redirect_opportunity_view()

    def _create_partner(self, lead_id, action, partner_id):
        """ Create partner based on action.
            :return dict: dictionary organized as followed: {lead_id: partner_assigned_id}
        """
        #TODO this method in only called by Lead2OpportunityPartner
        #wizard and would probably diserve to be refactored or at least
        #moved to a better place
        if action == 'each_exist_or_create':
            partner_id = self.with_context(
                active_id=lead_id)._find_matching_partner()
            action = 'create'
        result = self.env['crm.lead'].browse(
            lead_id).handle_partner_assignation(action, partner_id)
        return result.get(lead_id)
class OpSubjectRegistration(models.Model):
    _name = "op.subject.registration"
    _description = "Subject Registration Details"
    _inherit = ["mail.thread"]

    name = fields.Char('Name', readonly=True, default='New')
    student_id = fields.Many2one('op.student', 'Student', required=True,
                                 track_visibility='onchange')
    course_id = fields.Many2one('op.course', 'Course', required=True,
                                track_visibility='onchange')
    batch_id = fields.Many2one('op.batch', 'Batch', required=True,
                               track_visibility='onchange')
    compulsory_subject_ids = fields.Many2many(
        'op.subject', 'subject_compulsory_rel',
        'register_id', 'subject_id', string="Compulsory Subjects",
        readonly=True)
    elective_subject_ids = fields.Many2many(
        'op.subject', string="Elective Subjects")
    state = fields.Selection([
        ('draft', 'Draft'), ('submitted', 'Submitted'),
        ('approved', 'Approved'), ('rejected', 'Rejected')],
        default='draft', string='state', copy=False,
        track_visibility='onchange')
    max_unit_load = fields.Float('Maximum Unit Load',
                                 track_visibility='onchange')
    min_unit_load = fields.Float('Minimum Unit Load',
                                 track_visibility='onchange')

    def action_reset_draft(self):
        self.state = 'draft'

    def action_reject(self):
        self.state = 'rejected'

    def action_approve(self):
        for record in self:
            subject_ids = []
            for sub in record.compulsory_subject_ids:
                subject_ids.append(sub.id)
            for sub in record.elective_subject_ids:
                subject_ids.append(sub.id)
            course_id = self.env['op.student.course'].search([
                ('student_id', '=', record.student_id.id),
                ('course_id', '=', record.course_id.id)
            ], limit=1)
            if course_id:
                course_id.write({
                    'subject_ids': [[6, 0, list(set(subject_ids))]]
                })
                record.state = 'approved'
            else:
                raise ValidationError(
                    _("Course not found on student's admission!"))

    def action_submitted(self):
        self.state = 'submitted'

    @api.model
    def create(self, vals):
        if vals.get('name', 'New') == 'New':
            vals['name'] = self.env['ir.sequence'].next_by_code(
                'op.subject.registration') or '/'
        return super(OpSubjectRegistration, self).create(vals)

    def get_subjects(self):
        for record in self:
            subject_ids = []
            if record.course_id and record.course_id.subject_ids:
                for subject in record.course_id.subject_ids:
                    if subject.subject_type == 'compulsory':
                        subject_ids.append(subject.id)
            record.compulsory_subject_ids = [(6, 0, subject_ids)]
Example #21
0
class EventType(models.Model):
    _name = 'event.type'
    _description = 'Event Category'
    _order = 'sequence, id'

    @api.model
    def _get_default_event_type_mail_ids(self):
        return [(0, 0, {
            'notification_type': 'mail',
            'interval_unit': 'now',
            'interval_type': 'after_sub',
            'template_id': self.env.ref('event.event_subscription').id,
        }),
                (0, 0, {
                    'notification_type': 'mail',
                    'interval_nbr': 1,
                    'interval_unit': 'days',
                    'interval_type': 'before_event',
                    'template_id': self.env.ref('event.event_reminder').id,
                }),
                (0, 0, {
                    'notification_type': 'mail',
                    'interval_nbr': 10,
                    'interval_unit': 'days',
                    'interval_type': 'before_event',
                    'template_id': self.env.ref('event.event_reminder').id,
                })]

    name = fields.Char('Event Category', required=True, translate=True)
    sequence = fields.Integer()
    # registration
    has_seats_limitation = fields.Boolean('Limited Seats', default=False)
    default_registration_min = fields.Integer(
        'Minimum Registrations',
        default=0,
        help=
        "It will select this default minimum value when you choose this event")
    default_registration_max = fields.Integer(
        'Maximum Registrations',
        default=0,
        help=
        "It will select this default maximum value when you choose this event")
    auto_confirm = fields.Boolean(
        'Automatically Confirm Registrations',
        default=True,
        help="Events and registrations will automatically be confirmed "
        "upon creation, easing the flow for simple events.")
    # location
    is_online = fields.Boolean(
        'Online Event',
        help=
        'Online events like webinars do not require a specific location and are hosted online.'
    )
    use_timezone = fields.Boolean('Use Default Timezone')
    default_timezone = fields.Selection('_tz_get',
                                        string='Timezone',
                                        default=lambda self: self.env.user.tz)
    # communication
    use_hashtag = fields.Boolean('Use Default Hashtag')
    default_hashtag = fields.Char('Twitter Hashtag')
    use_mail_schedule = fields.Boolean('Automatically Send Emails',
                                       default=True)
    event_type_mail_ids = fields.One2many(
        'event.type.mail',
        'event_type_id',
        string='Mail Schedule',
        copy=False,
        default=lambda self: self._get_default_event_type_mail_ids())

    @api.onchange('has_seats_limitation')
    def _onchange_has_seats_limitation(self):
        if not self.has_seats_limitation:
            self.default_registration_min = 0
            self.default_registration_max = 0

    @api.model
    def _tz_get(self):
        return [(x, x) for x in pytz.all_timezones]
class SaleCouponReward(models.Model):
    _name = 'sale.coupon.reward'
    _description = "Sales Coupon Reward"
    _rec_name = 'reward_description'

    # VFE FIXME multi company
    """Rewards are not restricted to a company...
    You could have a reward_product_id limited to a specific company A.
    But still use this reward as reward of a program of company B...
    """
    reward_description = fields.Char('Reward Description')
    reward_type = fields.Selection(
        [
            ('discount', 'Discount'),
            ('product', 'Free Product'),
        ],
        string='Reward Type',
        default='discount',
        help="Discount - Reward will be provided as discount.\n" +
        "Free Product - Free product will be provide as reward \n" +
        "Free Shipping - Free shipping will be provided as reward (Need delivery module)"
    )
    # Product Reward
    reward_product_id = fields.Many2one('product.product',
                                        string="Free Product",
                                        help="Reward Product")
    reward_product_quantity = fields.Integer(string="Quantity",
                                             default=1,
                                             help="Reward product quantity")
    # Discount Reward
    discount_type = fields.Selection(
        [('percentage', 'Percentage'), ('fixed_amount', 'Fixed Amount')],
        default="percentage",
        help="Percentage - Entered percentage discount will be provided\n" +
        "Amount - Entered fixed amount discount will be provided")
    discount_percentage = fields.Float(
        string="Discount",
        default=10,
        help='The discount in percentage, between 1 to 100')
    discount_apply_on = fields.Selection(
        [('on_order', 'On Order'), ('cheapest_product', 'On Cheapest Product'),
         ('specific_products', 'On Specific Products')],
        default="on_order",
        help="On Order - Discount on whole order\n" +
        "Cheapest product - Discount on cheapest product of the order\n" +
        "Specific products - Discount on selected specific products")
    discount_specific_product_ids = fields.Many2many(
        'product.product',
        string="Products",
        help=
        "Products that will be discounted if the discount is applied on specific products"
    )
    discount_max_amount = fields.Float(
        default=0, help="Maximum amount of discount that can be provided")
    discount_fixed_amount = fields.Float(string="Fixed Amount",
                                         help='The discount in fixed amount')
    reward_product_uom_id = fields.Many2one(
        related='reward_product_id.product_tmpl_id.uom_id',
        string='Unit of Measure',
        readonly=True)
    discount_line_product_id = fields.Many2one(
        'product.product',
        string='Reward Line Product',
        copy=False,
        help=
        "Product used in the sales order to apply the discount. Each coupon program has its own reward product for reporting purpose"
    )

    @api.constrains('discount_percentage')
    def _check_discount_percentage(self):
        if self.filtered(lambda reward: reward.discount_type == 'percentage'
                         and (reward.discount_percentage < 0 or reward.
                              discount_percentage > 100)):
            raise ValidationError(
                _('Discount percentage should be between 1-100'))

    def name_get(self):
        """
        Returns a complete description of the reward
        """
        result = []
        for reward in self:
            reward_string = ""
            if reward.reward_type == 'product':
                reward_string = _("Free Product - %s" %
                                  (reward.reward_product_id.name))
            elif reward.reward_type == 'discount':
                if reward.discount_type == 'percentage':
                    reward_percentage = str(reward.discount_percentage)
                    if reward.discount_apply_on == 'on_order':
                        reward_string = _("%s%% discount on total amount" %
                                          (reward_percentage))
                    elif reward.discount_apply_on == 'specific_products':
                        if len(reward.discount_specific_product_ids) > 1:
                            reward_string = _("%s%% discount on products" %
                                              (reward_percentage))
                        else:
                            reward_string = _(
                                "%s%% discount on %s" %
                                (reward_percentage,
                                 reward.discount_specific_product_ids.name))
                    elif reward.discount_apply_on == 'cheapest_product':
                        reward_string = _("%s%% discount on cheapest product" %
                                          (reward_percentage))
                elif reward.discount_type == 'fixed_amount':
                    program = self.env['sale.coupon.program'].search([
                        ('reward_id', '=', reward.id)
                    ])
                    reward_string = _("%s %s discount on total amount" %
                                      (str(reward.discount_fixed_amount),
                                       program.currency_id.name))
            result.append((reward.id, reward_string))
        return result
Example #23
0
class AccountAnalyticLine(models.Model):
    _inherit = 'account.analytic.line'

    def _default_sale_line_domain(self):
        domain = super(AccountAnalyticLine, self)._default_sale_line_domain()
        return expression.OR(
            [domain, [('qty_delivered_method', '=', 'timesheet')]])

    timesheet_invoice_type = fields.Selection(
        [('billable_time', 'Billed on Timesheets'),
         ('billable_fixed', 'Billed at a Fixed price'),
         ('non_billable', 'Non Billable Tasks'),
         ('non_billable_project', 'No task found')],
        string="Billable Type",
        compute='_compute_timesheet_invoice_type',
        compute_sudo=True,
        store=True,
        readonly=True)
    timesheet_invoice_id = fields.Many2one(
        'account.move',
        string="Invoice",
        readonly=True,
        copy=False,
        help="Invoice created from the timesheet")

    @api.depends('so_line.product_id', 'project_id', 'task_id')
    def _compute_timesheet_invoice_type(self):
        for timesheet in self:
            if timesheet.project_id:  # AAL will be set to False
                invoice_type = 'non_billable_project' if not timesheet.task_id else 'non_billable'
                if timesheet.task_id and timesheet.so_line.product_id.type == 'service':
                    if timesheet.so_line.product_id.invoice_policy == 'delivery':
                        if timesheet.so_line.product_id.service_type == 'timesheet':
                            invoice_type = 'billable_time'
                        else:
                            invoice_type = 'billable_fixed'
                    elif timesheet.so_line.product_id.invoice_policy == 'order':
                        invoice_type = 'billable_fixed'
                timesheet.timesheet_invoice_type = invoice_type
            else:
                timesheet.timesheet_invoice_type = False

    @api.onchange('employee_id')
    def _onchange_task_id_employee_id(self):
        if self.project_id:  # timesheet only
            if self.task_id.billable_type == 'task_rate':
                self.so_line = self.task_id.sale_line_id
            elif self.task_id.billable_type == 'employee_rate':
                self.so_line = self._timesheet_determine_sale_line(
                    self.task_id, self.employee_id)
            else:
                self.so_line = False

    @api.constrains('so_line', 'project_id')
    def _check_sale_line_in_project_map(self):
        for timesheet in self:
            if timesheet.project_id and timesheet.so_line:  # billed timesheet
                if timesheet.so_line not in timesheet.project_id.mapped(
                        'sale_line_employee_ids.sale_line_id'
                ) | timesheet.task_id.sale_line_id | timesheet.project_id.sale_line_id:
                    raise ValidationError(
                        _("This timesheet line cannot be billed: there is no Sale Order Item defined on the task, nor on the project. Please define one to save your timesheet line."
                          ))

    def write(self, values):
        # prevent to update invoiced timesheets if one line is of type delivery
        self._check_can_write(values)
        result = super(AccountAnalyticLine, self).write(values)
        return result

    def _check_can_write(self, values):
        if self.sudo().filtered(
                lambda aal: aal.so_line.product_id.invoice_policy == "delivery"
        ) and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id):
            if any([
                    field_name in values for field_name in [
                        'unit_amount', 'employee_id', 'project_id', 'task_id',
                        'so_line', 'amount', 'date'
                    ]
            ]):
                raise UserError(
                    _('You can not modify already invoiced timesheets (linked to a Sales order items invoiced on Time and material).'
                      ))

    @api.model
    def _timesheet_preprocess(self, values):
        values = super(AccountAnalyticLine, self)._timesheet_preprocess(values)
        # task implies so line (at create)
        if 'task_id' in values and not values.get('so_line') and values.get(
                'employee_id'):
            task = self.env['project.task'].sudo().browse(values['task_id'])
            employee = self.env['hr.employee'].sudo().browse(
                values['employee_id'])
            values['so_line'] = self._timesheet_determine_sale_line(
                task, employee).id
        return values

    def _timesheet_postprocess_values(self, values):
        result = super(AccountAnalyticLine,
                       self)._timesheet_postprocess_values(values)
        # (re)compute the sale line
        if any([
                field_name in values
                for field_name in ['task_id', 'employee_id']
        ]):
            for timesheet in self:
                result[timesheet.id].update({
                    'so_line':
                    timesheet._timesheet_determine_sale_line(
                        timesheet.task_id, timesheet.employee_id).id,
                })
        return result

    @api.model
    def _timesheet_determine_sale_line(self, task, employee):
        """ Deduce the SO line associated to the timesheet line:
            1/ timesheet on task rate: the so line will be the one from the task
            2/ timesheet on employee rate task: find the SO line in the map of the project (even for subtask), or fallback on the SO line of the task, or fallback
                on the one on the project
            NOTE: this have to be consistent with `_compute_billable_type` on project.task.
        """
        if task.billable_type != 'no':
            if task.billable_type == 'employee_rate':
                map_entry = self.env['project.sale.line.employee.map'].search([
                    ('project_id', '=', task.project_id.id),
                    ('employee_id', '=', employee.id)
                ])
                if map_entry:
                    return map_entry.sale_line_id
                if task.sale_line_id:
                    return task.sale_line_id
                return task.project_id.sale_line_id
            elif task.billable_type == 'task_rate':
                return task.sale_line_id
        return self.env['sale.order.line']

    def _timesheet_get_portal_domain(self):
        """ Only the timesheets with a product invoiced on delivered quantity are concerned.
            since in ordered quantity, the timesheet quantity is not invoiced,
            thus there is no meaning of showing invoice with ordered quantity.
        """
        domain = super(AccountAnalyticLine,
                       self)._timesheet_get_portal_domain()
        return expression.AND([
            domain,
            [('timesheet_invoice_type', 'in',
              ['billable_time', 'non_billable'])]
        ])
Example #24
0
class MailMailStats(models.Model):
    """ MailMailStats models the statistics collected about emails. Those statistics
    are stored in a separated model and table to avoid bloating the mail_mail table
    with statistics values. This also allows to delete emails send with mass mailing
    without loosing the statistics about them. """

    _name = 'mail.mail.statistics'
    _description = 'Email Statistics'
    _rec_name = 'message_id'
    _order = 'message_id'

    mail_mail_id = fields.Many2one('mail.mail', string='Mail', index=True)
    mail_mail_id_int = fields.Integer(
        string='Mail ID (tech)',
        help=
        'ID of the related mail_mail. This field is an integer field because '
        'the related mail_mail can be deleted separately from its statistics. '
        'However the ID is needed for several action and controllers.',
        index=True,
    )
    message_id = fields.Char(string='Message-ID')
    model = fields.Char(string='Document model')
    res_id = fields.Integer(string='Document ID')
    # campaign / wave data
    mass_mailing_id = fields.Many2one('mail.mass_mailing',
                                      string='Mass Mailing',
                                      index=True)
    mass_mailing_campaign_id = fields.Many2one(
        related='mass_mailing_id.mass_mailing_campaign_id',
        string='Mass Mailing Campaign',
        store=True,
        readonly=True,
        index=True)
    # Bounce and tracking
    ignored = fields.Datetime(
        help='Date when the email has been invalidated. '
        'Invalid emails are blacklisted, opted-out or invalid email format')
    scheduled = fields.Datetime(help='Date when the email has been created',
                                default=fields.Datetime.now)
    sent = fields.Datetime(help='Date when the email has been sent')
    exception = fields.Datetime(
        help='Date of technical error leading to the email not being sent')
    opened = fields.Datetime(
        help='Date when the email has been opened the first time')
    replied = fields.Datetime(
        help='Date when this email has been replied for the first time.')
    bounced = fields.Datetime(help='Date when this email has bounced.')
    # Link tracking
    links_click_ids = fields.One2many('link.tracker.click',
                                      'mail_stat_id',
                                      string='Links click')
    clicked = fields.Datetime(
        help='Date when customer clicked on at least one tracked link')
    # Status
    state = fields.Selection(compute="_compute_state",
                             selection=[('outgoing', 'Outgoing'),
                                        ('exception', 'Exception'),
                                        ('sent', 'Sent'), ('opened', 'Opened'),
                                        ('replied', 'Replied'),
                                        ('bounced', 'Bounced'),
                                        ('ignored', 'Ignored')],
                             store=True)
    state_update = fields.Datetime(compute="_compute_state",
                                   string='State Update',
                                   help='Last state update of the mail',
                                   store=True)
    email = fields.Char(string="Recipient email address")

    @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced',
                 'exception', 'ignored')
    def _compute_state(self):
        self.update({'state_update': fields.Datetime.now()})
        for stat in self:
            if stat.ignored:
                stat.state = 'ignored'
            elif stat.exception:
                stat.state = 'exception'
            elif stat.opened or stat.clicked:
                stat.state = 'opened'
            elif stat.replied:
                stat.state = 'replied'
            elif stat.bounced:
                stat.state = 'bounced'
            elif stat.sent:
                stat.state = 'sent'
            else:
                stat.state = 'outgoing'

    @api.model
    def create(self, values):
        if 'mail_mail_id' in values:
            values['mail_mail_id_int'] = values['mail_mail_id']
        res = super(MailMailStats, self).create(values)
        return res

    def _get_records(self,
                     mail_mail_ids=None,
                     mail_message_ids=None,
                     domain=None):
        if not self.ids and mail_mail_ids:
            base_domain = [('mail_mail_id_int', 'in', mail_mail_ids)]
        elif not self.ids and mail_message_ids:
            base_domain = [('message_id', 'in', mail_message_ids)]
        else:
            base_domain = [('id', 'in', self.ids)]
        if domain:
            base_domain = ['&'] + domain + base_domain
        return self.search(base_domain)

    def set_opened(self, mail_mail_ids=None, mail_message_ids=None):
        statistics = self._get_records(mail_mail_ids, mail_message_ids,
                                       [('opened', '=', False)])
        statistics.write({'opened': fields.Datetime.now(), 'bounced': False})
        return statistics

    def set_clicked(self, mail_mail_ids=None, mail_message_ids=None):
        statistics = self._get_records(mail_mail_ids, mail_message_ids,
                                       [('clicked', '=', False)])
        statistics.write({'clicked': fields.Datetime.now()})
        return statistics

    def set_replied(self, mail_mail_ids=None, mail_message_ids=None):
        statistics = self._get_records(mail_mail_ids, mail_message_ids,
                                       [('replied', '=', False)])
        statistics.write({'replied': fields.Datetime.now()})
        return statistics

    def set_bounced(self, mail_mail_ids=None, mail_message_ids=None):
        statistics = self._get_records(mail_mail_ids, mail_message_ids,
                                       [('bounced', '=', False),
                                        ('opened', '=', False)])
        statistics.write({'bounced': fields.Datetime.now()})
        return statistics
Example #25
0
class MrpUnbuild(models.Model):
    _name = "mrp.unbuild"
    _description = "Unbuild Order"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'id desc'

    name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New'))
    product_id = fields.Many2one(
        'product.product', 'Product', check_company=True,
        domain="[('bom_ids', '!=', False), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        required=True, states={'done': [('readonly', True)]})
    company_id = fields.Many2one(
        'res.company', 'Company',
        default=lambda s: s.env.company,
        required=True, index=True, states={'done': [('readonly', True)]})
    product_qty = fields.Float(
        'Quantity', default=1.0,
        required=True, states={'done': [('readonly', True)]})
    product_uom_id = fields.Many2one(
        'uom.uom', 'Unit of Measure',
        required=True, states={'done': [('readonly', True)]})
    bom_id = fields.Many2one(
        'mrp.bom', 'Bill of Material',
        domain="""[
        '|',
            ('product_id', '=', product_id),
            '&',
                ('product_tmpl_id.product_variant_ids', '=', product_id),
                ('product_id','=',False),
        ('type', '=', 'normal'),
        '|',
            ('company_id', '=', company_id),
            ('company_id', '=', False)
        ]
""",
        required=True, states={'done': [('readonly', True)]}, check_company=True)
    mo_id = fields.Many2one(
        'mrp.production', 'Manufacturing Order',
        domain="[('state', 'in', ['done', 'cancel']), ('company_id', '=', company_id)]",
        states={'done': [('readonly', True)]}, check_company=True)
    lot_id = fields.Many2one(
        'stock.production.lot', 'Lot/Serial Number',
        domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True,
        states={'done': [('readonly', True)]}, help="Lot/Serial Number of the product to unbuild.")
    has_tracking=fields.Selection(related='product_id.tracking', readonly=True)
    location_id = fields.Many2one(
        'stock.location', 'Source Location',
        domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        check_company=True,
        required=True, states={'done': [('readonly', True)]}, help="Location where the product you want to unbuild is.")
    location_dest_id = fields.Many2one(
        'stock.location', 'Destination Location',
        domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
        check_company=True,
        required=True, states={'done': [('readonly', True)]}, help="Location where you want to send the components resulting from the unbuild order.")
    consume_line_ids = fields.One2many(
        'stock.move', 'consume_unbuild_id', readonly=True,
        string='Consumed Disassembly Lines')
    produce_line_ids = fields.One2many(
        'stock.move', 'unbuild_id', readonly=True,
        string='Processed Disassembly Lines')
    state = fields.Selection([
        ('draft', 'Draft'),
        ('done', 'Done')], string='Status', default='draft', index=True)

    @api.onchange('company_id')
    def _onchange_company_id(self):
        if self.company_id:
            warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.company_id.id)], limit=1)
            self.location_id = warehouse.lot_stock_id
            self.location_dest_id = warehouse.lot_stock_id
        else:
            self.location_id = False
            self.location_dest_id = False

    @api.onchange('mo_id')
    def _onchange_mo_id(self):
        if self.mo_id:
            self.product_id = self.mo_id.product_id.id
            self.product_qty = self.mo_id.product_qty

    @api.onchange('product_id')
    def _onchange_product_id(self):
        if self.product_id:
            self.bom_id = self.env['mrp.bom']._bom_find(product=self.product_id, company_id=self.company_id.id)
            self.product_uom_id = self.product_id.uom_id.id
            if self.company_id:
                return {'domain': {'mo_id': [('state', '=', 'done'), ('product_id', '=', self.product_id.id), ('company_id', '=', self.company_id.id)]}}

    @api.constrains('product_qty')
    def _check_qty(self):
        if self.product_qty <= 0:
            raise ValueError(_('Unbuild Order product quantity has to be strictly positive.'))

    @api.model
    def create(self, vals):
        if not vals.get('name') or vals['name'] == _('New'):
            vals['name'] = self.env['ir.sequence'].next_by_code('mrp.unbuild') or _('New')
        return super(MrpUnbuild, self).create(vals)

    def unlink(self):
        if 'done' in self.mapped('state'):
            raise UserError(_("You cannot delete an unbuild order if the state is 'Done'."))
        return super(MrpUnbuild, self).unlink()

    def action_unbuild(self):
        self.ensure_one()
        self._check_company()
        if self.product_id.tracking != 'none' and not self.lot_id.id:
            raise UserError(_('You should provide a lot number for the final product.'))

        if self.mo_id:
            if self.mo_id.state != 'done':
                raise UserError(_('You cannot unbuild a undone manufacturing order.'))

        consume_moves = self._generate_consume_moves()
        consume_moves._action_confirm()
        produce_moves = self._generate_produce_moves()

        finished_move = consume_moves.filtered(lambda m: m.product_id == self.product_id)
        consume_moves -= finished_move

        if any(produce_move.has_tracking != 'none' and not self.mo_id for produce_move in produce_moves):
            raise UserError(_('Some of your components are tracked, you have to specify a manufacturing order in order to retrieve the correct components.'))

        if any(consume_move.has_tracking != 'none' and not self.mo_id for consume_move in consume_moves):
            raise UserError(_('Some of your byproducts are tracked, you have to specify a manufacturing order in order to retrieve the correct byproducts.'))

        if finished_move.has_tracking != 'none':
            self.env['stock.move.line'].create({
                'move_id': finished_move.id,
                'lot_id': self.lot_id.id,
                'qty_done': finished_move.product_uom_qty,
                'product_id': finished_move.product_id.id,
                'product_uom_id': finished_move.product_uom.id,
                'location_id': finished_move.location_id.id,
                'location_dest_id': finished_move.location_dest_id.id,
            })
        else:
            finished_move.quantity_done = finished_move.product_uom_qty

        # TODO: Will fail if user do more than one unbuild with lot on the same MO. Need to check what other unbuild has aready took
        for move in produce_moves | consume_moves:
            if move.has_tracking != 'none':
                original_move = move in produce_moves and self.mo_id.move_raw_ids or self.mo_id.move_finished_ids
                original_move = original_move.filtered(lambda m: m.product_id == move.product_id)
                needed_quantity = move.product_qty
                moves_lines = original_move.mapped('move_line_ids')
                if move in produce_moves and self.lot_id:
                    moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.lot_produced_ids)
                for move_line in moves_lines:
                    # Iterate over all move_lines until we unbuilded the correct quantity.
                    taken_quantity = min(needed_quantity, move_line.qty_done)
                    if taken_quantity:
                        self.env['stock.move.line'].create({
                            'move_id': move.id,
                            'lot_id': move_line.lot_id.id,
                            'qty_done': taken_quantity,
                            'product_id': move.product_id.id,
                            'product_uom_id': move_line.product_uom_id.id,
                            'location_id': move.location_id.id,
                            'location_dest_id': move.location_dest_id.id,
                        })
                        needed_quantity -= taken_quantity
            else:
                move.quantity_done = move.product_uom_qty

        finished_move._action_done()
        consume_moves._action_done()
        produce_moves._action_done()
        produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.qty_done > 0)
        consume_moves.mapped('move_line_ids').write({'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]})

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

    def _generate_consume_moves(self):
        moves = self.env['stock.move']
        for unbuild in self:
            if unbuild.mo_id:
                finished_moves = unbuild.mo_id.move_finished_ids.filtered(lambda move: move.state == 'done')
                factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id)
                for finished_move in finished_moves:
                    moves += unbuild._generate_move_from_existing_move(finished_move, factor, finished_move.location_dest_id, finished_move.location_id)
            else:
                factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
                moves += unbuild._generate_move_from_bom_line(self.product_id, self.product_uom_id, unbuild.product_qty)
                for byproduct in unbuild.bom_id.byproduct_ids:
                    quantity = byproduct.product_qty * factor
                    moves += unbuild._generate_move_from_bom_line(byproduct.product_id, byproduct.product_uom_id, quantity, byproduct_id=byproduct.id)
        return moves

    def _generate_produce_moves(self):
        moves = self.env['stock.move']
        for unbuild in self:
            if unbuild.mo_id:
                raw_moves = unbuild.mo_id.move_raw_ids.filtered(lambda move: move.state == 'done')
                factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id)
                for raw_move in raw_moves:
                    moves += unbuild._generate_move_from_existing_move(raw_move, factor, raw_move.location_dest_id, self.location_dest_id)
            else:
                factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty
                boms, lines = unbuild.bom_id.explode(unbuild.product_id, factor, picking_type=unbuild.bom_id.picking_type_id)
                for line, line_data in lines:
                    moves += unbuild._generate_move_from_bom_line(line.product_id, line.product_uom_id, line_data['qty'], bom_line_id=line.id)
        return moves

    def _generate_move_from_existing_move(self, move, factor, location_id, location_dest_id):
        return self.env['stock.move'].create({
            'name': self.name,
            'date': self.create_date,
            'product_id': move.product_id.id,
            'product_uom_qty': move.product_uom_qty * factor,
            'product_uom': move.product_uom.id,
            'procure_method': 'make_to_stock',
            'location_dest_id': location_dest_id.id,
            'location_id': location_id.id,
            'warehouse_id': location_dest_id.get_warehouse().id,
            'unbuild_id': self.id,
            'company_id': move.company_id.id,
        })

    def _generate_move_from_bom_line(self, product, product_uom, quantity, bom_line_id=False, byproduct_id=False):
        location_id = bom_line_id and product.property_stock_production or self.location_id
        location_dest_id = bom_line_id and self.location_dest_id or product.with_context(force_company=self.company_id.id).property_stock_production
        warehouse = location_dest_id.get_warehouse()
        return self.env['stock.move'].create({
            'name': self.name,
            'date': self.create_date,
            'bom_line_id': bom_line_id,
            'byproduct_id': byproduct_id,
            'product_id': product.id,
            'product_uom_qty': quantity,
            'product_uom': product_uom.id,
            'procure_method': 'make_to_stock',
            'location_dest_id': location_dest_id.id,
            'location_id': location_id.id,
            'warehouse_id': warehouse.id,
            'unbuild_id': self.id,
            'company_id': self.company_id.id,
        })

    def action_validate(self):
        self.ensure_one()
        precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
        available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True)
        if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0:
            return self.action_unbuild()
        else:
            return {
                'name': _('Insufficient Quantity'),
                'view_mode': 'form',
                'res_model': 'stock.warn.insufficient.qty.unbuild',
                'view_id': self.env.ref('mrp.stock_warn_insufficient_qty_unbuild_form_view').id,
                'type': 'ir.actions.act_window',
                'context': {
                    'default_product_id': self.product_id.id,
                    'default_location_id': self.location_id.id,
                    'default_unbuild_id': self.id
                },
                'target': 'new'
            }
Example #26
0
class EagleeduExamValuation(models.Model):
    _name = 'eagleedu.exam.valuation'

    name = fields.Char(string='Name', default='New')
    exam_id = fields.Many2one('eagleedu.exam',
                              string='Exam',
                              required=True,
                              domain=[('state', '=', 'ongoing')])
    class_id = fields.Many2one('eagleedu.class', string='Class', required=True)
    division_id = fields.Many2one('eagleedu.class.division',
                                  string='Division',
                                  required=True)
    subject_id = fields.Many2one('eagleedu.syllabus',
                                 string='Subject',
                                 required=True)
    teachers_id = fields.Many2one('eagleedu.faculty', string='Evaluator')
    mark = fields.Integer(string='Max Mark', related='subject_id.total_mark')
    pass_mark = fields.Integer(string='Pass Mark',
                               related='subject_id.pass_mark')
    tut_mark = fields.Integer('Tutorial Mark', related='subject_id.tut_mark')
    tut_pass_mark = fields.Integer('Tutorial Pass Mark',
                                   related='subject_id.tut_pass')
    subj_mark = fields.Integer('Subjective Mark',
                               related='subject_id.subj_mark')
    subj_pass_mark = fields.Integer('Subjective Pass Mark',
                                    related='subject_id.subj_pass')

    obj_mark = fields.Integer('Objective Mark', related='subject_id.obj_mark')
    obj_pass_mark = fields.Integer('Objective Pass Mark',
                                   related='subject_id.obj_pass')

    prac_mark = fields.Integer('Practical Mark',
                               related='subject_id.prac_mark')
    prac_pass_mark = fields.Integer('Practical Pass Mark',
                                    related='subject_id.prac_pass')

    state = fields.Selection([('draft', 'Draft'), ('completed', 'Completed'),
                              ('cancel', 'Canceled')],
                             default='draft')
    valuation_line = fields.One2many('exam.valuation.line',
                                     'valuation_id',
                                     string='Students')
    mark_sheet_created = fields.Boolean(string='Mark sheet Created')
    date = fields.Date(string='Date', default=fields.Date.today)
    academic_year = fields.Many2one('eagleedu.academic.year',
                                    string='Academic Year',
                                    related='division_id.academic_year_id',
                                    store=True)
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        default=lambda self: self.env['res.company']._company_default_get())
    highest = fields.Integer('Highest mark Obtained')
    # @api.multi
    # def get_highest(self):
    #     for rec in self:
    #         evlauation_line=self.env['exam.valuation.line'].search([('valuation_id','=',rec.id)],order='mark_scored asc',limit=1)
    #
    #         rec.highest=evlauation_line.mark_scored
    @api.onchange('exam_id', 'division_id')
    def domain4subject(self):
        domain = []
        for rec in self:
            if rec.division_id.id:
                result_created = self.env['eagleedu.exam.valuation'].search([
                    ('exam_id.id', '=', rec.exam_id.id),
                    ('division_id.id', '=', rec.division_id.id)
                ])
                for res in result_created:
                    domain.append(res.subject_id.id)
        return {'domain': {'subject_id': [('id', '!=', domain)]}}

    @api.onchange('tut_mark', 'tut_pass_mark', 'subj_mark', 'subj_pass_mark',
                  'obj_mark', 'obj_pass_mark', 'prac_mark', 'prac_pass_mark')
    def calculate_marks(self):
        for rec in self:
            rec.mark = rec.tut_mark + rec.subj_mark + rec.obj_mark + rec.prac_mark
            rec.pass_mark = rec.tut_pass_mark + rec.subj_pass_mark + rec.obj_pass_mark + rec.prac_pass_mark

    @api.onchange('class_id')
    def onchange_class_id(self):
        domain = []
        if self.division_id.class_id != self.class_id:
            self.division_id = ''
        if self.class_id:
            domain = [('class_id', '=', self.class_id.id)]
        return {'domain': {'division_id': domain}}

    # @api.onchange('pass_mark')
    # def onchange_pass_mark(self):
    #     if self.pass_mark > self.mark:
    #         raise UserError(_('Pass mark must be less than Max Mark'))
    #     for records in self.valuation_line:
    #         if records.mark_scored >= self.pass_mark:
    #             records.pass_or_fail = True
    #         else:
    #             records.pass_or_fail = False

    @api.onchange('exam_id', 'subject_id')
    def onchange_exam_id(self):
        if self.exam_id:
            if self.exam_id.division_id:
                self.class_id = self.exam_id.class_id
                self.division_id = self.exam_id.division_id
            elif self.exam_id.class_id:
                self.class_id = self.exam_id.class_id
            else:
                self.class_id = ''
                self.division_id = ''
            self.mark = ''
            if self.subject_id:
                for sub in self.exam_id.subject_line:
                    if sub.subject_id.id == self.subject_id.id:
                        if sub.mark:
                            self.mark = sub.mark
        domain = []
        subjects = self.exam_id.subject_line
        for items in subjects:
            domain.append(items.subject_id.id)
        return {'domain': {'subject_id': [('id', 'in', domain)]}}

    # @api.model
    def create_mark_sheet(self):
        valuation_line_obj = self.env['exam.valuation.line']
        history = self.env['eagleedu.class.history'].search([
            ('class_id', '=', self.division_id.id), '|',
            ('compulsory_subjects', '=', self.subject_id.id), '|',
            ('selective_subjects', '=', self.subject_id.id),
            ('optional_subjects', '=', self.subject_id.id)
        ])  #division_id.student_ids
        if len(history) < 1:
            raise UserError(_('There are no students in this Division'))
        for student in history:
            data = {
                'student_id': student.student_id.id,
                'student_name': student.student_id.name,
                'valuation_id': self.id,
                'tut_mark': 0,
                'subj_mark': 0,
                'obj_mark': 0,
                'letter_grade': 'F',
                'prac_mark': 0,
                'grade_point': 0,
            }
            valuation_line_obj.create(data)
        self.mark_sheet_created = True

    @api.model
    def create(self, vals):
        res = super(EagleeduExamValuation, self).create(vals)
        valuation_obj = self.env['eagleedu.exam.valuation']
        search_valuation = valuation_obj.search([
            ('exam_id', '=', res.exam_id.id),
            ('division_id', '=', res.division_id.id),
            ('subject_id', '=', res.subject_id.id), ('state', '!=', 'cancel')
        ])
        if len(search_valuation) > 1:
            raise UserError(
                _('Valuation Sheet for \n Subject --> %s \nDivision --> %s \nExam --> %s \n is already created'
                  ) %
                (res.subject_id.name, res.division_id.name, res.exam_id.name))
        return res

    # @api.multi
    def valuation_completed(self):
        self.name = str(self.exam_id.exam_type.name) + '-' + str(
            self.exam_id.start_date)[0:10] + ' (' + str(
                self.division_id.name) + ')'
        result_obj = self.env['eagleedu.exam.results']
        result_line_obj = self.env['results.subject.line']
        for students in self.valuation_line:
            search_result = result_obj.search([
                ('exam_id', '=', self.exam_id.id),
                ('division_id', '=', self.division_id.id),
                ('student_id', '=', students.student_id.id)
            ])
            if len(search_result) < 1:
                result_data = {
                    'name': self.name,
                    'exam_id': self.exam_id.id,
                    'class_id': self.class_id.id,
                    'division_id': self.division_id.id,
                    'student_id': students.student_id.id,
                    'student_name': students.student_name,
                }
                result = result_obj.create(result_data)
                result_line_data = {
                    'name': self.name,
                    'tut_obt': students.tut_mark,
                    'tut_pr': students.tut_pr,
                    'obj_obt': students.obj_mark,
                    'obj_pr': students.obj_pr,
                    'subj_obt': students.subj_mark,
                    'subj_pr': students.subj_pr,
                    'prac_obt': students.prac_mark,
                    'prac_pr': students.prac_pr,
                    'subject_id': self.subject_id.id,
                    'max_mark': self.mark,
                    'pass_mark': self.pass_mark,
                    'mark_scored': students.mark_scored,
                    'pass_or_fail': students.pass_or_fail,
                    'result_id': result.id,
                    'exam_id': self.exam_id.id,
                    'class_id': self.class_id.id,
                    'division_id': self.division_id.id,
                    'student_id': students.student_id.id,
                    'student_name': students.student_name,
                    'letter_grade': students.letter_grade,
                    'grade_point': students.grade_point,
                }
                result_line_obj.create(result_line_data)
            else:
                result_line_data = {
                    'subject_id': self.subject_id.id,
                    'max_mark': self.mark,
                    'pass_mark': self.pass_mark,
                    'tut_obt': students.tut_mark,
                    'tut_pr': students.tut_pr,
                    'obj_obt': students.obj_mark,
                    'obj_pr': students.obj_pr,
                    'subj_obt': students.subj_mark,
                    'subj_pr': students.subj_pr,
                    'prac_obt': students.prac_mark,
                    'prac_pr': students.prac_pr,
                    'mark_scored': students.mark_scored,
                    'pass_or_fail': students.pass_or_fail,
                    'result_id': search_result.id,
                    'exam_id': self.exam_id.id,
                    'class_id': self.class_id.id,
                    'division_id': self.division_id.id,
                    'student_id': students.student_id.id,
                    'student_name': students.student_name,
                    'letter_grade': students.letter_grade,
                    'grade_point': students.grade_point,
                }
                result_line_obj.create(result_line_data)
        self.state = 'completed'

    # @api.multi
    def set_to_draft(self):
        result_line_obj = self.env['results.subject.line']
        result_obj = self.env['eagleedu.exam.results']
        for students in self.valuation_line:
            search_result = result_obj.search([
                ('exam_id', '=', self.exam_id.id),
                ('division_id', '=', self.division_id.id),
                ('student_id', '=', students.student_id.id)
            ])
            for rec in search_result:
                rec.state = 'draft'
            search_result_line = result_line_obj.search([
                ('result_id', '=', search_result.id),
                ('subject_id', '=', self.subject_id.id)
            ])
            search_result_line.unlink()
        self.state = 'draft'

    # @api.multi
    def valuation_canceled(self):
        self.state = 'cancel'
Example #27
0
class MailActivityType(models.Model):
    _inherit = "mail.activity.type"

    category = fields.Selection(selection_add=[('reminder', 'Reminder')])
Example #28
0
class fee_month(models.Model):

    _name = 'fee.month'

    _rec_name = 'name'

    List_Of_Month = [
        (1,'January'),
        (2,'February'),
        (3,'March'),
        (4,'April'),
        (5,'May'),
        (6,'June'),
        (7,'July'),
        (8,'August'),
        (9,'September'),
        (10,'October'),
        (11,'November'),
        (12,'December'),
        ]

    code= fields.Char(string='Code')
    name= fields.Selection(List_Of_Month,string='Month')
    year = fields.Char(string="Year")
    batch_id = fields.Many2one('batch',string='Batch')
    alt_month = fields.Boolean('Alternate Month')
    quater_month = fields.Boolean('Quater Month')
    # generate_month = fields.Boolean('Month Calculated')
    qtr_month = fields.Boolean('Quater Month')
    leave_month = fields.Boolean('Leave Month')

    @api.multi
    def name_get(self):
        res = []
        def _month(month):
            val = {
                1:'January',
                2:'February',
                3:'March',
                4:'April',
                5:'May',
                6:'June',
                7:'July',
                8:'August',
                9:'September',
                10:'October',
                11:'November',
                12:'December',
            }
            for i in val:
                if val.get(month):
                    return val[month]

        for record in self:
            name = _month(record.name)
            res.append((record.id, name))
        return res

    @api.model
    def allocation_alt_qtr_half_month_false(self, batch_id):
        for month_id in self.batch_id.month_ids.search([('batch_id','=',batch_id)]):
            month_id.write({'alt_month': False,
                            'quater_month': False,
                            'qtr_month': False})

    @api.model
    def allocation_alt_qtr_half_month(self, batch_id):
        code = 0
        total_month = self.batch_id.month_ids.search_count([('batch_id','=',batch_id),('leave_month','=',False)])
        if total_month % 2 == 0:
            half_month = total_month / 2
        else:
            total_month += 1
            half_month = total_month / 2
        for month_id in self.batch_id.month_ids.search([('batch_id','=',batch_id)]):
            if month_id.leave_month == False:
                code += 1
                # half year
                if code in [1,half_month+1]:
                    month_id.quater_month = True
                else:
                    month_id.quater_month = False
                # Quater month
                if code % 3 == 1:
                    month_id.qtr_month = True
                else:
                    month_id.qtr_month = False
                # Alter month
                if code % 2 == 0:
                    month_id.alt_month = False
                else:
                    month_id.alt_month = True

    @api.multi
    def make_it_leave_month(self):
        self.leave_month = True
        batch_id = self.batch_id.id
        self.allocation_alt_qtr_half_month_false(batch_id)
        self.allocation_alt_qtr_half_month(batch_id)

    @api.multi
    def make_it_unleave_month(self):
        self.leave_month = False
        batch_id = self.batch_id.id
        self.allocation_alt_qtr_half_month_false(batch_id)
        self.allocation_alt_qtr_half_month(batch_id)
Example #29
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='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', index=True)
    taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
    product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]")
    product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id')
    product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True)
    product_type = fields.Selection(related='product_id.type', readonly=True)
    price_unit = fields.Float(string='Unit Price', required=True, digits='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.move.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='Product Unit of Measure', store=True)

    qty_received_method = fields.Selection([('manual', 'Manual')], string="Received Qty Method", compute='_compute_qty_received_method', store=True,
        help="According to product configuration, the recieved quantity can be automatically computed by mechanism :\n"
             "  - Manual: the quantity is set manually on the line\n"
             "  - Stock Moves: the quantity comes from confirmed pickings\n")
    qty_received = fields.Float("Received Qty", compute='_compute_qty_received', inverse='_inverse_qty_received', compute_sudo=True, store=True, digits='Product Unit of Measure')
    qty_received_manual = fields.Float("Manual Received Qty", digits='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)

    display_type = fields.Selection([
        ('line_section', "Section"),
        ('line_note', "Note")], default=False, help="Technical field for UX purpose.")

    _sql_constraints = [
        ('accountable_required_fields',
            "CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND product_uom IS NOT NULL AND date_planned IS NOT NULL))",
            "Missing required fields on accountable purchase order line."),
        ('non_accountable_null_fields',
            "CHECK(display_type IS NULL OR (product_id IS NULL AND price_unit = 0 AND product_uom_qty = 0 AND product_uom IS NULL AND date_planned is NULL))",
            "Forbidden values on non-accountable purchase order line"),
    ]

    @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,
        }

    def _compute_tax_id(self):
        for line in self:
            fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.with_context(force_company=line.company_id.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.move_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.move_id.state not in ['cancel']:
                    if inv_line.move_id.type == 'in_invoice':
                        qty += inv_line.product_uom_id._compute_quantity(inv_line.quantity, line.product_uom)
                    elif inv_line.move_id.type == 'in_refund':
                        qty -= inv_line.product_uom_id._compute_quantity(inv_line.quantity, line.product_uom)
            line.qty_invoiced = qty

    @api.depends('product_id')
    def _compute_qty_received_method(self):
        for line in self:
            if line.product_id and line.product_id.type in ['consu', 'service']:
                line.qty_received_method = 'manual'
            else:
                line.qty_received_method = False

    @api.depends('qty_received_method', 'qty_received_manual')
    def _compute_qty_received(self):
        for line in self:
            if line.qty_received_method == 'manual':
                line.qty_received = line.qty_received_manual or 0.0
            else:
                line.qty_received = 0.0

    @api.onchange('qty_received')
    def _inverse_qty_received(self):
        """ When writing on qty_received, if the value should be modify manually (`qty_received_method` = 'manual' only),
            then we put the value in `qty_received_manual`. Otherwise, `qty_received_manual` should be False since the
            received qty is automatically compute by other mecanisms.
        """
        for line in self:
            if line.qty_received_method == 'manual':
                line.qty_received_manual = line.qty_received
            else:
                line.qty_received_manual = 0.0

    @api.model
    def create(self, values):
        if values.get('display_type', self.default_get(['display_type'])['display_type']):
            values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom=False, date_planned=False)

        order_id = values.get('order_id')
        if 'date_planned' not in values:
            order = self.env['purchase.order'].browse(order_id)
            if order.date_planned:
                values['date_planned'] = order.date_planned
        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

    def write(self, values):
        if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')):
            raise UserError("You cannot change the type of a purchase order line. Instead you should delete the current line and create a new line of the proper type.")

        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)

    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):
        if not self.product_id:
            return

        # 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_id_change()

        self._suggest_quantity()
        self._onchange_quantity()

    def _product_id_change(self):
        if not self.product_id:
            return

        self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id
        product_lang = self.product_id.with_context(
            lang=self.partner_id.lang,
            partner_id=self.partner_id.id,
            company_id=self.company_id.id,
        )
        self.name = self._get_product_purchase_description(product_lang)

        self._compute_tax_id()

    @api.onchange('product_id')
    def onchange_product_id_warning(self):
        if not self.product_id or not self.env.user.has_group('purchase.group_warning_purchase'):
            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.depends('product_uom', 'product_qty', 'product_id.uom_id')
    def _compute_product_uom_qty(self):
        for line in self:
            if line.product_id and 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

    def _get_product_purchase_description(self, product_lang):
        self.ensure_one()
        name = product_lang.display_name
        if product_lang.description_purchase:
            name += '\n' + product_lang.description_purchase

        return name

    def _prepare_account_move_line(self, move):
        self.ensure_one()
        if self.product_id.purchase_method == 'purchase':
            qty = self.product_qty - self.qty_invoiced
        else:
            qty = self.qty_received - self.qty_invoiced
        if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0:
            qty = 0.0

        if self.currency_id == move.company_id.currency_id:
            currency = False
        else:
            currency = move.currency_id

        return {
            'name': '%s: %s' % (self.order_id.name, self.name),
            'move_id': move.id,
            'currency_id': currency and currency.id or False,
            'purchase_line_id': self.id,
            'date_maturity': move.invoice_date_due,
            'product_uom_id': self.product_uom.id,
            'product_id': self.product_id.id,
            'price_unit': self.price_unit,
            'quantity': qty,
            'partner_id': move.partner_id.id,
            'analytic_account_id': self.account_analytic_id.id,
            'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)],
            'tax_ids': [(6, 0, self.taxes_id.ids)],
            'display_type': self.display_type,
        }
Example #30
0
class ResourceCalendar(models.Model):
    """ Calendar model for a resource. It has

     - attendance_ids: list of resource.calendar.attendance that are a working
                       interval in a given weekday.
     - leave_ids: list of leaves linked to this calendar. A leave can be general
                  or linked to a specific resource, depending on its resource_id.

    All methods in this class use intervals. An interval is a tuple holding
    (begin_datetime, end_datetime). A list of intervals is therefore a list of
    tuples, holding several intervals of work or leaves. """
    _name = "resource.calendar"
    _description = "Resource Working Time"

    @api.model
    def default_get(self, fields):
        res = super(ResourceCalendar, self).default_get(fields)
        if not res.get('name') and res.get('company_id'):
            res['name'] = _(
                'Working Hours of %s') % self.env['res.company'].browse(
                    res['company_id']).name
        if 'attendance_ids' in fields and not res.get('attendance_ids'):
            res['attendance_ids'] = [(0, 0, {
                'name': _('Monday Morning'),
                'dayofweek': '0',
                'hour_from': 8,
                'hour_to': 12,
                'day_period': 'morning'
            }),
                                     (0, 0, {
                                         'name': _('Monday Afternoon'),
                                         'dayofweek': '0',
                                         'hour_from': 13,
                                         'hour_to': 17,
                                         'day_period': 'afternoon'
                                     }),
                                     (0, 0, {
                                         'name': _('Tuesday Morning'),
                                         'dayofweek': '1',
                                         'hour_from': 8,
                                         'hour_to': 12,
                                         'day_period': 'morning'
                                     }),
                                     (0, 0, {
                                         'name': _('Tuesday Afternoon'),
                                         'dayofweek': '1',
                                         'hour_from': 13,
                                         'hour_to': 17,
                                         'day_period': 'afternoon'
                                     }),
                                     (0, 0, {
                                         'name': _('Wednesday Morning'),
                                         'dayofweek': '2',
                                         'hour_from': 8,
                                         'hour_to': 12,
                                         'day_period': 'morning'
                                     }),
                                     (0, 0, {
                                         'name': _('Wednesday Afternoon'),
                                         'dayofweek': '2',
                                         'hour_from': 13,
                                         'hour_to': 17,
                                         'day_period': 'afternoon'
                                     }),
                                     (0, 0, {
                                         'name': _('Thursday Morning'),
                                         'dayofweek': '3',
                                         'hour_from': 8,
                                         'hour_to': 12,
                                         'day_period': 'morning'
                                     }),
                                     (0, 0, {
                                         'name': _('Thursday Afternoon'),
                                         'dayofweek': '3',
                                         'hour_from': 13,
                                         'hour_to': 17,
                                         'day_period': 'afternoon'
                                     }),
                                     (0, 0, {
                                         'name': _('Friday Morning'),
                                         'dayofweek': '4',
                                         'hour_from': 8,
                                         'hour_to': 12,
                                         'day_period': 'morning'
                                     }),
                                     (0, 0, {
                                         'name': _('Friday Afternoon'),
                                         'dayofweek': '4',
                                         'hour_from': 13,
                                         'hour_to': 17,
                                         'day_period': 'afternoon'
                                     })]
        return res

    name = fields.Char(required=True)
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env.company)
    attendance_ids = fields.One2many('resource.calendar.attendance',
                                     'calendar_id',
                                     'Working Time',
                                     copy=True)
    leave_ids = fields.One2many('resource.calendar.leaves', 'calendar_id',
                                'Time Off')
    global_leave_ids = fields.One2many('resource.calendar.leaves',
                                       'calendar_id',
                                       'Global Time Off',
                                       domain=[('resource_id', '=', False)])
    hours_per_day = fields.Float(
        "Average Hour per Day",
        default=HOURS_PER_DAY,
        help=
        "Average hours per day a resource is supposed to work with this calendar."
    )
    tz = fields.Selection(
        _tz_get,
        string='Timezone',
        required=True,
        default=lambda self: self._context.get('tz'
                                               ) or self.env.user.tz or 'UTC',
        help=
        "This field is used in order to define in which timezone the resources will work."
    )
    two_weeks_calendar = fields.Boolean(string="Calendar in 2 weeks mode")
    two_weeks_explanation = fields.Char(
        'Explanation', compute="_compute_two_weeks_explanation")

    @api.depends('two_weeks_calendar')
    def _compute_two_weeks_explanation(self):
        today = fields.Date.today()
        week_type = _("odd") if int(
            math.floor((today.toordinal() - 1) / 7) % 2) else _("even")
        first_day = date_utils.start_of(today, 'week')
        last_day = date_utils.end_of(today, 'week')
        self.two_weeks_explanation = "This week (from %s to %s) is an %s week." % (
            first_day, last_day, week_type)

    def _get_global_attendances(self):
        return self.attendance_ids.filtered(
            lambda attendance: not attendance.date_from and not attendance.
            date_to and not attendance.resource_id and not attendance.
            display_type)

    def _compute_hours_per_day(self, attendances):
        if not attendances:
            return 0

        hour_count = 0.0
        for attendance in attendances:
            hour_count += attendance.hour_to - attendance.hour_from

        if self.two_weeks_calendar:
            number_of_days = len(
                set(
                    attendances.filtered(
                        lambda cal: cal.week_type == '1').mapped('dayofweek')))
            number_of_days += len(
                set(
                    attendances.filtered(
                        lambda cal: cal.week_type == '0').mapped('dayofweek')))
        else:
            number_of_days = len(set(attendances.mapped('dayofweek')))

        return float_round(hour_count / float(number_of_days),
                           precision_digits=2)

    @api.onchange('attendance_ids', 'two_weeks_calendar')
    def _onchange_hours_per_day(self):
        attendances = self._get_global_attendances()
        self.hours_per_day = self._compute_hours_per_day(attendances)

    def switch_calendar_type(self):
        if not self.two_weeks_calendar:
            self.attendance_ids.unlink()
            self.attendance_ids = [
                (0, 0, {
                    'name': 'Even week',
                    'dayofweek': '0',
                    'sequence': '0',
                    'hour_from': 0,
                    'day_period': 'morning',
                    'week_type': '0',
                    'hour_to': 0,
                    'display_type': 'line_section'
                }),
                (0, 0, {
                    'name': 'Odd week',
                    'dayofweek': '0',
                    'sequence': '25',
                    'hour_from': 0,
                    'day_period': 'morning',
                    'week_type': '1',
                    'hour_to': 0,
                    'display_type': 'line_section'
                }),
            ]

            self.two_weeks_calendar = True
            default_attendance = self.default_get(
                'attendance_ids')['attendance_ids']
            for idx, att in enumerate(default_attendance):
                att[2]["week_type"] = '0'
                att[2]["sequence"] = idx + 1
            self.attendance_ids = default_attendance
            for idx, att in enumerate(default_attendance):
                att[2]["week_type"] = '1'
                att[2]["sequence"] = idx + 26
            self.attendance_ids = default_attendance
        else:
            self.two_weeks_calendar = False
            self.attendance_ids.unlink()
            self.attendance_ids = self.default_get(
                'attendance_ids')['attendance_ids']
        self._onchange_hours_per_day()

    @api.onchange('attendance_ids')
    def _onchange_attendance_ids(self):
        if not self.two_weeks_calendar:
            return

        even_week_seq = self.attendance_ids.filtered(
            lambda att: att.display_type == 'line_section' and att.week_type ==
            '0')
        odd_week_seq = self.attendance_ids.filtered(
            lambda att: att.display_type == 'line_section' and att.week_type ==
            '1')
        if len(even_week_seq) != 1 or len(odd_week_seq) != 1:
            raise ValidationError(_("You can't delete section between weeks."))

        even_week_seq = even_week_seq.sequence
        odd_week_seq = odd_week_seq.sequence

        for line in self.attendance_ids.filtered(
                lambda att: att.display_type is False):
            if even_week_seq > odd_week_seq:
                line.week_type = '1' if even_week_seq > line.sequence else '0'
            else:
                line.week_type = '0' if odd_week_seq > line.sequence else '1'

    def _check_overlap(self, attendance_ids):
        """ attendance_ids correspond to attendance of a week,
            will check for each day of week that there are no superimpose. """
        result = []
        for attendance in attendance_ids.filtered(
                lambda att: not att.date_from and not att.date_to):
            # 0.000001 is added to each start hour to avoid to detect two contiguous intervals as superimposing.
            # Indeed Intervals function will join 2 intervals with the start and stop hour corresponding.
            result.append(
                (int(attendance.dayofweek) * 24 + attendance.hour_from +
                 0.000001, int(attendance.dayofweek) * 24 + attendance.hour_to,
                 attendance))

        if len(Intervals(result)) != len(result):
            raise ValidationError(_("Attendances can't overlap."))

    @api.constrains('attendance_ids')
    def _check_attendance(self):
        # Avoid superimpose in attendance
        for calendar in self:
            attendance_ids = calendar.attendance_ids.filtered(
                lambda attendance: not attendance.resource_id and attendance.
                display_type is False)
            if calendar.two_weeks_calendar:
                calendar._check_overlap(
                    attendance_ids.filtered(
                        lambda attendance: attendance.week_type == '0'))
                calendar._check_overlap(
                    attendance_ids.filtered(
                        lambda attendance: attendance.week_type == '1'))
            else:
                calendar._check_overlap(attendance_ids)

    # --------------------------------------------------
    # Computation API
    # --------------------------------------------------
    def _attendance_intervals(self,
                              start_dt,
                              end_dt,
                              resource=None,
                              domain=None):
        """ Return the attendance intervals in the given datetime range.
            The returned intervals are expressed in the resource's timezone.
        """
        assert start_dt.tzinfo and end_dt.tzinfo
        combine = datetime.combine

        resource_ids = [resource.id, False] if resource else [False]
        domain = domain if domain is not None else []
        domain = expression.AND([
            domain,
            [
                ('calendar_id', '=', self.id),
                ('resource_id', 'in', resource_ids),
                ('display_type', '=', False),
            ]
        ])

        # express all dates and times in the resource's timezone
        tz = timezone((resource or self).tz)
        start_dt = start_dt.astimezone(tz)
        end_dt = end_dt.astimezone(tz)

        # for each attendance spec, generate the intervals in the date range
        result = []
        for attendance in self.env['resource.calendar.attendance'].search(
                domain):
            start = start_dt.date()
            if attendance.date_from:
                start = max(start, attendance.date_from)
            until = end_dt.date()
            if attendance.date_to:
                until = min(until, attendance.date_to)
            if attendance.week_type:
                start_week_type = int(
                    math.floor((start.toordinal() - 1) / 7) % 2)
                if start_week_type != int(attendance.week_type):
                    # start must be the week of the attendance
                    # if it's not the case, we must remove one week
                    start = start + relativedelta(weeks=-1)
            weekday = int(attendance.dayofweek)

            if self.two_weeks_calendar and attendance.week_type:
                days = rrule(WEEKLY,
                             start,
                             interval=2,
                             until=until,
                             byweekday=weekday)
            else:
                days = rrule(DAILY, start, until=until, byweekday=weekday)

            for day in days:
                # attendance hours are interpreted in the resource's timezone
                dt0 = tz.localize(
                    combine(day, float_to_time(attendance.hour_from)))
                dt1 = tz.localize(
                    combine(day, float_to_time(attendance.hour_to)))
                result.append((max(start_dt, dt0), min(end_dt,
                                                       dt1), attendance))

        return Intervals(result)

    def _leave_intervals(self, start_dt, end_dt, resource=None, domain=None):
        """ Return the leave intervals in the given datetime range.
            The returned intervals are expressed in the calendar's timezone.
        """
        assert start_dt.tzinfo and end_dt.tzinfo
        self.ensure_one()

        # for the computation, express all datetimes in UTC
        resource_ids = [resource.id, False] if resource else [False]
        if domain is None:
            domain = [('time_type', '=', 'leave')]
        domain = domain + [
            ('calendar_id', '=', self.id),
            ('resource_id', 'in', resource_ids),
            ('date_from', '<=', datetime_to_string(end_dt)),
            ('date_to', '>=', datetime_to_string(start_dt)),
        ]

        # retrieve leave intervals in (start_dt, end_dt)
        tz = timezone((resource or self).tz)
        start_dt = start_dt.astimezone(tz)
        end_dt = end_dt.astimezone(tz)
        result = []
        for leave in self.env['resource.calendar.leaves'].search(domain):
            dt0 = string_to_datetime(leave.date_from).astimezone(tz)
            dt1 = string_to_datetime(leave.date_to).astimezone(tz)
            result.append((max(start_dt, dt0), min(end_dt, dt1), leave))

        return Intervals(result)

    def _work_intervals(self, start_dt, end_dt, resource=None, domain=None):
        """ Return the effective work intervals between the given datetimes. """
        return (self._attendance_intervals(start_dt, end_dt, resource) -
                self._leave_intervals(start_dt, end_dt, resource, domain))

    # --------------------------------------------------
    # Private Methods / Helpers
    # --------------------------------------------------

    def _get_days_data(self, intervals, day_total):
        """
        helper function to compute duration of `intervals`
        expressed in days and hours.
        `day_total` is a dict {date: n_hours} with the number of hours for each day.
        """
        day_hours = defaultdict(float)
        for start, stop, meta in intervals:
            day_hours[start.date()] += (stop - start).total_seconds() / 3600

        # compute number of days as quarters
        days = sum(
            float_utils.round(ROUNDING_FACTOR * day_hours[day] /
                              day_total[day]) / ROUNDING_FACTOR
            for day in day_hours)
        return {
            'days': days,
            'hours': sum(day_hours.values()),
        }

    def _get_day_total(self, from_datetime, to_datetime, resource=None):
        """
        @return dict with hours of attendance in each day between `from_datetime` and `to_datetime`
        """
        self.ensure_one()
        # total hours per day:  retrieve attendances with one extra day margin,
        # in order to compute the total hours on the first and last days
        from_full = from_datetime - timedelta(days=1)
        to_full = to_datetime + timedelta(days=1)
        intervals = self._attendance_intervals(from_full,
                                               to_full,
                                               resource=resource)
        day_total = defaultdict(float)
        for start, stop, meta in intervals:
            day_total[start.date()] += (stop - start).total_seconds() / 3600
        return day_total

    # --------------------------------------------------
    # External API
    # --------------------------------------------------

    def get_work_hours_count(self,
                             start_dt,
                             end_dt,
                             compute_leaves=True,
                             domain=None):
        """
            `compute_leaves` controls whether or not this method is taking into
            account the global leaves.

            `domain` controls the way leaves are recognized.
            None means default value ('time_type', '=', 'leave')

            Counts the number of work hours between two datetimes.
        """
        # Set timezone in UTC if no timezone is explicitly given
        if not start_dt.tzinfo:
            start_dt = start_dt.replace(tzinfo=utc)
        if not end_dt.tzinfo:
            end_dt = end_dt.replace(tzinfo=utc)

        if compute_leaves:
            intervals = self._work_intervals(start_dt, end_dt, domain=domain)
        else:
            intervals = self._attendance_intervals(start_dt, end_dt)

        return sum((stop - start).total_seconds() / 3600
                   for start, stop, meta in intervals)

    def get_work_duration_data(self,
                               from_datetime,
                               to_datetime,
                               compute_leaves=True,
                               domain=None):
        """
            Get the working duration (in days and hours) for a given period, only
            based on the current calendar. This method does not use resource to
            compute it.

            `domain` is used in order to recognise the leaves to take,
            None means default value ('time_type', '=', 'leave')

            Returns a dict {'days': n, 'hours': h} containing the
            quantity of working time expressed as days and as hours.
        """
        # naive datetimes are made explicit in UTC
        from_datetime, dummy = make_aware(from_datetime)
        to_datetime, dummy = make_aware(to_datetime)

        day_total = self._get_day_total(from_datetime, to_datetime)

        # actual hours per day
        if compute_leaves:
            intervals = self._work_intervals(from_datetime,
                                             to_datetime,
                                             domain=domain)
        else:
            intervals = self._attendance_intervals(from_datetime, to_datetime)

        return self._get_days_data(intervals, day_total)

    def plan_hours(self,
                   hours,
                   day_dt,
                   compute_leaves=False,
                   domain=None,
                   resource=None):
        """
        `compute_leaves` controls whether or not this method is taking into
        account the global leaves.

        `domain` controls the way leaves are recognized.
        None means default value ('time_type', '=', 'leave')

        Return datetime after having planned hours
        """
        day_dt, revert = make_aware(day_dt)

        # which method to use for retrieving intervals
        if compute_leaves:
            get_intervals = partial(self._work_intervals,
                                    domain=domain,
                                    resource=resource)
        else:
            get_intervals = self._attendance_intervals

        if hours >= 0:
            delta = timedelta(days=14)
            for n in range(100):
                dt = day_dt + delta * n
                for start, stop, meta in get_intervals(dt, dt + delta):
                    interval_hours = (stop - start).total_seconds() / 3600
                    if hours <= interval_hours:
                        return revert(start + timedelta(hours=hours))
                    hours -= interval_hours
            return False
        else:
            hours = abs(hours)
            delta = timedelta(days=14)
            for n in range(100):
                dt = day_dt - delta * n
                for start, stop, meta in reversed(get_intervals(
                        dt - delta, dt)):
                    interval_hours = (stop - start).total_seconds() / 3600
                    if hours <= interval_hours:
                        return revert(stop - timedelta(hours=hours))
                    hours -= interval_hours
            return False

    def plan_days(self, days, day_dt, compute_leaves=False, domain=None):
        """
        `compute_leaves` controls whether or not this method is taking into
        account the global leaves.

        `domain` controls the way leaves are recognized.
        None means default value ('time_type', '=', 'leave')

        Returns the datetime of a days scheduling.
        """
        day_dt, revert = make_aware(day_dt)

        # which method to use for retrieving intervals
        if compute_leaves:
            get_intervals = partial(self._work_intervals, domain=domain)
        else:
            get_intervals = self._attendance_intervals

        if days > 0:
            found = set()
            delta = timedelta(days=14)
            for n in range(100):
                dt = day_dt + delta * n
                for start, stop, meta in get_intervals(dt, dt + delta):
                    found.add(start.date())
                    if len(found) == days:
                        return revert(stop)
            return False

        elif days < 0:
            days = abs(days)
            found = set()
            delta = timedelta(days=14)
            for n in range(100):
                dt = day_dt - delta * n
                for start, stop, meta in reversed(get_intervals(
                        dt - delta, dt)):
                    found.add(start.date())
                    if len(found) == days:
                        return revert(start)
            return False

        else:
            return revert(day_dt)