Exemple #1
0
class Lead2OpportunityMassConvert(models.TransientModel):

    _name = 'crm.lead2opportunity.partner.mass'
    _description = 'Mass Lead To Opportunity Partner'
    _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 Channel',
                              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()
Exemple #2
0
class ProductTemplate(models.Model):
    _inherit = [
        "product.template", "website.seo.metadata", 'website.published.mixin',
        'rating.mixin'
    ]
    _order = 'website_published desc, website_sequence desc, name'
    _name = 'product.template'
    _mail_post_access = 'read'

    def _default_website(self):
        default_website_id = self.env.ref('website.default_website')
        return [default_website_id.id] if default_website_id else None

    website_description = fields.Html('Description for the website',
                                      sanitize_attributes=False,
                                      translate=html_translate)
    alternative_product_ids = fields.Many2many(
        'product.template',
        'product_alternative_rel',
        'src_id',
        'dest_id',
        string='Alternative Products',
        help='Suggest more expensive alternatives to '
        'your customers (upsell strategy). Those products show up on the product page.'
    )
    accessory_product_ids = fields.Many2many(
        'product.product',
        'product_accessory_rel',
        'src_id',
        'dest_id',
        string='Accessory Products',
        help='Accessories show up when the customer reviews the '
        'cart before paying (cross-sell strategy, e.g. for computers: mouse, keyboard, etc.). '
        'An algorithm figures out a list of accessories based on all the products added to cart.'
    )
    website_size_x = fields.Integer('Size X', default=1)
    website_size_y = fields.Integer('Size Y', default=1)
    website_sequence = fields.Integer(
        'Website Sequence',
        help="Determine the display order in the Website E-commerce",
        default=lambda self: self._default_website_sequence())
    public_categ_ids = fields.Many2many(
        'product.public.category',
        string='Website Product Category',
        help=
        "Categories can be published on the Shop page (online catalog grid) to help "
        "customers find all the items within a category. To publish them, go to the Shop page, "
        "hit Customize and turn *Product Categories* on. A product can belong to several categories."
    )
    product_image_ids = fields.One2many('product.image',
                                        'product_tmpl_id',
                                        string='Images')

    website_price = fields.Float('Website price',
                                 compute='_website_price',
                                 digits=dp.get_precision('Product Price'))
    website_public_price = fields.Float(
        'Website public price',
        compute='_website_price',
        digits=dp.get_precision('Product Price'))
    website_price_difference = fields.Boolean('Website price difference',
                                              compute='_website_price')
    website_ids = fields.Many2many('website',
                                   'website_prod_pub_rel',
                                   'website_id',
                                   'product_id',
                                   string='Websites',
                                   copy=False,
                                   default=_default_website,
                                   help='List of websites in which '
                                   'Product will published.')
    ribbon_id = fields.Many2one('product.ribbon', string="Product Ribbon")
    brand_id = fields.Many2one('product.brand', string="Product Brand")
    tag_ids = fields.Many2many('product.tags', string="Product Tags")

    def _website_price(self):
        # First filter out the ones that have no variant:
        # This makes sure that every template below has a corresponding product in the zipped result.
        self = self.filtered('product_variant_id')
        # use mapped who returns a recordset with only itself to prefetch (and don't prefetch every product_variant_ids)
        for template, product in pycompat.izip(
                self, self.mapped('product_variant_id')):
            template.website_price = product.website_price
            template.website_public_price = product.website_public_price
            template.website_price_difference = product.website_price_difference

    def _default_website_sequence(self):
        self._cr.execute("SELECT MIN(website_sequence) FROM %s" % self._table)
        min_sequence = self._cr.fetchone()[0]
        return min_sequence and min_sequence - 1 or 10

    def set_sequence_top(self):
        self.website_sequence = self.sudo().search(
            [], order='website_sequence desc', limit=1).website_sequence + 1

    def set_sequence_bottom(self):
        self.website_sequence = self.sudo().search(
            [], order='website_sequence', limit=1).website_sequence - 1

    def set_sequence_up(self):
        previous_product_tmpl = self.sudo().search(
            [('website_sequence', '>', self.website_sequence),
             ('website_published', '=', self.website_published)],
            order='website_sequence',
            limit=1)
        if previous_product_tmpl:
            previous_product_tmpl.website_sequence, self.website_sequence = self.website_sequence, previous_product_tmpl.website_sequence
        else:
            self.set_sequence_top()

    def set_sequence_down(self):
        next_prodcut_tmpl = self.search(
            [('website_sequence', '<', self.website_sequence),
             ('website_published', '=', self.website_published)],
            order='website_sequence desc',
            limit=1)
        if next_prodcut_tmpl:
            next_prodcut_tmpl.website_sequence, self.website_sequence = self.website_sequence, next_prodcut_tmpl.website_sequence
        else:
            return self.set_sequence_bottom()

    @api.multi
    def _compute_website_url(self):
        super(ProductTemplate, self)._compute_website_url()
        for product in self:
            product.website_url = "/shop/product/%s" % (product.id, )
Exemple #3
0
class PaymentTransaction(models.Model):
    """ Transaction Model. Each specific acquirer can extend the model by adding
    its own fields.

    Methods that can be added in an acquirer-specific implementation:

     - ``<name>_create``: method receiving values used when creating a new
       transaction and that returns a dictionary that will update those values.
       This method can be used to tweak some transaction values.

    Methods defined for convention, depending on your controllers:

     - ``<name>_form_feedback(self, data)``: method that handles the data coming
       from the acquirer after the transaction. It will generally receives data
       posted by the acquirer after the transaction.
    """
    _name = 'payment.transaction'
    _description = 'Payment Transaction'
    _order = 'id desc'
    _rec_name = 'reference'

    @api.model
    def _lang_get(self):
        return self.env['res.lang'].get_installed()

    @api.model
    def _get_default_partner_country_id(self):
        return self.env['res.company']._company_default_get(
            'payment.transaction').country_id.id

    create_date = fields.Datetime('Creation Date', readonly=True)
    date_validate = fields.Datetime('Validation Date')
    acquirer_id = fields.Many2one('payment.acquirer',
                                  'Acquirer',
                                  required=True)
    provider = fields.Selection(string='Provider',
                                related='acquirer_id.provider')
    type = fields.Selection([('validation', 'Validation of the bank card'),
                             ('server2server', 'Server To Server'),
                             ('form', 'Form'),
                             ('form_save', 'Form with tokenization')],
                            'Type',
                            default='form',
                            required=True)
    state = fields.Selection([('draft', 'Draft'), ('pending', 'Pending'),
                              ('authorized', 'Authorized'), ('done', 'Done'),
                              ('refunding', 'Refunding'),
                              ('refunded', 'Refunded'), ('error', 'Error'),
                              ('cancel', 'Canceled')],
                             'Status',
                             copy=False,
                             default='draft',
                             required=True,
                             track_visibility='onchange')
    state_message = fields.Text(
        'Message',
        help=
        'Field used to store error and/or validation messages for information')
    # payment
    amount = fields.Float('Amount',
                          digits=(16, 2),
                          required=True,
                          track_visibility='always',
                          help='Amount')
    fees = fields.Float(
        'Fees',
        digits=(16, 2),
        track_visibility='always',
        help='Fees amount; set by the system because depends on the acquirer')
    currency_id = fields.Many2one('res.currency', 'Currency', required=True)
    reference = fields.Char('Reference',
                            default=lambda self: self.env['ir.sequence'].
                            next_by_code('payment.transaction'),
                            required=True,
                            help='Internal reference of the TX')
    acquirer_reference = fields.Char(
        'Acquirer Reference',
        help='Reference of the TX as stored in the acquirer database')
    # duplicate partner / transaction data to store the values at transaction time
    partner_id = fields.Many2one('res.partner',
                                 'Customer',
                                 track_visibility='onchange')
    partner_name = fields.Char('Partner Name')
    partner_lang = fields.Selection(_lang_get,
                                    'Language',
                                    default=lambda self: self.env.lang)
    partner_email = fields.Char('Email')
    partner_zip = fields.Char('Zip')
    partner_address = fields.Char('Address')
    partner_city = fields.Char('City')
    partner_country_id = fields.Many2one(
        'res.country',
        'Country',
        default=_get_default_partner_country_id,
        required=True)
    partner_phone = fields.Char('Phone')
    html_3ds = fields.Char('3D Secure HTML')

    callback_model_id = fields.Many2one('ir.model',
                                        'Callback Document Model',
                                        groups="base.group_system")
    callback_res_id = fields.Integer('Callback Document ID',
                                     groups="base.group_system")
    callback_method = fields.Char('Callback Method',
                                  groups="base.group_system")
    callback_hash = fields.Char('Callback Hash', groups="base.group_system")

    payment_token_id = fields.Many2one(
        'payment.token',
        'Payment Token',
        domain="[('acquirer_id', '=', acquirer_id)]")

    @api.onchange('partner_id')
    def _onchange_partner_id(self):
        onchange_vals = self.on_change_partner_id(self.partner_id.id).get(
            'value', {})
        self.update(onchange_vals)

    @api.multi
    def on_change_partner_id(self, partner_id):
        partner = None
        if partner_id:
            partner = self.env['res.partner'].browse(partner_id)
            return {
                'value': {
                    'partner_name':
                    partner and partner.name or False,
                    'partner_lang':
                    partner and partner.lang or 'en_US',
                    'partner_email':
                    partner and partner.email or False,
                    'partner_zip':
                    partner and partner.zip or False,
                    'partner_address':
                    _partner_format_address(partner and partner.street or '',
                                            partner and partner.street2 or ''),
                    'partner_city':
                    partner and partner.city or False,
                    'partner_country_id':
                    partner and partner.country_id.id
                    or self._get_default_partner_country_id(),
                    'partner_phone':
                    partner and partner.phone or False,
                }
            }
        return {}

    @api.constrains('reference', 'state')
    def _check_reference(self):
        for transaction in self.filtered(lambda tx: tx.state not in
                                         ('cancel', 'error')):
            if self.search_count([('reference', '=', transaction.reference)
                                  ]) != 1:
                raise exceptions.ValidationError(
                    _('The payment transaction reference must be unique!'))
        return True

    @api.constrains('state', 'acquirer_id')
    def _check_authorize_state(self):
        failed_tx = self.filtered(
            lambda tx: tx.state == 'authorized' and tx.acquirer_id.
            provider not in self.env['payment.acquirer']._get_feature_support(
            )['authorize'])
        if failed_tx:
            raise exceptions.ValidationError(
                _('The %s payment acquirers are not allowed to manual capture mode!'
                  % failed_tx.mapped('acquirer_id.name')))

    @api.model
    def create(self, values):
        if values.get('partner_id'):  # @TDENOTE: not sure
            values.update(
                self.on_change_partner_id(values['partner_id'])['value'])

        # call custom create method if defined (i.e. ogone_create for ogone)
        if values.get('acquirer_id'):
            acquirer = self.env['payment.acquirer'].browse(
                values['acquirer_id'])

            # compute fees
            custom_method_name = '%s_compute_fees' % acquirer.provider
            if hasattr(acquirer, custom_method_name):
                fees = getattr(acquirer, custom_method_name)(
                    values.get('amount', 0.0), values.get('currency_id'),
                    values.get('partner_country_id'))
                values['fees'] = float_round(fees, 2)

            # custom create
            custom_method_name = '%s_create' % acquirer.provider
            if hasattr(acquirer, custom_method_name):
                values.update(getattr(self, custom_method_name)(values))

        # Default value of reference is
        tx = super(PaymentTransaction, self).create(values)
        if not values.get('reference'):
            tx.write({'reference': str(tx.id)})

        # Generate callback hash if it is configured on the tx; avoid generating unnecessary stuff
        # (limited sudo env for checking callback presence, must work for manual transactions too)
        tx_sudo = tx.sudo()
        if tx_sudo.callback_model_id and tx_sudo.callback_res_id and tx_sudo.callback_method:
            tx.write({'callback_hash': tx._generate_callback_hash()})

        return tx

    @api.multi
    def write(self, values):
        if ('acquirer_id' in values
                or 'amount' in values) and 'fees' not in values:
            # The acquirer or the amount has changed, and the fees are not explicitly forced. Fees must be recomputed.
            acquirer = None
            if values.get('acquirer_id'):
                acquirer = self.env['payment.acquirer'].browse(
                    values['acquirer_id'])
            for tx in self:
                vals = dict(values, fees=0.0)
                if not acquirer:
                    acquirer = tx.acquirer_id
                custom_method_name = '%s_compute_fees' % acquirer.provider
                # TDE FIXME: shouldn't we use fee_implemented ?
                if hasattr(acquirer, custom_method_name):
                    fees = getattr(acquirer, custom_method_name)(
                        (values['amount'] if 'amount' in values else tx.amount)
                        or 0.0, values.get('currency_id') or tx.currency_id.id,
                        values.get('partner_country_id')
                        or tx.partner_country_id.id)
                    vals['fees'] = float_round(fees, 2)
                res = super(PaymentTransaction, tx).write(vals)
            return res
        return super(PaymentTransaction, self).write(values)

    @api.model
    def get_next_reference(self, reference):
        ref_suffix = 1
        init_ref = reference
        while self.env['payment.transaction'].sudo().search_count([
            ('reference', '=', reference)
        ]):
            reference = init_ref + 'x' + str(ref_suffix)
            ref_suffix += 1
        return reference

    def _generate_callback_hash(self):
        self.ensure_one()
        secret = self.env['ir.config_parameter'].sudo().get_param(
            'database.secret')
        token = '%s%s%s' % (self.callback_model_id.model, self.callback_res_id,
                            self.sudo().callback_method)
        return hmac.new(secret.encode('utf-8'), token.encode('utf-8'),
                        hashlib.sha256).hexdigest()

    # --------------------------------------------------
    # FORM RELATED METHODS
    # --------------------------------------------------

    @api.multi
    def render(self):
        values = {
            'reference': self.reference,
            'amount': self.amount,
            'currency_id': self.currency_id.id,
            'currency': self.currency_id,
            'partner': self.partner_id,
            'partner_name': self.partner_name,
            'partner_lang': self.partner_lang,
            'partner_email': self.partner_email,
            'partner_zip': self.partner_zip,
            'partner_address': self.partner_address,
            'partner_city': self.partner_city,
            'partner_country_id': self.partner_country_id.id,
            'partner_country': self.partner_country_id,
            'partner_phone': self.partner_phone,
            'partner_state': None,
        }
        return self.acquirer_id.render(None, None, None, values=values)

    @api.model
    def form_feedback(self, data, acquirer_name):
        invalid_parameters, tx = None, None

        tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
        if hasattr(self, tx_find_method_name):
            tx = getattr(self, tx_find_method_name)(data)

        # TDE TODO: form_get_invalid_parameters from model to multi
        invalid_param_method_name = '_%s_form_get_invalid_parameters' % acquirer_name
        if hasattr(self, invalid_param_method_name):
            invalid_parameters = getattr(tx, invalid_param_method_name)(data)

        if invalid_parameters:
            _error_message = '%s: incorrect tx data:\n' % (acquirer_name)
            for item in invalid_parameters:
                _error_message += '\t%s: received %s instead of %s\n' % (
                    item[0], item[1], item[2])
            _logger.error(_error_message)
            return False

        # TDE TODO: form_validate from model to multi
        feedback_method_name = '_%s_form_validate' % acquirer_name
        if hasattr(self, feedback_method_name):
            return getattr(tx, feedback_method_name)(data)

        return True

    @api.multi
    def _post_process_after_done(self, **kwargs):
        return True

    # --------------------------------------------------
    # SERVER2SERVER RELATED METHODS
    # --------------------------------------------------

    @api.multi
    def s2s_do_transaction(self, **kwargs):
        custom_method_name = '%s_s2s_do_transaction' % self.acquirer_id.provider
        if hasattr(self, custom_method_name):
            return getattr(self, custom_method_name)(**kwargs)

    @api.multi
    def s2s_do_refund(self, **kwargs):
        custom_method_name = '%s_s2s_do_refund' % self.acquirer_id.provider
        if hasattr(self, custom_method_name):
            return getattr(self, custom_method_name)(**kwargs)

    @api.multi
    def s2s_capture_transaction(self, **kwargs):
        custom_method_name = '%s_s2s_capture_transaction' % self.acquirer_id.provider
        if hasattr(self, custom_method_name):
            return getattr(self, custom_method_name)(**kwargs)

    @api.multi
    def s2s_void_transaction(self, **kwargs):
        custom_method_name = '%s_s2s_void_transaction' % self.acquirer_id.provider
        if hasattr(self, custom_method_name):
            return getattr(self, custom_method_name)(**kwargs)

    @api.multi
    def s2s_get_tx_status(self):
        """ Get the tx status. """
        invalid_param_method_name = '_%s_s2s_get_tx_status' % self.acquirer_id.provider
        if hasattr(self, invalid_param_method_name):
            return getattr(self, invalid_param_method_name)()
        return True

    @api.multi
    def execute_callback(self):
        res = None
        for transaction in self:
            # limited sudo env, only for checking callback presence, not for running it!
            # manual transactions have no callback, and can pass without being run by admin user
            tx_sudo = transaction.sudo()
            if not (tx_sudo.callback_model_id and tx_sudo.callback_res_id
                    and tx_sudo.callback_method):
                continue

            valid_token = transaction._generate_callback_hash()
            if not consteq(ustr(valid_token), transaction.callback_hash):
                _logger.warning(
                    "Invalid callback signature for transaction %d" %
                    (transaction.id))
                continue

            record = self.env[transaction.callback_model_id.model].browse(
                transaction.callback_res_id).exists()
            if record:
                res = getattr(record, transaction.callback_method)(transaction)
            else:
                _logger.warning(
                    "Did not found record %s.%s for callback of transaction %d"
                    % (transaction.callback_model_id.model,
                       transaction.callback_res_id, transaction.id))
        return res

    @api.multi
    def action_capture(self):
        if any(self.mapped(lambda tx: tx.state != 'authorized')):
            raise ValidationError(
                _('Only transactions in the Authorized status can be captured.'
                  ))
        for tx in self:
            tx.s2s_capture_transaction()

    @api.multi
    def action_void(self):
        if any(self.mapped(lambda tx: tx.state != 'authorized')):
            raise ValidationError(
                _('Only transactions in the Authorized status can be voided.'))
        for tx in self:
            tx.s2s_void_transaction()
Exemple #4
0
class PayslipReport(models.Model):
    _name = "payslip.report"
    _description = "Payslip Analysis"
    _auto = False

    name = fields.Char(readonly=True)
    date_from = fields.Date(string='Date From', readonly=True)
    date_to = fields.Date(string='Date To', readonly=True)
    year = fields.Char(size=4, readonly=True)
    month = fields.Selection([('01', 'January'), ('02', 'February'),
                              ('03', 'March'), ('04', 'April'), ('05', 'May'),
                              ('06', 'June'), ('07', 'July'), ('08', 'August'),
                              ('09', 'September'), ('10', 'October'),
                              ('11', 'November'), ('12', 'December')],
                             readonly=True)
    day = fields.Char(size=128, readonly=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('done', 'Done'),
        ('cancel', 'Rejected'),
    ],
                             string='Status',
                             readonly=True)
    employee_id = fields.Many2one('hr.employee',
                                  string='Employee',
                                  readonly=True)
    nbr = fields.Integer(string='# Payslip lines', readonly=True)
    number = fields.Char(readonly=True)
    struct_id = fields.Many2one('hr.payroll.structure',
                                string='Structure',
                                readonly=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True)
    paid = fields.Boolean(string='Made Payment Order ? ', readonly=True)
    total = fields.Float(readonly=True)
    category_id = fields.Many2one('hr.salary.rule.category',
                                  string='Category',
                                  readonly=True)

    @api.model_cr
    def init(self):
        drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute("""
            create or replace view payslip_report as (
                select
                    min(l.id) as id,
                    l.name,
                    p.struct_id,
                    p.state,
                    p.date_from,
                    p.date_to,
                    p.number,
                    p.company_id,
                    p.paid,
                    l.category_id,
                    l.employee_id,
                    sum(l.total) as total,
                    to_char(p.date_from, 'YYYY') as year,
                    to_char(p.date_from, 'MM') as month,
                    to_char(p.date_from, 'YYYY-MM-DD') as day,
                    to_char(p.date_to, 'YYYY') as to_year,
                    to_char(p.date_to, 'MM') as to_month,
                    to_char(p.date_to, 'YYYY-MM-DD') as to_day,
                    1 AS nbr
                from
                    hr_payslip as p
                    left join hr_payslip_line as l on (p.id=l.slip_id)
                where
                    l.employee_id IS NOT NULL
                group by
                    p.number,l.name,p.date_from,p.date_to,p.state,p.company_id,p.paid,
                    l.employee_id,p.struct_id,l.category_id
            )
        """)
class AccountInvoiceReport(models.Model):
    _name = "account.invoice.report"
    _inherit = ['ir.branch.company.mixin']
    _description = "Invoices Statistics"
    _auto = False
    _rec_name = 'date'

    @api.multi
    @api.depends('currency_id', 'date', 'price_total', 'price_average',
                 'residual')
    def _compute_amounts_in_user_currency(self):
        """Compute the amounts in the currency of the user
        """
        context = dict(self._context or {})
        user_currency_id = self.env.user.company_id.currency_id
        currency_rate_id = self.env['res.currency.rate'].search(
            [('rate', '=', 1), '|',
             ('company_id', '=', self.env.user.company_id.id),
             ('company_id', '=', False)],
            limit=1)
        base_currency_id = currency_rate_id.currency_id
        ctx = context.copy()
        for record in self:
            ctx['date'] = record.date
            record.user_currency_price_total = base_currency_id.with_context(
                ctx).compute(record.price_total, user_currency_id)
            record.user_currency_price_average = base_currency_id.with_context(
                ctx).compute(record.price_average, user_currency_id)
            record.user_currency_residual = base_currency_id.with_context(
                ctx).compute(record.residual, user_currency_id)

    date = fields.Date(readonly=True)
    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 readonly=True)
    product_qty = fields.Float(string='Product Quantity', readonly=True)
    uom_name = fields.Char(string='Reference Unit of Measure', readonly=True)
    payment_term_id = fields.Many2one('account.payment.term',
                                      string='Payment Terms',
                                      oldname='payment_term',
                                      readonly=True)
    fiscal_position_id = fields.Many2one('account.fiscal.position',
                                         oldname='fiscal_position',
                                         string='Fiscal Position',
                                         readonly=True)
    currency_id = fields.Many2one('res.currency',
                                  string='Currency',
                                  readonly=True)
    categ_id = fields.Many2one('product.category',
                               string='Product Category',
                               readonly=True)
    journal_id = fields.Many2one('account.journal',
                                 string='Journal',
                                 readonly=True)
    partner_id = fields.Many2one('res.partner',
                                 string='Partner',
                                 readonly=True)
    commercial_partner_id = fields.Many2one('res.partner',
                                            string='Partner Company',
                                            help="Commercial Entity")
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True)
    user_id = fields.Many2one('res.users', string='Salesperson', readonly=True)
    price_total = fields.Float(string='Total Without Tax', readonly=True)
    user_currency_price_total = fields.Float(
        string="Total Without Tax",
        compute='_compute_amounts_in_user_currency',
        digits=0)
    price_average = fields.Float(string='Average Price',
                                 readonly=True,
                                 group_operator="avg")
    user_currency_price_average = fields.Float(
        string="Average Price",
        compute='_compute_amounts_in_user_currency',
        digits=0)
    currency_rate = fields.Float(string='Currency Rate',
                                 readonly=True,
                                 group_operator="avg",
                                 groups="base.group_multi_currency")
    nbr = fields.Integer(
        string='# of Lines',
        readonly=True)  # TDE FIXME master: rename into nbr_lines
    type = fields.Selection([
        ('out_invoice', 'Customer Invoice'),
        ('in_invoice', 'Vendor Bill'),
        ('out_refund', 'Customer Credit Note'),
        ('in_refund', 'Vendor Credit Note'),
    ],
                            readonly=True)
    state = fields.Selection([('draft', 'Draft'), ('open', 'Open'),
                              ('paid', 'Paid'), ('cancel', 'Cancelled')],
                             string='Invoice Status',
                             readonly=True)
    date_due = fields.Date(string='Due Date', readonly=True)
    account_id = fields.Many2one('account.account',
                                 string='Account',
                                 readonly=True,
                                 domain=[('deprecated', '=', False)])
    account_line_id = fields.Many2one('account.account',
                                      string='Account Line',
                                      readonly=True,
                                      domain=[('deprecated', '=', False)])
    partner_bank_id = fields.Many2one('res.partner.bank',
                                      string='Bank Account',
                                      readonly=True)
    residual = fields.Float(string='Due Amount', readonly=True)
    user_currency_residual = fields.Float(
        string="Total Residual",
        compute='_compute_amounts_in_user_currency',
        digits=0)
    country_id = fields.Many2one('res.country',
                                 string='Country of the Partner Company')
    account_analytic_id = fields.Many2one(
        'account.analytic.account',
        string='Analytic Account',
        groups="analytic.group_analytic_accounting")

    _order = 'date desc'

    _depends = {
        'account.invoice': [
            'account_id',
            'amount_total_company_signed',
            'commercial_partner_id',
            'company_id',
            'branch_id',
            'currency_id',
            'date_due',
            'date_invoice',
            'fiscal_position_id',
            'journal_id',
            'partner_bank_id',
            'partner_id',
            'payment_term_id',
            'residual',
            'state',
            'type',
            'user_id',
        ],
        'account.invoice.line': [
            'account_id',
            'invoice_id',
            'price_subtotal',
            'product_id',
            'quantity',
            'uom_id',
            'account_analytic_id',
        ],
        'product.product': ['product_tmpl_id'],
        'product.template': ['categ_id'],
        'product.uom': ['category_id', 'factor', 'name', 'uom_type'],
        'res.currency.rate': ['currency_id', 'name'],
        'res.partner': ['country_id'],
    }

    def _select(self):
        select_str = """
            SELECT sub.id, sub.date, sub.product_id, sub.partner_id, sub.country_id, sub.account_analytic_id,
                sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id,
                sub.fiscal_position_id, sub.user_id, sub.company_id, sub.branch_id, sub.nbr, sub.type, sub.state,
                sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id,
                sub.product_qty, sub.price_total as price_total, sub.price_average as price_average,
                COALESCE(cr.rate, 1) as currency_rate, sub.residual as residual, sub.commercial_partner_id as commercial_partner_id
        """
        return select_str

    def _sub_select(self):
        select_str = """
                SELECT ail.id AS id,
                    ai.date_invoice AS date,
                    ail.product_id, ai.partner_id, ai.payment_term_id, ail.account_analytic_id,
                    u2.name AS uom_name,
                    ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id,
                    1 AS nbr,
                    ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id,
                    ai.partner_bank_id,
                    SUM ((invoice_type.sign * ail.quantity) / u.factor * u2.factor) AS product_qty,
                    SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total,
                    SUM(ABS(ail.price_subtotal_signed)) / CASE
                            WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric
                               THEN SUM(ail.quantity / u.factor * u2.factor)
                               ELSE 1::numeric
                            END AS price_average,
                    ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
                    count(*) * invoice_type.sign AS residual,
                    ai.commercial_partner_id as commercial_partner_id,
                    partner.country_id
        """
        return select_str

    def _from(self):
        from_str = """
                FROM account_invoice_line ail
                JOIN account_invoice ai ON ai.id = ail.invoice_id
                JOIN res_partner partner ON ai.commercial_partner_id = partner.id
                LEFT JOIN product_product pr ON pr.id = ail.product_id
                left JOIN product_template pt ON pt.id = pr.product_tmpl_id
                LEFT JOIN product_uom u ON u.id = ail.uom_id
                LEFT JOIN product_uom u2 ON u2.id = pt.uom_id
                JOIN (
                    -- Temporary table to decide if the qty should be added or retrieved (Invoice vs Credit Note)
                    SELECT id,(CASE
                         WHEN ai.type::text = ANY (ARRAY['in_refund'::character varying::text, 'in_invoice'::character varying::text])
                            THEN -1
                            ELSE 1
                        END) AS sign
                    FROM account_invoice ai
                ) AS invoice_type ON invoice_type.id = ai.id
        """
        return from_str

    def _group_by(self):
        group_by_str = """
                GROUP BY ail.id, ail.product_id, ail.account_analytic_id, ai.date_invoice, ai.id,
                    ai.partner_id, ai.payment_term_id, u2.name, u2.id, ai.currency_id, ai.journal_id,
                    ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id, ai.type, invoice_type.sign, ai.state, pt.categ_id,
                    ai.date_due, ai.account_id, ail.account_id, ai.partner_bank_id, ai.residual_company_signed,
                    ai.amount_total_company_signed, ai.commercial_partner_id, partner.country_id
        """
        return group_by_str

    @api.model_cr
    def init(self):
        # self._table = account_invoice_report
        tools.drop_view_if_exists(self.env.cr, self._table)
        self.env.cr.execute(
            """CREATE or REPLACE VIEW %s as (
            WITH currency_rate AS (%s)
            %s
            FROM (
                %s %s %s
            ) AS sub
            LEFT JOIN currency_rate cr ON
                (cr.currency_id = sub.currency_id AND
                 cr.company_id = sub.company_id AND
                 cr.date_start <= COALESCE(sub.date, NOW()) AND
                 (cr.date_end IS NULL OR cr.date_end > COALESCE(sub.date, NOW())))
        )""" %
            (self._table, self.env['res.currency']._select_companies_rates(),
             self._select(), self._sub_select(), self._from(),
             self._group_by()))
Exemple #6
0
class PurchaseRequisition(models.Model):
    _name = "purchase.requisition"
    _description = "Purchase Requisition"
    _inherit = ['mail.thread']
    _order = "id desc"

    def _get_picking_in(self):
        pick_in = self.env.ref('stock.picking_type_in')
        if not pick_in:
            company = self.env['res.company']._company_default_get(
                'purchase.requisition')
            pick_in = self.env['stock.picking.type'].search(
                [('warehouse_id.company_id', '=', company.id),
                 ('code', '=', 'incoming')],
                limit=1,
            )
        return pick_in

    def _get_type_id(self):
        return self.env['purchase.requisition.type'].search([], limit=1)

    name = fields.Char(string='Agreement Reference',
                       required=True,
                       copy=False,
                       default=lambda self: self.env['ir.sequence'].
                       next_by_code('purchase.order.requisition'))
    origin = fields.Char(string='Source Document')
    order_count = fields.Integer(compute='_compute_orders_number',
                                 string='Number of Orders')
    vendor_id = fields.Many2one('res.partner', string="Vendor")
    type_id = fields.Many2one('purchase.requisition.type',
                              string="Agreement Type",
                              required=True,
                              default=_get_type_id)
    ordering_date = fields.Date(string="Ordering Date")
    date_end = fields.Datetime(string='Agreement Deadline')
    schedule_date = fields.Date(
        string='Delivery Date',
        index=True,
        help=
        "The expected and scheduled delivery date where all the products are received"
    )
    user_id = fields.Many2one('res.users',
                              string='Responsible',
                              default=lambda self: self.env.user)
    description = fields.Text()
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 required=True,
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('purchase.requisition'))
    purchase_ids = fields.One2many('purchase.order',
                                   'requisition_id',
                                   string='Purchase Orders',
                                   states={'done': [('readonly', True)]})
    line_ids = fields.One2many('purchase.requisition.line',
                               'requisition_id',
                               string='Products to Purchase',
                               states={'done': [('readonly', True)]},
                               copy=True)
    warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse')
    state = fields.Selection([('draft', 'Draft'), ('in_progress', 'Confirmed'),
                              ('open', 'Bid Selection'), ('done', 'Done'),
                              ('cancel', 'Cancelled')],
                             'Status',
                             track_visibility='onchange',
                             required=True,
                             copy=False,
                             default='draft')
    account_analytic_id = fields.Many2one('account.analytic.account',
                                          'Analytic Account')
    picking_type_id = fields.Many2one('stock.picking.type',
                                      'Operation Type',
                                      required=True,
                                      default=_get_picking_in)

    @api.multi
    @api.depends('purchase_ids')
    def _compute_orders_number(self):
        for requisition in self:
            requisition.order_count = len(requisition.purchase_ids)

    @api.multi
    def action_cancel(self):
        # try to set all associated quotations to cancel state
        for requisition in self:
            requisition.purchase_ids.button_cancel()
            for po in requisition.purchase_ids:
                po.message_post(body=_(
                    'Cancelled by the agreement associated to this quotation.')
                                )
        self.write({'state': 'cancel'})

    @api.multi
    def action_in_progress(self):
        if not all(obj.line_ids for obj in self):
            raise UserError(
                _('You cannot confirm call because there is no product line.'))
        self.write({'state': 'in_progress'})

    @api.multi
    def action_open(self):
        self.write({'state': 'open'})

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

    @api.multi
    def action_done(self):
        """
        Generate all purchase order based on selected lines, should only be called on one agreement at a time
        """
        if any(purchase_order.state in ['draft', 'sent', 'to approve']
               for purchase_order in self.mapped('purchase_ids')):
            raise UserError(
                _('You have to cancel or validate every RfQ before closing the purchase requisition.'
                  ))
        self.write({'state': 'done'})

    def _prepare_tender_values(self, product_id, product_qty, product_uom,
                               location_id, name, origin, values):
        return {
            'origin':
            origin,
            'date_end':
            values['date_planned'],
            'warehouse_id':
            values.get('warehouse_id') and values['warehouse_id'].id or False,
            'company_id':
            values['company_id'].id,
            'line_ids': [(0, 0, {
                'product_id':
                product_id.id,
                'product_uom_id':
                product_uom.id,
                'product_qty':
                product_qty,
                'move_dest_id':
                values.get('move_dest_ids') and values['move_dest_ids'][0].id
                or False,
            })],
        }
Exemple #7
0
class LunchOrderLine(models.Model):
    _name = 'lunch.order.line'
    _description = 'lunch order line'
    _order = 'date desc, id desc'

    name = fields.Char(related='product_id.name',
                       string="Product Name",
                       readonly=True)
    order_id = fields.Many2one('lunch.order',
                               'Order',
                               ondelete='cascade',
                               required=True)
    product_id = fields.Many2one('lunch.product', 'Product', required=True)
    category_id = fields.Many2one('lunch.product.category',
                                  string='Product Category',
                                  related='product_id.category_id',
                                  readonly=True,
                                  store=True)
    date = fields.Date(string='Date',
                       related='order_id.date',
                       readonly=True,
                       store=True)
    supplier = fields.Many2one('res.partner',
                               string='Vendor',
                               related='product_id.supplier',
                               readonly=True,
                               store=True)
    user_id = fields.Many2one('res.users',
                              string='User',
                              related='order_id.user_id',
                              readonly=True,
                              store=True)
    note = fields.Text('Note')
    price = fields.Float(related='product_id.price',
                         readonly=True,
                         store=True,
                         digits=dp.get_precision('Account'))
    state = fields.Selection([('new', 'New'), ('confirmed', 'Received'),
                              ('ordered', 'Ordered'),
                              ('cancelled', 'Cancelled')],
                             'Status',
                             readonly=True,
                             index=True,
                             default='new')
    cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move')
    currency_id = fields.Many2one('res.currency',
                                  related='order_id.currency_id')

    @api.one
    def order(self):
        """
        The order_line is ordered to the vendor but isn't received yet
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'ordered'
        else:
            raise AccessError(
                _("Only your lunch manager processes the orders."))

    @api.one
    def confirm(self):
        """
        confirm one or more order line, update order status and create new cashmove
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            if self.state != 'confirmed':
                values = {
                    'user_id': self.user_id.id,
                    'amount': -self.price,
                    'description': self.product_id.name,
                    'order_id': self.id,
                    'state': 'order',
                    'date': self.date,
                }
                self.env['lunch.cashmove'].create(values)
                self.state = 'confirmed'
        else:
            raise AccessError(
                _("Only your lunch manager sets the orders as received."))

    @api.one
    def cancel(self):
        """
        cancel one or more order.line, update order status and unlink existing cashmoves
        """
        if self.user_has_groups("lunch.group_lunch_manager"):
            self.state = 'cancelled'
            self.cashmove.unlink()
        else:
            raise AccessError(_("Only your lunch manager cancels the orders."))
class ProductAttribute(models.Model):
    _inherit = 'product.attribute'

    category_id = fields.Many2one('product.attribute.category', string="Category",
                                  help="Set a category to regroup similar attributes under "
                                  "the same section in the Comparison page of eCommerce")
Exemple #9
0
class PickingType(models.Model):
    _name = "stock.picking.type"
    _description = "The operation type determines the picking view"
    _order = 'sequence, id'

    name = fields.Char('Operation Types Name', required=True, translate=True)
    color = fields.Integer('Color')
    sequence = fields.Integer('Sequence', help="Used to order the 'All Operations' kanban view")
    sequence_id = fields.Many2one('ir.sequence', 'Reference Sequence', required=True)
    default_location_src_id = fields.Many2one(
        'stock.location', 'Default Source Location',
        help="This is the default source location when you create a picking manually with this operation type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the supplier location on the partner. ")
    default_location_dest_id = fields.Many2one(
        'stock.location', 'Default Destination Location',
        help="This is the default destination location when you create a picking manually with this operation type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the customer location on the partner. ")
    code = fields.Selection([('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True)
    return_picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type for Returns')
    show_entire_packs = fields.Boolean('Allow moving packs', help="If checked, this shows the packs to be moved as a whole in the Operations tab all the time, even if there was no entire pack reserved.")
    warehouse_id = fields.Many2one(
        'stock.warehouse', 'Warehouse', ondelete='cascade',
        default=lambda self: self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1))
    active = fields.Boolean('Active', default=True)
    use_create_lots = fields.Boolean(
        'Create New Lots/Serial Numbers', default=True,
        help="If this is checked only, it will suppose you want to create new Lots/Serial Numbers, so you can provide them in a text field. ")
    use_existing_lots = fields.Boolean(
        'Use Existing Lots/Serial Numbers', default=True,
        help="If this is checked, you will be able to choose the Lots/Serial Numbers. You can also decide to not put lots in this operation type.  This means it will create stock with no lot or not put a restriction on the lot taken. ")
    show_operations = fields.Boolean(
        'Show Detailed Operations', default=False,
        help="If this checkbox is ticked, the pickings lines will represent detailed stock operations. If not, the picking lines will represent an aggregate of detailed stock operations.")
    show_reserved = fields.Boolean(
        'Show Reserved', default=True, help="If this checkbox is ticked, actpy will show which products are reserved (lot/serial number, source location, source package).")

    # Statistics for the kanban view
    last_done_picking = fields.Char('Last 10 Done Pickings', compute='_compute_last_done_picking')
    count_picking_draft = fields.Integer(compute='_compute_picking_count')
    count_picking_ready = fields.Integer(compute='_compute_picking_count')
    count_picking = fields.Integer(compute='_compute_picking_count')
    count_picking_waiting = fields.Integer(compute='_compute_picking_count')
    count_picking_late = fields.Integer(compute='_compute_picking_count')
    count_picking_backorders = fields.Integer(compute='_compute_picking_count')
    rate_picking_late = fields.Integer(compute='_compute_picking_count')
    rate_picking_backorders = fields.Integer(compute='_compute_picking_count')

    barcode_nomenclature_id = fields.Many2one(
        'barcode.nomenclature', 'Barcode Nomenclature')

    @api.one
    def _compute_last_done_picking(self):
        # TDE TODO: true multi
        tristates = []
        for picking in self.env['stock.picking'].search([('picking_type_id', '=', self.id), ('state', '=', 'done')], order='date_done desc', limit=10):
            if picking.date_done > picking.date:
                tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Late'), 'value': -1})
            elif picking.backorder_id:
                tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Backorder exists'), 'value': 0})
            else:
                tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('OK'), 'value': 1})
        self.last_done_picking = json.dumps(tristates)

    def _compute_picking_count(self):
        # TDE TODO count picking can be done using previous two
        domains = {
            'count_picking_draft': [('state', '=', 'draft')],
            'count_picking_waiting': [('state', 'in', ('confirmed', 'waiting'))],
            'count_picking_ready': [('state', '=', 'assigned')],
            'count_picking': [('state', 'in', ('assigned', 'waiting', 'confirmed'))],
            'count_picking_late': [('scheduled_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed'))],
            'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting'))],
        }
        for field in domains:
            data = self.env['stock.picking'].read_group(domains[field] +
                [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)],
                ['picking_type_id'], ['picking_type_id'])
            count = {
                x['picking_type_id'][0]: x['picking_type_id_count']
                for x in data if x['picking_type_id']
            }
            for record in self:
                record[field] = count.get(record.id, 0)
        for record in self:
            record.rate_picking_late = record.count_picking and record.count_picking_late * 100 / record.count_picking or 0
            record.rate_picking_backorders = record.count_picking and record.count_picking_backorders * 100 / record.count_picking or 0

    def name_get(self):
        """ Display 'Warehouse_name: PickingType_name' """
        # TDE TODO remove context key support + update purchase
        res = []
        for picking_type in self:
            if self.env.context.get('special_shortened_wh_name'):
                if picking_type.warehouse_id:
                    name = picking_type.warehouse_id.name
                else:
                    name = _('Customer') + ' (' + picking_type.name + ')'
            elif picking_type.warehouse_id:
                name = picking_type.warehouse_id.name + ': ' + picking_type.name
            else:
                name = picking_type.name
            res.append((picking_type.id, name))
        return res

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

    @api.onchange('code')
    def onchange_picking_code(self):
        if self.code == 'incoming':
            self.default_location_src_id = self.env.ref('stock.stock_location_suppliers').id
            self.default_location_dest_id = self.env.ref('stock.stock_location_stock').id
        elif self.code == 'outgoing':
            self.default_location_src_id = self.env.ref('stock.stock_location_stock').id
            self.default_location_dest_id = self.env.ref('stock.stock_location_customers').id

    @api.onchange('show_operations')
    def onchange_show_operations(self):
        if self.show_operations is True:
            self.show_reserved = True

    def _get_action(self, action_xmlid):
        # TDE TODO check to have one view + custo in methods
        action = self.env.ref(action_xmlid).read()[0]
        if self:
            action['display_name'] = self.display_name
        return action

    def get_action_picking_tree_late(self):
        return self._get_action('stock.action_picking_tree_late')

    def get_action_picking_tree_backorder(self):
        return self._get_action('stock.action_picking_tree_backorder')

    def get_action_picking_tree_waiting(self):
        return self._get_action('stock.action_picking_tree_waiting')

    def get_action_picking_tree_ready(self):
        return self._get_action('stock.action_picking_tree_ready')

    def get_stock_picking_action_picking_type(self):
        return self._get_action('stock.stock_picking_action_picking_type')
Exemple #10
0
class MrpBom(models.Model):
    """ Defines bills of material for a product or a product template """
    _name = 'mrp.bom'
    _description = 'Bill of Material'
    _inherit = ['mail.thread']
    _rec_name = 'product_tmpl_id'
    _order = "sequence"

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

    code = fields.Char('Reference')
    active = fields.Boolean(
        'Active',
        default=True,
        help=
        "If the active field is set to False, it will allow you to hide the bills of material without removing it."
    )
    type = fields.Selection([('normal', 'Manufacture this product'),
                             ('phantom', 'Kit')],
                            'BoM Type',
                            default='normal',
                            required=True)
    product_tmpl_id = fields.Many2one(
        'product.template',
        'Product',
        domain="[('type', 'in', ['product', 'consu'])]",
        required=True)
    product_id = fields.Many2one(
        'product.product',
        'Product Variant',
        domain=
        "['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu'])]",
        help=
        "If a product variant is defined the BOM is available only for this product."
    )
    bom_line_ids = fields.One2many('mrp.bom.line',
                                   'bom_id',
                                   'BoM Lines',
                                   copy=True)
    product_qty = fields.Float('Quantity',
                               default=1.0,
                               digits=dp.get_precision('Unit of Measure'),
                               required=True)
    product_uom_id = fields.Many2one(
        'product.uom',
        'Product Unit of Measure',
        default=_get_default_product_uom_id,
        oldname='product_uom',
        required=True,
        help=
        "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"
    )
    sequence = fields.Integer(
        'Sequence',
        help=
        "Gives the sequence order when displaying a list of bills of material."
    )
    routing_id = fields.Many2one(
        'mrp.routing',
        'Routing',
        help=
        "The operations for producing this BoM.  When a routing is specified, the production orders will "
        " be executed through work orders, otherwise everything is processed in the production order itself. "
    )
    ready_to_produce = fields.Selection(
        [('all_available', 'All components available'),
         ('asap', 'The components of 1st operation')],
        string='Manufacturing Readiness',
        default='asap',
        required=True)
    picking_type_id = fields.Many2one(
        'stock.picking.type',
        'Operation Type',
        domain=[('code', '=', 'mrp_operation')],
        help=
        u"When a procurement has a ‘produce’ route with a operation type set, it will try to create "
        "a Manufacturing Order for that product using a BoM of the same operation type. That allows "
        "to define procurement rules which trigger different manufacturing orders with different BoMs."
    )
    company_id = fields.Many2one('res.company',
                                 'Company',
                                 default=lambda self: self.env['res.company'].
                                 _company_default_get('mrp.bom'),
                                 required=True)

    @api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids')
    def _check_product_recursion(self):
        for bom in self:
            if bom.bom_line_ids.filtered(lambda x: x.product_id.product_tmpl_id
                                         == bom.product_tmpl_id):
                raise ValidationError(
                    _('BoM line product %s should not be same as BoM product.')
                    % bom.display_name)

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

    @api.onchange('product_tmpl_id')
    def onchange_product_tmpl_id(self):
        if self.product_tmpl_id:
            self.product_uom_id = self.product_tmpl_id.uom_id.id

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

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

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

    @api.model
    def _bom_find(self,
                  product_tmpl=None,
                  product=None,
                  picking_type=None,
                  company_id=False):
        """ Finds BoM for particular product, picking and company """
        if product:
            if not product_tmpl:
                product_tmpl = product.product_tmpl_id
            domain = [
                '|', ('product_id', '=', product.id), '&',
                ('product_id', '=', False),
                ('product_tmpl_id', '=', product_tmpl.id)
            ]
        elif product_tmpl:
            domain = [('product_tmpl_id', '=', product_tmpl.id)]
        else:
            # neither product nor template, makes no sense to search
            return False
        if picking_type:
            domain += [
                '|', ('picking_type_id', '=', picking_type.id),
                ('picking_type_id', '=', False)
            ]
        if company_id or self.env.context.get('company_id'):
            domain = domain + [('company_id', '=', company_id
                                or self.env.context.get('company_id'))]
        # order to prioritize bom with product_id over the one without
        return self.search(domain, order='sequence, product_id', limit=1)

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

        graph = defaultdict(list)
        V = set()

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

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

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

            if current_line._skip_bom_line(current_product):
                continue

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

        return boms_done, lines_done
Exemple #11
0
class BarnchConfiguration(models.TransientModel):
    _name = 'branch.config'

    name = fields.Char(string='Name', required=True)
    code = fields.Char(string='Code', required=True)
    branch_id = fields.Many2one('res.branch', 'Branch')
    company_id = fields.Many2one('res.company', string="Company",
        default=lambda self: self.env.user.company_id, required=True)
    partner_id = fields.Many2one('res.partner', string='Partner')
    street = fields.Char()
    street2 = fields.Char()
    zip = fields.Char(change_default=True)
    city = fields.Char()
    state_id = fields.Many2one("res.country.state", string='State',
                               ondelete='restrict')
    country_id = fields.Many2one('res.country', string='Country',
                                 ondelete='restrict')
    email = fields.Char()
    phone = fields.Char()
    mobile = fields.Char()
    state = fields.Selection([('draft', 'Draft'), ('confirm', 'Confirm')],
        default='draft')
    user_ids = fields.Many2many('res.users', 'res_users_branch_rel',
                                 'user_id', 'branch_id', 'Allowed Branch for users',
                                 domain="[('company_id','=',company_id)]")
    default_user_ids = fields.Many2many('res.users', 'res_users_branch_default_rel',
                                 'user_id', 'branch_id', 'Default Branch for users',
                                 domain="[('company_id','=',company_id)]")

    @api.multi
    def branch_config(self):
        s_ids = self.search_read([('id', '=', self.id)], [])[0]
        branch = self.env['res.branch'].create({
            'name': s_ids['name'],
            'code': s_ids['code'],
            'street': s_ids['street'],
            'street2': s_ids['street2'],
            'zip': s_ids['zip'],
            'city': s_ids['city'],
            'state_id': s_ids['state_id'] and s_ids['state_id'][0],
            'country_id': s_ids['country_id'] and s_ids['country_id'][0],
            'email': s_ids['email'],
            'phone': s_ids['phone'],
            'company_id': s_ids['company_id'] and s_ids['company_id'][0],
            'mobile': s_ids['mobile'],
        })
        self.write({'state': 'confirm',
                    'partner_id': branch.partner_id.id,
                    'branch_id': branch.id})
        view_id = self.env.ref(
            'base_branch_company.view_branch_config')
        context = dict(self._context)
        return {'views': [(view_id.id, 'form')], 'view_id': view_id.id,
                'type': 'ir.actions.act_window', 'view_type': 'form',
                'view_mode': 'form', 'res_model': 'branch.config',
                'target': 'new', 'res_id': self.id, 'context': context, }


    @api.multi
    def finish_branch_config(self):
        for user_id in self.user_ids:
            user_id.write({'branch_ids': [(4, self.branch_id.id)]})
        for user_id in self.user_ids:
            user_id.write({'default_branch_id': self.branch_id.id})
Exemple #12
0
class MrpBomLine(models.Model):
    _name = 'mrp.bom.line'
    _order = "sequence, id"
    _rec_name = "product_id"

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

    product_id = fields.Many2one('product.product', 'Product', required=True)
    product_qty = fields.Float(
        'Product Quantity',
        default=1.0,
        digits=dp.get_precision('Product Unit of Measure'),
        required=True)
    product_uom_id = fields.Many2one(
        'product.uom',
        'Product Unit of Measure',
        default=_get_default_product_uom_id,
        oldname='product_uom',
        required=True,
        help=
        "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control"
    )
    sequence = fields.Integer('Sequence',
                              default=1,
                              help="Gives the sequence order when displaying.")
    routing_id = fields.Many2one(
        'mrp.routing',
        'Routing',
        related='bom_id.routing_id',
        store=True,
        help=
        "The list of operations to produce the finished product. The routing is mainly used to "
        "compute work center costs during operations and to plan future loads on work centers "
        "based on production planning.")
    bom_id = fields.Many2one('mrp.bom',
                             'Parent BoM',
                             index=True,
                             ondelete='cascade',
                             required=True)
    attribute_value_ids = fields.Many2many(
        'product.attribute.value',
        string='Variants',
        help="BOM Product Variants needed form apply this line.")
    operation_id = fields.Many2one(
        'mrp.routing.workcenter',
        'Consumed in Operation',
        help=
        "The operation where the components are consumed, or the finished products created."
    )
    child_bom_id = fields.Many2one('mrp.bom',
                                   'Sub BoM',
                                   compute='_compute_child_bom_id')
    child_line_ids = fields.One2many('mrp.bom.line',
                                     string="BOM lines of the referred bom",
                                     compute='_compute_child_line_ids')
    has_attachments = fields.Boolean('Has Attachments',
                                     compute='_compute_has_attachments')

    _sql_constraints = [
        ('bom_qty_zero', 'CHECK (product_qty>=0)',
         'All product quantities must be greater or equal to 0.\n'
         'Lines with 0 quantities can be used as optional lines. \n'
         'You should install the mrp_byproduct module if you want to manage extra products on BoMs !'
         ),
    ]

    @api.one
    @api.depends('product_id', 'bom_id')
    def _compute_child_bom_id(self):
        if not self.product_id:
            self.child_bom_id = False
        else:
            self.child_bom_id = self.env['mrp.bom']._bom_find(
                product_tmpl=self.product_id.product_tmpl_id,
                product=self.product_id,
                picking_type=self.bom_id.picking_type_id)

    @api.one
    @api.depends('product_id')
    def _compute_has_attachments(self):
        nbr_attach = self.env['ir.attachment'].search_count([
            '|', '&', ('res_model', '=', 'product.product'),
            ('res_id', '=', self.product_id.id), '&',
            ('res_model', '=', 'product.template'),
            ('res_id', '=', self.product_id.product_tmpl_id.id)
        ])
        self.has_attachments = bool(nbr_attach)

    @api.one
    @api.depends('child_bom_id')
    def _compute_child_line_ids(self):
        """ If the BOM line refers to a BOM, return the ids of the child BOM lines """
        self.child_line_ids = self.child_bom_id.bom_line_ids.ids

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

    @api.onchange('product_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, values):
        if 'product_id' in values and 'product_uom_id' not in values:
            values['product_uom_id'] = self.env['product.product'].browse(
                values['product_id']).uom_id.id
        return super(MrpBomLine, self).create(values)

    def _skip_bom_line(self, product):
        """ Control if a BoM line should be produce, can be inherited for add
        custom control. It currently checks that all variant values are in the
        product. """
        if self.attribute_value_ids:
            if not product or self.attribute_value_ids - product.attribute_value_ids:
                return True
        return False

    @api.multi
    def action_see_attachments(self):
        domain = [
            '|', '&', ('res_model', '=', 'product.product'),
            ('res_id', '=', self.product_id.id), '&',
            ('res_model', '=', 'product.template'),
            ('res_id', '=', self.product_id.product_tmpl_id.id)
        ]
        attachment_view = self.env.ref('mrp.view_document_file_kanban_mrp')
        return {
            'name':
            _('Attachments'),
            'domain':
            domain,
            'res_model':
            'mrp.document',
            'type':
            'ir.actions.act_window',
            'view_id':
            attachment_view.id,
            'views': [(attachment_view.id, 'kanban'), (False, 'form')],
            'view_mode':
            'kanban,tree,form',
            'view_type':
            'form',
            'help':
            _('''<p class="oe_view_nocontent_create">
                        Click to upload files to your product.
                    </p><p>
                        Use this feature to store any files, like drawings or specifications.
                    </p>'''),
            'limit':
            80,
            'context':
            "{'default_res_model': '%s','default_res_id': %d}" %
            ('product.product', self.product_id.id)
        }
class MergeOpportunity(models.TransientModel):
    """
        Merge opportunities together.
        If we're talking about opportunities, it's just because it makes more sense
        to merge opps than leads, because the leads are more ephemeral objects.
        But since opportunities are leads, it's also possible to merge leads
        together (resulting in a new lead), or leads and opps together (resulting
        in a new opp).
    """

    _name = 'crm.merge.opportunity'
    _description = 'Merge opportunities'

    @api.model
    def default_get(self, fields):
        """ Use active_ids from the context to fetch the leads/opps to merge.
            In order to get merged, these leads/opps can't be in 'Dead' or 'Closed'
        """
        record_ids = self._context.get('active_ids')
        result = super(MergeOpportunity, self).default_get(fields)

        if record_ids:
            if 'opportunity_ids' in fields:
                opp_ids = self.env['crm.lead'].browse(record_ids).filtered(
                    lambda opp: opp.probability < 100).ids
                result['opportunity_ids'] = opp_ids

        return result

    opportunity_ids = fields.Many2many('crm.lead',
                                       'merge_opportunity_rel',
                                       'merge_id',
                                       'opportunity_id',
                                       string='Leads/Opportunities')
    user_id = fields.Many2one('res.users', 'Salesperson', index=True)
    team_id = fields.Many2one('crm.team',
                              'Sales Channel',
                              oldname='section_id',
                              index=True)

    @api.multi
    def action_merge(self):
        self.ensure_one()
        merge_opportunity = self.opportunity_ids.merge_opportunity(
            self.user_id.id, self.team_id.id)

        # The newly created lead might be a lead or an opp: redirect toward the right view
        if merge_opportunity.type == 'opportunity':
            return merge_opportunity.redirect_opportunity_view()
        else:
            return merge_opportunity.redirect_lead_view()

    @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. """
        team_id = False
        if self.user_id:
            user_in_team = False
            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)
                ])
            if not user_in_team:
                team_id = self.env['crm.team'].search([
                    '|', ('user_id', '=', self.user_id.id),
                    ('member_ids', '=', self.user_id.id)
                ],
                                                      limit=1)
        self.team_id = team_id
Exemple #14
0
class Lead2OpportunityPartner(models.TransientModel):

    _name = 'crm.lead2opportunity.partner'
    _description = 'Lead To Opportunity Partner'
    _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 Channel',
                              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)
Exemple #15
0
class PurchaseRequisitionLine(models.Model):
    _name = "purchase.requisition.line"
    _description = "Purchase Requisition Line"
    _rec_name = 'product_id'

    product_id = fields.Many2one('product.product',
                                 string='Product',
                                 domain=[('purchase_ok', '=', True)],
                                 required=True)
    product_uom_id = fields.Many2one('product.uom',
                                     string='Product Unit of Measure')
    product_qty = fields.Float(
        string='Quantity', digits=dp.get_precision('Product Unit of Measure'))
    price_unit = fields.Float(string='Unit Price',
                              digits=dp.get_precision('Product Price'))
    qty_ordered = fields.Float(compute='_compute_ordered_qty',
                               string='Ordered Quantities')
    requisition_id = fields.Many2one('purchase.requisition',
                                     string='Purchase Agreement',
                                     ondelete='cascade')
    company_id = fields.Many2one(
        'res.company',
        related='requisition_id.company_id',
        string='Company',
        store=True,
        readonly=True,
        default=lambda self: self.env['res.company']._company_default_get(
            'purchase.requisition.line'))
    account_analytic_id = fields.Many2one('account.analytic.account',
                                          string='Analytic Account')
    schedule_date = fields.Date(string='Scheduled Date')
    move_dest_id = fields.Many2one('stock.move', 'Downstream Move')

    @api.multi
    @api.depends('requisition_id.purchase_ids.state')
    def _compute_ordered_qty(self):
        for line in self:
            total = 0.0
            for po in line.requisition_id.purchase_ids.filtered(
                    lambda purchase_order: purchase_order.state in
                ['purchase', 'done']):
                for po_line in po.order_line.filtered(
                        lambda order_line: order_line.product_id == line.
                        product_id):
                    if po_line.product_uom != line.product_uom_id:
                        total += po_line.product_uom._compute_quantity(
                            po_line.product_qty, line.product_uom_id)
                    else:
                        total += po_line.product_qty
            line.qty_ordered = total

    @api.onchange('product_id')
    def _onchange_product_id(self):
        if self.product_id:
            self.product_uom_id = self.product_id.uom_id
            self.product_qty = 1.0
        if not self.account_analytic_id:
            self.account_analytic_id = self.requisition_id.account_analytic_id
        if not self.schedule_date:
            self.schedule_date = self.requisition_id.schedule_date

    @api.multi
    def _prepare_purchase_order_line(self,
                                     name,
                                     product_qty=0.0,
                                     price_unit=0.0,
                                     taxes_ids=False):
        self.ensure_one()
        requisition = self.requisition_id
        return {
            'name':
            name,
            'product_id':
            self.product_id.id,
            'product_uom':
            self.product_id.uom_po_id.id,
            'product_qty':
            product_qty,
            'price_unit':
            price_unit,
            'taxes_id': [(6, 0, taxes_ids)],
            'date_planned':
            requisition.schedule_date or fields.Date.today(),
            'account_analytic_id':
            self.account_analytic_id.id,
            'move_dest_ids':
            self.move_dest_id and [(4, self.move_dest_id.id)] or []
        }
Exemple #16
0
class Picking(models.Model):
    _name = "stock.picking"
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = "Transfer"
    _order = "priority desc, date asc, id desc"

    name = fields.Char(
        'Reference', default='/',
        copy=False,  index=True,
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
    origin = fields.Char(
        'Source Document', index=True,
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
        help="Reference of the document")
    note = fields.Text('Notes')

    backorder_id = fields.Many2one(
        'stock.picking', 'Back Order of',
        copy=False, index=True,
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
        help="If this shipment was split, then this field links to the shipment which contains the already processed part.")

    move_type = fields.Selection([
        ('direct', 'As soon as possible'), ('one', 'When all products are ready')], 'Shipping Policy',
        default='direct', required=True,
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
        help="It specifies goods to be deliver partially or all at once")

    state = fields.Selection([
        ('draft', 'Draft'),
        ('waiting', 'Waiting Another Operation'),
        ('confirmed', 'Waiting'),
        ('assigned', 'Ready'),
        ('done', 'Done'),
        ('cancel', 'Cancelled'),
    ], string='Status', compute='_compute_state',
        copy=False, index=True, readonly=True, store=True, track_visibility='onchange',
        help=" * Draft: not confirmed yet and will not be scheduled until confirmed.\n"
             " * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows).\n"
             " * Waiting: if it is not ready to be sent because the required products could not be reserved.\n"
             " * Ready: products are reserved and ready to be sent. If the shipping policy is 'As soon as possible' this happens as soon as anything is reserved.\n"
             " * Done: has been processed, can't be modified or cancelled anymore.\n"
             " * Cancelled: has been cancelled, can't be confirmed anymore.")

    group_id = fields.Many2one(
        'procurement.group', 'Procurement Group',
        readonly=True, related='move_lines.group_id', store=True)

    priority = fields.Selection(
        PROCUREMENT_PRIORITIES, string='Priority',
        compute='_compute_priority', inverse='_set_priority', store=True,
        # default='1', required=True,  # TDE: required, depending on moves ? strange
        index=True, track_visibility='onchange',
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
        help="Priority for this picking. Setting manually a value here would set it as priority for all the moves")
    scheduled_date = fields.Datetime(
        'Scheduled Date', compute='_compute_scheduled_date', inverse='_set_scheduled_date', store=True,
        index=True, track_visibility='onchange',
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
        help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.")
    date = fields.Datetime(
        'Creation Date',
        default=fields.Datetime.now, index=True, track_visibility='onchange',
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
        help="Creation Date, usually the time of the order")
    date_done = fields.Datetime('Date of Transfer', copy=False, readonly=True, help="Completion Date of Transfer")

    location_id = fields.Many2one(
        'stock.location', "Source Location",
        default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_src_id,
        readonly=True, required=True,
        states={'draft': [('readonly', False)]})
    location_dest_id = fields.Many2one(
        'stock.location', "Destination Location",
        default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_dest_id,
        readonly=True, required=True,
        states={'draft': [('readonly', False)]})
    move_lines = fields.One2many('stock.move', 'picking_id', string="Stock Moves", copy=True)
    has_scrap_move = fields.Boolean(
        'Has Scrap Moves', compute='_has_scrap_move')
    picking_type_id = fields.Many2one(
        'stock.picking.type', 'Operation Type',
        required=True,
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
    picking_type_code = fields.Selection([
        ('incoming', 'Vendors'),
        ('outgoing', 'Customers'),
        ('internal', 'Internal')], related='picking_type_id.code',
        readonly=True)
    picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs',
        readonly=True)

    partner_id = fields.Many2one(
        'res.partner', 'Partner',
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})
    company_id = fields.Many2one(
        'res.company', 'Company',
        default=lambda self: self.env['res.company']._company_default_get('stock.picking'),
        index=True, required=True,
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]})

    branch_id = fields.Many2one('res.branch', 'Branch', ondelete="restrict",
                                default=lambda self: self.env['res.users']._get_default_branch(),
                                states={'done': [('readonly', True)],
                                        'cancel': [('readonly', True)]})

    move_line_ids = fields.One2many('stock.move.line', 'picking_id', 'Operations')

    move_line_exist = fields.Boolean(
        'Has Pack Operations', compute='_compute_move_line_exist',
        help='Check the existence of pack operation on the picking')

    has_packages = fields.Boolean(
        'Has Packages', compute='_compute_has_packages',
        help='Check the existence of destination packages on move lines')

    entire_package_ids = fields.One2many('stock.quant.package', compute='_compute_entire_package_ids',
                                         help='Those are the entire packages of a picking shown in the view of operations')
    entire_package_detail_ids = fields.One2many('stock.quant.package', compute='_compute_entire_package_ids',
                                                help='Those are the entire packages of a picking shown in the view of detailed operations')

    show_check_availability = fields.Boolean(
        compute='_compute_show_check_availability',
        help='Technical field used to compute whether the check availability button should be shown.')
    show_mark_as_todo = fields.Boolean(
        compute='_compute_show_mark_as_todo',
        help='Technical field used to compute whether the mark as todo button should be shown.')
    show_validate = fields.Boolean(
        compute='_compute_show_validate',
        help='Technical field used to compute whether the validate should be shown.')

    owner_id = fields.Many2one(
        'res.partner', 'Owner',
        states={'done': [('readonly', True)], 'cancel': [('readonly', True)]},
        help="Default Owner")
    printed = fields.Boolean('Printed')
    is_locked = fields.Boolean(default=True, help='When the picking is not done this allows changing the '
                               'initial demand. When the picking is done this allows '
                               'changing the done quantities.')
    # Used to search on pickings
    product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id')
    show_operations = fields.Boolean(compute='_compute_show_operations')
    show_lots_text = fields.Boolean(compute='_compute_show_lots_text')
    has_tracking = fields.Boolean(compute='_compute_has_tracking')

    _sql_constraints = [
        ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'),
    ]

    def _compute_has_tracking(self):
        for picking in self:
            picking.has_tracking = any(m.has_tracking != 'none' for m in picking.move_lines)

    @api.depends('picking_type_id.show_operations')
    def _compute_show_operations(self):
        for picking in self:
            if self.env.context.get('force_detailed_view'):
                picking.show_operations = True
                break
            if picking.picking_type_id.show_operations:
                if (picking.state == 'draft' and not self.env.context.get('planned_picking')) or picking.state != 'draft':
                    picking.show_operations = True
                else:
                    picking.show_operations = False
            else:
                picking.show_operations = False

    @api.depends('move_line_ids', 'picking_type_id.use_create_lots', 'picking_type_id.use_existing_lots', 'state')
    def _compute_show_lots_text(self):
        group_production_lot_enabled = self.user_has_groups('stock.group_production_lot')
        for picking in self:
            if not picking.move_line_ids:
                picking.show_lots_text = False
            elif group_production_lot_enabled and picking.picking_type_id.use_create_lots \
                    and not picking.picking_type_id.use_existing_lots and picking.state != 'done':
                picking.show_lots_text = True
            else:
                picking.show_lots_text = False

    @api.depends('move_type', 'move_lines.state', 'move_lines.picking_id')
    @api.one
    def _compute_state(self):
        ''' State of a picking depends on the state of its related stock.move
        - Draft: only used for "planned pickings"
        - Waiting: if the picking is not ready to be sent so if
          - (a) no quantity could be reserved at all or if
          - (b) some quantities could be reserved and the shipping policy is "deliver all at once"
        - Waiting another move: if the picking is waiting for another move
        - Ready: if the picking is ready to be sent so if:
          - (a) all quantities are reserved or if
          - (b) some quantities could be reserved and the shipping policy is "as soon as possible"
        - Done: if the picking is done.
        - Cancelled: if the picking is cancelled
        '''
        if not self.move_lines:
            self.state = 'draft'
        elif any(move.state == 'draft' for move in self.move_lines):  # TDE FIXME: should be all ?
            self.state = 'draft'
        elif all(move.state == 'cancel' for move in self.move_lines):
            self.state = 'cancel'
        elif all(move.state in ['cancel', 'done'] for move in self.move_lines):
            self.state = 'done'
        else:
            relevant_move_state = self.move_lines._get_relevant_state_among_moves()
            if relevant_move_state == 'partially_available':
                self.state = 'assigned'
            else:
                self.state = relevant_move_state

    @api.one
    @api.depends('move_lines.priority')
    def _compute_priority(self):
        if self.mapped('move_lines'):
            priorities = [priority for priority in self.mapped('move_lines.priority') if priority] or ['1']
            self.priority = max(priorities)
        else:
            self.priority = '1'

    @api.one
    def _set_priority(self):
        self.move_lines.write({'priority': self.priority})

    @api.one
    @api.depends('move_lines.date_expected')
    def _compute_scheduled_date(self):
        if self.move_type == 'direct':
            self.scheduled_date = min(self.move_lines.mapped('date_expected') or [fields.Datetime.now()])
        else:
            self.scheduled_date = max(self.move_lines.mapped('date_expected') or [fields.Datetime.now()])

    @api.one
    def _set_scheduled_date(self):
        self.move_lines.write({'date_expected': self.scheduled_date})

    @api.one
    def _has_scrap_move(self):
        # TDE FIXME: better implementation
        self.has_scrap_move = bool(self.env['stock.move'].search_count([('picking_id', '=', self.id), ('scrapped', '=', True)]))

    @api.one
    def _compute_move_line_exist(self):
        self.move_line_exist = bool(self.move_line_ids)

    @api.one
    def _compute_has_packages(self):
        self.has_packages = self.move_line_ids.filtered(lambda ml: ml.result_package_id)

    def _compute_entire_package_ids(self):
        """ This compute method populate the two one2Many containing all entire packages of the picking.
            An entire package is a package that is entirely reserved to be moved from a location to another one.
        """
        for picking in self:
            packages = self.env['stock.quant.package']
            packages_to_check = picking.move_line_ids\
                .filtered(lambda ml: ml.result_package_id and ml.package_id.id == ml.result_package_id.id)\
                .mapped('package_id')
            for package_to_check in packages_to_check:
                if picking.state in ('done', 'cancel') or picking._check_move_lines_map_quant_package(package_to_check):
                    packages |= package_to_check
            picking.entire_package_ids = packages
            picking.entire_package_detail_ids = packages

    @api.multi
    def _compute_show_check_availability(self):
        for picking in self:
            has_moves_to_reserve = any(
                move.state in ('waiting', 'confirmed', 'partially_available') and
                float_compare(move.product_uom_qty, 0, precision_rounding=move.product_uom.rounding)
                for move in picking.move_lines
            )
            picking.show_check_availability = picking.is_locked and picking.state in ('confirmed', 'waiting', 'assigned') and has_moves_to_reserve

    @api.multi
    @api.depends('state', 'move_lines')
    def _compute_show_mark_as_todo(self):
        for picking in self:
            if not picking.move_lines:
                picking.show_mark_as_todo = False
            elif self._context.get('planned_picking') and picking.state == 'draft':
                picking.show_mark_as_todo = True
            elif picking.state != 'draft' or not picking.id:
                picking.show_mark_as_todo = False
            else:
                picking.show_mark_as_todo = True

    @api.multi
    @api.depends('state', 'is_locked')
    def _compute_show_validate(self):
        for picking in self:
            if self._context.get('planned_picking') and picking.state == 'draft':
                picking.show_validate = False
            elif picking.state not in ('draft', 'waiting', 'confirmed', 'assigned') or not picking.is_locked:
                picking.show_validate = False
            else:
                picking.show_validate = True

    @api.onchange('picking_type_id', 'partner_id')
    def onchange_picking_type(self):
        if self.picking_type_id:
            if self.picking_type_id.default_location_src_id:
                location_id = self.picking_type_id.default_location_src_id.id
            elif self.partner_id:
                location_id = self.partner_id.property_stock_supplier.id
            else:
                customerloc, location_id = self.env['stock.warehouse']._get_partner_locations()

            if self.picking_type_id.default_location_dest_id:
                location_dest_id = self.picking_type_id.default_location_dest_id.id
            elif self.partner_id:
                location_dest_id = self.partner_id.property_stock_customer.id
            else:
                location_dest_id, supplierloc = self.env['stock.warehouse']._get_partner_locations()

            self.location_id = location_id
            self.location_dest_id = location_dest_id
        # TDE CLEANME move into onchange_partner_id
        if self.partner_id:
            if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id:
                partner = self.partner_id.parent_id
            elif self.partner_id.picking_warn not in ('no-message', 'block') and self.partner_id.parent_id.picking_warn == 'block':
                partner = self.partner_id.parent_id
            else:
                partner = self.partner_id
            if partner.picking_warn != 'no-message':
                if partner.picking_warn == 'block':
                    self.partner_id = False
                return {'warning': {
                    'title': ("Warning for %s") % partner.name,
                    'message': partner.picking_warn_msg
                }}

    @api.model
    def create(self, vals):
        # TDE FIXME: clean that brol
        defaults = self.default_get(['name', 'picking_type_id'])
        if vals.get('name', '/') == '/' and defaults.get('name', '/') == '/' and vals.get('picking_type_id', defaults.get('picking_type_id')):
            vals['name'] = self.env['stock.picking.type'].browse(vals.get('picking_type_id', defaults.get('picking_type_id'))).sequence_id.next_by_id()

        # TDE FIXME: what ?
        # As the on_change in one2many list is WIP, we will overwrite the locations on the stock moves here
        # As it is a create the format will be a list of (0, 0, dict)
        if vals.get('move_lines') and vals.get('location_id') and vals.get('location_dest_id'):
            for move in vals['move_lines']:
                if len(move) == 3:
                    move[2]['location_id'] = vals['location_id']
                    move[2]['location_dest_id'] = vals['location_dest_id']
        res = super(Picking, self).create(vals)
        res._autoconfirm_picking()
        return res

    @api.multi
    def write(self, vals):
        res = super(Picking, self).write(vals)
        # Change locations of moves if those of the picking change
        after_vals = {}
        if vals.get('location_id'):
            after_vals['location_id'] = vals['location_id']
        if vals.get('location_dest_id'):
            after_vals['location_dest_id'] = vals['location_dest_id']
        if after_vals:
            self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals)
        if vals.get('move_lines'):
            # Do not run autoconfirm if any of the moves has an initial demand. If an initial demand
            # is present in any of the moves, it means the picking was created through the "planned
            # transfer" mechanism.
            pickings_to_not_autoconfirm = self.env['stock.picking']
            for picking in self:
                if picking.state != 'draft':
                    continue
                for move in picking.move_lines:
                    if not float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding):
                        pickings_to_not_autoconfirm |= picking
                        break
            (self - pickings_to_not_autoconfirm)._autoconfirm_picking()
        return res

    @api.multi
    def unlink(self):
        self.mapped('move_lines')._action_cancel()
        self.mapped('move_lines').unlink() # Checks if moves are not done
        return super(Picking, self).unlink()

    # Actions
    # ----------------------------------------

    @api.one
    def action_assign_owner(self):
        self.move_line_ids.write({'owner_id': self.owner_id.id})

    @api.multi
    def do_print_picking(self):
        self.write({'printed': True})
        return self.env.ref('stock.action_report_picking').report_action(self)

    @api.multi
    def action_confirm(self):
        # call `_action_confirm` on every draft move
        self.mapped('move_lines')\
            .filtered(lambda move: move.state == 'draft')\
            ._action_confirm()
        # call `_action_assign` on every confirmed move which location_id bypasses the reservation
        self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production') and picking.state == 'confirmed')\
            .mapped('move_lines')._action_assign()
        if self.env.context.get('planned_picking') and len(self) == 1:
            action = self.env.ref('stock.action_picking_form')
            result = action.read()[0]
            result['res_id'] = self.id
            result['context'] = {
                'search_default_picking_type_id': [self.picking_type_id.id],
                'default_picking_type_id': self.picking_type_id.id,
                'contact_display': 'partner_address',
                'planned_picking': False,
            }
            return result
        else:
            return True

    @api.multi
    def action_assign(self):
        """ Check availability of picking moves.
        This has the effect of changing the state and reserve quants on available moves, and may
        also impact the state of the picking as it is computed based on move's states.
        @return: True
        """
        self.filtered(lambda picking: picking.state == 'draft').action_confirm()
        moves = self.mapped('move_lines').filtered(lambda move: move.state not in ('draft', 'cancel', 'done'))
        if not moves:
            raise UserError(_('Nothing to check the availability for.'))
        moves._action_assign()
        return True

    @api.multi
    def force_assign(self):
        """ Changes state of picking to available if moves are confirmed or waiting.
        @return: True
        """
        self.mapped('move_lines').filtered(lambda move: move.state in ['confirmed', 'waiting', 'partially_available'])._force_assign()
        return True

    @api.multi
    def action_cancel(self):
        self.mapped('move_lines')._action_cancel()
        self.write({'is_locked': True})
        return True

    @api.multi
    def action_done(self):
        """Changes picking state to done by processing the Stock Moves of the Picking

        Normally that happens when the button "Done" is pressed on a Picking view.
        @return: True
        """
        # TDE FIXME: remove decorator when migration the remaining
        todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'waiting', 'partially_available', 'assigned', 'confirmed'])
        # Check if there are ops not linked to moves yet
        for pick in self:
            # # Explode manually added packages
            # for ops in pick.move_line_ids.filtered(lambda x: not x.move_id and not x.product_id):
            #     for quant in ops.package_id.quant_ids: #Or use get_content for multiple levels
            #         self.move_line_ids.create({'product_id': quant.product_id.id,
            #                                    'package_id': quant.package_id.id,
            #                                    'result_package_id': ops.result_package_id,
            #                                    'lot_id': quant.lot_id.id,
            #                                    'owner_id': quant.owner_id.id,
            #                                    'product_uom_id': quant.product_id.uom_id.id,
            #                                    'product_qty': quant.qty,
            #                                    'qty_done': quant.qty,
            #                                    'location_id': quant.location_id.id, # Could be ops too
            #                                    'location_dest_id': ops.location_dest_id.id,
            #                                    'picking_id': pick.id
            #                                    }) # Might change first element
            # # Link existing moves or add moves when no one is related
            for ops in pick.move_line_ids.filtered(lambda x: not x.move_id):
                # Search move with this product
                moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id) 
                if moves: #could search move that needs it the most (that has some quantities left)
                    ops.move_id = moves[0].id
                else:
                    new_move = self.env['stock.move'].create({
                                                    'name': _('New Move:') + ops.product_id.display_name,
                                                    'product_id': ops.product_id.id,
                                                    'product_uom_qty': ops.qty_done,
                                                    'product_uom': ops.product_uom_id.id,
                                                    'location_id': pick.location_id.id,
                                                    'location_dest_id': pick.location_dest_id.id,
                                                    'picking_id': pick.id,
                                                   })
                    ops.move_id = new_move.id
                    new_move._action_confirm()
                    todo_moves |= new_move
                    #'qty_done': ops.qty_done})
        todo_moves._action_done()
        self.write({'date_done': fields.Datetime.now()})
        return True

    # Backward compatibility
    # Problem with fixed reference to a function:
    # it doesn't allow for overriding action_done() through do_transfer
    # get rid of me in master (and make me private ?)
    def do_transfer(self):
        return self.action_done()

    def _check_move_lines_map_quant_package(self, package):
        """ This method checks that all product of the package (quant) are well present in the move_line_ids of the picking. """
        all_in = True
        pack_move_lines = self.move_line_ids.filtered(lambda ml: ml.package_id == package)
        keys = ['product_id', 'lot_id']

        grouped_quants = {}
        for k, g in groupby(sorted(package.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)):
            grouped_quants[k] = sum(self.env['stock.quant'].concat(*list(g)).mapped('quantity'))

        grouped_ops = {}
        for k, g in groupby(sorted(pack_move_lines, key=itemgetter(*keys)), key=itemgetter(*keys)):
            grouped_ops[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty'))
        if any(grouped_quants.get(key, 0) - grouped_ops.get(key, 0) != 0 for key in grouped_quants) \
                or any(grouped_ops.get(key, 0) - grouped_quants.get(key, 0) != 0 for key in grouped_ops):
            all_in = False
        return all_in

    @api.multi
    def _check_entire_pack(self):
        """ This function check if entire packs are moved in the picking"""
        for picking in self:
            origin_packages = picking.move_line_ids.mapped("package_id")
            for pack in origin_packages:
                if picking._check_move_lines_map_quant_package(pack):
                    picking.move_line_ids.filtered(lambda ml: ml.package_id == pack).write({'result_package_id': pack.id})

    @api.multi
    def do_unreserve(self):
        for picking in self:
            picking.move_lines._do_unreserve()

    @api.multi
    def button_validate(self):
        self.ensure_one()
        if not self.move_lines and not self.move_line_ids:
            raise UserError(_('Please add some lines to move'))

        # If no lots when needed, raise error
        picking_type = self.picking_type_id
        no_quantities_done = all(float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
        no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids)
        if no_reserved_quantities and no_quantities_done:
            raise UserError(_('You cannot validate a transfer if you have not processed any quantity. You should rather cancel the transfer.'))

        if picking_type.use_create_lots or picking_type.use_existing_lots:
            lines_to_check = self.move_line_ids
            if not no_quantities_done:
                lines_to_check = lines_to_check.filtered(
                    lambda line: float_compare(line.qty_done, 0,
                                               precision_rounding=line.product_uom_id.rounding)
                )

            for line in lines_to_check:
                product = line.product_id
                if product and product.tracking != 'none':
                    if not line.lot_name and not line.lot_id:
                        raise UserError(_('You need to supply a lot/serial number for %s.') % product.display_name)
                    elif line.qty_done == 0:
                        raise UserError(_('You cannot validate a transfer if you have not processed any quantity for %s.') % product.display_name)

        if no_quantities_done:
            view = self.env.ref('stock.view_immediate_transfer')
            wiz = self.env['stock.immediate.transfer'].create({'pick_ids': [(4, self.id)]})
            return {
                'name': _('Immediate Transfer?'),
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'stock.immediate.transfer',
                'views': [(view.id, 'form')],
                'view_id': view.id,
                'target': 'new',
                'res_id': wiz.id,
                'context': self.env.context,
            }

        if self._get_overprocessed_stock_moves() and not self._context.get('skip_overprocessed_check'):
            view = self.env.ref('stock.view_overprocessed_transfer')
            wiz = self.env['stock.overprocessed.transfer'].create({'picking_id': self.id})
            return {
                'type': 'ir.actions.act_window',
                'view_type': 'form',
                'view_mode': 'form',
                'res_model': 'stock.overprocessed.transfer',
                'views': [(view.id, 'form')],
                'view_id': view.id,
                'target': 'new',
                'res_id': wiz.id,
                'context': self.env.context,
            }

        # Check backorder should check for other barcodes
        if self._check_backorder():
            return self.action_generate_backorder_wizard()
        self.action_done()
        return

    def action_generate_backorder_wizard(self):
        view = self.env.ref('stock.view_backorder_confirmation')
        wiz = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, p.id) for p in self]})
        return {
            'name': _('Create Backorder?'),
            'type': 'ir.actions.act_window',
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'stock.backorder.confirmation',
            'views': [(view.id, 'form')],
            'view_id': view.id,
            'target': 'new',
            'res_id': wiz.id,
            'context': self.env.context,
        }

    def action_toggle_is_locked(self):
        self.ensure_one()
        self.is_locked = not self.is_locked
        return True

    def _check_backorder(self):
        """ This method will loop over all the move lines of self and
        check if creating a backorder is necessary. This method is
        called during button_validate if the user has already processed
        some quantities and in the immediate transfer wizard that is
        displayed if the user has not processed any quantities.

        :return: True if a backorder is necessary else False
        """
        quantity_todo = {}
        quantity_done = {}
        for move in self.mapped('move_lines'):
            quantity_todo.setdefault(move.product_id.id, 0)
            quantity_done.setdefault(move.product_id.id, 0)
            quantity_todo[move.product_id.id] += move.product_uom_qty
            quantity_done[move.product_id.id] += move.quantity_done
        for ops in self.mapped('move_line_ids').filtered(lambda x: x.package_id and not x.product_id and not x.move_id):
            for quant in ops.package_id.quant_ids:
                quantity_done.setdefault(quant.product_id.id, 0)
                quantity_done[quant.product_id.id] += quant.qty
        for pack in self.mapped('move_line_ids').filtered(lambda x: x.product_id and not x.move_id):
            quantity_done.setdefault(pack.product_id.id, 0)
            quantity_done[pack.product_id.id] += pack.qty_done
        return any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done)

    @api.multi
    def _autoconfirm_picking(self):
        if not self._context.get('planned_picking'):
            for picking in self.filtered(lambda picking: picking.state not in ('done', 'cancel') and picking.move_lines):
                picking.action_confirm()

    def _get_overprocessed_stock_moves(self):
        self.ensure_one()
        return self.move_lines.filtered(
            lambda move: move.product_uom_qty != 0 and float_compare(move.quantity_done, move.product_uom_qty,
                                                                     precision_rounding=move.product_uom.rounding) == 1
        )

    @api.multi
    def _create_backorder(self, backorder_moves=[]):
        """ Move all non-done lines into a new backorder picking.
        """
        backorders = self.env['stock.picking']
        for picking in self:
            moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel'))
            if moves_to_backorder:
                backorder_picking = picking.copy({
                    'name': '/',
                    'move_lines': [],
                    'move_line_ids': [],
                    'backorder_id': picking.id
                })
                picking.message_post(
                    _('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % (
                        backorder_picking.id, backorder_picking.name))
                moves_to_backorder.write({'picking_id': backorder_picking.id})
                moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id})
                backorder_picking.action_assign()
                backorders |= backorder_picking
        return backorders

    def _put_in_pack(self):
        package = False
        for pick in self.filtered(lambda p: p.state not in ('done', 'cancel')):
            operations = pick.move_line_ids.filtered(lambda o: o.qty_done > 0 and not o.result_package_id)
            operation_ids = self.env['stock.move.line']
            if operations:
                package = self.env['stock.quant.package'].create({})
                for operation in operations:
                    if float_compare(operation.qty_done, operation.product_uom_qty, precision_rounding=operation.product_uom_id.rounding) >= 0:
                        operation_ids |= operation
                    else:
                        quantity_left_todo = float_round(
                            operation.product_uom_qty - operation.qty_done,
                            precision_rounding=operation.product_uom_id.rounding,
                            rounding_method='UP')
                        done_to_keep = operation.qty_done
                        new_operation = operation.copy(
                            default={'product_uom_qty': 0, 'qty_done': operation.qty_done})
                        operation.write({'product_uom_qty': quantity_left_todo, 'qty_done': 0.0})
                        new_operation.write({'product_uom_qty': done_to_keep})
                        operation_ids |= new_operation

                operation_ids.write({'result_package_id': package.id})
            else:
                raise UserError(_('Please process some quantities to put in the pack first!'))
        return package

    def put_in_pack(self):
        return self._put_in_pack()

    def button_scrap(self):
        self.ensure_one()
        products = self.env['product.product']
        for move in self.move_lines:
            if move.state not in ('draft', 'cancel') and move.product_id.type in ('product', 'consu'):
                products |= move.product_id
        return {
            'name': _('Scrap'),
            'view_type': 'form',
            'view_mode': 'form',
            'res_model': 'stock.scrap',
            'view_id': self.env.ref('stock.stock_scrap_form_view2').id,
            'type': 'ir.actions.act_window',
            'context': {'default_picking_id': self.id, 'product_ids': products.ids},
            'target': 'new',
        }

    def action_see_move_scrap(self):
        self.ensure_one()
        action = self.env.ref('stock.action_stock_scrap').read()[0]
        scraps = self.env['stock.scrap'].search([('picking_id', '=', self.id)])
        action['domain'] = [('id', 'in', scraps.ids)]
        return action

    def action_see_packages(self):
        self.ensure_one()
        action = self.env.ref('stock.action_package_view').read()[0]
        packages = self.move_line_ids.mapped('result_package_id')
        action['domain'] = [('id', 'in', packages.ids)]
        action['context'] = {'picking_id': self.id}
        return action
Exemple #17
0
class PurchaseOrder(models.Model):
    _inherit = "purchase.order"

    requisition_id = fields.Many2one('purchase.requisition',
                                     string='Purchase Agreement',
                                     copy=False)

    @api.onchange('requisition_id')
    def _onchange_requisition_id(self):
        if not self.requisition_id:
            return

        requisition = self.requisition_id
        if self.partner_id:
            partner = self.partner_id
        else:
            partner = requisition.vendor_id
        payment_term = partner.property_supplier_payment_term_id
        currency = partner.property_purchase_currency_id or requisition.company_id.currency_id

        FiscalPosition = self.env['account.fiscal.position']
        fpos = FiscalPosition.get_fiscal_position(partner.id)
        fpos = FiscalPosition.browse(fpos)

        self.partner_id = partner.id
        self.fiscal_position_id = fpos.id
        self.payment_term_id = payment_term.id,
        self.company_id = requisition.company_id.id
        self.currency_id = currency.id
        self.origin = requisition.name
        self.partner_ref = requisition.name  # to control vendor bill based on agreement reference
        self.notes = requisition.description
        self.date_order = requisition.date_end or fields.Datetime.now()
        self.picking_type_id = requisition.picking_type_id.id

        if requisition.type_id.line_copy != 'copy':
            return

        # Create PO lines if necessary
        order_lines = []
        for line in requisition.line_ids:
            # Compute name
            product_lang = line.product_id.with_context({
                'lang':
                partner.lang,
                'partner_id':
                partner.id,
            })
            name = product_lang.display_name
            if product_lang.description_purchase:
                name += '\n' + product_lang.description_purchase

            # Compute taxes
            if fpos:
                taxes_ids = fpos.map_tax(
                    line.product_id.supplier_taxes_id.filtered(
                        lambda tax: tax.company_id == requisition.company_id)
                ).ids
            else:
                taxes_ids = line.product_id.supplier_taxes_id.filtered(
                    lambda tax: tax.company_id == requisition.company_id).ids

            # Compute quantity and price_unit
            if line.product_uom_id != line.product_id.uom_po_id:
                product_qty = line.product_uom_id._compute_quantity(
                    line.product_qty, line.product_id.uom_po_id)
                price_unit = line.product_uom_id._compute_price(
                    line.price_unit, line.product_id.uom_po_id)
            else:
                product_qty = line.product_qty
                price_unit = line.price_unit

            if requisition.type_id.quantity_copy != 'copy':
                product_qty = 0

            # Compute price_unit in appropriate currency
            if requisition.company_id.currency_id != currency:
                price_unit = requisition.company_id.currency_id.compute(
                    price_unit, currency)

            # Create PO line
            order_line_values = line._prepare_purchase_order_line(
                name=name,
                product_qty=product_qty,
                price_unit=price_unit,
                taxes_ids=taxes_ids)
            order_lines.append((0, 0, order_line_values))
        self.order_line = order_lines

    @api.multi
    def button_confirm(self):
        res = super(PurchaseOrder, self).button_confirm()
        for po in self:
            if not po.requisition_id:
                continue
            if po.requisition_id.type_id.exclusive == 'exclusive':
                others_po = po.requisition_id.mapped('purchase_ids').filtered(
                    lambda r: r.id != po.id)
                others_po.button_cancel()
                po.requisition_id.action_done()
        return res

    @api.model
    def create(self, vals):
        purchase = super(PurchaseOrder, self).create(vals)
        if purchase.requisition_id:
            purchase.message_post_with_view(
                'mail.message_origin_link',
                values={
                    'self': purchase,
                    'origin': purchase.requisition_id
                },
                subtype_id=self.env['ir.model.data'].xmlid_to_res_id(
                    'mail.mt_note'))
        return purchase

    @api.multi
    def write(self, vals):
        result = super(PurchaseOrder, self).write(vals)
        if vals.get('requisition_id'):
            self.message_post_with_view(
                'mail.message_origin_link',
                values={
                    'self': self,
                    'origin': self.requisition_id,
                    'edit': True
                },
                subtype_id=self.env['ir.model.data'].xmlid_to_res_id(
                    'mail.mt_note'))
        return result
Exemple #18
0
class PosConfig(models.Model):
    _name = 'pos.config'

    def _default_sale_journal(self):
        journal = self.env.ref('point_of_sale.pos_sale_journal',
                               raise_if_not_found=False)
        if journal and journal.sudo().company_id == self.env.user.company_id:
            return journal
        return self._default_invoice_journal()

    def _default_invoice_journal(self):
        return self.env['account.journal'].search(
            [('type', '=', 'sale'),
             ('company_id', '=', self.env.user.company_id.id)],
            limit=1)

    def _default_pricelist(self):
        return self.env['product.pricelist'].search(
            [('currency_id', '=', self.env.user.company_id.currency_id.id)],
            limit=1)

    def _get_default_location(self):
        return self.env['stock.warehouse'].search(
            [('company_id', '=', self.env.user.company_id.id)],
            limit=1).lot_stock_id

    def _get_group_pos_manager(self):
        return self.env.ref('point_of_sale.group_pos_manager')

    def _get_group_pos_user(self):
        return self.env.ref('point_of_sale.group_pos_user')

    def _compute_default_customer_html(self):
        return self.env['ir.qweb'].render(
            'point_of_sale.customer_facing_display_html')

    name = fields.Char(string='Point of Sale Name',
                       index=True,
                       required=True,
                       help="An internal identification of the point of sale.")
    is_installed_account_accountant = fields.Boolean(
        compute="_compute_is_installed_account_accountant")
    journal_ids = fields.Many2many(
        'account.journal',
        'pos_config_journal_rel',
        'pos_config_id',
        'journal_id',
        string='Available Payment Methods',
        domain=
        "[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]",
    )
    picking_type_id = fields.Many2one('stock.picking.type',
                                      string='Operation Type')
    use_existing_lots = fields.Boolean(
        related='picking_type_id.use_existing_lots')
    stock_location_id = fields.Many2one('stock.location',
                                        string='Stock Location',
                                        domain=[('usage', '=', 'internal')],
                                        required=True,
                                        default=_get_default_location)
    journal_id = fields.Many2one(
        'account.journal',
        string='Sales Journal',
        domain=[('type', '=', 'sale')],
        help="Accounting journal used to post sales entries.",
        default=_default_sale_journal)
    invoice_journal_id = fields.Many2one(
        'account.journal',
        string='Invoice Journal',
        domain=[('type', '=', 'sale')],
        help="Accounting journal used to create invoices.",
        default=_default_invoice_journal)
    currency_id = fields.Many2one('res.currency',
                                  compute='_compute_currency',
                                  string="Currency")
    iface_cashdrawer = fields.Boolean(
        string='Cashdrawer', help="Automatically open the cashdrawer.")
    iface_payment_terminal = fields.Boolean(
        string='Payment Terminal',
        help="Enables Payment Terminal integration.")
    iface_electronic_scale = fields.Boolean(
        string='Electronic Scale',
        help="Enables Electronic Scale integration.")
    iface_vkeyboard = fields.Boolean(
        string='Virtual KeyBoard',
        help=
        u"Don’t turn this option on if you take orders on smartphones or tablets. \n Such devices already benefit from a native keyboard."
    )
    iface_customer_facing_display = fields.Boolean(
        string='Customer Facing Display',
        help="Show checkout to customers with a remotely-connected screen.")
    iface_print_via_proxy = fields.Boolean(
        string='Print via Proxy',
        help="Bypass browser printing and prints via the hardware proxy.")
    iface_scan_via_proxy = fields.Boolean(
        string='Scan via Proxy',
        help=
        "Enable barcode scanning with a remotely connected barcode scanner.")
    iface_invoicing = fields.Boolean(
        string='Invoicing',
        help='Enables invoice generation from the Point of Sale.')
    iface_big_scrollbars = fields.Boolean(
        'Large Scrollbars', help='For imprecise industrial touchscreens.')
    iface_print_auto = fields.Boolean(
        string='Automatic Receipt Printing',
        default=False,
        help=
        'The receipt will automatically be printed at the end of each order.')
    iface_print_skip_screen = fields.Boolean(
        string='Skip Preview Screen',
        default=True,
        help=
        'The receipt screen will be skipped if the receipt can be printed automatically.'
    )
    iface_precompute_cash = fields.Boolean(
        string='Prefill Cash Payment',
        help=
        'The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount.'
    )
    iface_tax_included = fields.Selection([('subtotal', 'Tax-Excluded Prices'),
                                           ('total', 'Tax-Included Prices')],
                                          "Tax Display",
                                          default='subtotal',
                                          required=True)
    iface_start_categ_id = fields.Many2one(
        'pos.category',
        string='Initial Category',
        help=
        'The point of sale will display this product category by default. If no category is specified, all available products will be shown.'
    )
    iface_display_categ_images = fields.Boolean(
        string='Display Category Pictures',
        help="The product categories will be displayed with pictures.")
    restrict_price_control = fields.Boolean(
        string='Restrict Price Modifications to Managers',
        help=
        "Only users with Manager access rights for PoS app can modify the product prices on orders."
    )
    cash_control = fields.Boolean(
        string='Cash Control',
        help="Check the amount of the cashbox at opening and closing.")
    receipt_header = fields.Text(
        string='Receipt Header',
        help=
        "A short text that will be inserted as a header in the printed receipt."
    )
    receipt_footer = fields.Text(
        string='Receipt Footer',
        help=
        "A short text that will be inserted as a footer in the printed receipt."
    )
    proxy_ip = fields.Char(
        string='IP Address',
        size=45,
        help=
        'The hostname or ip address of the hardware proxy, Will be autodetected if left empty.'
    )
    active = fields.Boolean(default=True)
    uuid = fields.Char(
        readonly=True,
        default=lambda self: str(uuid.uuid4()),
        help=
        'A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.'
    )
    sequence_id = fields.Many2one(
        'ir.sequence',
        string='Order IDs Sequence',
        readonly=True,
        help=
        "This sequence is automatically created by actpy but you can change it "
        "to customize the reference numbers of your orders.",
        copy=False)
    sequence_line_id = fields.Many2one(
        'ir.sequence',
        string='Order Line IDs Sequence',
        readonly=True,
        help=
        "This sequence is automatically created by actpy but you can change it "
        "to customize the reference numbers of your orders lines.",
        copy=False)
    session_ids = fields.One2many('pos.session',
                                  'config_id',
                                  string='Sessions')
    current_session_id = fields.Many2one('pos.session',
                                         compute='_compute_current_session',
                                         string="Current Session")
    current_session_state = fields.Char(compute='_compute_current_session')
    last_session_closing_cash = fields.Float(compute='_compute_last_session')
    last_session_closing_date = fields.Date(compute='_compute_last_session')
    pos_session_username = fields.Char(compute='_compute_current_session_user')
    pos_session_state = fields.Char(compute='_compute_current_session_user')
    group_by = fields.Boolean(
        string='Group Journal Items',
        default=True,
        help=
        "Check this if you want to group the Journal Items by Product while closing a Session."
    )
    pricelist_id = fields.Many2one(
        'product.pricelist',
        string='Default Pricelist',
        required=True,
        default=_default_pricelist,
        help=
        "The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured."
    )
    available_pricelist_ids = fields.Many2many(
        'product.pricelist',
        string='Available Pricelists',
        default=_default_pricelist,
        help=
        "Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply."
    )
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 required=True,
                                 default=lambda self: self.env.user.company_id)
    barcode_nomenclature_id = fields.Many2one(
        'barcode.nomenclature',
        string='Barcode Nomenclature',
        help=
        'Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers.'
    )
    group_pos_manager_id = fields.Many2one(
        'res.groups',
        string='Point of Sale Manager Group',
        default=_get_group_pos_manager,
        help=
        'This field is there to pass the id of the pos manager group to the point of sale client.'
    )
    group_pos_user_id = fields.Many2one(
        'res.groups',
        string='Point of Sale User Group',
        default=_get_group_pos_user,
        help=
        'This field is there to pass the id of the pos user group to the point of sale client.'
    )
    iface_tipproduct = fields.Boolean(string="Product tips")
    tip_product_id = fields.Many2one(
        'product.product',
        string='Tip Product',
        help="This product is used as reference on customer receipts.")
    fiscal_position_ids = fields.Many2many(
        'account.fiscal.position',
        string='Fiscal Positions',
        help=
        'This is useful for restaurants with onsite and take-away services that imply specific tax rates.'
    )
    default_fiscal_position_id = fields.Many2one(
        'account.fiscal.position', string='Default Fiscal Position')
    default_cashbox_lines_ids = fields.One2many('account.cashbox.line',
                                                'default_pos_id',
                                                string='Default Balance')
    customer_facing_display_html = fields.Html(
        string='Customer facing display content',
        translate=True,
        default=_compute_default_customer_html)
    use_pricelist = fields.Boolean("Use a pricelist.")
    group_sale_pricelist = fields.Boolean(
        "Use pricelists to adapt your price per customers",
        implied_group='product.group_sale_pricelist',
        help=
        """Allows to manage different prices based on rules per category of customers.
                    Example: 10% for retailers, promotion of 5 EUR on this product, etc."""
    )
    group_pricelist_item = fields.Boolean(
        "Show pricelists to customers",
        implied_group='product.group_pricelist_item')
    tax_regime = fields.Boolean("Tax Regime")
    tax_regime_selection = fields.Boolean("Tax Regime Selection value")
    barcode_scanner = fields.Boolean("Barcode Scanner")
    start_category = fields.Boolean("Set Start Category")
    module_pos_restaurant = fields.Boolean("Is a Bar/Restaurant")
    module_pos_discount = fields.Boolean("Global Discounts")
    module_pos_mercury = fields.Boolean(string="Integrated Card Payments")
    module_pos_reprint = fields.Boolean(string="Reprint Receipt")
    is_posbox = fields.Boolean("PosBox")
    is_header_or_footer = fields.Boolean("Header & Footer")

    def _compute_is_installed_account_accountant(self):
        account_accountant = self.env['ir.module.module'].sudo().search([
            ('name', '=', 'account_accountant'), ('state', '=', 'installed')
        ])
        for pos_config in self:
            pos_config.is_installed_account_accountant = account_accountant and account_accountant.id

    @api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id')
    def _compute_currency(self):
        for pos_config in self:
            if pos_config.journal_id:
                pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id
            else:
                pos_config.currency_id = self.env.user.company_id.currency_id.id

    @api.depends('session_ids')
    def _compute_current_session(self):
        for pos_config in self:
            session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and \
                not r.state == 'closed' and \
                not r.rescue)
            # sessions ordered by id desc
            pos_config.current_session_id = session and session[0].id or False
            pos_config.current_session_state = session and session[
                0].state or False

    @api.depends('session_ids')
    def _compute_last_session(self):
        PosSession = self.env['pos.session']
        for pos_config in self:
            session = PosSession.search_read(
                [('config_id', '=', pos_config.id), ('state', '=', 'closed')],
                ['cash_register_balance_end_real', 'stop_at'],
                order="stop_at desc",
                limit=1)
            if session:
                pos_config.last_session_closing_cash = session[0][
                    'cash_register_balance_end_real']
                pos_config.last_session_closing_date = session[0]['stop_at']
            else:
                pos_config.last_session_closing_cash = 0
                pos_config.last_session_closing_date = False

    @api.depends('session_ids')
    def _compute_current_session_user(self):
        for pos_config in self:
            session = pos_config.session_ids.filtered(lambda s: s.state in [
                'opening_control', 'opened', 'closing_control'
            ] and not s.rescue)
            pos_config.pos_session_username = session and session[
                0].user_id.name or False
            pos_config.pos_session_state = session and session[0].state or False

    @api.constrains('company_id', 'stock_location_id')
    def _check_company_location(self):
        if self.stock_location_id.company_id and self.stock_location_id.company_id.id != self.company_id.id:
            raise ValidationError(
                _("The company of the stock location is different than the one of point of sale"
                  ))

    @api.constrains('company_id', 'journal_id')
    def _check_company_journal(self):
        if self.journal_id and self.journal_id.company_id.id != self.company_id.id:
            raise ValidationError(
                _("The company of the sales journal is different than the one of point of sale"
                  ))

    @api.constrains('company_id', 'invoice_journal_id')
    def _check_company_invoice_journal(self):
        if self.invoice_journal_id and self.invoice_journal_id.company_id.id != self.company_id.id:
            raise ValidationError(
                _("The invoice journal and the point of sale must belong to the same company"
                  ))

    @api.constrains('company_id', 'journal_ids')
    def _check_company_payment(self):
        if self.env['account.journal'].search_count([
            ('id', 'in', self.journal_ids.ids),
            ('company_id', '!=', self.company_id.id)
        ]):
            raise ValidationError(
                _("The company of a payment method is different than the one of point of sale"
                  ))

    @api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id',
                    'invoice_journal_id', 'journal_ids')
    def _check_currencies(self):
        if self.pricelist_id not in self.available_pricelist_ids:
            raise ValidationError(
                _("The default pricelist must be included in the available pricelists."
                  ))
        if any(
                self.available_pricelist_ids.mapped(
                    lambda pricelist: pricelist.currency_id != self.currency_id
                )):
            raise ValidationError(
                _("All available pricelists must be in the same currency as the company or"
                  " as the Sales Journal set on this point of sale if you use"
                  " the Accounting application."))
        if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id:
            raise ValidationError(
                _("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set."
                  ))
        if any(
                self.journal_ids.mapped(
                    lambda journal: journal.currency_id and journal.currency_id
                    != self.currency_id)):
            raise ValidationError(
                _("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set."
                  ))

    @api.onchange('iface_print_via_proxy')
    def _onchange_iface_print_via_proxy(self):
        self.iface_print_auto = self.iface_print_via_proxy

    @api.onchange('picking_type_id')
    def _onchange_picking_type_id(self):
        if self.picking_type_id.default_location_src_id.usage == 'internal' and self.picking_type_id.default_location_dest_id.usage == 'customer':
            self.stock_location_id = self.picking_type_id.default_location_src_id.id

    @api.onchange('use_pricelist')
    def _onchange_use_pricelist(self):
        """
        If the 'pricelist' box is unchecked, we reset the pricelist_id to stop
        using a pricelist for this posbox. 
        """
        if not self.use_pricelist:
            self.pricelist_id = self._default_pricelist()
        else:
            self.update({
                'group_sale_pricelist': True,
                'group_pricelist_item': True,
            })

    @api.onchange('available_pricelist_ids')
    def _onchange_available_pricelist_ids(self):
        if self.pricelist_id not in self.available_pricelist_ids:
            self.pricelist_id = False

    @api.onchange('iface_scan_via_proxy')
    def _onchange_iface_scan_via_proxy(self):
        if self.iface_scan_via_proxy:
            self.barcode_scanner = True
        else:
            self.barcode_scanner = False

    @api.onchange('barcode_scanner')
    def _onchange_barcode_scanner(self):
        if self.barcode_scanner:
            self.barcode_nomenclature_id = self.env[
                'barcode.nomenclature'].search([], limit=1)
        else:
            self.barcode_nomenclature_id = False

    @api.onchange('is_posbox')
    def _onchange_is_posbox(self):
        if not self.is_posbox:
            self.proxy_ip = False
            self.iface_scan_via_proxy = False
            self.iface_electronic_scale = False
            self.iface_cashdrawer = False
            self.iface_print_via_proxy = False
            self.iface_customer_facing_display = False

    @api.onchange('tax_regime')
    def _onchange_tax_regime(self):
        if not self.tax_regime:
            self.default_fiscal_position_id = False

    @api.onchange('tax_regime_selection')
    def _onchange_tax_regime_selection(self):
        if not self.tax_regime_selection:
            self.fiscal_position_ids = [(5, 0, 0)]

    @api.onchange('start_category')
    def _onchange_start_category(self):
        if not self.start_category:
            self.iface_start_categ_id = False

    @api.onchange('is_header_or_footer')
    def _onchange_header_footer(self):
        if not self.is_header_or_footer:
            self.receipt_header = False
            self.receipt_footer = False

    @api.multi
    def name_get(self):
        result = []
        for config in self:
            if (not config.session_ids) or (config.session_ids[0].state
                                            == 'closed'):
                result.append(
                    (config.id, config.name + ' (' + _('not used') + ')'))
                continue
            result.append((config.id, config.name + ' (' +
                           config.session_ids[0].user_id.name + ')'))
        return result

    @api.model
    def create(self, values):
        if values.get('is_posbox') and values.get(
                'iface_customer_facing_display'):
            if values.get('customer_facing_display_html') and not values[
                    'customer_facing_display_html'].strip():
                values[
                    'customer_facing_display_html'] = self._compute_default_customer_html(
                    )
        IrSequence = self.env['ir.sequence'].sudo()
        val = {
            'name': _('POS Order %s') % values['name'],
            'padding': 4,
            'prefix': "%s/" % values['name'],
            'code': "pos.order",
            'company_id': values.get('company_id', False),
        }
        # force sequence_id field to new pos.order sequence
        values['sequence_id'] = IrSequence.create(val).id

        val.update(name=_('POS order line %s') % values['name'],
                   code='pos.order.line')
        values['sequence_line_id'] = IrSequence.create(val).id
        pos_config = super(PosConfig, self).create(values)
        pos_config.sudo()._check_modules_to_install()
        pos_config.sudo()._check_groups_implied()
        # If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install.
        return pos_config

    @api.multi
    def write(self, vals):
        if (self.is_posbox or vals.get('is_posbox')) and (
                self.iface_customer_facing_display
                or vals.get('iface_customer_facing_display')):
            facing_display = (self.customer_facing_display_html
                              or vals.get('customer_facing_display_html')
                              or '').strip()
            if not facing_display:
                vals[
                    'customer_facing_display_html'] = self._compute_default_customer_html(
                    )
        result = super(PosConfig, self).write(vals)
        self.sudo()._set_fiscal_position()
        self.sudo()._check_modules_to_install()
        self.sudo()._check_groups_implied()
        return result

    @api.multi
    def unlink(self):
        for pos_config in self.filtered(
                lambda pos_config: pos_config.sequence_id or pos_config.
                sequence_line_id):
            pos_config.sequence_id.unlink()
            pos_config.sequence_line_id.unlink()
        return super(PosConfig, self).unlink()

    def _set_fiscal_position(self):
        for config in self:
            if config.tax_regime and config.default_fiscal_position_id.id not in config.fiscal_position_ids.ids:
                config.fiscal_position_ids = [
                    (4, config.default_fiscal_position_id.id)
                ]
            elif not config.tax_regime_selection and not config.tax_regime and config.fiscal_position_ids.ids:
                config.fiscal_position_ids = [(5, 0, 0)]

    def _check_modules_to_install(self):
        module_installed = False
        for pos_config in self:
            for field_name in [
                    f for f in pos_config.fields_get_keys()
                    if f.startswith('module_')
            ]:
                module_name = field_name.split('module_')[1]
                module_to_install = self.env['ir.module.module'].sudo().search(
                    [('name', '=', module_name)])
                if getattr(pos_config,
                           field_name) and module_to_install.state not in (
                               'installed', 'to install', 'to upgrade'):
                    module_to_install.button_immediate_install()
                    module_installed = True
        # just in case we want to do something if we install a module. (like a refresh ...)
        return module_installed

    def _check_groups_implied(self):
        for pos_config in self:
            for field_name in [
                    f for f in pos_config.fields_get_keys()
                    if f.startswith('group_')
            ]:
                field = pos_config._fields[field_name]
                if field.type in ('boolean', 'selection') and hasattr(
                        field, 'implied_group'):
                    field_group_xmlids = getattr(field, 'group',
                                                 'base.group_user').split(',')
                    field_groups = self.env['res.groups'].concat(
                        *(self.env.ref(it) for it in field_group_xmlids))
                    field_groups.write({
                        'implied_ids':
                        [(4, self.env.ref(field.implied_group).id)]
                    })

    def execute(self):
        return {
            'type': 'ir.actions.client',
            'tag': 'reload',
            'params': {
                'wait': True
            }
        }

    # Methods to open the POS
    @api.multi
    def open_ui(self):
        """ open the pos interface """
        self.ensure_one()
        return {
            'type': 'ir.actions.act_url',
            'url': '/pos/web/',
            'target': 'self',
        }

    @api.multi
    def open_session_cb(self):
        """ new session button

        create one if none exist
        access cash control interface if enabled or start a session
        """
        self.ensure_one()
        if not self.current_session_id:
            self.current_session_id = self.env['pos.session'].create({
                'user_id':
                self.env.uid,
                'config_id':
                self.id
            })
            if self.current_session_id.state == 'opened':
                return self.open_ui()
            return self._open_session(self.current_session_id.id)
        return self._open_session(self.current_session_id.id)

    @api.multi
    def open_existing_session_cb(self):
        """ close session button

        access session form to validate entries
        """
        self.ensure_one()
        return self._open_session(self.current_session_id.id)

    def _open_session(self, session_id):
        return {
            'name': _('Session'),
            'view_type': 'form',
            'view_mode': 'form,tree',
            'res_model': 'pos.session',
            'res_id': session_id,
            'view_id': False,
            'type': 'ir.actions.act_window',
        }
Exemple #19
0
class LunchOrder(models.Model):
    """
    A lunch order contains one or more lunch order line(s). It is associated to a user for a given
    date. When creating a lunch order, applicable lunch alerts are displayed.
    """
    _name = 'lunch.order'
    _description = 'Lunch Order'
    _order = 'date desc'

    def _default_previous_order_ids(self):
        prev_order = self.env['lunch.order.line'].search(
            [('user_id', '=', self.env.uid),
             ('product_id.active', '!=', False)],
            limit=20,
            order='id desc')
        # If we return return prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        return list({(order.product_id, order.note): order.id
                     for order in prev_order}.values())

    user_id = fields.Many2one('res.users',
                              'User',
                              readonly=True,
                              states={'new': [('readonly', False)]},
                              default=lambda self: self.env.uid)
    date = fields.Date('Date',
                       required=True,
                       readonly=True,
                       states={'new': [('readonly', False)]},
                       default=fields.Date.context_today)
    order_line_ids = fields.One2many('lunch.order.line',
                                     'order_id',
                                     'Products',
                                     readonly=True,
                                     copy=True,
                                     states={
                                         'new': [('readonly', False)],
                                         False: [('readonly', False)]
                                     })
    total = fields.Float(compute='_compute_total', string="Total", store=True)
    state = fields.Selection([('new', 'New'), ('confirmed', 'Received'),
                              ('cancelled', 'Cancelled')],
                             'Status',
                             readonly=True,
                             index=True,
                             copy=False,
                             compute='_compute_order_state',
                             store=True)
    alerts = fields.Text(compute='_compute_alerts_get', string="Alerts")
    company_id = fields.Many2one('res.company',
                                 related='user_id.company_id',
                                 store=True)
    currency_id = fields.Many2one('res.currency',
                                  related='company_id.currency_id',
                                  readonly=True,
                                  store=True)
    cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance',
                                        multi='cash_move_balance')
    balance_visible = fields.Boolean(compute='_compute_cash_move_balance',
                                     multi='cash_move_balance')
    previous_order_ids = fields.Many2many('lunch.order.line',
                                          compute='_compute_previous_order')
    previous_order_widget = fields.Text(compute='_compute_previous_order')

    @api.one
    @api.depends('order_line_ids')
    def _compute_total(self):
        """
        get and sum the order lines' price
        """
        self.total = sum(orderline.price for orderline in self.order_line_ids)

    @api.multi
    def name_get(self):
        return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id))
                for order in self]

    @api.depends('state')
    def _compute_alerts_get(self):
        """
        get the alerts to display on the order form
        """
        alert_msg = [
            alert.message for alert in self.env['lunch.alert'].search([])
            if alert.display
        ]

        if self.state == 'new':
            self.alerts = alert_msg and '\n'.join(alert_msg) or False

    @api.multi
    @api.depends('user_id', 'state')
    def _compute_previous_order(self):
        self.ensure_one()
        self.previous_order_widget = json.dumps(False)

        prev_order = self.env['lunch.order.line'].search(
            [('user_id', '=', self.env.uid),
             ('product_id.active', '!=', False)],
            limit=20,
            order='date desc, id desc')
        # If we use prev_order.ids, we will have duplicates (identical orders).
        # Therefore, this following part removes duplicates based on product_id and note.
        self.previous_order_ids = list({(order.product_id, order.note):
                                        order.id
                                        for order in prev_order}.values())

        if self.previous_order_ids:
            lunch_data = {}
            for line in self.previous_order_ids:
                lunch_data[line.id] = {
                    'line_id': line.id,
                    'product_id': line.product_id.id,
                    'product_name': line.product_id.name,
                    'supplier': line.supplier.name,
                    'note': line.note,
                    'price': line.price,
                    'date': line.date,
                    'currency_id': line.currency_id.id,
                }
            # sort the old lunch orders by (date, id)
            lunch_data = OrderedDict(
                sorted(lunch_data.items(),
                       key=lambda t: (t[1]['date'], t[0]),
                       reverse=True))
            self.previous_order_widget = json.dumps(lunch_data)

    @api.one
    @api.depends('user_id')
    def _compute_cash_move_balance(self):
        domain = [('user_id', '=', self.user_id.id)]
        lunch_cash = self.env['lunch.cashmove'].read_group(
            domain, ['amount', 'user_id'], ['user_id'])
        if len(lunch_cash):
            self.cash_move_balance = lunch_cash[0]['amount']
        self.balance_visible = (self.user_id
                                == self.env.user) or self.user_has_groups(
                                    'lunch.group_lunch_manager')

    @api.one
    @api.constrains('date')
    def _check_date(self):
        """
        Prevents the user to create an order in the past
        """
        date_order = datetime.datetime.strptime(self.date, '%Y-%m-%d')
        date_today = datetime.datetime.strptime(
            fields.Date.context_today(self), '%Y-%m-%d')
        if (date_order < date_today):
            raise ValidationError(_('The date of your order is in the past.'))

    @api.one
    @api.depends('order_line_ids.state')
    def _compute_order_state(self):
        """
        Update the state of lunch.order based on its orderlines. Here is the logic:
        - if at least one order line is cancelled, the order is set as cancelled
        - if no line is cancelled but at least one line is not confirmed, the order is set as new
        - if all lines are confirmed, the order is set as confirmed
        """
        if not self.order_line_ids:
            self.state = 'new'
        else:
            isConfirmed = True
            for orderline in self.order_line_ids:
                if orderline.state == 'cancelled':
                    self.state = 'cancelled'
                    return
                elif orderline.state == 'confirmed':
                    continue
                else:
                    isConfirmed = False

            if isConfirmed:
                self.state = 'confirmed'
            else:
                self.state = 'new'
        return
Exemple #20
0
class HrPayslip(models.Model):
    _name = 'hr.payslip'
    _description = 'Pay Slip'

    struct_id = fields.Many2one(
        'hr.payroll.structure',
        string='Structure',
        readonly=True,
        states={'draft': [('readonly', False)]},
        help=
        'Defines the rules that have to be applied to this payslip, accordingly '
        'to the contract chosen. If you let empty the field contract, this field isn\'t '
        'mandatory anymore and thus the rules applied will be all the rules set on the '
        'structure of all contracts of the employee valid for the chosen period'
    )
    name = fields.Char(string='Payslip Name',
                       readonly=True,
                       states={'draft': [('readonly', False)]})
    number = fields.Char(string='Reference',
                         readonly=True,
                         copy=False,
                         states={'draft': [('readonly', False)]})
    employee_id = fields.Many2one('hr.employee',
                                  string='Employee',
                                  required=True,
                                  readonly=True,
                                  states={'draft': [('readonly', False)]})
    date_from = fields.Date(string='Date From',
                            readonly=True,
                            required=True,
                            default=time.strftime('%Y-%m-01'),
                            states={'draft': [('readonly', False)]})
    date_to = fields.Date(
        string='Date To',
        readonly=True,
        required=True,
        default=str(datetime.now() +
                    relativedelta.relativedelta(months=+1, day=1, days=-1))
        [:10],
        states={'draft': [('readonly', False)]})
    # this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed)
    state = fields.Selection(
        [
            ('draft', 'Draft'),
            ('verify', 'Waiting'),
            ('done', 'Done'),
            ('cancel', 'Rejected'),
        ],
        string='Status',
        index=True,
        readonly=True,
        copy=False,
        default='draft',
        help="""* When the payslip is created the status is \'Draft\'
                \n* If the payslip is under verification, the status is \'Waiting\'.
                \n* If the payslip is confirmed then status is set to \'Done\'.
                \n* When user cancel payslip the status is \'Rejected\'.""")
    line_ids = fields.One2many('hr.payslip.line',
                               'slip_id',
                               string='Payslip Lines',
                               readonly=True,
                               states={'draft': [('readonly', False)]})
    company_id = fields.Many2one(
        'res.company',
        string='Company',
        readonly=True,
        copy=False,
        default=lambda self: self.env['res.company']._company_default_get(),
        states={'draft': [('readonly', False)]})
    worked_days_line_ids = fields.One2many(
        'hr.payslip.worked_days',
        'payslip_id',
        string='Payslip Worked Days',
        copy=True,
        readonly=True,
        states={'draft': [('readonly', False)]})
    input_line_ids = fields.One2many('hr.payslip.input',
                                     'payslip_id',
                                     string='Payslip Inputs',
                                     readonly=True,
                                     states={'draft': [('readonly', False)]})
    paid = fields.Boolean(string='Made Payment Order ? ',
                          readonly=True,
                          copy=False,
                          states={'draft': [('readonly', False)]})
    note = fields.Text(string='Internal Note',
                       readonly=True,
                       states={'draft': [('readonly', False)]})
    contract_id = fields.Many2one('hr.contract',
                                  string='Contract',
                                  readonly=True,
                                  states={'draft': [('readonly', False)]})
    details_by_salary_rule_category = fields.One2many(
        'hr.payslip.line',
        compute='_compute_details_by_salary_rule_category',
        string='Details by Salary Rule Category')
    credit_note = fields.Boolean(
        string='Credit Note',
        readonly=True,
        states={'draft': [('readonly', False)]},
        help="Indicates this payslip has a refund of another")
    payslip_run_id = fields.Many2one('hr.payslip.run',
                                     string='Payslip Batches',
                                     readonly=True,
                                     copy=False,
                                     states={'draft': [('readonly', False)]})
    payslip_count = fields.Integer(compute='_compute_payslip_count',
                                   string="Payslip Computation Details")

    @api.multi
    def _compute_details_by_salary_rule_category(self):
        for payslip in self:
            payslip.details_by_salary_rule_category = payslip.mapped(
                'line_ids').filtered(lambda line: line.category_id)

    @api.multi
    def _compute_payslip_count(self):
        for payslip in self:
            payslip.payslip_count = len(payslip.line_ids)

    @api.constrains('date_from', 'date_to')
    def _check_dates(self):
        if any(
                self.filtered(
                    lambda payslip: payslip.date_from > payslip.date_to)):
            raise ValidationError(
                _("Payslip 'Date From' must be before 'Date To'."))

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

    @api.multi
    def action_payslip_done(self):
        self.compute_sheet()
        return self.write({'state': 'done'})

    @api.multi
    def action_payslip_cancel(self):
        if self.filtered(lambda slip: slip.state == 'done'):
            raise UserError(_("Cannot cancel a payslip that is done."))
        return self.write({'state': 'cancel'})

    @api.multi
    def refund_sheet(self):
        for payslip in self:
            copied_payslip = payslip.copy({
                'credit_note': True,
                'name': _('Refund: ') + payslip.name
            })
            copied_payslip.compute_sheet()
            copied_payslip.action_payslip_done()
        formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False)
        treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False)
        return {
            'name': ("Refund Payslip"),
            'view_mode':
            'tree, form',
            'view_id':
            False,
            'view_type':
            'form',
            'res_model':
            'hr.payslip',
            'type':
            'ir.actions.act_window',
            'target':
            'current',
            'domain':
            "[('id', 'in', %s)]" % copied_payslip.ids,
            'views': [(treeview_ref and treeview_ref.id or False, 'tree'),
                      (formview_ref and formview_ref.id or False, 'form')],
            'context': {}
        }

    @api.multi
    def check_done(self):
        return True

    @api.multi
    def unlink(self):
        if any(
                self.filtered(lambda payslip: payslip.state not in
                              ('draft', 'cancel'))):
            raise UserError(
                _('You cannot delete a payslip which is not draft or cancelled!'
                  ))
        return super(HrPayslip, self).unlink()

    # TODO move this function into hr_contract module, on hr.employee object
    @api.model
    def get_contract(self, employee, date_from, date_to):
        """
        @param employee: recordset of employee
        @param date_from: date field
        @param date_to: date field
        @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates
        """
        # a contract is valid if it ends between the given dates
        clause_1 = [
            '&', ('date_end', '<=', date_to), ('date_end', '>=', date_from)
        ]
        # OR if it starts between the given dates
        clause_2 = [
            '&', ('date_start', '<=', date_to), ('date_start', '>=', date_from)
        ]
        # OR if it starts before the date_from and finish after the date_end (or never finish)
        clause_3 = [
            '&', ('date_start', '<=', date_from), '|',
            ('date_end', '=', False), ('date_end', '>=', date_to)
        ]
        clause_final = [('employee_id', '=', employee.id),
                        ('state', '=', 'open'), '|', '|'
                        ] + clause_1 + clause_2 + clause_3
        return self.env['hr.contract'].search(clause_final).ids

    @api.multi
    def compute_sheet(self):
        for payslip in self:
            number = payslip.number or self.env['ir.sequence'].next_by_code(
                'salary.slip')
            # delete old payslip lines
            payslip.line_ids.unlink()
            # set the list of contract for which the rules have to be applied
            # if we don't give the contract, then the rules to apply should be for all current contracts of the employee
            contract_ids = payslip.contract_id.ids or \
                self.get_contract(payslip.employee_id, payslip.date_from, payslip.date_to)
            lines = [
                (0, 0, line)
                for line in self._get_payslip_lines(contract_ids, payslip.id)
            ]
            payslip.write({'line_ids': lines, 'number': number})
        return True

    @api.model
    def get_worked_day_lines(self, contracts, date_from, date_to):
        """
        @param contract: Browse record of contracts
        @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to
        """
        res = []
        # fill only if the contract as a working schedule linked
        for contract in contracts.filtered(
                lambda contract: contract.resource_calendar_id):
            day_from = datetime.combine(fields.Date.from_string(date_from),
                                        datetime_time.min)
            day_to = datetime.combine(fields.Date.from_string(date_to),
                                      datetime_time.max)

            # compute leave days
            leaves = {}
            day_leave_intervals = contract.employee_id.iter_leaves(
                day_from, day_to, calendar=contract.resource_calendar_id)
            for day_intervals in day_leave_intervals:
                for interval in day_intervals:
                    holiday = interval[2]['leaves'].holiday_id
                    current_leave_struct = leaves.setdefault(
                        holiday.holiday_status_id, {
                            'name': holiday.holiday_status_id.name,
                            'sequence': 5,
                            'code': holiday.holiday_status_id.name,
                            'number_of_days': 0.0,
                            'number_of_hours': 0.0,
                            'contract_id': contract.id,
                        })
                    leave_time = (interval[1] - interval[0]).seconds / 3600
                    current_leave_struct['number_of_hours'] += leave_time
                    work_hours = contract.employee_id.get_day_work_hours_count(
                        interval[0].date(),
                        calendar=contract.resource_calendar_id)
                    if work_hours:
                        current_leave_struct[
                            'number_of_days'] += leave_time / work_hours

            # compute worked days
            work_data = contract.employee_id.get_work_days_data(
                day_from, day_to, calendar=contract.resource_calendar_id)
            attendances = {
                'name': _("Normal Working Days paid at 100%"),
                'sequence': 1,
                'code': 'WORK100',
                'number_of_days': work_data['days'],
                'number_of_hours': work_data['hours'],
                'contract_id': contract.id,
            }

            res.append(attendances)
            res.extend(leaves.values())
        return res

    @api.model
    def get_inputs(self, contracts, date_from, date_to):
        res = []

        structure_ids = contracts.get_all_structures()
        rule_ids = self.env['hr.payroll.structure'].browse(
            structure_ids).get_all_rules()
        sorted_rule_ids = [
            id for id, sequence in sorted(rule_ids, key=lambda x: x[1])
        ]
        inputs = self.env['hr.salary.rule'].browse(sorted_rule_ids).mapped(
            'input_ids')

        for contract in contracts:
            for input in inputs:
                input_data = {
                    'name': input.name,
                    'code': input.code,
                    'contract_id': contract.id,
                }
                res += [input_data]
        return res

    @api.model
    def _get_payslip_lines(self, contract_ids, payslip_id):
        def _sum_salary_rule_category(localdict, category, amount):
            if category.parent_id:
                localdict = _sum_salary_rule_category(localdict,
                                                      category.parent_id,
                                                      amount)
            localdict['categories'].dict[
                category.code] = category.code in localdict[
                    'categories'].dict and localdict['categories'].dict[
                        category.code] + amount or amount
            return localdict

        class BrowsableObject(object):
            def __init__(self, employee_id, dict, env):
                self.employee_id = employee_id
                self.dict = dict
                self.env = env

            def __getattr__(self, attr):
                return attr in self.dict and self.dict.__getitem__(attr) or 0.0

        class InputLine(BrowsableObject):
            """a class that will be used into the python code, mainly for usability purposes"""
            def sum(self, code, from_date, to_date=None):
                if to_date is None:
                    to_date = fields.Date.today()
                self.env.cr.execute(
                    """
                    SELECT sum(amount) as sum
                    FROM hr_payslip as hp, hr_payslip_input as pi
                    WHERE hp.employee_id = %s AND hp.state = 'done'
                    AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""",
                    (self.employee_id, from_date, to_date, code))
                return self.env.cr.fetchone()[0] or 0.0

        class WorkedDays(BrowsableObject):
            """a class that will be used into the python code, mainly for usability purposes"""
            def _sum(self, code, from_date, to_date=None):
                if to_date is None:
                    to_date = fields.Date.today()
                self.env.cr.execute(
                    """
                    SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours
                    FROM hr_payslip as hp, hr_payslip_worked_days as pi
                    WHERE hp.employee_id = %s AND hp.state = 'done'
                    AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""",
                    (self.employee_id, from_date, to_date, code))
                return self.env.cr.fetchone()

            def sum(self, code, from_date, to_date=None):
                res = self._sum(code, from_date, to_date)
                return res and res[0] or 0.0

            def sum_hours(self, code, from_date, to_date=None):
                res = self._sum(code, from_date, to_date)
                return res and res[1] or 0.0

        class Payslips(BrowsableObject):
            """a class that will be used into the python code, mainly for usability purposes"""
            def sum(self, code, from_date, to_date=None):
                if to_date is None:
                    to_date = fields.Date.today()
                self.env.cr.execute(
                    """SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end)
                            FROM hr_payslip as hp, hr_payslip_line as pl
                            WHERE hp.employee_id = %s AND hp.state = 'done'
                            AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""",
                    (self.employee_id, from_date, to_date, code))
                res = self.env.cr.fetchone()
                return res and res[0] or 0.0

        #we keep a dict with the result because a value can be overwritten by another rule with the same code
        result_dict = {}
        rules_dict = {}
        worked_days_dict = {}
        inputs_dict = {}
        blacklist = []
        payslip = self.env['hr.payslip'].browse(payslip_id)
        for worked_days_line in payslip.worked_days_line_ids:
            worked_days_dict[worked_days_line.code] = worked_days_line
        for input_line in payslip.input_line_ids:
            inputs_dict[input_line.code] = input_line

        categories = BrowsableObject(payslip.employee_id.id, {}, self.env)
        inputs = InputLine(payslip.employee_id.id, inputs_dict, self.env)
        worked_days = WorkedDays(payslip.employee_id.id, worked_days_dict,
                                 self.env)
        payslips = Payslips(payslip.employee_id.id, payslip, self.env)
        rules = BrowsableObject(payslip.employee_id.id, rules_dict, self.env)

        baselocaldict = {
            'categories': categories,
            'rules': rules,
            'payslip': payslips,
            'worked_days': worked_days,
            'inputs': inputs
        }
        #get the ids of the structures on the contracts and their parent id as well
        contracts = self.env['hr.contract'].browse(contract_ids)
        if len(contracts) == 1 and payslip.struct_id:
            structure_ids = list(
                set(payslip.struct_id._get_parent_structure().ids))
        else:
            structure_ids = contracts.get_all_structures()
        #get the rules of the structure and thier children
        rule_ids = self.env['hr.payroll.structure'].browse(
            structure_ids).get_all_rules()
        #run the rules by sequence
        sorted_rule_ids = [
            id for id, sequence in sorted(rule_ids, key=lambda x: x[1])
        ]
        sorted_rules = self.env['hr.salary.rule'].browse(sorted_rule_ids)

        for contract in contracts:
            employee = contract.employee_id
            localdict = dict(baselocaldict,
                             employee=employee,
                             contract=contract)
            for rule in sorted_rules:
                key = rule.code + '-' + str(contract.id)
                localdict['result'] = None
                localdict['result_qty'] = 1.0
                localdict['result_rate'] = 100
                #check if the rule can be applied
                if rule._satisfy_condition(
                        localdict) and rule.id not in blacklist:
                    #compute the amount of the rule
                    amount, qty, rate = rule._compute_rule(localdict)
                    #check if there is already a rule computed with that code
                    previous_amount = rule.code in localdict and localdict[
                        rule.code] or 0.0
                    #set/overwrite the amount computed for this rule in the localdict
                    tot_rule = amount * qty * rate / 100.0
                    localdict[rule.code] = tot_rule
                    rules_dict[rule.code] = rule
                    #sum the amount for its salary category
                    localdict = _sum_salary_rule_category(
                        localdict, rule.category_id,
                        tot_rule - previous_amount)
                    #create/overwrite the rule in the temporary results
                    result_dict[key] = {
                        'salary_rule_id': rule.id,
                        'contract_id': contract.id,
                        'name': rule.name,
                        'code': rule.code,
                        'category_id': rule.category_id.id,
                        'sequence': rule.sequence,
                        'appears_on_payslip': rule.appears_on_payslip,
                        'condition_select': rule.condition_select,
                        'condition_python': rule.condition_python,
                        'condition_range': rule.condition_range,
                        'condition_range_min': rule.condition_range_min,
                        'condition_range_max': rule.condition_range_max,
                        'amount_select': rule.amount_select,
                        'amount_fix': rule.amount_fix,
                        'amount_python_compute': rule.amount_python_compute,
                        'amount_percentage': rule.amount_percentage,
                        'amount_percentage_base': rule.amount_percentage_base,
                        'register_id': rule.register_id.id,
                        'amount': amount,
                        'employee_id': contract.employee_id.id,
                        'quantity': qty,
                        'rate': rate,
                    }
                else:
                    #blacklist this rule and its children
                    blacklist += [
                        id for id, seq in rule._recursive_search_of_rules()
                    ]

        return list(result_dict.values())

    # YTI TODO To rename. This method is not really an onchange, as it is not in any view
    # employee_id and contract_id could be browse records
    @api.multi
    def onchange_employee_id(self,
                             date_from,
                             date_to,
                             employee_id=False,
                             contract_id=False):
        #defaults
        res = {
            'value': {
                'line_ids': [],
                #delete old input lines
                'input_line_ids': [(
                    2,
                    x,
                ) for x in self.input_line_ids.ids],
                #delete old worked days lines
                'worked_days_line_ids': [(
                    2,
                    x,
                ) for x in self.worked_days_line_ids.ids],
                #'details_by_salary_head':[], TODO put me back
                'name':
                '',
                'contract_id':
                False,
                'struct_id':
                False,
            }
        }
        if (not employee_id) or (not date_from) or (not date_to):
            return res
        ttyme = datetime.fromtimestamp(
            time.mktime(time.strptime(date_from, "%Y-%m-%d")))
        employee = self.env['hr.employee'].browse(employee_id)
        locale = self.env.context.get('lang') or 'en_US'
        res['value'].update({
            'name':
            _('Salary Slip of %s for %s') %
            (employee.name,
             tools.ustr(
                 babel.dates.format_date(
                     date=ttyme, format='MMMM-y', locale=locale))),
            'company_id':
            employee.company_id.id,
        })

        if not self.env.context.get('contract'):
            #fill with the first contract of the employee
            contract_ids = self.get_contract(employee, date_from, date_to)
        else:
            if contract_id:
                #set the list of contract for which the input have to be filled
                contract_ids = [contract_id]
            else:
                #if we don't give the contract, then the input to fill should be for all current contracts of the employee
                contract_ids = self.get_contract(employee, date_from, date_to)

        if not contract_ids:
            return res
        contract = self.env['hr.contract'].browse(contract_ids[0])
        res['value'].update({'contract_id': contract.id})
        struct = contract.struct_id
        if not struct:
            return res
        res['value'].update({
            'struct_id': struct.id,
        })
        #computation of the salary input
        contracts = self.env['hr.contract'].browse(contract_ids)
        worked_days_line_ids = self.get_worked_day_lines(
            contracts, date_from, date_to)
        input_line_ids = self.get_inputs(contracts, date_from, date_to)
        res['value'].update({
            'worked_days_line_ids': worked_days_line_ids,
            'input_line_ids': input_line_ids,
        })
        return res

    @api.onchange('employee_id', 'date_from', 'date_to')
    def onchange_employee(self):

        if (not self.employee_id) or (not self.date_from) or (
                not self.date_to):
            return

        employee = self.employee_id
        date_from = self.date_from
        date_to = self.date_to
        contract_ids = []

        ttyme = datetime.fromtimestamp(
            time.mktime(time.strptime(date_from, "%Y-%m-%d")))
        locale = self.env.context.get('lang') or 'en_US'
        self.name = _('Salary Slip of %s for %s') % (
            employee.name,
            tools.ustr(
                babel.dates.format_date(
                    date=ttyme, format='MMMM-y', locale=locale)))
        self.company_id = employee.company_id

        if not self.env.context.get('contract') or not self.contract_id:
            contract_ids = self.get_contract(employee, date_from, date_to)
            if not contract_ids:
                return
            self.contract_id = self.env['hr.contract'].browse(contract_ids[0])

        if not self.contract_id.struct_id:
            return
        self.struct_id = self.contract_id.struct_id

        #computation of the salary input
        contracts = self.env['hr.contract'].browse(contract_ids)
        worked_days_line_ids = self.get_worked_day_lines(
            contracts, date_from, date_to)
        worked_days_lines = self.worked_days_line_ids.browse([])
        for r in worked_days_line_ids:
            worked_days_lines += worked_days_lines.new(r)
        self.worked_days_line_ids = worked_days_lines

        input_line_ids = self.get_inputs(contracts, date_from, date_to)
        input_lines = self.input_line_ids.browse([])
        for r in input_line_ids:
            input_lines += input_lines.new(r)
        self.input_line_ids = input_lines
        return

    @api.onchange('contract_id')
    def onchange_contract(self):
        if not self.contract_id:
            self.struct_id = False
        self.with_context(contract=True).onchange_employee()
        return

    def get_salary_line_total(self, code):
        self.ensure_one()
        line = self.line_ids.filtered(lambda line: line.code == code)
        if line:
            return line[0].total
        else:
            return 0.0
Exemple #21
0
class MailMail(models.Model):
    """Add the mass mailing campaign data to mail"""
    _inherit = ['mail.mail']

    mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing')
    statistics_ids = fields.One2many('mail.mail.statistics', 'mail_mail_id', string='Statistics')

    @api.model
    def create(self, values):
        """ Override mail_mail creation to create an entry in mail.mail.statistics """
        # TDE note: should be after 'all values computed', to have values (FIXME after merging other branch holding create refactoring)
        mail = super(MailMail, self).create(values)
        if values.get('statistics_ids'):
            mail_sudo = mail.sudo()
            mail_sudo.statistics_ids.write({'message_id': mail_sudo.message_id, 'state': 'outgoing'})
        return mail

    def _get_tracking_url(self, partner=None):
        base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
        track_url = werkzeug.urls.url_join(
            base_url, 'mail/track/%(mail_id)s/blank.gif?%(params)s' % {
                'mail_id': self.id,
                'params': werkzeug.urls.url_encode({'db': self.env.cr.dbname})
            }
        )
        return '<img src="%s" alt=""/>' % track_url

    def _get_unsubscribe_url(self, email_to):
        base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
        url = werkzeug.urls.url_join(
            base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % {
                'mailing_id': self.mailing_id.id,
                'params': werkzeug.urls.url_encode({
                    'db': self.env.cr.dbname,
                    'res_id': self.res_id,
                    'email': email_to,
                    'token': self.mailing_id._unsubscribe_token(
                        self.res_id, email_to),
                }),
            }
        )
        return url

    @api.multi
    def send_get_mail_body(self, partner=None):
        """ Override to add the tracking URL to the body and to add
        Statistic_id in shorted urls """
        # TDE: temporary addition (mail was parameter) due to semi-new-API
        self.ensure_one()
        body = super(MailMail, self).send_get_mail_body(partner=partner)

        if self.mailing_id and body and self.statistics_ids:
            for match in re.findall(URL_REGEX, self.body_html):
                href = match[0]
                url = match[1]

                parsed = werkzeug.urls.url_parse(url, scheme='http')

                if parsed.scheme.startswith('http') and parsed.path.startswith('/r/'):
                    new_href = href.replace(url, url + '/m/' + str(self.statistics_ids[0].id))
                    body = body.replace(href, new_href)

        # prepend <base> tag for images using absolute urls
        domain = self.env["ir.config_parameter"].sudo().get_param("web.base.url")
        base = "<base href='%s'>" % domain
        body = tools.append_content_to_html(base, body, plaintext=False, container_tag='div')
        # resolve relative image url to absolute for outlook.com
        def _sub_relative2absolute(match):
            return match.group(1) + werkzeug.urls.url_join(domain, match.group(2))
        body = re.sub('(<img(?=\s)[^>]*\ssrc=")(/[^/][^"]+)', _sub_relative2absolute, body)
        body = re.sub(r'(<[^>]+\bstyle="[^"]+\burl\(\'?)(/[^/\'][^\'")]+)', _sub_relative2absolute, body)

        # generate tracking URL
        if self.statistics_ids:
            tracking_url = self._get_tracking_url(partner)
            if tracking_url:
                body = tools.append_content_to_html(body, tracking_url, plaintext=False, container_tag='div')
        return body

    @api.multi
    def send_get_email_dict(self, partner=None):
        # TDE: temporary addition (mail was parameter) due to semi-new-API
        res = super(MailMail, self).send_get_email_dict(partner)
        base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
        if self.mailing_id and res.get('body') and res.get('email_to'):
            emails = tools.email_split(res.get('email_to')[0])
            email_to = emails and emails[0] or False
            unsubscribe_url = self._get_unsubscribe_url(email_to)
            link_to_replace = base_url + '/unsubscribe_from_list'
            if link_to_replace in res['body']:
                res['body'] = res['body'].replace(link_to_replace, unsubscribe_url if unsubscribe_url else '#')
        return res

    @api.multi
    def _postprocess_sent_message(self, mail_sent=True):
        for mail in self:
            if mail_sent is True and mail.statistics_ids:
                mail.statistics_ids.write({'sent': fields.Datetime.now(), 'exception': False})
            elif mail_sent is False and mail.statistics_ids:
                mail.statistics_ids.write({'exception': fields.Datetime.now()})
        return super(MailMail, self)._postprocess_sent_message(mail_sent=mail_sent)
class ImLivechatChannelRule(models.Model):
    """ Channel Rules
        Rules defining access to the channel (countries, and url matching). It also provide the 'auto pop'
        option to open automatically the conversation.
    """

    _name = 'im_livechat.channel.rule'
    _description = 'Channel Rules'
    _order = 'sequence asc'

    regex_url = fields.Char(
        'URL Regex',
        help=
        "Regular expression specifying the web pages this rule will be applied on."
    )
    action = fields.Selection([('display_button', 'Display the button'), ('auto_popup', 'Auto popup'), ('hide_button', 'Hide the button')],
        string='Action', required=True, default='display_button',
        help="* 'Display the button' displays the chat button on the pages.\n"\
             "* 'Auto popup' displays the button and automatically open the conversation pane.\n"\
             "* 'Hide the button' hides the chat button on the pages.")
    auto_popup_timer = fields.Integer(
        'Auto popup timer',
        default=0,
        help=
        "Delay (in seconds) to automatically open the conversation window. Note: the selected action must be 'Auto popup' otherwise this parameter will not be taken into account."
    )
    channel_id = fields.Many2one('im_livechat.channel',
                                 'Channel',
                                 help="The channel of the rule")
    country_ids = fields.Many2many(
        'res.country',
        'im_livechat_channel_country_rel',
        'channel_id',
        'country_id',
        'Country',
        help=
        "The rule will only be applied for these countries. Example: if you select 'Belgium' and 'United States' and that you set the action to 'Hide Button', the chat button will be hidden on the specified URL from the visitors located in these 2 countries. This feature requires GeoIP installed on your server."
    )
    sequence = fields.Integer(
        'Matching order',
        default=10,
        help=
        "Given the order to find a matching rule. If 2 rules are matching for the given url/country, the one with the lowest sequence will be chosen."
    )

    def match_rule(self, channel_id, url, country_id=False):
        """ determine if a rule of the given channel matches with the given url
            :param channel_id : the identifier of the channel_id
            :param url : the url to match with a rule
            :param country_id : the identifier of the country
            :returns the rule that matches the given condition. False otherwise.
            :rtype : im_livechat.channel.rule
        """
        def _match(rules):
            for rule in rules:
                if re.search(rule.regex_url or '', url):
                    return rule
            return False

        # first, search the country specific rules (the first match is returned)
        if country_id:  # don't include the country in the research if geoIP is not installed
            domain = [('country_ids', 'in', [country_id]),
                      ('channel_id', '=', channel_id)]
            rule = _match(self.search(domain))
            if rule:
                return rule
        # second, fallback on the rules without country
        domain = [('country_ids', '=', False), ('channel_id', '=', channel_id)]
        return _match(self.search(domain))
Exemple #23
0
class HrPayrollAdvice(models.Model):
    '''
    Bank Advice
    '''
    _name = 'hr.payroll.advice'

    def _get_default_date(self):
        return fields.Date.from_string(fields.Date.today())

    name = fields.Char(readonly=True,
                       required=True,
                       states={'draft': [('readonly', False)]})
    note = fields.Text(
        string='Description',
        default=
        'Please make the payroll transfer from above account number to the below mentioned account numbers towards employee salaries:'
    )
    date = fields.Date(readonly=True,
                       required=True,
                       states={'draft': [('readonly', False)]},
                       default=_get_default_date,
                       help='Advice Date is used to search Payslips')
    state = fields.Selection([
        ('draft', 'Draft'),
        ('confirm', 'Confirmed'),
        ('cancel', 'Cancelled'),
    ],
                             string='Status',
                             default='draft',
                             index=True,
                             readonly=True)
    number = fields.Char(string='Reference', readonly=True)
    line_ids = fields.One2many('hr.payroll.advice.line',
                               'advice_id',
                               string='Employee Salary',
                               states={'draft': [('readonly', False)]},
                               readonly=True,
                               copy=True)
    chaque_nos = fields.Char(string='Cheque Numbers')
    neft = fields.Boolean(
        string='NEFT Transaction',
        help='Check this box if your company use online transfer for salary')
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 required=True,
                                 readonly=True,
                                 states={'draft': [('readonly', False)]},
                                 default=lambda self: self.env.user.company_id)
    bank_id = fields.Many2one(
        'res.bank',
        string='Bank',
        readonly=True,
        states={'draft': [('readonly', False)]},
        help='Select the Bank from which the salary is going to be paid')
    batch_id = fields.Many2one('hr.payslip.run', string='Batch', readonly=True)

    @api.multi
    def compute_advice(self):
        """
        Advice - Create Advice lines in Payment Advice and
        compute Advice lines.
        """
        for advice in self:
            old_lines = self.env['hr.payroll.advice.line'].search([
                ('advice_id', '=', advice.id)
            ])
            if old_lines:
                old_lines.unlink()
            payslips = self.env['hr.payslip'].search([
                ('date_from', '<=', advice.date),
                ('date_to', '>=', advice.date), ('state', '=', 'done')
            ])
            for slip in payslips:
                if not slip.employee_id.bank_account_id and not slip.employee_id.bank_account_id.acc_number:
                    raise UserError(
                        _('Please define bank account for the %s employee') %
                        (slip.employee_id.name, ))
                payslip_line = self.env['hr.payslip.line'].search(
                    [('slip_id', '=', slip.id), ('code', '=', 'NET')], limit=1)
                if payslip_line:
                    self.env['hr.payroll.advice.line'].create({
                        'advice_id':
                        advice.id,
                        'name':
                        slip.employee_id.bank_account_id.acc_number,
                        'ifsc_code':
                        slip.employee_id.bank_account_id.bank_bic or '',
                        'employee_id':
                        slip.employee_id.id,
                        'bysal':
                        payslip_line.total
                    })
                slip.advice_id = advice.id

    @api.multi
    def confirm_sheet(self):
        """
        confirm Advice - confirmed Advice after computing Advice Lines..
        """
        for advice in self:
            if not advice.line_ids:
                raise UserError(
                    _('You can not confirm Payment advice without advice lines.'
                      ))
            date = fields.Date.from_string(fields.Date.today())
            advice_year = date.strftime('%m') + '-' + date.strftime('%Y')
            number = self.env['ir.sequence'].next_by_code('payment.advice')
            advice.write({
                'number': 'PAY' + '/' + advice_year + '/' + number,
                'state': 'confirm',
            })

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

    @api.multi
    def cancel_sheet(self):
        """Marks Advice as cancelled.
        """
        self.write({'state': 'cancel'})

    @api.onchange('company_id')
    def _onchange_company_id(self):
        self.bank_id = self.company_id.partner_id.bank_ids and self.company_id.partner_id.bank_ids[
            0].bank_id.id or False
Exemple #24
0
class ProductChangeQuantity(models.TransientModel):
    _name = "stock.change.product.qty"
    _description = "Change Product Quantity"

    # TDE FIXME: strange dfeault method, was present before migration ? to check
    product_id = fields.Many2one('product.product', 'Product', required=True)
    product_tmpl_id = fields.Many2one('product.template',
                                      'Template',
                                      required=True)
    product_variant_count = fields.Integer(
        'Variant Count', related='product_tmpl_id.product_variant_count')
    new_quantity = fields.Float(
        'New Quantity on Hand',
        default=1,
        digits=dp.get_precision('Product Unit of Measure'),
        required=True,
        help=
        'This quantity is expressed in the Default Unit of Measure of the product.'
    )
    lot_id = fields.Many2one('stock.production.lot',
                             'Lot/Serial Number',
                             domain="[('product_id','=',product_id)]")
    location_id = fields.Many2one('stock.location',
                                  'Location',
                                  required=True,
                                  domain="[('usage', '=', 'internal')]")

    @api.model
    def default_get(self, fields):
        res = super(ProductChangeQuantity, self).default_get(fields)
        if not res.get('product_id') and self.env.context.get(
                'active_id') and self.env.context.get(
                    'active_model'
                ) == 'product.template' and self.env.context.get('active_id'):
            res['product_id'] = self.env['product.product'].search(
                [('product_tmpl_id', '=', self.env.context['active_id'])],
                limit=1).id
        elif not res.get('product_id') and self.env.context.get(
                'active_id') and self.env.context.get(
                    'active_model'
                ) == 'product.product' and self.env.context.get('active_id'):
            res['product_id'] = self.env['product.product'].browse(
                self.env.context['active_id']).id
        if 'location_id' in fields and not res.get('location_id'):
            company_user = self.env.user.company_id
            warehouse = self.env['stock.warehouse'].search(
                [('company_id', '=', company_user.id)], limit=1)
            if warehouse:
                res['location_id'] = warehouse.lot_stock_id.id
        return res

    @api.onchange('location_id', 'product_id')
    def onchange_location_id(self):
        # TDE FIXME: should'nt we use context / location ?
        if self.location_id and self.product_id:
            availability = self.product_id.with_context(
                compute_child=False)._product_available()
            self.new_quantity = availability[
                self.product_id.id]['qty_available']

    @api.onchange('product_id')
    def onchange_product_id(self):
        if self.product_id:
            self.product_tmpl_id = self.onchange_product_id_dict(
                self.product_id.id)['product_tmpl_id']

    def _action_start_line(self):
        product = self.product_id.with_context(location=self.location_id.id,
                                               lot_id=self.lot_id.id)
        th_qty = product.qty_available

        res = {
            'product_qty': self.new_quantity,
            'location_id': self.location_id.id,
            'product_id': self.product_id.id,
            'product_uom_id': self.product_id.uom_id.id,
            'theoretical_qty': th_qty,
            'prod_lot_id': self.lot_id.id,
        }

        return res

    def onchange_product_id_dict(self, product_id):
        return {
            'product_tmpl_id':
            self.env['product.product'].browse(product_id).product_tmpl_id.id,
        }

    @api.model
    def create(self, values):
        if values.get('product_id'):
            values.update(self.onchange_product_id_dict(values['product_id']))
        return super(ProductChangeQuantity, self).create(values)

    @api.constrains('new_quantity')
    def check_new_quantity(self):
        if any(wizard.new_quantity < 0 for wizard in self):
            raise UserError(_('Quantity cannot be negative.'))

    def change_product_qty(self):
        """ Changes the Product Quantity by making a Physical Inventory. """
        Inventory = self.env['stock.inventory']
        for wizard in self:
            product = wizard.product_id.with_context(
                location=wizard.location_id.id, lot_id=wizard.lot_id.id)
            line_data = wizard._action_start_line()

            if wizard.product_id.id and wizard.lot_id.id:
                inventory_filter = 'none'
            elif wizard.product_id.id:
                inventory_filter = 'product'
            else:
                inventory_filter = 'none'
            inventory = Inventory.create({
                'name':
                _('INV: %s') % tools.ustr(wizard.product_id.display_name),
                'filter':
                inventory_filter,
                'product_id':
                wizard.product_id.id,
                'location_id':
                wizard.location_id.id,
                'lot_id':
                wizard.lot_id.id,
                'line_ids': [(0, 0, line_data)],
            })
            inventory.action_done()
        return {'type': 'ir.actions.act_window_close'}
Exemple #25
0
class ProductPublicCategory(models.Model):
    _name = "product.public.category"
    _inherit = ["website.seo.metadata"]
    _description = "Website Product Category"
    _order = "sequence, name"

    def _default_website(self):
        default_website_id = self.env.ref('website.default_website')
        return [default_website_id.id] if default_website_id else None

    name = fields.Char(required=True, translate=True)
    parent_id = fields.Many2one('product.public.category',
                                string='Parent Category',
                                index=True)
    child_id = fields.One2many('product.public.category',
                               'parent_id',
                               string='Children Categories')
    sequence = fields.Integer(
        help=
        "Gives the sequence order when displaying a list of product categories."
    )
    # NOTE: there is no 'default image', because by default we don't show
    # thumbnails for categories. However if we have a thumbnail for at least one
    # category, then we display a default image on the other, so that the
    # buttons have consistent styling.
    # In this case, the default image is set by the js code.
    image = fields.Binary(
        attachment=True,
        help=
        "This field holds the image used as image for the category, limited to 1024x1024px."
    )
    image_medium = fields.Binary(
        string='Medium-sized image',
        attachment=True,
        help="Medium-sized image of the category. It is automatically "
        "resized as a 128x128px image, with aspect ratio preserved. "
        "Use this field in form views or some kanban views.")
    image_small = fields.Binary(
        string='Small-sized image',
        attachment=True,
        help="Small-sized image of the category. It is automatically "
        "resized as a 64x64px image, with aspect ratio preserved. "
        "Use this field anywhere a small image is required.")
    website_ids = fields.Many2many('website',
                                   'website_prod_public_categ_rel',
                                   'website_id',
                                   'category_id',
                                   default=_default_website,
                                   string='Websites',
                                   copy=False,
                                   help='List of websites in which '
                                   'category will published.')
    partner_tag_ids = fields.Many2many('res.partner.category',
                                       'partner_public_categ_tags_rel',
                                       'tag_id',
                                       'category_id',
                                       string='Partner Tags',
                                       help='If logged in customers/partners '
                                       'have this tag then this product '
                                       'category will appear to them in '
                                       'E-commerce website.\n\n'
                                       'If empty then it becomes general '
                                       'category which display to any '
                                       'customers/partners.')

    @api.model
    def create(self, vals):
        tools.image_resize_images(vals)
        res = super(ProductPublicCategory, self).create(vals)
        # @todo actpy:
        # Multi-Website: Check different test-cases for child & parent category
        if res.parent_id:
            res.parent_id.write({
                'website_ids':
                [(4, website_id.id) for website_id in res.website_ids]
            })
        return res

    @api.multi
    def write(self, vals):
        tools.image_resize_images(vals)
        res = super(ProductPublicCategory, self).write(vals)
        # @todo actpy:
        # Multi-Website: Check different test-cases for child & parent category
        if self.parent_id and self.website_ids.ids:
            self.parent_id.write({
                'website_ids':
                [(4, website_id.id) for website_id in self.website_ids]
            })
        if self.child_id:
            for child_id in self.child_id:
                for website_id in child_id.website_ids:
                    if website_id not in self.website_ids:
                        child_id.write({'website_ids': [(3, website_id.id)]})
        return res

    @api.constrains('parent_id')
    def check_parent_id(self):
        if not self._check_recursion():
            raise ValueError(
                _('Error ! You cannot create recursive categories.'))

    @api.multi
    def name_get(self):
        res = []
        for category in self:
            names = [category.name]
            parent_category = category.parent_id
            while parent_category:
                names.append(parent_category.name)
                parent_category = parent_category.parent_id
            res.append((category.id, ' / '.join(reversed(names))))
        return res
Exemple #26
0
class Users(models.Model):
    """ User class. A res.users record models an OpenERP user and is different
        from an employee.

        res.users class now inherits from res.partner. The partner model is
        used to store the data related to the partner: lang, name, address,
        avatar, ... The user model is now dedicated to technical data.
    """
    _name = "res.users"
    _description = 'Users'
    _inherits = {'res.partner': 'partner_id'}
    _order = 'name, login'
    __uid_cache = defaultdict(dict)             # {dbname: {uid: password}}

    # User can write on a few of his own fields (but not his groups for example)
    SELF_WRITEABLE_FIELDS = ['signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz']
    # User can read a few of his own fields
    SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update', 'action_id']

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

    def _companies_count(self):
        return self.env['res.company'].sudo().search_count([])

    partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True,
        string='Related Partner', help='Partner-related data of the user')
    login = fields.Char(required=True, help="Used to log into the system")
    password = fields.Char(default='', invisible=True, copy=False,
        help="Keep empty if you don't want the user to be able to connect on the system.")
    new_password = fields.Char(string='Set Password',
        compute='_compute_password', inverse='_inverse_password',
        help="Specify a value only when creating a user or if you're "\
             "changing the user's password, otherwise leave empty. After "\
             "a change of password, the user has to login again.")
    signature = fields.Html()
    active = fields.Boolean(default=True)
    action_id = fields.Many2one('ir.actions.actions', string='Home Action',
        help="If specified, this action will be opened at log on for this user, in addition to the standard menu.")
    groups_id = fields.Many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', string='Groups', default=_default_groups)
    log_ids = fields.One2many('res.users.log', 'create_uid', string='User log entries')
    login_date = fields.Datetime(related='log_ids.create_date', string='Latest connection')
    share = fields.Boolean(compute='_compute_share', compute_sudo=True, string='Share User', store=True,
         help="External user with limited access, created only for the purpose of sharing data.")
    companies_count = fields.Integer(compute='_compute_companies_count', string="Number of Companies", default=_companies_count)
    tz_offset = fields.Char(compute='_compute_tz_offset', string='Timezone offset', invisible=True)

    @api.model
    def _get_company(self):
        return self.env.user.company_id

    # Special behavior for this field: res.company.search() will only return the companies
    # available to the current user (should be the user's companies?), when the user_preference
    # context is set.
    company_id = fields.Many2one('res.company', string='Company', required=True, default=_get_company,
        help='The company this user is currently working for.', context={'user_preference': True})
    company_ids = fields.Many2many('res.company', 'res_company_users_rel', 'user_id', 'cid',
        string='Companies', default=_get_company)

    # overridden inherited fields to bypass access rights, in case you have
    # access to the user but not its corresponding partner
    name = fields.Char(related='partner_id.name', inherited=True)
    email = fields.Char(related='partner_id.email', inherited=True)

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

    def _compute_password(self):
        for user in self:
            user.password = ''

    def _inverse_password(self):
        for user in self:
            if not user.new_password:
                # Do not update the password if no value is provided, ignore silently.
                # For example web client submits False values for all empty fields.
                continue
            if user == self.env.user:
                # To change their own password, users must use the client-specific change password wizard,
                # so that the new password is immediately used for further RPC requests, otherwise the user
                # will face unexpected 'Access Denied' exceptions.
                raise UserError(_('Please use the change password wizard (in User Preferences or User menu) to change your own password.'))
            else:
                user.password = user.new_password

    @api.depends('groups_id')
    def _compute_share(self):
        for user in self:
            user.share = not user.has_group('base.group_user')

    @api.multi
    def _compute_companies_count(self):
        companies_count = self._companies_count()
        for user in self:
            user.companies_count = companies_count

    @api.depends('tz')
    def _compute_tz_offset(self):
        for user in self:
            user.tz_offset = datetime.datetime.now(pytz.timezone(user.tz or 'GMT')).strftime('%z')

    @api.onchange('login')
    def on_change_login(self):
        if self.login and tools.single_email_re.match(self.login):
            self.email = self.login

    @api.onchange('parent_id')
    def onchange_parent_id(self):
        return self.mapped('partner_id').onchange_parent_id()

    @api.multi
    @api.constrains('company_id', 'company_ids')
    def _check_company(self):
        if any(user.company_ids and user.company_id not in user.company_ids for user in self):
            raise ValidationError(_('The chosen company is not in the allowed companies for this user'))

    @api.multi
    @api.constrains('action_id')
    def _check_action_id(self):
        action_open_website = self.env.ref('base.action_open_website', raise_if_not_found=False)
        if action_open_website and any(user.action_id.id == action_open_website.id for user in self):
            raise ValidationError(_('The "App Switcher" action cannot be selected as home action.'))

    @api.multi
    def read(self, fields=None, load='_classic_read'):
        if fields and self == self.env.user:
            for key in fields:
                if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')):
                    break
            else:
                # safe fields only, so we read as super-user to bypass access rights
                self = self.sudo()

        result = super(Users, self).read(fields=fields, load=load)

        canwrite = self.env['ir.model.access'].check('res.users', 'write', False)
        if not canwrite:
            for vals in result:
                if vals['id'] != self._uid:
                    for key in USER_PRIVATE_FIELDS:
                        if key in vals:
                            vals[key] = '********'

        return result

    @api.model
    def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
        groupby_fields = set([groupby] if isinstance(groupby, pycompat.string_types) else groupby)
        if groupby_fields.intersection(USER_PRIVATE_FIELDS):
            raise AccessError(_("Invalid 'group by' parameter"))
        return super(Users, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)

    @api.model
    def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None):
        if self._uid != SUPERUSER_ID and args:
            domain_fields = {term[0] for term in args if isinstance(term, (tuple, list))}
            if domain_fields.intersection(USER_PRIVATE_FIELDS):
                raise AccessError(_('Invalid search criterion'))
        return super(Users, self)._search(args, offset=offset, limit=limit, order=order, count=count,
                                          access_rights_uid=access_rights_uid)

    @api.model
    def create(self, vals):
        user = super(Users, self).create(vals)
        user.partner_id.active = user.active
        if user.partner_id.company_id:
            user.partner_id.write({'company_id': user.company_id.id})
        return user

    @api.multi
    def write(self, values):
        if values.get('active') == False:
            for user in self:
                if user.id == SUPERUSER_ID:
                    raise UserError(_("You cannot deactivate the admin user."))
                elif user.id == self._uid:
                    raise UserError(_("You cannot deactivate the user you're currently logged in as."))

        if self == self.env.user:
            for key in list(values):
                if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')):
                    break
            else:
                if 'company_id' in values:
                    if values['company_id'] not in self.env.user.company_ids.ids:
                        del values['company_id']
                # safe fields only, so we write as super-user to bypass access rights
                self = self.sudo()

        res = super(Users, self).write(values)
        if 'company_id' in values:
            for user in self:
                # if partner is global we keep it that way
                if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']:
                    user.partner_id.write({'company_id': user.company_id.id})
            # clear default ir values when company changes
            self.env['ir.default'].clear_caches()

        # clear caches linked to the users
        if 'groups_id' in values:
            self.env['ir.model.access'].call_cache_clearing_methods()
            self.env['ir.rule'].clear_caches()
            self.has_group.clear_cache(self)
        if any(key.startswith('context_') or key in ('lang', 'tz') for key in values):
            self.context_get.clear_cache(self)
        if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS):
            db = self._cr.dbname
            for id in self.ids:
                self.__uid_cache[db].pop(id, None)
        if any(key in values for key in self._get_session_token_fields()):
            self._invalidate_session_cache()

        return res

    @api.multi
    def unlink(self):
        if SUPERUSER_ID in self.ids:
            raise UserError(_('You can not remove the admin user as it is used internally for resources created by actpy (updates, module installation, ...)'))
        db = self._cr.dbname
        for id in self.ids:
            self.__uid_cache[db].pop(id, None)
        self._invalidate_session_cache()
        return super(Users, self).unlink()

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

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

    @api.model
    @tools.ormcache('self._uid')
    def context_get(self):
        user = self.env.user
        result = {}
        for k in self._fields:
            if k.startswith('context_'):
                context_key = k[8:]
            elif k in ['lang', 'tz']:
                context_key = k
            else:
                context_key = False
            if context_key:
                res = getattr(user, k) or False
                if isinstance(res, models.BaseModel):
                    res = res.id
                result[context_key] = res or False
        return result

    @api.model
    @api.returns('ir.actions.act_window', lambda record: record.id)
    def action_get(self):
        return self.sudo().env.ref('base.action_res_users_my')

    def check_super(self, passwd):
        return check_super(passwd)

    @api.model
    def check_credentials(self, password):
        """ Override this method to plug additional authentication methods"""
        user = self.sudo().search([('id', '=', self._uid), ('password', '=', password)])
        if not user:
            raise AccessDenied()

    @api.model
    def _update_last_login(self):
        # only create new records to avoid any side-effect on concurrent transactions
        # extra records will be deleted by the periodical garbage collection
        self.env['res.users.log'].create({}) # populated by defaults

    @classmethod
    def _login(cls, db, login, password):
        if not password:
            return False
        user_id = False
        try:
            with cls.pool.cursor() as cr:
                self = api.Environment(cr, SUPERUSER_ID, {})[cls._name]
                user = self.search([('login', '=', login)])
                if user:
                    user_id = user.id
                    user.sudo(user_id).check_credentials(password)
                    user.sudo(user_id)._update_last_login()
        except AccessDenied:
            _logger.info("Login failed for db:%s login:%s", db, login)
            user_id = False
        return user_id

    @classmethod
    def authenticate(cls, db, login, password, user_agent_env):
        """Verifies and returns the user ID corresponding to the given
          ``login`` and ``password`` combination, or False if there was
          no matching user.
           :param str db: the database on which user is trying to authenticate
           :param str login: username
           :param str password: user password
           :param dict user_agent_env: environment dictionary describing any
               relevant environment attributes
        """
        uid = cls._login(db, login, password)
        if uid == SUPERUSER_ID:
            # Successfully logged in as admin!
            # Attempt to guess the web base url...
            if user_agent_env and user_agent_env.get('base_location'):
                try:
                    with cls.pool.cursor() as cr:
                        base = user_agent_env['base_location']
                        ICP = api.Environment(cr, uid, {})['ir.config_parameter']
                        if not ICP.get_param('web.base.url.freeze'):
                            ICP.set_param('web.base.url', base)
                except Exception:
                    _logger.exception("Failed to update web.base.url configuration parameter")
        return uid

    @classmethod
    def check(cls, db, uid, passwd):
        """Verifies that the given (uid, password) is authorized for the database ``db`` and
           raise an exception if it is not."""
        if not passwd:
            # empty passwords disallowed for obvious security reasons
            raise AccessDenied()
        db = cls.pool.db_name
        if cls.__uid_cache[db].get(uid) == passwd:
            return
        cr = cls.pool.cursor()
        try:
            self = api.Environment(cr, uid, {})[cls._name]
            self.check_credentials(passwd)
            cls.__uid_cache[db][uid] = passwd
        finally:
            cr.close()

    def _get_session_token_fields(self):
        return {'id', 'login', 'password', 'active'}

    @tools.ormcache('sid')
    def _compute_session_token(self, sid):
        """ Compute a session token given a session id and a user id """
        # retrieve the fields used to generate the session token
        session_fields = ', '.join(sorted(self._get_session_token_fields()))
        self.env.cr.execute("""SELECT %s, (SELECT value FROM ir_config_parameter WHERE key='database.secret')
                                FROM res_users
                                WHERE id=%%s""" % (session_fields), (self.id,))
        if self.env.cr.rowcount != 1:
            self._invalidate_session_cache()
            return False
        data_fields = self.env.cr.fetchone()
        # generate hmac key
        key = (u'%s' % (data_fields,)).encode('utf-8')
        # hmac the session id
        data = sid.encode('utf-8')
        h = hmac.new(key, data, sha256)
        # keep in the cache the token
        return h.hexdigest()

    @api.multi
    def _invalidate_session_cache(self):
        """ Clear the sessions cache """
        self._compute_session_token.clear_cache(self)

    @api.model
    def change_password(self, old_passwd, new_passwd):
        """Change current user password. Old password must be provided explicitly
        to prevent hijacking an existing user session, or for cases where the cleartext
        password is not used to authenticate requests.

        :return: True
        :raise: actpy.exceptions.AccessDenied when old password is wrong
        :raise: actpy.exceptions.UserError when new password is not set or empty
        """
        self.check(self._cr.dbname, self._uid, old_passwd)
        if new_passwd:
            # use self.env.user here, because it has uid=SUPERUSER_ID
            return self.env.user.write({'password': new_passwd})
        raise UserError(_("Setting empty passwords is not allowed for security reasons!"))

    @api.multi
    def preference_save(self):
        return {
            'type': 'ir.actions.client',
            'tag': 'reload_context',
        }

    @api.multi
    def preference_change_password(self):
        return {
            'type': 'ir.actions.client',
            'tag': 'change_password',
            'target': 'new',
        }

    @api.model
    def has_group(self, group_ext_id):
        # use singleton's id if called on a non-empty recordset, otherwise
        # context uid
        uid = self.id or self._uid
        return self.sudo(user=uid)._has_group(group_ext_id)

    @api.model
    @tools.ormcache('self._uid', 'group_ext_id')
    def _has_group(self, group_ext_id):
        """Checks whether user belongs to given group.

        :param str group_ext_id: external ID (XML ID) of the group.
           Must be provided in fully-qualified form (``module.ext_id``), as there
           is no implicit module to use..
        :return: True if the current user is a member of the group with the
           given external ID (XML ID), else False.
        """
        assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified"
        module, ext_id = group_ext_id.split('.')
        self._cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN
                            (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""",
                         (self._uid, module, ext_id))
        return bool(self._cr.fetchone())
    # for a few places explicitly clearing the has_group cache
    has_group.clear_cache = _has_group.clear_cache

    @api.multi
    def _is_public(self):
        self.ensure_one()
        return self.has_group('base.group_public')

    @api.multi
    def _is_system(self):
        self.ensure_one()
        return self.has_group('base.group_system')

    @api.multi
    def _is_admin(self):
        self.ensure_one()
        return self._is_superuser() or self.has_group('base.group_erp_manager')

    @api.multi
    def _is_superuser(self):
        self.ensure_one()
        return self.id == SUPERUSER_ID

    @api.model
    def get_company_currency_id(self):
        return self.env.user.company_id.currency_id.id
Exemple #27
0
class PaymentAcquirer(models.Model):
    """ Acquirer Model. Each specific acquirer can extend the model by adding
    its own fields, using the acquirer_name as a prefix for the new fields.
    Using the required_if_provider='<name>' attribute on fields it is possible
    to have required fields that depend on a specific acquirer.

    Each acquirer has a link to an ir.ui.view record that is a template of
    a button used to display the payment form. See examples in ``payment_ogone``
    and ``payment_paypal`` modules.

    Methods that should be added in an acquirer-specific implementation:

     - ``<name>_form_generate_values(self, reference, amount, currency,
       partner_id=False, partner_values=None, tx_custom_values=None)``:
       method that generates the values used to render the form button template.
     - ``<name>_get_form_action_url(self):``: method that returns the url of
       the button form. It is used for example in ecommerce application if you
       want to post some data to the acquirer.
     - ``<name>_compute_fees(self, amount, currency_id, country_id)``: computes
       the fees of the acquirer, using generic fields defined on the acquirer
       model (see fields definition).

    Each acquirer should also define controllers to handle communication between
    OpenERP and the acquirer. It generally consists in return urls given to the
    button form and that the acquirer uses to send the customer back after the
    transaction, with transaction details given as a POST request.
    """
    _name = 'payment.acquirer'
    _description = 'Payment Acquirer'
    _order = 'website_published desc, sequence, name'

    name = fields.Char('Name', required=True, translate=True)
    description = fields.Html('Description')
    sequence = fields.Integer('Sequence',
                              default=10,
                              help="Determine the display order")
    provider = fields.Selection(selection=[('manual', 'Manual Configuration')],
                                string='Provider',
                                default='manual',
                                required=True)
    company_id = fields.Many2one(
        'res.company',
        'Company',
        default=lambda self: self.env.user.company_id.id,
        required=True)
    view_template_id = fields.Many2one('ir.ui.view',
                                       'Form Button Template',
                                       required=True)
    registration_view_template_id = fields.Many2one(
        'ir.ui.view',
        'S2S Form Template',
        domain=[('type', '=', 'qweb')],
        help="Template for method registration")
    environment = fields.Selection([('test', 'Test'), ('prod', 'Production')],
                                   string='Environment',
                                   default='test',
                                   oldname='env',
                                   required=True)
    website_published = fields.Boolean(
        'Visible in Portal / Website',
        copy=False,
        help="Make this payment acquirer available (Customer invoices, etc.)")
    # Formerly associated to `authorize` option from auto_confirm
    capture_manually = fields.Boolean(
        string="Capture Amount Manually",
        help="Capture the amount from actpy, when the delivery is completed.")
    # Formerly associated to `generate_and_pay_invoice` option from auto_confirm
    journal_id = fields.Many2one(
        'account.journal',
        'Payment Journal',
        domain=[('type', 'in', ['bank', 'cash'])],
        default=lambda self: self.env['account.journal'].search(
            [('type', 'in', ['bank', 'cash'])], limit=1),
        help=
        """Payments will be registered into this journal. If you get paid straight on your bank account,
                select your bank account. If you get paid in batch for several transactions, create a specific
                payment journal for this payment acquirer to easily manage the bank reconciliation. You hold
                the amount in a temporary transfer account of your books (created automatically when you create
                the payment journal). Then when you get paid on your bank account by the payment acquirer, you
                reconcile the bank statement line with this temporary transfer account. Use reconciliation
                templates to do it in one-click.""")
    specific_countries = fields.Boolean(
        string="Specific Countries",
        help=
        "If you leave it empty, the payment acquirer will be available for all the countries."
    )
    country_ids = fields.Many2many(
        'res.country',
        'payment_country_rel',
        'payment_id',
        'country_id',
        'Countries',
        help=
        "This payment gateway is available for selected countries. If none is selected it is available for all countries."
    )

    pre_msg = fields.Html(
        'Help Message',
        translate=True,
        help='Message displayed to explain and help the payment process.')
    post_msg = fields.Html(
        'Thanks Message',
        translate=True,
        help='Message displayed after having done the payment process.')
    pending_msg = fields.Html(
        'Pending Message',
        translate=True,
        default=lambda s:
        _('<i>Pending,</i> Your online payment has been successfully processed. But your order is not validated yet.'
          ),
        help=
        'Message displayed, if order is in pending state after having done the payment process.'
    )
    done_msg = fields.Html(
        'Done Message',
        translate=True,
        default=lambda s:
        _('<i>Done,</i> Your online payment has been successfully processed. Thank you for your order.'
          ),
        help=
        'Message displayed, if order is done successfully after having done the payment process.'
    )
    cancel_msg = fields.Html(
        'Cancel Message',
        translate=True,
        default=lambda s: _('<i>Cancel,</i> Your payment has been cancelled.'),
        help='Message displayed, if order is cancel during the payment process.'
    )
    error_msg = fields.Html(
        'Error Message',
        translate=True,
        default=lambda s:
        _('<i>Error,</i> Please be aware that an error occurred during the transaction. The order has been confirmed but will not be paid. Do not hesitate to contact us if you have any questions on the status of your order.'
          ),
        help='Message displayed, if error is occur during the payment process.'
    )
    save_token = fields.Selection(
        [('none', 'Never'), ('ask', 'Let the customer decide'),
         ('always', 'Always')],
        string='Save Cards',
        default='none',
        help=
        "This option allows customers to save their credit card as a payment token and to reuse it for a later purchase. "
        "If you manage subscriptions (recurring invoicing), you need it to automatically charge the customer when you "
        "issue an invoice.")
    token_implemented = fields.Boolean('Saving Card Data supported',
                                       compute='_compute_feature_support',
                                       search='_search_is_tokenized')
    authorize_implemented = fields.Boolean('Authorize Mechanism Supported',
                                           compute='_compute_feature_support')
    fees_implemented = fields.Boolean('Fees Computation Supported',
                                      compute='_compute_feature_support')
    fees_active = fields.Boolean('Add Extra Fees')
    fees_dom_fixed = fields.Float('Fixed domestic fees')
    fees_dom_var = fields.Float('Variable domestic fees (in percents)')
    fees_int_fixed = fields.Float('Fixed international fees')
    fees_int_var = fields.Float('Variable international fees (in percents)')

    # TDE FIXME: remove that brol
    module_id = fields.Many2one('ir.module.module',
                                string='Corresponding Module')
    module_state = fields.Selection(selection=module.STATES,
                                    string='Installation State',
                                    related='module_id.state')

    image = fields.Binary(
        "Image",
        attachment=True,
        help=
        "This field holds the image used for this provider, limited to 1024x1024px"
    )
    image_medium = fields.Binary(
        "Medium-sized image",
        attachment=True,
        help="Medium-sized image of this provider. It is automatically "
        "resized as a 128x128px image, with aspect ratio preserved. "
        "Use this field in form views or some kanban views.")
    image_small = fields.Binary(
        "Small-sized image",
        attachment=True,
        help="Small-sized image of this provider. It is automatically "
        "resized as a 64x64px image, with aspect ratio preserved. "
        "Use this field anywhere a small image is required.")

    payment_icon_ids = fields.Many2many('payment.icon',
                                        string='Supported Payment Icons')
    payment_flow = fields.Selection(
        selection=[('form', 'Redirection to the acquirer website'),
                   ('s2s', 'Payment from Odoo')],
        default='form',
        required=True,
        string='Payment Flow',
        help=
        """Note: Subscriptions does not take this field in account, it uses server to server by default."""
    )

    def _search_is_tokenized(self, operator, value):
        tokenized = self._get_feature_support()['tokenize']
        if (operator, value) in [('=', True), ('!=', False)]:
            return [('provider', 'in', tokenized)]
        return [('provider', 'not in', tokenized)]

    @api.multi
    def _compute_feature_support(self):
        feature_support = self._get_feature_support()
        for acquirer in self:
            acquirer.fees_implemented = acquirer.provider in feature_support[
                'fees']
            acquirer.authorize_implemented = acquirer.provider in feature_support[
                'authorize']
            acquirer.token_implemented = acquirer.provider in feature_support[
                'tokenize']

    @api.multi
    def _check_required_if_provider(self):
        """ If the field has 'required_if_provider="<provider>"' attribute, then it
        required if record.provider is <provider>. """
        for acquirer in self:
            if any(
                    getattr(f, 'required_if_provider', None) ==
                    acquirer.provider and not acquirer[k]
                    for k, f in self._fields.items()):
                return False
        return True

    _constraints = [
        (_check_required_if_provider, 'Required fields not filled', []),
    ]

    def _get_feature_support(self):
        """Get advanced feature support by provider.

        Each provider should add its technical in the corresponding
        key for the following features:
            * fees: support payment fees computations
            * authorize: support authorizing payment (separates
                         authorization and capture)
            * tokenize: support saving payment data in a payment.tokenize
                        object
        """
        return dict(authorize=[], tokenize=[], fees=[])

    @api.model
    def create(self, vals):
        image_resize_images(vals)
        return super(PaymentAcquirer, self).create(vals)

    @api.multi
    def write(self, vals):
        image_resize_images(vals)
        return super(PaymentAcquirer, self).write(vals)

    @api.multi
    def toggle_website_published(self):
        self.write({'website_published': not self.website_published})
        return True

    @api.multi
    def get_form_action_url(self):
        """ Returns the form action URL, for form-based acquirer implementations. """
        if hasattr(self, '%s_get_form_action_url' % self.provider):
            return getattr(self, '%s_get_form_action_url' % self.provider)()
        return False

    def _get_available_payment_input(self, partner=None, company=None):
        """ Generic (model) method that fetches available payment mechanisms
        to use in all portal / eshop pages that want to use the payment form.

        It contains

         * form_acquirers: record set of acquirers based on a local form that
                           sends customer to the acquirer website;
         * s2s_acquirers: reset set of acquirers that send customer data to
                          acquirer without redirecting to any other website;
         * pms: record set of stored credit card data (aka payment.token)
                connected to a given partner to allow customers to reuse them """
        if not company:
            company = self.env.user.company_id
        if not partner:
            partner = self.env.user.partner_id
        active_acquirers = self.sudo().search([
            ('website_published', '=', True), ('company_id', '=', company.id)
        ])
        form_acquirers = active_acquirers.filtered(
            lambda acq: acq.payment_flow == 'form' and acq.view_template_id)
        s2s_acquirers = active_acquirers.filtered(
            lambda acq: acq.payment_flow == 's2s' and acq.
            registration_view_template_id)
        return {
            'form_acquirers':
            form_acquirers,
            's2s_acquirers':
            s2s_acquirers,
            'pms':
            self.env['payment.token'].search([('partner_id', '=', partner.id),
                                              ('acquirer_id', 'in',
                                               s2s_acquirers.ids)]),
        }

    @api.multi
    def render(self,
               reference,
               amount,
               currency_id,
               partner_id=False,
               values=None):
        """ Renders the form template of the given acquirer as a qWeb template.
        :param string reference: the transaction reference
        :param float amount: the amount the buyer has to pay
        :param currency_id: currency id
        :param dict partner_id: optional partner_id to fill values
        :param dict values: a dictionary of values for the transction that is
        given to the acquirer-specific method generating the form values

        All templates will receive:

         - acquirer: the payment.acquirer browse record
         - user: the current user browse record
         - currency_id: id of the transaction currency
         - amount: amount of the transaction
         - reference: reference of the transaction
         - partner_*: partner-related values
         - partner: optional partner browse record
         - 'feedback_url': feedback URL, controler that manage answer of the acquirer (without base url) -> FIXME
         - 'return_url': URL for coming back after payment validation (wihout base url) -> FIXME
         - 'cancel_url': URL if the client cancels the payment -> FIXME
         - 'error_url': URL if there is an issue with the payment -> FIXME
         - context: actpy context

        """
        if values is None:
            values = {}

        # reference and amount
        values.setdefault('reference', reference)
        amount = float_round(amount, 2)
        values.setdefault('amount', amount)

        # currency id
        currency_id = values.setdefault('currency_id', currency_id)
        if currency_id:
            currency = self.env['res.currency'].browse(currency_id)
        else:
            currency = self.env.user.company_id.currency_id
        values['currency'] = currency

        # Fill partner_* using values['partner_id'] or partner_id argument
        partner_id = values.get('partner_id', partner_id)
        billing_partner_id = values.get('billing_partner_id', partner_id)
        if partner_id:
            partner = self.env['res.partner'].browse(partner_id)
            if partner_id != billing_partner_id:
                billing_partner = self.env['res.partner'].browse(
                    billing_partner_id)
            else:
                billing_partner = partner
            values.update({
                'partner':
                partner,
                'partner_id':
                partner_id,
                'partner_name':
                partner.name,
                'partner_lang':
                partner.lang,
                'partner_email':
                partner.email,
                'partner_zip':
                partner.zip,
                'partner_city':
                partner.city,
                'partner_address':
                _partner_format_address(partner.street, partner.street2),
                'partner_country_id':
                partner.country_id.id,
                'partner_country':
                partner.country_id,
                'partner_phone':
                partner.phone,
                'partner_state':
                partner.state_id,
                'billing_partner':
                billing_partner,
                'billing_partner_id':
                billing_partner_id,
                'billing_partner_name':
                billing_partner.name,
                'billing_partner_commercial_company_name':
                billing_partner.commercial_company_name,
                'billing_partner_lang':
                billing_partner.lang,
                'billing_partner_email':
                billing_partner.email,
                'billing_partner_zip':
                billing_partner.zip,
                'billing_partner_city':
                billing_partner.city,
                'billing_partner_address':
                _partner_format_address(billing_partner.street,
                                        billing_partner.street2),
                'billing_partner_country_id':
                billing_partner.country_id.id,
                'billing_partner_country':
                billing_partner.country_id,
                'billing_partner_phone':
                billing_partner.phone,
                'billing_partner_state':
                billing_partner.state_id,
            })
        if values.get('partner_name'):
            values.update({
                'partner_first_name':
                _partner_split_name(values.get('partner_name'))[0],
                'partner_last_name':
                _partner_split_name(values.get('partner_name'))[1],
            })
        if values.get('billing_partner_name'):
            values.update({
                'billing_partner_first_name':
                _partner_split_name(values.get('billing_partner_name'))[0],
                'billing_partner_last_name':
                _partner_split_name(values.get('billing_partner_name'))[1],
            })

        # Fix address, country fields
        if not values.get('partner_address'):
            values['address'] = _partner_format_address(
                values.get('partner_street', ''),
                values.get('partner_street2', ''))
        if not values.get('partner_country') and values.get(
                'partner_country_id'):
            values['country'] = self.env['res.country'].browse(
                values.get('partner_country_id'))
        if not values.get('billing_partner_address'):
            values['billing_address'] = _partner_format_address(
                values.get('billing_partner_street', ''),
                values.get('billing_partner_street2', ''))
        if not values.get('billing_partner_country') and values.get(
                'billing_partner_country_id'):
            values['billing_country'] = self.env['res.country'].browse(
                values.get('billing_partner_country_id'))

        # compute fees
        fees_method_name = '%s_compute_fees' % self.provider
        if hasattr(self, fees_method_name):
            fees = getattr(self,
                           fees_method_name)(values['amount'],
                                             values['currency_id'],
                                             values.get('partner_country_id'))
            values['fees'] = float_round(fees, 2)

        # call <name>_form_generate_values to update the tx dict with acqurier specific values
        cust_method_name = '%s_form_generate_values' % (self.provider)
        if hasattr(self, cust_method_name):
            method = getattr(self, cust_method_name)
            values = method(values)

        values.update({
            'tx_url':
            self._context.get('tx_url', self.get_form_action_url()),
            'submit_class':
            self._context.get('submit_class', 'btn btn-link'),
            'submit_txt':
            self._context.get('submit_txt'),
            'acquirer':
            self,
            'user':
            self.env.user,
            'context':
            self._context,
            'type':
            values.get('type') or 'form',
        })
        values.setdefault('return_url', False)

        return self.view_template_id.render(values, engine='ir.qweb')

    def get_s2s_form_xml_id(self):
        if self.registration_view_template_id:
            model_data = self.env['ir.model.data'].search([
                ('model', '=', 'ir.ui.view'),
                ('res_id', '=', self.registration_view_template_id.id)
            ])
            return ('%s.%s') % (model_data.module, model_data.name)
        return False

    @api.multi
    def s2s_process(self, data):
        cust_method_name = '%s_s2s_form_process' % (self.provider)
        if not self.s2s_validate(data):
            return False
        if hasattr(self, cust_method_name):
            # As this method may be called in JSON and overriden in various addons
            # let us raise interesting errors before having stranges crashes
            if not data.get('partner_id'):
                raise ValueError(
                    _('Missing partner reference when trying to create a new payment token'
                      ))
            method = getattr(self, cust_method_name)
            return method(data)
        return True

    @api.multi
    def s2s_validate(self, data):
        cust_method_name = '%s_s2s_form_validate' % (self.provider)
        if hasattr(self, cust_method_name):
            method = getattr(self, cust_method_name)
            return method(data)
        return True

    @api.multi
    def toggle_environment_value(self):
        prod = self.filtered(lambda acquirer: acquirer.environment == 'prod')
        prod.write({'environment': 'test'})
        (self - prod).write({'environment': 'prod'})

    @api.multi
    def button_immediate_install(self):
        # TDE FIXME: remove that brol
        if self.module_id and self.module_state != 'installed':
            self.module_id.button_immediate_install()
            return {
                'type': 'ir.actions.client',
                'tag': 'reload',
            }
Exemple #28
0
class Groups(models.Model):
    _name = "res.groups"
    _description = "Access Groups"
    _rec_name = 'full_name'
    _order = 'name'

    name = fields.Char(required=True, translate=True)
    users = fields.Many2many('res.users', 'res_groups_users_rel', 'gid', 'uid')
    model_access = fields.One2many('ir.model.access', 'group_id', string='Access Controls', copy=True)
    rule_groups = fields.Many2many('ir.rule', 'rule_group_rel',
        'group_id', 'rule_group_id', string='Rules', domain=[('global', '=', False)])
    menu_access = fields.Many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', string='Access Menu')
    view_access = fields.Many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', string='Views')
    comment = fields.Text(translate=True)
    category_id = fields.Many2one('ir.module.category', string='Application', index=True)
    color = fields.Integer(string='Color Index')
    full_name = fields.Char(compute='_compute_full_name', string='Group Name', search='_search_full_name')
    share = fields.Boolean(string='Share Group', help="Group created to set access rights for sharing data with some users.")
    is_portal = fields.Boolean('Portal', help="If checked, this group is usable as a portal.")

    _sql_constraints = [
        ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique within an application!')
    ]

    @api.depends('category_id.name', 'name')
    def _compute_full_name(self):
        # Important: value must be stored in environment of group, not group1!
        for group, group1 in pycompat.izip(self, self.sudo()):
            if group1.category_id:
                group.full_name = '%s / %s' % (group1.category_id.name, group1.name)
            else:
                group.full_name = group1.name

    def _search_full_name(self, operator, operand):
        lst = True
        if isinstance(operand, bool):
            domains = [[('name', operator, operand)], [('category_id.name', operator, operand)]]
            if operator in expression.NEGATIVE_TERM_OPERATORS == (not operand):
                return expression.AND(domains)
            else:
                return expression.OR(domains)
        if isinstance(operand, pycompat.string_types):
            lst = False
            operand = [operand]
        where = []
        for group in operand:
            values = [v for v in group.split('/') if v]
            group_name = values.pop().strip()
            category_name = values and '/'.join(values).strip() or group_name
            group_domain = [('name', operator, lst and [group_name] or group_name)]
            category_domain = [('category_id.name', operator, lst and [category_name] or category_name)]
            if operator in expression.NEGATIVE_TERM_OPERATORS and not values:
                category_domain = expression.OR([category_domain, [('category_id', '=', False)]])
            if (operator in expression.NEGATIVE_TERM_OPERATORS) == (not values):
                sub_where = expression.AND([group_domain, category_domain])
            else:
                sub_where = expression.OR([group_domain, category_domain])
            if operator in expression.NEGATIVE_TERM_OPERATORS:
                where = expression.AND([where, sub_where])
            else:
                where = expression.OR([where, sub_where])
        return where

    @api.model
    def search(self, args, offset=0, limit=None, order=None, count=False):
        # add explicit ordering if search is sorted on full_name
        if order and order.startswith('full_name'):
            groups = super(Groups, self).search(args)
            groups = groups.sorted('full_name', reverse=order.endswith('DESC'))
            groups = groups[offset:offset+limit] if limit else groups[offset:]
            return len(groups) if count else groups.ids
        return super(Groups, self).search(args, offset=offset, limit=limit, order=order, count=count)

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

    @api.multi
    def write(self, vals):
        if 'name' in vals:
            if vals['name'].startswith('-'):
                raise UserError(_('The name of the group can not start with "-"'))
        # invalidate caches before updating groups, since the recomputation of
        # field 'share' depends on method has_group()
        self.env['ir.model.access'].call_cache_clearing_methods()
        self.env['res.users'].has_group.clear_cache(self.env['res.users'])
        return super(Groups, self).write(vals)
Exemple #29
0
class PaymentToken(models.Model):
    _name = 'payment.token'
    _order = 'partner_id, id desc'

    name = fields.Char('Name', help='Name of the payment token')
    short_name = fields.Char('Short name', compute='_compute_short_name')
    partner_id = fields.Many2one('res.partner', 'Partner', required=True)
    acquirer_id = fields.Many2one('payment.acquirer',
                                  'Acquirer Account',
                                  required=True)
    acquirer_ref = fields.Char('Acquirer Ref.', required=True)
    active = fields.Boolean('Active', default=True)
    payment_ids = fields.One2many('payment.transaction', 'payment_token_id',
                                  'Payment Transactions')
    verified = fields.Boolean(string='Verified', default=False)

    @api.model
    def create(self, values):
        # call custom create method if defined (i.e. ogone_create for ogone)
        if values.get('acquirer_id'):
            acquirer = self.env['payment.acquirer'].browse(
                values['acquirer_id'])

            # custom create
            custom_method_name = '%s_create' % acquirer.provider
            if hasattr(self, custom_method_name):
                values.update(getattr(self, custom_method_name)(values))
                # remove all non-model fields used by (provider)_create method to avoid warning
                fields_wl = set(self._fields) & set(values)
                values = {field: values[field] for field in fields_wl}
        return super(PaymentToken, self).create(values)

    """
        @TBE: stolen shamelessly from there https://www.paypal.com/us/selfhelp/article/why-is-there-a-$1.95-charge-on-my-card-statement-faq554
        Most of them are ~1.50€s
        TODO: See this with @AL & @DBO
    """
    VALIDATION_AMOUNTS = {
        'CAD': 2.45,
        'EUR': 1.50,
        'GBP': 1.00,
        'JPY': 200,
        'AUD': 2.00,
        'NZD': 3.00,
        'CHF': 3.00,
        'HKD': 15.00,
        'SEK': 15.00,
        'DKK': 12.50,
        'PLN': 6.50,
        'NOK': 15.00,
        'HUF': 400.00,
        'CZK': 50.00,
        'BRL': 4.00,
        'MYR': 10.00,
        'MXN': 20.00,
        'ILS': 8.00,
        'PHP': 100.00,
        'TWD': 70.00,
        'THB': 70.00
    }

    @api.model
    def validate(self, **kwargs):
        """
            This method allow to verify if this payment method is valid or not.
            It does this by withdrawing a certain amount and then refund it right after.
        """
        currency = self.partner_id.currency_id

        if self.VALIDATION_AMOUNTS.get(currency.name):
            amount = self.VALIDATION_AMOUNTS.get(currency.name)
        else:
            # If we don't find the user's currency, then we set the currency to EUR and the amount to 1€50.
            currency = self.env['res.currency'].search([('name', '=', 'EUR')])
            amount = 1.5

        if len(currency) != 1:
            _logger.error(
                "Error 'EUR' currency not found for payment method validation!"
            )
            return False

        reference = "VALIDATION-%s-%s" % (
            self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S'))
        tx = self.env['payment.transaction'].sudo().create({
            'amount':
            amount,
            'acquirer_id':
            self.acquirer_id.id,
            'type':
            'validation',
            'currency_id':
            currency.id,
            'reference':
            reference,
            'payment_token_id':
            self.id,
            'partner_id':
            self.partner_id.id,
            'partner_country_id':
            self.partner_id.country_id.id,
        })

        kwargs.update({'3d_secure': True})
        tx.s2s_do_transaction(**kwargs)

        # if 3D secure is called, then we do not refund right now
        if not tx.html_3ds:
            tx.s2s_do_refund()

        return tx

    @api.multi
    @api.depends('name')
    def _compute_short_name(self):
        for token in self:
            token.short_name = token.name.replace('XXXXXXXXXXXX', '***')

    @api.multi
    def get_linked_records(self):
        """ This method returns a dict containing all the records linked to the payment.token (e.g Subscriptions),
            the key is the id of the payment.token and the value is an array that must follow the scheme below.

            {
                token_id: [
                    'description': The model description (e.g 'Sale Subscription'),
                    'id': The id of the record,
                    'name': The name of the record,
                    'url': The url to access to this record.
                ]
            }
        """
        return {r.id: [] for r in self}
Exemple #30
0
class ResCompany(models.Model):
    _inherit = 'res.company'

    # To do in master : refactor to set sequences more generic

    l10n_fr_secure_sequence_id = fields.Many2one(
        'ir.sequence',
        'Sequence to use to ensure the securisation of data',
        readonly=True)

    @api.model
    def create(self, vals):
        company = super(ResCompany, self).create(vals)
        #when creating a new french company, create the securisation sequence as well
        if company._is_accounting_unalterable():
            sequence_fields = ['l10n_fr_secure_sequence_id']
            company._create_secure_sequence(sequence_fields)
        return company

    @api.multi
    def write(self, vals):
        res = super(ResCompany, self).write(vals)
        #if country changed to fr, create the securisation sequence
        for company in self:
            if company._is_accounting_unalterable():
                sequence_fields = ['l10n_fr_secure_sequence_id']
                company._create_secure_sequence(sequence_fields)
        return res

    def _create_secure_sequence(self, sequence_fields):
        """This function creates a no_gap sequence on each companies in self that will ensure
        a unique number is given to all posted account.move in such a way that we can always
        find the previous move of a journal entry.
        """
        for company in self:
            vals_write = {}
            for seq_field in sequence_fields:
                if not company[seq_field]:
                    vals = {
                        'name':
                        'French Securisation of ' + seq_field + ' - ' +
                        company.name,
                        'code':
                        'FRSECUR',
                        'implementation':
                        'no_gap',
                        'prefix':
                        '',
                        'suffix':
                        '',
                        'padding':
                        0,
                        'company_id':
                        company.id
                    }
                    seq = self.env['ir.sequence'].create(vals)
                    vals_write[seq_field] = seq.id
            if vals_write:
                company.write(vals_write)

    def _is_vat_french(self):
        return self.vat and self.vat.startswith('FR') and len(self.vat) == 13

    def _is_accounting_unalterable(self):
        if not self.vat and not self.country_id:
            return False
        return self.country_id and self.country_id.code in UNALTERABLE_COUNTRIES or self._is_vat_french(
        )