class HrPayslipRun(models.Model): _name = 'hr.payslip.run' _description = 'Payslip Batches' name = fields.Char(required=True, readonly=True, states={'draft': [('readonly', False)]}) slip_ids = fields.One2many('hr.payslip', 'payslip_run_id', string='Payslips', readonly=True, states={'draft': [('readonly', False)]}) state = fields.Selection([ ('draft', 'Draft'), ('close', 'Close'), ], string='Status', index=True, readonly=True, copy=False, default='draft') date_start = fields.Date(string='Date From', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: fields.Date.to_string(date.today().replace(day=1))) date_end = fields.Date(string='Date To', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: fields.Date.to_string((datetime.now() + relativedelta(months=+1, day=1, days=-1)).date())) credit_note = fields.Boolean(string='Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="If its checked, indicates that all payslips generated from here are refund payslips.") @api.multi def draft_payslip_run(self): return self.write({'state': 'draft'}) @api.multi def close_payslip_run(self): return self.write({'state': 'close'}) @api.multi def unlink(self): if any(self.filtered(lambda payslip_run: payslip_run.state not in ('draft'))): raise UserError(_('You cannot delete a payslip batch which is not draft!')) if any(self.mapped('slip_ids').filtered(lambda payslip: payslip.state not in ('draft','cancel'))): raise UserError(_('You cannot delete a payslip which is not draft or cancelled!')) return super(HrPayslipRun, self).unlink()
class Product(models.Model): _inherit = 'product.template' membership = fields.Boolean( help='Check if the product is eligible for membership.') membership_date_from = fields.Date( string='Membership Start Date', help='Date from which membership becomes active.') membership_date_to = fields.Date( string='Membership End Date', help='Date until which membership remains active.') _sql_constraints = [ ('membership_date_greater', 'check(membership_date_to >= membership_date_from)', 'Error ! Ending Date cannot be set before Beginning Date.') ] @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): if self._context.get('product') == 'membership_product': if view_type == 'form': view_id = self.env.ref( 'membership.membership_products_form').id else: view_id = self.env.ref( 'membership.membership_products_tree').id return super(Product, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu)
class AccountFiscalYear(models.Model): _name = 'account.fiscal.year' _description = 'Fiscal Year' name = fields.Char(string='Name', required=True) date_from = fields.Date(string='Start Date', required=True, help='Start Date, included in the fiscal year.') date_to = fields.Date(string='End Date', required=True, help='Ending Date, included in the fiscal year.') company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) @api.constrains('date_from', 'date_to', 'company_id') def _check_dates(self): ''' Check interleaving between fiscal years. There are 3 cases to consider: s1 s2 e1 e2 ( [----)----] s2 s1 e2 e1 [----(----] ) s1 s2 e2 e1 ( [----] ) ''' for fy in self: # Starting date must be prior to the ending date date_from = fy.date_from date_to = fy.date_to if date_to < date_from: raise ValidationError( _('The ending date must not be prior to the starting date.' )) domain = [ ('id', '!=', fy.id), ('company_id', '=', fy.company_id.id), '|', '|', '&', ('date_from', '<=', fy.date_from), ('date_to', '>=', fy.date_from), '&', ('date_from', '<=', fy.date_to), ('date_to', '>=', fy.date_to), '&', ('date_from', '<=', fy.date_from), ('date_to', '>=', fy.date_to), ] if self.search_count(domain) > 0: raise ValidationError( _('You can not have an overlap between two fiscal years, please correct the start and/or end dates of your fiscal years.' ))
class FleetVehicleAssignationLog(models.Model): _name = "fleet.vehicle.assignation.log" _description = "Drivers history on a vehicle" _order = "date_start" vehicle_id = fields.Many2one('fleet.vehicle', string="Vehicle", required=True) driver_id = fields.Many2one('res.partner', string="Driver", required=True) date_start = fields.Date(string="Start Date") date_end = fields.Date(string="End Date")
class ProductMargin(models.TransientModel): _name = 'product.margin' _description = 'Product Margin' from_date = fields.Date('From', default=time.strftime('%Y-01-01')) to_date = fields.Date('To', default=time.strftime('%Y-12-31')) invoice_state = fields.Selection([ ('paid', 'Paid'), ('open_paid', 'Open and Paid'), ('draft_open_paid', 'Draft, Open and Paid'), ], 'Invoice State', index=True, required=True, default="open_paid") @api.multi def action_open_window(self): self.ensure_one() context = dict(self.env.context or {}) def ref(module, xml_id): proxy = self.env['ir.model.data'] return proxy.get_object_reference(module, xml_id) model, search_view_id = ref('product', 'product_search_form_view') model, graph_view_id = ref('product_margin', 'view_product_margin_graph') model, form_view_id = ref('product_margin', 'view_product_margin_form') model, tree_view_id = ref('product_margin', 'view_product_margin_tree') context.update(invoice_state=self.invoice_state) if self.from_date: context.update(date_from=self.from_date) if self.to_date: context.update(date_to=self.to_date) views = [(tree_view_id, 'tree'), (form_view_id, 'form'), (graph_view_id, 'graph')] return { 'name': _('Product Margins'), 'context': context, 'view_type': 'form', "view_mode": 'tree,form,graph', 'res_model': 'product.product', 'type': 'ir.actions.act_window', 'views': views, 'view_id': False, 'search_view_id': search_view_id, }
class HrSalaryEmployeeBymonth(models.TransientModel): _name = 'hr.salary.employee.month' _description = 'Hr Salary Employee By Month Report' def _get_default_category(self): return self.env['hr.salary.rule.category'].search( [('code', '=', 'NET')], limit=1) def _get_default_start_date(self): year = fields.Date.from_string(fields.Date.today()).strftime('%Y') return '{}-01-01'.format(year) def _get_default_end_date(self): date = fields.Date.from_string(fields.Date.today()) return date.strftime('%Y') + '-' + date.strftime( '%m') + '-' + date.strftime('%d') start_date = fields.Date(string='Start Date', required=True, default=_get_default_start_date) end_date = fields.Date(string='End Date', required=True, default=_get_default_end_date) employee_ids = fields.Many2many('hr.employee', 'payroll_year_rel', 'payroll_year_id', 'employee_id', string='Employees', required=True) category_id = fields.Many2one('hr.salary.rule.category', string='Category', required=True, default=_get_default_category) @api.multi def print_report(self): """ To get the date and print the report @return: return report """ self.ensure_one() data = {'ids': self.env.context.get('active_ids', [])} res = self.read() res = res and res[0] or {} data.update({'form': res}) return self.env.ref( 'l10n_in_hr_payroll.action_report_hrsalarybymonth').report_action( self, data=data)
class FleetVehicleOdometer(models.Model): _name = 'fleet.vehicle.odometer' _description = 'Odometer log for a vehicle' _order = 'date desc' name = fields.Char(compute='_compute_vehicle_log_name', store=True) date = fields.Date(default=fields.Date.context_today) value = fields.Float('Odometer Value', group_operator="max") vehicle_id = fields.Many2one('fleet.vehicle', 'Vehicle', required=True) unit = fields.Selection(related='vehicle_id.odometer_unit', string="Unit", readonly=True) driver_id = fields.Many2one(related="vehicle_id.driver_id", string="Driver", readonly=False) @api.depends('vehicle_id', 'date') def _compute_vehicle_log_name(self): for record in self: name = record.vehicle_id.name if not name: name = str(record.date) elif record.date: name += ' / ' + str(record.date) record.name = name @api.onchange('vehicle_id') def _onchange_vehicle(self): if self.vehicle_id: self.unit = self.vehicle_id.odometer_unit
class HolidaysSummaryDept(models.TransientModel): _name = 'hr.holidays.summary.dept' _description = 'HR Leaves Summary Report By Department' date_from = fields.Date(string='From', required=True, default=lambda *a: time.strftime('%Y-%m-01')) depts = fields.Many2many('hr.department', 'summary_dept_rel', 'sum_id', 'dept_id', string='Department(s)') holiday_type = fields.Selection([('Approved', 'Approved'), ('Confirmed', 'Confirmed'), ('both', 'Both Approved and Confirmed')], string='Leave Type', required=True, default='Approved') @api.multi def print_report(self): self.ensure_one() [data] = self.read() if not data.get('depts'): raise UserError(_('You have to select at least one department.')) departments = self.env['hr.department'].browse(data['depts']) datas = {'ids': [], 'model': 'hr.department', 'form': data} return self.env.ref( 'hr_holidays.action_report_holidayssummary').with_context( from_transient_model=True).report_action(departments, data=datas)
class HolidaysSummaryEmployee(models.TransientModel): _name = 'hr.holidays.summary.employee' _description = 'HR Leaves Summary Report By Employee' date_from = fields.Date(string='From', required=True, default=lambda *a: time.strftime('%Y-%m-01')) emp = fields.Many2many('hr.employee', 'summary_emp_rel', 'sum_id', 'emp_id', string='Employee(s)') holiday_type = fields.Selection([('Approved', 'Approved'), ('Confirmed', 'Confirmed'), ('both', 'Both Approved and Confirmed')], string='Select Leave Type', required=True, default='Approved') @api.multi def print_report(self): self.ensure_one() [data] = self.read() data['emp'] = self.env.context.get('active_ids', []) employees = self.env['hr.employee'].browse(data['emp']) datas = {'ids': [], 'model': 'hr.employee', 'form': data} return self.env.ref( 'hr_holidays.action_report_holidayssummary').report_action( employees, data=datas)
class ConverterTest(models.Model): _name = 'web_editor.converter.test' _description = 'Web Editor Converter Test' # disable translation export for those brilliant field labels and values _translate = False char = fields.Char() integer = fields.Integer() float = fields.Float() numeric = fields.Float(digits=(16, 2)) many2one = fields.Many2one('web_editor.converter.test.sub') binary = fields.Binary() date = fields.Date() datetime = fields.Datetime() selection = fields.Selection([ (1, "réponse A"), (2, "réponse B"), (3, "réponse C"), (4, "réponse <D>"), ]) selection_str = fields.Selection([ ('A', "Qu'il n'est pas arrivé à Toronto"), ('B', "Qu'il était supposé arriver à Toronto"), ('C', "Qu'est-ce qu'il fout ce maudit pancake, tabernacle ?"), ('D', "La réponse D"), ], string=u"Lorsqu'un pancake prend l'avion à destination de Toronto et " u"qu'il fait une escale technique à St Claude, on dit:") html = fields.Html() text = fields.Text()
class CurrencyRate(models.Model): _name = "res.currency.rate" _description = "Currency Rate" _order = "name desc" name = fields.Date(string='Date', required=True, index=True, default=lambda self: fields.Date.today()) rate = fields.Float( digits=(12, 6), default=1.0, help='The rate of the currency to the currency of rate 1') currency_id = fields.Many2one('res.currency', string='Currency', readonly=True) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id) _sql_constraints = [ ('unique_name_per_day', 'unique (name,currency_id,company_id)', 'Only one currency rate per day allowed!'), ('currency_rate_check', 'CHECK (rate>0)', 'The currency rate must be strictly positive.'), ] @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): if operator in ['=', '!=']: try: date_format = '%Y-%m-%d' if self._context.get('lang'): lang_id = self.env['res.lang']._search( [('code', '=', self._context['lang'])], access_rights_uid=name_get_uid) if lang_id: date_format = self.browse(lang_id).date_format name = time.strftime('%Y-%m-%d', time.strptime(name, date_format)) except ValueError: try: args.append(('rate', operator, float(name))) except ValueError: return [] name = '' operator = 'ilike' return super(CurrencyRate, self)._name_search(name, args=args, operator=operator, limit=limit, name_get_uid=name_get_uid)
class PayslipLinesContributionRegister(models.TransientModel): _name = 'payslip.lines.contribution.register' _description = 'Payslip Lines by Contribution Registers' date_from = fields.Date(string='Date From', required=True, default=datetime.now().strftime('%Y-%m-01')) date_to = fields.Date(string='Date To', required=True, default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1))[:10]) @api.multi def print_report(self): active_ids = self.env.context.get('active_ids', []) datas = { 'ids': active_ids, 'model': 'hr.contribution.register', 'form': self.read()[0] } return self.env.ref('hr_payroll.action_contribution_register').report_action([], data=datas)
class AccountAnalyticLine(models.Model): _name = 'account.analytic.line' _description = 'Analytic Line' _order = 'date desc, id desc' @api.model def _default_user(self): return self.env.context.get('user_id', self.env.user.id) name = fields.Char('Description', required=True) date = fields.Date('Date', required=True, index=True, default=fields.Date.context_today) amount = fields.Monetary('Amount', required=True, default=0.0) unit_amount = fields.Float('Quantity', default=0.0) product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure') account_id = fields.Many2one('account.analytic.account', 'Analytic Account', required=True, ondelete='restrict', index=True) partner_id = fields.Many2one('res.partner', string='Partner') user_id = fields.Many2one('res.users', string='User', default=_default_user) tag_ids = fields.Many2many('account.analytic.tag', 'account_analytic_line_tag_rel', 'line_id', 'tag_id', string='Tags', copy=True) company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.user.company_id) currency_id = fields.Many2one(related="company_id.currency_id", string="Currency", readonly=True, store=True, compute_sudo=True) group_id = fields.Many2one('account.analytic.group', related='account_id.group_id', store=True, readonly=True, compute_sudo=True) @api.multi @api.constrains('company_id', 'account_id') def _check_company_id(self): for line in self: if line.account_id.company_id and line.company_id.id != line.account_id.company_id.id: raise ValidationError( _('The selected account belongs to another company that the one you\'re trying to create an analytic item for' ))
class ComplexModel(models.Model): _name = name('complex') _description = 'Tests: Base Import Model Complex' f = fields.Float() m = fields.Monetary() c = fields.Char() currency_id = fields.Many2one('res.currency') d = fields.Date() dt = fields.Datetime()
class YearlySalaryDetail(models.TransientModel): _name = 'yearly.salary.detail' _description = 'Hr Salary Employee By Category Report' def _get_default_date_from(self): year = fields.Date.from_string(fields.Date.today()).strftime('%Y') return '{}-01-01'.format(year) def _get_default_date_to(self): date = fields.Date.from_string(fields.Date.today()) return date.strftime('%Y') + '-' + date.strftime( '%m') + '-' + date.strftime('%d') employee_ids = fields.Many2many('hr.employee', 'payroll_emp_rel', 'payroll_id', 'employee_id', string='Employees', required=True) date_from = fields.Date(string='Start Date', required=True, default=_get_default_date_from) date_to = fields.Date(string='End Date', required=True, default=_get_default_date_to) @api.multi def print_report(self): """ To get the date and print the report @return: return report """ self.ensure_one() data = {'ids': self.env.context.get('active_ids', [])} res = self.read() res = res and res[0] or {} data.update({'form': res}) return self.env.ref( 'l10n_in_hr_payroll.action_report_hryearlysalary').report_action( self, data=data)
class TimesheetAttendance(models.Model): _name = 'hr.timesheet.attendance.report' _auto = False _description = 'Timesheet Attendance Report' user_id = fields.Many2one('res.users') date = fields.Date() total_timesheet = fields.Float() total_attendance = fields.Float() total_difference = fields.Float() @api.model_cr def init(self): tools.drop_view_if_exists(self.env.cr, self._table) self._cr.execute("""CREATE OR REPLACE VIEW %s AS ( SELECT max(id) AS id, t.user_id, t.date, coalesce(sum(t.attendance), 0) AS total_attendance, coalesce(sum(t.timesheet), 0) AS total_timesheet, coalesce(sum(t.attendance), 0) - coalesce(sum(t.timesheet), 0) as total_difference FROM ( SELECT -hr_attendance.id AS id, resource_resource.user_id AS user_id, hr_attendance.worked_hours AS attendance, NULL AS timesheet, hr_attendance.check_in::date AS date FROM hr_attendance LEFT JOIN hr_employee ON hr_employee.id = hr_attendance.employee_id LEFT JOIN resource_resource on resource_resource.id = hr_employee.resource_id UNION ALL SELECT ts.id AS id, ts.user_id AS user_id, NULL AS attendance, ts.unit_amount AS timesheet, ts.date AS date FROM account_analytic_line AS ts WHERE ts.project_id IS NOT NULL ) AS t GROUP BY t.user_id, t.date ORDER BY t.date ) """ % self._table)
class ResPartner(models.Model): _inherit = "res.partner" partner_latitude = fields.Float(string='Geo Latitude', digits=(16, 5)) partner_longitude = fields.Float(string='Geo Longitude', digits=(16, 5)) date_localization = fields.Date(string='Geolocation Date') @classmethod def _geo_localize(cls, apikey, street='', zip='', city='', state='', country=''): search = geo_query_address(street=street, zip=zip, city=city, state=state, country=country) result = geo_find(search, apikey) if result is None: search = geo_query_address(city=city, state=state, country=country) result = geo_find(search, apikey) return result @api.multi def geo_localize(self): # We need country names in English below apikey = self.env['ir.config_parameter'].sudo().get_param( 'google.api_key_geocode') for partner in self.with_context(lang='en_US'): result = partner._geo_localize(apikey, partner.street, partner.zip, partner.city, partner.state_id.name, partner.country_id.name) if result: partner.write({ 'partner_latitude': result[0], 'partner_longitude': result[1], 'date_localization': fields.Date.context_today(partner) }) return True
class StockInventory(models.Model): _inherit = "stock.inventory" accounting_date = fields.Date( 'Accounting Date', help="Date at which the accounting entries will be created" " in case of automated inventory valuation." " If empty, the inventory date will be used.") @api.multi def post_inventory(self): res = True acc_inventories = self.filtered(lambda inventory: inventory.accounting_date) for inventory in acc_inventories: res = super(StockInventory, inventory.with_context(force_period_date=inventory.accounting_date)).post_inventory() other_inventories = self - acc_inventories if other_inventories: res = super(StockInventory, other_inventories).post_inventory() return res
class Employee(models.Model): _inherit = "hr.employee" manager = fields.Boolean(string='Is a Manager') medic_exam = fields.Date(string='Medical Examination Date', groups="hr.group_hr_user") vehicle = fields.Char(string='Company Vehicle', groups="hr.group_hr_user") contract_ids = fields.One2many('hr.contract', 'employee_id', string='Employee Contracts') contract_id = fields.Many2one('hr.contract', compute='_compute_contract_id', string='Current Contract', help='Latest contract of the employee') contracts_count = fields.Integer(compute='_compute_contracts_count', string='Contract Count') def _compute_contract_id(self): """ get the lastest contract """ Contract = self.env['hr.contract'] for employee in self: employee.contract_id = Contract.search([('employee_id', '=', employee.id)], order='date_start desc', limit=1) def _compute_contracts_count(self): # read_group as sudo, since contract count is displayed on form view contract_data = self.env['hr.contract'].sudo().read_group([('employee_id', 'in', self.ids)], ['employee_id'], ['employee_id']) result = dict((data['employee_id'][0], data['employee_id_count']) for data in contract_data) for employee in self: employee.contracts_count = result.get(employee.id, 0)
class AccountMoveReversal(models.TransientModel): """ Account move reversal wizard, it cancel an account move by reversing it. """ _name = 'account.move.reversal' _description = 'Account Move Reversal' date = fields.Date(string='Reversal date', default=fields.Date.context_today, required=True) journal_id = fields.Many2one('account.journal', string='Use Specific Journal', help='If empty, uses the journal of the journal entry to be reversed.') @api.multi def reverse_moves(self): ac_move_ids = self._context.get('active_ids', False) res = self.env['account.move'].browse(ac_move_ids).reverse_moves(self.date, self.journal_id or False) if res: return { 'name': _('Reverse Moves'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move', 'domain': [('id', 'in', res)], } return {'type': 'ir.actions.act_window_close'}
class PricelistItem(models.Model): _name = "product.pricelist.item" _description = "Pricelist Item" _order = "applied_on, min_quantity desc, categ_id desc, id desc" # NOTE: if you change _order on this model, make sure it matches the SQL # query built in _compute_price_rule() above in this file to avoid # inconstencies and undeterministic issues. product_tmpl_id = fields.Many2one( 'product.template', 'Product Template', ondelete='cascade', help= "Specify a template if this rule only applies to one product template. Keep empty otherwise." ) product_id = fields.Many2one( 'product.product', 'Product', ondelete='cascade', help= "Specify a product if this rule only applies to one product. Keep empty otherwise." ) categ_id = fields.Many2one( 'product.category', 'Product Category', ondelete='cascade', help= "Specify a product category if this rule only applies to products belonging to this category or its children categories. Keep empty otherwise." ) min_quantity = fields.Integer( 'Min. Quantity', default=0, help="For the rule to apply, bought/sold quantity must be greater " "than or equal to the minimum quantity specified in this field.\n" "Expressed in the default unit of measure of the product.") applied_on = fields.Selection( [('3_global', 'Global'), ('2_product_category', ' Product Category'), ('1_product', 'Product'), ('0_product_variant', 'Product Variant')], "Apply On", default='3_global', required=True, help='Pricelist Item applicable on selected option') base = fields.Selection( [('list_price', 'Public Price'), ('standard_price', 'Cost'), ('pricelist', 'Other Pricelist')], "Based on", default='list_price', required=True, help='Base price for computation.\n' 'Public Price: The base price will be the Sale/public Price.\n' 'Cost Price : The base price will be the cost price.\n' 'Other Pricelist : Computation of the base price based on another Pricelist.' ) base_pricelist_id = fields.Many2one('product.pricelist', 'Other Pricelist') pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', index=True, ondelete='cascade') price_surcharge = fields.Float( 'Price Surcharge', digits=dp.get_precision('Product Price'), help= 'Specify the fixed amount to add or substract(if negative) to the amount calculated with the discount.' ) price_discount = fields.Float('Price Discount', default=0, digits=(16, 2)) price_round = fields.Float( 'Price Rounding', digits=dp.get_precision('Product Price'), help="Sets the price so that it is a multiple of this value.\n" "Rounding is applied after the discount and before the surcharge.\n" "To have prices that end in 9.99, set rounding 10, surcharge -0.01") price_min_margin = fields.Float( 'Min. Price Margin', digits=dp.get_precision('Product Price'), help='Specify the minimum amount of margin over the base price.') price_max_margin = fields.Float( 'Max. Price Margin', digits=dp.get_precision('Product Price'), help='Specify the maximum amount of margin over the base price.') company_id = fields.Many2one('res.company', 'Company', readonly=True, related='pricelist_id.company_id', store=True) currency_id = fields.Many2one('res.currency', 'Currency', readonly=True, related='pricelist_id.currency_id', store=True) date_start = fields.Date( 'Start Date', help="Starting date for the pricelist item validation") date_end = fields.Date( 'End Date', help="Ending valid for the pricelist item validation") compute_price = fields.Selection([('fixed', 'Fix Price'), ('percentage', 'Percentage (discount)'), ('formula', 'Formula')], index=True, default='fixed') fixed_price = fields.Float('Fixed Price', digits=dp.get_precision('Product Price')) percent_price = fields.Float('Percentage Price') # functional fields used for usability purposes name = fields.Char('Name', compute='_get_pricelist_item_name_price', help="Explicit rule name for this pricelist line.") price = fields.Char('Price', compute='_get_pricelist_item_name_price', help="Explicit rule name for this pricelist line.") @api.constrains('base_pricelist_id', 'pricelist_id', 'base') def _check_recursion(self): if any(item.base == 'pricelist' and item.pricelist_id and item.pricelist_id == item.base_pricelist_id for item in self): raise ValidationError( _('You cannot assign the Main Pricelist as Other Pricelist in PriceList Item' )) return True @api.constrains('price_min_margin', 'price_max_margin') def _check_margin(self): if any(item.price_min_margin > item.price_max_margin for item in self): raise ValidationError( _('The minimum margin should be lower than the maximum margin.' )) return True @api.one @api.depends('categ_id', 'product_tmpl_id', 'product_id', 'compute_price', 'fixed_price', \ 'pricelist_id', 'percent_price', 'price_discount', 'price_surcharge') def _get_pricelist_item_name_price(self): if self.categ_id: self.name = _("Category: %s") % (self.categ_id.name) elif self.product_tmpl_id: self.name = self.product_tmpl_id.name elif self.product_id: self.name = self.product_id.display_name.replace( '[%s]' % self.product_id.code, '') else: self.name = _("All Products") if self.compute_price == 'fixed': self.price = ("%s %s") % (float_repr( self.fixed_price, self.pricelist_id.currency_id.decimal_places, ), self.pricelist_id.currency_id.name) elif self.compute_price == 'percentage': self.price = _("%s %% discount") % (self.percent_price) else: self.price = _("%s %% discount and %s surcharge") % ( self.price_discount, self.price_surcharge) @api.onchange('applied_on') def _onchange_applied_on(self): if self.applied_on != '0_product_variant': self.product_id = False if self.applied_on != '1_product': self.product_tmpl_id = False if self.applied_on != '2_product_category': self.categ_id = False @api.onchange('compute_price') def _onchange_compute_price(self): if self.compute_price != 'fixed': self.fixed_price = 0.0 if self.compute_price != 'percentage': self.percent_price = 0.0 if self.compute_price != 'formula': self.update({ 'price_discount': 0.0, 'price_surcharge': 0.0, 'price_round': 0.0, 'price_min_margin': 0.0, 'price_max_margin': 0.0, }) @api.multi def write(self, values): res = super(PricelistItem, self).write(values) # When the pricelist changes we need the product.template price # to be invalided and recomputed. self.invalidate_cache() return res def _compute_price(self, price, price_uom, product, quantity=1.0, partner=False): """Compute the unit price of a product in the context of a pricelist application. The unused parameters are there to make the full context available for overrides. """ self.ensure_one() convert_to_price_uom = ( lambda price: product.uom_id._compute_price(price, price_uom)) if self.compute_price == 'fixed': price = convert_to_price_uom(self.fixed_price) elif self.compute_price == 'percentage': price = (price - (price * (self.percent_price / 100))) or 0.0 else: # complete formula price_limit = price price = (price - (price * (self.price_discount / 100))) or 0.0 if self.price_round: price = tools.float_round(price, precision_rounding=self.price_round) if self.price_surcharge: price_surcharge = convert_to_price_uom(self.price_surcharge) price += price_surcharge if self.price_min_margin: price_min_margin = convert_to_price_uom(self.price_min_margin) price = max(price, price_limit + price_min_margin) if self.price_max_margin: price_max_margin = convert_to_price_uom(self.price_max_margin) price = min(price, price_limit + price_max_margin) return price
class MailActivityMixin(models.AbstractModel): """ Mail Activity Mixin is a mixin class to use if you want to add activities management on a model. It works like the mail.thread mixin. It defines an activity_ids one2many field toward activities using res_id and res_model_id. Various related / computed fields are also added to have a global status of activities on documents. Activities come with a new JS widget for the form view. It is integrated in the Chatter widget although it is a separate widget. It displays activities linked to the current record and allow to schedule, edit and mark done activities. Use widget="mail_activity" on activity_ids field in form view to use it. There is also a kanban widget defined. It defines a small widget to integrate in kanban vignettes. It allow to manage activities directly from the kanban view. Use widget="kanban_activity" on activitiy_ids field in kanban view to use it. Some context keys allow to control the mixin behavior. Use those in some specific cases like import * ``mail_activity_automation_skip``: skip activities automation; it means no automated activities will be generated, updated or unlinked, allowing to save computation and avoid generating unwanted activities; """ _name = 'mail.activity.mixin' _description = 'Activity Mixin' activity_ids = fields.One2many( 'mail.activity', 'res_id', 'Activities', auto_join=True, groups="base.group_user", domain=lambda self: [('res_model', '=', self._name)]) activity_state = fields.Selection( [('overdue', 'Overdue'), ('today', 'Today'), ('planned', 'Planned')], string='Activity State', compute='_compute_activity_state', groups="base.group_user", help='Status based on activities\nOverdue: Due date is already passed\n' 'Today: Activity date is today\nPlanned: Future activities.') activity_user_id = fields.Many2one('res.users', 'Responsible User', related='activity_ids.user_id', readonly=False, search='_search_activity_user_id', groups="base.group_user") activity_type_id = fields.Many2one('mail.activity.type', 'Next Activity Type', related='activity_ids.activity_type_id', readonly=False, search='_search_activity_type_id', groups="base.group_user") activity_date_deadline = fields.Date( 'Next Activity Deadline', compute='_compute_activity_date_deadline', search='_search_activity_date_deadline', readonly=True, store=False, groups="base.group_user") activity_summary = fields.Char( 'Next Activity Summary', related='activity_ids.summary', readonly=False, search='_search_activity_summary', groups="base.group_user", ) @api.depends('activity_ids.state') def _compute_activity_state(self): for record in self: states = record.activity_ids.mapped('state') if 'overdue' in states: record.activity_state = 'overdue' elif 'today' in states: record.activity_state = 'today' elif 'planned' in states: record.activity_state = 'planned' @api.depends('activity_ids.date_deadline') def _compute_activity_date_deadline(self): for record in self: record.activity_date_deadline = record.activity_ids[: 1].date_deadline def _search_activity_date_deadline(self, operator, operand): if operator == '=' and not operand: return [('activity_ids', '=', False)] return [('activity_ids.date_deadline', operator, operand)] @api.model def _search_activity_user_id(self, operator, operand): return [('activity_ids.user_id', operator, operand)] @api.model def _search_activity_type_id(self, operator, operand): return [('activity_ids.activity_type_id', operator, operand)] @api.model def _search_activity_summary(self, operator, operand): return [('activity_ids.summary', operator, operand)] @api.multi def write(self, vals): # Delete activities of archived record. if 'active' in vals and vals['active'] is False: self.env['mail.activity'].sudo().search([ ('res_model', '=', self._name), ('res_id', 'in', self.ids) ]).unlink() return super(MailActivityMixin, self).write(vals) @api.multi def unlink(self): """ Override unlink to delete records activities through (res_model, res_id). """ record_ids = self.ids result = super(MailActivityMixin, self).unlink() self.env['mail.activity'].sudo().search([ ('res_model', '=', self._name), ('res_id', 'in', record_ids) ]).unlink() return result @api.multi def toggle_active(self): """ Before archiving the record we should also remove its ongoing activities. Otherwise they stay in the systray and concerning archived records it makes no sense. """ record_to_deactivate = self.filtered(lambda rec: rec.active) if record_to_deactivate: # use a sudo to bypass every access rights; all activities should be removed self.env['mail.activity'].sudo().search([ ('res_model', '=', self._name), ('res_id', 'in', record_to_deactivate.ids) ]).unlink() return super(MailActivityMixin, self).toggle_active() def activity_send_mail(self, template_id): """ Automatically send an email based on the given mail.template, given its ID. """ template = self.env['mail.template'].browse(template_id).exists() if not template: return False for record in self.with_context(mail_post_autofollow=True): record.message_post_with_template(template_id, composition_mode='comment') return True def activity_schedule(self, act_type_xmlid='', date_deadline=None, summary='', note='', **act_values): """ Schedule an activity on each record of the current record set. This method allow to provide as parameter act_type_xmlid. This is an xml_id of activity type instead of directly giving an activity_type_id. It is useful to avoid having various "env.ref" in the code and allow to let the mixin handle access rights. :param date_deadline: the day the activity must be scheduled on the timezone of the user must be considered to set the correct deadline """ if self.env.context.get('mail_activity_automation_skip'): return False if not date_deadline: date_deadline = fields.Date.context_today(self) if isinstance(date_deadline, datetime): _logger.warning("Scheduled deadline should be a date (got %s)", date_deadline) if act_type_xmlid: activity_type = self.sudo().env.ref(act_type_xmlid) else: activity_type = self.env['mail.activity.type'].sudo().browse( act_values['activity_type_id']) model_id = self.env['ir.model']._get(self._name).id activities = self.env['mail.activity'] for record in self: create_vals = { 'activity_type_id': activity_type.id, 'summary': summary or activity_type.summary, 'automated': True, 'note': note, 'date_deadline': date_deadline, 'res_model_id': model_id, 'res_id': record.id, } create_vals.update(act_values) activities |= self.env['mail.activity'].create(create_vals) return activities def activity_schedule_with_view(self, act_type_xmlid='', date_deadline=None, summary='', views_or_xmlid='', render_context=None, **act_values): """ Helper method: Schedule an activity on each record of the current record set. This method allow to the same mecanism as `activity_schedule`, but provide 2 additionnal parameters: :param views_or_xmlid: record of ir.ui.view or string representing the xmlid of the qweb template to render :type views_or_xmlid: string or recordset :param render_context: the values required to render the given qweb template :type render_context: dict """ if self.env.context.get('mail_activity_automation_skip'): return False render_context = render_context or dict() if isinstance(views_or_xmlid, pycompat.string_types): views = self.env.ref(views_or_xmlid, raise_if_not_found=False) else: views = views_or_xmlid if not views: return activities = self.env['mail.activity'] for record in self: render_context['object'] = record note = views.render(render_context, engine='ir.qweb', minimal_qcontext=True) activities |= record.activity_schedule( act_type_xmlid=act_type_xmlid, date_deadline=date_deadline, summary=summary, note=note, **act_values) return activities def activity_reschedule(self, act_type_xmlids, user_id=None, date_deadline=None, new_user_id=None): """ Reschedule some automated activities. Activities to reschedule are selected based on type xml ids and optionally by user. Purpose is to be able to * update the deadline to date_deadline; * update the responsible to new_user_id; """ if self.env.context.get('mail_activity_automation_skip'): return False Data = self.env['ir.model.data'].sudo() activity_types_ids = [ Data.xmlid_to_res_id(xmlid) for xmlid in act_type_xmlids ] domain = [ '&', '&', '&', ('res_model', '=', self._name), ('res_id', 'in', self.ids), ('automated', '=', True), ('activity_type_id', 'in', activity_types_ids) ] if user_id: domain = ['&'] + domain + [('user_id', '=', user_id)] activities = self.env['mail.activity'].search(domain) if activities: write_vals = {} if date_deadline: write_vals['date_deadline'] = date_deadline if new_user_id: write_vals['user_id'] = new_user_id activities.write(write_vals) return activities def activity_feedback(self, act_type_xmlids, user_id=None, feedback=None): """ Set activities as done, limiting to some activity types and optionally to a given user. """ if self.env.context.get('mail_activity_automation_skip'): return False Data = self.env['ir.model.data'].sudo() activity_types_ids = [ Data.xmlid_to_res_id(xmlid) for xmlid in act_type_xmlids ] domain = [ '&', '&', '&', ('res_model', '=', self._name), ('res_id', 'in', self.ids), ('automated', '=', True), ('activity_type_id', 'in', activity_types_ids) ] if user_id: domain = ['&'] + domain + [('user_id', '=', user_id)] activities = self.env['mail.activity'].search(domain) if activities: activities.action_feedback(feedback=feedback) return True def activity_unlink(self, act_type_xmlids, user_id=None): """ Unlink activities, limiting to some activity types and optionally to a given user. """ if self.env.context.get('mail_activity_automation_skip'): return False Data = self.env['ir.model.data'].sudo() activity_types_ids = [ Data.xmlid_to_res_id(xmlid) for xmlid in act_type_xmlids ] domain = [ '&', '&', '&', ('res_model', '=', self._name), ('res_id', 'in', self.ids), ('automated', '=', True), ('activity_type_id', 'in', activity_types_ids) ] if user_id: domain = ['&'] + domain + [('user_id', '=', user_id)] self.env['mail.activity'].search(domain).unlink() return True
class MailActivity(models.Model): """ An actual activity to perform. Activities are linked to documents using res_id and res_model_id fields. Activities have a deadline that can be used in kanban view to display a status. Once done activities are unlinked and a message is posted. This message has a new activity_type_id field that indicates the activity linked to the message. """ _name = 'mail.activity' _description = 'Activity' _order = 'date_deadline ASC' _rec_name = 'summary' @api.model def default_get(self, fields): res = super(MailActivity, self).default_get(fields) if not fields or 'res_model_id' in fields and res.get('res_model'): res['res_model_id'] = self.env['ir.model']._get( res['res_model']).id return res # owner res_id = fields.Integer('Related Document ID', index=True, required=True) res_model_id = fields.Many2one('ir.model', 'Document Model', index=True, ondelete='cascade', required=True) res_model = fields.Char('Related Document Model', index=True, related='res_model_id.model', store=True, readonly=True) res_name = fields.Char('Document Name', compute='_compute_res_name', store=True, help="Display name of the related document.", readonly=True) # activity activity_type_id = fields.Many2one( 'mail.activity.type', 'Activity', domain= "['|', ('res_model_id', '=', False), ('res_model_id', '=', res_model_id)]", ondelete='restrict') activity_category = fields.Selection(related='activity_type_id.category', readonly=True) activity_decoration = fields.Selection( related='activity_type_id.decoration_type', readonly=True) icon = fields.Char('Icon', related='activity_type_id.icon', readonly=True) summary = fields.Char('Summary') note = fields.Html('Note', sanitize_style=True) feedback = fields.Html('Feedback') date_deadline = fields.Date('Due Date', index=True, required=True, default=fields.Date.context_today) automated = fields.Boolean( 'Automated activity', readonly=True, help= 'Indicates this activity has been created automatically and not by any user.' ) # description user_id = fields.Many2one('res.users', 'Assigned to', default=lambda self: self.env.user, index=True, required=True) create_user_id = fields.Many2one('res.users', 'Creator', default=lambda self: self.env.user, index=True) state = fields.Selection([('overdue', 'Overdue'), ('today', 'Today'), ('planned', 'Planned')], 'State', compute='_compute_state') recommended_activity_type_id = fields.Many2one( 'mail.activity.type', string="Recommended Activity Type") previous_activity_type_id = fields.Many2one( 'mail.activity.type', string='Previous Activity Type', readonly=True) has_recommended_activities = fields.Boolean( 'Next activities available', compute='_compute_has_recommended_activities', help='Technical field for UX purpose') mail_template_ids = fields.Many2many( related='activity_type_id.mail_template_ids', readonly=False) force_next = fields.Boolean(related='activity_type_id.force_next', readonly=False) @api.multi @api.onchange('previous_activity_type_id') def _compute_has_recommended_activities(self): for record in self: record.has_recommended_activities = bool( record.previous_activity_type_id.next_type_ids) @api.multi @api.onchange('previous_activity_type_id') def _onchange_previous_activity_type_id(self): for record in self: if record.previous_activity_type_id.default_next_type_id: record.activity_type_id = record.previous_activity_type_id.default_next_type_id @api.depends('res_model', 'res_id') def _compute_res_name(self): for activity in self: activity.res_name = self.env[activity.res_model].browse( activity.res_id).name_get()[0][1] @api.depends('date_deadline') def _compute_state(self): for record in self.filtered(lambda activity: activity.date_deadline): tz = record.user_id.sudo().tz date_deadline = record.date_deadline record.state = self._compute_state_from_date(date_deadline, tz) @api.model def _compute_state_from_date(self, date_deadline, tz=False): date_deadline = fields.Date.from_string(date_deadline) today_default = date.today() today = today_default if tz: today_utc = pytz.UTC.localize(datetime.utcnow()) today_tz = today_utc.astimezone(pytz.timezone(tz)) today = date(year=today_tz.year, month=today_tz.month, day=today_tz.day) diff = (date_deadline - today) if diff.days == 0: return 'today' elif diff.days < 0: return 'overdue' else: return 'planned' @api.onchange('activity_type_id') def _onchange_activity_type_id(self): if self.activity_type_id: self.summary = self.activity_type_id.summary # Date.context_today is correct because date_deadline is a Date and is meant to be # expressed in user TZ base = fields.Date.context_today(self) if self.activity_type_id.delay_from == 'previous_activity' and 'activity_previous_deadline' in self.env.context: base = fields.Date.from_string( self.env.context.get('activity_previous_deadline')) self.date_deadline = base + relativedelta( **{ self.activity_type_id.delay_unit: self.activity_type_id.delay_count }) @api.onchange('recommended_activity_type_id') def _onchange_recommended_activity_type_id(self): if self.recommended_activity_type_id: self.activity_type_id = self.recommended_activity_type_id @api.multi def _check_access(self, operation): """ Rule to access activities * create: check write rights on related document; * write: rule OR write rights on document; * unlink: rule OR write rights on document; """ self.check_access_rights( operation, raise_exception=True) # will raise an AccessError if operation in ('write', 'unlink'): try: self.check_access_rule(operation) except exceptions.AccessError: pass else: return doc_operation = 'read' if operation == 'read' else 'write' activity_to_documents = dict() for activity in self.sudo(): activity_to_documents.setdefault(activity.res_model, list()).append(activity.res_id) for model, res_ids in activity_to_documents.items(): self.env[model].check_access_rights(doc_operation, raise_exception=True) try: self.env[model].browse(res_ids).check_access_rule( doc_operation) except exceptions.AccessError: raise exceptions.AccessError( _('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)' ) % (self._description, operation) + ' - ({} {}, {} {})'.format(_('Records:'), res_ids[:6], _('User:'******'read') except exceptions.AccessError: raise exceptions.UserError( _('Assigned user %s has no access to the document and is not able to handle this activity.' ) % activity.user_id.display_name) else: try: target_user = activity.user_id target_record = self.env[activity.res_model].browse( activity.res_id) if hasattr(target_record, 'company_id') and ( target_record.company_id != target_user.company_id and (len(target_user.sudo().company_ids) > 1)): return # in that case we skip the check, assuming it would fail because of the company model.browse(activity.res_id).check_access_rule('read') except exceptions.AccessError: raise exceptions.UserError( _('Assigned user %s has no access to the document and is not able to handle this activity.' ) % activity.user_id.display_name) @api.model def create(self, values): # already compute default values to be sure those are computed using the current user values_w_defaults = self.default_get(self._fields.keys()) values_w_defaults.update(values) # continue as sudo because activities are somewhat protected activity = super(MailActivity, self.sudo()).create(values_w_defaults) activity_user = activity.sudo(self.env.user) activity_user._check_access('create') need_sudo = False try: # in multicompany, reading the partner might break partner_id = activity_user.user_id.partner_id.id except exceptions.AccessError: need_sudo = True partner_id = activity_user.user_id.sudo().partner_id.id # send a notification to assigned user; in case of manually done activity also check # target has rights on document otherwise we prevent its creation. Automated activities # are checked since they are integrated into business flows that should not crash. if activity_user.user_id != self.env.user: if not activity_user.automated: activity_user._check_access_assignation() if not self.env.context.get('mail_activity_quick_update', False): if need_sudo: activity_user.sudo().action_notify() else: activity_user.action_notify() self.env[activity_user.res_model].browse( activity_user.res_id).message_subscribe(partner_ids=[partner_id]) if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone((self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), { 'type': 'activity_updated', 'activity_created': True }) return activity_user @api.multi def write(self, values): self._check_access('write') if values.get('user_id'): pre_responsibles = self.mapped('user_id.partner_id') res = super(MailActivity, self.sudo()).write(values) if values.get('user_id'): if values['user_id'] != self.env.uid: to_check = self.filtered(lambda act: not act.automated) to_check._check_access_assignation() if not self.env.context.get('mail_activity_quick_update', False): self.action_notify() for activity in self: self.env[activity.res_model].browse( activity.res_id).message_subscribe( partner_ids=[activity.user_id.partner_id.id]) if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', activity.user_id.partner_id.id), { 'type': 'activity_updated', 'activity_created': True }) for activity in self: if activity.date_deadline <= fields.Date.today(): for partner in pre_responsibles: self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', partner.id), { 'type': 'activity_updated', 'activity_deleted': True }) return res @api.multi def unlink(self): self._check_access('unlink') for activity in self: if activity.date_deadline <= fields.Date.today(): self.env['bus.bus'].sendone( (self._cr.dbname, 'res.partner', activity.user_id.sudo().partner_id.id), { 'type': 'activity_updated', 'activity_deleted': True }) return super(MailActivity, self.sudo()).unlink() @api.multi def name_get(self): res = [] for record in self: name = record.summary or record.activity_type_id.display_name res.append((record.id, name)) return res @api.multi def action_notify(self): body_template = self.env.ref('mail.message_activity_assigned') for activity in self: model_description = self.env['ir.model']._get( activity.res_model).display_name body = body_template.render(dict( activity=activity, model_description=model_description), engine='ir.qweb', minimal_qcontext=True) self.env['mail.thread'].message_notify( partner_ids=activity.user_id.partner_id.ids, body=body, subject=_('%s: %s assigned to you') % (activity.res_name, activity.summary or activity.activity_type_id.name), record_name=activity.res_name, model_description=model_description, notif_layout='mail.mail_notification_light') @api.multi def action_done(self): """ Wrapper without feedback because web button add context as parameter, therefore setting context to feedback """ return self.action_feedback() def action_feedback(self, feedback=False): message = self.env['mail.message'] if feedback: self.write(dict(feedback=feedback)) # Search for all attachments linked to the activities we are about to unlink. This way, we # can link them to the message posted and prevent their deletion. attachments = self.env['ir.attachment'].search_read([ ('res_model', '=', self._name), ('res_id', 'in', self.ids), ], ['id', 'res_id']) activity_attachments = defaultdict(list) for attachment in attachments: activity_id = attachment['res_id'] activity_attachments[activity_id].append(attachment['id']) for activity in self: record = self.env[activity.res_model].browse(activity.res_id) record.message_post_with_view( 'mail.message_activity_done', values={'activity': activity}, subtype_id=self.env['ir.model.data'].xmlid_to_res_id( 'mail.mt_activities'), mail_activity_type_id=activity.activity_type_id.id, ) # Moving the attachments in the message # TODO: Fix void res_id on attachment when you create an activity with an image # directly, see route /web_editor/attachment/add activity_message = record.message_ids[0] message_attachments = self.env['ir.attachment'].browse( activity_attachments[activity.id]) if message_attachments: message_attachments.write({ 'res_id': activity_message.id, 'res_model': activity_message._name, }) activity_message.attachment_ids = message_attachments message |= activity_message self.unlink() return message.ids and message.ids[0] or False def action_done_schedule_next(self): """ Wrapper without feedback because web button add context as parameter, therefore setting context to feedback """ return self.action_feedback_schedule_next() @api.multi def action_feedback_schedule_next(self, feedback=False): ctx = dict( clean_context(self.env.context), default_previous_activity_type_id=self.activity_type_id.id, activity_previous_deadline=self.date_deadline, default_res_id=self.res_id, default_res_model=self.res_model, ) force_next = self.force_next self.action_feedback( feedback) # will unlink activity, dont access self after that if force_next: Activity = self.env['mail.activity'].with_context(ctx) res = Activity.new(Activity.default_get(Activity.fields_get())) res._onchange_previous_activity_type_id() res._onchange_activity_type_id() Activity.create(res._convert_to_write(res._cache)) return False else: return { 'name': _('Schedule an Activity'), 'context': ctx, 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.activity', 'views': [(False, 'form')], 'type': 'ir.actions.act_window', 'target': 'new', } @api.multi def action_close_dialog(self): return {'type': 'ir.actions.act_window_close'} @api.multi def activity_format(self): activities = self.read() mail_template_ids = set([ template_id for activity in activities for template_id in activity["mail_template_ids"] ]) mail_template_info = self.env["mail.template"].browse( mail_template_ids).read(['id', 'name']) mail_template_dict = dict([(mail_template['id'], mail_template) for mail_template in mail_template_info]) for activity in activities: activity['mail_template_ids'] = [ mail_template_dict[mail_template_id] for mail_template_id in activity['mail_template_ids'] ] return activities @api.model def get_activity_data(self, res_model, domain): activity_domain = [('res_model', '=', res_model)] if domain: res = self.env[res_model].search(domain) activity_domain.append(('res_id', 'in', res.ids)) grouped_activities = self.env['mail.activity'].read_group( activity_domain, [ 'res_id', 'activity_type_id', 'ids:array_agg(id)', 'date_deadline:min(date_deadline)' ], ['res_id', 'activity_type_id'], lazy=False) # filter out unreadable records if not domain: res_ids = tuple(a['res_id'] for a in grouped_activities) res = self.env[res_model].search([('id', 'in', res_ids)]) grouped_activities = [ a for a in grouped_activities if a['res_id'] in res.ids ] activity_type_ids = self.env['mail.activity.type'] res_id_to_deadline = {} activity_data = defaultdict(dict) for group in grouped_activities: res_id = group['res_id'] activity_type_id = (group.get('activity_type_id') or (False, False))[0] activity_type_ids |= self.env['mail.activity.type'].browse( activity_type_id ) # we will get the name when reading mail_template_ids res_id_to_deadline[res_id] = group['date_deadline'] if ( res_id not in res_id_to_deadline or group['date_deadline'] < res_id_to_deadline[res_id]) else res_id_to_deadline[res_id] state = self._compute_state_from_date(group['date_deadline'], self.user_id.sudo().tz) activity_data[res_id][activity_type_id] = { 'count': group['__count'], 'ids': group['ids'], 'state': state, 'o_closest_deadline': group['date_deadline'], } res_ids_sorted = sorted(res_id_to_deadline, key=lambda item: res_id_to_deadline[item]) res_id_to_name = dict( self.env[res_model].browse(res_ids_sorted).name_get()) activity_type_infos = [] for elem in sorted(activity_type_ids, key=lambda item: item.sequence): mail_template_info = [] for mail_template_id in elem.mail_template_ids: mail_template_info.append({ "id": mail_template_id.id, "name": mail_template_id.name }) activity_type_infos.append( [elem.id, elem.name, mail_template_info]) return { 'activity_types': activity_type_infos, 'res_ids': [(rid, res_id_to_name[rid]) for rid in res_ids_sorted], 'grouped_activities': activity_data, 'model': res_model, }
class PurchaseRequisition(models.Model): _name = "purchase.requisition" _description = "Purchase Requisition" _inherit = ['mail.thread'] _order = "id desc" def _get_picking_in(self): pick_in = self.env.ref('stock.picking_type_in', raise_if_not_found=False) company = self.env['res.company']._company_default_get( 'purchase.requisition') if not pick_in or pick_in.sudo( ).warehouse_id.company_id.id != company.id: pick_in = self.env['stock.picking.type'].search( [('warehouse_id.company_id', '=', company.id), ('code', '=', 'incoming')], limit=1, ) return pick_in def _get_type_id(self): return self.env['purchase.requisition.type'].search([], limit=1) name = fields.Char(string='Agreement Reference', required=True, copy=False, default='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") type_id = fields.Many2one('purchase.requisition.type', string="Agreement Type", required=True, default=_get_type_id) ordering_date = fields.Date(string="Ordering Date", track_visibility='onchange') date_end = fields.Datetime(string='Agreement Deadline', track_visibility='onchange') schedule_date = fields.Date( string='Delivery Date', index=True, help= "The expected and scheduled delivery date where all the products are received", track_visibility='onchange') user_id = fields.Many2one('res.users', string='Purchase Representative', default=lambda self: self.env.user) description = fields.Text() company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env['res.company']. _company_default_get('purchase.requisition')) purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]}) line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True) warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse') state = fields.Selection(PURCHASE_REQUISITION_STATES, 'Status', track_visibility='onchange', required=True, copy=False, default='draft') state_blanket_order = fields.Selection(PURCHASE_REQUISITION_STATES, compute='_set_state') picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', required=True, default=_get_picking_in) 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.user.company_id.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): if not self.vendor_id: self.currency_id = self.env.user.company_id.currency_id.id else: self.currency_id = self.vendor_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id requisitions = self.env['purchase.requisition'].search([ ('vendor_id', '=', self.vendor_id.id), ('state', '=', 'ongoing'), ('type_id.quantity_copy', '=', 'none'), ]) 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 to use to complete this open blanket order instead of creating a new one." ) warning = {'title': title, 'message': message} return {'warning': warning} @api.multi @api.depends('purchase_ids') def _compute_orders_number(self): for requisition in self: requisition.order_count = len(requisition.purchase_ids) @api.multi def action_cancel(self): # try to set all associated quotations to cancel state for requisition in self: 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'}) @api.multi def action_in_progress(self): self.ensure_one() if not all(obj.line_ids for obj in self): 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') @api.multi def action_open(self): self.write({'state': 'open'}) def action_draft(self): self.ensure_one() self.name = 'New' self.write({'state': 'draft'}) @api.multi def action_done(self): """ Generate all purchase order based on selected lines, should only be called on one agreement at a time """ if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')): raise UserError( _('You have to cancel or validate every RfQ before closing the purchase requisition.' )) for requisition in self: for requisition_line in requisition.line_ids: requisition_line.supplier_info_ids.unlink() self.write({'state': 'done'}) def _prepare_tender_values(self, product_id, product_qty, product_uom, location_id, name, origin, values): return { 'origin': origin, 'date_end': values['date_planned'], 'warehouse_id': values.get('warehouse_id') and values['warehouse_id'].id or False, 'company_id': values['company_id'].id, 'line_ids': [(0, 0, { 'product_id': product_id.id, 'product_uom_id': product_uom.id, 'product_qty': product_qty, 'move_dest_id': values.get('move_dest_ids') and values['move_dest_ids'][0].id or False, })], } 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 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') product_qty = fields.Float( string='Quantity', digits=dp.get_precision('Product Unit of Measure')) price_unit = fields.Float(string='Unit Price', digits=dp.get_precision('Product Price')) qty_ordered = fields.Float(compute='_compute_ordered_qty', string='Ordered Quantities') requisition_id = fields.Many2one('purchase.requisition', 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['res.company']._company_default_get( 'purchase.requisition.line')) 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') move_dest_id = fields.Many2one('stock.move', 'Downstream Move') 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 @api.multi 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_id': purchase_requisition.id, 'purchase_requisition_line_id': self.id, }) @api.multi @api.depends('requisition_id.purchase_ids.state') def _compute_ordered_qty(self): for line in self: total = 0.0 for po in line.requisition_id.purchase_ids.filtered( lambda purchase_order: purchase_order.state in ['purchase', 'done']): for po_line in po.order_line.filtered( lambda order_line: order_line.product_id == line. product_id): if po_line.product_uom != line.product_uom_id: total += po_line.product_uom._compute_quantity( po_line.product_qty, line.product_uom_id) else: total += po_line.product_qty line.qty_ordered = total @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_po_id self.product_qty = 1.0 if not self.schedule_date: self.schedule_date = self.requisition_id.schedule_date @api.multi def _prepare_purchase_order_line(self, name, product_qty=0.0, price_unit=0.0, taxes_ids=False): self.ensure_one() requisition = self.requisition_id 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, 'move_dest_ids': self.move_dest_id and [(4, self.move_dest_id.id)] or [] }
class Employee(models.Model): _inherit = "hr.employee" remaining_leaves = fields.Float( compute='_compute_remaining_leaves', string='Remaining Legal 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.leave.type', 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_date_start_work(self): return self.create_date def _get_remaining_leaves(self): """ Helper to compute the remaining leaves for the current employees :returns dict where the key is the employee id, and the value is the remain leaves """ self._cr.execute( """ SELECT sum(h.number_of_days) AS days, h.employee_id FROM ( SELECT holiday_status_id, number_of_days, state, employee_id FROM hr_leave_allocation UNION ALL SELECT holiday_status_id, (number_of_days * -1) as number_of_days, state, employee_id FROM hr_leave ) h join hr_leave_type s ON (s.id=h.holiday_status_id) WHERE s.active = true AND h.state='validate' AND (s.allocation_type='fixed' OR s.allocation_type='fixed_allocation') AND h.employee_id in %s GROUP BY h.employee_id""", (tuple(self.ids), )) return dict((row['employee_id'], row['days']) for row in self._cr.dictfetchall()) @api.multi def _compute_remaining_leaves(self): remaining = self._get_remaining_leaves() for employee in self: employee.remaining_leaves = float_round(remaining.get( employee.id, 0.0), precision_digits=2) @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.leave'].sudo().search([ ('employee_id', 'in', self.ids), ('date_from', '<=', fields.Datetime.now()), ('date_to', '>=', fields.Datetime.now()), ('holiday_status_id.active', '=', True), ('state', 'not in', ('cancel', 'refuse')) ]) leave_data = {} for holiday in holidays: leave_data[holiday.employee_id.id] = {} leave_data[holiday.employee_id. id]['leave_date_from'] = holiday.date_from.date() leave_data[holiday.employee_id. id]['leave_date_to'] = holiday.date_to.date() leave_data[ holiday.employee_id.id]['current_leave_state'] = holiday.state leave_data[holiday.employee_id. id]['current_leave_id'] = holiday.holiday_status_id.id for employee in self: employee.leave_date_from = leave_data.get( employee.id, {}).get('leave_date_from') employee.leave_date_to = leave_data.get(employee.id, {}).get('leave_date_to') employee.current_leave_state = leave_data.get( employee.id, {}).get('current_leave_state') employee.current_leave_id = leave_data.get( employee.id, {}).get('current_leave_id') @api.multi def _compute_leaves_count(self): all_leaves = self.env['hr.leave.report'].read_group( [('employee_id', 'in', self.ids), ('holiday_status_id.allocation_type', '!=', 'no'), ('holiday_status_id.active', '=', 'True'), ('state', '=', 'validate')], fields=['number_of_days', 'employee_id'], groupby=['employee_id']) mapping = dict([(leave['employee_id'][0], leave['number_of_days']) for leave in all_leaves]) for employee in self: employee.leaves_count = float_round(mapping.get(employee.id, 0), precision_digits=2) @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.leave'].read_group( [('employee_id', 'in', self.ids), ('state', 'not in', ['cancel', 'refuse']), ('date_from', '<=', today_end), ('date_to', '>=', today_start)], ['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.leave'].sudo().search([ ('employee_id', '!=', False), ('state', 'not in', ['cancel', 'refuse']), ('date_from', '<=', today_end), ('date_to', '>=', today_start) ]) return [('id', 'in', holidays.mapped('employee_id').ids)] def write(self, values): res = super(Employee, self).write(values) if 'parent_id' in values or 'department_id' in values: hr_vals = {} if values.get('parent_id') is not None: hr_vals['manager_id'] = values['parent_id'] if values.get('department_id') is not None: hr_vals['department_id'] = values['department_id'] holidays = self.env['hr.leave'].sudo().search([ ('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids) ]) holidays.write(hr_vals) allocations = self.env['hr.leave.allocation'].sudo().search([ ('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids) ]) allocations.write(hr_vals) return res
class Employee(models.Model): _name = "hr.employee" _description = "Employee" _order = 'name' _inherit = ['mail.thread', 'mail.activity.mixin', 'resource.mixin'] _mail_post_access = 'read' @api.model def _default_image(self): image_path = get_module_resource('hr', 'static/src/img', 'default_image.png') return tools.image_resize_image_big( base64.b64encode(open(image_path, 'rb').read())) # resource and user # required on the resource, make sure required="True" set in the view name = fields.Char(related='resource_id.name', store=True, oldname='name_related', readonly=False) user_id = fields.Many2one('res.users', 'User', related='resource_id.user_id', store=True, readonly=False) active = fields.Boolean('Active', related='resource_id.active', default=True, store=True, readonly=False) # private partner address_home_id = fields.Many2one( 'res.partner', 'Private Address', help= 'Enter here the private address of the employee, not the one linked to your company.', groups="hr.group_hr_user") is_address_home_a_company = fields.Boolean( 'The employee adress has a company linked', compute='_compute_is_address_home_a_company', ) country_id = fields.Many2one('res.country', 'Nationality (Country)', groups="hr.group_hr_user") gender = fields.Selection([('male', 'Male'), ('female', 'Female'), ('other', 'Other')], groups="hr.group_hr_user", default="male") marital = fields.Selection([('single', 'Single'), ('married', 'Married'), ('cohabitant', 'Legal Cohabitant'), ('widower', 'Widower'), ('divorced', 'Divorced')], string='Marital Status', groups="hr.group_hr_user", default='single') spouse_complete_name = fields.Char(string="Spouse Complete Name", groups="hr.group_hr_user") spouse_birthdate = fields.Date(string="Spouse Birthdate", groups="hr.group_hr_user") children = fields.Integer(string='Number of Children', groups="hr.group_hr_user") place_of_birth = fields.Char('Place of Birth', groups="hr.group_hr_user") country_of_birth = fields.Many2one('res.country', string="Country of Birth", groups="hr.group_hr_user") birthday = fields.Date('Date of Birth', groups="hr.group_hr_user") ssnid = fields.Char('SSN No', help='Social Security Number', groups="hr.group_hr_user") sinid = fields.Char('SIN No', help='Social Insurance Number', groups="hr.group_hr_user") identification_id = fields.Char(string='Identification No', groups="hr.group_hr_user") passport_id = fields.Char('Passport No', groups="hr.group_hr_user") bank_account_id = fields.Many2one( 'res.partner.bank', 'Bank Account Number', domain="[('partner_id', '=', address_home_id)]", groups="hr.group_hr_user", help='Employee bank salary account') permit_no = fields.Char('Work Permit No', groups="hr.group_hr_user") visa_no = fields.Char('Visa No', groups="hr.group_hr_user") visa_expire = fields.Date('Visa Expire Date', groups="hr.group_hr_user") additional_note = fields.Text(string='Additional Note', groups="hr.group_hr_user") certificate = fields.Selection([ ('bachelor', 'Bachelor'), ('master', 'Master'), ('other', 'Other'), ], 'Certificate Level', default='master', groups="hr.group_hr_user") study_field = fields.Char("Field of Study", placeholder='Computer Science', groups="hr.group_hr_user") study_school = fields.Char("School", groups="hr.group_hr_user") emergency_contact = fields.Char("Emergency Contact", groups="hr.group_hr_user") emergency_phone = fields.Char("Emergency Phone", groups="hr.group_hr_user") km_home_work = fields.Integer(string="Km home-work", groups="hr.group_hr_user") google_drive_link = fields.Char(string="Employee Documents", groups="hr.group_hr_user") job_title = fields.Char("Job Title") # image: all image fields are base64 encoded and PIL-supported image = fields.Binary( "Photo", default=_default_image, attachment=True, help= "This field holds the image used as photo for the employee, limited to 1024x1024px." ) image_medium = fields.Binary( "Medium-sized photo", attachment=True, help="Medium-sized photo of the employee. It is automatically " "resized as a 128x128px image, with aspect ratio preserved. " "Use this field in form views or some kanban views.") image_small = fields.Binary( "Small-sized photo", attachment=True, help="Small-sized photo of the employee. It is automatically " "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") # work address_id = fields.Many2one('res.partner', 'Work Address') work_phone = fields.Char('Work Phone') mobile_phone = fields.Char('Work Mobile') work_email = fields.Char('Work Email') work_location = fields.Char('Work Location') # employee in company job_id = fields.Many2one('hr.job', 'Job Position') department_id = fields.Many2one('hr.department', 'Department') parent_id = fields.Many2one('hr.employee', 'Manager') child_ids = fields.One2many('hr.employee', 'parent_id', string='Subordinates') coach_id = fields.Many2one('hr.employee', 'Coach') category_ids = fields.Many2many('hr.employee.category', 'employee_category_rel', 'emp_id', 'category_id', string='Tags') # misc notes = fields.Text('Notes') color = fields.Integer('Color Index', default=0) @api.constrains('parent_id') def _check_parent_id(self): for employee in self: if not employee._check_recursion(): raise ValidationError( _('You cannot create a recursive hierarchy.')) @api.onchange('job_id') def _onchange_job_id(self): if self.job_id: self.job_title = self.job_id.name @api.onchange('address_id') def _onchange_address(self): self.work_phone = self.address_id.phone self.mobile_phone = self.address_id.mobile @api.onchange('company_id') def _onchange_company(self): address = self.company_id.partner_id.address_get(['default']) self.address_id = address['default'] if address else False @api.onchange('department_id') def _onchange_department(self): self.parent_id = self.department_id.manager_id @api.onchange('user_id') def _onchange_user(self): if self.user_id: self.update(self._sync_user(self.user_id)) @api.onchange('resource_calendar_id') def _onchange_timezone(self): if self.resource_calendar_id and not self.tz: self.tz = self.resource_calendar_id.tz def _sync_user(self, user): vals = dict( name=user.name, image=user.image, work_email=user.email, ) if user.tz: vals['tz'] = user.tz return vals @api.model def create(self, vals): if vals.get('user_id'): vals.update( self._sync_user(self.env['res.users'].browse(vals['user_id']))) tools.image_resize_images(vals) employee = super(Employee, self).create(vals) if employee.department_id: self.env['mail.channel'].sudo().search([ ('subscription_department_ids', 'in', employee.department_id.id) ])._subscribe_users() return employee @api.multi def write(self, vals): if 'address_home_id' in vals: account_id = vals.get('bank_account_id') or self.bank_account_id.id if account_id: self.env['res.partner.bank'].browse( account_id).partner_id = vals['address_home_id'] if vals.get('user_id'): vals.update( self._sync_user(self.env['res.users'].browse(vals['user_id']))) tools.image_resize_images(vals) res = super(Employee, self).write(vals) if vals.get('department_id') or vals.get('user_id'): department_id = vals['department_id'] if vals.get( 'department_id') else self[:1].department_id.id # When added to a department or changing user, subscribe to the channels auto-subscribed by department self.env['mail.channel'].sudo().search([ ('subscription_department_ids', 'in', department_id) ])._subscribe_users() return res @api.multi def unlink(self): resources = self.mapped('resource_id') super(Employee, self).unlink() return resources.unlink() @api.depends('address_home_id.parent_id') def _compute_is_address_home_a_company(self): """Checks that choosen address (res.partner) is not linked to a company. """ for employee in self: try: employee.is_address_home_a_company = employee.address_home_id.parent_id.id is not False except AccessError: employee.is_address_home_a_company = False @api.model def get_import_templates(self): return [{ 'label': _('Import Template for Employees'), 'template': '/hr/static/xls/hr_employee.xls' }]
class LandedCost(models.Model): _name = 'stock.landed.cost' _description = 'Stock Landed Cost' _inherit = 'mail.thread' name = fields.Char('Name', default=lambda self: _('New'), copy=False, readonly=True, track_visibility='always') date = fields.Date('Date', default=fields.Date.context_today, copy=False, required=True, states={'done': [('readonly', True)]}, track_visibility='onchange') picking_ids = fields.Many2many('stock.picking', string='Transfers', copy=False, states={'done': [('readonly', True)]}) cost_lines = fields.One2many('stock.landed.cost.lines', 'cost_id', 'Cost Lines', copy=True, states={'done': [('readonly', True)]}) valuation_adjustment_lines = fields.One2many( 'stock.valuation.adjustment.lines', 'cost_id', 'Valuation Adjustments', states={'done': [('readonly', True)]}) description = fields.Text('Item Description', states={'done': [('readonly', True)]}) amount_total = fields.Float('Total', compute='_compute_total_amount', digits=0, store=True, track_visibility='always') state = fields.Selection([('draft', 'Draft'), ('done', 'Posted'), ('cancel', 'Cancelled')], 'State', default='draft', copy=False, readonly=True, track_visibility='onchange') 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)]}) company_id = fields.Many2one('res.company', string="Company", related='account_journal_id.company_id', readonly=False) @api.one @api.depends('cost_lines.price_unit') def _compute_total_amount(self): self.amount_total = sum(line.price_unit for line in self.cost_lines) @api.model def create(self, vals): if vals.get('name', _('New')) == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code( 'stock.landed.cost') return super(LandedCost, self).create(vals) @api.multi def unlink(self): self.button_cancel() return super(LandedCost, self).unlink() @api.multi def _track_subtype(self, init_values): if 'state' in init_values and self.state == 'done': return 'stock_landed_costs.mt_stock_landed_cost_open' return super(LandedCost, self)._track_subtype(init_values) @api.multi 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'}) @api.multi def button_validate(self): if any(cost.state != 'draft' for cost in self): raise UserError(_('Only draft landed costs can be validated')) if any(not cost.valuation_adjustment_lines for cost in self): raise UserError( _('No valuation adjustments lines. You should maybe recompute the landed costs.' )) if not self._check_sum(): raise UserError( _('Cost and adjustments lines do not match. You should maybe recompute the landed costs.' )) for cost in self: move = self.env['account.move'] move_vals = { 'journal_id': cost.account_journal_id.id, 'date': cost.date, 'ref': cost.name, 'line_ids': [], } for line in cost.valuation_adjustment_lines.filtered( lambda line: line.move_id): # Prorate the value at what's still in stock cost_to_add = ( line.move_id.remaining_qty / line.move_id.product_qty) * line.additional_landed_cost new_landed_cost_value = line.move_id.landed_cost_value + line.additional_landed_cost line.move_id.write({ 'landed_cost_value': new_landed_cost_value, 'value': line.move_id.value + line.additional_landed_cost, 'remaining_value': line.move_id.remaining_value + cost_to_add, 'price_unit': (line.move_id.value + line.additional_landed_cost) / line.move_id.product_qty, }) # `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 - line.move_id.remaining_qty elif line.move_id._is_out(): qty_out = line.move_id.product_qty move_vals['line_ids'] += line._create_accounting_entries( move, qty_out) move = move.create(move_vals) cost.write({'state': 'done', 'account_move_id': move.id}) move.post() return True def _check_sum(self): """ Check if each cost line its valuation lines sum to the correct amount and if the overall total amount is correct also """ prec_digits = self.env.user.company_id.currency_id.decimal_places for landed_cost in self: total_amount = sum( landed_cost.valuation_adjustment_lines.mapped( 'additional_landed_cost')) if not tools.float_is_zero(total_amount - landed_cost.amount_total, precision_digits=prec_digits): return False val_to_cost_lines = defaultdict(lambda: 0.0) for val_line in landed_cost.valuation_adjustment_lines: val_to_cost_lines[ val_line.cost_line_id] += val_line.additional_landed_cost if any(not tools.float_is_zero(cost_line.price_unit - val_amount, precision_digits=prec_digits) for cost_line, val_amount in val_to_cost_lines.items()): return False return True def get_valuation_lines(self): lines = [] for move in self.mapped('picking_ids').mapped('move_lines'): # it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost if move.product_id.valuation != 'real_time' or move.product_id.cost_method != 'fifo': continue vals = { 'product_id': move.product_id.id, 'move_id': move.id, 'quantity': move.product_qty, 'former_cost': move.value, 'weight': move.product_id.weight * move.product_qty, 'volume': move.product_id.volume * move.product_qty } lines.append(vals) if not lines and self.mapped('picking_ids'): raise UserError( _("You cannot apply landed costs on the chosen transfer(s). Landed costs can only be applied for products with automated inventory valuation and FIFO costing method." )) return lines @api.multi def compute_landed_cost(self): AdjustementLines = self.env['stock.valuation.adjustment.lines'] AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink() digits = dp.get_precision('Product Price')(self._cr) towrite_dict = {} for cost in self.filtered(lambda cost: cost.picking_ids): total_qty = 0.0 total_cost = 0.0 total_weight = 0.0 total_volume = 0.0 total_line = 0.0 all_val_line_values = cost.get_valuation_lines() for val_line_values in all_val_line_values: for cost_line in cost.cost_lines: val_line_values.update({ 'cost_id': cost.id, 'cost_line_id': cost_line.id }) self.env['stock.valuation.adjustment.lines'].create( val_line_values) total_qty += val_line_values.get('quantity', 0.0) total_weight += val_line_values.get('weight', 0.0) total_volume += val_line_values.get('volume', 0.0) former_cost = val_line_values.get('former_cost', 0.0) # round this because former_cost on the valuation lines is also rounded total_cost += tools.float_round( former_cost, precision_digits=digits[1]) if digits else former_cost total_line += 1 for line in cost.cost_lines: value_split = 0.0 for valuation in cost.valuation_adjustment_lines: value = 0.0 if valuation.cost_line_id and valuation.cost_line_id.id == line.id: if line.split_method == 'by_quantity' and total_qty: per_unit = (line.price_unit / total_qty) value = valuation.quantity * per_unit elif line.split_method == 'by_weight' and total_weight: per_unit = (line.price_unit / total_weight) value = valuation.weight * per_unit elif line.split_method == 'by_volume' and total_volume: per_unit = (line.price_unit / total_volume) value = valuation.volume * per_unit elif line.split_method == 'equal': value = (line.price_unit / total_line) elif line.split_method == 'by_current_cost_price' and total_cost: per_unit = (line.price_unit / total_cost) value = valuation.former_cost * per_unit else: value = (line.price_unit / total_line) if digits: value = tools.float_round( value, precision_digits=digits[1], 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
class AccountInvoiceReport(models.Model): _name = "account.invoice.report" _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 """ 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 for record in self: date = record.date or fields.Date.today() company = record.company_id record.user_currency_price_total = base_currency_id._convert( record.price_total, user_currency_id, company, date) record.user_currency_price_average = base_currency_id._convert( record.price_average, user_currency_id, company, date) record.user_currency_residual = base_currency_id._convert( record.residual, user_currency_id, company, date) number = fields.Char('Invoice #', readonly=True) date = fields.Date(readonly=True, string="Invoice Date") 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='Untaxed Total', readonly=True) user_currency_price_total = fields.Float( string="Total Without Tax in Currency", 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 in Currency", 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='Line Count', readonly=True) # TDE FIXME master: rename into nbr_lines invoice_id = fields.Many2one('account.invoice', readonly=True) 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='Receivable/Payable Account', readonly=True, domain=[('deprecated', '=', False)]) account_line_id = fields.Many2one('account.account', string='Revenue/Expense Account', 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="Partner Company's Country") account_analytic_id = fields.Many2one( 'account.analytic.account', string='Analytic Account', groups="analytic.group_analytic_accounting") amount_total = fields.Float(string='Total', readonly=True) _order = 'date desc' _depends = { 'account.invoice': [ 'account_id', 'amount_total_company_signed', 'commercial_partner_id', 'company_id', 'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id', 'journal_id', 'number', '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'], 'uom.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.number, 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.nbr, sub.invoice_id, 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, sub.amount_total / COALESCE(cr.rate, 1) as amount_total, 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, ai.number as number, 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, 1 AS nbr, ai.id AS invoice_id, 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) / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) AS product_qty, SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total, SUM(ail.price_total * invoice_type.sign_qty) AS amount_total, SUM(ABS(ail.price_subtotal_signed)) / CASE WHEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) <> 0::numeric THEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) 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 uom_uom u ON u.id = ail.uom_id LEFT JOIN uom_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.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 WHERE ail.account_id IS NOT NULL %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 ReportMembership(models.Model): '''Membership Analysis''' _name = 'report.membership' _description = 'Membership Analysis' _auto = False _rec_name = 'start_date' start_date = fields.Date(string='Start Date', readonly=True) date_to = fields.Date(string='End Date', readonly=True, help="End membership date") num_waiting = fields.Integer(string='# Waiting', readonly=True) num_invoiced = fields.Integer(string='# Invoiced', readonly=True) num_paid = fields.Integer(string='# Paid', readonly=True) tot_pending = fields.Float(string='Pending Amount', digits=0, readonly=True) tot_earned = fields.Float(string='Earned Amount', digits=0, readonly=True) partner_id = fields.Many2one('res.partner', string='Member', readonly=True) associate_member_id = fields.Many2one('res.partner', string='Associate Member', readonly=True) membership_id = fields.Many2one('product.product', string='Membership Product', readonly=True) membership_state = fields.Selection(STATE, string='Current Membership State', readonly=True) user_id = fields.Many2one('res.users', string='Salesperson', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) quantity = fields.Integer(readonly=True) @api.model_cr def init(self): '''Create the view''' tools.drop_view_if_exists(self._cr, self._table) self._cr.execute(""" CREATE OR REPLACE VIEW %s AS ( SELECT MIN(id) AS id, partner_id, count(membership_id) as quantity, user_id, membership_state, associate_member_id, membership_amount, date_to, start_date, COUNT(num_waiting) AS num_waiting, COUNT(num_invoiced) AS num_invoiced, COUNT(num_paid) AS num_paid, SUM(tot_pending) AS tot_pending, SUM(tot_earned) AS tot_earned, membership_id, company_id FROM (SELECT MIN(p.id) AS id, p.id AS partner_id, p.user_id AS user_id, p.membership_state AS membership_state, p.associate_member AS associate_member_id, p.membership_amount AS membership_amount, p.membership_stop AS date_to, p.membership_start AS start_date, CASE WHEN ml.state = 'waiting' THEN ml.id END AS num_waiting, CASE WHEN ml.state = 'invoiced' THEN ml.id END AS num_invoiced, CASE WHEN ml.state = 'paid' THEN ml.id END AS num_paid, CASE WHEN ml.state IN ('waiting', 'invoiced') THEN SUM(il.price_subtotal) ELSE 0 END AS tot_pending, CASE WHEN ml.state = 'paid' OR p.membership_state = 'old' THEN SUM(il.price_subtotal) ELSE 0 END AS tot_earned, ml.membership_id AS membership_id, p.company_id AS company_id FROM res_partner p LEFT JOIN membership_membership_line ml ON (ml.partner = p.id) LEFT JOIN account_invoice_line il ON (ml.account_invoice_line = il.id) LEFT JOIN account_invoice ai ON (il.invoice_id = ai.id) WHERE p.membership_state != 'none' and p.active = 'true' GROUP BY p.id, p.user_id, p.membership_state, p.associate_member, p.membership_amount, p.membership_start, ml.membership_id, p.company_id, ml.state, ml.id ) AS foo GROUP BY start_date, date_to, partner_id, user_id, membership_id, company_id, membership_state, associate_member_id, membership_amount )""" % (self._table,))