Пример #1
0
class MembershipLine(models.Model):
    _name = 'membership.membership_line'
    _rec_name = 'partner'
    _order = 'id desc'
    _description = 'Membership Line'

    partner = fields.Many2one('res.partner',
                              string='Partner',
                              ondelete='cascade',
                              index=True)
    membership_id = fields.Many2one('product.product',
                                    string="Membership",
                                    required=True)
    date_from = fields.Date(string='From', readonly=True)
    date_to = fields.Date(string='To', readonly=True)
    date_cancel = fields.Date(string='Cancel date')
    date = fields.Date(string='Join Date',
                       help="Date on which member has joined the membership")
    member_price = fields.Float(string='Membership Fee',
                                digits='Product Price',
                                required=True,
                                help='Amount for the membership')
    account_invoice_line = fields.Many2one('account.move.line',
                                           string='Account Invoice line',
                                           readonly=True,
                                           ondelete='cascade')
    account_invoice_id = fields.Many2one(
        'account.move',
        related='account_invoice_line.move_id',
        string='Invoice',
        readonly=True)
    company_id = fields.Many2one(
        'res.company',
        related='account_invoice_line.move_id.company_id',
        string="Company",
        readonly=True,
        store=True)
    state = fields.Selection(
        STATE,
        compute='_compute_state',
        string='Membership Status',
        store=True,
        help="It indicates the membership status.\n"
        "-Non Member: A member who has not applied for any membership.\n"
        "-Cancelled Member: A member who has cancelled his membership.\n"
        "-Old Member: A member whose membership date has expired.\n"
        "-Waiting Member: A member who has applied for the membership and whose invoice is going to be created.\n"
        "-Invoiced Member: A member whose invoice has been created.\n"
        "-Paid Member: A member who has paid the membership amount.")

    @api.depends('account_invoice_id.state',
                 'account_invoice_id.amount_residual',
                 'account_invoice_id.invoice_payment_state')
    def _compute_state(self):
        """Compute the state lines """
        if not self:
            return

        self._cr.execute(
            '''
            SELECT reversed_entry_id, COUNT(id)
            FROM account_move
            WHERE reversed_entry_id IN %s
            GROUP BY reversed_entry_id
        ''', [tuple(self.mapped('account_invoice_id.id'))])
        reverse_map = dict(self._cr.fetchall())
        for line in self:
            move_state = line.account_invoice_id.state
            payment_state = line.account_invoice_id.invoice_payment_state

            line.state = 'none'
            if move_state == 'draft':
                line.state = 'waiting'
            elif move_state == 'posted':
                if payment_state == 'paid':
                    if reverse_map.get(line.account_invoice_id.id):
                        line.state = 'canceled'
                    else:
                        line.state = 'paid'
                elif payment_state == 'in_payment':
                    line.state = 'paid'
                elif payment_state == 'not_paid':
                    line.state = 'invoiced'
            elif move_state == 'cancel':
                line.state = 'canceled'
Пример #2
0
class TrialBalanceReportAccount(models.TransientModel):
    _name = 'report_trial_balance_account'
    _inherit = 'account_financial_report_abstract'
    _order = 'sequence, code ASC, name'

    report_id = fields.Many2one(
        comodel_name='report_trial_balance',
        ondelete='cascade',
        index=True
    )
    hide_line = fields.Boolean(compute='_compute_hide_line')
    # Data fields, used to keep link with real object.
    # Sequence is a Char later built with 'parent_path' for groups
    # and parent_path + account code for accounts
    sequence = fields.Char(index=True, default='1')
    level = fields.Integer(index=True, default=1)

    # Data fields, used to keep link with real object
    account_id = fields.Many2one(
        'account.account',
        index=True
    )

    account_group_id = fields.Many2one(
        'account.group',
        index=True
    )
    parent_id = fields.Many2one(
        'account.group',
        index=True
    )
    child_account_ids = fields.Char(
        string="Child accounts")
    compute_account_ids = fields.Many2many(
        'account.account',
        string="Compute accounts", store=True)

    # Data fields, used for report display
    code = fields.Char()
    name = fields.Char()

    currency_id = fields.Many2one('res.currency')
    initial_balance = fields.Float(digits=(16, 2))
    initial_balance_foreign_currency = fields.Float(digits=(16, 2))
    debit = fields.Float(digits=(16, 2))
    credit = fields.Float(digits=(16, 2))
    period_balance = fields.Float(digits=(16, 2))
    final_balance = fields.Float(digits=(16, 2))
    final_balance_foreign_currency = fields.Float(digits=(16, 2))

    # Data fields, used to browse report data
    partner_ids = fields.One2many(
        comodel_name='report_trial_balance_partner',
        inverse_name='report_account_id'
    )

    @api.depends(
        'currency_id',
        'report_id',
        'report_id.hide_account_at_0',
        'report_id.limit_hierarchy_level',
        'report_id.show_hierarchy_level',
        'initial_balance',
        'final_balance',
        'debit',
        'credit',
    )
    def _compute_hide_line(self):
        for rec in self:
            report = rec.report_id
            r = (rec.currency_id or report.company_id.currency_id).rounding
            if report.hide_account_at_0 and (
                    float_is_zero(rec.initial_balance, precision_rounding=r)
                    and float_is_zero(rec.final_balance, precision_rounding=r)
                    and float_is_zero(rec.debit, precision_rounding=r)
                    and float_is_zero(rec.credit, precision_rounding=r)):
                rec.hide_line = True
            elif report.limit_hierarchy_level and report.show_hierarchy_level:
                if report.hide_parent_hierarchy_level:
                    distinct_level = rec.level != report.show_hierarchy_level
                    if rec.account_group_id and distinct_level:
                        rec.hide_line = True
                    elif rec.level and distinct_level:
                        rec.hide_line = True
                elif not report.hide_parent_hierarchy_level and \
                        rec.level > report.show_hierarchy_level:
                    rec.hide_line = True
Пример #3
0
class PosPayment(models.Model):
    """ Used to register payments made in a pos.order.

    See `payment_ids` field of pos.order model.
    The main characteristics of pos.payment can be read from
    `payment_method_id`.
    """

    _name = "pos.payment"
    _description = "Point of Sale Payments"
    _order = "id desc"

    name = fields.Char(string='Label', readonly=True)
    pos_order_id = fields.Many2one('pos.order', string='Order', required=True)
    amount = fields.Monetary(string='Amount',
                             required=True,
                             currency_field='currency_id',
                             readonly=True,
                             help="Total amount of the payment.")
    payment_method_id = fields.Many2one('pos.payment.method',
                                        string='Payment Method',
                                        required=True)
    payment_date = fields.Datetime(string='Date',
                                   required=True,
                                   readonly=True,
                                   default=lambda self: fields.Datetime.now())
    currency_id = fields.Many2one('res.currency',
                                  string='Currency',
                                  related='pos_order_id.currency_id')
    currency_rate = fields.Float(
        string='Conversion Rate',
        related='pos_order_id.currency_rate',
        help='Conversion rate from company currency to order currency.')
    partner_id = fields.Many2one('res.partner',
                                 string='Customer',
                                 related='pos_order_id.partner_id')
    session_id = fields.Many2one('pos.session',
                                 string='Session',
                                 related='pos_order_id.session_id',
                                 store=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 related='pos_order_id.company_id')
    card_type = fields.Char('Type of card used')
    transaction_id = fields.Char('Payment Transaction ID')

    @api.model
    def name_get(self):
        res = []
        for payment in self:
            if payment.name:
                res.append((payment.id, _('%s %s') %
                            (payment.name,
                             formatLang(self.env,
                                        payment.amount,
                                        currency_obj=payment.currency_id))))
            else:
                res.append((payment.id,
                            formatLang(self.env,
                                       payment.amount,
                                       currency_obj=payment.currency_id)))
        return res
Пример #4
0
class AssetAssetReport(models.Model):
    _name = "asset.asset.report"
    _description = "Assets Analysis"
    _auto = False

    name = fields.Char(string='Year', required=False, readonly=True)
    date = fields.Date(readonly=True)
    depreciation_date = fields.Date(string='Depreciation Date', readonly=True)
    asset_id = fields.Many2one('account.asset.asset',
                               string='Asset',
                               readonly=True)
    asset_category_id = fields.Many2one('account.asset.category',
                                        string='Asset category',
                                        readonly=True)
    partner_id = fields.Many2one('res.partner',
                                 string='Partner',
                                 readonly=True)
    state = fields.Selection([('draft', 'Draft'), ('open', 'Running'),
                              ('close', 'Close')],
                             string='Status',
                             readonly=True)
    depreciation_value = fields.Float(string='Amount of Depreciation Lines',
                                      readonly=True)
    installment_value = fields.Float(string='Amount of Installment Lines',
                                     readonly=True)
    move_check = fields.Boolean(string='Posted', readonly=True)
    installment_nbr = fields.Integer(string='Installment Count', readonly=True)
    depreciation_nbr = fields.Integer(string='Depreciation Count',
                                      readonly=True)
    gross_value = fields.Float(string='Gross Amount', readonly=True)
    posted_value = fields.Float(string='Posted Amount', readonly=True)
    unposted_value = fields.Float(string='Unposted Amount', readonly=True)
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 readonly=True)

    def init(self):
        tools.drop_view_if_exists(self._cr, 'asset_asset_report')
        self._cr.execute("""
            create or replace view asset_asset_report as (
                select
                    min(dl.id) as id,
                    dl.name as name,
                    dl.depreciation_date as depreciation_date,
                    a.date as date,
                    (CASE WHEN dlmin.id = min(dl.id)
                      THEN a.value
                      ELSE 0
                      END) as gross_value,
                    dl.amount as depreciation_value,
                    dl.amount as installment_value,
                    (CASE WHEN dl.move_check
                      THEN dl.amount
                      ELSE 0
                      END) as posted_value,
                    (CASE WHEN NOT dl.move_check
                      THEN dl.amount
                      ELSE 0
                      END) as unposted_value,
                    dl.asset_id as asset_id,
                    dl.move_check as move_check,
                    a.category_id as asset_category_id,
                    a.partner_id as partner_id,
                    a.state as state,
                    count(dl.*) as installment_nbr,
                    count(dl.*) as depreciation_nbr,
                    a.company_id as company_id
                from account_asset_depreciation_line dl
                    left join account_asset_asset a on (dl.asset_id=a.id)
                    left join (select min(d.id) as id,ac.id as ac_id from account_asset_depreciation_line as d inner join account_asset_asset as ac ON (ac.id=d.asset_id) group by ac_id) as dlmin on dlmin.ac_id=a.id
                where a.active is true 
                group by
                    dl.amount,dl.asset_id,dl.depreciation_date,dl.name,
                    a.date, dl.move_check, a.state, a.category_id, a.partner_id, a.company_id,
                    a.value, a.id, a.salvage_value, dlmin.id
        )""")
Пример #5
0
class LeaveReport(models.Model):
    _name = "hr.leave.report"
    _description = 'Time Off Summary / Report'
    _auto = False
    _order = "date_from DESC, employee_id"

    employee_id = fields.Many2one('hr.employee', string="Employee", readonly=True)
    name = fields.Char('Description', readonly=True)
    number_of_days = fields.Float('Number of Days', readonly=True)
    leave_type = fields.Selection([
        ('allocation', 'Allocation Request'),
        ('request', 'Time Off Request')
        ], string='Request Type', readonly=True)
    department_id = fields.Many2one('hr.department', string='Department', readonly=True)
    category_id = fields.Many2one('hr.employee.category', string='Employee Tag', readonly=True)
    holiday_status_id = fields.Many2one("hr.leave.type", string="Leave Type", readonly=True)
    state = fields.Selection([
        ('draft', 'To Submit'),
        ('cancel', 'Cancelled'),
        ('confirm', 'To Approve'),
        ('refuse', 'Refused'),
        ('validate1', 'Second Approval'),
        ('validate', 'Approved')
        ], string='Status', readonly=True)
    holiday_type = fields.Selection([
        ('employee', 'By Employee'),
        ('category', 'By Employee Tag')
    ], string='Allocation Mode', readonly=True)
    date_from = fields.Datetime('Start Date', readonly=True)
    date_to = fields.Datetime('End Date', readonly=True)
    payslip_status = fields.Boolean('Reported in last payslips', readonly=True)

    def init(self):
        tools.drop_view_if_exists(self._cr, 'hr_leave_report')

        self._cr.execute("""
            CREATE or REPLACE view hr_leave_report as (
                SELECT row_number() over(ORDER BY leaves.employee_id) as id,
                leaves.employee_id as employee_id, leaves.name as name,
                leaves.number_of_days as number_of_days, leaves.leave_type as leave_type,
                leaves.category_id as category_id, leaves.department_id as department_id,
                leaves.holiday_status_id as holiday_status_id, leaves.state as state,
                leaves.holiday_type as holiday_type, leaves.date_from as date_from,
                leaves.date_to as date_to, leaves.payslip_status as payslip_status
                from (select
                    allocation.employee_id as employee_id,
                    allocation.name as name,
                    allocation.number_of_days as number_of_days,
                    allocation.category_id as category_id,
                    allocation.department_id as department_id,
                    allocation.holiday_status_id as holiday_status_id,
                    allocation.state as state,
                    allocation.holiday_type,
                    null as date_from,
                    null as date_to,
                    FALSE as payslip_status,
                    'allocation' as leave_type
                from hr_leave_allocation as allocation
                union all select
                    request.employee_id as employee_id,
                    request.name as name,
                    (request.number_of_days * -1) as number_of_days,
                    request.category_id as category_id,
                    request.department_id as department_id,
                    request.holiday_status_id as holiday_status_id,
                    request.state as state,
                    request.holiday_type,
                    request.date_from as date_from,
                    request.date_to as date_to,
                    request.payslip_status as payslip_status,
                    'request' as leave_type
                from hr_leave as request) leaves
            );
        """)

    def _read_from_database(self, field_names, inherited_field_names=[]):
        if 'name' in field_names and 'employee_id' not in field_names:
            field_names.append('employee_id')
        super(LeaveReport, self)._read_from_database(field_names, inherited_field_names)
        if 'name' in field_names:
            if self.user_has_groups('hr_holidays.group_hr_holidays_user'):
                return
            current_employee = self.env['hr.employee'].sudo().search([('user_id', '=', self.env.uid)], limit=1)
            for record in self:
                emp_id = record._cache.get('employee_id', [False])[0]
                if emp_id != current_employee.id:
                    try:
                        record._cache['name']
                        record._cache['name'] = '*****'
                    except Exception:
                        # skip SpecialValue (e.g. for missing record or access right)
                        pass

    @api.model
    def action_time_off_analysis(self):
        domain = [('holiday_type', '=', 'employee')]

        if self.env.context.get('active_ids'):
            domain = expression.AND([
                domain,
                [('employee_id', 'in', self.env.context.get('active_ids', []))]
            ])

        return {
            'name': _('Time Off Analysis'),
            'type': 'ir.actions.act_window',
            'res_model': 'hr.leave.report',
            'view_mode': 'tree,form,pivot',
            'search_view_id': self.env.ref('hr_holidays.view_hr_holidays_filter_report').id,
            'domain': domain,
            'context': {
                'search_default_group_type': True,
                'search_default_year': True
            }
        }

    @api.model
    def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
        if not self.user_has_groups('hr_holidays.group_hr_holidays_user') and 'name' in groupby:
            raise exceptions.UserError(_('Such grouping is not allowed.'))
        return super(LeaveReport, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
Пример #6
0
class StockPicking(models.Model):
    _inherit = 'stock.picking'


    @api.depends('move_line_ids', 'move_line_ids.result_package_id')
    def _compute_packages(self):
        for package in self:
            packs = set()
            for move_line in package.move_line_ids:
                if move_line.result_package_id:
                    packs.add(move_line.result_package_id.id)
            package.package_ids = list(packs)

    @api.depends('move_line_ids', 'move_line_ids.result_package_id', 'move_line_ids.product_uom_id', 'move_line_ids.qty_done')
    def _compute_bulk_weight(self):
        for picking in self:
            weight = 0.0
            for move_line in picking.move_line_ids:
                if move_line.product_id and not move_line.result_package_id:
                    weight += move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id) * move_line.product_id.weight
            picking.weight_bulk = weight

    @api.depends('package_ids', 'weight_bulk')
    def _compute_shipping_weight(self):
        for picking in self:
            picking.shipping_weight = picking.weight_bulk + sum([pack.shipping_weight for pack in picking.package_ids])

    def _get_default_weight_uom(self):
        return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()

    def _compute_weight_uom_name(self):
        for package in self:
            package.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()

    carrier_price = fields.Float(string="Shipping Cost")
    delivery_type = fields.Selection(related='carrier_id.delivery_type', readonly=True)
    carrier_id = fields.Many2one("delivery.carrier", string="Carrier", check_company=True)
    volume = fields.Float(copy=False)
    weight = fields.Float(compute='_cal_weight', digits='Stock Weight', store=True, help="Total weight of the products in the picking.", compute_sudo=True)
    carrier_tracking_ref = fields.Char(string='Tracking Reference', copy=False)
    carrier_tracking_url = fields.Char(string='Tracking URL', compute='_compute_carrier_tracking_url')
    weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', readonly=True, default=_get_default_weight_uom)
    package_ids = fields.Many2many('stock.quant.package', compute='_compute_packages', string='Packages')
    weight_bulk = fields.Float('Bulk Weight', compute='_compute_bulk_weight')
    shipping_weight = fields.Float("Weight for Shipping", compute='_compute_shipping_weight', help="Total weight of the packages and products which are not in a package. That's the weight used to compute the cost of the shipping.")
    is_return_picking = fields.Boolean(compute='_compute_return_picking')
    return_label_ids = fields.One2many('ir.attachment', compute='_compute_return_label')

    @api.depends('carrier_id', 'carrier_tracking_ref')
    def _compute_carrier_tracking_url(self):
        for picking in self:
            picking.carrier_tracking_url = picking.carrier_id.get_tracking_link(picking) if picking.carrier_id and picking.carrier_tracking_ref else False

    @api.depends('carrier_id', 'move_ids_without_package')
    def _compute_return_picking(self):
        for picking in self:
            if picking.carrier_id and picking.carrier_id.can_generate_return:
                picking.is_return_picking = any(m.origin_returned_move_id for m in picking.move_ids_without_package)
            else:
                picking.is_return_picking = False

    def _compute_return_label(self):
        for picking in self:
            if picking.carrier_id:
                picking.return_label_ids = self.env['ir.attachment'].search([('res_model', '=', 'stock.picking'), ('res_id', '=', picking.id), ('name', 'like', '%s%%' % picking.carrier_id.get_return_label_prefix())])

    @api.depends('move_lines')
    def _cal_weight(self):
        for picking in self:
            picking.weight = sum(move.weight for move in picking.move_lines if move.state != 'cancel')

    def _send_confirmation_email(self):
        for pick in self:
            if pick.carrier_id:
                if pick.carrier_id.integration_level == 'rate_and_ship' and pick.picking_type_code != 'incoming':
                    pick.send_to_shipper()
        return super(StockPicking, self)._send_confirmation_email()

    def _pre_put_in_pack_hook(self, move_line_ids):
        res = super(StockPicking, self)._pre_put_in_pack_hook(move_line_ids)
        if not res:
            if self.carrier_id:
                return self._set_delivery_packaging()
        else:
            return res

    def _set_delivery_packaging(self):
        """ This method returns an action allowing to set the product packaging and the shipping weight
         on the stock.quant.package.
        """
        self.ensure_one()
        view_id = self.env.ref('delivery.choose_delivery_package_view_form').id
        return {
            'name': _('Package Details'),
            'type': 'ir.actions.act_window',
            'view_mode': 'form',
            'res_model': 'choose.delivery.package',
            'view_id': view_id,
            'views': [(view_id, 'form')],
            'target': 'new',
            'context': dict(
                self.env.context,
                current_package_carrier_type=self.carrier_id.delivery_type,
                default_picking_id=self.id
            ),
        }

    def send_to_shipper(self):
        self.ensure_one()
        res = self.carrier_id.send_shipping(self)[0]
        if self.carrier_id.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= self.carrier_id.amount:
            res['exact_price'] = 0.0
        self.carrier_price = res['exact_price'] * (1.0 + (self.carrier_id.margin / 100.0))
        if res['tracking_number']:
            self.carrier_tracking_ref = res['tracking_number']
        order_currency = self.sale_id.currency_id or self.company_id.currency_id
        msg = _("Shipment sent to carrier %s for shipping with tracking number %s<br/>Cost: %.2f %s") % (self.carrier_id.name, self.carrier_tracking_ref, self.carrier_price, order_currency.name)
        self.message_post(body=msg)
        self._add_delivery_cost_to_so()

    def print_return_label(self):
        self.ensure_one()
        res = self.carrier_id.get_return_label(self)

    def _add_delivery_cost_to_so(self):
        self.ensure_one()
        sale_order = self.sale_id
        if sale_order and self.carrier_id.invoice_policy == 'real' and self.carrier_price:
            delivery_lines = sale_order.order_line.filtered(lambda l: l.is_delivery and l.currency_id.is_zero(l.price_unit) and l.product_id == self.carrier_id.product_id)
            if not delivery_lines:
                sale_order._create_delivery_line(self.carrier_id, self.carrier_price)
            else:
                delivery_line = delivery_lines[0]
                delivery_line[0].write({
                    'price_unit': self.carrier_price,
                    # remove the estimated price from the description
                    'name': sale_order.carrier_id.with_context(lang=self.partner_id.lang).name,
                })

    def open_website_url(self):
        self.ensure_one()
        if not self.carrier_tracking_url:
            raise UserError(_("Your delivery method has no redirect on courier provider's website to track this order."))

        carrier_trackers = []
        try:
            carrier_trackers = json.loads(self.carrier_tracking_url)
        except ValueError:
            carrier_trackers = self.carrier_tracking_url
        else:
            msg = "Tracking links for shipment: <br/>"
            for tracker in carrier_trackers:
                msg += '<a href=' + tracker[1] + '>' + tracker[0] + '</a><br/>'
            self.message_post(body=msg)
            return self.env.ref('delivery.act_delivery_trackers_url').read()[0]

        client_action = {
            'type': 'ir.actions.act_url',
            'name': "Shipment Tracking Page",
            'target': 'new',
            'url': self.carrier_tracking_url,
        }
        return client_action

    def cancel_shipment(self):
        for picking in self:
            picking.carrier_id.cancel_shipment(self)
            msg = "Shipment %s cancelled" % picking.carrier_tracking_ref
            picking.message_post(body=msg)
            picking.carrier_tracking_ref = False

    def check_packages_are_identical(self):
        '''Some shippers require identical packages in the same shipment. This utility checks it.'''
        self.ensure_one()
        if self.package_ids:
            packages = [p.packaging_id for p in self.package_ids]
            if len(set(packages)) != 1:
                package_names = ', '.join([str(p.name) for p in packages])
                raise UserError(_('You are shipping different packaging types in the same shipment.\nPackaging Types: %s' % package_names))
        return True
Пример #7
0
class CrmLead(models.Model):
    _inherit = "crm.lead"
    partner_latitude = fields.Float('Geo Latitude', digits=(16, 5))
    partner_longitude = fields.Float('Geo Longitude', digits=(16, 5))
    partner_assigned_id = fields.Many2one(
        'res.partner',
        'Assigned Partner',
        track_visibility='onchange',
        help="Partner this case has been forwarded/assigned to.",
        index=True)
    partner_declined_ids = fields.Many2many('res.partner',
                                            'crm_lead_declined_partner',
                                            'lead_id',
                                            'partner_id',
                                            string='Partner not interested')
    date_assign = fields.Date(
        'Partner Assignation Date',
        help="Last date this case was forwarded/assigned to a partner")

    @api.multi
    def _merge_data(self, fields):
        fields += [
            'partner_latitude', 'partner_longitude', 'partner_assigned_id',
            'date_assign'
        ]
        return super(CrmLead, self)._merge_data(fields)

    @api.onchange("partner_assigned_id")
    def onchange_assign_id(self):
        """This function updates the "assignation date" automatically, when manually assign a partner in the geo assign tab
        """
        partner_assigned = self.partner_assigned_id
        if not partner_assigned:
            self.date_assign = False
        else:
            self.date_assign = fields.Date.context_today(self)
            self.user_id = partner_assigned.user_id

    @api.multi
    def assign_salesman_of_assigned_partner(self):
        salesmans_leads = {}
        for lead in self:
            if (lead.stage_id.probability > 0 and lead.stage_id.probability <
                    100) or lead.stage_id.sequence == 1:
                if lead.partner_assigned_id and lead.partner_assigned_id.user_id != lead.user_id:
                    salesmans_leads.setdefault(
                        lead.partner_assigned_id.user_id.id,
                        []).append(lead.id)

        for salesman_id, leads_ids in salesmans_leads.items():
            leads = self.browse(leads_ids)
            leads.write({'user_id': salesman_id})

    @api.multi
    def action_assign_partner(self):
        return self.assign_partner(partner_id=False)

    @api.multi
    def assign_partner(self, partner_id=False):
        partner_dict = {}
        res = False
        if not partner_id:
            partner_dict = self.search_geo_partner()
        for lead in self:
            if not partner_id:
                partner_id = partner_dict.get(lead.id, False)
            if not partner_id:
                tag_to_add = self.env.ref(
                    'website_crm_partner_assign.tag_portal_lead_partner_unavailable',
                    False)
                lead.write({'tag_ids': [(4, tag_to_add.id, False)]})
                continue
            lead.assign_geo_localize(
                lead.partner_latitude,
                lead.partner_longitude,
            )
            partner = self.env['res.partner'].browse(partner_id)
            if partner.user_id:
                lead.allocate_salesman(partner.user_id.ids,
                                       team_id=partner.team_id.id)
            values = {
                'date_assign': fields.Date.context_today(lead),
                'partner_assigned_id': partner_id
            }
            lead.write(values)
        return res

    @api.multi
    def assign_geo_localize(self, latitude=False, longitude=False):
        if latitude and longitude:
            self.write({
                'partner_latitude': latitude,
                'partner_longitude': longitude
            })
            return True
        # Don't pass context to browse()! We need country name in english below
        for lead in self:
            if lead.partner_latitude and lead.partner_longitude:
                continue
            if lead.country_id:
                apikey = self.env['ir.config_parameter'].sudo().get_param(
                    'google.api_key_geocode')
                result = self.env['res.partner']._geo_localize(
                    apikey, lead.street, lead.zip, lead.city,
                    lead.state_id.name, lead.country_id.name)
                if result:
                    lead.write({
                        'partner_latitude': result[0],
                        'partner_longitude': result[1]
                    })
        return True

    @api.multi
    def search_geo_partner(self):
        Partner = self.env['res.partner']
        res_partner_ids = {}
        self.assign_geo_localize()
        for lead in self:
            partner_ids = []
            if not lead.country_id:
                continue
            latitude = lead.partner_latitude
            longitude = lead.partner_longitude
            if latitude and longitude:
                # 1. first way: in the same country, small area
                partner_ids = Partner.search([
                    ('partner_weight', '>', 0),
                    ('partner_latitude', '>', latitude - 2),
                    ('partner_latitude', '<', latitude + 2),
                    ('partner_longitude', '>', longitude - 1.5),
                    ('partner_longitude', '<', longitude + 1.5),
                    ('country_id', '=', lead.country_id.id),
                    ('id', 'not in', lead.partner_declined_ids.mapped('id')),
                ])

                # 2. second way: in the same country, big area
                if not partner_ids:
                    partner_ids = Partner.search([
                        ('partner_weight', '>', 0),
                        ('partner_latitude', '>', latitude - 4),
                        ('partner_latitude', '<', latitude + 4),
                        ('partner_longitude', '>', longitude - 3),
                        ('partner_longitude', '<', longitude + 3),
                        ('country_id', '=', lead.country_id.id),
                        ('id', 'not in',
                         lead.partner_declined_ids.mapped('id')),
                    ])

                # 3. third way: in the same country, extra large area
                if not partner_ids:
                    partner_ids = Partner.search([
                        ('partner_weight', '>', 0),
                        ('partner_latitude', '>', latitude - 8),
                        ('partner_latitude', '<', latitude + 8),
                        ('partner_longitude', '>', longitude - 8),
                        ('partner_longitude', '<', longitude + 8),
                        ('country_id', '=', lead.country_id.id),
                        ('id', 'not in',
                         lead.partner_declined_ids.mapped('id')),
                    ])

                # 5. fifth way: anywhere in same country
                if not partner_ids:
                    # still haven't found any, let's take all partners in the country!
                    partner_ids = Partner.search([
                        ('partner_weight', '>', 0),
                        ('country_id', '=', lead.country_id.id),
                        ('id', 'not in',
                         lead.partner_declined_ids.mapped('id')),
                    ])

                # 6. sixth way: closest partner whatsoever, just to have at least one result
                if not partner_ids:
                    # warning: point() type takes (longitude, latitude) as parameters in this order!
                    self._cr.execute(
                        """SELECT id, distance
                                  FROM  (select id, (point(partner_longitude, partner_latitude) <-> point(%s,%s)) AS distance FROM res_partner
                                  WHERE active
                                        AND partner_longitude is not null
                                        AND partner_latitude is not null
                                        AND partner_weight > 0
                                        AND id not in (select partner_id from crm_lead_declined_partner where lead_id = %s)
                                        ) AS d
                                  ORDER BY distance LIMIT 1""",
                        (longitude, latitude, lead.id))
                    res = self._cr.dictfetchone()
                    if res:
                        partner_ids = Partner.browse([res['id']])

                total_weight = 0
                toassign = []
                for partner in partner_ids:
                    total_weight += partner.partner_weight
                    toassign.append((partner.id, total_weight))

                random.shuffle(
                    toassign
                )  # avoid always giving the leads to the first ones in db natural order!
                nearest_weight = random.randint(0, total_weight)
                for partner_id, weight in toassign:
                    if nearest_weight <= weight:
                        res_partner_ids[lead.id] = partner_id
                        break
        return res_partner_ids

    @api.multi
    def partner_interested(self, comment=False):
        message = _('<p>I am interested by this lead.</p>')
        if comment:
            message += '<p>%s</p>' % comment
        for lead in self:
            lead.message_post(body=message, subtype="mail.mt_note")
            lead.sudo().convert_opportunity(
                lead.partner_id.id)  # sudo required to convert partner data

    @api.multi
    def partner_desinterested(self,
                              comment=False,
                              contacted=False,
                              spam=False):
        if contacted:
            message = '<p>%s</p>' % _(
                'I am not interested by this lead. I contacted the lead.')
        else:
            message = '<p>%s</p>' % _(
                'I am not interested by this lead. I have not contacted the lead.'
            )
        partner_ids = self.env['res.partner'].search([
            ('id', 'child_of',
             self.env.user.partner_id.commercial_partner_id.id)
        ])
        self.message_unsubscribe(partner_ids=partner_ids.ids)
        if comment:
            message += '<p>%s</p>' % comment
        self.message_post(body=message, subtype="mail.mt_note")
        values = {
            'partner_assigned_id': False,
        }

        if spam:
            tag_spam = self.env.ref(
                'website_crm_partner_assign.tag_portal_lead_is_spam', False)
            if tag_spam and tag_spam not in self.tag_ids:
                values['tag_ids'] = [(4, tag_spam.id, False)]
        if partner_ids:
            values['partner_declined_ids'] = [(4, p, 0)
                                              for p in partner_ids.ids]
        self.sudo().write(values)

    @api.multi
    def update_lead_portal(self, values):
        self.check_access_rights('write')
        for lead in self:
            lead_values = {
                'planned_revenue': values['planned_revenue'],
                'probability': values['probability'],
                'priority': values['priority'],
                'date_deadline': values['date_deadline'] or False,
            }
            # As activities may belong to several users, only the current portal user activity
            # will be modified by the portal form. If no activity exist we create a new one instead
            # that we assign to the portal user.

            user_activity = lead.sudo().activity_ids.filtered(
                lambda activity: activity.user_id == self.env.user)[:1]
            if values['activity_date_deadline']:
                if user_activity:
                    user_activity.sudo().write({
                        'activity_type_id':
                        values['activity_type_id'],
                        'summary':
                        values['activity_summary'],
                        'date_deadline':
                        values['activity_date_deadline'],
                    })
                else:
                    self.env['mail.activity'].sudo().create({
                        'res_model_id':
                        self.env.ref('crm.model_crm_lead').id,
                        'res_id':
                        lead.id,
                        'user_id':
                        self.env.user.id,
                        'activity_type_id':
                        values['activity_type_id'],
                        'summary':
                        values['activity_summary'],
                        'date_deadline':
                        values['activity_date_deadline'],
                    })
            lead.write(lead_values)

    @api.model
    def create_opp_portal(self, values):
        if not (self.env.user.partner_id.grade_id
                or self.env.user.commercial_partner_id.grade_id):
            raise AccessDenied()
        user = self.env.user
        self = self.sudo()
        if not (values['contact_name'] and values['description']
                and values['title']):
            return {'errors': _('All fields are required !')}
        tag_own = self.env.ref(
            'website_crm_partner_assign.tag_portal_lead_own_opp', False)
        values = {
            'contact_name': values['contact_name'],
            'name': values['title'],
            'description': values['description'],
            'priority': '2',
            'partner_assigned_id': user.commercial_partner_id.id,
        }
        if tag_own:
            values['tag_ids'] = [(4, tag_own.id, False)]

        lead = self.create(values)
        lead.assign_salesman_of_assigned_partner()
        lead.convert_opportunity(lead.partner_id.id)
        return {'id': lead.id}

    #
    #   DO NOT FORWARD PORT IN MASTER
    #   instead, crm.lead should implement portal.mixin
    #
    @api.multi
    def get_access_action(self, access_uid=None):
        """ Instead of the classic form view, redirect to the online document for
        portal users or if force_website=True in the context. """
        self.ensure_one()

        user, record = self.env.user, self
        if access_uid:
            try:
                record.check_access_rights('read')
                record.check_access_rule("read")
            except AccessError:
                return super(CrmLead, self).get_access_action(access_uid)
            user = self.env['res.users'].sudo().browse(access_uid)
            record = self.sudo(user)
        if user.share or self.env.context.get('force_website'):
            try:
                record.check_access_rights('read')
                record.check_access_rule('read')
            except AccessError:
                pass
            else:
                return {
                    'type': 'ir.actions.act_url',
                    'url': '/my/opportunity/%s' % record.id,
                }
        return super(CrmLead, self).get_access_action(access_uid)
Пример #8
0
class LandedCost(models.Model):
    _name = 'stock.landed.cost'
    _description = 'Stock Landed Cost'
    _inherit = ['mail.thread', 'mail.activity.mixin']

    def _default_account_journal_id(self):
        """Take the journal configured in the company, else fallback on the stock journal."""
        lc_journal = self.env['account.journal']
        if self.env.company.lc_journal_id:
            lc_journal = self.env.company.lc_journal_id
        else:
            ir_property = self.env['ir.property'].search([
                ('name', '=', 'property_stock_journal'),
                ('company_id', '=', self.env.company.id)
            ], limit=1)
            if ir_property:
                lc_journal = ir_property.get_by_record()
        return lc_journal

    name = fields.Char(
        'Name', default=lambda self: _('New'),
        copy=False, readonly=True, tracking=True)
    date = fields.Date(
        'Date', default=fields.Date.context_today,
        copy=False, required=True, states={'done': [('readonly', True)]}, tracking=True)
    picking_ids = fields.Many2many(
        'stock.picking', string='Transfers',
        copy=False, states={'done': [('readonly', True)]})
    cost_lines = fields.One2many(
        'stock.landed.cost.lines', 'cost_id', 'Cost Lines',
        copy=True, states={'done': [('readonly', True)]})
    valuation_adjustment_lines = fields.One2many(
        'stock.valuation.adjustment.lines', 'cost_id', 'Valuation Adjustments',
        states={'done': [('readonly', True)]})
    description = fields.Text(
        'Item Description', states={'done': [('readonly', True)]})
    amount_total = fields.Float(
        'Total', compute='_compute_total_amount',
        digits=0, store=True, tracking=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('done', 'Posted'),
        ('cancel', 'Cancelled')], 'State', default='draft',
        copy=False, readonly=True, tracking=True)
    account_move_id = fields.Many2one(
        'account.move', 'Journal Entry',
        copy=False, readonly=True)
    account_journal_id = fields.Many2one(
        'account.journal', 'Account Journal',
        required=True, states={'done': [('readonly', True)]}, default=lambda self: self._default_account_journal_id())
    company_id = fields.Many2one('res.company', string="Company",
        related='account_journal_id.company_id')
    stock_valuation_layer_ids = fields.One2many('stock.valuation.layer', 'stock_landed_cost_id')
    vendor_bill_id = fields.Many2one(
        'account.move', 'Vendor Bill', copy=False, domain=[('type', '=', 'in_invoice')])
    currency_id = fields.Many2one('res.currency', related='company_id.currency_id')

    @api.depends('cost_lines.price_unit')
    def _compute_total_amount(self):
        for cost in self:
            cost.amount_total = sum(line.price_unit for line in cost.cost_lines)

    @api.model
    def create(self, vals):
        if vals.get('name', _('New')) == _('New'):
            vals['name'] = self.env['ir.sequence'].next_by_code('stock.landed.cost')
        return super(LandedCost, self).create(vals)

    def unlink(self):
        self.button_cancel()
        return super(LandedCost, self).unlink()

    def _track_subtype(self, init_values):
        if 'state' in init_values and self.state == 'done':
            return self.env.ref('stock_landed_costs.mt_stock_landed_cost_open')
        return super(LandedCost, self)._track_subtype(init_values)

    def button_cancel(self):
        if any(cost.state == 'done' for cost in self):
            raise UserError(
                _('Validated landed costs cannot be cancelled, but you could create negative landed costs to reverse them'))
        return self.write({'state': 'cancel'})

    def button_validate(self):
        if any(cost.state != 'draft' for cost in self):
            raise UserError(_('Only draft landed costs can be validated'))
        if not all(cost.picking_ids for cost in self):
            raise UserError(_('Please define the transfers on which those additional costs should apply.'))
        cost_without_adjusment_lines = self.filtered(lambda c: not c.valuation_adjustment_lines)
        if cost_without_adjusment_lines:
            cost_without_adjusment_lines.compute_landed_cost()
        if not self._check_sum():
            raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.'))

        for cost in self:
            move = self.env['account.move']
            move_vals = {
                'journal_id': cost.account_journal_id.id,
                'date': cost.date,
                'ref': cost.name,
                'line_ids': [],
                'type': 'entry',
            }
            for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id):
                remaining_qty = sum(line.move_id.stock_valuation_layer_ids.mapped('remaining_qty'))
                linked_layer = line.move_id.stock_valuation_layer_ids[-1]  # Maybe the LC layer should be linked to multiple IN layer?

                # Prorate the value at what's still in stock
                cost_to_add = (remaining_qty / line.move_id.product_qty) * line.additional_landed_cost
                if not cost.company_id.currency_id.is_zero(cost_to_add):
                    valuation_layer = self.env['stock.valuation.layer'].create({
                        'value': cost_to_add,
                        'unit_cost': 0,
                        'quantity': 0,
                        'remaining_qty': 0,
                        'stock_valuation_layer_id': linked_layer.id,
                        'description': cost.name,
                        'stock_move_id': line.move_id.id,
                        'product_id': line.move_id.product_id.id,
                        'stock_landed_cost_id': cost.id,
                        'company_id': cost.company_id.id,
                    })
                    move_vals['stock_valuation_layer_ids'] = [(6, None, [valuation_layer.id])]
                    linked_layer.remaining_value += cost_to_add
                # Update the AVCO
                product = line.move_id.product_id
                if product.cost_method == 'average' and not float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding):
                    product.with_context(force_company=self.company_id.id).sudo().standard_price += cost_to_add / product.quantity_svl
                # `remaining_qty` is negative if the move is out and delivered proudcts that were not
                # in stock.
                qty_out = 0
                if line.move_id._is_in():
                    qty_out = line.move_id.product_qty - remaining_qty
                elif line.move_id._is_out():
                    qty_out = line.move_id.product_qty
                move_vals['line_ids'] += line._create_accounting_entries(move, qty_out)

            move = move.create(move_vals)
            cost.write({'state': 'done', 'account_move_id': move.id})
            move.post()

            if cost.vendor_bill_id and cost.vendor_bill_id.state == 'posted' and cost.company_id.anglo_saxon_accounting:
                all_amls = cost.vendor_bill_id.line_ids | cost.account_move_id.line_ids
                for product in cost.cost_lines.product_id:
                    accounts = product.product_tmpl_id.get_product_accounts()
                    input_account = accounts['stock_input']
                    all_amls.filtered(lambda aml: aml.account_id == input_account).reconcile()
        return True

    def _check_sum(self):
        """ Check if each cost line its valuation lines sum to the correct amount
        and if the overall total amount is correct also """
        prec_digits = self.env.company.currency_id.decimal_places
        for landed_cost in self:
            total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost'))
            if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits):
                return False

            val_to_cost_lines = defaultdict(lambda: 0.0)
            for val_line in landed_cost.valuation_adjustment_lines:
                val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost
            if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits)
                   for cost_line, val_amount in val_to_cost_lines.items()):
                return False
        return True

    def get_valuation_lines(self):
        lines = []

        for move in self.mapped('picking_ids').mapped('move_lines'):
            # it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost
            if move.product_id.valuation != 'real_time' or move.product_id.cost_method not in ('fifo', 'average'):
                continue
            vals = {
                'product_id': move.product_id.id,
                'move_id': move.id,
                'quantity': move.product_qty,
                'former_cost': sum(move.stock_valuation_layer_ids.mapped('value')),
                'weight': move.product_id.weight * move.product_qty,
                'volume': move.product_id.volume * move.product_qty
            }
            lines.append(vals)

        if not lines and self.mapped('picking_ids'):
            raise UserError(_("You cannot apply landed costs on the chosen transfer(s). Landed costs can only be applied for products with automated inventory valuation and FIFO or average costing method."))
        return lines

    def compute_landed_cost(self):
        AdjustementLines = self.env['stock.valuation.adjustment.lines']
        AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink()

        digits = self.env['decimal.precision'].precision_get('Product Price')
        towrite_dict = {}
        for cost in self.filtered(lambda cost: cost.picking_ids):
            total_qty = 0.0
            total_cost = 0.0
            total_weight = 0.0
            total_volume = 0.0
            total_line = 0.0
            all_val_line_values = cost.get_valuation_lines()
            for val_line_values in all_val_line_values:
                for cost_line in cost.cost_lines:
                    val_line_values.update({'cost_id': cost.id, 'cost_line_id': cost_line.id})
                    self.env['stock.valuation.adjustment.lines'].create(val_line_values)
                total_qty += val_line_values.get('quantity', 0.0)
                total_weight += val_line_values.get('weight', 0.0)
                total_volume += val_line_values.get('volume', 0.0)

                former_cost = val_line_values.get('former_cost', 0.0)
                # round this because former_cost on the valuation lines is also rounded
                total_cost += tools.float_round(former_cost, precision_digits=digits) if digits else former_cost

                total_line += 1

            for line in cost.cost_lines:
                value_split = 0.0
                for valuation in cost.valuation_adjustment_lines:
                    value = 0.0
                    if valuation.cost_line_id and valuation.cost_line_id.id == line.id:
                        if line.split_method == 'by_quantity' and total_qty:
                            per_unit = (line.price_unit / total_qty)
                            value = valuation.quantity * per_unit
                        elif line.split_method == 'by_weight' and total_weight:
                            per_unit = (line.price_unit / total_weight)
                            value = valuation.weight * per_unit
                        elif line.split_method == 'by_volume' and total_volume:
                            per_unit = (line.price_unit / total_volume)
                            value = valuation.volume * per_unit
                        elif line.split_method == 'equal':
                            value = (line.price_unit / total_line)
                        elif line.split_method == 'by_current_cost_price' and total_cost:
                            per_unit = (line.price_unit / total_cost)
                            value = valuation.former_cost * per_unit
                        else:
                            value = (line.price_unit / total_line)

                        if digits:
                            value = tools.float_round(value, precision_digits=digits, rounding_method='UP')
                            fnc = min if line.price_unit > 0 else max
                            value = fnc(value, line.price_unit - value_split)
                            value_split += value

                        if valuation.id not in towrite_dict:
                            towrite_dict[valuation.id] = value
                        else:
                            towrite_dict[valuation.id] += value
        for key, value in towrite_dict.items():
            AdjustementLines.browse(key).write({'additional_landed_cost': value})
        return True

    def action_view_stock_valuation_layers(self):
        self.ensure_one()
        domain = [('id', 'in', self.stock_valuation_layer_ids.ids)]
        action = self.env.ref('stock_account.stock_valuation_layer_action').read()[0]
        return dict(action, domain=domain)
Пример #9
0
class AdjustmentLines(models.Model):
    _name = 'stock.valuation.adjustment.lines'
    _description = 'Valuation Adjustment Lines'

    name = fields.Char(
        'Description', compute='_compute_name', store=True)
    cost_id = fields.Many2one(
        'stock.landed.cost', 'Landed Cost',
        ondelete='cascade', required=True)
    cost_line_id = fields.Many2one(
        'stock.landed.cost.lines', 'Cost Line', readonly=True)
    move_id = fields.Many2one('stock.move', 'Stock Move', readonly=True)
    product_id = fields.Many2one('product.product', 'Product', required=True)
    quantity = fields.Float(
        'Quantity', default=1.0,
        digits=0, required=True)
    weight = fields.Float(
        'Weight', default=1.0,
        digits='Stock Weight')
    volume = fields.Float(
        'Volume', default=1.0)
    former_cost = fields.Float(
        'Original Value', digits='Product Price')
    additional_landed_cost = fields.Float(
        'Additional Landed Cost',
        digits='Product Price')
    final_cost = fields.Float(
        'New Value', compute='_compute_final_cost',
        digits=0, store=True)
    currency_id = fields.Many2one('res.currency', related='cost_id.company_id.currency_id')

    @api.depends('cost_line_id.name', 'product_id.code', 'product_id.name')
    def _compute_name(self):
        for line in self:
            name = '%s - ' % (line.cost_line_id.name if line.cost_line_id else '')
            line.name = name + (line.product_id.code or line.product_id.name or '')

    @api.depends('former_cost', 'additional_landed_cost')
    def _compute_final_cost(self):
        for line in self:
            line.final_cost = line.former_cost + line.additional_landed_cost

    def _create_accounting_entries(self, move, qty_out):
        # TDE CLEANME: product chosen for computation ?
        cost_product = self.cost_line_id.product_id
        if not cost_product:
            return False
        accounts = self.product_id.product_tmpl_id.get_product_accounts()
        debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False
        # If the stock move is dropshipped move we need to get the cost account instead the stock valuation account
        if self.move_id._is_dropshipped():
            debit_account_id = accounts.get('expense') and accounts['expense'].id or False
        already_out_account_id = accounts['stock_output'].id
        credit_account_id = self.cost_line_id.account_id.id or cost_product.categ_id.property_stock_account_input_categ_id.id

        if not credit_account_id:
            raise UserError(_('Please configure Stock Expense Account for product: %s.') % (cost_product.name))

        return self._create_account_move_line(move, credit_account_id, debit_account_id, qty_out, already_out_account_id)

    def _create_account_move_line(self, move, credit_account_id, debit_account_id, qty_out, already_out_account_id):
        """
        Generate the account.move.line values to track the landed cost.
        Afterwards, for the goods that are already out of stock, we should create the out moves
        """
        AccountMoveLine = []

        base_line = {
            'name': self.name,
            'product_id': self.product_id.id,
            'quantity': 0,
        }
        debit_line = dict(base_line, account_id=debit_account_id)
        credit_line = dict(base_line, account_id=credit_account_id)
        diff = self.additional_landed_cost
        if diff > 0:
            debit_line['debit'] = diff
            credit_line['credit'] = diff
        else:
            # negative cost, reverse the entry
            debit_line['credit'] = -diff
            credit_line['debit'] = -diff
        AccountMoveLine.append([0, 0, debit_line])
        AccountMoveLine.append([0, 0, credit_line])

        # Create account move lines for quants already out of stock
        if qty_out > 0:
            debit_line = dict(base_line,
                              name=(self.name + ": " + str(qty_out) + _(' already out')),
                              quantity=0,
                              account_id=already_out_account_id)
            credit_line = dict(base_line,
                               name=(self.name + ": " + str(qty_out) + _(' already out')),
                               quantity=0,
                               account_id=debit_account_id)
            diff = diff * qty_out / self.quantity
            if diff > 0:
                debit_line['debit'] = diff
                credit_line['credit'] = diff
            else:
                # negative cost, reverse the entry
                debit_line['credit'] = -diff
                credit_line['debit'] = -diff
            AccountMoveLine.append([0, 0, debit_line])
            AccountMoveLine.append([0, 0, credit_line])

            if self.env.company.anglo_saxon_accounting:
                expense_account_id = self.product_id.product_tmpl_id.get_product_accounts()['expense'].id
                debit_line = dict(base_line,
                                  name=(self.name + ": " + str(qty_out) + _(' already out')),
                                  quantity=0,
                                  account_id=expense_account_id)
                credit_line = dict(base_line,
                                   name=(self.name + ": " + str(qty_out) + _(' already out')),
                                   quantity=0,
                                   account_id=already_out_account_id)

                if diff > 0:
                    debit_line['debit'] = diff
                    credit_line['credit'] = diff
                else:
                    # negative cost, reverse the entry
                    debit_line['credit'] = -diff
                    credit_line['debit'] = -diff
                AccountMoveLine.append([0, 0, debit_line])
                AccountMoveLine.append([0, 0, credit_line])

        return AccountMoveLine
Пример #10
0
class MrpAbstractWorkorder(models.AbstractModel):
    _name = "mrp.abstract.workorder"
    _description = "Common code between produce wizards and workorders."
    _check_company_auto = True

    production_id = fields.Many2one('mrp.production',
                                    'Manufacturing Order',
                                    required=True,
                                    check_company=True)
    product_id = fields.Many2one(related='production_id.product_id',
                                 readonly=True,
                                 store=True,
                                 check_company=True)
    qty_producing = fields.Float(string='Currently Produced Quantity',
                                 digits='Product Unit of Measure')
    product_uom_id = fields.Many2one('uom.uom',
                                     'Unit of Measure',
                                     required=True,
                                     readonly=True)
    finished_lot_id = fields.Many2one(
        'stock.production.lot',
        string='Lot/Serial Number',
        domain=
        "[('product_id', '=', product_id), ('company_id', '=', company_id)]",
        check_company=True)
    product_tracking = fields.Selection(related="product_id.tracking")
    consumption = fields.Selection(
        [('strict', 'Strict'), ('flexible', 'Flexible')],
        required=True,
    )
    use_create_components_lots = fields.Boolean(
        related="production_id.picking_type_id.use_create_components_lots")
    company_id = fields.Many2one(related='production_id.company_id')

    @api.model
    def _prepare_component_quantity(self, move, qty_producing):
        """ helper that computes quantity to consume (or to create in case of byproduct)
        depending on the quantity producing and the move's unit factor"""
        if move.product_id.tracking == 'serial':
            uom = move.product_id.uom_id
        else:
            uom = move.product_uom
        return move.product_uom._compute_quantity(qty_producing *
                                                  move.unit_factor,
                                                  uom,
                                                  round=False)

    def _workorder_line_ids(self):
        self.ensure_one()
        return self.raw_workorder_line_ids | self.finished_workorder_line_ids

    @api.onchange('qty_producing')
    def _onchange_qty_producing(self):
        """ Modify the qty currently producing will modify the existing
        workorder line in order to match the new quantity to consume for each
        component and their reserved quantity.
        """
        if self.qty_producing <= 0:
            raise UserError(
                _('You have to produce at least one %s.') %
                self.product_uom_id.name)
        line_values = self._update_workorder_lines()
        for values in line_values['to_create']:
            self.env[self._workorder_line_ids()._name].new(values)
        for line in line_values['to_delete']:
            if line in self.raw_workorder_line_ids:
                self.raw_workorder_line_ids -= line
            else:
                self.finished_workorder_line_ids -= line
        for line, vals in line_values['to_update'].items():
            line.update(vals)

    def _update_workorder_lines(self):
        """ Update workorder lines, according to the new qty currently
        produced. It returns a dict with line to create, update or delete.
        It do not directly write or unlink the line because this function is
        used in onchange and request that write on db (e.g. workorder creation).
        """
        line_values = {'to_create': [], 'to_delete': [], 'to_update': {}}
        # moves are actual records
        move_finished_ids = self.move_finished_ids._origin.filtered(
            lambda move: move.product_id != self.product_id and move.state
            not in ('done', 'cancel'))
        move_raw_ids = self.move_raw_ids._origin.filtered(
            lambda move: move.state not in ('done', 'cancel'))
        for move in move_raw_ids | move_finished_ids:
            move_workorder_lines = self._workorder_line_ids().filtered(
                lambda w: w.move_id == move)

            # Compute the new quantity for the current component
            rounding = move.product_uom.rounding
            new_qty = self._prepare_component_quantity(move,
                                                       self.qty_producing)

            # In case the production uom is different than the workorder uom
            # it means the product is serial and production uom is not the reference
            new_qty = self.product_uom_id._compute_quantity(
                new_qty, self.production_id.product_uom_id, round=False)
            qty_todo = float_round(
                new_qty - sum(move_workorder_lines.mapped('qty_to_consume')),
                precision_rounding=rounding)

            # Remove or lower quantity on exisiting workorder lines
            if float_compare(qty_todo, 0.0, precision_rounding=rounding) < 0:
                qty_todo = abs(qty_todo)
                # Try to decrease or remove lines that are not reserved and
                # partialy reserved first. A different decrease strategy could
                # be define in _unreserve_order method.
                for workorder_line in move_workorder_lines.sorted(
                        key=lambda wl: wl._unreserve_order()):
                    if float_compare(qty_todo, 0,
                                     precision_rounding=rounding) <= 0:
                        break
                    # If the quantity to consume on the line is lower than the
                    # quantity to remove, the line could be remove.
                    if float_compare(workorder_line.qty_to_consume,
                                     qty_todo,
                                     precision_rounding=rounding) <= 0:
                        qty_todo = float_round(qty_todo -
                                               workorder_line.qty_to_consume,
                                               precision_rounding=rounding)
                        if line_values['to_delete']:
                            line_values['to_delete'] |= workorder_line
                        else:
                            line_values['to_delete'] = workorder_line
                    # decrease the quantity on the line
                    else:
                        new_val = workorder_line.qty_to_consume - qty_todo
                        # avoid to write a negative reserved quantity
                        new_reserved = max(
                            0, workorder_line.qty_reserved - qty_todo)
                        line_values['to_update'][workorder_line] = {
                            'qty_to_consume': new_val,
                            'qty_done': new_val,
                            'qty_reserved': new_reserved,
                        }
                        qty_todo = 0
            else:
                # Search among wo lines which one could be updated
                qty_reserved_wl = defaultdict(float)
                # Try to update the line with the greater reservation first in
                # order to promote bigger batch.
                for workorder_line in move_workorder_lines.sorted(
                        key=lambda wl: wl.qty_reserved, reverse=True):
                    rounding = workorder_line.product_uom_id.rounding
                    if float_compare(qty_todo, 0,
                                     precision_rounding=rounding) <= 0:
                        break
                    move_lines = workorder_line._get_move_lines()
                    qty_reserved_wl[
                        workorder_line.lot_id] += workorder_line.qty_reserved
                    # The reserved quantity according to exisiting move line
                    # already produced (with qty_done set) and other production
                    # lines with the same lot that are currently on production.
                    qty_reserved_remaining = sum(
                        move_lines.mapped('product_uom_qty')) - sum(
                            move_lines.mapped('qty_done')) - qty_reserved_wl[
                                workorder_line.lot_id]
                    if float_compare(qty_reserved_remaining,
                                     0,
                                     precision_rounding=rounding) > 0:
                        qty_to_add = min(qty_reserved_remaining, qty_todo)
                        line_values['to_update'][workorder_line] = {
                            'qty_done':
                            workorder_line.qty_to_consume + qty_to_add,
                            'qty_to_consume':
                            workorder_line.qty_to_consume + qty_to_add,
                            'qty_reserved':
                            workorder_line.qty_reserved + qty_to_add,
                        }
                        qty_todo -= qty_to_add
                        qty_reserved_wl[workorder_line.lot_id] += qty_to_add

                    # If a line exists without reservation and without lot. It
                    # means that previous operations could not find any reserved
                    # quantity and created a line without lot prefilled. In this
                    # case, the system will not find an existing move line with
                    # available reservation anymore and will increase this line
                    # instead of creating a new line without lot and reserved
                    # quantities.
                    if not workorder_line.qty_reserved and not workorder_line.lot_id and workorder_line.product_tracking != 'serial':
                        line_values['to_update'][workorder_line] = {
                            'qty_done':
                            workorder_line.qty_to_consume + qty_todo,
                            'qty_to_consume':
                            workorder_line.qty_to_consume + qty_todo,
                        }
                        qty_todo = 0

                # if there are still qty_todo, create new wo lines
                if float_compare(qty_todo, 0.0,
                                 precision_rounding=rounding) > 0:
                    for values in self._generate_lines_values(move, qty_todo):
                        line_values['to_create'].append(values)
        return line_values

    @api.model
    def _generate_lines_values(self, move, qty_to_consume):
        """ Create workorder line. First generate line based on the reservation,
        in order to prefill reserved quantity, lot and serial number.
        If the quantity to consume is greater than the reservation quantity then
        create line with the correct quantity to consume but without lot or
        serial number.
        """
        lines = []
        is_tracked = move.product_id.tracking != 'none'
        if move in self.move_raw_ids._origin:
            # Get the inverse_name (many2one on line) of raw_workorder_line_ids
            initial_line_values = {
                self.raw_workorder_line_ids._get_raw_workorder_inverse_name():
                self.id
            }
        else:
            # Get the inverse_name (many2one on line) of finished_workorder_line_ids
            initial_line_values = {
                self.finished_workorder_line_ids._get_finished_workoder_inverse_name(
                ):
                self.id
            }
        for move_line in move.move_line_ids:
            line = dict(initial_line_values)
            if float_compare(
                    qty_to_consume,
                    0.0,
                    precision_rounding=move.product_uom.rounding) <= 0:
                break
            # move line already 'used' in workorder (from its lot for instance)
            if move_line.lot_produced_ids or float_compare(
                    move_line.product_uom_qty,
                    move_line.qty_done,
                    precision_rounding=move.product_uom.rounding) <= 0:
                continue
            # search wo line on which the lot is not fully consumed or other reserved lot
            linked_wo_line = self._workorder_line_ids().filtered(
                lambda line: line.move_id == move and line.lot_id == move_line.
                lot_id)
            if linked_wo_line:
                if float_compare(
                        sum(linked_wo_line.mapped('qty_to_consume')),
                        move_line.product_uom_qty - move_line.qty_done,
                        precision_rounding=move.product_uom.rounding) < 0:
                    to_consume_in_line = min(
                        qty_to_consume,
                        move_line.product_uom_qty - move_line.qty_done -
                        sum(linked_wo_line.mapped('qty_to_consume')))
                else:
                    continue
            else:
                to_consume_in_line = min(
                    qty_to_consume,
                    move_line.product_uom_qty - move_line.qty_done)
            line.update({
                'move_id':
                move.id,
                'product_id':
                move.product_id.id,
                'product_uom_id':
                is_tracked and move.product_id.uom_id.id
                or move.product_uom.id,
                'qty_to_consume':
                to_consume_in_line,
                'qty_reserved':
                to_consume_in_line,
                'lot_id':
                move_line.lot_id.id,
                'qty_done':
                to_consume_in_line,
            })
            lines.append(line)
            qty_to_consume -= to_consume_in_line
        # The move has not reserved the whole quantity so we create new wo lines
        if float_compare(qty_to_consume,
                         0.0,
                         precision_rounding=move.product_uom.rounding) > 0:
            line = dict(initial_line_values)
            if move.product_id.tracking == 'serial':
                while float_compare(
                        qty_to_consume,
                        0.0,
                        precision_rounding=move.product_uom.rounding) > 0:
                    line.update({
                        'move_id': move.id,
                        'product_id': move.product_id.id,
                        'product_uom_id': move.product_id.uom_id.id,
                        'qty_to_consume': 1,
                        'qty_done': 1,
                    })
                    lines.append(line)
                    qty_to_consume -= 1
            else:
                line.update({
                    'move_id': move.id,
                    'product_id': move.product_id.id,
                    'product_uom_id': move.product_uom.id,
                    'qty_to_consume': qty_to_consume,
                    'qty_done': qty_to_consume,
                })
                lines.append(line)
        return lines

    def _update_finished_move(self):
        """ Update the finished move & move lines in order to set the finished
        product lot on it as well as the produced quantity. This method get the
        information either from the last workorder or from the Produce wizard."""
        production_move = self.production_id.move_finished_ids.filtered(
            lambda move: move.product_id == self.product_id and move.state
            not in ('done', 'cancel'))
        if production_move and production_move.product_id.tracking != 'none':
            if not self.finished_lot_id:
                raise UserError(
                    _('You need to provide a lot for the finished product.'))
            move_line = production_move.move_line_ids.filtered(
                lambda line: line.lot_id.id == self.finished_lot_id.id)
            if move_line:
                if self.product_id.tracking == 'serial':
                    raise UserError(
                        _('You cannot produce the same serial number twice.'))
                move_line.product_uom_qty += self.qty_producing
                move_line.qty_done += self.qty_producing
            else:
                location_dest_id = production_move.location_dest_id._get_putaway_strategy(
                    self.product_id).id or production_move.location_dest_id.id
                move_line.create({
                    'move_id': production_move.id,
                    'product_id': production_move.product_id.id,
                    'lot_id': self.finished_lot_id.id,
                    'product_uom_qty': self.qty_producing,
                    'product_uom_id': self.product_uom_id.id,
                    'qty_done': self.qty_producing,
                    'location_id': production_move.location_id.id,
                    'location_dest_id': location_dest_id,
                })
        else:
            rounding = production_move.product_uom.rounding
            production_move._set_quantity_done(
                float_round(self.qty_producing, precision_rounding=rounding))

    def _update_moves(self):
        """ Once the production is done. Modify the workorder lines into
        stock move line with the registered lot and quantity done.
        """
        # Before writting produce quantities, we ensure they respect the bom strictness
        self._strict_consumption_check()
        vals_list = []
        workorder_lines_to_process = self._workorder_line_ids().filtered(
            lambda line: line.product_id != self.product_id and line.qty_done >
            0)
        for line in workorder_lines_to_process:
            line._update_move_lines()
            if float_compare(
                    line.qty_done,
                    0,
                    precision_rounding=line.product_uom_id.rounding) > 0:
                vals_list += line._create_extra_move_lines()

        self._workorder_line_ids().filtered(
            lambda line: line.product_id != self.product_id).unlink()
        self.env['stock.move.line'].create(vals_list)

    def _strict_consumption_check(self):
        if self.consumption == 'strict':
            for move in self.move_raw_ids:
                lines = self._workorder_line_ids().filtered(
                    lambda l: l.move_id == move)
                qty_done = sum(lines.mapped('qty_done'))
                qty_to_consume = sum(lines.mapped('qty_to_consume'))
                rounding = self.product_uom_id.rounding
                if float_compare(qty_done,
                                 qty_to_consume,
                                 precision_rounding=rounding) != 0:
                    raise UserError(
                        _('You should consume the quantity of %s defined in the BoM. If you want to consume more or less components, change the consumption setting on the BoM.'
                          ) % lines[0].product_id.name)
Пример #11
0
class MrpAbstractWorkorderLine(models.AbstractModel):
    _name = "mrp.abstract.workorder.line"
    _description = "Abstract model to implement product_produce_line as well as\
    workorder_line"

    _check_company_auto = True

    move_id = fields.Many2one('stock.move', check_company=True)
    product_id = fields.Many2one('product.product',
                                 'Product',
                                 required=True,
                                 check_company=True)
    product_tracking = fields.Selection(related="product_id.tracking")
    lot_id = fields.Many2one(
        'stock.production.lot',
        'Lot/Serial Number',
        check_company=True,
        domain=
        "[('product_id', '=', product_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]"
    )
    qty_to_consume = fields.Float('To Consume',
                                  digits='Product Unit of Measure')
    product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure')
    qty_done = fields.Float('Consumed', digits='Product Unit of Measure')
    qty_reserved = fields.Float('Reserved', digits='Product Unit of Measure')
    company_id = fields.Many2one('res.company', compute='_compute_company_id')

    @api.onchange('lot_id')
    def _onchange_lot_id(self):
        """ When the user is encoding a produce line for a tracked product, we apply some logic to
        help him. This onchange will automatically switch `qty_done` to 1.0.
        """
        if self.product_id.tracking == 'serial':
            self.qty_done = 1

    @api.onchange('product_id')
    def _onchange_product_id(self):
        if self.product_id and not self.move_id:
            self.product_uom_id = self.product_id.uom_id

    @api.onchange('qty_done')
    def _onchange_qty_done(self):
        """ When the user is encoding a produce line for a tracked product, we apply some logic to
        help him. This onchange will warn him if he set `qty_done` to a non-supported value.
        """
        res = {}
        if self.product_id.tracking == 'serial' and not float_is_zero(
                self.qty_done, self.product_uom_id.rounding):
            if float_compare(
                    self.qty_done,
                    1.0,
                    precision_rounding=self.product_uom_id.rounding) != 0:
                message = _(
                    'You can only process 1.0 %s of products with unique serial number.'
                ) % self.product_id.uom_id.name
                res['warning'] = {'title': _('Warning'), 'message': message}
        return res

    def _compute_company_id(self):
        for line in self:
            line.company_id = line._get_production().company_id

    def _update_move_lines(self):
        """ update a move line to save the workorder line data"""
        self.ensure_one()
        if self.lot_id:
            move_lines = self.move_id.move_line_ids.filtered(
                lambda ml: ml.lot_id == self.lot_id and not ml.lot_produced_ids
            )
        else:
            move_lines = self.move_id.move_line_ids.filtered(
                lambda ml: not ml.lot_id and not ml.lot_produced_ids)

        # Sanity check: if the product is a serial number and `lot` is already present in the other
        # consumed move lines, raise.
        if self.product_id.tracking != 'none' and not self.lot_id:
            raise UserError(
                _('Please enter a lot or serial number for %s !' %
                  self.product_id.display_name))

        if self.lot_id and self.product_id.tracking == 'serial' and self.lot_id in self.move_id.move_line_ids.filtered(
                lambda ml: ml.qty_done).mapped('lot_id'):
            raise UserError(
                _('You cannot consume the same serial number twice. Please correct the serial numbers encoded.'
                  ))

        # Update reservation and quantity done
        for ml in move_lines:
            rounding = ml.product_uom_id.rounding
            if float_compare(self.qty_done, 0,
                             precision_rounding=rounding) <= 0:
                break
            quantity_to_process = min(self.qty_done,
                                      ml.product_uom_qty - ml.qty_done)
            self.qty_done -= quantity_to_process

            new_quantity_done = (ml.qty_done + quantity_to_process)
            # if we produce less than the reserved quantity to produce the finished products
            # in different lots,
            # we create different component_move_lines to record which one was used
            # on which lot of finished product
            if float_compare(new_quantity_done,
                             ml.product_uom_qty,
                             precision_rounding=rounding) >= 0:
                ml.write({
                    'qty_done': new_quantity_done,
                    'lot_produced_ids': self._get_produced_lots(),
                })
            else:
                new_qty_reserved = ml.product_uom_qty - new_quantity_done
                default = {
                    'product_uom_qty': new_quantity_done,
                    'qty_done': new_quantity_done,
                    'lot_produced_ids': self._get_produced_lots(),
                }
                ml.copy(default=default)
                ml.with_context(bypass_reservation_update=True).write({
                    'product_uom_qty':
                    new_qty_reserved,
                    'qty_done':
                    0
                })

    def _create_extra_move_lines(self):
        """Create new sml if quantity produced is bigger than the reserved one"""
        vals_list = []
        quants = self.env['stock.quant']._gather(self.product_id,
                                                 self.move_id.location_id,
                                                 lot_id=self.lot_id,
                                                 strict=False)
        # Search for a sub-locations where the product is available.
        # Loop on the quants to get the locations. If there is not enough
        # quantity into stock, we take the move location. Anyway, no
        # reservation is made, so it is still possible to change it afterwards.
        for quant in quants:
            quantity = quant.quantity - quant.reserved_quantity
            quantity = self.product_id.uom_id._compute_quantity(
                quantity, self.product_uom_id, rounding_method='HALF-UP')
            rounding = quant.product_uom_id.rounding
            if (float_compare(quant.quantity, 0, precision_rounding=rounding)
                    <= 0 or float_compare(
                        quantity,
                        0,
                        precision_rounding=self.product_uom_id.rounding) <= 0):
                continue
            vals = {
                'move_id': self.move_id.id,
                'product_id': self.product_id.id,
                'location_id': quant.location_id.id,
                'location_dest_id': self.move_id.location_dest_id.id,
                'product_uom_qty': 0,
                'product_uom_id': self.product_uom_id.id,
                'qty_done': min(quantity, self.qty_done),
                'lot_produced_ids': self._get_produced_lots(),
            }
            if self.lot_id:
                vals.update({'lot_id': self.lot_id.id})

            vals_list.append(vals)
            self.qty_done -= vals['qty_done']
            # If all the qty_done is distributed, we can close the loop
            if float_compare(
                    self.qty_done,
                    0,
                    precision_rounding=self.product_id.uom_id.rounding) <= 0:
                break

        if float_compare(
                self.qty_done,
                0,
                precision_rounding=self.product_id.uom_id.rounding) > 0:
            vals = {
                'move_id': self.move_id.id,
                'product_id': self.product_id.id,
                'location_id': self.move_id.location_id.id,
                'location_dest_id': self.move_id.location_dest_id.id,
                'product_uom_qty': 0,
                'product_uom_id': self.product_uom_id.id,
                'qty_done': self.qty_done,
                'lot_produced_ids': self._get_produced_lots(),
            }
            if self.lot_id:
                vals.update({'lot_id': self.lot_id.id})

            vals_list.append(vals)

        return vals_list

    def _unreserve_order(self):
        """ Unreserve line with lower reserved quantity first """
        self.ensure_one()
        return (self.qty_reserved, )

    def _get_move_lines(self):
        return self.move_id.move_line_ids.filtered(
            lambda ml: ml.lot_id == self.lot_id and ml.product_id == self.
            product_id)

    def _get_produced_lots(self):
        return self.move_id in self._get_production(
        ).move_raw_ids and self._get_final_lots() and [
            (4, lot.id) for lot in self._get_final_lots()
        ]

    @api.model
    def _get_raw_workorder_inverse_name(self):
        raise NotImplementedError(
            'Method _get_raw_workorder_inverse_name() undefined on %s' % self)

    @api.model
    def _get_finished_workoder_inverse_name(self):
        raise NotImplementedError(
            'Method _get_finished_workoder_inverse_name() undefined on %s' %
            self)

    # To be implemented in specific model
    def _get_final_lots(self):
        raise NotImplementedError('Method _get_final_lots() undefined on %s' %
                                  self)

    def _get_production(self):
        raise NotImplementedError('Method _get_production() undefined on %s' %
                                  self)
class ContractCreation(models.TransientModel):
    _name = "saas.contract.creation"
    _description = 'Contract Creation Wizard.'

    plan_id = fields.Many2one(comodel_name="saas.plan", string="Related SaaS Plan", required=False)
    partner_id = fields.Many2one(
        comodel_name="res.partner",
        string="Partner",
        required=True,
    )
    recurring_interval = fields.Integer(
        default=1,
        string='Billing Cycle',
        help="Repeat every (Days/Week/Month/Year)",
    )
    recurring_rule_type = fields.Selection(
        [('daily', 'Day(s)'),
         ('weekly', 'Week(s)'),
         ('monthly', 'Month(s)'),
         ('monthlylastday', 'Month(s) last day'),
         ('yearly', 'Year(s)'),
         ],
        default='monthly',
        string='Recurrence',
        help="Specify Interval for automatic invoice generation.", readonly=True,
    )
    # billing_criteria = fields.Selection(
    #     selection=BILLING_CRITERIA,
    #     string="Billing Criteria",
    #     required=True)
    invoice_product_id = fields.Many2one(comodel_name="product.product", required=True, string="Invoice Product")
    pricelist_id = fields.Many2one(
        comodel_name='product.pricelist',
        string='Pricelist'
    )
    currency_id = fields.Many2one(comodel_name="res.currency")
    contract_rate = fields.Float(string="Contract Rate")
    per_user_pricing = fields.Boolean(string="Per user pricing")
    user_cost = fields.Float(string="Per User cost")
    min_users = fields.Integer(string="Min. No. of users", help="""Range for Number of users in cliet's Instance""")
    max_users = fields.Integer(string="Max. No. of users", help="""Range for Number of users in cliet's Instance""")
    saas_users = fields.Integer(string="No. of users")
    contract_price = fields.Float(string="Contract Price", help="""Pricing for Contract""")
    user_billing = fields.Float(string="User Billing", help="""User Based Billing""")
    total_cost = fields.Float(string="Total Contract Cost")
    due_users_price = fields.Float(string="Due users price", default=1.0)
    auto_create_invoice = fields.Boolean(string="Automatically create next invoice")
    start_date = fields.Date(
        string='Purchase Date',
        required=True
    )
    total_cycles = fields.Integer(
        string="Number of Cycles(Remaining/Total)", default=1)
    trial_period = fields.Integer(
        string="Complimentary(Free) days", default=0)

    @api.model
    def get_date_delta(self, interval):
        return relativedelta(months=interval)


    @api.onchange('user_cost', 'contract_rate', 'saas_users', 'total_cycles')
    def calculate_total_cost(self):
        for obj in self:
            obj.contract_price = obj.contract_rate * obj.total_cycles    
            if obj.per_user_pricing and obj.saas_users:
                if obj.saas_users < obj.min_users:
                    raise Warning("No. of users can't be less than %r"%obj.min_users)
                if obj.max_users != -1 and obj.saas_users > obj.max_users:
                    raise Warning("No. of users can't be greater than %r"%obj.max_users)
                obj.user_billing = obj.saas_users * obj.user_cost * obj.total_cycles
            obj.total_cost = obj.contract_price + obj.user_billing
            _logger.info("+++11++++OBJ>TOTALCOST+++++++%s",obj.total_cost)

    @api.model
    def create(self, vals):
        if self.user_billing:
            vals['user_billing'] = self.saas_users * self.user_cost * self.total_cycles
        if self.contract_price:
            vals['contract_price'] = self.contract_rate * self.total_cycles
        if self.total_cost:
            vals['total_cost'] = vals['contract_price'] + vals['user_billing']
        res = super(ContractCreation, self).create(vals)
        return res
    
    def write(self, vals):
        for obj in self:
            if not obj.user_billing:
                vals['user_billing'] = obj.saas_users * obj.user_cost * obj.total_cycles
            if not obj.contract_price:
                vals['contract_price'] = obj.contract_rate * obj.total_cycles
            if not obj.total_cost:
                vals['total_cost'] = vals['contract_price'] + vals['user_billing']
            res = super(ContractCreation, self).write(vals)
            return res

    @api.onchange('trial_period')
    def trial_period_change(self):
        relative_delta = relativedelta(days=self.trial_period)
        old_date = fields.Date.from_string(fields.Date.today())
        self.start_date = fields.Date.to_string(old_date + relative_delta)

    @api.onchange('plan_id')
    def plan_id_change(self):
        self.recurring_interval = self.plan_id.recurring_interval
        self.recurring_rule_type = self.plan_id.recurring_rule_type
        self.per_user_pricing = self.plan_id.per_user_pricing
        self.user_cost = self.plan_id.user_cost
        self.min_users = self.plan_id.min_users
        self.max_users = self.plan_id.max_users
        self.saas_users = self.plan_id.min_users
        self.trial_period = self.plan_id.trial_period
        self.contract_price = self.contract_rate * self.total_cycles
        self.user_billing = self.saas_users * self.user_cost * self.total_cycles 
        self.total_cost = self.contract_price + self.user_billing
        self.due_users_price = self.plan_id.due_users_price
        relative_delta = relativedelta(days=self.trial_period)
        old_date = fields.Date.from_string(fields.Date.today())
        self.start_date = fields.Date.to_string(old_date + relative_delta)
        _logger.info("=============%s",self.total_cost)
        _logger.info("=============%s",self.contract_price)

    @api.onchange('partner_id')
    def partner_id_change(self):
        self.pricelist_id = self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False
        self.currency_id = self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.currency_id.id or False

    @api.onchange('invoice_product_id')
    def invoice_product_id_change(self):
        self.contract_rate = self.invoice_product_id and self.invoice_product_id.lst_price or False
        return {
            'domain': {'invoice_product_id' : [('saas_plan_id', '=', self.plan_id.id)]}
        }
        
    def action_create_contract(self):
        for obj in self:
            if obj.per_user_pricing:
                obj.user_billing = obj.saas_users * obj.user_cost * obj.total_cycles
                if obj.saas_users < obj.min_users and obj.max_users != -1 and obj.saas_users > obj.max_users:
                    raise Warning("Please select number of users in limit {} - {}".format(obj.min_users, obj.max_users))
            obj.total_cost = obj.contract_price + obj.user_billing
            vals = dict(
                partner_id=obj.partner_id and obj.partner_id.id or False,
                recurring_interval=obj.recurring_interval,
                recurring_rule_type=obj.recurring_rule_type,
                invoice_product_id=obj.invoice_product_id and obj.invoice_product_id.id or False,
                pricelist_id=obj.partner_id.property_product_pricelist and obj.partner_id.property_product_pricelist.id or False,
                currency_id=obj.partner_id.property_product_pricelist and obj.partner_id.property_product_pricelist.currency_id and obj.partner_id.property_product_pricelist.currency_id.id or False,
                start_date=obj.start_date,
                total_cycles=obj.total_cycles,
                trial_period=obj.trial_period,
                remaining_cycles=obj.total_cycles,
                next_invoice_date=obj.start_date,
                contract_rate=obj.contract_rate,
                contract_price=obj.contract_price,
                due_users_price=obj.due_users_price,
                total_cost=obj.total_cost,
                per_user_pricing=obj.per_user_pricing,
                user_billing=obj.user_billing,
                user_cost=obj.user_cost,
                saas_users=obj.saas_users,
                min_users=obj.min_users,
                max_users=obj.max_users,
                auto_create_invoice=obj.auto_create_invoice,
                saas_module_ids=[(6, 0 , obj.plan_id.saas_module_ids.ids)],
                server_id=obj.plan_id.server_id.id,
                db_template=obj.plan_id.db_template,
                plan_id=obj.plan_id.id,
                from_backend=True,
            )

            try:
                _logger.info("!!!!!!!===!!!!!!!!%s",obj.total_cost)
                record_id = self.env['saas.contract'].create(vals)
                _logger.info("--------Contract--Created-------%r", record_id)
            except Exception as e:
                _logger.info("--------Exception-While-Creating-Contract-------%r", e)
            else:
                imd = self.env['ir.model.data']
                action = imd.xmlid_to_object('eagle_saas_kit.saas_contract_action')
                list_view_id = imd.xmlid_to_res_id('eagle_saas_kit.saas_contract_tree_view')
                form_view_id = imd.xmlid_to_res_id('eagle_saas_kit.saas_contract_form_view')

                return {
                    'name': action.name,
                    'res_id': record_id.id,
                    'type': action.type,
                    'views': [[form_view_id, 'form'], [list_view_id, 'tree'], ],
                    'target': action.target,
                    'context': action.context,
                    'res_model': action.res_model,
                }
Пример #13
0
class DeliveryCarrier(models.Model):
    _name = 'delivery.carrier'
    _description = "Shipping Methods"
    _order = 'sequence, id'
    ''' A Shipping Provider

    In order to add your own external provider, follow these steps:

    1. Create your model MyProvider that _inherit 'delivery.carrier'
    2. Extend the selection of the field "delivery_type" with a pair
       ('<my_provider>', 'My Provider')
    3. Add your methods:
       <my_provider>_rate_shipment
       <my_provider>_send_shipping
       <my_provider>_get_tracking_link
       <my_provider>_cancel_shipment
       _<my_provider>_get_default_custom_package_code
       (they are documented hereunder)
    '''

    # -------------------------------- #
    # Internals for shipping providers #
    # -------------------------------- #

    name = fields.Char('Delivery Method', required=True, translate=True)
    active = fields.Boolean(default=True)
    sequence = fields.Integer(help="Determine the display order", default=10)
    # This field will be overwritten by internal shipping providers by adding their own type (ex: 'fedex')
    delivery_type = fields.Selection([('fixed', 'Fixed Price')],
                                     string='Provider',
                                     default='fixed',
                                     required=True)
    integration_level = fields.Selection(
        [('rate', 'Get Rate'),
         ('rate_and_ship', 'Get Rate and Create Shipment')],
        string="Integration Level",
        default='rate_and_ship',
        help="Action while validating Delivery Orders")
    prod_environment = fields.Boolean(
        "Environment",
        help="Set to True if your credentials are certified for production.")
    debug_logging = fields.Boolean(
        'Debug logging', help="Log requests in order to ease debugging")
    company_id = fields.Many2one('res.company',
                                 string='Company',
                                 related='product_id.company_id',
                                 store=True,
                                 readonly=False)
    product_id = fields.Many2one('product.product',
                                 string='Delivery Product',
                                 required=True,
                                 ondelete='restrict')

    invoice_policy = fields.Selection(
        [('estimated', 'Estimated cost'), ('real', 'Real cost')],
        string='Invoicing Policy',
        default='estimated',
        required=True,
        help=
        "Estimated Cost: the customer will be invoiced the estimated cost of the shipping.\nReal Cost: the customer will be invoiced the real cost of the shipping, the cost of the shipping will be updated on the SO after the delivery."
    )

    country_ids = fields.Many2many('res.country',
                                   'delivery_carrier_country_rel',
                                   'carrier_id', 'country_id', 'Countries')
    state_ids = fields.Many2many('res.country.state',
                                 'delivery_carrier_state_rel', 'carrier_id',
                                 'state_id', 'States')
    zip_from = fields.Char('Zip From')
    zip_to = fields.Char('Zip To')

    margin = fields.Float(
        help='This percentage will be added to the shipping price.')
    free_over = fields.Boolean(
        'Free if order amount is above',
        help=
        "If the order total amount (shipping excluded) is above or equal to this value, the customer benefits from a free shipping",
        default=False)
    amount = fields.Float(
        string='Amount',
        help=
        "Amount of the order to benefit from a free shipping, expressed in the company currency"
    )

    can_generate_return = fields.Boolean(
        compute="_compute_can_generate_return")
    return_label_on_delivery = fields.Boolean(
        string="Generate Return Label",
        help="The return label is automatically generated at the delivery.")
    get_return_label_from_portal = fields.Boolean(
        string="Return Label Accessible from Customer Portal",
        help=
        "The return label can be downloaded by the customer from the customer portal."
    )

    _sql_constraints = [
        ('margin_not_under_100_percent', 'CHECK (margin >= -100)',
         'Margin cannot be lower than -100%'),
    ]

    @api.depends('delivery_type')
    def _compute_can_generate_return(self):
        for carrier in self:
            carrier.can_generate_return = False

    def toggle_prod_environment(self):
        for c in self:
            c.prod_environment = not c.prod_environment

    def toggle_debug(self):
        for c in self:
            c.debug_logging = not c.debug_logging

    def install_more_provider(self):
        return {
            'name':
            'New Providers',
            'view_mode':
            'kanban,form',
            'res_model':
            'ir.module.module',
            'domain': [['name', '=like', 'delivery_%'],
                       ['name', '!=', 'delivery_barcode']],
            'type':
            'ir.actions.act_window',
            'help':
            _('''<p class="o_view_nocontent">
                    Buy Eagle Enterprise now to get more providers.
                </p>'''),
        }

    def available_carriers(self, partner):
        return self.filtered(lambda c: c._match_address(partner))

    def _match_address(self, partner):
        self.ensure_one()
        if self.country_ids and partner.country_id not in self.country_ids:
            return False
        if self.state_ids and partner.state_id not in self.state_ids:
            return False
        if self.zip_from and (partner.zip
                              or '').upper() < self.zip_from.upper():
            return False
        if self.zip_to and (partner.zip or '').upper() > self.zip_to.upper():
            return False
        return True

    @api.onchange('integration_level')
    def _onchange_integration_level(self):
        if self.integration_level == 'rate':
            self.invoice_policy = 'estimated'

    @api.onchange('can_generate_return')
    def _onchange_can_generate_return(self):
        if not self.can_generate_return:
            self.return_label_on_delivery = False

    @api.onchange('return_label_on_delivery')
    def _onchange_return_label_on_delivery(self):
        if not self.return_label_on_delivery:
            self.get_return_label_from_portal = False

    @api.onchange('state_ids')
    def onchange_states(self):
        self.country_ids = [
            (6, 0,
             self.country_ids.ids + self.state_ids.mapped('country_id.id'))
        ]

    @api.onchange('country_ids')
    def onchange_countries(self):
        self.state_ids = [
            (6, 0,
             self.state_ids.filtered(lambda state: state.id in self.country_ids
                                     .mapped('state_ids').ids).ids)
        ]

    # -------------------------- #
    # API for external providers #
    # -------------------------- #

    def rate_shipment(self, order):
        ''' Compute the price of the order shipment

        :param order: record of sale.order
        :return dict: {'success': boolean,
                       'price': a float,
                       'error_message': a string containing an error message,
                       'warning_message': a string containing a warning message}
                       # TODO maybe the currency code?
        '''
        self.ensure_one()
        if hasattr(self, '%s_rate_shipment' % self.delivery_type):
            res = getattr(self, '%s_rate_shipment' % self.delivery_type)(order)
            # apply margin on computed price
            res['price'] = float(res['price']) * (1.0 + (self.margin / 100.0))
            # save the real price in case a free_over rule overide it to 0
            res['carrier_price'] = res['price']
            # free when order is large enough
            if res['success'] and self.free_over and order._compute_amount_total_without_delivery(
            ) >= self.amount:
                res['warning_message'] = _(
                    'The shipping is free since the order amount exceeds %.2f.'
                ) % (self.amount)
                res['price'] = 0.0
            return res

    def send_shipping(self, pickings):
        ''' Send the package to the service provider

        :param pickings: A recordset of pickings
        :return list: A list of dictionaries (one per picking) containing of the form::
                         { 'exact_price': price,
                           'tracking_number': number }
                           # TODO missing labels per package
                           # TODO missing currency
                           # TODO missing success, error, warnings
        '''
        self.ensure_one()
        if hasattr(self, '%s_send_shipping' % self.delivery_type):
            return getattr(self,
                           '%s_send_shipping' % self.delivery_type)(pickings)

    def get_return_label(self,
                         pickings,
                         tracking_number=None,
                         origin_date=None):
        self.ensure_one()
        if self.can_generate_return:
            return getattr(self, '%s_get_return_label' % self.delivery_type)(
                pickings, tracking_number, origin_date)

    def get_return_label_prefix(self):
        return 'ReturnLabel-%s' % self.delivery_type

    def get_tracking_link(self, picking):
        ''' Ask the tracking link to the service provider

        :param picking: record of stock.picking
        :return str: an URL containing the tracking link or False
        '''
        self.ensure_one()
        if hasattr(self, '%s_get_tracking_link' % self.delivery_type):
            return getattr(self, '%s_get_tracking_link' %
                           self.delivery_type)(picking)

    def cancel_shipment(self, pickings):
        ''' Cancel a shipment

        :param pickings: A recordset of pickings
        '''
        self.ensure_one()
        if hasattr(self, '%s_cancel_shipment' % self.delivery_type):
            return getattr(self,
                           '%s_cancel_shipment' % self.delivery_type)(pickings)

    def log_xml(self, xml_string, func):
        self.ensure_one()

        if self.debug_logging:
            self.flush()
            db_name = self._cr.dbname

            # Use a new cursor to avoid rollback that could be caused by an upper method
            try:
                db_registry = registry(db_name)
                with db_registry.cursor() as cr:
                    env = api.Environment(cr, SUPERUSER_ID, {})
                    IrLogging = env['ir.logging']
                    IrLogging.sudo().create({
                        'name': 'delivery.carrier',
                        'type': 'server',
                        'dbname': db_name,
                        'level': 'DEBUG',
                        'message': xml_string,
                        'path': self.delivery_type,
                        'func': func,
                        'line': 1
                    })
            except psycopg2.Error:
                pass

    def _get_default_custom_package_code(self):
        """ Some delivery carriers require a prefix to be sent in order to use custom
        packages (ie not official ones). This optional method will return it as a string.
        """
        self.ensure_one()
        if hasattr(self,
                   '_%s_get_default_custom_package_code' % self.delivery_type):
            return getattr(
                self,
                '_%s_get_default_custom_package_code' % self.delivery_type)()
        else:
            return False

    # ------------------------------------------------ #
    # Fixed price shipping, aka a very simple provider #
    # ------------------------------------------------ #

    fixed_price = fields.Float(compute='_compute_fixed_price',
                               inverse='_set_product_fixed_price',
                               store=True,
                               string='Fixed Price')

    @api.depends('product_id.list_price',
                 'product_id.product_tmpl_id.list_price')
    def _compute_fixed_price(self):
        for carrier in self:
            carrier.fixed_price = carrier.product_id.list_price

    def _set_product_fixed_price(self):
        for carrier in self:
            carrier.product_id.list_price = carrier.fixed_price

    def fixed_rate_shipment(self, order):
        carrier = self._match_address(order.partner_shipping_id)
        if not carrier:
            return {
                'success':
                False,
                'price':
                0.0,
                'error_message':
                _('Error: this delivery method is not available for this address.'
                  ),
                'warning_message':
                False
            }
        price = self.fixed_price
        if self.company_id and self.company_id.currency_id.id != order.currency_id.id:
            price = self.company_id.currency_id._convert(
                price, order.currency_id, self.company_id, fields.Date.today())
        return {
            'success': True,
            'price': price,
            'error_message': False,
            'warning_message': False
        }

    def fixed_send_shipping(self, pickings):
        res = []
        for p in pickings:
            res = res + [{
                'exact_price': p.carrier_id.fixed_price,
                'tracking_number': False
            }]
        return res

    def fixed_get_tracking_link(self, picking):
        return False

    def fixed_cancel_shipment(self, pickings):
        raise NotImplementedError()
Пример #14
0
class AccountInvoiceLine(models.Model):
    _inherit = 'account.invoice.line'

    asset_category_id = fields.Many2one('account.asset.category', string='Asset Category')
    asset_start_date = fields.Date(string='Asset Start Date', compute='_get_asset_date', readonly=True, store=True)
    asset_end_date = fields.Date(string='Asset End Date', compute='_get_asset_date', readonly=True, store=True)
    asset_mrr = fields.Float(string='Monthly Recurring Revenue', compute='_get_asset_date', readonly=True, digits=dp.get_precision('Account'), store=True)

    @api.one
    @api.depends('asset_category_id', 'invoice_id.date_invoice')
    def _get_asset_date(self):
        self.asset_mrr = 0
        self.asset_start_date = False
        self.asset_end_date = False
        cat = self.asset_category_id
        if cat:
            if cat.method_number == 0 or cat.method_period == 0:
                raise UserError(_('The number of depreciations or the period length of your asset category cannot be 0.'))
            months = cat.method_number * cat.method_period
            if self.invoice_id.type in ['out_invoice', 'out_refund']:
                self.asset_mrr = self.price_subtotal_signed / months
            if self.invoice_id.date_invoice:
                start_date = self.invoice_id.date_invoice.replace(day=1)
                end_date = (start_date + relativedelta(months=months, days=-1))
                self.asset_start_date = start_date
                self.asset_end_date = end_date

    @api.one
    def asset_create(self):
        if self.asset_category_id:
            vals = {
                'name': self.name,
                'code': self.invoice_id.number or False,
                'category_id': self.asset_category_id.id,
                'value': self.price_subtotal_signed,
                'partner_id': self.invoice_id.partner_id.id,
                'company_id': self.invoice_id.company_id.id,
                'currency_id': self.invoice_id.company_currency_id.id,
                'date': self.invoice_id.date_invoice,
                'invoice_id': self.invoice_id.id,
            }
            changed_vals = self.env['account.asset.asset'].onchange_category_id_values(vals['category_id'])
            vals.update(changed_vals['value'])
            asset = self.env['account.asset.asset'].create(vals)
            if self.asset_category_id.open_asset:
                asset.validate()
        return True

    @api.onchange('asset_category_id')
    def onchange_asset_category_id(self):
        if self.invoice_id.type == 'out_invoice' and self.asset_category_id:
            self.account_id = self.asset_category_id.account_asset_id.id
        elif self.invoice_id.type == 'in_invoice' and self.asset_category_id:
            self.account_id = self.asset_category_id.account_asset_id.id

    @api.onchange('uom_id')
    def _onchange_uom_id(self):
        result = super(AccountInvoiceLine, self)._onchange_uom_id()
        self.onchange_asset_category_id()
        return result

    @api.onchange('product_id')
    def _onchange_product_id(self):
        vals = super(AccountInvoiceLine, self)._onchange_product_id()
        if self.product_id:
            if self.invoice_id.type == 'out_invoice':
                self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id
            elif self.invoice_id.type == 'in_invoice':
                self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id
        return vals

    def _set_additional_fields(self, invoice):
        if not self.asset_category_id:
            if invoice.type == 'out_invoice':
                self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id
            elif invoice.type == 'in_invoice':
                self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id
            self.onchange_asset_category_id()
        super(AccountInvoiceLine, self)._set_additional_fields(invoice)

    def get_invoice_line_account(self, type, product, fpos, company):
        return product.asset_category_id.account_asset_id or super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
Пример #15
0
class FleetVehicleLogContract(models.Model):
    _inherit = 'fleet.vehicle.log.contract'

    recurring_cost_amount_depreciated = fields.Float(
        "Recurring Cost Amount (depreciated)", track_visibility="onchange")
Пример #16
0
class SurveyUserInputLine(models.Model):
    _name = 'survey.user_input_line'
    _description = 'Survey User Input Line'
    _rec_name = 'user_input_id'
    _order = 'question_sequence,id'

    # survey data
    user_input_id = fields.Many2one('survey.user_input',
                                    string='User Input',
                                    ondelete='cascade',
                                    required=True)
    survey_id = fields.Many2one(related='user_input_id.survey_id',
                                string='Survey',
                                store=True,
                                readonly=False)
    question_id = fields.Many2one('survey.question',
                                  string='Question',
                                  ondelete='cascade',
                                  required=True)
    page_id = fields.Many2one(related='question_id.page_id',
                              string="Section",
                              readonly=False)
    question_sequence = fields.Integer('Sequence',
                                       related='question_id.sequence',
                                       store=True)
    # answer
    skipped = fields.Boolean('Skipped')
    answer_type = fields.Selection([('text', 'Text'), ('number', 'Number'),
                                    ('date', 'Date'), ('datetime', 'Datetime'),
                                    ('free_text', 'Free Text'),
                                    ('suggestion', 'Suggestion')],
                                   string='Answer Type')
    value_text = fields.Char('Text answer')
    value_number = fields.Float('Numerical answer')
    value_date = fields.Date('Date answer')
    value_datetime = fields.Datetime('Datetime answer')
    value_free_text = fields.Text('Free Text answer')
    value_suggested = fields.Many2one('survey.label',
                                      string="Suggested answer")
    value_suggested_row = fields.Many2one('survey.label', string="Row answer")
    answer_score = fields.Float('Score')
    answer_is_correct = fields.Boolean('Correct',
                                       compute='_compute_answer_is_correct')

    @api.depends('value_suggested', 'question_id')
    def _compute_answer_is_correct(self):
        for answer in self:
            if answer.value_suggested and answer.question_id.question_type in [
                    'simple_choice', 'multiple_choice'
            ]:
                answer.answer_is_correct = answer.value_suggested.is_correct
            else:
                answer.answer_is_correct = False

    @api.constrains('skipped', 'answer_type')
    def _answered_or_skipped(self):
        for uil in self:
            if not uil.skipped != bool(uil.answer_type):
                raise ValidationError(
                    _('This question cannot be unanswered or skipped.'))

    @api.constrains('answer_type')
    def _check_answer_type(self):
        for uil in self:
            fields_type = {
                'text': bool(uil.value_text),
                'number': (bool(uil.value_number) or uil.value_number == 0),
                'date': bool(uil.value_date),
                'free_text': bool(uil.value_free_text),
                'suggestion': bool(uil.value_suggested)
            }
            if not fields_type.get(uil.answer_type, True):
                raise ValidationError(
                    _('The answer must be in the right type'))

    @api.model_create_multi
    def create(self, vals_list):
        for vals in vals_list:
            value_suggested = vals.get('value_suggested')
            if value_suggested:
                vals.update({
                    'answer_score':
                    self.env['survey.label'].browse(
                        int(value_suggested)).answer_score
                })
        return super(SurveyUserInputLine, self).create(vals_list)

    def write(self, vals):
        value_suggested = vals.get('value_suggested')
        if value_suggested:
            vals.update({
                'answer_score':
                self.env['survey.label'].browse(
                    int(value_suggested)).answer_score
            })
        return super(SurveyUserInputLine, self).write(vals)

    @api.model
    def save_lines(self, user_input_id, question, post, answer_tag):
        """ Save answers to questions, depending on question type

            If an answer already exists for question and user_input_id, it will be
            overwritten (in order to maintain data consistency).
        """
        try:
            saver = getattr(self, 'save_line_' + question.question_type)
        except AttributeError:
            _logger.error(question.question_type +
                          ": This type of question has no saving function")
            return False
        else:
            saver(user_input_id, question, post, answer_tag)

    @api.model
    def save_line_free_text(self, user_input_id, question, post, answer_tag):
        vals = {
            'user_input_id': user_input_id,
            'question_id': question.id,
            'survey_id': question.survey_id.id,
            'skipped': False,
        }
        if answer_tag in post and post[answer_tag].strip():
            vals.update({
                'answer_type': 'free_text',
                'value_free_text': post[answer_tag]
            })
        else:
            vals.update({'answer_type': None, 'skipped': True})
        old_uil = self.search([('user_input_id', '=', user_input_id),
                               ('survey_id', '=', question.survey_id.id),
                               ('question_id', '=', question.id)])
        if old_uil:
            old_uil.write(vals)
        else:
            old_uil.create(vals)
        return True

    @api.model
    def save_line_textbox(self, user_input_id, question, post, answer_tag):
        vals = {
            'user_input_id': user_input_id,
            'question_id': question.id,
            'survey_id': question.survey_id.id,
            'skipped': False
        }
        if answer_tag in post and post[answer_tag].strip():
            vals.update({
                'answer_type': 'text',
                'value_text': post[answer_tag]
            })
        else:
            vals.update({'answer_type': None, 'skipped': True})
        old_uil = self.search([('user_input_id', '=', user_input_id),
                               ('survey_id', '=', question.survey_id.id),
                               ('question_id', '=', question.id)])
        if old_uil:
            old_uil.write(vals)
        else:
            old_uil.create(vals)
        return True

    @api.model
    def save_line_numerical_box(self, user_input_id, question, post,
                                answer_tag):
        vals = {
            'user_input_id': user_input_id,
            'question_id': question.id,
            'survey_id': question.survey_id.id,
            'skipped': False
        }
        if answer_tag in post and post[answer_tag].strip():
            vals.update({
                'answer_type': 'number',
                'value_number': float(post[answer_tag])
            })
        else:
            vals.update({'answer_type': None, 'skipped': True})
        old_uil = self.search([('user_input_id', '=', user_input_id),
                               ('survey_id', '=', question.survey_id.id),
                               ('question_id', '=', question.id)])
        if old_uil:
            old_uil.write(vals)
        else:
            old_uil.create(vals)
        return True

    @api.model
    def save_line_date(self, user_input_id, question, post, answer_tag):
        vals = {
            'user_input_id': user_input_id,
            'question_id': question.id,
            'survey_id': question.survey_id.id,
            'skipped': False
        }
        if answer_tag in post and post[answer_tag].strip():
            vals.update({
                'answer_type': 'date',
                'value_date': post[answer_tag]
            })
        else:
            vals.update({'answer_type': None, 'skipped': True})
        old_uil = self.search([('user_input_id', '=', user_input_id),
                               ('survey_id', '=', question.survey_id.id),
                               ('question_id', '=', question.id)])
        if old_uil:
            old_uil.write(vals)
        else:
            old_uil.create(vals)
        return True

    @api.model
    def save_line_datetime(self, user_input_id, question, post, answer_tag):
        vals = {
            'user_input_id': user_input_id,
            'question_id': question.id,
            'survey_id': question.survey_id.id,
            'skipped': False
        }
        if answer_tag in post and post[answer_tag].strip():
            vals.update({
                'answer_type': 'datetime',
                'value_datetime': post[answer_tag]
            })
        else:
            vals.update({'answer_type': None, 'skipped': True})
        old_uil = self.search([('user_input_id', '=', user_input_id),
                               ('survey_id', '=', question.survey_id.id),
                               ('question_id', '=', question.id)])
        if old_uil:
            old_uil.write(vals)
        else:
            old_uil.create(vals)
        return True

    @api.model
    def save_line_simple_choice(self, user_input_id, question, post,
                                answer_tag):
        vals = {
            'user_input_id': user_input_id,
            'question_id': question.id,
            'survey_id': question.survey_id.id,
            'skipped': False
        }
        old_uil = self.search([('user_input_id', '=', user_input_id),
                               ('survey_id', '=', question.survey_id.id),
                               ('question_id', '=', question.id)])
        old_uil.sudo().unlink()

        if answer_tag in post and post[answer_tag].strip():
            vals.update({
                'answer_type': 'suggestion',
                'value_suggested': int(post[answer_tag])
            })
        else:
            vals.update({'answer_type': None, 'skipped': True})

        # '-1' indicates 'comment count as an answer' so do not need to record it
        if post.get(answer_tag) and post.get(answer_tag) != '-1':
            self.create(vals)

        comment_answer = post.pop(("%s_%s" % (answer_tag, 'comment')),
                                  '').strip()
        if comment_answer:
            vals.update({
                'answer_type': 'text',
                'value_text': comment_answer,
                'skipped': False,
                'value_suggested': False
            })
            self.create(vals)

        return True

    @api.model
    def save_line_multiple_choice(self, user_input_id, question, post,
                                  answer_tag):
        vals = {
            'user_input_id': user_input_id,
            'question_id': question.id,
            'survey_id': question.survey_id.id,
            'skipped': False
        }
        old_uil = self.search([('user_input_id', '=', user_input_id),
                               ('survey_id', '=', question.survey_id.id),
                               ('question_id', '=', question.id)])
        old_uil.sudo().unlink()

        ca_dict = dict_keys_startswith(post, answer_tag + '_')
        comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')),
                                     '').strip()
        if len(ca_dict) > 0:
            for key in ca_dict:
                # '-1' indicates 'comment count as an answer' so do not need to record it
                if key != ('%s_%s' % (answer_tag, '-1')):
                    val = ca_dict[key]
                    vals.update({
                        'answer_type': 'suggestion',
                        'value_suggested': bool(val) and int(val)
                    })
                    self.create(vals)
        if comment_answer:
            vals.update({
                'answer_type': 'text',
                'value_text': comment_answer,
                'value_suggested': False
            })
            self.create(vals)
        if not ca_dict and not comment_answer:
            vals.update({'answer_type': None, 'skipped': True})
            self.create(vals)
        return True

    @api.model
    def save_line_matrix(self, user_input_id, question, post, answer_tag):
        vals = {
            'user_input_id': user_input_id,
            'question_id': question.id,
            'survey_id': question.survey_id.id,
            'skipped': False
        }
        old_uil = self.search([('user_input_id', '=', user_input_id),
                               ('survey_id', '=', question.survey_id.id),
                               ('question_id', '=', question.id)])
        old_uil.sudo().unlink()

        no_answers = True
        ca_dict = dict_keys_startswith(post, answer_tag + '_')

        comment_answer = ca_dict.pop(("%s_%s" % (answer_tag, 'comment')),
                                     '').strip()
        if comment_answer:
            vals.update({'answer_type': 'text', 'value_text': comment_answer})
            self.create(vals)
            no_answers = False

        if question.matrix_subtype == 'simple':
            for row in question.labels_ids_2:
                a_tag = "%s_%s" % (answer_tag, row.id)
                if a_tag in ca_dict:
                    no_answers = False
                    vals.update({
                        'answer_type': 'suggestion',
                        'value_suggested': ca_dict[a_tag],
                        'value_suggested_row': row.id
                    })
                    self.create(vals)

        elif question.matrix_subtype == 'multiple':
            for col in question.labels_ids:
                for row in question.labels_ids_2:
                    a_tag = "%s_%s_%s" % (answer_tag, row.id, col.id)
                    if a_tag in ca_dict:
                        no_answers = False
                        vals.update({
                            'answer_type': 'suggestion',
                            'value_suggested': col.id,
                            'value_suggested_row': row.id
                        })
                        self.create(vals)
        if no_answers:
            vals.update({'answer_type': None, 'skipped': True})
            self.create(vals)
        return True
Пример #17
0
class SaleAdvancePaymentInv(models.TransientModel):
    _name = "sale.advance.payment.inv"
    _description = "Sales Advance Payment Invoice"

    @api.model
    def _count(self):
        return len(self._context.get('active_ids', []))

    @api.model
    def _default_product_id(self):
        product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id')
        return self.env['product.product'].browse(int(product_id)).exists()

    @api.model
    def _default_deposit_account_id(self):
        return self._default_product_id().property_account_income_id

    @api.model
    def _default_deposit_taxes_id(self):
        return self._default_product_id().taxes_id

    @api.model
    def _default_has_down_payment(self):
        if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False):
            sale_order = self.env['sale.order'].browse(self._context.get('active_id'))
            return sale_order.order_line.filtered(
                lambda sale_order_line: sale_order_line.is_downpayment
            )

        return False

    @api.model
    def _default_currency_id(self):
        if self._context.get('active_model') == 'sale.order' and self._context.get('active_id', False):
            sale_order = self.env['sale.order'].browse(self._context.get('active_id'))
            return sale_order.currency_id

    advance_payment_method = fields.Selection([
        ('delivered', 'Regular invoice'),
        ('percentage', 'Down payment (percentage)'),
        ('fixed', 'Down payment (fixed amount)')
        ], string='Create Invoice', default='delivered', required=True,
        help="A standard invoice is issued with all the order lines ready for invoicing, \
        according to their invoicing policy (based on ordered or delivered quantity).")
    deduct_down_payments = fields.Boolean('Deduct down payments', default=True)
    has_down_payments = fields.Boolean('Has down payments', default=_default_has_down_payment, readonly=True)
    product_id = fields.Many2one('product.product', string='Down Payment Product', domain=[('type', '=', 'service')],
        default=_default_product_id)
    count = fields.Integer(default=_count, string='Order Count')
    amount = fields.Float('Down Payment Amount', digits='Account', help="The percentage of amount to be invoiced in advance, taxes excluded.")
    currency_id = fields.Many2one('res.currency', string='Currency', default=_default_currency_id)
    fixed_amount = fields.Monetary('Down Payment Amount(Fixed)', help="The fixed amount to be invoiced in advance, taxes excluded.")
    deposit_account_id = fields.Many2one("account.account", string="Income Account", domain=[('deprecated', '=', False)],
        help="Account used for deposits", default=_default_deposit_account_id)
    deposit_taxes_id = fields.Many2many("account.tax", string="Customer Taxes", help="Taxes used for deposits", default=_default_deposit_taxes_id)

    @api.onchange('advance_payment_method')
    def onchange_advance_payment_method(self):
        if self.advance_payment_method == 'percentage':
            return {'value': {'amount': 0}}
        return {}

    def _create_invoice(self, order, so_line, amount):
        if (self.advance_payment_method == 'percentage' and self.amount <= 0.00) or (self.advance_payment_method == 'fixed' and self.fixed_amount <= 0.00):
            raise UserError(_('The value of the down payment amount must be positive.'))
        if self.advance_payment_method == 'percentage':
            amount = order.amount_untaxed * self.amount / 100
            name = _("Down payment of %s%%") % (self.amount,)
        else:
            amount = self.fixed_amount
            name = _('Down Payment')

        invoice_vals = {
            'type': 'out_invoice',
            'invoice_origin': order.name,
            'invoice_user_id': order.user_id.id,
            'narration': order.note,
            'partner_id': order.partner_invoice_id.id,
            'fiscal_position_id': order.fiscal_position_id.id or order.partner_id.property_account_position_id.id,
            'partner_shipping_id': order.partner_shipping_id.id,
            'currency_id': order.pricelist_id.currency_id.id,
            'invoice_payment_ref': order.client_order_ref,
            'invoice_payment_term_id': order.payment_term_id.id,
            'invoice_partner_bank_id': order.company_id.partner_id.bank_ids[:1],
            'team_id': order.team_id.id,
            'campaign_id': order.campaign_id.id,
            'medium_id': order.medium_id.id,
            'source_id': order.source_id.id,
            'invoice_line_ids': [(0, 0, {
                'name': name,
                'price_unit': amount,
                'quantity': 1.0,
                'product_id': self.product_id.id,
                'product_uom_id': so_line.product_uom.id,
                'tax_ids': [(6, 0, so_line.tax_id.ids)],
                'sale_line_ids': [(6, 0, [so_line.id])],
                'analytic_tag_ids': [(6, 0, so_line.analytic_tag_ids.ids)],
                'analytic_account_id': order.analytic_account_id.id or False,
            })],
        }
        if order.fiscal_position_id:
            invoice_vals['fiscal_position_id'] = order.fiscal_position_id.id
        invoice = self.env['account.move'].create(invoice_vals)
        invoice.message_post_with_view('mail.message_origin_link',
                    values={'self': invoice, 'origin': order},
                    subtype_id=self.env.ref('mail.mt_note').id)
        return invoice

    def create_invoices(self):
        sale_orders = self.env['sale.order'].browse(self._context.get('active_ids', []))

        if self.advance_payment_method == 'delivered':
            sale_orders._create_invoices(final=self.deduct_down_payments)
        else:
            # Create deposit product if necessary
            if not self.product_id:
                vals = self._prepare_deposit_product()
                self.product_id = self.env['product.product'].create(vals)
                self.env['ir.config_parameter'].sudo().set_param('sale.default_deposit_product_id', self.product_id.id)

            sale_line_obj = self.env['sale.order.line']
            for order in sale_orders:
                if self.advance_payment_method == 'percentage':
                    amount = order.amount_untaxed * self.amount / 100
                else:
                    amount = self.fixed_amount
                if self.product_id.invoice_policy != 'order':
                    raise UserError(_('The product used to invoice a down payment should have an invoice policy set to "Ordered quantities". Please update your deposit product to be able to create a deposit invoice.'))
                if self.product_id.type != 'service':
                    raise UserError(_("The product used to invoice a down payment should be of type 'Service'. Please use another product or update this product."))
                taxes = self.product_id.taxes_id.filtered(lambda r: not order.company_id or r.company_id == order.company_id)
                if order.fiscal_position_id and taxes:
                    tax_ids = order.fiscal_position_id.map_tax(taxes, self.product_id, order.partner_shipping_id).ids
                else:
                    tax_ids = taxes.ids
                context = {'lang': order.partner_id.lang}
                analytic_tag_ids = []
                for line in order.order_line:
                    analytic_tag_ids = [(4, analytic_tag.id, None) for analytic_tag in line.analytic_tag_ids]
                so_line = sale_line_obj.create({
                    'name': _('Down Payment: %s') % (time.strftime('%m %Y'),),
                    'price_unit': amount,
                    'product_uom_qty': 0.0,
                    'order_id': order.id,
                    'discount': 0.0,
                    'product_uom': self.product_id.uom_id.id,
                    'product_id': self.product_id.id,
                    'analytic_tag_ids': analytic_tag_ids,
                    'tax_id': [(6, 0, tax_ids)],
                    'is_downpayment': True,
                })
                del context
                self._create_invoice(order, so_line, amount)
        if self._context.get('open_invoices', False):
            return sale_orders.action_view_invoice()
        return {'type': 'ir.actions.act_window_close'}

    def _prepare_deposit_product(self):
        return {
            'name': 'Down payment',
            'type': 'service',
            'invoice_policy': 'order',
            'property_account_income_id': self.deposit_account_id.id,
            'taxes_id': [(6, 0, self.deposit_taxes_id.ids)],
            'company_id': False,
        }
Пример #18
0
class SurveyUserInput(models.Model):
    """ Metadata for a set of one user's answers to a particular survey """

    _name = "survey.user_input"
    _rec_name = 'survey_id'
    _description = 'Survey User Input'

    # description
    survey_id = fields.Many2one('survey.survey',
                                string='Survey',
                                required=True,
                                readonly=True,
                                ondelete='cascade')
    scoring_type = fields.Selection(string="Scoring",
                                    related="survey_id.scoring_type")
    is_attempts_limited = fields.Boolean(
        "Limited number of attempts", related='survey_id.is_attempts_limited')
    attempts_limit = fields.Integer("Number of attempts",
                                    related='survey_id.attempts_limit')
    start_datetime = fields.Datetime('Start date and time', readonly=True)
    is_time_limit_reached = fields.Boolean(
        "Is time limit reached?", compute='_compute_is_time_limit_reached')
    input_type = fields.Selection([('manually', 'Manual'),
                                   ('link', 'Invitation')],
                                  string='Answer Type',
                                  default='manually',
                                  required=True,
                                  readonly=True)
    state = fields.Selection([('new', 'Not started yet'),
                              ('skip', 'Partially completed'),
                              ('done', 'Completed')],
                             string='Status',
                             default='new',
                             readonly=True)
    test_entry = fields.Boolean(readonly=True)
    # identification and access
    token = fields.Char('Identification token',
                        default=lambda self: str(uuid.uuid4()),
                        readonly=True,
                        required=True,
                        copy=False)
    # no unique constraint, as it identifies a pool of attempts
    invite_token = fields.Char('Invite token', readonly=True, copy=False)
    partner_id = fields.Many2one('res.partner',
                                 string='Partner',
                                 readonly=True)
    email = fields.Char('E-mail', readonly=True)
    attempt_number = fields.Integer("Attempt n°",
                                    compute='_compute_attempt_number')

    # Displaying data
    last_displayed_page_id = fields.Many2one(
        'survey.question', string='Last displayed question/page')
    # answers
    user_input_line_ids = fields.One2many('survey.user_input_line',
                                          'user_input_id',
                                          string='Answers',
                                          copy=True)
    # Pre-defined questions
    question_ids = fields.Many2many('survey.question',
                                    string='Predefined Questions',
                                    readonly=True)
    deadline = fields.Datetime(
        'Deadline',
        help="Datetime until customer can open the survey and submit answers")
    # Stored for performance reasons while displaying results page
    quizz_score = fields.Float("Score (%)",
                               compute="_compute_quizz_score",
                               store=True,
                               compute_sudo=True)
    quizz_passed = fields.Boolean('Quizz Passed',
                                  compute='_compute_quizz_passed',
                                  store=True,
                                  compute_sudo=True)

    @api.depends('user_input_line_ids.answer_score',
                 'user_input_line_ids.question_id')
    def _compute_quizz_score(self):
        for user_input in self:
            total_possible_score = sum([
                answer_score if answer_score > 0 else 0 for answer_score in
                user_input.question_ids.mapped('labels_ids.answer_score')
            ])

            if total_possible_score == 0:
                user_input.quizz_score = 0
            else:
                score = (sum(
                    user_input.user_input_line_ids.mapped('answer_score')) /
                         total_possible_score) * 100
                user_input.quizz_score = round(score, 2) if score > 0 else 0

    @api.depends('quizz_score', 'survey_id.passing_score')
    def _compute_quizz_passed(self):
        for user_input in self:
            user_input.quizz_passed = user_input.quizz_score >= user_input.survey_id.passing_score

    _sql_constraints = [
        ('unique_token', 'UNIQUE (token)', 'A token must be unique!'),
    ]

    @api.model
    def do_clean_emptys(self):
        """ Remove empty user inputs that have been created manually
            (used as a cronjob declared in data/survey_cron.xml)
        """
        an_hour_ago = fields.Datetime.to_string(datetime.datetime.now() -
                                                datetime.timedelta(hours=1))
        self.search([('input_type', '=', 'manually'), ('state', '=', 'new'),
                     ('create_date', '<', an_hour_ago)]).unlink()

    @api.model
    def _generate_invite_token(self):
        return str(uuid.uuid4())

    def action_resend(self):
        partners = self.env['res.partner']
        emails = []
        for user_answer in self:
            if user_answer.partner_id:
                partners |= user_answer.partner_id
            elif user_answer.email:
                emails.append(user_answer.email)

        return self.survey_id.with_context(
            default_existing_mode='resend',
            default_partner_ids=partners.ids,
            default_emails=','.join(emails)).action_send_survey()

    def action_print_answers(self):
        """ Open the website page with the survey form """
        self.ensure_one()
        return {
            'type':
            'ir.actions.act_url',
            'name':
            "View Answers",
            'target':
            'self',
            'url':
            '/survey/print/%s?answer_token=%s' %
            (self.survey_id.access_token, self.token)
        }

    @api.depends('start_datetime', 'survey_id.is_time_limited',
                 'survey_id.time_limit')
    def _compute_is_time_limit_reached(self):
        """ Checks that the user_input is not exceeding the survey's time limit. """
        for user_input in self:
            user_input.is_time_limit_reached = user_input.survey_id.is_time_limited and fields.Datetime.now() \
                > user_input.start_datetime + relativedelta(minutes=user_input.survey_id.time_limit)

    @api.depends('state', 'test_entry', 'survey_id.is_attempts_limited',
                 'partner_id', 'email', 'invite_token')
    def _compute_attempt_number(self):
        attempts_to_compute = self.filtered(
            lambda user_input: user_input.state == 'done' and not user_input.
            test_entry and user_input.survey_id.is_attempts_limited)

        for user_input in (self - attempts_to_compute):
            user_input.attempt_number = 1

        if attempts_to_compute:
            self.env.cr.execute(
                """SELECT user_input.id, (COUNT(previous_user_input.id) + 1) AS attempt_number
                FROM survey_user_input user_input
                LEFT OUTER JOIN survey_user_input previous_user_input
                ON user_input.survey_id = previous_user_input.survey_id
                AND previous_user_input.state = 'done'
                AND previous_user_input.test_entry = False
                AND previous_user_input.id < user_input.id
                AND (user_input.invite_token IS NULL OR user_input.invite_token = previous_user_input.invite_token)
                AND (user_input.partner_id = previous_user_input.partner_id OR user_input.email = previous_user_input.email)
                WHERE user_input.id IN %s
                GROUP BY user_input.id;
            """, (tuple(attempts_to_compute.ids), ))

            attempts_count_results = self.env.cr.dictfetchall()

            for user_input in attempts_to_compute:
                attempt_number = 1
                for attempts_count_result in attempts_count_results:
                    if attempts_count_result['id'] == user_input.id:
                        attempt_number = attempts_count_result[
                            'attempt_number']
                        break

                user_input.attempt_number = attempt_number

    def _mark_done(self):
        """ This method will:
        1. mark the state as 'done'
        2. send the certification email with attached document if
        - The survey is a certification
        - It has a certification_mail_template_id set
        - The user succeeded the test
        Will also run challenge Cron to give the certification badge if any."""
        self.write({'state': 'done'})
        Challenge = self.env['gamification.challenge'].sudo()
        badge_ids = []
        for user_input in self:
            if user_input.survey_id.certificate and user_input.quizz_passed:
                if user_input.survey_id.certification_mail_template_id and not user_input.test_entry:
                    user_input.survey_id.certification_mail_template_id.send_mail(
                        user_input.id,
                        notif_layout="mail.mail_notification_light")
                if user_input.survey_id.certification_give_badge:
                    badge_ids.append(
                        user_input.survey_id.certification_badge_id.id)

        if badge_ids:
            challenges = Challenge.search([('reward_id', 'in', badge_ids)])
            if challenges:
                Challenge._cron_update(ids=challenges.ids, commit=False)

    def _get_survey_url(self):
        self.ensure_one()
        return '/survey/start/%s?answer_token=%s' % (
            self.survey_id.access_token, self.token)
Пример #19
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
            )
        """)
Пример #20
0
class ProductTemplateAttributeValue(models.Model):
    _inherit = "product.template.attribute.value"

    price_factor = fields.Float(
        "Price Factor", digits=dp.get_precision("Product Price"), default=1.0
    )
Пример #21
0
class HrEmployeeBase(models.AbstractModel):
    _inherit = "hr.employee.base"

    leave_manager_id = fields.Many2one(
        'res.users',
        string='Time Off',
        help="User responsible of leaves approval.")
    remaining_leaves = fields.Float(
        compute='_compute_remaining_leaves',
        string='Remaining Paid Time Off',
        help=
        'Total number of paid time off allocated to this employee, change this value to create allocation/time off request. '
        'Total based on all the time off types without overriding limit.')
    current_leave_state = fields.Selection(compute='_compute_leave_status',
                                           string="Current Time Off Status",
                                           selection=[
                                               ('draft', 'New'),
                                               ('confirm', 'Waiting Approval'),
                                               ('refuse', 'Refused'),
                                               ('validate1',
                                                'Waiting Second Approval'),
                                               ('validate', 'Approved'),
                                               ('cancel', 'Cancelled')
                                           ])
    current_leave_id = fields.Many2one('hr.leave.type',
                                       compute='_compute_leave_status',
                                       string="Current Time Off Type")
    leave_date_from = fields.Date('From Date', compute='_compute_leave_status')
    leave_date_to = fields.Date('To Date', compute='_compute_leave_status')
    leaves_count = fields.Float('Number of Time Off',
                                compute='_compute_remaining_leaves')
    allocation_count = fields.Float('Total number of days allocated.',
                                    compute='_compute_allocation_count')
    allocation_used_count = fields.Float(
        'Total number of days off used',
        compute='_compute_total_allocation_used')
    show_leaves = fields.Boolean('Able to see Remaining Time Off',
                                 compute='_compute_show_leaves')
    is_absent = fields.Boolean('Absent Today',
                               compute='_compute_leave_status',
                               search='_search_absent_employee')
    allocation_display = fields.Char(compute='_compute_allocation_count')
    allocation_used_display = fields.Char(
        compute='_compute_total_allocation_used')

    def _get_date_start_work(self):
        return self.create_date

    def _get_remaining_leaves(self):
        """ Helper to compute the remaining leaves for the current employees
            :returns dict where the key is the employee id, and the value is the remain leaves
        """
        self._cr.execute(
            """
            SELECT
                sum(h.number_of_days) AS days,
                h.employee_id
            FROM
                (
                    SELECT holiday_status_id, number_of_days,
                        state, employee_id
                    FROM hr_leave_allocation
                    UNION ALL
                    SELECT holiday_status_id, (number_of_days * -1) as number_of_days,
                        state, employee_id
                    FROM hr_leave
                ) h
                join hr_leave_type s ON (s.id=h.holiday_status_id)
            WHERE
                s.active = true AND h.state='validate' AND
                (s.allocation_type='fixed' OR s.allocation_type='fixed_allocation') AND
                h.employee_id in %s
            GROUP BY h.employee_id""", (tuple(self.ids), ))
        return dict((row['employee_id'], row['days'])
                    for row in self._cr.dictfetchall())

    def _compute_remaining_leaves(self):
        remaining = self._get_remaining_leaves()
        for employee in self:
            value = float_round(remaining.get(employee.id, 0.0),
                                precision_digits=2)
            employee.leaves_count = value
            employee.remaining_leaves = value

    def _compute_allocation_count(self):
        for employee in self:
            allocations = self.env['hr.leave.allocation'].search([
                ('employee_id', '=', employee.id),
                ('holiday_status_id.active', '=', True),
                ('state', '=', 'validate'),
                '|',
                ('date_to', '=', False),
                ('date_to', '>=', datetime.date.today()),
            ])
            employee.allocation_count = sum(
                allocations.mapped('number_of_days'))
            employee.allocation_display = "%g" % employee.allocation_count

    def _compute_total_allocation_used(self):
        for employee in self:
            employee.allocation_used_count = employee.allocation_count - employee.remaining_leaves
            employee.allocation_used_display = "%g" % employee.allocation_used_count

    def _compute_presence_state(self):
        super()._compute_presence_state()
        employees = self.filtered(lambda employee: employee.hr_presence_state
                                  != 'present' and employee.is_absent)
        employees.update({'hr_presence_state': 'absent'})

    def _compute_leave_status(self):
        # Used SUPERUSER_ID to forcefully get status of other user's leave, to bypass record rule
        holidays = self.env['hr.leave'].sudo().search([
            ('employee_id', 'in', self.ids),
            ('date_from', '<=', fields.Datetime.now()),
            ('date_to', '>=', fields.Datetime.now()),
            ('state', 'not in', ('cancel', 'refuse'))
        ])
        leave_data = {}
        for holiday in holidays:
            leave_data[holiday.employee_id.id] = {}
            leave_data[holiday.employee_id.
                       id]['leave_date_from'] = holiday.date_from.date()
            leave_data[holiday.employee_id.
                       id]['leave_date_to'] = holiday.date_to.date()
            leave_data[
                holiday.employee_id.id]['current_leave_state'] = holiday.state
            leave_data[holiday.employee_id.
                       id]['current_leave_id'] = holiday.holiday_status_id.id

        for employee in self:
            employee.leave_date_from = leave_data.get(
                employee.id, {}).get('leave_date_from')
            employee.leave_date_to = leave_data.get(employee.id,
                                                    {}).get('leave_date_to')
            employee.current_leave_state = leave_data.get(
                employee.id, {}).get('current_leave_state')
            employee.current_leave_id = leave_data.get(
                employee.id, {}).get('current_leave_id')
            employee.is_absent = leave_data.get(
                employee.id) and leave_data.get(
                    employee.id, {}).get('current_leave_state') not in [
                        'cancel', 'refuse', 'draft'
                    ]

    @api.onchange('parent_id')
    def _onchange_parent_id(self):
        super(HrEmployeeBase, self)._onchange_parent_id()
        previous_manager = self._origin.parent_id.user_id
        manager = self.parent_id.user_id
        if manager and self.leave_manager_id == previous_manager or not self.leave_manager_id:
            self.leave_manager_id = manager

    def _compute_show_leaves(self):
        show_leaves = self.env['res.users'].has_group(
            'hr_holidays.group_hr_holidays_user')
        for employee in self:
            if show_leaves or employee.user_id == self.env.user:
                employee.show_leaves = True
            else:
                employee.show_leaves = False

    def _search_absent_employee(self, operator, value):
        holidays = self.env['hr.leave'].sudo().search([
            ('employee_id', '!=', False),
            ('state', 'not in', ['cancel', 'refuse']),
            ('date_from', '<=', datetime.datetime.utcnow()),
            ('date_to', '>=', datetime.datetime.utcnow())
        ])
        return [('id', 'in', holidays.mapped('employee_id').ids)]

    @api.model
    def create(self, values):
        if 'parent_id' in values:
            manager = self.env['hr.employee'].browse(
                values['parent_id']).user_id
            values['leave_manager_id'] = values.get('leave_manager_id',
                                                    manager.id)
        return super(HrEmployeeBase, self).create(values)

    def write(self, values):
        if 'parent_id' in values:
            manager = self.env['hr.employee'].browse(
                values['parent_id']).user_id
            if manager:
                to_change = self.filtered(
                    lambda e: e.leave_manager_id == e.parent_id.user_id or
                    not e.leave_manager_id)
                to_change.write({
                    'leave_manager_id':
                    values.get('leave_manager_id', manager.id)
                })

        res = super(HrEmployeeBase, self).write(values)
        if 'parent_id' in values or 'department_id' in values:
            today_date = fields.Datetime.now()
            hr_vals = {}
            if values.get('parent_id') is not None:
                hr_vals['manager_id'] = values['parent_id']
            if values.get('department_id') is not None:
                hr_vals['department_id'] = values['department_id']
            holidays = self.env['hr.leave'].sudo().search([
                '|', ('state', 'in', ['draft', 'confirm']),
                ('date_from', '>', today_date), ('employee_id', 'in', self.ids)
            ])
            holidays.write(hr_vals)
            allocations = self.env['hr.leave.allocation'].sudo().search([
                ('state', 'in', ['draft', 'confirm']),
                ('employee_id', 'in', self.ids)
            ])
            allocations.write(hr_vals)
        return res
Пример #22
0
class Task(models.Model):
    _name = "project.task"
    _description = "Task"
    _date_name = "date_assign"
    _inherit = ['portal.mixin', 'mail.thread.cc', 'mail.activity.mixin', 'rating.mixin']
    _mail_post_access = 'read'
    _order = "priority desc, sequence, id desc"
    _check_company_auto = True

    @api.model
    def default_get(self, fields_list):
        result = super(Task, self).default_get(fields_list)
        # find default value from parent for the not given ones
        parent_task_id = result.get('parent_id') or self._context.get('default_parent_id')
        if parent_task_id:
            parent_values = self._subtask_values_from_parent(parent_task_id)
            for fname, value in parent_values.items():
                if fname not in result:
                    result[fname] = value
        return result

    @api.model
    def _get_default_partner(self):
        if 'default_project_id' in self.env.context:
            default_project_id = self.env['project.project'].browse(self.env.context['default_project_id'])
            return default_project_id.exists().partner_id

    def _get_default_stage_id(self):
        """ Gives default stage_id """
        project_id = self.env.context.get('default_project_id')
        if not project_id:
            return False
        return self.stage_find(project_id, [('fold', '=', False)])

    @api.model
    def _default_company_id(self):
        if self._context.get('default_project_id'):
            return self.env['project.project'].browse(self._context['default_project_id']).company_id
        return self.env.company

    @api.model
    def _read_group_stage_ids(self, stages, domain, order):
        search_domain = [('id', 'in', stages.ids)]
        if 'default_project_id' in self.env.context:
            search_domain = ['|', ('project_ids', '=', self.env.context['default_project_id'])] + search_domain

        stage_ids = stages._search(search_domain, order=order, access_rights_uid=SUPERUSER_ID)
        return stages.browse(stage_ids)

    active = fields.Boolean(default=True)
    name = fields.Char(string='Title', tracking=True, required=True, index=True)
    description = fields.Html(string='Description')
    priority = fields.Selection([
        ('0', 'Normal'),
        ('1', 'Important'),
    ], default='0', index=True, string="Priority")
    sequence = fields.Integer(string='Sequence', index=True, default=10,
        help="Gives the sequence order when displaying a list of tasks.")
    stage_id = fields.Many2one('project.task.type', string='Stage', ondelete='restrict', tracking=True, index=True,
        default=_get_default_stage_id, group_expand='_read_group_stage_ids',
        domain="[('project_ids', '=', project_id)]", copy=False)
    tag_ids = fields.Many2many('project.tags', string='Tags')
    kanban_state = fields.Selection([
        ('normal', 'Grey'),
        ('done', 'Green'),
        ('blocked', 'Red')], string='Kanban State',
        copy=False, default='normal', required=True)
    kanban_state_label = fields.Char(compute='_compute_kanban_state_label', string='Kanban State Label', tracking=True)
    create_date = fields.Datetime("Created On", readonly=True, index=True)
    write_date = fields.Datetime("Last Updated On", readonly=True, index=True)
    date_end = fields.Datetime(string='Ending Date', index=True, copy=False)
    date_assign = fields.Datetime(string='Assigning Date', index=True, copy=False, readonly=True)
    date_deadline = fields.Date(string='Deadline', index=True, copy=False, tracking=True)
    date_deadline_formatted = fields.Char(compute='_compute_date_deadline_formatted')
    date_last_stage_update = fields.Datetime(string='Last Stage Update',
        index=True,
        copy=False,
        readonly=True)
    project_id = fields.Many2one('project.project', string='Project', default=lambda self: self.env.context.get('default_project_id'),
        index=True, tracking=True, check_company=True, change_default=True)
    planned_hours = fields.Float("Planned Hours", help='It is the time planned to achieve the task. If this document has sub-tasks, it means the time needed to achieve this tasks and its childs.',tracking=True)
    subtask_planned_hours = fields.Float("Subtasks", compute='_compute_subtask_planned_hours', help="Computed using sum of hours planned of all subtasks created from main task. Usually these hours are less or equal to the Planned Hours (of main task).")
    user_id = fields.Many2one('res.users',
        string='Assigned to',
        default=lambda self: self.env.uid,
        index=True, tracking=True)
    partner_id = fields.Many2one('res.partner',
        string='Customer',
        default=lambda self: self._get_default_partner(),
        domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
    partner_city = fields.Char(related='partner_id.city', readonly=False)
    manager_id = fields.Many2one('res.users', string='Project Manager', related='project_id.user_id', readonly=True, related_sudo=False)
    company_id = fields.Many2one('res.company', string='Company', required=True, default=_default_company_id)
    color = fields.Integer(string='Color Index')
    user_email = fields.Char(related='user_id.email', string='User Email', readonly=True, related_sudo=False)
    attachment_ids = fields.One2many('ir.attachment', compute='_compute_attachment_ids', string="Main Attachments",
        help="Attachment that don't come from message.")
    # In the domain of displayed_image_id, we couln't use attachment_ids because a one2many is represented as a list of commands so we used res_model & res_id
    displayed_image_id = fields.Many2one('ir.attachment', domain="[('res_model', '=', 'project.task'), ('res_id', '=', id), ('mimetype', 'ilike', 'image')]", string='Cover Image')
    legend_blocked = fields.Char(related='stage_id.legend_blocked', string='Kanban Blocked Explanation', readonly=True, related_sudo=False)
    legend_done = fields.Char(related='stage_id.legend_done', string='Kanban Valid Explanation', readonly=True, related_sudo=False)
    legend_normal = fields.Char(related='stage_id.legend_normal', string='Kanban Ongoing Explanation', readonly=True, related_sudo=False)
    parent_id = fields.Many2one('project.task', string='Parent Task', index=True)
    child_ids = fields.One2many('project.task', 'parent_id', string="Sub-tasks", context={'active_test': False})
    subtask_project_id = fields.Many2one('project.project', related="project_id.subtask_project_id", string='Sub-task Project', readonly=True)
    subtask_count = fields.Integer("Sub-task count", compute='_compute_subtask_count')
    email_from = fields.Char(string='Email', help="These people will receive email.", index=True)
    # Computed field about working time elapsed between record creation and assignation/closing.
    working_hours_open = fields.Float(compute='_compute_elapsed', string='Working hours to assign', store=True, group_operator="avg")
    working_hours_close = fields.Float(compute='_compute_elapsed', string='Working hours to close', store=True, group_operator="avg")
    working_days_open = fields.Float(compute='_compute_elapsed', string='Working days to assign', store=True, group_operator="avg")
    working_days_close = fields.Float(compute='_compute_elapsed', string='Working days to close', store=True, group_operator="avg")
    # customer portal: include comment and incoming emails in communication history
    website_message_ids = fields.One2many(domain=lambda self: [('model', '=', self._name), ('message_type', 'in', ['email', 'comment'])])

    @api.depends('date_deadline')
    def _compute_date_deadline_formatted(self):
        for task in self:
            task.date_deadline_formatted = format_date(self.env, task.date_deadline) if task.date_deadline else None

    def _compute_attachment_ids(self):
        for task in self:
            attachment_ids = self.env['ir.attachment'].search([('res_id', '=', task.id), ('res_model', '=', 'project.task')]).ids
            message_attachment_ids = task.mapped('message_ids.attachment_ids').ids  # from mail_thread
            task.attachment_ids = [(6, 0, list(set(attachment_ids) - set(message_attachment_ids)))]

    @api.depends('create_date', 'date_end', 'date_assign')
    def _compute_elapsed(self):
        task_linked_to_calendar = self.filtered(
            lambda task: task.project_id.resource_calendar_id and task.create_date
        )
        for task in task_linked_to_calendar:
            dt_create_date = fields.Datetime.from_string(task.create_date)

            if task.date_assign:
                dt_date_assign = fields.Datetime.from_string(task.date_assign)
                duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_assign, compute_leaves=True)
                task.working_hours_open = duration_data['hours']
                task.working_days_open = duration_data['days']
            else:
                task.working_hours_open = 0.0
                task.working_days_open = 0.0

            if task.date_end:
                dt_date_end = fields.Datetime.from_string(task.date_end)
                duration_data = task.project_id.resource_calendar_id.get_work_duration_data(dt_create_date, dt_date_end, compute_leaves=True)
                task.working_hours_close = duration_data['hours']
                task.working_days_close = duration_data['days']
            else:
                task.working_hours_close = 0.0
                task.working_days_close = 0.0

        (self - task_linked_to_calendar).update(dict.fromkeys(
            ['working_hours_open', 'working_hours_close', 'working_days_open', 'working_days_close'], 0.0))

    @api.depends('stage_id', 'kanban_state')
    def _compute_kanban_state_label(self):
        for task in self:
            if task.kanban_state == 'normal':
                task.kanban_state_label = task.legend_normal
            elif task.kanban_state == 'blocked':
                task.kanban_state_label = task.legend_blocked
            else:
                task.kanban_state_label = task.legend_done

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

    def _compute_access_warning(self):
        super(Task, self)._compute_access_warning()
        for task in self.filtered(lambda x: x.project_id.privacy_visibility != 'portal'):
            task.access_warning = _(
                "The task cannot be shared with the recipient(s) because the privacy of the project is too restricted. Set the privacy of the project to 'Visible by following customers' in order to make it accessible by the recipient(s).")

    @api.depends('child_ids.planned_hours')
    def _compute_subtask_planned_hours(self):
        for task in self:
            task.subtask_planned_hours = sum(task.child_ids.mapped('planned_hours'))

    @api.depends('child_ids')
    def _compute_subtask_count(self):
        """ Note: since we accept only one level subtask, we can use a read_group here """
        task_data = self.env['project.task'].read_group([('parent_id', 'in', self.ids)], ['parent_id'], ['parent_id'])
        mapping = dict((data['parent_id'][0], data['parent_id_count']) for data in task_data)
        for task in self:
            task.subtask_count = mapping.get(task.id, 0)

    @api.onchange('partner_id')
    def _onchange_partner_id(self):
        self.email_from = self.partner_id.email

    @api.onchange('parent_id')
    def _onchange_parent_id(self):
        if self.parent_id:
            for field_name, value in self._subtask_values_from_parent(self.parent_id.id).items():
                if not self[field_name]:
                    self[field_name] = value

    @api.onchange('project_id')
    def _onchange_project(self):
        if self.project_id:
            # find partner
            if self.project_id.partner_id:
                self.partner_id = self.project_id.partner_id
            # find stage
            if self.project_id not in self.stage_id.project_ids:
                self.stage_id = self.stage_find(self.project_id.id, [('fold', '=', False)])
            # keep multi company consistency
            self.company_id = self.project_id.company_id
        else:
            self.stage_id = False

    @api.constrains('parent_id', 'child_ids')
    def _check_subtask_level(self):
        for task in self:
            if task.parent_id and task.child_ids:
                raise ValidationError(_('Task %s cannot have several subtask levels.' % (task.name,)))

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

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

    @api.model
    def get_empty_list_help(self, help):
        tname = _("task")
        project_id = self.env.context.get('default_project_id', False)
        if project_id:
            name = self.env['project.project'].browse(project_id).label_tasks
            if name: tname = name.lower()

        self = self.with_context(
            empty_list_help_id=self.env.context.get('default_project_id'),
            empty_list_help_model='project.project',
            empty_list_help_document_name=tname,
        )
        return super(Task, self).get_empty_list_help(help)

    # ----------------------------------------
    # Case management
    # ----------------------------------------

    def stage_find(self, section_id, domain=[], order='sequence'):
        """ Override of the base.stage method
            Parameter of the stage search taken from the lead:
            - section_id: if set, stages must belong to this section or
              be a default stage; if not set, stages must be default
              stages
        """
        # collect all section_ids
        section_ids = []
        if section_id:
            section_ids.append(section_id)
        section_ids.extend(self.mapped('project_id').ids)
        search_domain = []
        if section_ids:
            search_domain = [('|')] * (len(section_ids) - 1)
            for section_id in section_ids:
                search_domain.append(('project_ids', '=', section_id))
        search_domain += list(domain)
        # perform search, return the first found
        return self.env['project.task.type'].search(search_domain, order=order, limit=1).id

    # ------------------------------------------------
    # CRUD overrides
    # ------------------------------------------------

    @api.model
    def create(self, vals):
        # context: no_log, because subtype already handle this
        context = dict(self.env.context)
        # for default stage
        if vals.get('project_id') and not context.get('default_project_id'):
            context['default_project_id'] = vals.get('project_id')
        # user_id change: update date_assign
        if vals.get('user_id'):
            vals['date_assign'] = fields.Datetime.now()
        # Stage change: Update date_end if folded stage and date_last_stage_update
        if vals.get('stage_id'):
            vals.update(self.update_date_end(vals['stage_id']))
            vals['date_last_stage_update'] = fields.Datetime.now()
        # substask default values
        if vals.get('parent_id'):
            for fname, value in self._subtask_values_from_parent(vals['parent_id']).items():
                if fname not in vals:
                    vals[fname] = value
        task = super(Task, self.with_context(context)).create(vals)
        return task

    def write(self, vals):
        now = fields.Datetime.now()
        # stage change: update date_last_stage_update
        if 'stage_id' in vals:
            vals.update(self.update_date_end(vals['stage_id']))
            vals['date_last_stage_update'] = now
            # reset kanban state when changing stage
            if 'kanban_state' not in vals:
                vals['kanban_state'] = 'normal'
        # user_id change: update date_assign
        if vals.get('user_id') and 'date_assign' not in vals:
            vals['date_assign'] = now

        result = super(Task, self).write(vals)
        # rating on stage
        if 'stage_id' in vals and vals.get('stage_id'):
            self.filtered(lambda x: x.project_id.rating_status == 'stage')._send_task_rating_mail(force_send=True)
        return result

    def update_date_end(self, stage_id):
        project_task_type = self.env['project.task.type'].browse(stage_id)
        if project_task_type.fold:
            return {'date_end': fields.Datetime.now()}
        return {'date_end': False}

    # ---------------------------------------------------
    # Subtasks
    # ---------------------------------------------------

    def _subtask_default_fields(self):
        """ Return the list of field name for default value when creating a subtask """
        return ['partner_id', 'email_from']

    def _subtask_values_from_parent(self, parent_id):
        """ Get values for substask implied field of the given"""
        result = {}
        parent_task = self.env['project.task'].browse(parent_id)
        for field_name in self._subtask_default_fields():
            result[field_name] = parent_task[field_name]
        # special case for the subtask default project
        result['project_id'] = parent_task.project_id.subtask_project_id
        return self._convert_to_write(result)

    # ---------------------------------------------------
    # Mail gateway
    # ---------------------------------------------------

    def _track_template(self, changes):
        res = super(Task, self)._track_template(changes)
        test_task = self[0]
        if 'stage_id' in changes and test_task.stage_id.mail_template_id:
            res['stage_id'] = (test_task.stage_id.mail_template_id, {
                'auto_delete_message': True,
                'subtype_id': self.env['ir.model.data'].xmlid_to_res_id('mail.mt_note'),
                'email_layout_xmlid': 'mail.mail_notification_light'
            })
        return res

    def _creation_subtype(self):
        return self.env.ref('project.mt_task_new')

    def _track_subtype(self, init_values):
        self.ensure_one()
        if 'kanban_state_label' in init_values and self.kanban_state == 'blocked':
            return self.env.ref('project.mt_task_blocked')
        elif 'kanban_state_label' in init_values and self.kanban_state == 'done':
            return self.env.ref('project.mt_task_ready')
        elif 'stage_id' in init_values:
            return self.env.ref('project.mt_task_stage')
        return super(Task, self)._track_subtype(init_values)

    def _notify_get_groups(self):
        """ Handle project users and managers recipients that can assign
        tasks and create new one directly from notification emails. Also give
        access button to portal users and portal customers. If they are notified
        they should probably have access to the document. """
        groups = super(Task, self)._notify_get_groups()

        self.ensure_one()

        project_user_group_id = self.env.ref('project.group_project_user').id
        new_group = (
            'group_project_user',
            lambda pdata: pdata['type'] == 'user' and project_user_group_id in pdata['groups'],
            {},
        )

        if not self.user_id and not self.stage_id.fold:
            take_action = self._notify_get_action_link('assign')
            project_actions = [{'url': take_action, 'title': _('I take it')}]
            new_group[2]['actions'] = project_actions

        groups = [new_group] + groups

        for group_name, group_method, group_data in groups:
            if group_name != 'customer':
                group_data['has_button_access'] = True

        return groups

    def _notify_get_reply_to(self, default=None, records=None, company=None, doc_names=None):
        """ Override to set alias of tasks to their project if any. """
        aliases = self.sudo().mapped('project_id')._notify_get_reply_to(default=default, records=None, company=company, doc_names=None)
        res = {task.id: aliases.get(task.project_id.id) for task in self}
        leftover = self.filtered(lambda rec: not rec.project_id)
        if leftover:
            res.update(super(Task, leftover)._notify_get_reply_to(default=default, records=None, company=company, doc_names=doc_names))
        return res

    def email_split(self, msg):
        email_list = tools.email_split((msg.get('to') or '') + ',' + (msg.get('cc') or ''))
        # check left-part is not already an alias
        aliases = self.mapped('project_id.alias_name')
        return [x for x in email_list if x.split('@')[0] not in aliases]

    @api.model
    def message_new(self, msg, custom_values=None):
        """ Overrides mail_thread message_new that is called by the mailgateway
            through message_process.
            This override updates the document according to the email.
        """
        # remove default author when going through the mail gateway. Indeed we
        # do not want to explicitly set user_id to False; however we do not
        # want the gateway user to be responsible if no other responsible is
        # found.
        create_context = dict(self.env.context or {})
        create_context['default_user_id'] = False
        if custom_values is None:
            custom_values = {}
        defaults = {
            'name': msg.get('subject') or _("No Subject"),
            'email_from': msg.get('from'),
            'planned_hours': 0.0,
            'partner_id': msg.get('author_id')
        }
        defaults.update(custom_values)

        task = super(Task, self.with_context(create_context)).message_new(msg, custom_values=defaults)
        email_list = task.email_split(msg)
        partner_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(email_list, records=task, force_create=False) if p]
        task.message_subscribe(partner_ids)
        return task

    def message_update(self, msg, update_vals=None):
        """ Override to update the task according to the email. """
        email_list = self.email_split(msg)
        partner_ids = [p.id for p in self.env['mail.thread']._mail_find_partner_from_emails(email_list, records=self, force_create=False) if p]
        self.message_subscribe(partner_ids)
        return super(Task, self).message_update(msg, update_vals=update_vals)

    def _message_get_suggested_recipients(self):
        recipients = super(Task, self)._message_get_suggested_recipients()
        for task in self:
            if task.partner_id:
                reason = _('Customer Email') if task.partner_id.email else _('Customer')
                task._message_add_suggested_recipient(recipients, partner=task.partner_id, reason=reason)
            elif task.email_from:
                task._message_add_suggested_recipient(recipients, email=task.email_from, reason=_('Customer Email'))
        return recipients

    def _notify_email_header_dict(self):
        headers = super(Task, self)._notify_email_header_dict()
        if self.project_id:
            current_objects = [h for h in headers.get('X-Eagle-Objects', '').split(',') if h]
            current_objects.insert(0, 'project.project-%s, ' % self.project_id.id)
            headers['X-Eagle-Objects'] = ','.join(current_objects)
        if self.tag_ids:
            headers['X-Eagle-Tags'] = ','.join(self.tag_ids.mapped('name'))
        return headers

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

    def action_assign_to_me(self):
        self.write({'user_id': self.env.user.id})

    def action_open_parent_task(self):
        return {
            'name': _('Parent Task'),
            'view_mode': 'form',
            'res_model': 'project.task',
            'res_id': self.parent_id.id,
            'type': 'ir.actions.act_window',
            'context': dict(self._context, create=False)
        }

    def action_subtask(self):
        action = self.env.ref('project.project_task_action_sub_task').read()[0]

        # only display subtasks of current task
        action['domain'] = [('id', 'child_of', self.id), ('id', '!=', self.id)]

        # update context, with all default values as 'quick_create' does not contains all field in its view
        if self._context.get('default_project_id'):
            default_project = self.env['project.project'].browse(self.env.context['default_project_id'])
        else:
            default_project = self.project_id.subtask_project_id or self.project_id
        ctx = dict(self.env.context)
        ctx.update({
            'default_name': self.env.context.get('name', self.name) + ':',
            'default_parent_id': self.id,  # will give default subtask field in `default_get`
            'default_company_id': default_project.company_id.id if default_project else self.env.company.id,
            'search_default_parent_id': self.id,
        })
        parent_values = self._subtask_values_from_parent(self.id)
        for fname, value in parent_values.items():
            if 'default_' + fname not in ctx:
                ctx['default_' + fname] = value
        action['context'] = ctx

        return action

    # ---------------------------------------------------
    # Rating business
    # ---------------------------------------------------

    def _send_task_rating_mail(self, force_send=False):
        for task in self:
            rating_template = task.stage_id.rating_template_id
            if rating_template:
                task.rating_send_request(rating_template, lang=task.partner_id.lang, force_send=force_send)

    def rating_get_partner_id(self):
        res = super(Task, self).rating_get_partner_id()
        if not res and self.project_id.partner_id:
            return self.project_id.partner_id
        return res

    def rating_apply(self, rate, token=None, feedback=None, subtype=None):
        return super(Task, self).rating_apply(rate, token=token, feedback=feedback, subtype="project.mt_task_rating")

    def _rating_get_parent_field_name(self):
        return 'project_id'
Пример #23
0
class AccountInvoiceSend(models.TransientModel):
    _name = 'account.invoice.send'
    _inherit = 'account.invoice.send'
    _description = 'Account Invoice Send'

    partner_id = fields.Many2one('res.partner', compute='_get_partner', string='Partner')
    snailmail_is_letter = fields.Boolean('Send by Post', help='Allows to send the document by Snailmail (coventional posting delivery service)', default=lambda self: self.env.company.invoice_is_snailmail)
    snailmail_cost = fields.Float(string='Stamp(s)', compute='_compute_snailmail_cost', readonly=True)
    invalid_addresses = fields.Integer('Invalid Addresses Count', compute='_compute_invalid_addresses')
    invalid_invoice_ids = fields.Many2many('account.move', string='Invalid Addresses', compute='_compute_invalid_addresses')

    @api.depends('invoice_ids')
    def _compute_invalid_addresses(self):
        for wizard in self:
            invalid_invoices = wizard.invoice_ids.filtered(lambda i: not self.env['snailmail.letter']._is_valid_address(i.partner_id))
            wizard.invalid_invoice_ids = invalid_invoices
            wizard.invalid_addresses = len(invalid_invoices)

    @api.depends('invoice_ids')
    def _get_partner(self):
        self.partner_id = self.env['res.partner']
        for wizard in self:
            if wizard.invoice_ids and len(wizard.invoice_ids) == 1:
                wizard.partner_id = wizard.invoice_ids.partner_id.id

    @api.depends('snailmail_is_letter')
    def _compute_snailmail_cost(self):
        for wizard in self:
            wizard.snailmail_cost = len(wizard.invoice_ids.ids)

    def snailmail_print_action(self):
        self.ensure_one()
        letters = self.env['snailmail.letter']
        for invoice in self.invoice_ids:
            letter = self.env['snailmail.letter'].create({
                'partner_id': invoice.partner_id.id,
                'model': 'account.move',
                'res_id': invoice.id,
                'user_id': self.env.user.id,
                'company_id': invoice.company_id.id,
                'report_template': self.env.ref('account.account_invoices').id
            })
            letters |= letter

        self.invoice_ids.filtered(lambda inv: not inv.invoice_sent).write({'invoice_sent': True})
        if len(self.invoice_ids) == 1:
            letters._snailmail_print()
        else:
            letters._snailmail_print(immediate=False)

    def send_and_print_action(self):
        if self.snailmail_is_letter:
            if self.invalid_addresses and self.composition_mode == "mass_mail":
                self.notify_invalid_addresses()
            self.snailmail_print_action()
        res = super(AccountInvoiceSend, self).send_and_print_action()
        return res

    def notify_invalid_addresses(self):
        self.ensure_one()
        self.env['bus.bus'].sendone(
            (self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
            {'type': 'snailmail_invalid_address', 'title': _("Invalid Addresses"),
            'message': _("%s of the selected invoice(s) had an invalid address and were not sent") % self.invalid_addresses}
        )

    def invalid_addresses_action(self):
        return {
            'name': _('Invalid Addresses'),
            'type': 'ir.actions.act_window',
            'view_mode': 'kanban,tree,form',
            'res_model': 'account.move',
            'domain': [('id', 'in', self.mapped('invalid_invoice_ids').ids)],
        }
Пример #24
0
class Task(models.Model):
    _inherit = "project.task"

    analytic_account_active = fields.Boolean("Analytic Account", related='project_id.analytic_account_id.active', readonly=True)
    allow_timesheets = fields.Boolean("Allow timesheets", related='project_id.allow_timesheets', help="Timesheets can be logged on this task.", readonly=True)
    remaining_hours = fields.Float("Remaining Hours", compute='_compute_remaining_hours', store=True, readonly=True, help="Total remaining time, can be re-estimated periodically by the assignee of the task.")
    effective_hours = fields.Float("Hours Spent", compute='_compute_effective_hours', compute_sudo=True, store=True, help="Computed using the sum of the task work done.")
    total_hours_spent = fields.Float("Total Hours", compute='_compute_total_hours_spent', store=True, help="Computed as: Time Spent + Sub-tasks Hours.")
    progress = fields.Float("Progress", compute='_compute_progress_hours', store=True, group_operator="avg", help="Display progress of current task.")
    subtask_effective_hours = fields.Float("Sub-tasks Hours Spent", compute='_compute_subtask_effective_hours', store=True, help="Sum of actually spent hours on the subtask(s)", oldname='children_hours')
    timesheet_ids = fields.One2many('account.analytic.line', 'task_id', 'Timesheets')

    @api.depends('timesheet_ids.unit_amount')
    def _compute_effective_hours(self):
        for task in self:
            task.effective_hours = round(sum(task.timesheet_ids.mapped('unit_amount')), 2)

    @api.depends('effective_hours', 'subtask_effective_hours', 'planned_hours')
    def _compute_progress_hours(self):
        for task in self:
            if (task.planned_hours > 0.0):
                task_total_hours = task.effective_hours + task.subtask_effective_hours
                if task_total_hours > task.planned_hours:
                    task.progress = 100
                else:
                    task.progress = round(100.0 * task_total_hours / task.planned_hours, 2)
            else:
                task.progress = 0.0

    @api.depends('effective_hours', 'subtask_effective_hours', 'planned_hours')
    def _compute_remaining_hours(self):
        for task in self:
            task.remaining_hours = task.planned_hours - task.effective_hours - task.subtask_effective_hours

    @api.depends('effective_hours', 'subtask_effective_hours')
    def _compute_total_hours_spent(self):
        for task in self:
            task.total_hours_spent = task.effective_hours + task.subtask_effective_hours

    @api.depends('child_ids.effective_hours', 'child_ids.subtask_effective_hours')
    def _compute_subtask_effective_hours(self):
        for task in self:
            task.subtask_effective_hours = sum(child_task.effective_hours + child_task.subtask_effective_hours for child_task in task.child_ids)

    # ---------------------------------------------------------
    # ORM
    # ---------------------------------------------------------

    @api.multi
    def write(self, values):
        # a timesheet must have an analytic account (and a project)
        if 'project_id' in values and self and not values.get('project_id'):
                raise UserError(_('This task must be part of a project because they some timesheets are linked to it.'))
        return super(Task, self).write(values)

    @api.model
    def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False):
        """ Set the correct label for `unit_amount`, depending on company UoM """
        result = super(Task, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
        result['arch'] = self.env['account.analytic.line']._apply_timesheet_label(result['arch'])
        return result
Пример #25
0
class pay_manually_wiz(models.TransientModel):

    _name = 'tc.fee.pay.manually.wiz'

    amount = fields.Float(string='Amount')
    journal_id = fields.Many2one('account.journal', string="Payment Method")
    cheque = fields.Boolean(string='Cheque')
    jounral_id_store = fields.Char(string='Jounral Store')
    cheque_start_date = fields.Date('Cheque Start Date')
    cheque_expiry_date = fields.Date('Cheque Expiry Date')
    bank_name = fields.Char('Bank Name')
    party_name = fields.Char('Party Name')
    chk_num = fields.Char('Cheque Number')
    label_change = fields.Boolean(string="Label Change")

    @api.onchange('journal_id')
    def store_jounral(self):
        self.cheque = self.journal_id.is_cheque

    @api.model
    def default_get(self, fields):
        id = self._context.get('active_id')
        res = super(pay_manually_wiz, self).default_get(fields)
        student_tc_rec = self.env['trensfer.certificate'].browse(id)
        res['amount'] = student_tc_rec.credit + student_tc_rec.parent_credit
        return res

    @api.model
    def _get_period(self):
        if self._context is None: context = {}
        if self._context.get('period_id', False):
            return self._context.get('period_id')
        periods = self.env['account.period'].search([])
        return periods and periods[0] or False

    @api.model
    def _get_currency(self):
        if self._context is None: self._context = {}
        journal_pool = self.env['account.journal']
        journal_id = self._context.get('journal_id', False)
        if journal_id:
            if isinstance(journal_id, (list, tuple)):
                # sometimes journal_id is a pair (id, display_name)
                journal_id = journal_id[0]
            journal = journal_pool.browse(journal_id)
            if journal.currency:
                return journal.currency.id
        return self.env['res.users'].browse(
            self._uid).company_id.currency_id.id

    @api.one
    def submit_fee(self):
        print '=======3333333333333333=submit fee========='
        """
        :return:
        """
        active_id = self._context['active_ids']
        print(active_id, '==========active_id  ----amount----->>>',
              self.amount)
        trensfer_certificate_obj = self.env['trensfer.certificate']
        account_payment_obj = self.env['account.payment']
        payment_vals = {}
        for trensfer_certificate_rec in trensfer_certificate_obj.browse(
                active_id):
            if self.amount > 0.00:
                payment_vals = self.get_payment_vals('customer', 'inbound',
                                                     trensfer_certificate_rec)
            if self.amount < 0.00:
                payment_vals = self.get_payment_vals('supplier', 'outbound',
                                                     trensfer_certificate_rec)
                trensfer_certificate_rec.credit -= self.amount
            if payment_vals:
                payment_rec = account_payment_obj.create(payment_vals)
                payment_rec.post()
                print(payment_rec, trensfer_certificate_rec.credit,
                      trensfer_certificate_rec.state,
                      '==============payment_rec===---vals ---> ')
                # print(paymment_vals)

    def get_payment_vals(self, partner_type, payment_type,
                         trensfer_certificate_rec):
        if abs(self.amount) > 0.00:
            payment_vals = {
                # 'period_id': period_id,
                'partner_type': partner_type,
                'partner_id': trensfer_certificate_rec.name.id,
                'journal_id': self.journal_id.id,
                'amount': abs(self.amount),
                'payment_method_id': 1,
                'payment_type': payment_type,
            }
            return payment_vals


#     @api.one
#     def submit_fee(self):
#         print '========submit fee========='
#         """
#         :return:
#         """
#         active_id = self._context['active_ids']
#         trensfer_certificate_obj = self.env['trensfer.certificate']
#         account_voucher_obj = self.env['account.voucher']
#         voucher_line_obj = self.env['account.voucher.line']
#         for trensfer_certificate_rec in trensfer_certificate_obj.browse(active_id):
#             if self.amount > 0.00:
#                 period_rec = self._get_period()
#                 curency_id = self._get_currency()
#                 vouch_sequence = self.env['ir.sequence'].get('voucher.payfort') or '/'
#                 voucher_data = {
#                     'period_id': period_rec.id,
#                     'journal_id': self.journal_id.id,
#                     'account_id': self.journal_id.default_debit_account_id.id,
#                     'partner_id': trensfer_certificate_rec.name.id,
#                     'currency_id': curency_id,
#                     'reference': trensfer_certificate_rec.code,
#                     'amount': self.amount,
#                     'type': 'receipt' or 'payment',
#                     'state': 'draft',
#                     'pay_now': 'pay_later',
#                     'name': trensfer_certificate_rec.code,
#                     'date': time.strftime('%Y-%m-%d'),
#                     'company_id': 1,
#                     'tax_id': False,
#                     'payment_option': 'without_writeoff',
#                     'comment': _('Write-Off'),
#                     'payfort_type': True,
#                     'payfort_link_order_id' : vouch_sequence,
#                     'cheque_start_date':self.cheque_start_date,
#                     'cheque_expiry_date':self.cheque_expiry_date,
#                     'bank_name':self.bank_name,
#                     'cheque':self.cheque,
#                     'party_name' :self.party_name,
#                     'chk_num':self.chk_num,
#                     'voucher_type':'sale' or 'purchase',
#                     }
#                 print voucher_data,'====================voucher_data'
#                 voucher_rec = account_voucher_obj.create(voucher_data)
#                 print voucher_rec,'=====================voucher rec'
#                 date = time.strftime('%Y-%m-%d')
#                 res = voucher_rec.onchange_partner_id(voucher_rec.partner_id.id, self.journal_id.id,
#                                       self.amount,
#                                       voucher_rec.currency_id.id,
#                                       voucher_rec.type, date)
#                 print res,'----------------res-----'
#                 for line_data in res['value']['line_cr_ids']:
#                     print line_data,'====================== line data ============'
#                     voucher_lines = {
#                         'move_line_id': line_data['move_line_id'],
#                         'amount': line_data['amount_unreconciled'] or line_data['amount'],
#                         'name': line_data['name'],
#                         'amount_unreconciled': line_data['amount_unreconciled'],
#                         'type': line_data['type'],
#                         'amount_original': line_data['amount_original'],
#                         'account_id': line_data['account_id'],
#                         'voucher_id': voucher_rec.id,
#                         'reconcile': True
#                     }
#                     print voucher_lines,'====================---------------------voucher_lines pay manualy-'
#                     self.env['account.voucher.line'].sudo().create(voucher_lines)
#
#                 # Validate voucher (Add Journal Entries)
#                 voucher_rec.proforma_voucher()
#                 trensfer_certificate_rec.send_fee_receipt_mail(voucher_rec)
#
#             if self.amount < 0.00:
#                 period_rec = self._get_period()
#                 curency_id = self._get_currency()
#                 vouch_sequence = self.env['ir.sequence'].get('voucher.payfort') or '/'
#                 voucher_data = {
#                     'period_id': period_rec.id,
#                     'journal_id': self.journal_id.id,
#                     'account_id': self.journal_id.default_debit_account_id.id,
#                     'partner_id': trensfer_certificate_rec.name.id,
#                     'currency_id': curency_id,
#                     'reference': trensfer_certificate_rec.name.name,
#                     'amount': self.amount,
#                     'type': 'receipt' or 'payment',
#                     'state': 'draft',
#                     'pay_now': 'pay_later',
#                     'name': '',
#                     'date': time.strftime('%Y-%m-%d'),
#                     'company_id': 1,
#                     'tax_id': False,
#                     'payment_option': 'without_writeoff',
#                     'comment': _('Write-Off'),
#                     'payfort_type': True,
#                     'payfort_link_order_id': vouch_sequence,
#                     'cheque_start_date':self.cheque_start_date,
#                     'cheque_expiry_date':self.cheque_expiry_date,
#                     'bank_name':self.bank_name,
#                     'cheque':self.cheque,
#                     'party_name' :self.party_name,
#                     'chk_num':self.chk_num,
#                     }
#                 print voucher_data,'1111111111111111111111111  voucher idata'
#                 voucher_rec = account_voucher_obj.create(voucher_data)
#                 date = time.strftime('%Y-%m-%d')
#                 res = voucher_rec.onchange_partner_id(voucher_rec.partner_id.id, self.journal_id.id,
#                                       self.amount,
#                                       voucher_rec.currency_id.id,
#                                       voucher_rec.type, date)
#                 print res,'===============5555555555555555555555555  voucher rec'
#                 for line_data in res['value']['line_dr_ids']:
#                     voucher_lines = {
#                         'move_line_id': line_data['move_line_id'],
#                         'amount': line_data['amount_unreconciled'] or line_data['amount'],
#                         'name': line_data['name'],
#                         'amount_unreconciled': line_data['amount_unreconciled'],
#                         'type': line_data['type'],
#                         'amount_original': line_data['amount_original'],
#                         'account_id': line_data['account_id'],
#                         'voucher_id': voucher_rec.id,
#                         'reconcile': True
#                     }
#                     print voucher_lines,'99999999999999999999999 voucher lines'
#                     voucher_line_obj.sudo().create(voucher_lines)
#
#                 # Validate voucher (Add Journal Entries)
#                 voucher_rec.proforma_voucher()
Пример #26
0
class StockQuant(models.Model):
    _name = 'stock.quant'
    _description = 'Quants'
    _rec_name = 'product_id'

    product_id = fields.Many2one('product.product',
                                 'Product',
                                 ondelete='restrict',
                                 readonly=True,
                                 required=True)
    # so user can filter on template in webclient
    product_tmpl_id = fields.Many2one('product.template',
                                      string='Product Template',
                                      related='product_id.product_tmpl_id',
                                      readonly=False)
    product_uom_id = fields.Many2one('uom.uom',
                                     'Unit of Measure',
                                     readonly=True,
                                     related='product_id.uom_id')
    company_id = fields.Many2one(related='location_id.company_id',
                                 string='Company',
                                 store=True,
                                 readonly=True)
    location_id = fields.Many2one('stock.location',
                                  'Location',
                                  auto_join=True,
                                  ondelete='restrict',
                                  readonly=True,
                                  required=True)
    lot_id = fields.Many2one('stock.production.lot',
                             'Lot/Serial Number',
                             ondelete='restrict',
                             readonly=True)
    package_id = fields.Many2one('stock.quant.package',
                                 'Package',
                                 help='The package containing this quant',
                                 readonly=True,
                                 ondelete='restrict')
    owner_id = fields.Many2one('res.partner',
                               'Owner',
                               help='This is the owner of the quant',
                               readonly=True)
    quantity = fields.Float(
        'Quantity',
        help=
        'Quantity of products in this quant, in the default unit of measure of the product',
        readonly=True,
        required=True,
        oldname='qty')
    reserved_quantity = fields.Float(
        'Reserved Quantity',
        default=0.0,
        help=
        'Quantity of reserved products in this quant, in the default unit of measure of the product',
        readonly=True,
        required=True)
    in_date = fields.Datetime('Incoming Date', readonly=True)

    def action_view_stock_moves(self):
        self.ensure_one()
        action = self.env.ref('stock.stock_move_line_action').read()[0]
        action['domain'] = [
            ('product_id', '=', self.product_id.id),
            '|',
            ('location_id', '=', self.location_id.id),
            ('location_dest_id', '=', self.location_id.id),
            ('lot_id', '=', self.lot_id.id),
            '|',
            ('package_id', '=', self.package_id.id),
            ('result_package_id', '=', self.package_id.id),
        ]
        return action

    @api.constrains('product_id')
    def check_product_id(self):
        if any(elem.product_id.type != 'product' for elem in self):
            raise ValidationError(
                _('Quants cannot be created for consumables or services.'))

    @api.constrains('quantity')
    def check_quantity(self):
        for quant in self:
            if float_compare(
                    quant.quantity,
                    1,
                    precision_rounding=quant.product_uom_id.rounding
            ) > 0 and quant.lot_id and quant.product_id.tracking == 'serial':
                raise ValidationError(
                    _('A serial number should only be linked to a single product.'
                      ))

    @api.constrains('location_id')
    def check_location_id(self):
        for quant in self:
            if quant.location_id.usage == 'view':
                raise ValidationError(
                    _('You cannot take products from or deliver products to a location of type "view".'
                      ))

    @api.one
    def _compute_name(self):
        self.name = '%s: %s%s' % (self.lot_id.name or self.product_id.code
                                  or '', self.quantity,
                                  self.product_id.uom_id.name)

    @api.model
    def _get_removal_strategy(self, product_id, location_id):
        if product_id.categ_id.removal_strategy_id:
            return product_id.categ_id.removal_strategy_id.method
        loc = location_id
        while loc:
            if loc.removal_strategy_id:
                return loc.removal_strategy_id.method
            loc = loc.location_id
        return 'fifo'

    @api.model
    def _get_removal_strategy_order(self, removal_strategy):
        if removal_strategy == 'fifo':
            return 'in_date ASC NULLS FIRST, id'
        elif removal_strategy == 'lifo':
            return 'in_date DESC NULLS LAST, id desc'
        raise UserError(
            _('Removal strategy %s not implemented.') % (removal_strategy, ))

    def _gather(self,
                product_id,
                location_id,
                lot_id=None,
                package_id=None,
                owner_id=None,
                strict=False):
        removal_strategy = self._get_removal_strategy(product_id, location_id)
        removal_strategy_order = self._get_removal_strategy_order(
            removal_strategy)
        domain = [
            ('product_id', '=', product_id.id),
        ]
        if not strict:
            if lot_id:
                domain = expression.AND([[('lot_id', '=', lot_id.id)], domain])
            if package_id:
                domain = expression.AND([[('package_id', '=', package_id.id)],
                                         domain])
            if owner_id:
                domain = expression.AND([[('owner_id', '=', owner_id.id)],
                                         domain])
            domain = expression.AND([[('location_id', 'child_of',
                                       location_id.id)], domain])
        else:
            domain = expression.AND([[('lot_id', '=', lot_id and lot_id.id
                                       or False)], domain])
            domain = expression.AND([[
                ('package_id', '=', package_id and package_id.id or False)
            ], domain])
            domain = expression.AND([[
                ('owner_id', '=', owner_id and owner_id.id or False)
            ], domain])
            domain = expression.AND([[('location_id', '=', location_id.id)],
                                     domain])

        # Copy code of _search for special NULLS FIRST/LAST order
        self.sudo(self._uid).check_access_rights('read')
        query = self._where_calc(domain)
        self._apply_ir_rules(query, 'read')
        from_clause, where_clause, where_clause_params = query.get_sql()
        where_str = where_clause and (" WHERE %s" % where_clause) or ''
        query_str = 'SELECT "%s".id FROM ' % self._table + from_clause + where_str + " ORDER BY " + removal_strategy_order
        self._cr.execute(query_str, where_clause_params)
        res = self._cr.fetchall()
        # No uniquify list necessary as auto_join is not applied anyways...
        return self.browse([x[0] for x in res])

    @api.model
    def _get_available_quantity(self,
                                product_id,
                                location_id,
                                lot_id=None,
                                package_id=None,
                                owner_id=None,
                                strict=False,
                                allow_negative=False):
        """ Return the available quantity, i.e. the sum of `quantity` minus the sum of
        `reserved_quantity`, for the set of quants sharing the combination of `product_id,
        location_id` if `strict` is set to False or sharing the *exact same characteristics*
        otherwise.
        This method is called in the following usecases:
            - when a stock move checks its availability
            - when a stock move actually assign
            - when editing a move line, to check if the new value is forced or not
            - when validating a move line with some forced values and have to potentially unlink an
              equivalent move line in another picking
        In the two first usecases, `strict` should be set to `False`, as we don't know what exact
        quants we'll reserve, and the characteristics are meaningless in this context.
        In the last ones, `strict` should be set to `True`, as we work on a specific set of
        characteristics.

        :return: available quantity as a float
        """
        self = self.sudo()
        quants = self._gather(product_id,
                              location_id,
                              lot_id=lot_id,
                              package_id=package_id,
                              owner_id=owner_id,
                              strict=strict)
        rounding = product_id.uom_id.rounding
        if product_id.tracking == 'none':
            available_quantity = sum(quants.mapped('quantity')) - sum(
                quants.mapped('reserved_quantity'))
            if allow_negative:
                return available_quantity
            else:
                return available_quantity if float_compare(
                    available_quantity, 0.0,
                    precision_rounding=rounding) >= 0.0 else 0.0
        else:
            availaible_quantities = {
                lot_id: 0.0
                for lot_id in list(set(quants.mapped('lot_id'))) +
                ['untracked']
            }
            for quant in quants:
                if not quant.lot_id:
                    availaible_quantities[
                        'untracked'] += quant.quantity - quant.reserved_quantity
                else:
                    availaible_quantities[
                        quant.
                        lot_id] += quant.quantity - quant.reserved_quantity
            if allow_negative:
                return sum(availaible_quantities.values())
            else:
                return sum([
                    available_quantity
                    for available_quantity in availaible_quantities.values()
                    if float_compare(
                        available_quantity, 0, precision_rounding=rounding) > 0
                ])

    @api.model
    def _update_available_quantity(self,
                                   product_id,
                                   location_id,
                                   quantity,
                                   lot_id=None,
                                   package_id=None,
                                   owner_id=None,
                                   in_date=None):
        """ Increase or decrease `reserved_quantity` of a set of quants for a given set of
        product_id/location_id/lot_id/package_id/owner_id.

        :param product_id:
        :param location_id:
        :param quantity:
        :param lot_id:
        :param package_id:
        :param owner_id:
        :param datetime in_date: Should only be passed when calls to this method are done in
                                 order to move a quant. When creating a tracked quant, the
                                 current datetime will be used.
        :return: tuple (available_quantity, in_date as a datetime)
        """
        self = self.sudo()
        quants = self._gather(product_id,
                              location_id,
                              lot_id=lot_id,
                              package_id=package_id,
                              owner_id=owner_id,
                              strict=True)

        incoming_dates = [d for d in quants.mapped('in_date') if d]
        incoming_dates = [
            fields.Datetime.from_string(incoming_date)
            for incoming_date in incoming_dates
        ]
        if in_date:
            incoming_dates += [in_date]
        # If multiple incoming dates are available for a given lot_id/package_id/owner_id, we
        # consider only the oldest one as being relevant.
        if incoming_dates:
            in_date = fields.Datetime.to_string(min(incoming_dates))
        else:
            in_date = fields.Datetime.now()

        for quant in quants:
            try:
                with self._cr.savepoint():
                    self._cr.execute(
                        "SELECT 1 FROM stock_quant WHERE id = %s FOR UPDATE NOWAIT",
                        [quant.id],
                        log_exceptions=False)
                    quant.write({
                        'quantity': quant.quantity + quantity,
                        'in_date': in_date,
                    })
                    break
            except OperationalError as e:
                if e.pgcode == '55P03':  # could not obtain the lock
                    continue
                else:
                    raise
        else:
            self.create({
                'product_id': product_id.id,
                'location_id': location_id.id,
                'quantity': quantity,
                'lot_id': lot_id and lot_id.id,
                'package_id': package_id and package_id.id,
                'owner_id': owner_id and owner_id.id,
                'in_date': in_date,
            })
        return self._get_available_quantity(
            product_id,
            location_id,
            lot_id=lot_id,
            package_id=package_id,
            owner_id=owner_id,
            strict=False,
            allow_negative=True), fields.Datetime.from_string(in_date)

    @api.model
    def _update_reserved_quantity(self,
                                  product_id,
                                  location_id,
                                  quantity,
                                  lot_id=None,
                                  package_id=None,
                                  owner_id=None,
                                  strict=False):
        """ Increase the reserved quantity, i.e. increase `reserved_quantity` for the set of quants
        sharing the combination of `product_id, location_id` if `strict` is set to False or sharing
        the *exact same characteristics* otherwise. Typically, this method is called when reserving
        a move or updating a reserved move line. When reserving a chained move, the strict flag
        should be enabled (to reserve exactly what was brought). When the move is MTS,it could take
        anything from the stock, so we disable the flag. When editing a move line, we naturally
        enable the flag, to reflect the reservation according to the edition.

        :return: a list of tuples (quant, quantity_reserved) showing on which quant the reservation
            was done and how much the system was able to reserve on it
        """
        self = self.sudo()
        rounding = product_id.uom_id.rounding
        quants = self._gather(product_id,
                              location_id,
                              lot_id=lot_id,
                              package_id=package_id,
                              owner_id=owner_id,
                              strict=strict)
        reserved_quants = []

        if float_compare(quantity, 0, precision_rounding=rounding) > 0:
            # if we want to reserve
            available_quantity = self._get_available_quantity(
                product_id,
                location_id,
                lot_id=lot_id,
                package_id=package_id,
                owner_id=owner_id,
                strict=strict)
            if float_compare(quantity,
                             available_quantity,
                             precision_rounding=rounding) > 0:
                raise UserError(
                    _('It is not possible to reserve more products of %s than you have in stock.'
                      ) % product_id.display_name)
        elif float_compare(quantity, 0, precision_rounding=rounding) < 0:
            # if we want to unreserve
            available_quantity = sum(quants.mapped('reserved_quantity'))
            if float_compare(abs(quantity),
                             available_quantity,
                             precision_rounding=rounding) > 0:
                raise UserError(
                    _('It is not possible to unreserve more products of %s than you have in stock.'
                      ) % product_id.display_name)
        else:
            return reserved_quants

        for quant in quants:
            if float_compare(quantity, 0, precision_rounding=rounding) > 0:
                max_quantity_on_quant = quant.quantity - quant.reserved_quantity
                if float_compare(max_quantity_on_quant,
                                 0,
                                 precision_rounding=rounding) <= 0:
                    continue
                max_quantity_on_quant = min(max_quantity_on_quant, quantity)
                quant.reserved_quantity += max_quantity_on_quant
                reserved_quants.append((quant, max_quantity_on_quant))
                quantity -= max_quantity_on_quant
                available_quantity -= max_quantity_on_quant
            else:
                max_quantity_on_quant = min(quant.reserved_quantity,
                                            abs(quantity))
                quant.reserved_quantity -= max_quantity_on_quant
                reserved_quants.append((quant, -max_quantity_on_quant))
                quantity += max_quantity_on_quant
                available_quantity += max_quantity_on_quant

            if float_is_zero(
                    quantity, precision_rounding=rounding) or float_is_zero(
                        available_quantity, precision_rounding=rounding):
                break
        return reserved_quants

    @api.model
    def _unlink_zero_quants(self):
        """ _update_available_quantity may leave quants with no
        quantity and no reserved_quantity. It used to directly unlink
        these zero quants but this proved to hurt the performance as
        this method is often called in batch and each unlink invalidate
        the cache. We defer the calls to unlink in this method.
        """
        precision_digits = max(
            6,
            self.env.ref('product.decimal_product_uom').digits * 2)
        # Use a select instead of ORM search for UoM robustness.
        query = """SELECT id FROM stock_quant WHERE round(quantity::numeric, %s) = 0 AND round(reserved_quantity::numeric, %s) = 0;"""
        params = (precision_digits, precision_digits)
        self.env.cr.execute(query, params)
        quant_ids = self.env['stock.quant'].browse(
            [quant['id'] for quant in self.env.cr.dictfetchall()])
        quant_ids.sudo().unlink()

    @api.model
    def _merge_quants(self):
        """ In a situation where one transaction is updating a quant via
        `_update_available_quantity` and another concurrent one calls this function with the same
        argument, we’ll create a new quant in order for these transactions to not rollback. This
        method will find and deduplicate these quants.
        """
        query = """WITH
                        dupes AS (
                            SELECT min(id) as to_update_quant_id,
                                (array_agg(id ORDER BY id))[2:array_length(array_agg(id), 1)] as to_delete_quant_ids,
                                SUM(reserved_quantity) as reserved_quantity,
                                SUM(quantity) as quantity
                            FROM stock_quant
                            GROUP BY product_id, company_id, location_id, lot_id, package_id, owner_id, in_date
                            HAVING count(id) > 1
                        ),
                        _up AS (
                            UPDATE stock_quant q
                                SET quantity = d.quantity,
                                    reserved_quantity = d.reserved_quantity
                            FROM dupes d
                            WHERE d.to_update_quant_id = q.id
                        )
                   DELETE FROM stock_quant WHERE id in (SELECT unnest(to_delete_quant_ids) from dupes)
        """
        try:
            with self.env.cr.savepoint():
                self.env.cr.execute(query)
        except Error as e:
            _logger.info('an error occured while merging quants: %s',
                         e.pgerror)
Пример #27
0
class StockMove(models.Model):
    _inherit = "stock.move"

    to_refund = fields.Boolean(
        string="To Refund (update SO/PO)",
        copy=False,
        help=
        'Trigger a decrease of the delivered/received quantity in the associated Sale Order/Purchase Order'
    )
    value = fields.Float(copy=False)
    remaining_qty = fields.Float(copy=False)
    remaining_value = fields.Float(copy=False)
    account_move_ids = fields.One2many('account.move', 'stock_move_id')

    @api.multi
    def action_get_account_moves(self):
        self.ensure_one()
        action_ref = self.env.ref('account.action_move_journal_line')
        if not action_ref:
            return False
        action_data = action_ref.read()[0]
        action_data['domain'] = [('id', 'in', self.account_move_ids.ids)]
        return action_data

    def _get_price_unit(self):
        """ Returns the unit price to store on the quant """
        return not self.company_id.currency_id.is_zero(
            self.price_unit
        ) and self.price_unit or self.product_id.standard_price

    @api.model
    def _get_in_base_domain(self, company_id=False):
        # Domain:
        # - state is done
        # - coming from a location without company, or an inventory location within the same company
        # - going to a location within the same company
        domain = [
            ('state', '=', 'done'),
            '&',
            '|',
            ('location_id.company_id', '=', False),
            '&',
            ('location_id.usage', 'in', ['inventory', 'production']),
            ('location_id.company_id', '=', company_id
             or self.env.user.company_id.id),
            ('location_dest_id.company_id', '=', company_id
             or self.env.user.company_id.id),
        ]
        return domain

    @api.model
    def _get_all_base_domain(self, company_id=False):
        # Domain:
        # - state is done
        # Then, either 'in' or 'out' moves.
        # 'in' moves:
        # - coming from a location without company, or an inventory location within the same company
        # - going to a location within the same company
        # 'out' moves:
        # - coming from to a location within the same company
        # - going to a location without company, or an inventory location within the same company
        domain = [
            ('state', '=', 'done'),
            '|',
            '&',
            '|',
            ('location_id.company_id', '=', False),
            '&',
            ('location_id.usage', 'in', ['inventory', 'production']),
            ('location_id.company_id', '=', company_id
             or self.env.user.company_id.id),
            ('location_dest_id.company_id', '=', company_id
             or self.env.user.company_id.id),
            '&',
            ('location_id.company_id', '=', company_id
             or self.env.user.company_id.id),
            '|',
            ('location_dest_id.company_id', '=', False),
            '&',
            ('location_dest_id.usage', 'in', ['inventory', 'production']),
            ('location_dest_id.company_id', '=', company_id
             or self.env.user.company_id.id),
        ]
        return domain

    def _get_in_domain(self):
        return [('product_id', '=', self.product_id.id)
                ] + self._get_in_base_domain(company_id=self.company_id.id)

    def _get_all_domain(self):
        return [('product_id', '=', self.product_id.id)
                ] + self._get_all_base_domain(company_id=self.company_id.id)

    def _is_in(self):
        """ Check if the move should be considered as entering the company so that the cost method
        will be able to apply the correct logic.

        :return: True if the move is entering the company else False
        """
        for move_line in self.move_line_ids.filtered(
                lambda ml: not ml.owner_id):
            if not move_line.location_id._should_be_valued(
            ) and move_line.location_dest_id._should_be_valued():
                return True
        return False

    def _is_out(self):
        """ Check if the move should be considered as leaving the company so that the cost method
        will be able to apply the correct logic.

        :return: True if the move is leaving the company else False
        """
        for move_line in self.move_line_ids.filtered(
                lambda ml: not ml.owner_id):
            if move_line.location_id._should_be_valued(
            ) and not move_line.location_dest_id._should_be_valued():
                return True
        return False

    def _is_dropshipped(self):
        """ Check if the move should be considered as a dropshipping move so that the cost method
        will be able to apply the correct logic.

        :return: True if the move is a dropshipping one else False
        """
        return self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'customer'

    def _is_dropshipped_returned(self):
        """ Check if the move should be considered as a returned dropshipping move so that the cost
        method will be able to apply the correct logic.

        :return: True if the move is a returned dropshipping one else False
        """
        return self.location_id.usage == 'customer' and self.location_dest_id.usage == 'supplier'

    @api.model
    def _run_fifo(self, move, quantity=None):
        """ Value `move` according to the FIFO rule, meaning we consume the
        oldest receipt first. Candidates receipts are marked consumed or free
        thanks to their `remaining_qty` and `remaining_value` fields.
        By definition, `move` should be an outgoing stock move.

        :param quantity: quantity to value instead of `move.product_qty`
        :returns: valued amount in absolute
        """
        move.ensure_one()

        # Deal with possible move lines that do not impact the valuation.
        valued_move_lines = move.move_line_ids.filtered(
            lambda ml: ml.location_id._should_be_valued() and not ml.
            location_dest_id._should_be_valued() and not ml.owner_id)
        valued_quantity = 0
        for valued_move_line in valued_move_lines:
            valued_quantity += valued_move_line.product_uom_id._compute_quantity(
                valued_move_line.qty_done, move.product_id.uom_id)

        # Find back incoming stock moves (called candidates here) to value this move.
        qty_to_take_on_candidates = quantity or valued_quantity
        candidates = move.product_id._get_fifo_candidates_in_move_with_company(
            move.company_id.id)
        new_standard_price = 0
        tmp_value = 0  # to accumulate the value taken on the candidates
        for candidate in candidates:
            new_standard_price = candidate.price_unit
            if candidate.remaining_qty <= qty_to_take_on_candidates:
                qty_taken_on_candidate = candidate.remaining_qty
            else:
                qty_taken_on_candidate = qty_to_take_on_candidates

            # As applying a landed cost do not update the unit price, naivelly doing
            # something like qty_taken_on_candidate * candidate.price_unit won't make
            # the additional value brought by the landed cost go away.
            candidate_price_unit = candidate.remaining_value / candidate.remaining_qty
            value_taken_on_candidate = qty_taken_on_candidate * candidate_price_unit
            candidate_vals = {
                'remaining_qty':
                candidate.remaining_qty - qty_taken_on_candidate,
                'remaining_value':
                candidate.remaining_value - value_taken_on_candidate,
            }
            candidate.write(candidate_vals)

            qty_to_take_on_candidates -= qty_taken_on_candidate
            tmp_value += value_taken_on_candidate
            if qty_to_take_on_candidates == 0:
                break

        # Update the standard price with the price of the last used candidate, if any.
        if new_standard_price and move.product_id.cost_method == 'fifo':
            move.product_id.sudo().with_context(force_company=move.company_id.id) \
                .standard_price = new_standard_price

        # If there's still quantity to value but we're out of candidates, we fall in the
        # negative stock use case. We chose to value the out move at the price of the
        # last out and a correction entry will be made once `_fifo_vacuum` is called.
        if qty_to_take_on_candidates == 0:
            move.write({
                'value':
                -tmp_value if not quantity else move.value
                or -tmp_value,  # outgoing move are valued negatively
                'price_unit':
                -tmp_value / (move.product_qty or quantity),
            })
        elif qty_to_take_on_candidates > 0:
            last_fifo_price = new_standard_price or move.product_id.standard_price
            negative_stock_value = last_fifo_price * -qty_to_take_on_candidates
            tmp_value += abs(negative_stock_value)
            vals = {
                'remaining_qty':
                move.remaining_qty + -qty_to_take_on_candidates,
                'remaining_value': move.remaining_value + negative_stock_value,
                'value': -tmp_value,
                'price_unit': -1 * last_fifo_price,
            }
            move.write(vals)
        return tmp_value

    def _run_valuation(self, quantity=None):
        self.ensure_one()
        value_to_return = 0
        if self._is_in():
            valued_move_lines = self.move_line_ids.filtered(
                lambda ml: not ml.location_id._should_be_valued() and ml.
                location_dest_id._should_be_valued() and not ml.owner_id)
            valued_quantity = 0
            for valued_move_line in valued_move_lines:
                valued_quantity += valued_move_line.product_uom_id._compute_quantity(
                    valued_move_line.qty_done, self.product_id.uom_id)

            # Note: we always compute the fifo `remaining_value` and `remaining_qty` fields no
            # matter which cost method is set, to ease the switching of cost method.
            vals = {}
            price_unit = self._get_price_unit()
            value = price_unit * (quantity or valued_quantity)
            value_to_return = value if quantity is None or not self.value else self.value
            vals = {
                'price_unit':
                price_unit,
                'value':
                value_to_return,
                'remaining_value':
                value if quantity is None else self.remaining_value + value,
            }
            vals[
                'remaining_qty'] = valued_quantity if quantity is None else self.remaining_qty + quantity

            if self.product_id.cost_method == 'standard':
                value = self.product_id.standard_price * (quantity
                                                          or valued_quantity)
                value_to_return = value if quantity is None or not self.value else self.value
                vals.update({
                    'price_unit': self.product_id.standard_price,
                    'value': value_to_return,
                })
            self.write(vals)
        elif self._is_out():
            valued_move_lines = self.move_line_ids.filtered(
                lambda ml: ml.location_id._should_be_valued() and not ml.
                location_dest_id._should_be_valued() and not ml.owner_id)
            valued_quantity = 0
            for valued_move_line in valued_move_lines:
                valued_quantity += valued_move_line.product_uom_id._compute_quantity(
                    valued_move_line.qty_done, self.product_id.uom_id)
            self.env['stock.move']._run_fifo(self, quantity=quantity)
            if self.product_id.cost_method in ['standard', 'average']:
                curr_rounding = self.company_id.currency_id.rounding
                value = -float_round(
                    self.product_id.standard_price *
                    (valued_quantity if quantity is None else quantity),
                    precision_rounding=curr_rounding)
                value_to_return = value if quantity is None else self.value + value
                self.write({
                    'value': value_to_return,
                    'price_unit': value / valued_quantity,
                })
        elif self._is_dropshipped() or self._is_dropshipped_returned():
            curr_rounding = self.company_id.currency_id.rounding
            if self.product_id.cost_method in ['fifo']:
                price_unit = self._get_price_unit()
                # see test_dropship_fifo_perpetual_anglosaxon_ordered
                self.product_id.standard_price = price_unit
            else:
                price_unit = self.product_id.standard_price
            value = float_round(self.product_qty * price_unit,
                                precision_rounding=curr_rounding)
            value_to_return = value if self._is_dropshipped() else -value
            # In move have a positive value, out move have a negative value, let's arbitrary say
            # dropship are positive.
            self.write({
                'value':
                value_to_return,
                'price_unit':
                price_unit if self._is_dropshipped() else -price_unit,
            })
        return value_to_return

    def _action_done(self):
        self.product_price_update_before_done()
        res = super(StockMove, self)._action_done()
        for move in res:
            # Apply restrictions on the stock move to be able to make
            # consistent accounting entries.
            if move._is_in() and move._is_out():
                raise UserError(
                    _("The move lines are not in a consistent state: some are entering and other are leaving the company."
                      ))
            company_src = move.mapped('move_line_ids.location_id.company_id')
            company_dst = move.mapped(
                'move_line_ids.location_dest_id.company_id')
            try:
                if company_src:
                    company_src.ensure_one()
                if company_dst:
                    company_dst.ensure_one()
            except ValueError:
                raise UserError(
                    _("The move lines are not in a consistent states: they do not share the same origin or destination company."
                      ))
            if company_src and company_dst and company_src.id != company_dst.id:
                raise UserError(
                    _("The move lines are not in a consistent states: they are doing an intercompany in a single step while they should go through the intercompany transit location."
                      ))
            move._run_valuation()
        for move in res.filtered(
                lambda m: m.product_id.valuation == 'real_time' and
            (m._is_in() or m._is_out() or m._is_dropshipped() or m.
             _is_dropshipped_returned())):
            move._account_entry_move()
        return res

    @api.multi
    def product_price_update_before_done(self, forced_qty=None):
        tmpl_dict = defaultdict(lambda: 0.0)
        # adapt standard price on incomming moves if the product cost_method is 'average'
        std_price_update = {}
        for move in self.filtered(lambda move: move._is_in() and move.
                                  product_id.cost_method == 'average'):
            product_tot_qty_available = move.product_id.qty_available + tmpl_dict[
                move.product_id.id]
            rounding = move.product_id.uom_id.rounding

            qty_done = move.product_uom._compute_quantity(
                move.quantity_done, move.product_id.uom_id)
            if float_is_zero(product_tot_qty_available,
                             precision_rounding=rounding):
                new_std_price = move._get_price_unit()
            elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding) or \
                    float_is_zero(product_tot_qty_available + qty_done, precision_rounding=rounding):
                new_std_price = move._get_price_unit()
            else:
                # Get the standard price
                amount_unit = std_price_update.get(
                    (move.company_id.id,
                     move.product_id.id)) or move.product_id.standard_price
                qty = forced_qty or qty_done
                new_std_price = ((amount_unit * product_tot_qty_available) +
                                 (move._get_price_unit() * qty)) / (
                                     product_tot_qty_available + qty)

            tmpl_dict[move.product_id.id] += qty_done
            # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
            move.product_id.with_context(
                force_company=move.company_id.id).sudo().write(
                    {'standard_price': new_std_price})
            std_price_update[move.company_id.id,
                             move.product_id.id] = new_std_price

    @api.model
    def _fifo_vacuum(self):
        """ Every moves that need to be fixed are identifiable by having a negative `remaining_qty`.
        """
        for move in self.filtered(
                lambda m: (m._is_in() or m._is_out()) and m.remaining_qty < 0):
            domain = [('remaining_qty', '>', 0), '|', ('date', '>', move.date),
                      '&', ('date', '=', move.date), ('id', '>', move.id)]
            domain += move._get_in_domain()
            candidates = self.search(domain, order='date, id')
            if not candidates:
                continue
            qty_to_take_on_candidates = abs(move.remaining_qty)
            qty_taken_on_candidates = 0
            tmp_value = 0
            for candidate in candidates:
                if candidate.remaining_qty <= qty_to_take_on_candidates:
                    qty_taken_on_candidate = candidate.remaining_qty
                else:
                    qty_taken_on_candidate = qty_to_take_on_candidates
                qty_taken_on_candidates += qty_taken_on_candidate

                value_taken_on_candidate = qty_taken_on_candidate * candidate.price_unit
                candidate_vals = {
                    'remaining_qty':
                    candidate.remaining_qty - qty_taken_on_candidate,
                    'remaining_value':
                    candidate.remaining_value - value_taken_on_candidate,
                }
                candidate.write(candidate_vals)

                qty_to_take_on_candidates -= qty_taken_on_candidate
                tmp_value += value_taken_on_candidate
                if qty_to_take_on_candidates == 0:
                    break

            # When working with `price_unit`, beware that out move are negative.
            move_price_unit = move.price_unit if move._is_out(
            ) else -1 * move.price_unit
            # Get the estimated value we will correct.
            remaining_value_before_vacuum = qty_taken_on_candidates * move_price_unit
            new_remaining_qty = move.remaining_qty + qty_taken_on_candidates
            new_remaining_value = new_remaining_qty * abs(move.price_unit)

            corrected_value = remaining_value_before_vacuum + tmp_value
            move.write({
                'remaining_value': new_remaining_value,
                'remaining_qty': new_remaining_qty,
                'value': move.value - corrected_value,
            })

            if move.product_id.valuation == 'real_time':
                # If `corrected_value` is 0, absolutely do *not* call `_account_entry_move`. We
                # force the amount in the context, but in the case it is 0 it'll create an entry
                # for the entire cost of the move. This case happens when the candidates moves
                # entirely compensate the problematic move.
                if move.company_id.currency_id.is_zero(corrected_value):
                    continue

                if move._is_in():
                    # If we just compensated an IN move that has a negative remaining
                    # quantity, it means the move has returned more items than it received.
                    # The correction should behave as a return too. As `_account_entry_move`
                    # will post the natural values for an IN move (credit IN account, debit
                    # OUT one), we inverse the sign to create the correct entries.
                    move.with_context(force_valuation_amount=-corrected_value,
                                      forced_quantity=0)._account_entry_move()
                else:
                    move.with_context(force_valuation_amount=corrected_value,
                                      forced_quantity=0)._account_entry_move()

    @api.model
    def _run_fifo_vacuum(self):
        # Call `_fifo_vacuum` on concerned moves
        fifo_valued_products = self.env['product.product']
        fifo_valued_products |= self.env['product.template'].search([
            ('property_cost_method', '=', 'fifo')
        ]).mapped('product_variant_ids')
        fifo_valued_categories = self.env['product.category'].search([
            ('property_cost_method', '=', 'fifo')
        ])
        fifo_valued_products |= self.env['product.product'].search([
            ('categ_id', 'child_of', fifo_valued_categories.ids)
        ])
        moves_to_vacuum = self.search([('product_id', 'in',
                                        fifo_valued_products.ids),
                                       ('remaining_qty', '<', 0)] +
                                      self._get_all_base_domain())
        moves_to_vacuum._fifo_vacuum()

    @api.multi
    def _get_accounting_data_for_valuation(self):
        """ Return the accounts and journal to use to post Journal Entries for
        the real-time valuation of the quant. """
        self.ensure_one()
        accounts_data = self.product_id.product_tmpl_id.get_product_accounts()

        if self.location_id.valuation_out_account_id:
            acc_src = self.location_id.valuation_out_account_id.id
        else:
            acc_src = accounts_data['stock_input'].id

        if self.location_dest_id.valuation_in_account_id:
            acc_dest = self.location_dest_id.valuation_in_account_id.id
        else:
            acc_dest = accounts_data['stock_output'].id

        acc_valuation = accounts_data.get('stock_valuation', False)
        if acc_valuation:
            acc_valuation = acc_valuation.id
        if not accounts_data.get('stock_journal', False):
            raise UserError(
                _('You don\'t have any stock journal defined on your product category, check if you have installed a chart of accounts.'
                  ))
        if not acc_src:
            raise UserError(
                _('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.'
                  ) % (self.product_id.display_name))
        if not acc_dest:
            raise UserError(
                _('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.'
                  ) % (self.product_id.display_name))
        if not acc_valuation:
            raise UserError(
                _('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'
                  ))
        journal_id = accounts_data['stock_journal'].id
        return journal_id, acc_src, acc_dest, acc_valuation

    def _prepare_account_move_line(self, qty, cost, credit_account_id,
                                   debit_account_id):
        """
        Generate the account.move.line values to post to track the stock valuation difference due to the
        processing of the given quant.
        """
        self.ensure_one()

        if self._context.get('force_valuation_amount'):
            valuation_amount = self._context.get('force_valuation_amount')
        else:
            valuation_amount = cost

        # the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
        # the company currency... so we need to use round() before creating the accounting entries.
        debit_value = self.company_id.currency_id.round(valuation_amount)

        # check that all data is correct
        if self.company_id.currency_id.is_zero(
                debit_value) and not self.env['ir.config_parameter'].sudo(
                ).get_param('stock_account.allow_zero_cost'):
            raise UserError(
                _("The cost of %s is currently equal to 0. Change the cost or the configuration of your product to avoid an incorrect valuation."
                  ) % (self.product_id.display_name, ))
        credit_value = debit_value

        valuation_partner_id = self._get_partner_id_for_valuation_lines()
        res = [(0, 0, line_vals)
               for line_vals in self._generate_valuation_lines_data(
                   valuation_partner_id, qty, debit_value, credit_value,
                   debit_account_id, credit_account_id).values()]

        return res

    def _generate_valuation_lines_data(self, partner_id, qty, debit_value,
                                       credit_value, debit_account_id,
                                       credit_account_id):
        # This method returns a dictonary to provide an easy extension hook to modify the valuation lines (see purchase for an example)
        self.ensure_one()

        if self._context.get('forced_ref'):
            ref = self._context['forced_ref']
        else:
            ref = self.picking_id.name

        debit_line_vals = {
            'name': self.name,
            'product_id': self.product_id.id,
            'quantity': qty,
            'product_uom_id': self.product_id.uom_id.id,
            'ref': ref,
            'partner_id': partner_id,
            'debit': debit_value if debit_value > 0 else 0,
            'credit': -debit_value if debit_value < 0 else 0,
            'account_id': debit_account_id,
        }

        credit_line_vals = {
            'name': self.name,
            'product_id': self.product_id.id,
            'quantity': qty,
            'product_uom_id': self.product_id.uom_id.id,
            'ref': ref,
            'partner_id': partner_id,
            'credit': credit_value if credit_value > 0 else 0,
            'debit': -credit_value if credit_value < 0 else 0,
            'account_id': credit_account_id,
        }

        rslt = {
            'credit_line_vals': credit_line_vals,
            'debit_line_vals': debit_line_vals
        }
        if credit_value != debit_value:
            # for supplier returns of product in average costing method, in anglo saxon mode
            diff_amount = debit_value - credit_value
            price_diff_account = self.product_id.property_account_creditor_price_difference

            if not price_diff_account:
                price_diff_account = self.product_id.categ_id.property_account_creditor_price_difference_categ
            if not price_diff_account:
                raise UserError(
                    _('Configuration error. Please configure the price difference account on the product or its category to process this operation.'
                      ))

            rslt['price_diff_line_vals'] = {
                'name': self.name,
                'product_id': self.product_id.id,
                'quantity': qty,
                'product_uom_id': self.product_id.uom_id.id,
                'ref': ref,
                'partner_id': partner_id,
                'credit': diff_amount > 0 and diff_amount or 0,
                'debit': diff_amount < 0 and -diff_amount or 0,
                'account_id': price_diff_account.id,
            }
        return rslt

    def _get_partner_id_for_valuation_lines(self):
        return (self.picking_id.partner_id
                and self.env['res.partner']._find_accounting_partner(
                    self.picking_id.partner_id).id) or False

    def _prepare_move_split_vals(self, uom_qty):
        vals = super(StockMove, self)._prepare_move_split_vals(uom_qty)
        vals['to_refund'] = self.to_refund
        return vals

    def _create_account_move_line(self, credit_account_id, debit_account_id,
                                  journal_id):
        self.ensure_one()
        AccountMove = self.env['account.move']
        quantity = self.env.context.get('forced_quantity', self.product_qty)
        quantity = quantity if self._is_in() else -1 * quantity

        # Make an informative `ref` on the created account move to differentiate between classic
        # movements, vacuum and edition of past moves.
        ref = self.picking_id.name
        if self.env.context.get('force_valuation_amount'):
            if self.env.context.get('forced_quantity') == 0:
                ref = 'Revaluation of %s (negative inventory)' % ref
            elif self.env.context.get('forced_quantity') is not None:
                ref = 'Correction of %s (modification of past move)' % ref

        move_lines = self.with_context(
            forced_ref=ref)._prepare_account_move_line(quantity,
                                                       abs(self.value),
                                                       credit_account_id,
                                                       debit_account_id)
        if move_lines:
            date = self._context.get('force_period_date',
                                     fields.Date.context_today(self))
            new_account_move = AccountMove.sudo().create({
                'journal_id':
                journal_id,
                'line_ids':
                move_lines,
                'date':
                date,
                'ref':
                ref,
                'stock_move_id':
                self.id,
            })
            new_account_move.post()

    def _account_entry_move(self):
        """ Accounting Valuation Entries """
        self.ensure_one()
        if self.product_id.type != 'product':
            # no stock valuation for consumable products
            return False
        if self.restrict_partner_id:
            # if the move isn't owned by the company, we don't make any valuation
            return False

        location_from = self.location_id
        location_to = self.location_dest_id
        company_from = self._is_out() and self.mapped(
            'move_line_ids.location_id.company_id') or False
        company_to = self._is_in() and self.mapped(
            'move_line_ids.location_dest_id.company_id') or False

        # Create Journal Entry for products arriving in the company; in case of routes making the link between several
        # warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries
        if self._is_in():
            journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(
            )
            if location_from and location_from.usage == 'customer':  # goods returned from customer
                self.with_context(
                    force_company=company_to.id)._create_account_move_line(
                        acc_dest, acc_valuation, journal_id)
            else:
                self.with_context(
                    force_company=company_to.id)._create_account_move_line(
                        acc_src, acc_valuation, journal_id)

        # Create Journal Entry for products leaving the company
        if self._is_out():
            journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(
            )
            if location_to and location_to.usage == 'supplier':  # goods returned to supplier
                self.with_context(
                    force_company=company_from.id)._create_account_move_line(
                        acc_valuation, acc_src, journal_id)
            else:
                self.with_context(
                    force_company=company_from.id)._create_account_move_line(
                        acc_valuation, acc_dest, journal_id)

        if self.company_id.anglo_saxon_accounting:
            # Creates an account entry from stock_input to stock_output on a dropship move. https://github.com/eagle/eagle/issues/12687
            journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(
            )
            if self._is_dropshipped():
                self.with_context(force_company=self.company_id.id
                                  )._create_account_move_line(
                                      acc_src, acc_dest, journal_id)
            elif self._is_dropshipped_returned():
                self.with_context(force_company=self.company_id.id
                                  )._create_account_move_line(
                                      acc_dest, acc_src, journal_id)

        if self.company_id.anglo_saxon_accounting:
            #eventually reconcile together the invoice and valuation accounting entries on the stock interim accounts
            self._get_related_invoices()._anglo_saxon_reconcile_valuation(
                product=self.product_id)

    def _get_related_invoices(
            self):  # To be overridden in purchase and sale_stock
        """ This method is overrided in both purchase and sale_stock modules to adapt
        to the way they mix stock moves with invoices.
        """
        return self.env['account.invoice']
Пример #28
0
class FleetVehicle(models.Model):
    _inherit = 'fleet.vehicle'

    co2_fee = fields.Float(compute='_compute_co2_fee',
                           string="CO2 Fee",
                           store=True)
    total_depreciated_cost = fields.Float(
        compute='_compute_total_depreciated_cost',
        store=True,
        string="Total Cost (Depreciated)",
        track_visibility="onchange",
        help="This includes all the depreciated costs and the CO2 fee")
    total_cost = fields.Float(
        compute='_compute_total_cost',
        string="Total Cost",
        help="This include all the costs and the CO2 fee")
    fuel_type = fields.Selection(required=True, default='diesel')
    atn = fields.Float(compute='_compute_car_atn', string="ATN")
    acquisition_date = fields.Date(required=True)

    @api.depends('co2_fee', 'log_contracts', 'log_contracts.state',
                 'log_contracts.recurring_cost_amount_depreciated')
    def _compute_total_depreciated_cost(self):
        for car in self:
            car.total_depreciated_cost = car.co2_fee + \
                sum(car.log_contracts.filtered(
                    lambda contract: contract.state == 'open'
                ).mapped('recurring_cost_amount_depreciated'))

    @api.depends('co2_fee', 'log_contracts', 'log_contracts.state',
                 'log_contracts.cost_generated')
    def _compute_total_cost(self):
        for car in self:
            car.total_cost = car.co2_fee
            contracts = car.log_contracts.filtered(
                lambda contract: contract.state == 'open' and contract.
                cost_frequency != 'no')
            for contract in contracts:
                if contract.cost_frequency == "daily":
                    car.total_cost += contract.cost_generated * 30.0
                elif contract.cost_frequency == "weekly":
                    car.total_cost += contract.cost_generated * 4.0
                elif contract.cost_frequency == "monthly":
                    car.total_cost += contract.cost_generated
                elif contract.cost_frequency == "yearly":
                    car.total_cost += contract.cost_generated / 12.0

    def _get_co2_fee(self, co2, fuel_type):
        fuel_coefficient = {
            'diesel': 600,
            'gasoline': 768,
            'lpg': 990,
            'electric': 0,
            'hybrid': 600
        }
        co2_fee = 0
        if fuel_type and fuel_type != 'electric':
            if not co2:
                co2 = 165 if fuel_type in ['diesel', 'hybrid'] else 182
            co2_fee = (((co2 * 9.0) - fuel_coefficient.get(fuel_type, 0)) *
                       144.97 / 114.08) / 12.0
        return max(co2_fee, 26.47)

    @api.depends('co2', 'fuel_type')
    def _compute_co2_fee(self):
        for car in self:
            car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type)

    @api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2')
    def _compute_car_atn(self):
        for car in self:
            car.atn = car._get_car_atn(car.acquisition_date, car.car_value,
                                       car.fuel_type, car.co2)

    @api.depends('model_id', 'license_plate', 'log_contracts',
                 'acquisition_date', 'co2_fee', 'log_contracts',
                 'log_contracts.state',
                 'log_contracts.recurring_cost_amount_depreciated')
    def _compute_vehicle_name(self):
        super(FleetVehicle, self)._compute_vehicle_name()
        for vehicle in self:
            acquisition_date = vehicle._get_acquisition_date()
            vehicle.name += u" \u2022 " + str(
                round(vehicle.total_depreciated_cost,
                      2)) + u" \u2022 " + acquisition_date

    @api.model
    def create(self, vals):
        res = super(FleetVehicle, self).create(vals)
        if not res.log_contracts:
            self.env['fleet.vehicle.log.contract'].create({
                'vehicle_id':
                res.id,
                'recurring_cost_amount_depreciated':
                res.model_id.default_recurring_cost_amount_depreciated,
                'purchaser_id':
                res.driver_id.id,
            })
        return res

    def _get_acquisition_date(self):
        self.ensure_one()
        return babel.dates.format_date(date=self.acquisition_date,
                                       format='MMMM y',
                                       locale=self._context.get('lang')
                                       or 'en_US')

    def _get_car_atn(self, acquisition_date, car_value, fuel_type, co2):
        # Compute the correction coefficient from the age of the car
        now = Date.today()
        if acquisition_date:
            number_of_month = ((now.year - acquisition_date.year) * 12.0 +
                               now.month - acquisition_date.month +
                               int(bool(now.day - acquisition_date.day + 1)))
            if number_of_month <= 12:
                age_coefficient = 1.00
            elif number_of_month <= 24:
                age_coefficient = 0.94
            elif number_of_month <= 36:
                age_coefficient = 0.88
            elif number_of_month <= 48:
                age_coefficient = 0.82
            elif number_of_month <= 60:
                age_coefficient = 0.76
            else:
                age_coefficient = 0.70
            car_value = car_value * age_coefficient
            # Compute atn value from corrected car_value
            magic_coeff = 6.0 / 7.0  # Don't ask me why
            if fuel_type == 'electric':
                atn = 0.0
            else:
                if fuel_type in ['diesel', 'hybrid']:
                    reference = 88.0
                else:
                    reference = 107.0

                if not co2:
                    co2 = 195 if fuel_type in ['diesel', 'hybrid'] else 205

                if co2 <= reference:
                    atn = car_value * max(0.04,
                                          (0.055 - 0.001 *
                                           (reference - co2))) * magic_coeff
                else:
                    atn = car_value * min(0.18,
                                          (0.055 + 0.001 *
                                           (co2 - reference))) * magic_coeff
            return max(1310, atn) / 12.0

    @api.onchange('model_id')
    def _onchange_model_id(self):
        self.car_value = self.model_id.default_car_value
        self.co2 = self.model_id.default_co2
        self.fuel_type = self.model_id.default_fuel_type
Пример #29
0
class OpAssignment(models.Model):
    _name = "op.assignment"
    _inherit = "mail.thread"
    _description = "Assignment"
    _order = "submission_date DESC"

    name = fields.Char('Name', size=64, required=True)
    course_id = fields.Many2one('op.course', 'Course', required=True)
    batch_id = fields.Many2one('op.batch', 'Batch', required=True)
    subject_id = fields.Many2one('op.subject', 'Subject', required=True)
    faculty_id = fields.Many2one('op.faculty',
                                 'Faculty',
                                 default=lambda self: self.env['op.faculty'].
                                 search([('user_id', '=', self.env.uid)]),
                                 required=True)
    assignment_type_id = fields.Many2one('op.assignment.type',
                                         'Assignment Type',
                                         required=True)
    marks = fields.Float('Marks', required=True, track_visibility='onchange')
    description = fields.Text('Description', required=True)
    state = fields.Selection([
        ('draft', 'Draft'),
        ('publish', 'Published'),
        ('finish', 'Finished'),
        ('cancel', 'Cancel'),
    ],
                             'State',
                             required=True,
                             default='draft',
                             track_visibility='onchange')
    issued_date = fields.Datetime(string='Issued Date',
                                  required=True,
                                  default=lambda self: fields.Datetime.now())
    submission_date = fields.Datetime('Submission Date',
                                      required=True,
                                      track_visibility='onchange')
    allocation_ids = fields.Many2many('op.student', string='Allocated To')
    assignment_sub_line = fields.One2many('op.assignment.sub.line',
                                          'assignment_id', 'Submissions')
    reviewer = fields.Many2one('op.faculty', 'Reviewer')

    @api.constrains('issued_date', 'submission_date')
    def check_dates(self):
        for record in self:
            issued_date = fields.Date.from_string(record.issued_date)
            submission_date = fields.Date.from_string(record.submission_date)
            if issued_date > submission_date:
                raise ValidationError(
                    _("Submission Date cannot be set before Issue Date."))

    @api.onchange('course_id')
    def onchange_course(self):
        self.batch_id = False
        if self.course_id:
            subject_ids = self.env['op.course'].search([
                ('id', '=', self.course_id.id)
            ]).subject_ids
            return {'domain': {'subject_id': [('id', 'in', subject_ids.ids)]}}

    def act_publish(self):
        result = self.state = 'publish'
        return result and result or False

    def act_finish(self):
        result = self.state = 'finish'
        return result and result or False

    def act_cancel(self):
        self.state = 'cancel'

    def act_set_to_draft(self):
        self.state = 'draft'
Пример #30
0
class SaleOrderTemplateLine(models.Model):
    _name = "sale.order.template.line"
    _description = "Quotation Template Line"
    _order = 'sale_order_template_id, sequence, id'

    sequence = fields.Integer(
        'Sequence',
        help=
        "Gives the sequence order when displaying a list of sale quote lines.",
        default=10)
    sale_order_template_id = fields.Many2one('sale.order.template',
                                             'Quotation Template Reference',
                                             required=True,
                                             ondelete='cascade',
                                             index=True)
    name = fields.Text('Description', required=True, translate=True)
    product_id = fields.Many2one('product.product',
                                 'Product',
                                 domain=[('sale_ok', '=', True)])
    price_unit = fields.Float('Unit Price',
                              required=True,
                              digits=dp.get_precision('Product Price'))
    discount = fields.Float('Discount (%)',
                            digits=dp.get_precision('Discount'),
                            default=0.0)
    product_uom_qty = fields.Float('Quantity',
                                   required=True,
                                   digits=dp.get_precision('Product UoS'),
                                   default=1)
    product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure')

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

    @api.onchange('product_id')
    def _onchange_product_id(self):
        self.ensure_one()
        if self.product_id:
            name = self.product_id.name_get()[0][1]
            if self.product_id.description_sale:
                name += '\n' + self.product_id.description_sale
            self.name = name
            self.price_unit = self.product_id.lst_price
            self.product_uom_id = self.product_id.uom_id.id
            domain = {
                'product_uom_id':
                [('category_id', '=', self.product_id.uom_id.category_id.id)]
            }
            return {'domain': domain}

    @api.onchange('product_uom_id')
    def _onchange_product_uom(self):
        if self.product_id and self.product_uom_id:
            self.price_unit = self.product_id.uom_id._compute_price(
                self.product_id.lst_price, self.product_uom_id)

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

    @api.multi
    def write(self, values):
        if 'display_type' in values and self.filtered(
                lambda line: line.display_type != values.get('display_type')):
            raise UserError(
                "You cannot change the type of a sale quote line. Instead you should delete the current line and create a new line of the proper type."
            )
        return super(SaleOrderTemplateLine, self).write(values)

    _sql_constraints = [
        ('accountable_product_id_required',
         "CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND product_uom_id IS NOT NULL))",
         "Missing required product and UoM on accountable sale quote line."),
        ('non_accountable_fields_null',
         "CHECK(display_type IS NULL OR (product_id IS NULL AND price_unit = 0 AND product_uom_qty = 0 AND product_uom_id IS NULL))",
         "Forbidden product, unit price, quantity, and UoM on non-accountable sale quote line"
         ),
    ]