class StockValuationLayerRevaluation(models.TransientModel): _name = 'stock.valuation.layer.revaluation' _description = "Wizard model to reavaluate a stock inventory for a product" _check_company_auto = True @api.model def default_get(self, default_fields): res = super().default_get(default_fields) if res.get('product_id'): product = self.env['product.product'].browse(res['product_id']) if product.categ_id.property_cost_method == 'standard': raise UserError( _("You cannot revalue a product with a standard cost method." )) if product.quantity_svl <= 0: raise UserError( _("You cannot revalue a product with an empty or negative stock." )) if 'account_journal_id' not in res and 'account_journal_id' in default_fields and product.categ_id.property_valuation == 'real_time': accounts = product.product_tmpl_id.get_product_accounts() res['account_journal_id'] = accounts['stock_journal'].id return res company_id = fields.Many2one('res.company', "Company", readonly=True, required=True) currency_id = fields.Many2one('res.currency', "Currency", related='company_id.currency_id', required=True) product_id = fields.Many2one('product.product', "Related product", required=True, check_company=True) property_valuation = fields.Selection( related='product_id.categ_id.property_valuation') product_uom_name = fields.Char("Unit of Measure", related='product_id.uom_id.name') current_value_svl = fields.Float("Current Value", related="product_id.value_svl") current_quantity_svl = fields.Float("Current Quantity", related="product_id.quantity_svl") added_value = fields.Monetary("Added value", required=True) new_value = fields.Monetary("New value", compute='_compute_new_value') new_value_by_qty = fields.Monetary("New value by quantity", compute='_compute_new_value') reason = fields.Char("Reason", help="Reason of the revaluation") account_journal_id = fields.Many2one('account.journal', "Journal", check_company=True) account_id = fields.Many2one('account.account', "Counterpart Account", domain=[('deprecated', '=', False)], check_company=True) date = fields.Date("Accounting Date") @api.depends('current_value_svl', 'current_quantity_svl', 'added_value') def _compute_new_value(self): for reval in self: reval.new_value = reval.current_value_svl + reval.added_value if not float_is_zero( reval.current_quantity_svl, precision_rounding=self.product_id.uom_id.rounding): reval.new_value_by_qty = reval.new_value / reval.current_quantity_svl else: reval.new_value_by_qty = 0.0 def action_validate_revaluation(self): """ Revaluate the stock for `self.product_id` in `self.company_id`. - Change the stardard price with the new valuation by product unit. - Create a manual stock valuation layer with the `added_value` of `self`. - Distribute the `added_value` on the remaining_value of layers still in stock (with a remaining quantity) - If the Inventory Valuation of the product category is automated, create related account move. """ self.ensure_one() if self.currency_id.is_zero(self.added_value): raise UserError( _("The added value doesn't have any impact on the stock valuation" )) product_id = self.product_id.with_company(self.company_id) remaining_svls = self.env['stock.valuation.layer'].search([ ('product_id', '=', product_id.id), ('remaining_qty', '>', 0), ('company_id', '=', self.company_id.id), ]) # Create a manual stock valuation layer if self.reason: description = _("Manual Stock Valuation: %s.", self.reason) else: description = _("Manual Stock Valuation: No Reason Given.") if product_id.categ_id.property_cost_method == 'average': description += _( " Product cost updated from %(previous)s to %(new_cost)s.", previous=product_id.standard_price, new_cost=product_id.standard_price + self.added_value / self.current_quantity_svl) revaluation_svl_vals = { 'company_id': self.company_id.id, 'product_id': product_id.id, 'description': description, 'value': self.added_value, 'quantity': 0, } remaining_qty = sum(remaining_svls.mapped('remaining_qty')) remaining_value = self.added_value remaining_value_unit_cost = self.currency_id.round(remaining_value / remaining_qty) for svl in remaining_svls: if float_is_zero( svl.remaining_qty - remaining_qty, precision_rounding=self.product_id.uom_id.rounding): svl.remaining_value += remaining_value else: taken_remaining_value = remaining_value_unit_cost * svl.remaining_qty svl.remaining_value += taken_remaining_value remaining_value -= taken_remaining_value remaining_qty -= svl.remaining_qty revaluation_svl = self.env['stock.valuation.layer'].create( revaluation_svl_vals) # Update the stardard price in case of AVCO if product_id.categ_id.property_cost_method == 'average': product_id.with_context( disable_auto_svl=True ).standard_price += self.added_value / self.current_quantity_svl # If the Inventory Valuation of the product category is automated, create related account move. if self.property_valuation != 'real_time': return True accounts = product_id.product_tmpl_id.get_product_accounts() if self.added_value < 0: debit_account_id = self.account_id.id credit_account_id = accounts.get( 'stock_valuation') and accounts['stock_valuation'].id else: debit_account_id = accounts.get( 'stock_valuation') and accounts['stock_valuation'].id credit_account_id = self.account_id.id move_vals = { 'journal_id': self.account_journal_id.id or accounts['stock_journal'].id, 'company_id': self.company_id.id, 'ref': _("Revaluation of %s", product_id.display_name), 'stock_valuation_layer_ids': [(6, None, [revaluation_svl.id])], 'date': self.date or fields.Date.today(), 'move_type': 'entry', 'line_ids': [(0, 0, { 'name': _( '%(user)s changed stock valuation from %(previous)s to %(new_value)s - %(product)s', user=self.env.user.name, previous=self.current_value_svl, new_value=self.current_value_svl + self.added_value, product=product_id.display_name, ), 'account_id': debit_account_id, 'debit': abs(self.added_value), 'credit': 0, 'product_id': product_id.id, }), (0, 0, { 'name': _( '%(user)s changed stock valuation from %(previous)s to %(new_value)s - %(product)s', user=self.env.user.name, previous=self.current_value_svl, new_value=self.current_value_svl + self.added_value, product=product_id.display_name, ), 'account_id': credit_account_id, 'debit': 0, 'credit': abs(self.added_value), 'product_id': product_id.id, })], } account_move = self.env['account.move'].create(move_vals) account_move._post() return True
class StockLandedCost(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: lc_journal = self.env['ir.property']._get("property_stock_journal", "product.category") 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) target_model = fields.Selection( [('picking', 'Transfers')], string="Apply On", required=True, default='picking', copy=False, states={'done': [('readonly', True)]}) picking_ids = fields.Many2many( 'stock.picking', string='Transfers', copy=False, states={'done': [('readonly', True)]}) allowed_picking_ids = fields.Many2many('stock.picking', compute='_compute_allowed_picking_ids') 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.Monetary( 'Total', compute='_compute_total_amount', 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=[('move_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.depends('company_id') def _compute_allowed_picking_ids(self): # Backport of f329de26: allowed_picking_ids is useless, view_stock_landed_cost_form no longer uses it, # the field and its compute are kept since this is a stable version. Still, this compute has been made # more resilient to MemoryErrors. valued_picking_ids_per_company = defaultdict(list) if self.company_id: self.env.cr.execute("""SELECT sm.picking_id, sm.company_id FROM stock_move AS sm INNER JOIN stock_valuation_layer AS svl ON svl.stock_move_id = sm.id WHERE sm.picking_id IS NOT NULL AND sm.company_id IN %s GROUP BY sm.picking_id, sm.company_id""", [tuple(self.company_id.ids)]) for res in self.env.cr.fetchall(): valued_picking_ids_per_company[res[1]].append(res[0]) for cost in self: n = 5000 cost.allowed_picking_ids = valued_picking_ids_per_company[cost.company_id.id][:n] for ids_chunk in tools.split_every(n, valued_picking_ids_per_company[cost.company_id.id][n:]): cost.allowed_picking_ids = [(4, id_) for id_ in ids_chunk] @api.onchange('target_model') def _onchange_target_model(self): if self.target_model != 'picking': self.picking_ids = False @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().create(vals) def unlink(self): self.button_cancel() return super().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()._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): self._check_can_validate() 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: cost = cost.with_company(cost.company_id) move = self.env['account.move'] move_vals = { 'journal_id': cost.account_journal_id.id, 'date': cost.date, 'ref': cost.name, 'line_ids': [], 'move_type': 'entry', } valuation_layer_ids = [] cost_to_add_byproduct = defaultdict(lambda: 0.0) 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] # 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, }) linked_layer.remaining_value += cost_to_add valuation_layer_ids.append(valuation_layer.id) # Update the AVCO product = line.move_id.product_id if product.cost_method == 'average': cost_to_add_byproduct[product] += cost_to_add # Products with manual inventory valuation are ignored because they do not need to create journal entries. if product.valuation != "real_time": continue # `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) # batch standard price computation avoid recompute quantity_svl at each iteration products = self.env['product.product'].browse(p.id for p in cost_to_add_byproduct.keys()) for product in products: # iterate on recordset to prefetch efficiently quantity_svl if not float_is_zero(product.quantity_svl, precision_rounding=product.uom_id.rounding): product.with_company(cost.company_id).sudo().with_context(disable_auto_svl=True).standard_price += cost_to_add_byproduct[product] / product.quantity_svl move_vals['stock_valuation_layer_ids'] = [(6, None, valuation_layer_ids)] # We will only create the accounting entry when there are defined lines (the lines will be those linked to products of real_time valuation category). cost_vals = {'state': 'done'} if move_vals.get("line_ids"): move = move.create(move_vals) cost_vals.update({'account_move_id': move.id}) cost.write(cost_vals) if cost.account_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 and not aml.reconciled).reconcile() return True def get_valuation_lines(self): self.ensure_one() lines = [] for move in self._get_targeted_move_ids(): # 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.cost_method not in ('fifo', 'average') or move.state == 'cancel' or not move.product_qty: 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: target_model_descriptions = dict(self._fields['target_model']._description_selection(self.env)) raise UserError(_("You cannot apply landed costs on the chosen %s(s). Landed costs can only be applied for products with FIFO or average costing method.", target_model_descriptions[self.target_model])) return lines def compute_landed_cost(self): AdjustementLines = self.env['stock.valuation.adjustment.lines'] AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink() towrite_dict = {} for cost in self.filtered(lambda cost: cost._get_targeted_move_ids()): rounding = cost.currency_id.rounding 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 += cost.currency_id.round(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 rounding: value = tools.float_round(value, precision_rounding=rounding, 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["ir.actions.actions"]._for_xml_id("stock_account.stock_valuation_layer_action") return dict(action, domain=domain) def _get_targeted_move_ids(self): return self.picking_ids.move_lines def _check_can_validate(self): if any(cost.state != 'draft' for cost in self): raise UserError(_('Only draft landed costs can be validated')) for cost in self: if not cost._get_targeted_move_ids(): target_model_descriptions = dict(self._fields['target_model']._description_selection(self.env)) raise UserError(_('Please define %s on which those additional costs should apply.', target_model_descriptions[cost.target_model])) 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
class CrmLeadReportAssign(models.Model): """ CRM Lead Report """ _name = "crm.lead.report.assign" _auto = False _description = "CRM Lead Report" partner_assigned_id = fields.Many2one('res.partner', 'Partner', readonly=True) grade_id = fields.Many2one('res.partner.grade', 'Grade', readonly=True) user_id = fields.Many2one('res.users', 'User', readonly=True) country_id = fields.Many2one('res.country', 'Country', readonly=True) team_id = fields.Many2one('crm.team', 'Sales Channel', oldname='section_id', readonly=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) date_assign = fields.Date('Assign Date', readonly=True) create_date = fields.Datetime('Create Date', readonly=True) delay_open = fields.Float('Delay to Assign', digits=(16, 2), readonly=True, group_operator="avg", help="Number of Days to open the case") delay_close = fields.Float('Delay to Close', digits=(16, 2), readonly=True, group_operator="avg", help="Number of Days to close the case") delay_expected = fields.Float('Overpassed Deadline', digits=(16, 2), readonly=True, group_operator="avg") probability = fields.Float('Avg Probability', digits=(16, 2), readonly=True, group_operator="avg") probability_max = fields.Float('Max Probability', digits=(16, 2), readonly=True, group_operator="max") planned_revenue = fields.Float('Planned Revenue', digits=(16, 2), readonly=True) probable_revenue = fields.Float('Probable Revenue', digits=(16, 2), readonly=True) tag_ids = fields.Many2many('crm.lead.tag', 'crm_lead_tag_rel', 'lead_id', 'tag_id', 'Tags') partner_id = fields.Many2one('res.partner', 'Customer', readonly=True) opening_date = fields.Datetime('Opening Date', readonly=True) date_closed = fields.Datetime('Close Date', readonly=True) nbr_cases = fields.Integer('# of Cases', readonly=True, oldname='nbr') company_id = fields.Many2one('res.company', 'Company', readonly=True) priority = fields.Selection(crm_stage.AVAILABLE_PRIORITIES, 'Priority') type = fields.Selection( [('lead', 'Lead'), ('opportunity', 'Opportunity')], 'Type', help="Type is used to separate Leads and Opportunities") @api.model_cr def init(self): """ CRM Lead Report @param cr: the current row, from the database cursor """ tools.drop_view_if_exists(self._cr, 'crm_lead_report_assign') self._cr.execute(""" CREATE OR REPLACE VIEW crm_lead_report_assign AS ( SELECT c.id, c.date_open as opening_date, c.date_closed as date_closed, c.date_assign, c.user_id, c.probability, c.probability as probability_max, c.type, c.company_id, c.priority, c.team_id, c.partner_id, c.country_id, c.planned_revenue, c.partner_assigned_id, p.grade_id, p.date as partner_date, c.planned_revenue*(c.probability/100) as probable_revenue, 1 as nbr_cases, c.create_date as create_date, extract('epoch' from (c.write_date-c.create_date))/(3600*24) as delay_close, extract('epoch' from (c.date_deadline - c.date_closed))/(3600*24) as delay_expected, extract('epoch' from (c.date_open-c.create_date))/(3600*24) as delay_open FROM crm_lead c left join res_partner p on (c.partner_assigned_id=p.id) )""")
class Employee(models.Model): _inherit = "hr.employee" remaining_leaves = fields.Float( compute='_compute_remaining_leaves', string='Remaining Legal Leaves', inverse='_inverse_remaining_leaves', help= 'Total number of legal leaves allocated to this employee, change this value to create allocation/leave request. ' 'Total based on all the leave types without overriding limit.') current_leave_state = fields.Selection(compute='_compute_leave_status', string="Current Leave Status", selection=[ ('draft', 'New'), ('confirm', 'Waiting Approval'), ('refuse', 'Refused'), ('validate1', 'Waiting Second Approval'), ('validate', 'Approved'), ('cancel', 'Cancelled') ]) current_leave_id = fields.Many2one('hr.holidays.status', compute='_compute_leave_status', string="Current Leave 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 Leaves', compute='_compute_leaves_count') show_leaves = fields.Boolean('Able to see Remaining Leaves', compute='_compute_show_leaves') is_absent_totay = fields.Boolean('Absent Today', compute='_compute_absent_employee', search='_search_absent_employee') 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 hr_holidays h join hr_holidays_status s ON (s.id=h.holiday_status_id) WHERE h.state='validate' AND s.limit=False 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()) @api.multi def _compute_remaining_leaves(self): remaining = self._get_remaining_leaves() for employee in self: employee.remaining_leaves = remaining.get(employee.id, 0.0) @api.multi def _inverse_remaining_leaves(self): status_list = self.env['hr.holidays.status'].search([('limit', '=', False)]) # Create leaves (adding remaining leaves) or raise (reducing remaining leaves) actual_remaining = self._get_remaining_leaves() for employee in self.filtered( lambda employee: employee.remaining_leaves): # check the status list. This is done here and not before the loop to avoid raising # exception on employee creation (since we are in a computed field). if len(status_list) != 1: raise UserError( _("The feature behind the field 'Remaining Legal Leaves' can only be used when there is only one " "leave type with the option 'Allow to Override Limit' unchecked. (%s Found). " "Otherwise, the update is ambiguous as we cannot decide on which leave type the update has to be done. " "\n You may prefer to use the classic menus 'Leave Requests' and 'Allocation Requests' located in Leaves Application " "to manage the leave days of the employees if the configuration does not allow to use this field." ) % (len(status_list))) status = status_list[0] if status_list else None if not status: continue # if a status is found, then compute remaing leave for current employee difference = employee.remaining_leaves - actual_remaining.get( employee.id, 0) if difference > 0: leave = self.env['hr.holidays'].create({ 'name': _('Allocation for %s') % employee.name, 'employee_id': employee.id, 'holiday_status_id': status.id, 'type': 'add', 'holiday_type': 'employee', 'number_of_days_temp': difference }) leave.action_approve() if leave.double_validation: leave.action_validate() elif difference < 0: raise UserError( _('You cannot reduce validated allocation requests')) @api.multi 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.holidays'].sudo().search([ ('employee_id', 'in', self.ids), ('date_from', '<=', fields.Datetime.now()), ('date_to', '>=', fields.Datetime.now()), ('type', '=', 'remove'), ('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 leave_data[ holiday.employee_id.id]['leave_date_to'] = holiday.date_to 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') @api.multi def _compute_leaves_count(self): leaves = self.env['hr.holidays'].read_group( [('employee_id', 'in', self.ids), ('holiday_status_id.limit', '=', False), ('state', '=', 'validate')], fields=['number_of_days', 'employee_id'], groupby=['employee_id']) mapping = dict([(leave['employee_id'][0], leave['number_of_days']) for leave in leaves]) for employee in self: employee.leaves_count = mapping.get(employee.id) @api.multi 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 @api.multi def _compute_absent_employee(self): today_date = datetime.datetime.utcnow().date() today_start = fields.Datetime.to_string( today_date) # get the midnight of the current utc day today_end = fields.Datetime.to_string( today_date + relativedelta(hours=23, minutes=59, seconds=59)) data = self.env['hr.holidays'].read_group( [('employee_id', 'in', self.ids), ('state', 'not in', ['cancel', 'refuse']), ('date_from', '<=', today_end), ('date_to', '>=', today_start), ('type', '=', 'remove')], ['employee_id'], ['employee_id']) result = dict.fromkeys(self.ids, False) for item in data: if item['employee_id_count'] >= 1: result[item['employee_id'][0]] = True for employee in self: employee.is_absent_totay = result[employee.id] @api.multi def _search_absent_employee(self, operator, value): today_date = datetime.datetime.utcnow().date() today_start = fields.Datetime.to_string( today_date) # get the midnight of the current utc day today_end = fields.Datetime.to_string( today_date + relativedelta(hours=23, minutes=59, seconds=59)) holidays = self.env['hr.holidays'].sudo().search([ ('employee_id', '!=', False), ('state', 'not in', ['cancel', 'refuse']), ('date_from', '<=', today_end), ('date_to', '>=', today_start), ('type', '=', 'remove') ]) return [('id', 'in', holidays.mapped('employee_id').ids)]
class FleetVehicle(models.Model): _inherit = 'mail.thread' _name = 'fleet.vehicle' _description = 'Information on a vehicle' _order = 'license_plate asc' def _get_default_state(self): state = self.env.ref('fleet.vehicle_state_active', raise_if_not_found=False) return state and state.id or False name = fields.Char(compute="_compute_vehicle_name", store=True) active = fields.Boolean('Active', default=True, track_visibility="onchange") company_id = fields.Many2one('res.company', 'Company') license_plate = fields.Char( required=True, help='License plate number of the vehicle (i = plate number for a car)' ) vin_sn = fields.Char( 'Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False) driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle') model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle') log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs') log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs') log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts') cost_count = fields.Integer(compute="_compute_count_all", string="Costs") contract_count = fields.Integer(compute="_compute_count_all", string='Contracts') service_count = fields.Integer(compute="_compute_count_all", string='Services') fuel_logs_count = fields.Integer(compute="_compute_count_all", string='Fuel Logs') odometer_count = fields.Integer(compute="_compute_count_all", string='Odometer') acquisition_date = fields.Date( 'Immatriculation Date', required=False, help='Date when the vehicle has been immatriculated') color = fields.Char(help='Color of the vehicle') state_id = fields.Many2one('fleet.vehicle.state', 'State', default=_get_default_state, help='Current state of the vehicle', ondelete="set null") location = fields.Char(help='Location of the vehicle (garage, ...)') seats = fields.Integer('Seats Number', help='Number of seats of the vehicle') model_year = fields.Char('Model Year', help='Year of the model') doors = fields.Integer('Doors Number', help='Number of doors of the vehicle', default=5) tag_ids = fields.Many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id', 'tag_id', 'Tags', copy=False) odometer = fields.Float( compute='_get_odometer', inverse='_set_odometer', string='Last Odometer', help='Odometer measure of the vehicle at the moment of this log') odometer_unit = fields.Selection([('kilometers', 'Kilometers'), ('miles', 'Miles')], 'Odometer Unit', default='kilometers', help='Unit of the odometer ', required=True) transmission = fields.Selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle') fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle') horsepower = fields.Integer() horsepower_tax = fields.Float('Horsepower Taxation') power = fields.Integer('Power', help='Power in kW of the vehicle') co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle') image = fields.Binary(related='model_id.image', string="Logo") image_medium = fields.Binary(related='model_id.image_medium', string="Logo (medium)") image_small = fields.Binary(related='model_id.image_small', string="Logo (small)") contract_renewal_due_soon = fields.Boolean( compute='_compute_contract_reminder', search='_search_contract_renewal_due_soon', string='Has Contracts to renew', multi='contract_info') contract_renewal_overdue = fields.Boolean( compute='_compute_contract_reminder', search='_search_get_overdue_contract_reminder', string='Has Contracts Overdue', multi='contract_info') contract_renewal_name = fields.Text( compute='_compute_contract_reminder', string='Name of contract to renew soon', multi='contract_info') contract_renewal_total = fields.Text( compute='_compute_contract_reminder', string='Total of contracts due or overdue minus one', multi='contract_info') car_value = fields.Float(string="Catalog Value (VAT Incl.)", help='Value of the bought vehicle') residual_value = fields.Float() _sql_constraints = [('driver_id_unique', 'UNIQUE(driver_id)', 'Only one car can be assigned to the same employee!')] @api.depends('model_id', 'license_plate') def _compute_vehicle_name(self): for record in self: record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + record.license_plate def _get_odometer(self): FleetVehicalOdometer = self.env['fleet.vehicle.odometer'] for record in self: vehicle_odometer = FleetVehicalOdometer.search( [('vehicle_id', '=', record.id)], limit=1, order='value desc') if vehicle_odometer: record.odometer = vehicle_odometer.value else: record.odometer = 0 def _set_odometer(self): for record in self: if record.odometer: date = fields.Date.context_today(record) data = { 'value': record.odometer, 'date': date, 'vehicle_id': record.id } self.env['fleet.vehicle.odometer'].create(data) def _compute_count_all(self): Odometer = self.env['fleet.vehicle.odometer'] LogFuel = self.env['fleet.vehicle.log.fuel'] LogService = self.env['fleet.vehicle.log.services'] LogContract = self.env['fleet.vehicle.log.contract'] Cost = self.env['fleet.vehicle.cost'] for record in self: record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)]) record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', record.id)]) record.service_count = LogService.search_count([('vehicle_id', '=', record.id)]) record.contract_count = LogContract.search_count([ ('vehicle_id', '=', record.id), ('state', '!=', 'closed') ]) record.cost_count = Cost.search_count([('vehicle_id', '=', record.id), ('parent_id', '=', False)]) @api.depends('log_contracts') def _compute_contract_reminder(self): for record in self: overdue = False due_soon = False total = 0 name = '' for element in record.log_contracts: if element.state in ('open', 'expired') and element.expiration_date: current_date_str = fields.Date.context_today(record) due_time_str = element.expiration_date current_date = fields.Date.from_string(current_date_str) due_time = fields.Date.from_string(due_time_str) diff_time = (due_time - current_date).days if diff_time < 0: overdue = True total += 1 if diff_time < 15 and diff_time >= 0: due_soon = True total += 1 if overdue or due_soon: log_contract = self.env[ 'fleet.vehicle.log.contract'].search( [('vehicle_id', '=', record.id), ('state', 'in', ('open', 'expired'))], limit=1, order='expiration_date asc') if log_contract: # we display only the name of the oldest overdue/due soon contract name = log_contract.cost_subtype_id.name record.contract_renewal_overdue = overdue record.contract_renewal_due_soon = due_soon record.contract_renewal_total = total - 1 # we remove 1 from the real total for display purposes record.contract_renewal_name = name def _search_contract_renewal_due_soon(self, operator, value): res = [] assert operator in ('=', '!=', '<>') and value in ( True, False), 'Operation not supported' if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False): search_operator = 'in' else: search_operator = 'not in' today = fields.Date.context_today(self) datetime_today = fields.Datetime.from_string(today) limit_date = fields.Datetime.to_string(datetime_today + relativedelta(days=+15)) self.env.cr.execute( """SELECT cost.vehicle_id, count(contract.id) AS contract_number FROM fleet_vehicle_cost cost LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id WHERE contract.expiration_date IS NOT NULL AND contract.expiration_date > %s AND contract.expiration_date < %s AND contract.state IN ('open', 'expired') GROUP BY cost.vehicle_id""", (today, limit_date)) res_ids = [x[0] for x in self.env.cr.fetchall()] res.append(('id', search_operator, res_ids)) return res def _search_get_overdue_contract_reminder(self, operator, value): res = [] assert operator in ('=', '!=', '<>') and value in ( True, False), 'Operation not supported' if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False): search_operator = 'in' else: search_operator = 'not in' today = fields.Date.context_today(self) self.env.cr.execute( '''SELECT cost.vehicle_id, count(contract.id) AS contract_number FROM fleet_vehicle_cost cost LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id WHERE contract.expiration_date IS NOT NULL AND contract.expiration_date < %s AND contract.state IN ('open', 'expired') GROUP BY cost.vehicle_id ''', (today, )) res_ids = [x[0] for x in self.env.cr.fetchall()] res.append(('id', search_operator, res_ids)) return res @api.onchange('model_id') def _onchange_model(self): if self.model_id: self.image_medium = self.model_id.image else: self.image_medium = False @api.model def create(self, data): vehicle = super(FleetVehicle, self.with_context(mail_create_nolog=True)).create(data) vehicle.message_post(body=_('%s %s has been added to the fleet!') % (vehicle.model_id.name, vehicle.license_plate)) return vehicle @api.multi def write(self, vals): """ This function write an entry in the openchatter whenever we change important information on the vehicle like the model, the drive, the state of the vehicle or its license plate """ for vehicle in self: changes = [] if 'model_id' in vals and vehicle.model_id.id != vals['model_id']: value = self.env['fleet.vehicle.model'].browse( vals['model_id']).name oldmodel = vehicle.model_id.name or _('None') changes.append( _("Model: from '%s' to '%s'") % (oldmodel, value)) if 'driver_id' in vals and vehicle.driver_id.id != vals[ 'driver_id']: value = self.env['res.partner'].browse(vals['driver_id']).name olddriver = (vehicle.driver_id.name) or _('None') changes.append( _("Driver: from '%s' to '%s'") % (olddriver, value)) if 'state_id' in vals and vehicle.state_id.id != vals['state_id']: value = self.env['fleet.vehicle.state'].browse( vals['state_id']).name oldstate = vehicle.state_id.name or _('None') changes.append( _("State: from '%s' to '%s'") % (oldstate, value)) if 'license_plate' in vals and vehicle.license_plate != vals[ 'license_plate']: old_license_plate = vehicle.license_plate or _('None') changes.append( _("License Plate: from '%s' to '%s'") % (old_license_plate, vals['license_plate'])) if len(changes) > 0: self.message_post(body=", ".join(changes)) return super(FleetVehicle, self).write(vals) @api.multi def return_action_to_open(self): """ This opens the xml view specified in xml_id for the current vehicle """ self.ensure_one() xml_id = self.env.context.get('xml_id') if xml_id: res = self.env['ir.actions.act_window'].for_xml_id('fleet', xml_id) res.update(context=dict(self.env.context, default_vehicle_id=self.id, group_by=False), domain=[('vehicle_id', '=', self.id)]) return res return False @api.multi def act_show_log_cost(self): """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs @return: the costs log view """ self.ensure_one() res = self.env['ir.actions.act_window'].for_xml_id( 'fleet', 'fleet_vehicle_costs_action') res.update(context=dict(self.env.context, default_vehicle_id=self.id, search_default_parent_false=True), domain=[('vehicle_id', '=', self.id)]) return res
class ShipmentReportEpt(models.TransientModel): _name = "shipment.report.ept" from_date = fields.Date('From') to_date = fields.Date('To')
class PurchaseRequisitionLine(models.Model): _name = "purchase.requisition.line" _description = "Purchase Requisition Line" _rec_name = 'product_id' product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], required=True) product_uom_id = fields.Many2one('uom.uom', string='Product Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]") product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id') product_qty = fields.Float(string='Quantity', digits='Product Unit of Measure') product_description_variants = fields.Char('Custom Description') price_unit = fields.Float(string='Unit Price', digits='Product Price') qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities') requisition_id = fields.Many2one('purchase.requisition', required=True, string='Purchase Agreement', ondelete='cascade') company_id = fields.Many2one('res.company', related='requisition_id.company_id', string='Company', store=True, readonly=True, default= lambda self: self.env.company) account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') schedule_date = fields.Date(string='Scheduled Date') supplier_info_ids = fields.One2many('product.supplierinfo', 'purchase_requisition_line_id') @api.model def create(self,vals): res = super(PurchaseRequisitionLine, self).create(vals) if res.requisition_id.state not in ['draft', 'cancel', 'done'] and res.requisition_id.is_quantity_copy == 'none': supplier_infos = self.env['product.supplierinfo'].search([ ('product_id', '=', vals.get('product_id')), ('name', '=', res.requisition_id.vendor_id.id), ]) if not any(s.purchase_requisition_id for s in supplier_infos): res.create_supplier_info() if vals['price_unit'] <= 0.0: raise UserError(_('You cannot confirm the blanket order without price.')) return res def write(self, vals): res = super(PurchaseRequisitionLine, self).write(vals) if 'price_unit' in vals: if vals['price_unit'] <= 0.0 and any( requisition.state not in ['draft', 'cancel', 'done'] and requisition.is_quantity_copy == 'none' for requisition in self.mapped('requisition_id')): raise UserError(_('You cannot confirm the blanket order without price.')) # If the price is updated, we have to update the related SupplierInfo self.supplier_info_ids.write({'price': vals['price_unit']}) return res def unlink(self): to_unlink = self.filtered(lambda r: r.requisition_id.state not in ['draft', 'cancel', 'done']) to_unlink.mapped('supplier_info_ids').unlink() return super(PurchaseRequisitionLine, self).unlink() def create_supplier_info(self): purchase_requisition = self.requisition_id if purchase_requisition.type_id.quantity_copy == 'none' and purchase_requisition.vendor_id: # create a supplier_info only in case of blanket order self.env['product.supplierinfo'].create({ 'name': purchase_requisition.vendor_id.id, 'product_id': self.product_id.id, 'product_tmpl_id': self.product_id.product_tmpl_id.id, 'price': self.price_unit, 'currency_id': self.requisition_id.currency_id.id, 'purchase_requisition_line_id': self.id, }) @api.depends('requisition_id.purchase_ids.state') def _compute_ordered_qty(self): line_found = set() for line in self: total = 0.0 for po in line.requisition_id.purchase_ids.filtered(lambda purchase_order: purchase_order.state in ['purchase', 'done']): for po_line in po.order_line.filtered(lambda order_line: order_line.product_id == line.product_id): if po_line.product_uom != line.product_uom_id: total += po_line.product_uom._compute_quantity(po_line.product_qty, line.product_uom_id) else: total += po_line.product_qty if line.product_id not in line_found : line.qty_ordered = total line_found.add(line.product_id) else: line.qty_ordered = 0 @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_po_id self.product_qty = 1.0 if not self.schedule_date: self.schedule_date = self.requisition_id.schedule_date def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, taxes_ids=False): self.ensure_one() requisition = self.requisition_id if self.product_description_variants: name += '\n' + self.product_description_variants if requisition.schedule_date: date_planned = datetime.combine(requisition.schedule_date, time.min) else: date_planned = datetime.now() return { 'name': name, 'product_id': self.product_id.id, 'product_uom': self.product_id.uom_po_id.id, 'product_qty': product_qty, 'price_unit': price_unit, 'taxes_id': [(6, 0, taxes_ids)], 'date_planned': date_planned, 'account_analytic_id': self.account_analytic_id.id, 'analytic_tag_ids': self.analytic_tag_ids.ids, }
class SalonOrder(models.Model): _name = 'salon.order' @api.depends('order_line.price_subtotal') def sub_total_update(self): for order in self: amount_untaxed = 0.0 for line in order.order_line: amount_untaxed += line.price_subtotal order.price_subtotal = amount_untaxed for order in self: total_time_taken = 0.0 for line in order.order_line: total_time_taken += line.time_taken order.time_taken_total = total_time_taken time_takes = total_time_taken hours = int(time_takes) minutes = (time_takes - hours) * 60 start_time_store = datetime.strptime(self.start_time, "%Y-%m-%d %H:%M:%S") self.write({ 'end_time': start_time_store + timedelta(hours=hours, minutes=minutes) }) if self.end_time: self.write({'end_time_only': str(self.end_time)[11:16]}) if self.start_time: salon_start_time = self.start_time salon_start_time_date = salon_start_time[0:10] self.write({'start_date_only': salon_start_time_date}) self.write({'start_time_only': str(self.start_time)[11:16]}) name = fields.Char(string='Salon', required=True, copy=False, readonly=True, default='Draft Salon Order') start_time = fields.Datetime(string="Start time", default=date.today(), required=True) end_time = fields.Datetime(string="End time") date = fields.Datetime(string="Date", required=True, default=date.today()) color = fields.Integer(string="Colour", default=6) partner_id = fields.Many2one( 'res.partner', string="Customer", required=False, help="If the customer is a regular customer, " "then you can add the customer in your database") customer_name = fields.Char(string="Name", required=True) amount = fields.Float(string="Amount") chair_id = fields.Many2one('salon.chair', string="Chair", required=True) price_subtotal = fields.Float(string='Total', compute='sub_total_update', readonly=True, store=True) time_taken_total = fields.Float(string="Total time taken") note = fields.Text('Terms and conditions') order_line = fields.One2many('salon.order.lines', 'salon_order', string="Order Lines") stage_id = fields.Many2one('salon.stages', string="Stages", default=1) inv_stage_identifier = fields.Boolean(string="Stage Identifier") invoice_number = fields.Integer(string="Invoice Number") validation_controller = fields.Boolean(string="Validation controller", default=False) start_date_only = fields.Date(string="Date Only") booking_identifier = fields.Boolean(string="Booking Identifier") start_time_only = fields.Char(string="Start Time Only") end_time_only = fields.Char(string="End Time Only") chair_user = fields.Many2one('res.users', string="Chair User") salon_order_created_user = fields.Integer( string="Salon Order Created User", default=lambda self: self._uid) @api.onchange('start_time') def start_date_change(self): salon_start_time = self.start_time salon_start_time_date = salon_start_time[0:10] self.write({'start_date_only': salon_start_time_date}) @api.multi def action_view_invoice_salon(self): imd = self.env['ir.model.data'] action = imd.xmlid_to_object('account.action_invoice_tree1') list_view_id = imd.xmlid_to_res_id('account.invoice_tree') form_view_id = imd.xmlid_to_res_id('account.invoice_form') result = { 'name': action.name, 'help': action.help, 'type': action.type, 'views': [[form_view_id, 'form'], [list_view_id, 'tree'], [False, 'graph'], [False, 'kanban'], [False, 'calendar'], [False, 'pivot']], 'target': action.target, 'context': action.context, 'res_model': action.res_model, 'res_id': self.invoice_number, } return result @api.multi def write(self, cr): if 'stage_id' in cr.keys(): if self.stage_id.id == 3 and cr['stage_id'] != 4: raise ValidationError(_("You can't perform that move !")) if self.stage_id.id == 1 and cr['stage_id'] not in [2, 5]: raise ValidationError(_("You can't perform that move!")) if self.stage_id.id == 4: raise ValidationError( _("You can't move a salon order from closed stage !")) if self.stage_id.id == 5: raise ValidationError( _("You can't move a salon order from cancel stage !")) if self.stage_id.id == 2 and (cr['stage_id'] == 1 or cr['stage_id'] == 4): raise ValidationError(_("You can't perform that move !")) if self.stage_id.id == 2 and cr[ 'stage_id'] == 3 and self.inv_stage_identifier is False: self.salon_invoice_create() if 'stage_id' in cr.keys() and self.name == "Draft Salon Order": if cr['stage_id'] == 2: self.salon_confirm() return super(SalonOrder, self).write(cr) @api.multi def salon_confirm(self): sequence_code = 'salon.order.sequence' order_date = self.date order_date = order_date[0:10] self.name = self.env['ir.sequence'].with_context( ir_sequence_date=order_date).next_by_code(sequence_code) if self.partner_id: self.partner_id.partner_salon = True self.stage_id = 2 self.chair_id.number_of_orders = len(self.env['salon.order'].search([ ("chair_id", "=", self.chair_id.id), ("stage_id", "in", [2, 3]) ])) self.chair_id.total_time_taken_chair = self.chair_id.total_time_taken_chair + self.time_taken_total self.chair_user = self.chair_id.user_of_chair @api.multi def salon_validate(self): self.validation_controller = True @api.multi def salon_close(self): self.stage_id = 4 self.chair_id.number_of_orders = len(self.env['salon.order'].search([ ("chair_id", "=", self.chair_id.id), ("stage_id", "in", [2, 3]) ])) self.chair_id.total_time_taken_chair = self.chair_id.total_time_taken_chair - self.time_taken_total @api.multi def salon_cancel(self): self.stage_id = 5 self.chair_id.number_of_orders = len(self.env['salon.order'].search([ ("chair_id", "=", self.chair_id.id), ("stage_id", "in", [2, 3]) ])) if self.stage_id.id != 1: self.chair_id.total_time_taken_chair = self.chair_id.total_time_taken_chair - self.time_taken_total @api.multi def button_total_update(self): for order in self: amount_untaxed = 0.0 for line in order.order_line: amount_untaxed += line.price_subtotal order.price_subtotal = amount_untaxed @api.onchange('chair_id') def onchange_chair(self): if 'active_id' in self._context.keys(): self.chair_id = self._context['active_id'] @api.multi def salon_invoice_create(self): inv_obj = self.env['account.invoice'] inv_line_obj = self.env['account.invoice.line'] if self.partner_id: supplier = self.partner_id else: supplier = self.partner_id.search([("name", "=", "Salon Default Customer")]) company_id = self.env['res.users'].browse(1).company_id currency_salon = company_id.currency_id.id inv_data = { 'name': supplier.name, 'reference': supplier.name, 'account_id': supplier.property_account_payable_id.id, 'partner_id': supplier.id, 'currency_id': currency_salon, 'journal_id': 1, 'origin': self.name, 'company_id': company_id.id, } inv_id = inv_obj.create(inv_data) self.invoice_number = inv_id product_id = self.env['product.product'].search([("name", "=", "Salon Service")]) for records in self.order_line: if product_id.property_account_income_id.id: income_account = product_id.property_account_income_id.id elif product_id.categ_id.property_account_income_categ_id.id: income_account = product_id.categ_id.property_account_income_categ_id.id else: raise UserError( _('Please define income account for this product: "%s" (id:%d).' ) % (product_id.name, product_id.id)) inv_line_data = { 'name': records.service_id.name, 'account_id': income_account, 'price_unit': records.price, 'quantity': 1, 'product_id': product_id.id, 'invoice_id': inv_id.id, } inv_line_obj.create(inv_line_data) imd = self.env['ir.model.data'] action = imd.xmlid_to_object('account.action_invoice_tree1') list_view_id = imd.xmlid_to_res_id('account.invoice_tree') form_view_id = imd.xmlid_to_res_id('account.invoice_form') result = { 'name': action.name, 'help': action.help, 'type': 'ir.actions.act_window', 'views': [[list_view_id, 'tree'], [form_view_id, 'form'], [False, 'graph'], [False, 'kanban'], [False, 'calendar'], [False, 'pivot']], 'target': action.target, 'context': action.context, 'res_model': 'account.invoice', } if len(inv_id) > 1: result['domain'] = "[('id','in',%s)]" % inv_id.ids elif len(inv_id) == 1: result['views'] = [(form_view_id, 'form')] result['res_id'] = inv_id.ids[0] else: result = {'type': 'ir.actions.act_window_close'} self.inv_stage_identifier = True self.stage_id = 3 invoiced_records = self.env['salon.order'].search([ ('stage_id', 'in', [3, 4]), ('chair_id', '=', self.chair_id.id) ]) total = 0 for rows in invoiced_records: invoiced_date = rows.date invoiced_date = invoiced_date[0:10] if invoiced_date == str(date.today()): total = total + rows.price_subtotal self.chair_id.collection_today = total self.chair_id.number_of_orders = len(self.env['salon.order'].search([ ("chair_id", "=", self.chair_id.id), ("stage_id", "in", [2, 3]) ])) return result @api.multi def unlink(self): for order in self: if order.stage_id.id == 3 or order.stage_id.id == 4: raise UserError(_("You can't delete an invoiced salon order!")) return super(SalonOrder, self).unlink()
class StockCycleCount(models.Model): _name = 'stock.cycle.count' _description = "Stock Cycle Counts" _inherit = 'mail.thread' @api.multi def _count_inventory_adj(self): self.ensure_one() self.inventory_adj_count = len(self.stock_adjustment_ids) @api.model def create(self, vals): vals['name'] = self.env['ir.sequence'].next_by_code( 'stock.cycle.count') or '' return super(StockCycleCount, self).create(vals) @api.model def _company_get(self): company_id = self.env['res.company']._company_default_get(self._name) return company_id name = fields.Char(string='Name', readonly=True) location_id = fields.Many2one( 'stock.location', string='Location', required=True, readonly=True, states={'draft': [('readonly', False)]}) responsible_id = fields.Many2one( 'res.users', string='Assigned to', readonly=True, states={'draft': [ ('readonly', False)]}, track_visibility='onchange') date_deadline = fields.Date( string='Required Date', readonly=True, states={'draft': [('readonly', False)]}, track_visibility='onchange') cycle_count_rule_id = fields.Many2one( 'stock.cycle.count.rule', string='Cycle count rule', required=True, readonly=True, states={'draft': [('readonly', False)]}, track_visibility='onchange') state = fields.Selection(selection=[ ('draft', 'Planned'), ('open', 'Execution'), ('cancelled', 'Cancelled'), ('done', 'Done') ], string='State', default='draft', track_visibility='onchange') stock_adjustment_ids = fields.One2many('stock.inventory', 'cycle_count_id', string='Inventory Adjustment', track_visibility='onchange') inventory_adj_count = fields.Integer(compute=_count_inventory_adj) company_id = fields.Many2one( 'res.company', string='Company', required=True, default=_company_get, readonly=True) @api.multi def do_cancel(self): self.ensure_one() self.state = 'cancelled' @api.model def _prepare_inventory_adjustment(self): return { 'name': 'INV/{}'.format(self.name), 'cycle_count_id': self.id, 'location_id': self.location_id.id, 'exclude_sublocation': True } @api.multi def action_create_inventory_adjustment(self): self.ensure_one() if self.state != 'draft': raise UserError(_( "You can only confirm cycle counts in state 'Planned'." )) data = self._prepare_inventory_adjustment() self.env['stock.inventory'].create(data) self.state = 'open' return True @api.multi def action_view_inventory(self): self.ensure_one() result = self.env.ref('stock.action_inventory_form').read()[0] adjustment_ids = self.mapped('stock_adjustment_ids').ids if len(adjustment_ids) > 0: result['domain'] = [('id', 'in', adjustment_ids)] return result
class OpResultTemplate(models.Model): _name = 'op.result.template' _inherit = ['mail.thread'] _description = 'Result Template' _rec_name = 'name' exam_session_id = fields.Many2one( 'op.exam.session', 'Exam Session', required=True, track_visibility='onchange') evaluation_type = fields.Selection( related='exam_session_id.evaluation_type', store=True, track_visibility='onchange') name = fields.Char("Name", size=254, required=True, track_visibility='onchange') result_date = fields.Date( 'Result Date', required=True, default=fields.Date.today(), track_visibility='onchange') grade_ids = fields.Many2many( 'op.grade.configuration', string='Grade Configuration') state = fields.Selection( [('draft', 'Draft'), ('result_generated', 'Result Generated')], 'State', default='draft', track_visibility='onchange') @api.multi @api.constrains('exam_session_id') def _check_exam_session(self): for record in self: for exam in record.exam_session_id.exam_ids: if exam.state != 'done': raise ValidationError( _('All subject exam should be done.')) @api.multi @api.constrains('grade_ids') def _check_min_max_per(self): for record in self: count = 0 for grade in record.grade_ids: for sub_grade in record.grade_ids: if grade != sub_grade: if (sub_grade.min_per <= grade.min_per and sub_grade.max_per >= grade.min_per) or \ (sub_grade.min_per <= grade.max_per and sub_grade.max_per >= grade.max_per): count += 1 if count > 0: raise ValidationError( _('Percentage range conflict with other record.')) @api.multi def generate_result(self): for record in self: marksheet_reg_id = self.env['op.marksheet.register'].create({ 'name': 'Mark Sheet for %s' % record.exam_session_id.name, 'exam_session_id': record.exam_session_id.id, 'generated_date': fields.Date.today(), 'generated_by': self.env.uid, 'status': 'draft', 'result_template_id': record.id }) student_dict = {} for exam in record.exam_session_id.exam_ids: for attendee in exam.attendees_line: result_line_id = self.env['op.result.line'].create({ 'student_id': attendee.student_id.id, 'exam_id': exam.id, 'marks': str(attendee.marks and attendee.marks or 0), }) if attendee.student_id.id not in student_dict: student_dict[attendee.student_id.id] = [] student_dict[attendee.student_id.id].append(result_line_id) for student in student_dict: marksheet_line_id = self.env['op.marksheet.line'].create({ 'student_id': student, 'marksheet_reg_id': marksheet_reg_id.id, }) for result_line in student_dict[student]: result_line.marksheet_line_id = marksheet_line_id record.state = 'result_generated'
class DateRangeGenerator(models.TransientModel): _name = 'date.range.generator' @api.model def _default_company(self): return self.env['res.company']._company_default_get('date.range') name_prefix = fields.Char('Range name prefix', required=True) date_start = fields.Date(strint='Start date', required=True) type_id = fields.Many2one(comodel_name='date.range.type', string='Type', required=True, domain="['|', ('company_id', '=', company_id), " "('company_id', '=', False)]", ondelete='cascade') company_id = fields.Many2one(comodel_name='res.company', string='Company', default=_default_company) unit_of_time = fields.Selection([(YEARLY, 'years'), (MONTHLY, 'months'), (WEEKLY, 'weeks'), (DAILY, 'days')], required=True) duration_count = fields.Integer('Duration', required=True) count = fields.Integer(string="Number of ranges to generate", required=True) @api.multi def _compute_date_ranges(self): self.ensure_one() vals = rrule(freq=self.unit_of_time, interval=self.duration_count, dtstart=fields.Date.from_string(self.date_start), count=self.count + 1) vals = list(vals) date_ranges = [] count_digits = len(str(self.count)) for idx, dt_start in enumerate(vals[:-1]): date_start = fields.Date.to_string(dt_start.date()) # always remove 1 day for the date_end since range limits are # inclusive dt_end = vals[idx + 1].date() - relativedelta(days=1) date_end = fields.Date.to_string(dt_end) date_ranges.append({ 'name': '%s%0*d' % (self.name_prefix, count_digits, idx + 1), 'date_start': date_start, 'date_end': date_end, 'type_id': self.type_id.id, 'company_id': self.company_id.id }) return date_ranges @api.onchange('company_id') def _onchange_company_id(self): if self.company_id and self.type_id.company_id and \ self.type_id.company_id != self.company_id: self._cache.update( self._convert_to_cache({'type_id': False}, update=True)) @api.multi @api.constrains('company_id', 'type_id') def _check_company_id_type_id(self): for rec in self.sudo(): if rec.company_id and rec.type_id.company_id and\ rec.company_id != rec.type_id.company_id: raise ValidationError( _('The Company in the Date Range Generator and in ' 'Date Range Type must be the same.')) @api.multi def action_apply(self): date_ranges = self._compute_date_ranges() if date_ranges: for dr in date_ranges: self.env['date.range'].create(dr) return self.env['ir.actions.act_window'].for_xml_id( module='date_range', xml_id='date_range_action')
class AccountAnalyticDefault(models.Model): _name = "account.analytic.default" _description = "Analytic Distribution" _rec_name = "analytic_id" _order = "sequence" sequence = fields.Integer( string='Sequence', help= "Gives the sequence order when displaying a list of analytic distribution" ) analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') product_id = fields.Many2one( 'product.product', string='Product', ondelete='cascade', help= "Select a product which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this product, it will automatically take this as an analytic account)" ) partner_id = fields.Many2one( 'res.partner', string='Partner', ondelete='cascade', help= "Select a partner which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this partner, it will automatically take this as an analytic account)" ) account_id = fields.Many2one( 'account.account', string='Account', ondelete='cascade', help= "Select an accounting account which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this account, it will automatically take this as an analytic account)" ) user_id = fields.Many2one( 'res.users', string='User', ondelete='cascade', help= "Select a user which will use analytic account specified in analytic default." ) company_id = fields.Many2one( 'res.company', string='Company', ondelete='cascade', help= "Select a company which will use analytic account specified in analytic default (e.g. create new customer invoice or Sales order if we select this company, it will automatically take this as an analytic account)" ) date_start = fields.Date( string='Start Date', help="Default start date for this Analytic Account.") date_stop = fields.Date(string='End Date', help="Default end date for this Analytic Account.") @api.constrains('analytic_id', 'analytic_tag_ids') def _check_account_or_tags(self): if any(not default.analytic_id and not default.analytic_tag_ids for default in self): raise ValidationError( _('An analytic default requires at least an analytic account or an analytic tag.' )) @api.model def account_get(self, product_id=None, partner_id=None, account_id=None, user_id=None, date=None, company_id=None): domain = [] if product_id: domain += ['|', ('product_id', '=', product_id)] domain += [('product_id', '=', False)] if partner_id: domain += ['|', ('partner_id', '=', partner_id)] domain += [('partner_id', '=', False)] if account_id: domain += ['|', ('account_id', '=', account_id)] domain += [('account_id', '=', False)] if company_id: domain += ['|', ('company_id', '=', company_id)] domain += [('company_id', '=', False)] if user_id: domain += ['|', ('user_id', '=', user_id)] domain += [('user_id', '=', False)] if date: domain += [ '|', ('date_start', '<=', date), ('date_start', '=', False) ] domain += [ '|', ('date_stop', '>=', date), ('date_stop', '=', False) ] best_index = -1 res = self.env['account.analytic.default'] for rec in self.search(domain): index = 0 if rec.product_id: index += 1 if rec.partner_id: index += 1 if rec.account_id: index += 1 if rec.company_id: index += 1 if rec.user_id: index += 1 if rec.date_start: index += 1 if rec.date_stop: index += 1 if index > best_index: res = rec best_index = index return res
class HrExpenseSheet(models.Model): _name = "hr.expense.sheet" _inherit = ['mail.thread'] _description = "Expense Report" _order = "accounting_date desc, id desc" name = fields.Char(string='Expense Report Summary', required=True) expense_line_ids = fields.One2many('hr.expense', 'sheet_id', string='Expense Lines', states={ 'approve': [('readonly', True)], 'done': [('readonly', True)], 'post': [('readonly', True)] }, copy=False) state = fields.Selection([('submit', 'Submitted'), ('approve', 'Approved'), ('post', 'Posted'), ('done', 'Paid'), ('cancel', 'Refused')], string='Status', index=True, readonly=True, track_visibility='onchange', copy=False, default='submit', required=True, help='Expense Report State') employee_id = fields.Many2one( 'hr.employee', string="Employee", required=True, readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1)) address_id = fields.Many2one('res.partner', string="Employee Home Address") payment_mode = fields.Selection( [("own_account", "Employee (to reimburse)"), ("company_account", "Company")], related='expense_line_ids.payment_mode', default='own_account', readonly=True, string="Payment By") responsible_id = fields.Many2one('res.users', 'Validation By', readonly=True, copy=False, states={ 'submit': [('readonly', False)], 'submit': [('readonly', False)] }) total_amount = fields.Float(string='Total Amount', store=True, compute='_compute_amount', digits=dp.get_precision('Account')) company_id = fields.Many2one('res.company', string='Company', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id) currency_id = fields.Many2one( 'res.currency', string='Currency', readonly=True, states={'submit': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id) attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments') journal_id = fields.Many2one( 'account.journal', string='Expense Journal', states={ 'done': [('readonly', True)], 'post': [('readonly', True)] }, default=lambda self: self.env['ir.model.data']. xmlid_to_object('hr_expense.hr_expense_account_journal') or self.env[ 'account.journal'].search([('type', '=', 'purchase')], limit=1), help="The journal used when the expense is done.") bank_journal_id = fields.Many2one( 'account.journal', string='Bank Journal', states={ 'done': [('readonly', True)], 'post': [('readonly', True)] }, default=lambda self: self.env['account.journal'].search( [('type', 'in', ['cash', 'bank'])], limit=1), help="The payment method used when the expense is paid by the company." ) accounting_date = fields.Date(string="Date") account_move_id = fields.Many2one('account.move', string='Journal Entry', ondelete='restrict', copy=False) department_id = fields.Many2one('hr.department', string='Department', states={ 'post': [('readonly', True)], 'done': [('readonly', True)] }) @api.multi def check_consistency(self): for rec in self: expense_lines = rec.expense_line_ids if not expense_lines: continue if any(expense.employee_id != rec.employee_id for expense in expense_lines): raise UserError( _("Expenses must belong to the same Employee.")) if any(expense.payment_mode != expense_lines[0].payment_mode for expense in expense_lines): raise UserError( _("Expenses must have been paid by the same entity (Company or employee)" )) @api.model def create(self, vals): self._create_set_followers(vals) sheet = super(HrExpenseSheet, self).create(vals) sheet.check_consistency() return sheet @api.multi def write(self, vals): res = super(HrExpenseSheet, self).write(vals) self.check_consistency() if vals.get('employee_id'): self._add_followers() return res @api.multi def unlink(self): for expense in self: if expense.state in ['post', 'done']: raise UserError( _('You cannot delete a posted or paid expense.')) super(HrExpenseSheet, self).unlink() @api.multi def set_to_paid(self): self.write({'state': 'done'}) @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'approve': return 'hr_expense.mt_expense_approved' elif 'state' in init_values and self.state == 'submit': return 'hr_expense.mt_expense_confirmed' elif 'state' in init_values and self.state == 'cancel': return 'hr_expense.mt_expense_refused' elif 'state' in init_values and self.state == 'done': return 'hr_expense.mt_expense_paid' return super(HrExpenseSheet, self)._track_subtype(init_values) def _get_users_to_subscribe(self, employee=False): users = self.env['res.users'] employee = employee or self.employee_id if employee.user_id: users |= employee.user_id if employee.parent_id: users |= employee.parent_id.user_id if employee.department_id and employee.department_id.manager_id and employee.parent_id != employee.department_id.manager_id: users |= employee.department_id.manager_id.user_id return users def _add_followers(self): users = self._get_users_to_subscribe() self.message_subscribe_users(user_ids=users.ids) @api.model def _create_set_followers(self, values): # Add the followers at creation, so they can be notified employee_id = values.get('employee_id') if not employee_id: return employee = self.env['hr.employee'].browse(employee_id) users = self._get_users_to_subscribe(employee=employee) - self.env.user values['message_follower_ids'] = [] MailFollowers = self.env['mail.followers'] for partner in users.mapped('partner_id'): values[ 'message_follower_ids'] += MailFollowers._add_follower_command( self._name, [], {partner.id: None}, {})[0] @api.onchange('employee_id') def _onchange_employee_id(self): self.address_id = self.employee_id.address_home_id self.department_id = self.employee_id.department_id @api.one @api.depends('expense_line_ids', 'expense_line_ids.total_amount', 'expense_line_ids.currency_id') def _compute_amount(self): total_amount = 0.0 for expense in self.expense_line_ids: total_amount += expense.currency_id.with_context( date=expense.date, company_id=expense.company_id.id).compute( expense.total_amount, self.currency_id) self.total_amount = total_amount @api.one def _compute_attachment_number(self): self.attachment_number = sum( self.expense_line_ids.mapped('attachment_number')) @api.multi def refuse_sheet(self, reason): if not self.user_has_groups('hr_expense.group_hr_expense_user'): raise UserError(_("Only HR Officers can refuse expenses")) self.write({'state': 'cancel'}) for sheet in self: sheet.message_post_with_view( 'hr_expense.hr_expense_template_refuse_reason', values={ 'reason': reason, 'is_sheet': True, 'name': self.name }) @api.multi def approve_expense_sheets(self): if not self.user_has_groups('hr_expense.group_hr_expense_user'): raise UserError(_("Only HR Officers can approve expenses")) self.write({'state': 'approve', 'responsible_id': self.env.user.id}) @api.multi def paid_expense_sheets(self): self.write({'state': 'done'}) @api.multi def reset_expense_sheets(self): self.mapped('expense_line_ids').write({'is_refused': False}) return self.write({'state': 'submit'}) @api.multi def action_sheet_move_create(self): if any(sheet.state != 'approve' for sheet in self): raise UserError( _("You can only generate accounting entry for approved expense(s)." )) if any(not sheet.journal_id for sheet in self): raise UserError( _("Expenses must have an expense journal specified to generate accounting entries." )) expense_line_ids = self.mapped('expense_line_ids')\ .filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.user.company_id.currency_id).rounding)) res = expense_line_ids.action_move_create() if not self.accounting_date: self.accounting_date = self.account_move_id.date if self.payment_mode == 'own_account' and expense_line_ids: self.write({'state': 'post'}) else: self.write({'state': 'done'}) return res @api.multi def action_get_attachment_view(self): res = self.env['ir.actions.act_window'].for_xml_id( 'base', 'action_attachment') res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.expense_line_ids.ids)] res['context'] = { 'default_res_model': 'hr.expense.sheet', 'default_res_id': self.id, 'create': False, 'edit': False, } return res @api.one @api.constrains('expense_line_ids', 'employee_id') def _check_employee(self): employee_ids = self.expense_line_ids.mapped('employee_id') if len(employee_ids) > 1 or (len(employee_ids) == 1 and employee_ids != self.employee_id): raise ValidationError( _('You cannot add expense lines of another employee.')) @api.one @api.constrains('expense_line_ids') def _check_payment_mode(self): payment_mode = set(self.expense_line_ids.mapped('payment_mode')) if len(payment_mode) > 1: raise ValidationError( _('You cannot report expenses with different payment modes.'))
class HrExpense(models.Model): _name = "hr.expense" _inherit = ['mail.thread'] _description = "Expense" _order = "date desc, id desc" name = fields.Char(string='Expense Description', readonly=True, required=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }) date = fields.Date(readonly=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }, default=fields.Date.context_today, string="Expense Date") employee_id = fields.Many2one( 'hr.employee', string="Employee", required=True, readonly=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }, default=lambda self: self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1)) product_id = fields.Many2one('product.product', string='Product', readonly=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }, domain=[('can_be_expensed', '=', True)], required=True) product_uom_id = fields.Many2one( 'product.uom', string='Unit of Measure', required=True, readonly=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }, default=lambda self: self.env['product.uom'].search( [], limit=1, order='id')) unit_amount = fields.Float(string='Unit Price', readonly=True, required=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }, digits=dp.get_precision('Product Price')) quantity = fields.Float(required=True, readonly=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }, digits=dp.get_precision('Product Unit of Measure'), default=1) tax_ids = fields.Many2many('account.tax', 'expense_tax', 'expense_id', 'tax_id', string='Taxes', states={ 'done': [('readonly', True)], 'post': [('readonly', True)] }) untaxed_amount = fields.Float(string='Subtotal', store=True, compute='_compute_amount', digits=dp.get_precision('Account')) total_amount = fields.Float(string='Total', store=True, compute='_compute_amount', digits=dp.get_precision('Account')) company_id = fields.Many2one('res.company', string='Company', readonly=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }, default=lambda self: self.env.user.company_id) currency_id = fields.Many2one( 'res.currency', string='Currency', readonly=True, states={ 'draft': [('readonly', False)], 'refused': [('readonly', False)] }, default=lambda self: self.env.user.company_id.currency_id) analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', states={ 'post': [('readonly', True)], 'done': [('readonly', True)] }, oldname='analytic_account') account_id = fields.Many2one( 'account.account', string='Account', states={ 'post': [('readonly', True)], 'done': [('readonly', True)] }, default=lambda self: self.env['ir.property'].get( 'property_account_expense_categ_id', 'product.category'), help="An expense account is expected") description = fields.Text() payment_mode = fields.Selection( [("own_account", "Employee (to reimburse)"), ("company_account", "Company")], default='own_account', states={ 'done': [('readonly', True)], 'post': [('readonly', True)], 'submitted': [('readonly', True)] }, string="Payment By") attachment_number = fields.Integer(compute='_compute_attachment_number', string='Number of Attachments') state = fields.Selection([('draft', 'To Submit'), ('reported', 'Reported'), ('done', 'Posted'), ('refused', 'Refused')], compute='_compute_state', string='Status', copy=False, index=True, readonly=True, store=True, help="Status of the expense.") sheet_id = fields.Many2one('hr.expense.sheet', string="Expense Report", readonly=True, copy=False) reference = fields.Char(string="Bill Reference") is_refused = fields.Boolean( string="Explicitely Refused by manager or acccountant", readonly=True, copy=False) @api.depends('sheet_id', 'sheet_id.account_move_id', 'sheet_id.state') def _compute_state(self): for expense in self: if not expense.sheet_id: expense.state = "draft" elif expense.sheet_id.state == "cancel": expense.state = "refused" elif not expense.sheet_id.account_move_id: expense.state = "reported" else: expense.state = "done" @api.depends('quantity', 'unit_amount', 'tax_ids', 'currency_id') def _compute_amount(self): for expense in self: expense.untaxed_amount = expense.unit_amount * expense.quantity taxes = expense.tax_ids.compute_all( expense.unit_amount, expense.currency_id, expense.quantity, expense.product_id, expense.employee_id.user_id.partner_id) expense.total_amount = taxes.get('total_included') @api.multi def _compute_attachment_number(self): attachment_data = self.env['ir.attachment'].read_group( [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)], ['res_id'], ['res_id']) attachment = dict( (data['res_id'], data['res_id_count']) for data in attachment_data) for expense in self: expense.attachment_number = attachment.get(expense.id, 0) @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: if not self.name: self.name = self.product_id.display_name or '' self.unit_amount = self.product_id.price_compute('standard_price')[ self.product_id.id] self.product_uom_id = self.product_id.uom_id self.tax_ids = self.product_id.supplier_taxes_id account = self.product_id.product_tmpl_id._get_product_accounts( )['expense'] if account: self.account_id = account @api.onchange('product_uom_id') def _onchange_product_uom_id(self): if self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id: raise UserError( _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure' )) @api.multi def view_sheet(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'hr.expense.sheet', 'target': 'current', 'res_id': self.sheet_id.id } @api.multi def submit_expenses(self): if any(expense.state != 'draft' for expense in self): raise UserError(_("You cannot report twice the same line!")) if len(self.mapped('employee_id')) != 1: raise UserError( _("You cannot report expenses for different employees in the same report!" )) return { 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'hr.expense.sheet', 'target': 'current', 'context': { 'default_expense_line_ids': [line.id for line in self], 'default_employee_id': self[0].employee_id.id, 'default_name': self[0].name if len(self.ids) == 1 else '' } } def _prepare_move_line(self, line): ''' This function prepares move line of account.move related to an expense ''' partner_id = self.employee_id.address_home_id.commercial_partner_id.id return { 'date_maturity': line.get('date_maturity'), 'partner_id': partner_id, 'name': line['name'][:64], 'debit': line['price'] > 0 and line['price'], 'credit': line['price'] < 0 and -line['price'], 'account_id': line['account_id'], 'analytic_line_ids': line.get('analytic_line_ids'), 'amount_currency': line['price'] > 0 and abs(line.get('amount_currency')) or -abs(line.get('amount_currency')), 'currency_id': line.get('currency_id'), 'tax_line_id': line.get('tax_line_id'), 'tax_ids': line.get('tax_ids'), 'quantity': line.get('quantity', 1.00), 'product_id': line.get('product_id'), 'product_uom_id': line.get('uom_id'), 'analytic_account_id': line.get('analytic_account_id'), 'payment_id': line.get('payment_id'), 'expense_id': line.get('expense_id'), } @api.multi def _compute_expense_totals(self, company_currency, account_move_lines, move_date): ''' internal method used for computation of total amount of an expense in the company currency and in the expense currency, given the account_move_lines that will be created. It also do some small transformations at these account_move_lines (for multi-currency purposes) :param account_move_lines: list of dict :rtype: tuple of 3 elements (a, b ,c) a: total in company currency b: total in hr.expense currency c: account_move_lines potentially modified ''' self.ensure_one() total = 0.0 total_currency = 0.0 for line in account_move_lines: line['currency_id'] = False line['amount_currency'] = False if self.currency_id != company_currency: line['currency_id'] = self.currency_id.id line['amount_currency'] = line['price'] line['price'] = self.currency_id.with_context( date=move_date or fields.Date.context_today(self)).compute( line['price'], company_currency) total -= line['price'] total_currency -= line['amount_currency'] or line['price'] return total, total_currency, account_move_lines @api.multi def action_move_create(self): ''' main function that is called when trying to create the accounting entries related to an expense ''' move_group_by_sheet = {} for expense in self: journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id #create the move that will contain the accounting entries acc_date = expense.sheet_id.accounting_date or expense.date if not expense.sheet_id.id in move_group_by_sheet: move = self.env['account.move'].create({ 'journal_id': journal.id, 'company_id': self.env.user.company_id.id, 'date': acc_date, 'ref': expense.sheet_id.name, # force the name to the default value, to avoid an eventual 'default_name' in the context # to set it to '' which cause no number to be given to the account.move when posted. 'name': '/', }) move_group_by_sheet[expense.sheet_id.id] = move else: move = move_group_by_sheet[expense.sheet_id.id] company_currency = expense.company_id.currency_id diff_currency_p = expense.currency_id != company_currency #one account.move.line per expense (+taxes..) move_lines = expense._move_line_get() #create one more move line, a counterline for the total on payable account payment_id = False total, total_currency, move_lines = expense._compute_expense_totals( company_currency, move_lines, acc_date) if expense.payment_mode == 'company_account': if not expense.sheet_id.bank_journal_id.default_credit_account_id: raise UserError( _("No credit account found for the %s journal, please configure one." ) % (expense.sheet_id.bank_journal_id.name)) emp_account = expense.sheet_id.bank_journal_id.default_credit_account_id.id journal = expense.sheet_id.bank_journal_id #create payment payment_methods = ( total < 0 ) and journal.outbound_payment_method_ids or journal.inbound_payment_method_ids journal_currency = journal.currency_id or journal.company_id.currency_id payment = self.env['account.payment'].create({ 'payment_method_id': payment_methods and payment_methods[0].id or False, 'payment_type': total < 0 and 'outbound' or 'inbound', 'partner_id': expense.employee_id.address_home_id.commercial_partner_id. id, 'partner_type': 'supplier', 'journal_id': journal.id, 'payment_date': expense.date, 'state': 'reconciled', 'currency_id': diff_currency_p and expense.currency_id.id or journal_currency.id, 'amount': diff_currency_p and abs(total_currency) or abs(total), 'name': expense.name, }) payment_id = payment.id else: if not expense.employee_id.address_home_id: raise UserError( _("No Home Address found for the employee %s, please configure one." ) % (expense.employee_id.name)) emp_account = expense.employee_id.address_home_id.property_account_payable_id.id aml_name = expense.employee_id.name + ': ' + expense.name.split( '\n')[0][:64] move_lines.append({ 'type': 'dest', 'name': aml_name, 'price': total, 'account_id': emp_account, 'date_maturity': acc_date, 'amount_currency': diff_currency_p and total_currency or False, 'currency_id': diff_currency_p and expense.currency_id.id or False, 'payment_id': payment_id, 'expense_id': expense.id, }) #convert eml into an osv-valid format lines = [(0, 0, expense._prepare_move_line(x)) for x in move_lines] move.with_context(dont_create_taxes=True).write( {'line_ids': lines}) expense.sheet_id.write({'account_move_id': move.id}) if expense.payment_mode == 'company_account': expense.sheet_id.paid_expense_sheets() for move in move_group_by_sheet.values(): move.post() return True @api.multi def _prepare_move_line_value(self): self.ensure_one() if self.account_id: account = self.account_id elif self.product_id: account = self.product_id.product_tmpl_id._get_product_accounts( )['expense'] if not account: raise UserError( _("No Expense account found for the product %s (or for its category), please configure one." ) % (self.product_id.name)) else: account = self.env['ir.property'].with_context( force_company=self.company_id.id).get( 'property_account_expense_categ_id', 'product.category') if not account: raise UserError( _('Please configure Default Expense account for Product expense: `property_account_expense_categ_id`.' )) aml_name = self.employee_id.name + ': ' + self.name.split('\n')[0][:64] move_line = { 'type': 'src', 'name': aml_name, 'price_unit': self.unit_amount, 'quantity': self.quantity, 'price': self.total_amount, 'account_id': account.id, 'product_id': self.product_id.id, 'uom_id': self.product_uom_id.id, 'analytic_account_id': self.analytic_account_id.id, 'expense_id': self.id, } return move_line @api.multi def _move_line_get(self): account_move = [] for expense in self: move_line = expense._prepare_move_line_value() account_move.append(move_line) # Calculate tax lines and adjust base line taxes = expense.tax_ids.with_context(round=True).compute_all( expense.unit_amount, expense.currency_id, expense.quantity, expense.product_id) account_move[-1]['price'] = taxes['total_excluded'] account_move[-1]['tax_ids'] = [(6, 0, expense.tax_ids.ids)] for tax in taxes['taxes']: account_move.append({ 'type': 'tax', 'name': tax['name'], 'price_unit': tax['amount'], 'quantity': 1, 'price': tax['amount'], 'account_id': tax['account_id'] or move_line['account_id'], 'tax_line_id': tax['id'], 'expense_id': expense.id, }) return account_move @api.multi def unlink(self): for expense in self: if expense.state in ['done']: raise UserError(_('You cannot delete a posted expense.')) super(HrExpense, self).unlink() @api.multi def action_get_attachment_view(self): self.ensure_one() res = self.env['ir.actions.act_window'].for_xml_id( 'base', 'action_attachment') res['domain'] = [('res_model', '=', 'hr.expense'), ('res_id', 'in', self.ids)] res['context'] = { 'default_res_model': 'hr.expense', 'default_res_id': self.id } return res @api.multi def refuse_expense(self, reason): self.write({'is_refused': True}) self.sheet_id.write({'state': 'cancel'}) self.sheet_id.message_post_with_view( 'hr_expense.hr_expense_template_refuse_reason', values={ 'reason': reason, 'is_sheet': False, 'name': self.name }) @api.model def get_empty_list_help(self, help_message): if help_message: use_mailgateway = self.env['ir.config_parameter'].sudo().get_param( 'hr_expense.use_mailgateway') alias_record = use_mailgateway and self.env.ref( 'hr_expense.mail_alias_expense') or False if alias_record and alias_record.alias_domain and alias_record.alias_name: link = "<a id='o_mail_test' href='mailto:%(email)s?subject=Lunch%%20with%%20customer%%3A%%20%%2412.32'>%(email)s</a>" % { 'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain) } return '<p class="oe_view_nocontent_create">%s<br/>%s</p>%s' % ( _('Click to add a new expense,'), _('or send receipts by email to %s.') % (link, ), help_message) return super(HrExpense, self).get_empty_list_help(help_message) @api.model def message_new(self, msg_dict, custom_values=None): if custom_values is None: custom_values = {} email_address = email_split(msg_dict.get('email_from', False))[0] employee = self.env['hr.employee'].search([ '|', ('work_email', 'ilike', email_address), ('user_id.email', 'ilike', email_address) ], limit=1) expense_description = msg_dict.get('subject', '') # Match the first occurence of '[]' in the string and extract the content inside it # Example: '[foo] bar (baz)' becomes 'foo'. This is potentially the product code # of the product to encode on the expense. If not, take the default product instead # which is 'Fixed Cost' default_product = self.env.ref('hr_expense.product_product_fixed_cost') pattern = '\[([^)]*)\]' product_code = re.search(pattern, expense_description) if product_code is None: product = default_product else: expense_description = expense_description.replace( product_code.group(), '') product = self.env['product.product'].search([ ('default_code', 'ilike', product_code.group(1)) ]) or default_product pattern = '[-+]?(\d+(\.\d*)?|\.\d+)([eE][-+]?\d+)?' # Match the last occurence of a float in the string # Example: '[foo] 50.3 bar 34.5' becomes '34.5'. This is potentially the price # to encode on the expense. If not, take 1.0 instead expense_price = re.findall(pattern, expense_description) # TODO: International formatting if not expense_price: price = 1.0 else: price = expense_price[-1][0] expense_description = expense_description.replace(price, '') try: price = float(price) except ValueError: price = 1.0 custom_values.update({ 'name': expense_description.strip(), 'employee_id': employee.id, 'product_id': product.id, 'product_uom_id': product.uom_id.id, 'quantity': 1, 'unit_amount': price, 'company_id': employee.company_id.id, }) return super(HrExpense, self).message_new(msg_dict, custom_values)
class ProfitabilityAnalysis(models.Model): _name = "project.profitability.report" _description = "Project Profitability Report" _order = 'project_id, sale_line_id' _auto = False analytic_account_id = fields.Many2one('account.analytic.account', string='Analytic Account', readonly=True) project_id = fields.Many2one('project.project', string='Project', readonly=True) currency_id = fields.Many2one('res.currency', string='Project Currency', readonly=True) company_id = fields.Many2one('res.company', string='Project Company', readonly=True) user_id = fields.Many2one('res.users', string='Project Manager', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) line_date = fields.Date("Date", readonly=True) # cost timesheet_unit_amount = fields.Float("Timesheet Duration", digits=(16, 2), readonly=True, group_operator="sum") timesheet_cost = fields.Float("Timesheet Cost", digits=(16, 2), readonly=True, group_operator="sum") expense_cost = fields.Float("Other Costs", digits=(16, 2), readonly=True, group_operator="sum") # sale revenue order_confirmation_date = fields.Datetime('Sales Order Confirmation Date', readonly=True) sale_line_id = fields.Many2one('sale.order.line', string='Sale Order Line', readonly=True) sale_order_id = fields.Many2one('sale.order', string='Sale Order', readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Invoice", digits=(16, 2), readonly=True, group_operator="sum") amount_untaxed_invoiced = fields.Float("Untaxed Amount Invoiced", digits=(16, 2), readonly=True, group_operator="sum") expense_amount_untaxed_to_invoice = fields.Float("Untaxed Amount to Re-invoice", digits=(16, 2), readonly=True, group_operator="sum") expense_amount_untaxed_invoiced = fields.Float("Untaxed Amount Re-invoiced", digits=(16, 2), readonly=True, group_operator="sum") other_revenues = fields.Float("Other Revenues", digits=(16, 2), readonly=True, group_operator="sum", help="All revenues that are not from timesheets and that are linked to the analytic account of the project.") margin = fields.Float("Margin", digits=(16, 2), readonly=True, group_operator="sum") _depends = { 'sale.order.line': [ 'order_id', 'invoice_status', 'price_reduce', 'product_id', 'qty_invoiced', 'untaxed_amount_invoiced', 'untaxed_amount_to_invoice', 'currency_id', 'company_id', 'is_downpayment', 'project_id', 'task_id', 'qty_delivered_method', ], 'sale.order': [ 'date_order', 'user_id', 'partner_id', 'currency_id', 'analytic_account_id', 'order_line', 'invoice_status', 'amount_untaxed', 'currency_rate', 'company_id', 'project_id', ], } def init(self): tools.drop_view_if_exists(self._cr, self._table) query = """ CREATE VIEW %s AS ( SELECT sub.id as id, sub.project_id as project_id, sub.user_id as user_id, sub.sale_line_id as sale_line_id, sub.analytic_account_id as analytic_account_id, sub.partner_id as partner_id, sub.company_id as company_id, sub.currency_id as currency_id, sub.sale_order_id as sale_order_id, sub.order_confirmation_date as order_confirmation_date, sub.product_id as product_id, sub.sale_qty_delivered_method as sale_qty_delivered_method, sub.expense_amount_untaxed_to_invoice as expense_amount_untaxed_to_invoice, sub.expense_amount_untaxed_invoiced as expense_amount_untaxed_invoiced, sub.amount_untaxed_to_invoice as amount_untaxed_to_invoice, sub.amount_untaxed_invoiced as amount_untaxed_invoiced, sub.timesheet_unit_amount as timesheet_unit_amount, sub.timesheet_cost as timesheet_cost, sub.expense_cost as expense_cost, sub.other_revenues as other_revenues, sub.line_date as line_date, (sub.expense_amount_untaxed_to_invoice + sub.expense_amount_untaxed_invoiced + sub.amount_untaxed_to_invoice + sub.amount_untaxed_invoiced + sub.other_revenues + sub.timesheet_cost + sub.expense_cost) as margin FROM ( SELECT ROW_NUMBER() OVER (ORDER BY P.id, SOL.id) AS id, P.id AS project_id, P.user_id AS user_id, SOL.id AS sale_line_id, P.analytic_account_id AS analytic_account_id, P.partner_id AS partner_id, C.id AS company_id, C.currency_id AS currency_id, S.id AS sale_order_id, S.date_order AS order_confirmation_date, SOL.product_id AS product_id, SOL.qty_delivered_method AS sale_qty_delivered_method, COST_SUMMARY.expense_amount_untaxed_to_invoice AS expense_amount_untaxed_to_invoice, COST_SUMMARY.expense_amount_untaxed_invoiced AS expense_amount_untaxed_invoiced, COST_SUMMARY.amount_untaxed_to_invoice AS amount_untaxed_to_invoice, COST_SUMMARY.amount_untaxed_invoiced AS amount_untaxed_invoiced, COST_SUMMARY.timesheet_unit_amount AS timesheet_unit_amount, COST_SUMMARY.timesheet_cost AS timesheet_cost, COST_SUMMARY.expense_cost AS expense_cost, COST_SUMMARY.other_revenues AS other_revenues, COST_SUMMARY.line_date::date AS line_date FROM project_project P JOIN res_company C ON C.id = P.company_id LEFT JOIN ( -- Each costs and revenues will be retrieved individually by sub-requests -- This is required to able to get the date SELECT project_id, analytic_account_id, sale_line_id, SUM(timesheet_unit_amount) AS timesheet_unit_amount, SUM(timesheet_cost) AS timesheet_cost, SUM(expense_cost) AS expense_cost, SUM(other_revenues) AS other_revenues, SUM(expense_amount_untaxed_to_invoice) AS expense_amount_untaxed_to_invoice, SUM(expense_amount_untaxed_invoiced) AS expense_amount_untaxed_invoiced, SUM(amount_untaxed_to_invoice) AS amount_untaxed_to_invoice, SUM(amount_untaxed_invoiced) AS amount_untaxed_invoiced, line_date AS line_date FROM ( -- Get the timesheet costs SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, TS.so_line AS sale_line_id, TS.unit_amount AS timesheet_unit_amount, TS.amount AS timesheet_cost, 0.0 AS other_revenues, 0.0 AS expense_cost, 0.0 AS expense_amount_untaxed_to_invoice, 0.0 AS expense_amount_untaxed_invoiced, 0.0 AS amount_untaxed_to_invoice, 0.0 AS amount_untaxed_invoiced, TS.date AS line_date FROM account_analytic_line TS, project_project P WHERE TS.project_id IS NOT NULL AND P.id = TS.project_id AND P.active = 't' AND P.allow_timesheets = 't' UNION ALL -- Get the other revenues (products that are not services) SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, AAL.so_line AS sale_line_id, 0.0 AS timesheet_unit_amount, 0.0 AS timesheet_cost, AAL.amount AS other_revenues, 0.0 AS expense_cost, 0.0 AS expense_amount_untaxed_to_invoice, 0.0 AS expense_amount_untaxed_invoiced, 0.0 AS amount_untaxed_to_invoice, 0.0 AS amount_untaxed_invoiced, AAL.date AS line_date FROM project_project P JOIN account_analytic_account AA ON P.analytic_account_id = AA.id JOIN account_analytic_line AAL ON AAL.account_id = AA.id LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.invoice_line_id = AAL.move_id LEFT JOIN sale_order_line SOL ON SOINV.order_line_id = SOL.id LEFT JOIN account_move_line AML ON AAL.move_id = AML.id AND AML.parent_state = 'posted' AND AML.exclude_from_invoice_tab = 'f' -- Check if it's not a Credit Note for a Vendor Bill LEFT JOIN account_move RBILL ON RBILL.id = AML.move_id LEFT JOIN account_move_line BILLL ON BILLL.move_id = RBILL.reversed_entry_id AND BILLL.parent_state = 'posted' AND BILLL.exclude_from_invoice_tab = 'f' AND BILLL.product_id = AML.product_id -- Check if it's not an Invoice reversed by a Credit Note LEFT JOIN account_move RINV ON RINV.reversed_entry_id = AML.move_id LEFT JOIN account_move_line RINVL ON RINVL.move_id = RINV.id AND RINVL.parent_state = 'posted' AND RINVL.exclude_from_invoice_tab = 'f' AND RINVL.product_id = AML.product_id WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' AND BILLL.id IS NULL AND RINVL.id IS NULL AND (SOL.id IS NULL OR (SOL.is_expense IS NOT TRUE AND SOL.is_downpayment IS NOT TRUE AND SOL.is_service IS NOT TRUE)) UNION ALL -- Get the expense costs from account analytic line SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, AAL.so_line AS sale_line_id, 0.0 AS timesheet_unit_amount, 0.0 AS timesheet_cost, 0.0 AS other_revenues, AAL.amount AS expense_cost, 0.0 AS expense_amount_untaxed_to_invoice, 0.0 AS expense_amount_untaxed_invoiced, 0.0 AS amount_untaxed_to_invoice, 0.0 AS amount_untaxed_invoiced, AAL.date AS line_date FROM project_project P JOIN account_analytic_account AA ON P.analytic_account_id = AA.id JOIN account_analytic_line AAL ON AAL.account_id = AA.id LEFT JOIN account_move_line AML ON AAL.move_id = AML.id AND AML.parent_state = 'posted' AND AML.exclude_from_invoice_tab = 'f' -- Check if it's not a Credit Note for an Invoice LEFT JOIN account_move RINV ON RINV.id = AML.move_id LEFT JOIN account_move_line INVL ON INVL.move_id = RINV.reversed_entry_id AND INVL.parent_state = 'posted' AND INVL.exclude_from_invoice_tab = 'f' AND INVL.product_id = AML.product_id -- Check if it's not a Bill reversed by a Credit Note LEFT JOIN account_move RBILL ON RBILL.reversed_entry_id = AML.move_id LEFT JOIN account_move_line RBILLL ON RBILLL.move_id = RBILL.id AND RBILLL.parent_state = 'posted' AND RBILLL.exclude_from_invoice_tab = 'f' AND RBILLL.product_id = AML.product_id -- Check if the AAL is not related to a consumed downpayment (when the SOL is fully invoiced - with downpayment discounted.) LEFT JOIN sale_order_line_invoice_rel SOINVDOWN ON SOINVDOWN.invoice_line_id = AML.id LEFT JOIN sale_order_line SOLDOWN on SOINVDOWN.order_line_id = SOLDOWN.id AND SOLDOWN.is_downpayment = 't' WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND INVL.id IS NULL AND RBILLL.id IS NULL AND SOLDOWN.id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' UNION ALL -- Get the following values: expense amount untaxed to invoice/invoiced, amount untaxed to invoice/invoiced -- These values have to be computed from all the records retrieved just above but grouped by project and sale order line SELECT AMOUNT_UNTAXED.project_id AS project_id, AMOUNT_UNTAXED.analytic_account_id AS analytic_account_id, AMOUNT_UNTAXED.sale_line_id AS sale_line_id, 0.0 AS timesheet_unit_amount, 0.0 AS timesheet_cost, 0.0 AS other_revenues, 0.0 AS expense_cost, CASE WHEN SOL.qty_delivered_method = 'analytic' THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) ELSE 0.0 END AS expense_amount_untaxed_to_invoice, CASE WHEN SOL.qty_delivered_method = 'analytic' AND SOL.invoice_status = 'invoiced' THEN CASE WHEN T.expense_policy = 'sales_price' THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) ELSE -AMOUNT_UNTAXED.expense_cost END ELSE 0.0 END AS expense_amount_untaxed_invoiced, CASE WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_to_invoice / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) ELSE 0.0 END AS amount_untaxed_to_invoice, CASE WHEN SOL.qty_delivered_method IN ('timesheet', 'manual') THEN (SOL.untaxed_amount_invoiced / CASE COALESCE(S.currency_rate, 0) WHEN 0 THEN 1.0 ELSE S.currency_rate END) ELSE 0.0 END AS amount_untaxed_invoiced, S.date_order AS line_date FROM project_project P JOIN res_company C ON C.id = P.company_id LEFT JOIN ( -- Gets SOL linked to timesheets SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, AAL.so_line AS sale_line_id, 0.0 AS expense_cost FROM account_analytic_line AAL, project_project P WHERE AAL.project_id IS NOT NULL AND P.id = AAL.project_id AND P.active = 't' GROUP BY P.id, AAL.so_line UNION -- Service SOL linked to a project task AND not yet timesheeted SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, SOL.id AS sale_line_id, 0.0 AS expense_cost FROM sale_order_line SOL JOIN project_task T ON T.sale_line_id = SOL.id JOIN project_project P ON T.project_id = P.id LEFT JOIN account_analytic_line AAL ON AAL.task_id = T.id WHERE SOL.is_service = 't' AND AAL.id IS NULL -- not timesheeted AND P.active = 't' AND P.allow_timesheets = 't' GROUP BY P.id, SOL.id UNION -- Service SOL linked to project AND not yet timesheeted SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, SOL.id AS sale_line_id, 0.0 AS expense_cost FROM sale_order_line SOL JOIN project_project P ON P.sale_line_id = SOL.id LEFT JOIN account_analytic_line AAL ON AAL.project_id = P.id LEFT JOIN project_task T ON T.sale_line_id = SOL.id WHERE SOL.is_service = 't' AND AAL.id IS NULL -- not timesheeted AND (T.id IS NULL OR T.project_id != P.id) -- not linked to a task in this project AND P.active = 't' AND P.allow_timesheets = 't' GROUP BY P.id, SOL.id UNION -- Service SOL linked to analytic account AND not yet timesheeted SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, SOL.id AS sale_line_id, 0.0 AS expense_cost FROM sale_order_line SOL JOIN sale_order SO ON SO.id = SOL.order_id JOIN account_analytic_account AA ON AA.id = SO.analytic_account_id JOIN project_project P ON P.analytic_account_id = AA.id LEFT JOIN project_project PSOL ON PSOL.sale_line_id = SOL.id LEFT JOIN project_task TSOL ON TSOL.sale_line_id = SOL.id LEFT JOIN account_analytic_line AAL ON AAL.so_line = SOL.id WHERE SOL.is_service = 't' AND AAL.id IS NULL -- not timesheeted AND TSOL.id IS NULL -- not linked to a task AND PSOL.id IS NULL -- not linked to a project AND P.active = 't' AND P.allow_timesheets = 't' GROUP BY P.id, SOL.id UNION SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, AAL.so_line AS sale_line_id, 0.0 AS expense_cost FROM project_project P LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id WHERE AAL.amount > 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' GROUP BY P.id, AA.id, AAL.so_line UNION SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, AAL.so_line AS sale_line_id, SUM(AAL.amount) AS expense_cost FROM project_project P LEFT JOIN account_analytic_account AA ON P.analytic_account_id = AA.id LEFT JOIN account_analytic_line AAL ON AAL.account_id = AA.id WHERE AAL.amount < 0.0 AND AAL.project_id IS NULL AND P.active = 't' AND P.allow_timesheets = 't' GROUP BY P.id, AA.id, AAL.so_line UNION SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, SOLDOWN.id AS sale_line_id, 0.0 AS expense_cost FROM project_project P LEFT JOIN sale_order_line SOL ON P.sale_line_id = SOL.id LEFT JOIN sale_order SO ON SO.id = SOL.order_id OR SO.analytic_account_id = P.analytic_account_id LEFT JOIN sale_order_line SOLDOWN ON SOLDOWN.order_id = SO.id AND SOLDOWN.is_downpayment = 't' LEFT JOIN sale_order_line_invoice_rel SOINV ON SOINV.order_line_id = SOLDOWN.id LEFT JOIN account_move_line INVL ON SOINV.invoice_line_id = INVL.id AND INVL.parent_state = 'posted' AND INVL.exclude_from_invoice_tab = 'f' LEFT JOIN account_move RINV ON INVL.move_id = RINV.reversed_entry_id LEFT JOIN account_move_line RINVL ON RINV.id = RINVL.move_id AND RINVL.parent_state = 'posted' AND RINVL.exclude_from_invoice_tab = 'f' AND RINVL.product_id = SOLDOWN.product_id LEFT JOIN account_analytic_line ANLI ON ANLI.move_id = RINVL.id AND ANLI.amount < 0.0 WHERE ANLI.id IS NULL -- there are no credit note for this downpayment AND P.active = 't' AND P.allow_timesheets = 't' GROUP BY P.id, SOLDOWN.id UNION SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, SOL.id AS sale_line_id, 0.0 AS expense_cost FROM sale_order_line SOL INNER JOIN project_project P ON SOL.project_id = P.id WHERE P.active = 't' AND P.allow_timesheets = 't' UNION SELECT P.id AS project_id, P.analytic_account_id AS analytic_account_id, SOL.id AS sale_line_id, 0.0 AS expense_cost FROM sale_order_line SOL INNER JOIN project_task T ON SOL.task_id = T.id INNER JOIN project_project P ON P.id = T.project_id WHERE P.active = 't' AND P.allow_timesheets = 't' ) AMOUNT_UNTAXED ON AMOUNT_UNTAXED.project_id = P.id LEFT JOIN sale_order_line SOL ON AMOUNT_UNTAXED.sale_line_id = SOL.id LEFT JOIN sale_order S ON SOL.order_id = S.id LEFT JOIN product_product PP on (SOL.product_id = PP.id) LEFT JOIN product_template T on (PP.product_tmpl_id = T.id) WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL ) SUB_COST_SUMMARY GROUP BY project_id, analytic_account_id, sale_line_id, line_date ) COST_SUMMARY ON COST_SUMMARY.project_id = P.id LEFT JOIN sale_order_line SOL ON COST_SUMMARY.sale_line_id = SOL.id LEFT JOIN sale_order S ON SOL.order_id = S.id WHERE P.active = 't' AND P.analytic_account_id IS NOT NULL ) AS sub ) """ % self._table self._cr.execute(query)
class PayslipReport(models.Model): _name = "payslip.report" _description = "Payslip Analysis" _auto = False name = fields.Char(readonly=True) date_from = fields.Date(string='Date From', readonly=True) date_to = fields.Date(string='Date To', readonly=True) year = fields.Char(size=4, readonly=True) month = fields.Selection([('01', 'January'), ('02', 'February'), ('03', 'March'), ('04', 'April'), ('05', 'May'), ('06', 'June'), ('07', 'July'), ('08', 'August'), ('09', 'September'), ('10', 'October'), ('11', 'November'), ('12', 'December')], readonly=True) day = fields.Char(size=128, readonly=True) state = fields.Selection([ ('draft', 'Draft'), ('done', 'Done'), ('cancel', 'Rejected'), ], string='Status', readonly=True) employee_id = fields.Many2one('hr.employee', string='Employee', readonly=True) nbr = fields.Integer(string='# Payslip lines', readonly=True) number = fields.Char(readonly=True) struct_id = fields.Many2one('hr.payroll.structure', string='Structure', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) paid = fields.Boolean(string='Made Payment Order ? ', readonly=True) total = fields.Float(readonly=True) category_id = fields.Many2one('hr.salary.rule.category', string='Category', readonly=True) @api.model_cr def init(self): drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute(""" create or replace view payslip_report as ( select min(l.id) as id, l.name, p.struct_id, p.state, p.date_from, p.date_to, p.number, p.company_id, p.paid, l.category_id, l.employee_id, sum(l.total) as total, to_char(p.date_from, 'YYYY') as year, to_char(p.date_from, 'MM') as month, to_char(p.date_from, 'YYYY-MM-DD') as day, to_char(p.date_to, 'YYYY') as to_year, to_char(p.date_to, 'MM') as to_month, to_char(p.date_to, 'YYYY-MM-DD') as to_day, 1 AS nbr from hr_payslip as p left join hr_payslip_line as l on (p.id=l.slip_id) where l.employee_id IS NOT NULL group by p.number,l.name,p.date_from,p.date_to,p.state,p.company_id,p.paid, l.employee_id,p.struct_id,l.category_id ) """)
class CrossoveredBudgetLines(models.Model): _name = "crossovered.budget.lines" _description = "Budget Line" crossovered_budget_id = fields.Many2one('crossovered.budget', 'Budget', ondelete='cascade', index=True, required=True) analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account') general_budget_id = fields.Many2one('account.budget.post', 'Budgetary Position', required=True) date_from = fields.Date('Start Date', required=True) date_to = fields.Date('End Date', required=True) paid_date = fields.Date('Paid Date') planned_amount = fields.Float('Planned Amount', required=True, digits=0) practical_amount = fields.Float(compute='_compute_practical_amount', string='Practical Amount', digits=0) theoritical_amount = fields.Float(compute='_compute_theoritical_amount', string='Theoretical Amount', digits=0) percentage = fields.Float(compute='_compute_percentage', string='Achievement') company_id = fields.Many2one(related='crossovered_budget_id.company_id', comodel_name='res.company', string='Company', store=True, readonly=True) @api.multi def _compute_practical_amount(self): for line in self: result = 0.0 acc_ids = line.general_budget_id.account_ids.ids date_to = self.env.context.get('wizard_date_to') or line.date_to date_from = self.env.context.get('wizard_date_from') or line.date_from if line.analytic_account_id.id: self.env.cr.execute(""" SELECT SUM(amount) FROM account_analytic_line WHERE account_id=%s AND (date between to_date(%s,'yyyy-mm-dd') AND to_date(%s,'yyyy-mm-dd')) AND general_account_id=ANY(%s)""", (line.analytic_account_id.id, date_from, date_to, acc_ids,)) result = self.env.cr.fetchone()[0] or 0.0 line.practical_amount = result @api.multi def _compute_theoritical_amount(self): today = fields.Datetime.now() for line in self: # Used for the report if self.env.context.get('wizard_date_from') and self.env.context.get('wizard_date_to'): date_from = fields.Datetime.from_string(self.env.context.get('wizard_date_from')) date_to = fields.Datetime.from_string(self.env.context.get('wizard_date_to')) if date_from < fields.Datetime.from_string(line.date_from): date_from = fields.Datetime.from_string(line.date_from) elif date_from > fields.Datetime.from_string(line.date_to): date_from = False if date_to > fields.Datetime.from_string(line.date_to): date_to = fields.Datetime.from_string(line.date_to) elif date_to < fields.Datetime.from_string(line.date_from): date_to = False theo_amt = 0.00 if date_from and date_to: line_timedelta = fields.Datetime.from_string(line.date_to) - fields.Datetime.from_string(line.date_from) elapsed_timedelta = date_to - date_from if elapsed_timedelta.days > 0: theo_amt = (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.planned_amount else: if line.paid_date: if fields.Datetime.from_string(line.date_to) <= fields.Datetime.from_string(line.paid_date): theo_amt = 0.00 else: theo_amt = line.planned_amount else: line_timedelta = fields.Datetime.from_string(line.date_to) - fields.Datetime.from_string(line.date_from) elapsed_timedelta = fields.Datetime.from_string(today) - (fields.Datetime.from_string(line.date_from)) if elapsed_timedelta.days < 0: # If the budget line has not started yet, theoretical amount should be zero theo_amt = 0.00 elif line_timedelta.days > 0 and fields.Datetime.from_string(today) < fields.Datetime.from_string(line.date_to): # If today is between the budget line date_from and date_to theo_amt = (elapsed_timedelta.total_seconds() / line_timedelta.total_seconds()) * line.planned_amount else: theo_amt = line.planned_amount line.theoritical_amount = theo_amt @api.multi def _compute_percentage(self): for line in self: if line.theoritical_amount != 0.00: line.percentage = float((line.practical_amount or 0.0) / line.theoritical_amount) * 100 else: line.percentage = 0.00
class ProductProduct(models.Model): _inherit = "product.product" date_from = fields.Date(compute='_compute_product_margin_fields_values', string='Margin Date From') date_to = fields.Date(compute='_compute_product_margin_fields_values', string='Margin Date To') invoice_state = fields.Selection( compute='_compute_product_margin_fields_values', selection=[('paid', 'Paid'), ('open_paid', 'Open and Paid'), ('draft_open_paid', 'Draft, Open and Paid')], string='Invoice State', readonly=True) sale_avg_price = fields.Float( compute='_compute_product_margin_fields_values', string='Avg. Sale Unit Price', help="Avg. Price in Customer Invoices.") purchase_avg_price = fields.Float( compute='_compute_product_margin_fields_values', string='Avg. Purchase Unit Price', help="Avg. Price in Vendor Bills ") sale_num_invoiced = fields.Float( compute='_compute_product_margin_fields_values', string='# Invoiced in Sale', help="Sum of Quantity in Customer Invoices") purchase_num_invoiced = fields.Float( compute='_compute_product_margin_fields_values', string='# Invoiced in Purchase', help="Sum of Quantity in Vendor Bills") sales_gap = fields.Float(compute='_compute_product_margin_fields_values', string='Sales Gap', help="Expected Sale - Turn Over") purchase_gap = fields.Float( compute='_compute_product_margin_fields_values', string='Purchase Gap', help="Normal Cost - Total Cost") turnover = fields.Float( compute='_compute_product_margin_fields_values', string='Turnover', help= "Sum of Multiplication of Invoice price and quantity of Customer Invoices" ) total_cost = fields.Float( compute='_compute_product_margin_fields_values', string='Total Cost', help= "Sum of Multiplication of Invoice price and quantity of Vendor Bills ") sale_expected = fields.Float( compute='_compute_product_margin_fields_values', string='Expected Sale', help= "Sum of Multiplication of Sale Catalog price and quantity of Customer Invoices" ) normal_cost = fields.Float( compute='_compute_product_margin_fields_values', string='Normal Cost', help="Sum of Multiplication of Cost price and quantity of Vendor Bills" ) total_margin = fields.Float( compute='_compute_product_margin_fields_values', string='Total Margin', help="Turnover - Standard price") expected_margin = fields.Float( compute='_compute_product_margin_fields_values', string='Expected Margin', help="Expected Sale - Normal Cost") total_margin_rate = fields.Float( compute='_compute_product_margin_fields_values', string='Total Margin Rate(%)', help="Total margin * 100 / Turnover") expected_margin_rate = fields.Float( compute='_compute_product_margin_fields_values', string='Expected Margin (%)', help="Expected margin * 100 / Expected Sale") @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): """ Inherit read_group to calculate the sum of the non-stored fields, as it is not automatically done anymore through the XML. """ res = super(ProductProduct, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) fields_list = [ 'turnover', 'sale_avg_price', 'sale_purchase_price', 'sale_num_invoiced', 'purchase_num_invoiced', 'sales_gap', 'purchase_gap', 'total_cost', 'sale_expected', 'normal_cost', 'total_margin', 'expected_margin', 'total_margin_rate', 'expected_margin_rate' ] if any(x in fields for x in fields_list): # Calculate first for every product in which line it needs to be applied re_ind = 0 prod_re = {} tot_products = self.browse([]) for re in res: if re.get('__domain'): products = self.search(re['__domain']) tot_products |= products for prod in products: prod_re[prod.id] = re_ind re_ind += 1 res_val = tot_products._compute_product_margin_fields_values( field_names=[x for x in fields if fields in fields_list]) for key in res_val: for l in res_val[key]: re = res[prod_re[key]] if re.get(l): re[l] += res_val[key][l] else: re[l] = res_val[key][l] return res def _compute_product_margin_fields_values(self, field_names=None): res = {} if field_names is None: field_names = [] for val in self: res[val.id] = {} date_from = self.env.context.get('date_from', time.strftime('%Y-01-01')) date_to = self.env.context.get('date_to', time.strftime('%Y-12-31')) invoice_state = self.env.context.get('invoice_state', 'open_paid') res[val.id]['date_from'] = date_from res[val.id]['date_to'] = date_to res[val.id]['invoice_state'] = invoice_state states = () payment_states = () if invoice_state == 'paid': states = ('posted', ) payment_states = ('paid', ) elif invoice_state == 'open_paid': states = ('posted', ) payment_states = ('not_paid', 'paid') elif invoice_state == 'draft_open_paid': states = ('posted', 'draft') payment_states = ('not_paid', 'paid') company_id = self.env.company.id #Cost price is calculated afterwards as it is a property self.env['account.move.line'].flush([ 'price_unit', 'quantity', 'balance', 'product_id', 'display_type' ]) self.env['account.move'].flush([ 'state', 'payment_state', 'move_type', 'invoice_date', 'company_id' ]) self.env['product.template'].flush(['list_price']) sqlstr = """ WITH currency_rate AS ({}) SELECT SUM( l.price_unit / (CASE COALESCE(cr.rate, 0) WHEN 0 THEN 1.0 ELSE cr.rate END) * l.quantity * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END) * ((100 - l.discount) * 0.01) ) / NULLIF(SUM(l.quantity * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END)), 0) AS avg_unit_price, SUM(l.quantity * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END)) AS num_qty, SUM(ABS(l.balance) * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END)) AS total, SUM(l.quantity * pt.list_price * (CASE WHEN i.move_type IN ('out_invoice', 'in_invoice') THEN 1 ELSE -1 END)) AS sale_expected FROM account_move_line l LEFT JOIN account_move i ON (l.move_id = i.id) LEFT JOIN product_product product ON (product.id=l.product_id) LEFT JOIN product_template pt ON (pt.id = product.product_tmpl_id) left join currency_rate cr on (cr.currency_id = i.currency_id and cr.company_id = i.company_id and cr.date_start <= COALESCE(i.invoice_date, NOW()) and (cr.date_end IS NULL OR cr.date_end > COALESCE(i.invoice_date, NOW()))) WHERE l.product_id = %s AND i.state IN %s AND i.payment_state IN %s AND i.move_type IN %s AND i.invoice_date BETWEEN %s AND %s AND i.company_id = %s AND l.display_type IS NULL AND l.exclude_from_invoice_tab = false """.format(self.env['res.currency']._select_companies_rates()) invoice_types = ('out_invoice', 'out_refund') self.env.cr.execute(sqlstr, (val.id, states, payment_states, invoice_types, date_from, date_to, company_id)) result = self.env.cr.fetchall()[0] res[val.id]['sale_avg_price'] = result[0] and result[0] or 0.0 res[val.id]['sale_num_invoiced'] = result[1] and result[1] or 0.0 res[val.id]['turnover'] = result[2] and result[2] or 0.0 res[val.id]['sale_expected'] = result[3] and result[3] or 0.0 res[val.id]['sales_gap'] = res[val.id]['sale_expected'] - res[ val.id]['turnover'] invoice_types = ('in_invoice', 'in_refund') self.env.cr.execute(sqlstr, (val.id, states, payment_states, invoice_types, date_from, date_to, company_id)) result = self.env.cr.fetchall()[0] res[val.id]['purchase_avg_price'] = result[0] and result[0] or 0.0 res[val. id]['purchase_num_invoiced'] = result[1] and result[1] or 0.0 res[val.id]['total_cost'] = result[2] and result[2] or 0.0 res[val.id]['normal_cost'] = val.standard_price * res[ val.id]['purchase_num_invoiced'] res[val.id]['purchase_gap'] = res[val.id]['normal_cost'] - res[ val.id]['total_cost'] res[val.id]['total_margin'] = res[val.id]['turnover'] - res[ val.id]['total_cost'] res[val.id]['expected_margin'] = res[ val.id]['sale_expected'] - res[val.id]['normal_cost'] res[val.id]['total_margin_rate'] = res[val.id]['turnover'] and res[ val.id]['total_margin'] * 100 / res[val.id]['turnover'] or 0.0 res[val.id]['expected_margin_rate'] = res[ val.id]['sale_expected'] and res[ val.id]['expected_margin'] * 100 / res[ val.id]['sale_expected'] or 0.0 for k, v in res[val.id].items(): setattr(val, k, v) return res
class Contract(models.Model): _name = 'hr.contract' _description = 'Contract' _inherit = ['mail.thread'] name = fields.Char('Contract Reference', required=True) employee_id = fields.Many2one('hr.employee', string='Employee') department_id = fields.Many2one('hr.department', string="Department") type_id = fields.Many2one( 'hr.contract.type', string="Contract Type", required=True, default=lambda self: self.env['hr.contract.type'].search([], limit=1)) job_id = fields.Many2one('hr.job', string='Job Position') date_start = fields.Date('Start Date', required=True, default=fields.Date.today, help="Start date of the contract.") date_end = fields.Date( 'End Date', help="End date of the contract (if it's a fixed-term contract).") trial_date_end = fields.Date( 'End of Trial Period', help="End date of the trial period (if there is one).") resource_calendar_id = fields.Many2one( 'resource.calendar', 'Working Schedule', default=lambda self: self.env['res.company']._company_default_get( ).resource_calendar_id.id) wage = fields.Monetary('Wage', digits=(16, 2), required=True, track_visibility="onchange", help="Employee's monthly gross wage.") advantages = fields.Text('Advantages') notes = fields.Text('Notes') state = fields.Selection([('draft', 'New'), ('open', 'Running'), ('pending', 'To Renew'), ('close', 'Expired'), ('cancel', 'Cancelled')], string='Status', group_expand='_expand_states', track_visibility='onchange', help='Status of the contract', default='draft') company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id) currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True) permit_no = fields.Char('Work Permit No', related="employee_id.permit_no") visa_no = fields.Char('Visa No', related="employee_id.visa_no") visa_expire = fields.Date('Visa Expire Date', related="employee_id.visa_expire") def _expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] @api.onchange('employee_id') def _onchange_employee_id(self): if self.employee_id: self.job_id = self.employee_id.job_id self.department_id = self.employee_id.department_id self.resource_calendar_id = self.employee_id.resource_calendar_id @api.constrains('date_start', 'date_end') def _check_dates(self): if self.filtered(lambda c: c.date_end and c.date_start > c.date_end): raise ValidationError( _('Contract start date must be less than contract end date.')) @api.model def update_state(self): self.search([ ('state', '=', 'open'), '|', '&', ('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=7))), ('date_end', '>=', fields.Date.to_string(date.today() + relativedelta(days=1))), '&', ('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=60))), ('visa_expire', '>=', fields.Date.to_string(date.today() + relativedelta(days=1))), ]).write({'state': 'pending'}) self.search([ ('state', 'in', ('open', 'pending')), '|', ('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))), ('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))), ]).write({'state': 'close'}) return True @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'pending': return 'hr_contract.mt_contract_pending' elif 'state' in init_values and self.state == 'close': return 'hr_contract.mt_contract_close' return super(Contract, self)._track_subtype(init_values)
class SaleOrder(models.Model): """Add several date fields to Sales Orders, computed or user-entered""" _inherit = 'sale.order' commitment_date = fields.Datetime( compute='_compute_commitment_date', string='Commitment Date', store=True, help="Date by which the products are sure to be delivered. This is " "a date that you can promise to the customer, based on the " "Product Lead Times.") requested_date = fields.Datetime( 'Requested Date', readonly=True, states={ 'draft': [('readonly', False)], 'sent': [('readonly', False)] }, copy=False, help="Date by which the customer has requested the items to be " "delivered.\n" "When this Order gets confirmed, the Delivery Order's " "expected date will be computed based on this date and the " "Company's Security Delay.\n" "Leave this field empty if you want the Delivery Order to be " "processed as soon as possible. In that case the expected " "date will be computed using the default method: based on " "the Product Lead Times and the Company's Security Delay.") effective_date = fields.Date( compute='_compute_picking_ids', string='Effective Date', store=True, help="Date on which the first Delivery Order was created.") @api.depends('date_order', 'order_line.customer_lead') def _compute_commitment_date(self): """Compute the commitment date""" for order in self: dates_list = [] order_datetime = fields.Datetime.from_string(order.date_order) for line in order.order_line.filtered( lambda x: x.state != 'cancel'): dt = order_datetime + timedelta(days=line.customer_lead or 0.0) dates_list.append(dt) if dates_list: commit_date = min( dates_list) if order.picking_policy == 'direct' else max( dates_list) order.commitment_date = fields.Datetime.to_string(commit_date) def _compute_picking_ids(self): super(SaleOrder, self)._compute_picking_ids() for order in self: dates_list = [] for pick in order.picking_ids: dates_list.append(fields.Datetime.from_string(pick.date)) if dates_list: order.effective_date = fields.Datetime.to_string( min(dates_list)) @api.onchange('requested_date') def onchange_requested_date(self): """Warn if the requested dates is sooner than the commitment date""" if (self.requested_date and self.commitment_date and self.requested_date < self.commitment_date): return { 'warning': { 'title': _('Requested date is too soon!'), 'message': _("The date requested by the customer is " "sooner than the commitment date. You may be " "unable to honor the customer's request.") } }
class PurchaseRequisition(models.Model): _name = "purchase.requisition" _description = "Purchase Requisition" _inherit = ['mail.thread', 'mail.activity.mixin'] _order = "id desc" def _get_type_id(self): return self.env['purchase.requisition.type'].search([], limit=1) name = fields.Char(string='Reference', required=True, copy=False, default='New', readonly=True) origin = fields.Char(string='Source Document') order_count = fields.Integer(compute='_compute_orders_number', string='Number of Orders') vendor_id = fields.Many2one('res.partner', string="Vendor", domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") type_id = fields.Many2one('purchase.requisition.type', string="Agreement Type", required=True, default=_get_type_id) ordering_date = fields.Date(string="Ordering Date", tracking=True) date_end = fields.Datetime(string='Agreement Deadline', tracking=True) schedule_date = fields.Date(string='Delivery Date', index=True, help="The expected and scheduled delivery date where all the products are received", tracking=True) user_id = fields.Many2one( 'res.users', string='Purchase Representative', default=lambda self: self.env.user, check_company=True) description = fields.Text() company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.company) purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]}) line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True) product_id = fields.Many2one('product.product', related='line_ids.product_id', string='Product', readonly=False) state = fields.Selection(PURCHASE_REQUISITION_STATES, 'Status', tracking=True, required=True, copy=False, default='draft') state_blanket_order = fields.Selection(PURCHASE_REQUISITION_STATES, compute='_set_state') is_quantity_copy = fields.Selection(related='type_id.quantity_copy', readonly=True) currency_id = fields.Many2one('res.currency', 'Currency', required=True, default=lambda self: self.env.company.currency_id.id) @api.depends('state') def _set_state(self): for requisition in self: requisition.state_blanket_order = requisition.state @api.onchange('vendor_id') def _onchange_vendor(self): self = self.with_company(self.company_id) if not self.vendor_id: self.currency_id = self.env.company.currency_id.id else: self.currency_id = self.vendor_id.property_purchase_currency_id.id or self.env.company.currency_id.id requisitions = self.env['purchase.requisition'].search([ ('vendor_id', '=', self.vendor_id.id), ('state', '=', 'ongoing'), ('type_id.quantity_copy', '=', 'none'), ('company_id', '=', self.company_id.id), ]) if any(requisitions): title = _("Warning for %s", self.vendor_id.name) message = _("There is already an open blanket order for this supplier. We suggest you complete this open blanket order, instead of creating a new one.") warning = { 'title': title, 'message': message } return {'warning': warning} @api.depends('purchase_ids') def _compute_orders_number(self): for requisition in self: requisition.order_count = len(requisition.purchase_ids) def action_cancel(self): # try to set all associated quotations to cancel state for requisition in self: for requisition_line in requisition.line_ids: requisition_line.supplier_info_ids.unlink() requisition.purchase_ids.button_cancel() for po in requisition.purchase_ids: po.message_post(body=_('Cancelled by the agreement associated to this quotation.')) self.write({'state': 'cancel'}) def action_in_progress(self): self.ensure_one() if not self.line_ids: raise UserError(_("You cannot confirm agreement '%s' because there is no product line.", self.name)) if self.type_id.quantity_copy == 'none' and self.vendor_id: for requisition_line in self.line_ids: if requisition_line.price_unit <= 0.0: raise UserError(_('You cannot confirm the blanket order without price.')) if requisition_line.product_qty <= 0.0: raise UserError(_('You cannot confirm the blanket order without quantity.')) requisition_line.create_supplier_info() self.write({'state': 'ongoing'}) else: self.write({'state': 'in_progress'}) # Set the sequence number regarding the requisition type if self.name == 'New': if self.is_quantity_copy != 'none': self.name = self.env['ir.sequence'].next_by_code('purchase.requisition.purchase.tender') else: self.name = self.env['ir.sequence'].next_by_code('purchase.requisition.blanket.order') def action_open(self): self.write({'state': 'open'}) def action_draft(self): self.ensure_one() self.name = 'New' self.write({'state': 'draft'}) def action_done(self): """ Generate all purchase order based on selected lines, should only be called on one agreement at a time """ if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')): raise UserError(_('You have to cancel or validate every RfQ before closing the purchase requisition.')) for requisition in self: for requisition_line in requisition.line_ids: requisition_line.supplier_info_ids.unlink() self.write({'state': 'done'}) def unlink(self): if any(requisition.state not in ('draft', 'cancel') for requisition in self): raise UserError(_('You can only delete draft requisitions.')) # Draft requisitions could have some requisition lines. self.mapped('line_ids').unlink() return super(PurchaseRequisition, self).unlink()
class Digest(models.Model): _name = 'digest.digest' _description = 'Digest' # Digest description name = fields.Char(string='Name', required=True, translate=True) user_ids = fields.Many2many('res.users', string='Recipients', domain="[('share', '=', False)]") periodicity = fields.Selection([('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('quarterly', 'Quarterly')], string='Periodicity', default='weekly', required=True) next_run_date = fields.Date(string='Next Send Date') template_id = fields.Many2one( 'mail.template', string='Email Template', domain="[('model','=','digest.digest')]", default=lambda self: self.env.ref('digest.digest_mail_template'), required=True) currency_id = fields.Many2one(related="company_id.currency_id", string='Currency', readonly=False) company_id = fields.Many2one( 'res.company', string='Company', default=lambda self: self.env.user.company_id.id) available_fields = fields.Char(compute='_compute_available_fields') is_subscribed = fields.Boolean('Is user subscribed', compute='_compute_is_subscribed') state = fields.Selection([('activated', 'Activated'), ('deactivated', 'Deactivated')], string='Status', readonly=True, default='activated') # First base-related KPIs kpi_res_users_connected = fields.Boolean('Connected Users') kpi_res_users_connected_value = fields.Integer( compute='_compute_kpi_res_users_connected_value') kpi_mail_message_total = fields.Boolean('Messages') kpi_mail_message_total_value = fields.Integer( compute='_compute_kpi_mail_message_total_value') def _compute_is_subscribed(self): for digest in self: digest.is_subscribed = self.env.user in digest.user_ids def _compute_available_fields(self): for digest in self: kpis_values_fields = [] for field_name, field in digest._fields.items(): if field.type == 'boolean' and field_name.startswith( ('kpi_', 'x_kpi_')) and digest[field_name]: kpis_values_fields += [field_name + '_value'] digest.available_fields = ', '.join(kpis_values_fields) def _get_kpi_compute_parameters(self): return fields.Date.to_string( self._context.get('start_date')), fields.Date.to_string( self._context.get('end_date')), self._context.get('company') def _compute_kpi_res_users_connected_value(self): for record in self: start, end, company = record._get_kpi_compute_parameters() user_connected = self.env['res.users'].search_count([ ('company_id', '=', company.id), ('login_date', '>=', start), ('login_date', '<', end) ]) record.kpi_res_users_connected_value = user_connected def _compute_kpi_mail_message_total_value(self): for record in self: start, end, company = record._get_kpi_compute_parameters() total_messages = self.env['mail.message'].search_count([ ('create_date', '>=', start), ('create_date', '<', end) ]) record.kpi_mail_message_total_value = total_messages @api.onchange('periodicity') def _onchange_periodicity(self): self.next_run_date = self._get_next_run_date() @api.model def create(self, vals): vals['next_run_date'] = date.today() + relativedelta(days=3) return super(Digest, self).create(vals) @api.multi def action_subscribe(self): if self.env.user not in self.user_ids: self.sudo().user_ids |= self.env.user @api.multi def action_unsubcribe(self): if self.env.user in self.user_ids: self.sudo().user_ids -= self.env.user @api.multi def action_activate(self): self.state = 'activated' @api.multi def action_deactivate(self): self.state = 'deactivated' def action_send(self): for digest in self: for user in digest.user_ids: subject = '%s: %s' % (user.company_id.name, digest.name) digest.template_id.with_context( user=user, company=user.company_id).send_mail(digest.id, force_send=True, raise_exception=True, email_values={ 'email_to': user.email, 'subject': subject }) digest.next_run_date = digest._get_next_run_date() def compute_kpis(self, company, user): self.ensure_one() if not company: company = self.env.user.company_id if not user: user = self.env.user res = {} for tf_name, tf in self._compute_timeframes(company).items(): digest = self.with_context(start_date=tf[0][0], end_date=tf[0][1], company=company).sudo(user.id) previous_digest = self.with_context(start_date=tf[1][0], end_date=tf[1][1], company=company).sudo(user.id) kpis = {} for field_name, field in self._fields.items(): if field.type == 'boolean' and field_name.startswith( ('kpi_', 'x_kpi_')) and self[field_name]: try: compute_value = digest[field_name + '_value'] previous_value = previous_digest[field_name + '_value'] except AccessError: # no access rights -> just skip that digest details from that user's digest email continue margin = self._get_margin_value(compute_value, previous_value) if self._fields[field_name + '_value'].type == 'monetary': converted_amount = self._format_human_readable_amount( compute_value) kpis.update({ field_name: { field_name: self._format_currency_amount( converted_amount, company.currency_id), 'margin': margin } }) else: kpis.update({ field_name: { field_name: compute_value, 'margin': margin } }) res.update({tf_name: kpis}) return res def compute_tips(self, company, user): tip = self.env['digest.tip'].search( [('user_ids', '!=', user.id), '|', ('group_id', 'in', user.groups_id.ids), ('group_id', '=', False)], limit=1) if not tip: return False tip.user_ids = [4, user.id] body = tools.html_sanitize(tip.tip_description) tip_description = self.env['mail.template'].render_template( body, 'digest.tip', self.id) return tip_description def compute_kpis_actions(self, company, user): """ Give an optional action to display in digest email linked to some KPIs. :return dict: key: kpi name (field name), value: an action that will be concatenated with /web#action={action} """ return {} def _get_next_run_date(self): self.ensure_one() if self.periodicity == 'daily': delta = relativedelta(days=1) elif self.periodicity == 'weekly': delta = relativedelta(weeks=1) elif self.periodicity == 'monthly': delta = relativedelta(months=1) elif self.periodicity == 'quarterly': delta = relativedelta(months=3) return date.today() + delta def _compute_timeframes(self, company): now = datetime.utcnow() tz_name = company.resource_calendar_id.tz if tz_name: now = pytz.timezone(tz_name).localize(now) start_date = now.date() return { 'yesterday': ((start_date + relativedelta(days=-1), start_date), (start_date + relativedelta(days=-2), start_date + relativedelta(days=-1))), 'lastweek': ((start_date + relativedelta(weeks=-1), start_date), (start_date + relativedelta(weeks=-2), start_date + relativedelta(weeks=-1))), 'lastmonth': ((start_date + relativedelta(months=-1), start_date), (start_date + relativedelta(months=-2), start_date + relativedelta(months=-1))), } def _get_margin_value(self, value, previous_value=0.0): margin = 0.0 if (value != previous_value) and (value != 0.0 and previous_value != 0.0): margin = float_round( (float(value - previous_value) / previous_value or 1) * 100, precision_digits=2) return margin def _format_currency_amount(self, amount, currency_id): pre = post = u'' if currency_id.position == 'before': pre = u'{symbol}\N{NO-BREAK SPACE}'.format( symbol=currency_id.symbol or '') else: post = u'\N{NO-BREAK SPACE}{symbol}'.format( symbol=currency_id.symbol or '') return u'{pre}{0}{post}'.format(amount, pre=pre, post=post) def _format_human_readable_amount(self, amount, suffix=''): for unit in ['', 'K', 'M', 'G']: if abs(amount) < 1000.0: return "%3.1f%s%s" % (amount, unit, suffix) amount /= 1000.0 return "%.1f%s%s" % (amount, 'T', suffix) @api.model def _cron_send_digest_email(self): digests = self.search([('next_run_date', '=', fields.Date.today()), ('state', '=', 'activated')]) for digest in digests: try: digest.action_send() except MailDeliveryException as e: _logger.warning( 'MailDeliveryException while sending digest %d. Digest is now scheduled for next cron update.' )
class Digest(models.Model): _name = 'digest.digest' _description = 'Digest' # Digest description name = fields.Char(string='Name', required=True, translate=True) user_ids = fields.Many2many('res.users', string='Recipients', domain="[('share', '=', False)]") periodicity = fields.Selection([('daily', 'Daily'), ('weekly', 'Weekly'), ('monthly', 'Monthly'), ('quarterly', 'Quarterly')], string='Periodicity', default='daily', required=True) next_run_date = fields.Date(string='Next Send Date') currency_id = fields.Many2one(related="company_id.currency_id", string='Currency', readonly=False) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company.id) available_fields = fields.Char(compute='_compute_available_fields') is_subscribed = fields.Boolean('Is user subscribed', compute='_compute_is_subscribed') state = fields.Selection([('activated', 'Activated'), ('deactivated', 'Deactivated')], string='Status', readonly=True, default='activated') # First base-related KPIs kpi_res_users_connected = fields.Boolean('Connected Users') kpi_res_users_connected_value = fields.Integer( compute='_compute_kpi_res_users_connected_value') kpi_mail_message_total = fields.Boolean('Messages') kpi_mail_message_total_value = fields.Integer( compute='_compute_kpi_mail_message_total_value') def _compute_is_subscribed(self): for digest in self: digest.is_subscribed = self.env.user in digest.user_ids def _compute_available_fields(self): for digest in self: kpis_values_fields = [] for field_name, field in digest._fields.items(): if field.type == 'boolean' and field_name.startswith( ('kpi_', 'x_kpi_', 'x_studio_kpi_')) and digest[field_name]: kpis_values_fields += [field_name + '_value'] digest.available_fields = ', '.join(kpis_values_fields) def _get_kpi_compute_parameters(self): return fields.Date.to_string( self._context.get('start_date')), fields.Date.to_string( self._context.get('end_date')), self.env.company def _compute_kpi_res_users_connected_value(self): for record in self: start, end, company = record._get_kpi_compute_parameters() user_connected = self.env['res.users'].search_count([ ('company_id', '=', company.id), ('login_date', '>=', start), ('login_date', '<', end) ]) record.kpi_res_users_connected_value = user_connected def _compute_kpi_mail_message_total_value(self): discussion_subtype_id = self.env.ref('mail.mt_comment').id for record in self: start, end, company = record._get_kpi_compute_parameters() total_messages = self.env['mail.message'].search_count([ ('create_date', '>=', start), ('create_date', '<', end), ('subtype_id', '=', discussion_subtype_id), ('message_type', 'in', ['comment', 'email']) ]) record.kpi_mail_message_total_value = total_messages @api.onchange('periodicity') def _onchange_periodicity(self): self.next_run_date = self._get_next_run_date() @api.model def create(self, vals): digest = super(Digest, self).create(vals) if not digest.next_run_date: digest.next_run_date = digest._get_next_run_date() return digest # ------------------------------------------------------------ # ACTIONS # ------------------------------------------------------------ def action_subscribe(self): if self.env.user.has_group( 'base.group_user') and self.env.user not in self.user_ids: self.sudo().user_ids |= self.env.user def action_unsubcribe(self): if self.env.user.has_group( 'base.group_user') and self.env.user in self.user_ids: self.sudo().user_ids -= self.env.user def action_activate(self): self.state = 'activated' def action_deactivate(self): self.state = 'deactivated' def action_set_periodicity(self, periodicity): self.periodicity = periodicity def action_send(self): to_slowdown = self._check_daily_logs() for digest in self: for user in digest.user_ids: digest.with_context(digest_slowdown=digest in to_slowdown, lang=user.lang)._action_send_to_user( user, tips_count=1) if digest in to_slowdown: digest.write({'periodicity': 'weekly'}) digest.next_run_date = digest._get_next_run_date() def _action_send_to_user(self, user, tips_count=1, consum_tips=True): web_base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') rendered_body = self.env['mail.render.mixin']._render_template( 'digest.digest_mail_main', 'digest.digest', self.ids, engine='qweb', add_context={ 'title': self.name, 'top_button_label': _('Connect'), 'top_button_url': url_join(web_base_url, '/web/login'), 'company': user.company_id, 'user': user, 'tips_count': tips_count, 'formatted_date': datetime.today().strftime('%B %d, %Y'), 'display_mobile_banner': True, 'kpi_data': self.compute_kpis(user.company_id, user), 'tips': self.compute_tips(user.company_id, user, tips_count=tips_count, consumed=consum_tips), 'preferences': self.compute_preferences(user.company_id, user), }, post_process=True)[self.id] full_mail = self.env['mail.render.mixin']._render_encapsulate( 'digest.digest_mail_layout', rendered_body, add_context={ 'company': user.company_id, 'user': user, }, ) # create a mail_mail based on values, without attachments mail_values = { 'auto_delete': True, 'email_from': (self.company_id.partner_id.email_formatted or self.env.user.email_formatted or self.env.ref('base.user_root').email_formatted), 'email_to': user.email_formatted, 'body_html': full_mail, 'state': 'outgoing', 'subject': '%s: %s' % (user.company_id.name, self.name), } mail = self.env['mail.mail'].sudo().create(mail_values) return True @api.model def _cron_send_digest_email(self): digests = self.search([('next_run_date', '<=', fields.Date.today()), ('state', '=', 'activated')]) for digest in digests: try: digest.action_send() except MailDeliveryException as e: _logger.warning( 'MailDeliveryException while sending digest %d. Digest is now scheduled for next cron update.', digest.id) # ------------------------------------------------------------ # KPIS # ------------------------------------------------------------ def compute_kpis(self, company, user): """ Compute KPIs to display in the digest template. It is expected to be a list of KPIs, each containing values for 3 columns display. :return list: result [{ 'kpi_name': 'kpi_mail_message', 'kpi_fullname': 'Messages', # translated 'kpi_action': 'crm.crm_lead_action_pipeline', # xml id of an action to execute 'kpi_col1': { 'value': '12.0', 'margin': 32.36, 'col_subtitle': 'Yesterday', # translated }, 'kpi_col2': { ... }, 'kpi_col3': { ... }, }, { ... }] """ self.ensure_one() digest_fields = self._get_kpi_fields() invalid_fields = [] kpis = [ dict( kpi_name=field_name, kpi_fullname=self.env['ir.model.fields']._get( self._name, field_name).field_description, kpi_action=False, kpi_col1=dict(), kpi_col2=dict(), kpi_col3=dict(), ) for field_name in digest_fields ] kpis_actions = self._compute_kpis_actions(company, user) for col_index, (tf_name, tf) in enumerate(self._compute_timeframes(company)): digest = self.with_context( start_date=tf[0][0], end_date=tf[0][1]).with_user(user).with_company(company) previous_digest = self.with_context( start_date=tf[1][0], end_date=tf[1][1]).with_user(user).with_company(company) for index, field_name in enumerate(digest_fields): kpi_values = kpis[index] kpi_values['kpi_action'] = kpis_actions.get(field_name) try: compute_value = digest[field_name + '_value'] # Context start and end date is different each time so invalidate to recompute. digest.invalidate_cache([field_name + '_value']) previous_value = previous_digest[field_name + '_value'] # Context start and end date is different each time so invalidate to recompute. previous_digest.invalidate_cache([field_name + '_value']) except AccessError: # no access rights -> just skip that digest details from that user's digest email invalid_fields.append(field_name) continue margin = self._get_margin_value(compute_value, previous_value) if self._fields['%s_value' % field_name].type == 'monetary': converted_amount = tools.format_decimalized_amount( compute_value) compute_value = self._format_currency_amount( converted_amount, company.currency_id) kpi_values['kpi_col%s' % (col_index + 1)].update({ 'value': compute_value, 'margin': margin, 'col_subtitle': tf_name, }) # filter failed KPIs return [kpi for kpi in kpis if kpi['kpi_name'] not in invalid_fields] def compute_tips(self, company, user, tips_count=1, consumed=True): tips = self.env['digest.tip'].search( [('user_ids', '!=', user.id), '|', ('group_id', 'in', user.groups_id.ids), ('group_id', '=', False)], limit=tips_count) tip_descriptions = [ self.env['mail.render.mixin']._render_template( tools.html_sanitize(tip.tip_description), 'digest.tip', tip.ids, post_process=True)[tip.id] for tip in tips ] if consumed: tips.user_ids += user return tip_descriptions def _compute_kpis_actions(self, company, user): """ Give an optional action to display in digest email linked to some KPIs. :return dict: key: kpi name (field name), value: an action that will be concatenated with /web#action={action} """ return {} def compute_preferences(self, company, user): """ Give an optional text for preferences, like a shortcut for configuration. :return string: html to put in template """ preferences = [] if self._context.get('digest_slowdown'): preferences.append( _("We have noticed you did not connect these last few days so we've automatically switched your preference to weekly Digests." )) elif self.periodicity == 'daily' and user.has_group( 'base.group_erp_manager'): preferences.append( '<p>%s<br /><a href="/digest/%s/set_periodicity?periodicity=weekly" target="_blank" style="color:#009EFB; font-weight: bold;">%s</a></p>' % (_('Prefer a broader overview ?'), self.id, _('Switch to weekly Digests'))) if user.has_group('base.group_erp_manager'): preferences.append( '<p>%s<br /><a href="/web#view_type=form&model=%s&id=%s" target="_blank" style="color:#009EFB; font-weight: bold;">%s</a></p>' % (_('Want to customize this email?'), self._name, self.id, _('Choose the metrics you care about'))) return preferences def _get_next_run_date(self): self.ensure_one() if self.periodicity == 'daily': delta = relativedelta(days=1) if self.periodicity == 'weekly': delta = relativedelta(weeks=1) elif self.periodicity == 'monthly': delta = relativedelta(months=1) elif self.periodicity == 'quarterly': delta = relativedelta(months=3) return date.today() + delta def _compute_timeframes(self, company): now = datetime.utcnow() tz_name = company.resource_calendar_id.tz if tz_name: now = pytz.timezone(tz_name).localize(now) start_date = now.date() return [(_('Yesterday'), ((start_date + relativedelta(days=-1), start_date), (start_date + relativedelta(days=-2), start_date + relativedelta(days=-1)))), (_('Last 7 Days'), ((start_date + relativedelta(weeks=-1), start_date), (start_date + relativedelta(weeks=-2), start_date + relativedelta(weeks=-1)))), (_('Last 30 Days'), ((start_date + relativedelta(months=-1), start_date), (start_date + relativedelta(months=-2), start_date + relativedelta(months=-1))))] # ------------------------------------------------------------ # FORMATTING / TOOLS # ------------------------------------------------------------ def _get_kpi_fields(self): return [ field_name for field_name, field in self._fields.items() if field.type == 'boolean' and field_name.startswith(( 'kpi_', 'x_kpi_', 'x_studio_kpi_')) and self[field_name] ] def _get_margin_value(self, value, previous_value=0.0): margin = 0.0 if (value != previous_value) and (value != 0.0 and previous_value != 0.0): margin = float_round( (float(value - previous_value) / previous_value or 1) * 100, precision_digits=2) return margin def _check_daily_logs(self): three_days_ago = datetime.now().replace( hour=0, minute=0, second=0, microsecond=0) - relativedelta(days=3) to_slowdown = self.env['digest.digest'] for digest in self.filtered( lambda digest: digest.periodicity == 'daily'): users_logs = self.env['res.users.log'].sudo().search_count([ ('create_uid', 'in', digest.user_ids.ids), ('create_date', '>=', three_days_ago) ]) if not users_logs: to_slowdown += digest return to_slowdown def _format_currency_amount(self, amount, currency_id): pre = currency_id.position == 'before' symbol = u'{symbol}'.format(symbol=currency_id.symbol or '') return u'{pre}{0}{post}'.format(amount, pre=symbol if pre else '', post=symbol if not pre else '')
class AccountInvoiceReport(models.Model): _name = "account.invoice.report" _inherit = ['ir.branch.company.mixin'] _description = "Invoices Statistics" _auto = False _rec_name = 'date' @api.multi @api.depends('currency_id', 'date', 'price_total', 'price_average', 'residual') def _compute_amounts_in_user_currency(self): """Compute the amounts in the currency of the user """ context = dict(self._context or {}) user_currency_id = self.env.user.company_id.currency_id currency_rate_id = self.env['res.currency.rate'].search([ ('rate', '=', 1), '|', ('company_id', '=', self.env.user.company_id.id), ('company_id', '=', False)], limit=1) base_currency_id = currency_rate_id.currency_id ctx = context.copy() for record in self: ctx['date'] = record.date record.user_currency_price_total = base_currency_id.with_context(ctx).compute(record.price_total, user_currency_id) record.user_currency_price_average = base_currency_id.with_context(ctx).compute(record.price_average, user_currency_id) record.user_currency_residual = base_currency_id.with_context(ctx).compute(record.residual, user_currency_id) date = fields.Date(readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) product_qty = fields.Float(string='Product Quantity', readonly=True) uom_name = fields.Char(string='Reference Unit of Measure', readonly=True) payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term', readonly=True) fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position', readonly=True) currency_id = fields.Many2one('res.currency', string='Currency', readonly=True) categ_id = fields.Many2one('product.category', string='Product Category', readonly=True) journal_id = fields.Many2one('account.journal', string='Journal', readonly=True) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) commercial_partner_id = fields.Many2one('res.partner', string='Partner Company', help="Commercial Entity") company_id = fields.Many2one('res.company', string='Company', readonly=True) user_id = fields.Many2one('res.users', string='Salesperson', readonly=True) price_total = fields.Float(string='Total Without Tax', readonly=True) user_currency_price_total = fields.Float(string="Total Without Tax", compute='_compute_amounts_in_user_currency', digits=0) price_average = fields.Float(string='Average Price', readonly=True, group_operator="avg") user_currency_price_average = fields.Float(string="Average Price", compute='_compute_amounts_in_user_currency', digits=0) currency_rate = fields.Float(string='Currency Rate', readonly=True, group_operator="avg", groups="base.group_multi_currency") nbr = fields.Integer(string='# of Lines', readonly=True) # TDE FIXME master: rename into nbr_lines type = fields.Selection([ ('out_invoice', 'Customer Invoice'), ('in_invoice', 'Vendor Bill'), ('out_refund', 'Customer Credit Note'), ('in_refund', 'Vendor Credit Note'), ], readonly=True) state = fields.Selection([ ('draft', 'Draft'), ('open', 'Open'), ('paid', 'Paid'), ('cancel', 'Cancelled') ], string='Invoice Status', readonly=True) date_due = fields.Date(string='Due Date', readonly=True) account_id = fields.Many2one('account.account', string='Account', readonly=True, domain=[('deprecated', '=', False)]) account_line_id = fields.Many2one('account.account', string='Account Line', readonly=True, domain=[('deprecated', '=', False)]) partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account', readonly=True) residual = fields.Float(string='Due Amount', readonly=True) user_currency_residual = fields.Float(string="Total Residual", compute='_compute_amounts_in_user_currency', digits=0) country_id = fields.Many2one('res.country', string='Country of the Partner Company') account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account', groups="analytic.group_analytic_accounting") _order = 'date desc' _depends = { 'account.invoice': [ 'account_id', 'amount_total_company_signed', 'commercial_partner_id', 'company_id', 'branch_id', 'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id', 'journal_id', 'partner_bank_id', 'partner_id', 'payment_term_id', 'residual', 'state', 'type', 'user_id', ], 'account.invoice.line': [ 'account_id', 'invoice_id', 'price_subtotal', 'product_id', 'quantity', 'uom_id', 'account_analytic_id', ], 'product.product': ['product_tmpl_id'], 'product.template': ['categ_id'], 'product.uom': ['category_id', 'factor', 'name', 'uom_type'], 'res.currency.rate': ['currency_id', 'name'], 'res.partner': ['country_id'], } def _select(self): select_str = """ SELECT sub.id, sub.date, sub.product_id, sub.partner_id, sub.country_id, sub.account_analytic_id, sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id, sub.fiscal_position_id, sub.user_id, sub.company_id, sub.branch_id, sub.nbr, sub.type, sub.state, sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id, sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, COALESCE(cr.rate, 1) as currency_rate, sub.residual as residual, sub.commercial_partner_id as commercial_partner_id """ return select_str def _sub_select(self): select_str = """ SELECT ail.id AS id, ai.date_invoice AS date, ail.product_id, ai.partner_id, ai.payment_term_id, ail.account_analytic_id, u2.name AS uom_name, ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id, 1 AS nbr, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id, ai.partner_bank_id, SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty, SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total, SUM(ABS(ail.price_subtotal_signed)) / CASE WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric THEN SUM(ail.quantity / u.factor * u2.factor) ELSE 1::numeric END AS price_average, ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * count(*) * invoice_type.sign AS residual, ai.commercial_partner_id as commercial_partner_id, coalesce(partner.country_id, partner_ai.country_id) AS country_id """ return select_str def _from(self): from_str = """ FROM account_invoice_line ail JOIN account_invoice ai ON ai.id = ail.invoice_id JOIN res_partner partner ON ai.commercial_partner_id = partner.id JOIN res_partner partner_ai ON ai.partner_id = partner_ai.id LEFT JOIN product_product pr ON pr.id = ail.product_id left JOIN product_template pt ON pt.id = pr.product_tmpl_id LEFT JOIN product_uom u ON u.id = ail.uom_id LEFT JOIN product_uom u2 ON u2.id = pt.uom_id JOIN ( -- Temporary table to decide if the qty should be added or retrieved (Invoice vs Credit Note) SELECT id,(CASE WHEN ai.type::text = ANY (ARRAY['in_refund'::character varying::text, 'in_invoice'::character varying::text]) THEN -1 ELSE 1 END) AS sign,(CASE WHEN ai.type::text = ANY (ARRAY['out_refund'::character varying::text, 'in_invoice'::character varying::text]) THEN -1 ELSE 1 END) AS sign_qty FROM account_invoice ai ) AS invoice_type ON invoice_type.id = ai.id """ return from_str def _group_by(self): group_by_str = """ GROUP BY ail.id, ail.product_id, ail.account_analytic_id, ai.date_invoice, ai.id, ai.partner_id, ai.payment_term_id, u2.name, u2.id, ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id, ai.type, invoice_type.sign, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id, ai.partner_bank_id, ai.residual_company_signed, ai.amount_total_company_signed, ai.commercial_partner_id, coalesce(partner.country_id, partner_ai.country_id) """ return group_by_str @api.model_cr def init(self): # self._table = account_invoice_report tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute("""CREATE or REPLACE VIEW %s as ( WITH currency_rate AS (%s) %s FROM ( %s %s %s ) AS sub LEFT JOIN currency_rate cr ON (cr.currency_id = sub.currency_id AND cr.company_id = sub.company_id AND cr.date_start <= COALESCE(sub.date, NOW()) AND (cr.date_end IS NULL OR cr.date_end > COALESCE(sub.date, NOW()))) )""" % ( self._table, self.env['res.currency']._select_companies_rates(), self._select(), self._sub_select(), self._from(), self._group_by()))
class FleetReport(models.Model): _name = "fleet.vehicle.cost.report" _description = "Fleet Analysis Report" _auto = False _order = 'date_start desc' company_id = fields.Many2one('res.company', 'Company', readonly=True) vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', readonly=True) name = fields.Char('Vehicle Name', readonly=True) driver_id = fields.Many2one('res.partner', 'Driver', readonly=True) fuel_type = fields.Char('Fuel', readonly=True) date_start = fields.Date('Date', readonly=True) cost = fields.Float('Cost', readonly=True) cost_type = fields.Selection(string='Cost Type', selection=[('contract', 'Contract'), ('service', 'Service')], readonly=True) def init(self): query = """ WITH service_costs AS ( SELECT ve.id AS vehicle_id, ve.company_id AS company_id, ve.name AS name, ve.driver_id AS driver_id, ve.fuel_type AS fuel_type, date(date_trunc('month', d)) AS date_start, COALESCE(sum(se.amount), 0) AS COST, 'service' AS cost_type FROM fleet_vehicle ve CROSS JOIN generate_series(( SELECT min(date) FROM fleet_vehicle_log_services), CURRENT_DATE + '1 month'::interval, '1 month') d LEFT JOIN fleet_vehicle_log_services se ON se.vehicle_id = ve.id AND date_trunc('month', se.date) = date_trunc('month', d) WHERE ve.active AND se.active AND se.state != 'cancelled' GROUP BY ve.id, ve.company_id, ve.name, date_start, d ORDER BY ve.id, date_start ), contract_costs AS ( SELECT ve.id AS vehicle_id, ve.company_id AS company_id, ve.name AS name, ve.driver_id AS driver_id, ve.fuel_type AS fuel_type, date(date_trunc('month', d)) AS date_start, (COALESCE(sum(co.amount), 0) + COALESCE(sum(cod.cost_generated * extract(day FROM least (date_trunc('month', d) + interval '1 month', cod.expiration_date) - greatest (date_trunc('month', d), cod.start_date))), 0) + COALESCE(sum(com.cost_generated), 0) + COALESCE(sum(coy.cost_generated), 0)) AS COST, 'contract' AS cost_type FROM fleet_vehicle ve CROSS JOIN generate_series(( SELECT min(acquisition_date) FROM fleet_vehicle), CURRENT_DATE + '1 month'::interval, '1 month') d LEFT JOIN fleet_vehicle_log_contract co ON co.vehicle_id = ve.id AND date_trunc('month', co.date) = date_trunc('month', d) LEFT JOIN fleet_vehicle_log_contract cod ON cod.vehicle_id = ve.id AND date_trunc('month', cod.start_date) <= date_trunc('month', d) AND date_trunc('month', cod.expiration_date) >= date_trunc('month', d) AND cod.cost_frequency = 'daily' LEFT JOIN fleet_vehicle_log_contract com ON com.vehicle_id = ve.id AND date_trunc('month', com.start_date) <= date_trunc('month', d) AND date_trunc('month', com.expiration_date) >= date_trunc('month', d) AND com.cost_frequency = 'monthly' LEFT JOIN fleet_vehicle_log_contract coy ON coy.vehicle_id = ve.id AND date_trunc('month', coy.date) = date_trunc('month', d) AND date_trunc('month', coy.start_date) <= date_trunc('month', d) AND date_trunc('month', coy.expiration_date) >= date_trunc('month', d) AND coy.cost_frequency = 'yearly' WHERE ve.active GROUP BY ve.id, ve.company_id, ve.name, date_start, d ORDER BY ve.id, date_start ) SELECT vehicle_id AS id, company_id, vehicle_id, name, driver_id, fuel_type, date_start, COST, 'service' as cost_type FROM service_costs sc UNION ALL ( SELECT vehicle_id AS id, company_id, vehicle_id, name, driver_id, fuel_type, date_start, COST, 'contract' as cost_type FROM contract_costs cc) """ tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute( sql.SQL("""CREATE or REPLACE VIEW {} as ({})""").format( sql.Identifier(self._table), sql.SQL(query)))
class TaxAdjustments(models.TransientModel): _name = 'tax.adjustments.wizard' _description = 'Wizard for Tax Adjustments' @api.multi def _get_default_journal(self): return self.env['account.journal'].search([('type', '=', 'general')], limit=1).id reason = fields.Char(string='Justification', required=True) journal_id = fields.Many2one('account.journal', string='Journal', required=True, default=_get_default_journal, domain=[('type', '=', 'general')]) date = fields.Date(required=True, default=fields.Date.context_today) debit_account_id = fields.Many2one('account.account', string='Debit account', required=True, domain=[('deprecated', '=', False)]) credit_account_id = fields.Many2one('account.account', string='Credit account', required=True, domain=[('deprecated', '=', False)]) amount = fields.Monetary(currency_field='company_currency_id', required=True) adjustment_type = fields.Selection( [('debit', 'Applied on debit journal item'), ('credit', 'Applied on credit journal item')], string="Adjustment Type", store=False, required=True) company_currency_id = fields.Many2one( 'res.currency', readonly=True, default=lambda self: self.env.user.company_id.currency_id) tax_id = fields.Many2one('account.tax', string='Adjustment Tax', ondelete='restrict', domain=[('type_tax_use', '=', 'none'), ('tax_adjustment', '=', True)], required=True) @api.multi def _create_move(self): adjustment_type = self.env.context.get( 'adjustment_type', (self.amount > 0.0 and 'debit' or 'credit')) debit_vals = { 'name': self.reason, 'debit': abs(self.amount), 'credit': 0.0, 'account_id': self.debit_account_id.id, 'tax_line_id': adjustment_type == 'debit' and self.tax_id.id or False, } credit_vals = { 'name': self.reason, 'debit': 0.0, 'credit': abs(self.amount), 'account_id': self.credit_account_id.id, 'tax_line_id': adjustment_type == 'credit' and self.tax_id.id or False, } vals = { 'journal_id': self.journal_id.id, 'date': self.date, 'state': 'draft', 'line_ids': [(0, 0, debit_vals), (0, 0, credit_vals)] } move = self.env['account.move'].create(vals) move.post() return move.id @api.multi def create_move_debit(self): return self.with_context(adjustment_type='debit').create_move() @api.multi def create_move_credit(self): return self.with_context(adjustment_type='credit').create_move() def create_move(self): #create the adjustment move move_id = self._create_move() #return an action showing the created move action = self.env.ref( self.env.context.get('action', 'account.action_move_line_form')) result = action.read()[0] result['views'] = [(False, 'form')] result['res_id'] = move_id return result
class MailMail(models.Model): """ Model holding RFC2822 email messages to send. This model also provides facilities to queue and send new email messages. """ _name = 'mail.mail' _description = 'Outgoing Mails' _inherits = {'mail.message': 'mail_message_id'} _order = 'id desc' _rec_name = 'subject' # content mail_message_id = fields.Many2one('mail.message', 'Message', required=True, ondelete='cascade', index=True, auto_join=True) body_html = fields.Text('Rich-text Contents', help="Rich-text/HTML message") references = fields.Text( 'References', help='Message references, such as identifiers of previous messages', readonly=1) headers = fields.Text('Headers', copy=False) # Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification # and during unlink() we will not cascade delete the parent and its attachments notification = fields.Boolean( 'Is Notification', help= 'Mail has been created to notify people of an existing mail.message') # recipients email_to = fields.Text('To', help='Message recipients (emails)') email_cc = fields.Char('Cc', help='Carbon copy message recipients') recipient_ids = fields.Many2many('res.partner', string='To (Partners)') # process state = fields.Selection([ ('outgoing', 'Outgoing'), ('sent', 'Sent'), ('received', 'Received'), ('exception', 'Delivery Failed'), ('cancel', 'Cancelled'), ], 'Status', readonly=True, copy=False, default='outgoing') auto_delete = fields.Boolean( 'Auto Delete', help="Permanently delete this email after sending it, to save space") keep_days = fields.Integer( 'Keep days', default=-1, help="This value defines the no. of days " "the emails should be recorded " "in the system: \n -1 = Email will be deleted " "immediately once it is send \n greater than 0 = Email " "will be deleted after " "the no. of days are met.") delete_date = fields.Date(compute='_compute_delete_on_date', string='Delete on.', store=True) failure_reason = fields.Text( 'Failure Reason', readonly=1, help= "Failure reason. This is usually the exception thrown by the email server, stored to ease the debugging of mailing issues." ) scheduled_date = fields.Char( 'Scheduled Send Date', help= "If set, the queue manager will send the email after the date. If not set, the email will be send as soon as possible." ) @api.multi @api.depends('keep_days') def _compute_delete_on_date(self): for obj in self: mail_date = fields.Datetime.from_string(obj.date) if obj.keep_days > 0: delete_on = mail_date + datetime.timedelta(days=obj.keep_days) obj.delete_date = delete_on else: obj.delete_date = mail_date.date() @api.model def create(self, values): # notification field: if not set, set if mail comes from an existing mail.message if 'notification' not in values and values.get('mail_message_id'): values['notification'] = True if not values.get('mail_message_id'): self = self.with_context(message_create_from_mail_mail=True) new_mail = super(MailMail, self).create(values) if values.get('attachment_ids'): new_mail.attachment_ids.check(mode='read') return new_mail @api.multi def write(self, vals): res = super(MailMail, self).write(vals) if vals.get('attachment_ids'): for mail in self: mail.attachment_ids.check(mode='read') return res @api.multi def unlink(self): # cascade-delete the parent message for all mails that are not created for a notification to_cascade = self.search([('notification', '=', False), ('id', 'in', self.ids) ]).mapped('mail_message_id') res = super(MailMail, self).unlink() to_cascade.unlink() return res @api.model def default_get(self, fields): # protection for `default_type` values leaking from menu action context (e.g. for invoices) # To remove when automatic context propagation is removed in web client if self._context.get('default_type') not in type( self).message_type.base_field.selection: self = self.with_context(dict(self._context, default_type=None)) return super(MailMail, self).default_get(fields) @api.multi def mark_outgoing(self): return self.write({'state': 'outgoing'}) @api.multi def cancel(self): return self.write({'state': 'cancel'}) @api.model def process_email_unlink(self): mail_ids = self.sudo().search([('delete_date', '=', datetime.datetime.now().date())]) mail_ids.filtered('auto_delete').unlink() @api.model def process_email_queue(self, ids=None): """Send immediately queued messages, committing after each message is sent - this is not transactional and should not be called during another transaction! :param list ids: optional list of emails ids to send. If passed no search is performed, and these ids are used instead. :param dict context: if a 'filters' key is present in context, this value will be used as an additional filter to further restrict the outgoing messages to send (by default all 'outgoing' messages are sent). """ filters = [ '&', ('state', '=', 'outgoing'), '|', ('scheduled_date', '<', datetime.datetime.now()), ('scheduled_date', '=', False) ] if 'filters' in self._context: filters.extend(self._context['filters']) # TODO: make limit configurable filtered_ids = self.search(filters, limit=10000).ids if not ids: ids = filtered_ids else: ids = list(set(filtered_ids) & set(ids)) ids.sort() res = None try: # auto-commit except in testing mode auto_commit = not getattr(threading.currentThread(), 'testing', False) res = self.browse(ids).send(auto_commit=auto_commit) except Exception: _logger.exception("Failed processing mail queue") return res @api.multi def _postprocess_sent_message(self, mail_sent=True): """Perform any post-processing necessary after sending ``mail`` successfully, including deleting it completely along with its attachment if the ``auto_delete`` flag of the mail was set. Overridden by subclasses for extra post-processing behaviors. :return: True """ notif_emails = self.filtered(lambda email: email.notification) if notif_emails: notifications = self.env['mail.notification'].search([ ('mail_message_id', 'in', notif_emails.mapped('mail_message_id').ids), ('is_email', '=', True) ]) if mail_sent: notifications.write({ 'email_status': 'sent', }) else: notifications.write({ 'email_status': 'exception', }) if mail_sent: if self.keep_days > 0: return True self.sudo().filtered(lambda self: self.auto_delete).unlink() return True # ------------------------------------------------------ # mail_mail formatting, tools and send mechanism # ------------------------------------------------------ @api.multi def send_get_mail_body(self, partner=None): """Return a specific ir_email body. The main purpose of this method is to be inherited to add custom content depending on some module.""" self.ensure_one() body = self.body_html or '' return body @api.multi def send_get_mail_to(self, partner=None): """Forge the email_to with the following heuristic: - if 'partner', recipient specific (Partner Name <email>) - else fallback on mail.email_to splitting """ self.ensure_one() if partner: email_to = [ formataddr((partner.name or 'False', partner.email or 'False')) ] else: email_to = tools.email_split_and_format(self.email_to) return email_to @api.multi def send_get_email_dict(self, partner=None): """Return a dictionary for specific email values, depending on a partner, or generic to the whole recipients given by mail.email_to. :param Model partner: specific recipient partner """ self.ensure_one() body = self.send_get_mail_body(partner=partner) body_alternative = tools.html2plaintext(body) res = { 'body': body, 'body_alternative': body_alternative, 'email_to': self.send_get_mail_to(partner=partner), } return res @api.multi def _split_by_server(self): """Returns an iterator of pairs `(mail_server_id, record_ids)` for current recordset. The same `mail_server_id` may repeat in order to limit batch size according to the `mail.session.batch.size` system parameter. """ groups = defaultdict(list) # Turn prefetch OFF to avoid MemoryError on very large mail queues, we only care # about the mail server ids in this case. for mail in self.with_context(prefetch_fields=False): groups[mail.mail_server_id.id].append(mail.id) sys_params = self.env['ir.config_parameter'].sudo() batch_size = int(sys_params.get_param('mail.session.batch.size', 1000)) for server_id, record_ids in groups.items(): for mail_batch in tools.split_every(batch_size, record_ids): yield server_id, mail_batch @api.multi def send(self, auto_commit=False, raise_exception=False): """ Sends the selected emails immediately, ignoring their current state (mails that have already been sent should not be passed unless they should actually be re-sent). Emails successfully delivered are marked as 'sent', and those that fail to be deliver are marked as 'exception', and the corresponding error mail is output in the server logs. :param bool auto_commit: whether to force a commit of the mail status after sending each mail (meant only for scheduler processing); should never be True during normal transactions (default: False) :param bool raise_exception: whether to raise an exception if the email sending process has failed :return: True """ for server_id, batch_ids in self._split_by_server(): smtp_session = None try: smtp_session = self.env['ir.mail_server'].connect( mail_server_id=server_id) except Exception as exc: if raise_exception: # To be consistent and backward compatible with mail_mail.send() raised # exceptions, it is encapsulated into an Flectra MailDeliveryException raise MailDeliveryException( _('Unable to connect to SMTP Server'), exc) else: self.browse(batch_ids).write({ 'state': 'exception', 'failure_reason': exc }) else: self.browse(batch_ids)._send(auto_commit=auto_commit, raise_exception=raise_exception, smtp_session=smtp_session) _logger.info('Sent batch %s emails via mail server ID #%s', len(batch_ids), server_id) finally: if smtp_session: smtp_session.quit() @api.multi def _send(self, auto_commit=False, raise_exception=False, smtp_session=None): IrMailServer = self.env['ir.mail_server'] for mail_id in self.ids: try: mail = self.browse(mail_id) if mail.state != 'outgoing': if mail.state != 'exception' and mail.auto_delete and \ mail.keep_days < 0: mail.sudo().unlink() continue # TDE note: remove me when model_id field is present on mail.message - done here to avoid doing it multiple times in the sub method if mail.model: model = self.env['ir.model']._get(mail.model)[0] else: model = None if model: mail = mail.with_context(model_name=model.name) # load attachment binary data with a separate read(), as prefetching all # `datas` (binary field) could bloat the browse cache, triggerring # soft/hard mem limits with temporary data. attachments = [(a['datas_fname'], base64.b64decode(a['datas']), a['mimetype']) for a in mail.attachment_ids.sudo().read( ['datas_fname', 'datas', 'mimetype'])] # specific behavior to customize the send email for notified partners email_list = [] if mail.email_to: email_list.append(mail.send_get_email_dict()) for partner in mail.recipient_ids: email_list.append( mail.send_get_email_dict(partner=partner)) # headers headers = {} ICP = self.env['ir.config_parameter'].sudo() bounce_alias = ICP.get_param("mail.bounce.alias") catchall_domain = ICP.get_param("mail.catchall.domain") if bounce_alias and catchall_domain: if mail.model and mail.res_id: headers['Return-Path'] = '%s+%d-%s-%d@%s' % ( bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain) else: headers['Return-Path'] = '%s+%d@%s' % ( bounce_alias, mail.id, catchall_domain) if mail.headers: try: headers.update(safe_eval(mail.headers)) except Exception: pass # Writing on the mail object may fail (e.g. lock on user) which # would trigger a rollback *after* actually sending the email. # To avoid sending twice the same email, provoke the failure earlier mail.write({ 'state': 'exception', 'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.' ), }) mail_sent = False # build an RFC2822 email.message.Message object and send it without queuing res = None for email in email_list: msg = IrMailServer.build_email( email_from=mail.email_from, email_to=email.get('email_to'), subject=mail.subject, body=email.get('body'), body_alternative=email.get('body_alternative'), email_cc=tools.email_split(mail.email_cc), reply_to=mail.reply_to, attachments=attachments, message_id=mail.message_id, references=mail.references, object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)), subtype='html', subtype_alternative='plain', headers=headers) try: res = IrMailServer.send_email( msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session) except AssertionError as error: if str(error) == IrMailServer.NO_VALID_RECIPIENT: # No valid recipient found for this particular # mail item -> ignore error to avoid blocking # delivery to next recipients, if any. If this is # the only recipient, the mail will show as failed. _logger.info( "Ignoring invalid recipients for mail.mail %s: %s", mail.message_id, email.get('email_to')) else: raise if res: mail.write({ 'state': 'sent', 'message_id': res, 'failure_reason': False }) mail_sent = True # /!\ can't use mail.state here, as mail.refresh() will cause an error # see revid:[email protected] in 6.1 if mail_sent: _logger.info( 'Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id) mail._postprocess_sent_message(mail_sent=mail_sent) except MemoryError: # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job # instead of marking the mail as failed _logger.exception( 'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option', mail.id, mail.message_id) raise except psycopg2.Error: # If an error with the database occurs, chances are that the cursor is unusable. # This will lead to an `psycopg2.InternalError` being raised when trying to write # `state`, shadowing the original exception and forbid a retry on concurrent # update. Let's bubble it. raise except Exception as e: failure_reason = tools.ustr(e) _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason) mail.write({ 'state': 'exception', 'failure_reason': failure_reason }) mail._postprocess_sent_message(mail_sent=False) if raise_exception: if isinstance(e, AssertionError): # get the args of the original error, wrap into a value and throw a MailDeliveryException # that is an except_orm, with name and value as arguments value = '. '.join(e.args) raise MailDeliveryException(_("Mail Delivery Failed"), value) raise if auto_commit is True: self._cr.commit() return True
class GroupOnDate(models.Model): _name = 'test_read_group.on_date' _description = 'Group Test Read On Date' date = fields.Date("Date") value = fields.Integer("Value")
class AccountInvoiceLine(models.Model): _inherit = 'account.move.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="Account", store=True) @api.depends('asset_category_id', 'move_id.invoice_date') def _get_asset_date(self): for rec in self: rec.asset_mrr = 0 rec.asset_start_date = False rec.asset_end_date = False cat = rec.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 rec.move_id.move_type in ['out_invoice', 'out_refund']: rec.asset_mrr = rec.price_subtotal / months if rec.move_id.invoice_date: start_date = rec.move_id.invoice_date.replace(day=1) end_date = (start_date + relativedelta(months=months, days=-1)) rec.asset_start_date = start_date rec.asset_end_date = end_date def asset_create(self): if self.asset_category_id: vals = { 'name': self.name, 'code': self.name or False, 'category_id': self.asset_category_id.id, 'value': self.price_subtotal, 'partner_id': self.move_id.partner_id.id, 'company_id': self.move_id.company_id.id, 'currency_id': self.move_id.company_currency_id.id, 'date': self.move_id.invoice_date, 'invoice_id': self.move_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.move_id.move_type == 'out_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id elif self.move_id.move_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: for rec in self: if rec.move_id.move_type == 'out_invoice': rec.asset_category_id = rec.product_id.product_tmpl_id.deferred_revenue_category_id elif rec.move_id.move_type == 'in_invoice': rec.asset_category_id = rec.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)
class Partner(models.Model): _description = 'Contact' _inherit = ['format.address.mixin'] _name = "res.partner" _order = "display_name" def _default_category(self): return self.env['res.partner.category'].browse( self._context.get('category_id')) def _default_company(self): return self.env['res.company']._company_default_get('res.partner') name = fields.Char(index=True) display_name = fields.Char(compute='_compute_display_name', store=True, index=True) date = fields.Date(index=True) title = fields.Many2one('res.partner.title') parent_id = fields.Many2one('res.partner', string='Related Company', index=True) parent_name = fields.Char(related='parent_id.name', readonly=True, string='Parent name') child_ids = fields.One2many( 'res.partner', 'parent_id', string='Contacts', domain=[('active', '=', True) ]) # force "active_test" domain to bypass _search() override ref = fields.Char(string='Internal Reference', index=True) lang = fields.Selection( _lang_get, string='Language', default=lambda self: self.env.lang, help= "If the selected language is loaded in the system, all documents related to " "this contact will be printed in this language. If not, it will be English." ) tz = fields.Selection( _tz_get, string='Timezone', default=lambda self: self._context.get('tz'), help= "The partner's timezone, used to output proper date and time values " "inside printed reports. It is important to set a value for this field. " "You should use the same timezone that is otherwise used to pick and " "render date and time values: your computer's timezone.") tz_offset = fields.Char(compute='_compute_tz_offset', string='Timezone offset', invisible=True) user_id = fields.Many2one( 'res.users', string='Salesperson', help= 'The internal user that is in charge of communicating with this contact if any.' ) vat = fields.Char(string='TIN', help="Tax Identification Number. " "Fill it if the company is subjected to taxes. " "Used by the some of the legal statements.") bank_ids = fields.One2many('res.partner.bank', 'partner_id', string='Banks') website = fields.Char(help="Website of Partner or Company") comment = fields.Text(string='Notes') category_id = fields.Many2many('res.partner.category', column1='partner_id', column2='category_id', string='Tags', default=_default_category) credit_limit = fields.Float(string='Credit Limit') barcode = fields.Char(oldname='ean13') active = fields.Boolean(default=True) customer = fields.Boolean( string='Is a Customer', default=True, help="Check this box if this contact is a customer.") supplier = fields.Boolean( string='Is a Vendor', help="Check this box if this contact is a vendor. " "If it's not checked, purchase people will not see it when encoding a purchase order." ) employee = fields.Boolean( help="Check this box if this contact is an Employee.") function = fields.Char(string='Job Position') type = fields.Selection( [('contact', 'Contact'), ('invoice', 'Invoice address'), ('delivery', 'Shipping address'), ('other', 'Other address')], string='Address Type', default='contact', help= "Used to select automatically the right address according to the context in sales and purchases documents." ) street = fields.Char() street2 = fields.Char() zip = fields.Char(change_default=True) city = fields.Char() state_id = fields.Many2one("res.country.state", string='State', ondelete='restrict') country_id = fields.Many2one('res.country', string='Country', ondelete='restrict') email = fields.Char() email_formatted = fields.Char( 'Formatted Email', compute='_compute_email_formatted', help='Format email address "Name <email@domain>"') phone = fields.Char() mobile = fields.Char() is_company = fields.Boolean( string='Is a Company', default=False, help="Check if the contact is a company, otherwise it is a person") industry_id = fields.Many2one('res.partner.industry', 'Industry') # company_type is only an interface field, do not use it in business logic company_type = fields.Selection(string='Company Type', selection=[('person', 'Individual'), ('company', 'Company')], compute='_compute_company_type', inverse='_write_company_type') company_id = fields.Many2one('res.company', 'Company', index=True, default=_default_company) color = fields.Integer(string='Color Index', default=0) user_ids = fields.One2many('res.users', 'partner_id', string='Users', auto_join=True) partner_share = fields.Boolean( 'Share Partner', compute='_compute_partner_share', store=True, help= "Either customer (no user), either shared user. Indicated the current partner is a customer without " "access or with a limited access created for sharing data.") contact_address = fields.Char(compute='_compute_contact_address', string='Complete Address') # technical field used for managing commercial fields commercial_partner_id = fields.Many2one( 'res.partner', compute='_compute_commercial_partner', string='Commercial Entity', store=True, index=True) commercial_partner_country_id = fields.Many2one( 'res.country', related='commercial_partner_id.country_id', store=True) commercial_company_name = fields.Char( 'Company Name Entity', compute='_compute_commercial_company_name', store=True) company_name = fields.Char('Company Name') # image: all image fields are base64 encoded and PIL-supported image = fields.Binary( "Image", attachment=True, help= "This field holds the image used as avatar for this contact, limited to 1024x1024px", ) image_medium = fields.Binary("Medium-sized image", attachment=True, help="Medium-sized image of this contact. It is automatically "\ "resized as a 128x128px image, with aspect ratio preserved. "\ "Use this field in form views or some kanban views.") image_small = fields.Binary("Small-sized image", attachment=True, help="Small-sized image of this contact. It is automatically "\ "resized as a 64x64px image, with aspect ratio preserved. "\ "Use this field anywhere a small image is required.") # hack to allow using plain browse record in qweb views, and used in ir.qweb.field.contact self = fields.Many2one(comodel_name=_name, compute='_compute_get_ids') _sql_constraints = [ ('check_name', "CHECK( (type='contact' AND name IS NOT NULL) or (type!='contact') )", 'Contacts require a name.'), ] @api.depends('is_company', 'name', 'parent_id.name', 'type', 'company_name') def _compute_display_name(self): diff = dict(show_address=None, show_address_only=None, show_email=None) names = dict(self.with_context(**diff).name_get()) for partner in self: partner.display_name = names.get(partner.id) @api.depends('tz') def _compute_tz_offset(self): for partner in self: partner.tz_offset = datetime.datetime.now( pytz.timezone(partner.tz or 'GMT')).strftime('%z') @api.depends('user_ids.share') def _compute_partner_share(self): for partner in self: partner.partner_share = not partner.user_ids or any( user.share for user in partner.user_ids) @api.depends(lambda self: self._display_address_depends()) def _compute_contact_address(self): for partner in self: partner.contact_address = partner._display_address() @api.one def _compute_get_ids(self): self.self = self.id @api.depends('is_company', 'parent_id.commercial_partner_id') def _compute_commercial_partner(self): for partner in self: if partner.is_company or not partner.parent_id: partner.commercial_partner_id = partner else: partner.commercial_partner_id = partner.parent_id.commercial_partner_id @api.depends('company_name', 'parent_id.is_company', 'commercial_partner_id.name') def _compute_commercial_company_name(self): for partner in self: p = partner.commercial_partner_id partner.commercial_company_name = p.is_company and p.name or partner.company_name @api.model def _get_default_image(self, partner_type, is_company, parent_id): if getattr(threading.currentThread(), 'testing', False) or self._context.get('install_mode'): return False colorize, img_path, image = False, False, False if partner_type in ['other'] and parent_id: parent_image = self.browse(parent_id).image image = parent_image and base64.b64decode(parent_image) or None if not image and partner_type == 'invoice': img_path = get_module_resource('base', 'static/src/img', 'money.png') elif not image and partner_type == 'delivery': img_path = get_module_resource('base', 'static/src/img', 'truck.png') elif not image and is_company: img_path = get_module_resource('base', 'static/src/img', 'company_image.png') elif not image: img_path = get_module_resource('base', 'static/src/img', 'avatar.png') colorize = True if img_path: with open(img_path, 'rb') as f: image = f.read() if image and colorize: image = tools.image_colorize(image) return tools.image_resize_image_big(base64.b64encode(image)) @api.model def _fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): if (not view_id) and (view_type == 'form') and self._context.get('force_email'): view_id = self.env.ref('base.view_partner_simple_form').id res = super(Partner, self)._fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) if view_type == 'form': res['arch'] = self._fields_view_get_address(res['arch']) return res @api.constrains('parent_id') def _check_parent_id(self): if not self._check_recursion(): raise ValidationError( _('You cannot create recursive Partner hierarchies.')) @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_('%s (copy)') % self.name) return super(Partner, self).copy(default) @api.onchange('parent_id') def onchange_parent_id(self): # return values in result, as this method is used by _fields_sync() if not self.parent_id: return result = {} partner = getattr(self, '_origin', self) if partner.parent_id and partner.parent_id != self.parent_id: result['warning'] = { 'title': _('Warning'), 'message': _('Changing the company of a contact should only be done if it ' 'was never correctly set. If an existing contact starts working for a new ' 'company then a new contact should be created under that new ' 'company. You can use the "Discard" button to abandon this change.' ) } if partner.type == 'contact' or self.type == 'contact': # for contacts: copy the parent address, if set (aka, at least one # value is set in the address: otherwise, keep the one from the # contact) address_fields = self._address_fields() if any(self.parent_id[key] for key in address_fields): def convert(value): return value.id if isinstance(value, models.BaseModel) else value result['value'] = { key: convert(self.parent_id[key]) for key in address_fields } return result @api.onchange('country_id') def _onchange_country_id(self): if self.country_id: return { 'domain': { 'state_id': [('country_id', '=', self.country_id.id)] } } else: return {'domain': {'state_id': []}} @api.onchange('email') def onchange_email(self): if not self.image and self._context.get( 'gravatar_image') and self.email: self.image = self._get_gravatar_image(self.email) @api.depends('name', 'email') def _compute_email_formatted(self): for partner in self: partner.email_formatted = formataddr( (partner.name or u"False", partner.email or u"False")) @api.depends('is_company') def _compute_company_type(self): for partner in self: partner.company_type = 'company' if partner.is_company else 'person' def _write_company_type(self): for partner in self: partner.is_company = partner.company_type == 'company' @api.onchange('company_type') def onchange_company_type(self): self.is_company = (self.company_type == 'company') @api.multi def _update_fields_values(self, fields): """ Returns dict of write() values for synchronizing ``fields`` """ values = {} for fname in fields: field = self._fields[fname] if field.type == 'many2one': values[fname] = self[fname].id elif field.type == 'one2many': raise AssertionError( _('One2Many fields cannot be synchronized as part of `commercial_fields` or `address fields`' )) elif field.type == 'many2many': values[fname] = [(6, 0, self[fname].ids)] else: values[fname] = self[fname] return values @api.model def _address_fields(self): """Returns the list of address fields that are synced from the parent.""" return list(ADDRESS_FIELDS) @api.multi def update_address(self, vals): addr_vals = { key: vals[key] for key in self._address_fields() if key in vals } if addr_vals: return super(Partner, self).write(addr_vals) @api.model def _commercial_fields(self): """ Returns the list of fields that are managed by the commercial entity to which a partner belongs. These fields are meant to be hidden on partners that aren't `commercial entities` themselves, and will be delegated to the parent `commercial entity`. The list is meant to be extended by inheriting classes. """ return ['vat', 'credit_limit'] @api.multi def _commercial_sync_from_company(self): """ Handle sync of commercial fields when a new parent commercial entity is set, as if they were related fields """ commercial_partner = self.commercial_partner_id if commercial_partner != self: sync_vals = commercial_partner._update_fields_values( self._commercial_fields()) self.write(sync_vals) @api.multi def _commercial_sync_to_children(self): """ Handle sync of commercial fields to descendants """ commercial_partner = self.commercial_partner_id sync_vals = commercial_partner._update_fields_values( self._commercial_fields()) sync_children = self.child_ids.filtered(lambda c: not c.is_company) for child in sync_children: child._commercial_sync_to_children() sync_children._compute_commercial_partner() return sync_children.write(sync_vals) @api.multi def _fields_sync(self, values): """ Sync commercial fields and address fields from company and to children after create/update, just as if those were all modeled as fields.related to the parent """ # 1. From UPSTREAM: sync from parent if values.get('parent_id') or values.get('type', 'contact'): # 1a. Commercial fields: sync if parent changed if values.get('parent_id'): self._commercial_sync_from_company() # 1b. Address fields: sync if parent or use_parent changed *and* both are now set if self.parent_id and self.type == 'contact': onchange_vals = self.onchange_parent_id().get('value', {}) self.update_address(onchange_vals) # 2. To DOWNSTREAM: sync children if self.child_ids: # 2a. Commercial Fields: sync if commercial entity if self.commercial_partner_id == self: commercial_fields = self._commercial_fields() if any(field in values for field in commercial_fields): self._commercial_sync_to_children() for child in self.child_ids.filtered(lambda c: not c.is_company): if child.commercial_partner_id != self.commercial_partner_id: self._commercial_sync_to_children() break # 2b. Address fields: sync if address changed address_fields = self._address_fields() if any(field in values for field in address_fields): contacts = self.child_ids.filtered( lambda c: c.type == 'contact') contacts.update_address(values) @api.multi def _handle_first_contact_creation(self): """ On creation of first contact for a company (or root) that has no address, assume contact address was meant to be company address """ parent = self.parent_id address_fields = self._address_fields() if (parent.is_company or not parent.parent_id) and len(parent.child_ids) == 1 and \ any(self[f] for f in address_fields) and not any(parent[f] for f in address_fields): addr_vals = self._update_fields_values(address_fields) parent.update_address(addr_vals) def _clean_website(self, website): url = urls.url_parse(website) if not url.scheme: if not url.netloc: url = url.replace(netloc=url.path, path='') website = url.replace(scheme='http').to_url() return website @api.multi def write(self, vals): # res.partner must only allow to set the company_id of a partner if it # is the same as the company of all users that inherit from this partner # (this is to allow the code from res_users to write to the partner!) or # if setting the company_id to False (this is compatible with any user # company) if vals.get('website'): vals['website'] = self._clean_website(vals['website']) if vals.get('parent_id'): vals['company_name'] = False if vals.get('company_id'): company = self.env['res.company'].browse(vals['company_id']) for partner in self: if partner.user_ids: companies = set(user.company_id for user in partner.user_ids) if len(companies) > 1 or company not in companies: raise UserError( _("You can not change the company as the partner/user has multiple user linked with different companies." )) tools.image_resize_images(vals) result = True # To write in SUPERUSER on field is_company and avoid access rights problems. if 'is_company' in vals and self.user_has_groups( 'base.group_partner_manager' ) and not self.env.uid == SUPERUSER_ID: result = super(Partner, self).sudo().write( {'is_company': vals.get('is_company')}) del vals['is_company'] result = result and super(Partner, self).write(vals) for partner in self: if any( u.has_group('base.group_user') for u in partner.user_ids if u != self.env.user): self.env['res.users'].check_access_rights('write') partner._fields_sync(vals) return result @api.model def create(self, vals): if vals.get('website'): vals['website'] = self._clean_website(vals['website']) if vals.get('parent_id'): vals['company_name'] = False # compute default image in create, because computing gravatar in the onchange # cannot be easily performed if default images are in the way if not vals.get('image'): vals['image'] = self._get_default_image(vals.get('type'), vals.get('is_company'), vals.get('parent_id')) tools.image_resize_images(vals) partner = super(Partner, self).create(vals) partner._fields_sync(vals) partner._handle_first_contact_creation() return partner @api.multi def create_company(self): self.ensure_one() if self.company_name: # Create parent company values = dict(name=self.company_name, is_company=True) values.update(self._update_fields_values(self._address_fields())) new_company = self.create(values) # Set new company as my parent self.write({ 'parent_id': new_company.id, 'child_ids': [(1, partner_id, dict(parent_id=new_company.id)) for partner_id in self.child_ids.ids] }) return True @api.multi def open_commercial_entity(self): """ Utility method used to add an "Open Company" button in partner views """ self.ensure_one() return { 'type': 'ir.actions.act_window', 'res_model': 'res.partner', 'view_mode': 'form', 'res_id': self.commercial_partner_id.id, 'target': 'current', 'flags': { 'form': { 'action_buttons': True } } } @api.multi def open_parent(self): """ Utility method used to add an "Open Parent" button in partner views """ self.ensure_one() address_form_id = self.env.ref('base.view_partner_address_form').id return { 'type': 'ir.actions.act_window', 'res_model': 'res.partner', 'view_mode': 'form', 'views': [(address_form_id, 'form')], 'res_id': self.parent_id.id, 'target': 'new', 'flags': { 'form': { 'action_buttons': True } } } @api.multi def name_get(self): res = [] for partner in self: name = partner.name or '' if partner.company_name or partner.parent_id: if not name and partner.type in [ 'invoice', 'delivery', 'other' ]: name = dict( self.fields_get(['type' ])['type']['selection'])[partner.type] if not partner.is_company: name = "%s, %s" % (partner.commercial_company_name or partner.parent_id.name, name) if self._context.get('show_address_only'): name = partner._display_address(without_company=True) if self._context.get('show_address'): name = name + "\n" + partner._display_address( without_company=True) name = name.replace('\n\n', '\n') name = name.replace('\n\n', '\n') if self._context.get('show_email') and partner.email: name = "%s <%s>" % (name, partner.email) if self._context.get('html_format'): name = name.replace('\n', '<br/>') res.append((partner.id, name)) return res def _parse_partner_name(self, text, context=None): """ Supported syntax: - 'Raoul <*****@*****.**>': will find name and email address - otherwise: default, everything is set as the name """ emails = tools.email_split(text.replace(' ', ',')) if emails: email = emails[0] name = text[:text.index(email)].replace('"', '').replace('<', '').strip() else: name, email = text, '' return name, email @api.model def name_create(self, name): """ Override of orm's name_create method for partners. The purpose is to handle some basic formats to create partners using the name_create. If only an email address is received and that the regex cannot find a name, the name will have the email value. If 'force_email' key in context: must find the email address. """ name, email = self._parse_partner_name(name) if self._context.get('force_email') and not email: raise UserError( _("Couldn't create contact without email address!")) if not name and email: name = email partner = self.create({ self._rec_name: name or email, 'email': email or self.env.context.get('default_email', False) }) return partner.name_get()[0] @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): """ Override search() to always show inactive children when searching via ``child_of`` operator. The ORM will always call search() with a simple domain of the form [('parent_id', 'in', [ids])]. """ # a special ``domain`` is set on the ``child_ids`` o2m to bypass this logic, as it uses similar domain expressions if len(args) == 1 and len(args[0]) == 3 and args[0][:2] == ('parent_id','in') \ and args[0][2] != [False]: self = self.with_context(active_test=False) return super(Partner, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) @api.model def name_search(self, name, args=None, operator='ilike', limit=100): if args is None: args = [] if name and operator in ('=', 'ilike', '=ilike', 'like', '=like'): self.check_access_rights('read') where_query = self._where_calc(args) self._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql( ) where_str = where_clause and (" WHERE %s AND " % where_clause) or ' WHERE ' # search on the name of the contacts and of its company search_name = name if operator in ('ilike', 'like'): search_name = '%%%s%%' % name if operator in ('=ilike', '=like'): operator = operator[1:] unaccent = get_unaccent_wrapper(self.env.cr) query = """SELECT id FROM res_partner {where} ({email} {operator} {percent} OR {display_name} {operator} {percent} OR {reference} {operator} {percent} OR {vat} {operator} {percent}) -- don't panic, trust postgres bitmap ORDER BY {display_name} {operator} {percent} desc, {display_name} """.format( where=where_str, operator=operator, email=unaccent('email'), display_name=unaccent('display_name'), reference=unaccent('ref'), percent=unaccent('%s'), vat=unaccent('vat'), ) where_clause_params += [search_name] * 5 if limit: query += ' limit %s' where_clause_params.append(limit) self.env.cr.execute(query, where_clause_params) partner_ids = [row[0] for row in self.env.cr.fetchall()] if partner_ids: return self.browse(partner_ids).name_get() else: return [] return super(Partner, self).name_search(name, args, operator=operator, limit=limit) @api.model def find_or_create(self, email): """ Find a partner with the given ``email`` or use :py:method:`~.name_create` to create one :param str email: email-like string, which should contain at least one email, e.g. ``"Raoul Grosbedon <*****@*****.**>"``""" assert email, 'an email is required for find_or_create to work' emails = tools.email_split(email) if emails: email = emails[0] partners = self.search([('email', '=ilike', email)], limit=1) return partners.id or self.name_create(email)[0] def _get_gravatar_image(self, email): email_hash = hashlib.md5(email.lower().encode('utf-8')).hexdigest() url = "https://www.gravatar.com/avatar/" + email_hash try: res = requests.get(url, params={'d': '404', 's': '128'}, timeout=5) if res.status_code != requests.codes.ok: return False except requests.exceptions.ConnectionError as e: return False return base64.b64encode(res.content) @api.multi def _email_send(self, email_from, subject, body, on_error=None): for partner in self.filtered('email'): tools.email_send(email_from, [partner.email], subject, body, on_error) return True @api.multi def address_get(self, adr_pref=None): """ Find contacts/addresses of the right type(s) by doing a depth-first-search through descendants within company boundaries (stop at entities flagged ``is_company``) then continuing the search at the ancestors that are within the same company boundaries. Defaults to partners of type ``'default'`` when the exact type is not found, or to the provided partner itself if no type ``'default'`` is found either. """ adr_pref = set(adr_pref or []) if 'contact' not in adr_pref: adr_pref.add('contact') result = {} visited = set() for partner in self: current_partner = partner while current_partner: to_scan = [current_partner] # Scan descendants, DFS while to_scan: record = to_scan.pop(0) visited.add(record) if record.type in adr_pref and not result.get(record.type): result[record.type] = record.id if len(result) == len(adr_pref): return result to_scan = [ c for c in record.child_ids if c not in visited if not c.is_company ] + to_scan # Continue scanning at ancestor if current_partner is not a commercial entity if current_partner.is_company or not current_partner.parent_id: break current_partner = current_partner.parent_id # default to type 'contact' or the partner itself default = result.get('contact', self.id or False) for adr_type in adr_pref: result[adr_type] = result.get(adr_type) or default return result @api.model def view_header_get(self, view_id, view_type): res = super(Partner, self).view_header_get(view_id, view_type) if res: return res if not self._context.get('category_id'): return False return _('Partners: ') + self.env['res.partner.category'].browse( self._context['category_id']).name @api.model @api.returns('self') def main_partner(self): ''' Return the main partner ''' return self.env.ref('base.main_partner') @api.model def _get_default_address_format(self): return "%(street)s\n%(street2)s\n%(city)s %(state_code)s %(zip)s\n%(country_name)s" @api.multi def _display_address(self, without_company=False): ''' The purpose of this function is to build and return an address formatted accordingly to the standards of the country where it belongs. :param address: browse record of the res.partner to format :returns: the address formatted in a display that fit its country habits (or the default ones if not country is specified) :rtype: string ''' # get the information that will be injected into the display format # get the address format address_format = self.country_id.address_format or \ self._get_default_address_format() args = { 'state_code': self.state_id.code or '', 'state_name': self.state_id.name or '', 'country_code': self.country_id.code or '', 'country_name': self.country_id.name or '', 'company_name': self.commercial_company_name or '', } for field in self._address_fields(): args[field] = getattr(self, field) or '' if without_company: args['company_name'] = '' elif self.commercial_company_name: address_format = '%(company_name)s\n' + address_format return address_format % args def _display_address_depends(self): # field dependencies of method _display_address() return self._address_fields() + [ 'country_id.address_format', 'country_id.code', 'country_id.name', 'company_name', 'state_id.code', 'state_id.name', ]