class VendorDelayReport(models.Model): _name = "vendor.delay.report" _description = "Vendor Delay Report" _auto = False partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True) product_id = fields.Many2one('product.product', 'Product', readonly=True) category_id = fields.Many2one('product.category', 'Product Category', readonly=True) date = fields.Datetime('Effective Date', readonly=True) qty_total = fields.Float('Total Quantity', readonly=True) qty_on_time = fields.Float('On-Time Quantity', readonly=True) on_time_rate = fields.Float('On-Time Delivery Rate', readonly=True) def init(self): tools.drop_view_if_exists(self.env.cr, 'vendor_delay_report') self.env.cr.execute(""" CREATE OR replace VIEW vendor_delay_report AS( SELECT m.id AS id, m.date AS date, m.purchase_line_id AS purchase_line_id, m.product_id AS product_id, Min(pc.id) AS category_id, Min(po.partner_id) AS partner_id, Sum(pol.product_uom_qty) AS qty_total, Sum(CASE WHEN (pol.date_planned::date >= m.date::date) THEN ml.qty_done ELSE 0 END) AS qty_on_time FROM stock_move m JOIN stock_move_line ml ON m.id = ml.move_id JOIN purchase_order_line pol ON pol.id = m.purchase_line_id JOIN purchase_order po ON po.id = pol.order_id JOIN product_product p ON p.id = m.product_id JOIN product_template pt ON pt.id = p.product_tmpl_id JOIN product_category pc ON pc.id = pt.categ_id WHERE m.state = 'done' GROUP BY m.id )""") @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): if 'on_time_rate' not in fields: res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) return res fields.remove('on_time_rate') if 'qty_total' not in fields: fields.append('qty_total') if 'qty_on_time' not in fields: fields.append('qty_on_time') res = super().read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) for group in res: if group['qty_total'] == 0: on_time_rate = 100 else: on_time_rate = group['qty_on_time'] / group['qty_total'] * 100 group.update({'on_time_rate': on_time_rate}) return res
class FleetVehicleLogContract(models.Model): _inherit = 'fleet.vehicle.log.contract' recurring_cost_amount_depreciated = fields.Float( "Recurring Cost Amount (depreciated)")
class SiiTax(models.Model): _inherit = 'account.tax' sii_code = fields.Integer(string='SII Code', ) sii_type = fields.Selection( [ ('A', 'Anticipado'), ('R', 'Retención'), ], string="Tipo de impuesto para el SII", ) retencion = fields.Float( string="Valor retención", default=0.00, ) no_rec = fields.Boolean(string="Es No Recuperable", ) activo_fijo = fields.Boolean( string="Activo Fijo", default=False, ) @api.multi def compute_all(self, price_unit, currency=None, quantity=1.0, product=None, partner=None, discount=None): """ Returns all information required to apply taxes (in self + their children in case of a tax goup). We consider the sequence of the parent for group of taxes. Eg. considering letters as taxes and alphabetic order as sequence : [G, B([A, D, F]), E, C] will be computed as [A, D, F, C, E, G] RETURN: { 'total_excluded': 0.0, # Total without taxes 'total_included': 0.0, # Total with taxes 'taxes': [{ # One dict for each tax in self and their children 'id': int, 'name': str, 'amount': float, 'sequence': int, 'account_id': int, 'refund_account_id': int, 'analytic': boolean, }] } """ if len(self) == 0: company_id = self.env.user.company_id else: company_id = self[0].company_id if not currency: currency = company_id.currency_id taxes = [] # By default, for each tax, tax amount will first be computed # and rounded at the 'Account' decimal precision for each # PO/SO/invoice line and then these rounded amounts will be # summed, leading to the total amount for that tax. But, if the # company has tax_calculation_rounding_method = round_globally, # we still follow the same method, but we use a much larger # precision when we round the tax amount for each line (we use # the 'Account' decimal precision + 5), and that way it's like # rounding after the sum of the tax amounts of each line prec = currency.decimal_places base = round(price_unit * quantity, prec + 2) base = round(base, prec) tot_discount = round(base * ((discount or 0.0) / 100)) base -= tot_discount total_excluded = base total_included = base if company_id.tax_calculation_rounding_method == 'round_globally' or not bool( self.env.context.get("round", True)): prec += 5 # Sorting key is mandatory in this case. When no key is provided, sorted() will perform a # search. However, the search method is overridden in account.tax in order to add a domain # depending on the context. This domain might filter out some taxes from self, e.g. in the # case of group taxes. for tax in self.sorted(key=lambda r: r.sequence): if tax.amount_type == 'group': ret = tax.children_tax_ids.compute_all(price_unit, currency, quantity, product, partner) total_excluded = ret['total_excluded'] base = ret['base'] total_included = ret['total_included'] tax_amount_retencion = ret['retencion'] tax_amount = total_included - total_excluded + tax_amount_retencion taxes += ret['taxes'] continue tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner) if company_id.tax_calculation_rounding_method == 'round_globally' or not bool( self.env.context.get("round", True)): tax_amount = round(tax_amount, prec) else: tax_amount = currency.round(tax_amount) tax_amount_retencion = 0 if tax.sii_type in ['R']: tax_amount_retencion = tax._compute_amount_ret( base, price_unit, quantity, product, partner) if company_id.tax_calculation_rounding_method == 'round_globally' or not bool( self.env.context.get("round", True)): tax_amount_retencion = round(tax_amount_retencion, prec) if tax.price_include: total_excluded -= (tax_amount - tax_amount_retencion) total_included -= (tax_amount_retencion) base -= (tax_amount - tax_amount_retencion) else: total_included += (tax_amount - tax_amount_retencion) else: if tax.price_include: total_excluded -= tax_amount base -= tax_amount else: total_included += tax_amount # Keep base amount used for the current tax tax_base = base if tax.include_base_amount: base += tax_amount taxes.append({ 'id': tax.id, 'name': tax.with_context(**{ 'lang': partner.lang } if partner else {}).name, 'amount': tax_amount, 'retencion': tax_amount_retencion, 'base': tax_base, 'sequence': tax.sequence, 'account_id': tax.account_id.id, 'refund_account_id': tax.refund_account_id.id, 'analytic': tax.analytic, }) return { 'taxes': sorted(taxes, key=lambda k: k['sequence']), 'total_excluded': currency.round(total_excluded) if bool( self.env.context.get("round", True)) else total_excluded, 'total_included': currency.round(total_included) if bool( self.env.context.get("round", True)) else total_included, 'base': base, } def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None): if self.amount_type == 'percent' and self.price_include: neto = base_amount / (1 + self.amount / 100) tax = base_amount - neto return tax return super(SiiTax, self)._compute_amount(base_amount, price_unit, quantity, product, partner) def _compute_amount_ret(self, base_amount, price_unit, quantity=1.0, product=None, partner=None): if self.amount_type == 'percent' and self.price_include: neto = base_amount / (1 + self.retencion / 100) tax = base_amount - neto return tax if (self.amount_type == 'percent' and not self.price_include) or (self.amount_type == 'division' and self.price_include): return base_amount * self.retencion / 100
class AccountInvoice(models.Model): _inherit = "account.invoice" @api.multi @api.depends('discount_amount', 'discount_per', 'amount_untaxed', 'invoice_line_ids') def _get_discount(self): for record in self: total_discount = 0.0 for invoice_line_id in record.invoice_line_ids: total_price = (invoice_line_id.quantity * invoice_line_id.price_unit) total_discount += \ (total_price * invoice_line_id.discount) / 100 record.discount = record.currency_id.round(total_discount) @api.multi @api.depends('invoice_line_ids', 'discount_per', 'discount_amount') def _get_total_amount(self): for invoice_id in self: invoice_id.gross_amount = sum([ invoice_id.currency_id.round(line_id.quantity * line_id.price_unit) for line_id in invoice_id.invoice_line_ids ]) discount_method = fields.Selection([('fixed', 'Fixed'), ('per', 'Percentage')], string="Discount Method") discount_amount = fields.Float(string="Discount Amount") discount_per = fields.Float(string="Discount (%)") discount = fields.Monetary(string='Discount', readonly=True, compute='_get_discount', track_visibility='always') gross_amount = fields.Float(string="Gross Amount", compute='_get_total_amount', store=True) @api.multi def calculate_discount(self): for line in self.invoice_line_ids: line.write({'discount': 0.0}) # amount_untaxed = self.amount_untaxed gross_amount = self.gross_amount if self.discount_method == 'per': for line in self.invoice_line_ids: line.write({'discount': self.discount_per}) self._onchange_invoice_line_ids() else: for line in self.invoice_line_ids: discount_value_ratio = \ (self.discount_amount * line.price_subtotal) / \ gross_amount if discount_value_ratio: discount_per_ratio = \ (discount_value_ratio * 100) / line.price_subtotal line.write({'discount': discount_per_ratio}) self._onchange_invoice_line_ids() self._check_constrains() @api.multi @api.returns('self') def refund(self, date_invoice=None, date=None, description=None, journal_id=None): result = super(AccountInvoice, self).refund(date_invoice=date_invoice, date=date, description=description, journal_id=journal_id) result.write({ 'discount_method': result.refund_invoice_id.discount_method, 'discount_amount': result.refund_invoice_id.discount_amount, 'discount_per': result.refund_invoice_id.discount_per }) result.calculate_discount() return result @api.constrains('discount_per', 'discount_amount', 'invoice_line_ids') def _check_constrains(self): self.onchange_discount_per() self.onchange_discount_amount() @api.onchange('discount_method') def onchange_discount_method(self): if not self.refund_invoice_id: self.discount_amount = 0.0 self.discount_per = 0.0 if self.discount_method and not self.invoice_line_ids: raise Warning('No Invoice Line(s) were found!') @api.multi def get_maximum_per_amount(self): account_dis_config_obj = self.env['account.discount.config'] max_percentage = 0 max_amount = 0 check_group = False for groups_id in self.env.user.groups_id: account_dis_config_id = account_dis_config_obj.search([ ('group_id', '=', groups_id.id) ]) if account_dis_config_id: check_group = True if account_dis_config_id.percentage > max_percentage: max_percentage = account_dis_config_id.percentage if account_dis_config_id.fix_amount > max_amount: max_amount = account_dis_config_id.fix_amount return { 'max_percentage': max_percentage, 'max_amount': max_amount, 'check_group': check_group } @api.onchange('discount_per') def onchange_discount_per(self): values = self.get_maximum_per_amount() if self.discount_method == 'per' and ( self.discount_per > 100 or self.discount_per < 0) \ and values.get('check_group', False): raise Warning(_("Percentage should be between 0% to 100%")) if self.discount_per > values.get('max_percentage', False) \ and values.get('check_group', False): raise Warning( _("You are not allowed to apply Discount Percentage " "(%s) more than configured Discount Percentage " "(%s) in configuration setting!") % (formatLang(self.env, self.discount_per, digits=2), formatLang(self.env, values['max_percentage'], digits=2))) config_id = self.env['res.config.settings'].search([], order='id desc', limit=1) if config_id and config_id.global_discount_invoice_apply: global_percentage = config_id.global_discount_percentage_invoice if global_percentage < self.discount_per: raise Warning( _("You are not allowed to apply Discount " "Percentage(%s) more than configured Discount" " Percentage (%s) in configuration setting!") % (formatLang(self.env, self.discount_per, digits=2), formatLang(self.env, config_id.global_discount_percentage_invoice, digits=2))) @api.onchange('discount_amount') def onchange_discount_amount(self): values = self.get_maximum_per_amount() if self.discount < 0: raise Warning(_("Discount should be less than Gross Amount")) discount = self.discount or self.discount_amount if self.gross_amount and discount > self.gross_amount: raise Warning( _("Discount (%s) should be less than " "Gross Amount (%s).") % (formatLang(self.env, discount, digits=2), formatLang(self.env, self.gross_amount, digits=2))) if self.discount > values.get('max_amount', False) \ and values.get('check_group', False): raise Warning( _("You're not allowed to apply this amount of " "discount as discount Amount (%s) is greater than" " assign Fix Amount (%s).") % (formatLang(self.env, self.discount, digits=2), formatLang(self.env, values['max_amount'], digits=2))) config_id = self.env['res.config.settings'].search([], order='id desc', limit=1) if config_id and config_id.global_discount_invoice_apply: fix_amount = config_id.global_discount_fix_invoice_amount if fix_amount < self.discount_amount: raise Warning( _("You're not allowed to apply this amount of" " discount as discount Amount (%s) is greater" " than Configuration Amount (%s).") % (formatLang(self.env, self.discount, digits=2), formatLang(self.env, config_id.global_discount_fix_invoice_amount, digits=2)))
class PosSaleReport(models.Model): _name = "report.all.channels.sales" _description = "Sales by Channel (All in One)" _auto = False name = fields.Char('Order Reference', readonly=True) partner_id = fields.Many2one('res.partner', 'Partner', readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True) date_order = fields.Datetime(string='Date Order', readonly=True) user_id = fields.Many2one('res.users', 'Salesperson', readonly=True) categ_id = fields.Many2one('product.category', 'Product Category', readonly=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) price_total = fields.Float('Total', readonly=True) pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True) country_id = fields.Many2one('res.country', 'Partner Country', readonly=True) price_subtotal = fields.Float(string='Price Subtotal', readonly=True) product_qty = fields.Float('Product Quantity', readonly=True) analytic_account_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True) team_id = fields.Many2one('crm.team', 'Sales Team', readonly=True) def _so(self): so_str = """ SELECT sol.id AS id, so.name AS name, so.partner_id AS partner_id, sol.product_id AS product_id, pro.product_tmpl_id AS product_tmpl_id, so.date_order AS date_order, so.user_id AS user_id, pt.categ_id AS categ_id, so.company_id AS company_id, sol.price_total / CASE COALESCE(so.currency_rate, 0) WHEN 0 THEN 1.0 ELSE so.currency_rate END AS price_total, so.pricelist_id AS pricelist_id, rp.country_id AS country_id, sol.price_subtotal / CASE COALESCE(so.currency_rate, 0) WHEN 0 THEN 1.0 ELSE so.currency_rate END AS price_subtotal, (sol.product_uom_qty / u.factor * u2.factor) as product_qty, so.analytic_account_id AS analytic_account_id, so.team_id AS team_id FROM sale_order_line sol JOIN sale_order so ON (sol.order_id = so.id) LEFT JOIN product_product pro ON (sol.product_id = pro.id) JOIN res_partner rp ON (so.partner_id = rp.id) LEFT JOIN product_template pt ON (pro.product_tmpl_id = pt.id) LEFT JOIN product_pricelist pp ON (so.pricelist_id = pp.id) LEFT JOIN uom_uom u on (u.id=sol.product_uom) LEFT JOIN uom_uom u2 on (u2.id=pt.uom_id) WHERE so.state in ('sale','done') """ return so_str def _from(self): return """(%s)""" % (self._so()) def _get_main_request(self): request = """ CREATE or REPLACE VIEW %s AS SELECT id AS id, name, partner_id, product_id, product_tmpl_id, date_order, user_id, categ_id, company_id, price_total, pricelist_id, analytic_account_id, country_id, team_id, price_subtotal, product_qty FROM %s AS foo""" % (self._table, self._from()) return request def init(self): tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute(self._get_main_request())
class MrpByProduct(models.Model): _name = 'mrp.bom.byproduct' _description = 'Byproduct' _rec_name = "product_id" _check_company_auto = True product_id = fields.Many2one('product.product', 'By-product', required=True, check_company=True) company_id = fields.Many2one(related='bom_id.company_id', store=True, index=True, readonly=True) product_qty = fields.Float('Quantity', default=1.0, digits='Product Unit of Measure', required=True) product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True) bom_id = fields.Many2one('mrp.bom', 'BoM', ondelete='cascade') allowed_operation_ids = fields.Many2many( 'mrp.routing.workcenter', compute='_compute_allowed_operation_ids') operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Produced in Operation', check_company=True, domain="[('id', 'in', allowed_operation_ids)]") @api.depends('bom_id') def _compute_allowed_operation_ids(self): for byproduct in self: if not byproduct.bom_id.operation_ids: byproduct.allowed_operation_ids = self.env[ 'mrp.routing.workcenter'] else: operation_domain = [ ('id', 'in', byproduct.bom_id.operation_ids.ids), '|', ('company_id', '=', byproduct.company_id.id), ('company_id', '=', False) ] byproduct.allowed_operation_ids = self.env[ 'mrp.routing.workcenter'].search(operation_domain) @api.onchange('product_id') def onchange_product_id(self): """ Changes UoM if product_id changes. """ if self.product_id: self.product_uom_id = self.product_id.uom_id.id @api.onchange('product_uom_id') def onchange_uom(self): res = {} if self.product_uom_id and self.product_id and self.product_uom_id.category_id != self.product_id.uom_id.category_id: res['warning'] = { 'title': _('Warning'), 'message': _('The unit of measure you choose is in a different category than the product unit of measure.' ) } self.product_uom_id = self.product_id.uom_id.id return res
class OpMediaMovement(models.Model): _name = 'op.media.movement' _inherit = 'mail.thread' _description = 'Media Movement' _rec_name = 'media_id' media_id = fields.Many2one('op.media', 'Media', required=True) media_unit_id = fields.Many2one('op.media.unit', 'Media Unit', required=True, track_visibility='onchange') type = fields.Selection([('student', 'Student'), ('faculty', 'Faculty')], 'Student/Faculty', required=True) student_id = fields.Many2one('op.student', 'Student') faculty_id = fields.Many2one('op.faculty', 'Faculty') library_card_id = fields.Many2one('op.library.card', 'Library Card', required=True, track_visibility='onchange') issued_date = fields.Date('Issued Date', required=True, default=fields.Date.today()) return_date = fields.Date('Due Date', required=True) actual_return_date = fields.Date('Actual Return Date') penalty = fields.Float('Penalty') partner_id = fields.Many2one('res.partner', 'Person', track_visibility='onchange') reserver_name = fields.Char('Person Name', size=256) state = fields.Selection([('available', 'Available'), ('reserve', 'Reserved'), ('issue', 'Issued'), ('lost', 'Lost'), ('return', 'Returned'), ('return_done', 'Returned Done')], 'Status', default='available', track_visibility='onchange') media_type_id = fields.Many2one(related='media_id.media_type_id', store=True, string='Media Type') user_id = fields.Many2one('res.users', related='student_id.user_id', string='Users') invoice_id = fields.Many2one('account.invoice', 'Invoice', readonly=True) @api.multi def get_diff_day(self): for media_mov_id in self: today_date = datetime.strptime(fields.Date.today(), '%Y-%m-%d') return_date = datetime.strptime(media_mov_id.return_date, '%Y-%m-%d') diff = today_date - return_date return abs(diff.days) @api.constrains('issued_date', 'return_date') def _check_date(self): if self.issued_date > self.return_date: raise ValidationError( _('Return Date cannot be set before Issued Date.')) @api.constrains('issued_date', 'actual_return_date') def check_actual_return_date(self): if self.actual_return_date: if self.issued_date > self.actual_return_date: raise ValidationError( _('Actual Return Date cannot be set before Issued Date')) @api.onchange('media_unit_id') def onchange_media_unit_id(self): self.state = self.media_unit_id.state self.media_id = self.media_unit_id.media_id @api.onchange('library_card_id') def onchange_library_card_id(self): self.type = self.library_card_id.type self.student_id = self.library_card_id.student_id.id self.faculty_id = self.library_card_id.faculty_id.id self.return_date = date.today() + \ timedelta(days=self.library_card_id.library_card_type_id.duration) @api.multi def issue_media(self): ''' function to issue media ''' for record in self: if record.media_unit_id.state and \ record.media_unit_id.state == 'available': record.media_unit_id.state = 'issue' record.state = 'issue' @api.multi def return_media(self, return_date): for record in self: if not return_date: return_date = fields.Date.today() record.actual_return_date = return_date record.calculate_penalty() if record.penalty > 0.0: record.state = 'return' else: record.state = 'return_done' record.media_unit_id.state = 'available' @api.multi def calculate_penalty(self): for record in self: penalty_amt = 0 penalty_days = 0 standard_diff = days_between(record.return_date, record.issued_date) actual_diff = days_between(record.actual_return_date, record.issued_date) if record.library_card_id and \ record.library_card_id.library_card_type_id: penalty_days = actual_diff > standard_diff and actual_diff - \ standard_diff or penalty_days penalty_amt = penalty_days * \ record.library_card_id.library_card_type_id.\ penalty_amt_per_day record.write({'penalty': penalty_amt}) @api.multi def create_penalty_invoice(self): for rec in self: account_id = False product = self.env.ref('openeducat_library.op_product_7') if product.id: account_id = product.property_account_income_id.id if not account_id: account_id = \ product.categ_id.property_account_income_categ_id.id if not account_id: raise UserError( _('There is no income account defined for this \ product: "%s". You may have to install a chart of \ account from Accounting app, settings \ menu.') % (product.name, )) invoice = self.env['account.invoice'].create({ 'partner_id': self.student_id.partner_id.id, 'type': 'out_invoice', 'reference': False, 'date_invoice': fields.Date.today(), 'account_id': self.student_id.partner_id.property_account_receivable_id.id, 'invoice_line_ids': [(0, 0, { 'name': product.name, 'account_id': account_id, 'price_unit': self.penalty, 'quantity': 1.0, 'discount': 0.0, 'uom_id': product.uom_id.id, 'product_id': product.id, })], }) invoice.compute_taxes() invoice.action_invoice_open() self.invoice_id = invoice.id
class ProjectSprint(models.Model): _name = "project.sprint" _inherit = ['ir.branch.company.mixin', 'mail.thread'] _description = "Sprint of the Project" _rec_name = 'sprint_seq' sprint_seq = fields.Char( string="Reference", readonly=True) name = fields.Char("Sprint Name", required=True, track_visibility="onchange") goal_of_sprint = fields.Char("Goal of Sprint", track_visibility="onchange") meeting_date = fields.Datetime("Planning Meeting Date", required=True, track_visibility="onchange") hour = fields.Float(string="Hour", track_visibility="onchange") time_zone = fields.Selection([ ('am', 'AM'), ('pm', 'PM'), ], track_visibility="onchange") estimated_velocity = fields.Integer( compute="calculate_estimated_velocity", string="Estimated Velocity", store=True, track_visibility="onchange") actual_velocity = fields.Integer( compute="calculate_actual_velocity", string="Actual Velocity", store=True, track_visibility="onchange") sprint_planning_line = fields.One2many( 'sprint.planning.line', 'sprint_id', string="Sprint Planning Lines") project_id = fields.Many2one('project.project', string="Project", track_visibility="onchange") start_date = fields.Date(string="Start Date", track_visibility="onchange") end_date = fields.Date(string="End Date", track_visibility="onchange") working_days = fields.Integer( compute="calculate_business_days", string="Business Days", store=True, track_visibility="onchange") productivity_hours = fields.Float(string="Productivity Hours", track_visibility="onchange") productivity_per = fields.Float( compute="calculate_productivity_per", string="Productivity (%)", store=True, track_visibility="onchange") holiday_type = fields.Selection( [('hours', 'Hours'), ('days', 'Days')], string="Holiday (Hours / Days)", default='hours', track_visibility="onchange") holiday_count = fields.Float(string="Holiday Count", track_visibility="onchange") holiday_days = fields.Float( compute="calculate_holiday_days", string="Holiday Days", store=True, track_visibility="onchange") state = fields.Selection([ ('draft', 'Draft'), ('in_progress', 'In Progress'), ('pending', 'Pending'), ('done', 'Done'), ('cancel', 'Cancel')], string="State", default='draft', track_visibility="onchange") team_id = fields.Many2one('project.team', string="Team", track_visibility="onchange", required=True) task_line = fields.One2many('project.task', 'sprint_id', string='Tasks') color = fields.Integer('Color Index') @api.depends('start_date', 'end_date') def days_calculate(self): if self.start_date and self.end_date: diff = datetime.strptime( self.end_date, '%Y-%m-%d').date() - datetime.strptime( self.start_date, '%Y-%m-%d').date() self.duration = diff.days duration = fields.Integer( "Duration (In Days)", compute='days_calculate', store=True, track_visibility="onchange") @api.multi def _get_task_count(self): for record in self: record.number_of_tasks = record.env['project.task'].search_count([ ('sprint_id', '=', record.id)]) number_of_tasks = fields.Integer( string="# of tasks", compute="_get_task_count") @api.multi def _get_story_count(self): for record in self: count = record.env['project.story'].search_count([ ('sprint_id', '=', record.id)]) record.number_of_stories = count number_of_stories = fields.Integer( string="# of stories", compute="_get_story_count") @api.multi def _get_retrospective_count(self): for record in self: count = record.env['retrospective'].search_count([ ('sprint_id', '=', record.id)]) record.number_of_retrospectives = count number_of_retrospectives = fields.Integer( string="# of Retrospectives", compute="_get_retrospective_count") @api.multi @api.depends('task_line', 'task_line.stage_id', 'task_line.sprint_id', 'estimated_velocity', 'start_date', 'end_date', 'project_id', 'team_id') def calculate_tasks_json(self): data = [] for record in self: task_ids = self.env['project.task'].search([ ('sprint_id', '=', record.id)]) velocity = record.estimated_velocity or 1.0 for task in task_ids: data.append({ 'task': task.task_seq or '/', 'velocity': task.velocity or 0, 'per': round(((float(task.velocity) * 100) / float( velocity)), 2) }) record.tasks_json = data tasks_json = fields.Char( string="Tasks", compute="calculate_tasks_json", store=True) @api.multi def get_data(self): task_dict_list = [] for record in self: task_pool = self.env['project.task'].search([ ('sprint_id', '=', record.id)]) for task in task_pool: task_dict = { 'reference': task.task_seq, 'name': task.name, 'velocity': task.velocity, 'start_date': task.start_date, 'end_date': task.end_date, 'actual_end_date': task.actual_end_date, 'assigned_to': task.user_id.name, 'state': task.stage_id.name, } task_dict_list.append(task_dict) return task_dict_list @api.multi def set_state_open(self): self.state = 'in_progress' @api.multi def set_state_cancel(self): self.state = 'cancel' @api.multi def set_state_pending(self): self.state = 'pending' @api.multi def redirect_to_view(self, model, caption): return { 'name': (_(caption)), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': model, 'domain': [('sprint_id', '=', self.id)], 'context': { 'default_sprint_id': self.id, 'default_project_id': self.project_id.id } } @api.multi def action_view_tasks(self): return self.redirect_to_view("project.task", "Tasks") @api.multi def action_view_stories(self): return self.redirect_to_view("project.story", "Stories") @api.multi def action_view_release_planning(self): return self.redirect_to_view("release.planning", "Release Planning") @api.multi def action_view_retrospective(self): return self.redirect_to_view("retrospective", "Retrospective") @api.constrains('start_date', 'end_date') def check_dates(self): if self.start_date and self.end_date and ( self.start_date > self.end_date): raise ValidationError( "Start Date can not be greater than End date, Dude!") @api.onchange('holiday_type') def onchange_holiday_type(self): self.holiday_count = 0.0 self.holiday_days = 0.0 @api.multi @api.depends('project_id', 'project_id.no_of_days', 'start_date', 'end_date') def calculate_business_days(self): for record in self: if record.start_date and record.end_date: days_dict = { 0: (1, 2, 3, 4, 5, 6, 7), 1: (2, 3, 4, 5, 6, 7), 2: (3, 4, 5, 6, 7), 3: (4, 5, 6, 7), 4: (5, 6, 7), 5: (6, 7), 6: (7,), 7: (), } start = datetime.strptime(record.start_date, "%Y-%m-%d").date() end = datetime.strptime(record.end_date, "%Y-%m-%d").date() delta = timedelta(days=1) days = 0 if record.project_id and end > start: working_days = record.project_id.no_of_days non_working_days = days_dict[working_days] while end != start: end -= delta if end.isoweekday() not in non_working_days: days += 1 record.working_days = days @api.multi @api.depends('project_id', 'project_id.no_of_hours', 'productivity_hours') def calculate_productivity_per(self): for record in self: project_id = record.project_id no_of_hours = project_id.no_of_hours if project_id else 0 prod_hours = record.productivity_hours if project_id and no_of_hours > 0 and prod_hours: record.productivity_per = (prod_hours / no_of_hours) * 100 @api.multi @api.depends('project_id', 'project_id.no_of_hours', 'holiday_count') def calculate_holiday_days(self): for record in self: if record.holiday_type == 'days' and record.project_id: hours = record.holiday_count * record.project_id.no_of_hours record.holiday_days = hours @api.multi @api.depends('project_id', 'task_line', 'task_line.velocity') def calculate_estimated_velocity(self): for record in self: task_ids = record.env['project.task'].search([ ('sprint_id', '=', record.id) ]) total_velocity = sum([ task.velocity for task in task_ids if task.velocity]) record.estimated_velocity = total_velocity @api.multi @api.depends('project_id', 'end_date', 'task_line', 'task_line.velocity', 'task_line.stage_id') def calculate_actual_velocity(self): for record in self: task_ids = record.env['project.task'].search([ ('sprint_id', '=', record.id), ('actual_end_date', '<=', record.end_date), ]) total_velocity = sum([ task.velocity for task in task_ids if task.velocity]) record.actual_velocity = total_velocity @api.onchange('duration') def onchange_start_date(self): if self.start_date: end_date = datetime.strptime( self.start_date, '%Y-%m-%d') + timedelta(days=self.duration) self.end_date = end_date @api.onchange('project_id') def onchange_project(self): if self.project_id and self.project_id.branch_id: self.branch_id = self.project_id.branch_id @api.multi def check_sprint_state(self): next_call = datetime.today() for record in self.search([('state', '!=', 'done')]): if record.end_date: end_date = datetime.strptime( record.end_date, '%Y-%m-%d').date() if end_date < next_call: record.state = 'done' @api.constrains('sprint_planning_line') def check_users_in_planning_line(self): user_list = [] for user in self.sprint_planning_line: if user.user_id.id not in user_list: user_list.append(user.user_id.id) else: raise ValidationError( "You can't add the same user twice in Sprint Planning!") @api.model def create(self, vals): vals['sprint_seq'] = self.env[ 'ir.sequence'].next_by_code('project.sprint') res = super(ProjectSprint, self).create(vals) partner_list = [] mail_channel_id = self.env['mail.channel'].sudo().search([ ('name', '=', 'Project Sprint') ]) if mail_channel_id: mail_channel_ids = self.env['mail.followers'].sudo().search([ ('channel_id', '=', mail_channel_id.id), ('res_model', '=', res._name), ('res_id', '=', res.id), ]) if not mail_channel_ids: self.env['mail.followers'].sudo().create({ 'channel_id': mail_channel_id.id, 'res_model': res._name, 'res_id': res.id, }) if 'team_id' in vals: team_id = self.env['project.team'].browse(vals['team_id']) partner_list += [member.partner_id.id for member in team_id.member_ids] for follower in partner_list: if follower: mail_follower_ids = self.env['mail.followers'].sudo().search([ ('res_id', '=', res.id), ('partner_id', '=', follower), ('res_model', '=', res._name), ]) if not mail_follower_ids: self.env['mail.followers'].sudo().create({ 'res_id': res.id, 'res_model': res._name, 'partner_id': follower, 'team_id': team_id.id, }) return res @api.multi def write(self, vals): res = super(ProjectSprint, self).write(vals) if 'team_id' in vals: team_id = self.env['project.team'].browse(vals['team_id']) else: team_id = self.team_id delete_team_id = self.env['mail.followers'].sudo().search([ ('team_id', '!=', team_id.id), ('res_id', '=', self.id), ]) delete_team_id.unlink() partner_list = [member.partner_id.id for member in team_id.member_ids] for follower in partner_list: if follower: mail_follower_ids = self.env['mail.followers'].sudo().search([ ('res_id', '=', self.id), ('partner_id', '=', follower), ('res_model', '=', self._name), ]) if not mail_follower_ids: self.env['mail.followers'].sudo().create({ 'res_id': self.id, 'res_model': self._name, 'partner_id': follower, 'team_id': team_id.id, }) return res @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'draft': return 'project_scrum.state_sprint_draft' elif 'state' in init_values and self.state == 'in_progress': return 'project_scrum.state_sprint_in_progress' elif 'state' in init_values and self.state == 'pending': return 'project_scrum.state_sprint_pending' elif 'state' in init_values and self.state == 'done': return 'project_scrum.state_sprint_done' elif 'state' in init_values and self.state == 'cancel': return 'project_scrum.state_sprint_cancel' return super(ProjectSprint, self)._track_subtype(init_values)
class OpStudentFeesDetails(models.Model): _name = 'op.student.fees.details' _description = 'Student Fees Details' fees_line_id = fields.Many2one('op.fees.terms.line', 'Fees Line') invoice_id = fields.Many2one('account.invoice', 'Invoice') amount = fields.Float('Fees Amount') date = fields.Date('Submit Date') product_id = fields.Many2one('product.product', 'Product') student_id = fields.Many2one('op.student', 'Student') state = fields.Selection([ ('draft', 'Draft'), ('invoice', 'Invoice Created')], 'Status') invoice_state = fields.Selection([ ('draft', 'Draft'), ('proforma', 'Pro-forma'), ('proforma2', 'Pro-forma'), ('open', 'Open'), ('paid', 'Paid'), ('cancel', 'Cancelled')], 'State', related="invoice_id.state", readonly=True) @api.multi def get_invoice(self): """ Create invoice for fee payment process of student """ inv_obj = self.env['account.invoice'] partner_id = self.student_id.partner_id student = self.student_id account_id = False product = self.product_id if product.property_account_income_id: account_id = product.property_account_income_id.id if not account_id: account_id = product.categ_id.property_account_income_categ_id.id if not account_id: raise UserError( _('There is no income account defined for this product: "%s". \ You may have to install a chart of account from Accounting \ app, settings menu.') % (product.name,)) if self.amount <= 0.00: raise UserError(_('The value of the deposit amount must be \ positive.')) else: amount = self.amount name = product.name invoice = inv_obj.create({ 'name': student.name, 'origin': student.gr_no or False, 'type': 'out_invoice', 'reference': False, 'account_id': partner_id.property_account_receivable_id.id, 'partner_id': partner_id.id, 'invoice_line_ids': [(0, 0, { 'name': name, 'origin': student.gr_no, 'account_id': account_id, 'price_unit': amount, 'quantity': 1.0, 'discount': 0.0, 'uom_id': product.uom_id.id, 'product_id': product.id, })], }) invoice.compute_taxes() self.state = 'invoice' self.invoice_id = invoice.id return True def action_get_invoice(self): value = True if self.invoice_id: form_view = self.env.ref('account.invoice_form') tree_view = self.env.ref('account.invoice_tree') value = { 'domain': str([('id', '=', self.invoice_id.id)]), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.invoice', 'view_id': False, 'views': [(form_view and form_view.id or False, 'form'), (tree_view and tree_view.id or False, 'tree')], 'type': 'ir.actions.act_window', 'res_id': self.invoice_id.id, 'target': 'current', 'nodestroy': True } return value
class HolidaysType(models.Model): _name = "hr.holidays.status" _description = "Leave Type" name = fields.Char('Leave Type', required=True, translate=True) categ_id = fields.Many2one( 'calendar.event.type', string='Meeting Type', help= 'Once a leave is validated, Flectra will create a corresponding meeting of this type in the calendar.' ) color_name = fields.Selection( [('red', 'Red'), ('blue', 'Blue'), ('lightgreen', 'Light Green'), ('lightblue', 'Light Blue'), ('lightyellow', 'Light Yellow'), ('magenta', 'Magenta'), ('lightcyan', 'Light Cyan'), ('black', 'Black'), ('lightpink', 'Light Pink'), ('brown', 'Brown'), ('violet', 'Violet'), ('lightcoral', 'Light Coral'), ('lightsalmon', 'Light Salmon'), ('lavender', 'Lavender'), ('wheat', 'Wheat'), ('ivory', 'Ivory')], string='Color in Report', required=True, default='red', help= 'This color will be used in the leaves summary located in Reporting > Leaves by Department.' ) limit = fields.Boolean( 'Allow to Override Limit', help= 'If you select this check box, the system allows the employees to take more leaves ' 'than the available ones for this type and will not take them into account for the ' '"Remaining Legal Leaves" defined on the employee form.') active = fields.Boolean( 'Active', default=True, help= "If the active field is set to false, it will allow you to hide the leave type without removing it." ) max_leaves = fields.Float( compute='_compute_leaves', string='Maximum Allowed', help= 'This value is given by the sum of all leaves requests with a positive value.' ) leaves_taken = fields.Float( compute='_compute_leaves', string='Leaves Already Taken', help= 'This value is given by the sum of all leaves requests with a negative value.' ) remaining_leaves = fields.Float( compute='_compute_leaves', string='Remaining Leaves', help='Maximum Leaves Allowed - Leaves Already Taken') virtual_remaining_leaves = fields.Float( compute='_compute_leaves', string='Virtual Remaining Leaves', help= 'Maximum Leaves Allowed - Leaves Already Taken - Leaves Waiting Approval' ) double_validation = fields.Boolean( string='Apply Double Validation', help= "When selected, the Allocation/Leave Requests for this type require a second validation to be approved." ) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id) @api.multi def get_days(self, employee_id): # need to use `dict` constructor to create a dict per id result = dict((id, dict(max_leaves=0, leaves_taken=0, remaining_leaves=0, virtual_remaining_leaves=0)) for id in self.ids) holidays = self.env['hr.holidays'].search([ ('employee_id', '=', employee_id), ('state', 'in', ['confirm', 'validate1', 'validate']), ('holiday_status_id', 'in', self.ids) ]) for holiday in holidays: status_dict = result[holiday.holiday_status_id.id] if holiday.type == 'add': if holiday.state == 'validate': # note: add only validated allocation even for the virtual # count; otherwise pending then refused allocation allow # the employee to create more leaves than possible status_dict[ 'virtual_remaining_leaves'] += holiday.number_of_days_temp status_dict['max_leaves'] += holiday.number_of_days_temp status_dict[ 'remaining_leaves'] += holiday.number_of_days_temp elif holiday.type == 'remove': # number of days is negative status_dict[ 'virtual_remaining_leaves'] -= holiday.number_of_days_temp if holiday.state == 'validate': status_dict['leaves_taken'] += holiday.number_of_days_temp status_dict[ 'remaining_leaves'] -= holiday.number_of_days_temp return result @api.multi def _compute_leaves(self): data_days = {} if 'employee_id' in self._context: employee_id = self._context['employee_id'] else: employee_id = self.env['hr.employee'].search( [('user_id', '=', self.env.user.id)], limit=1).id if employee_id: data_days = self.get_days(employee_id) for holiday_status in self: result = data_days.get(holiday_status.id, {}) holiday_status.max_leaves = result.get('max_leaves', 0) holiday_status.leaves_taken = result.get('leaves_taken', 0) holiday_status.remaining_leaves = result.get('remaining_leaves', 0) holiday_status.virtual_remaining_leaves = result.get( 'virtual_remaining_leaves', 0) @api.multi def name_get(self): if not self._context.get('employee_id'): # leave counts is based on employee_id, would be inaccurate if not based on correct employee return super(HolidaysType, self).name_get() res = [] for record in self: name = record.name if not record.limit: name = "%(name)s (%(count)s)" % { 'name': name, 'count': _('%g remaining out of %g') % (float_round(record.virtual_remaining_leaves or 0.0, precision_digits=2) + 0.0, record.max_leaves or 0.0) } res.append((record.id, name)) return res @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): """ Override _search to order the results, according to some employee. The order is the following - limit (limited leaves first, such as Legal Leaves) - virtual remaining leaves (higher the better, so using reverse on sorted) This override is necessary because those fields are not stored and depends on an employee_id given in context. This sort will be done when there is an employee_id in context and that no other order has been given to the method. """ leave_ids = super(HolidaysType, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) if not count and not order and self._context.get('employee_id'): leaves = self.browse(leave_ids) sort_key = lambda l: (not l.limit, l.virtual_remaining_leaves) return leaves.sorted(key=sort_key, reverse=True).ids return leave_ids
class Holidays(models.Model): _name = "hr.holidays" _description = "Leave" _order = "type desc, date_from desc" _inherit = ['mail.thread'] def _default_employee(self): return self.env.context.get( 'default_employee_id') or self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) name = fields.Char('Description') state = fields.Selection( [('draft', 'To Submit'), ('cancel', 'Cancelled'), ('confirm', 'To Approve'), ('refuse', 'Refused'), ('validate1', 'Second Approval'), ('validate', 'Approved')], string='Status', readonly=True, track_visibility='onchange', copy=False, default='confirm', help= "The status is set to 'To Submit', when a leave request is created." + "\nThe status is 'To Approve', when leave request is confirmed by user." + "\nThe status is 'Refused', when leave request is refused by manager." + "\nThe status is 'Approved', when leave request is approved by manager." ) payslip_status = fields.Boolean( 'Reported in last payslips', help= 'Green this button when the leave has been taken into account in the payslip.' ) report_note = fields.Text('HR Comments') user_id = fields.Many2one('res.users', string='User', related='employee_id.user_id', related_sudo=True, compute_sudo=True, store=True, default=lambda self: self.env.uid, readonly=True) date_from = fields.Datetime('Start Date', readonly=True, index=True, copy=False, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, track_visibility='onchange') date_to = fields.Datetime('End Date', readonly=True, copy=False, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, track_visibility='onchange') holiday_status_id = fields.Many2one("hr.holidays.status", string="Leave Type", required=True, readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }) employee_id = fields.Many2one('hr.employee', string='Employee', index=True, readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, default=_default_employee, track_visibility='onchange') manager_id = fields.Many2one('hr.employee', string='Manager', readonly=True) notes = fields.Text('Reasons', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }) number_of_days_temp = fields.Float( 'Allocation', copy=False, readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= 'Number of days of the leave request according to your working schedule.' ) number_of_days = fields.Float('Number of Days', compute='_compute_number_of_days', store=True, track_visibility='onchange') meeting_id = fields.Many2one('calendar.event', string='Meeting') type = fields.Selection( [('remove', 'Leave Request'), ('add', 'Allocation Request')], string='Request Type', required=True, readonly=True, index=True, track_visibility='always', default='remove', states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help="Choose 'Leave Request' if someone wants to take an off-day. " "\nChoose 'Allocation Request' if you want to increase the number of leaves available for someone" ) parent_id = fields.Many2one('hr.holidays', string='Parent', copy=False) linked_request_ids = fields.One2many('hr.holidays', 'parent_id', string='Linked Requests') department_id = fields.Many2one('hr.department', string='Department', readonly=True) category_id = fields.Many2one('hr.employee.category', string='Employee Tag', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help='Category of Employee') holiday_type = fields.Selection( [('employee', 'By Employee'), ('category', 'By Employee Tag')], string='Allocation Mode', readonly=True, required=True, default='employee', states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= 'By Employee: Allocation/Request for individual Employee, By Employee Tag: Allocation/Request for group of employees in category' ) first_approver_id = fields.Many2one( 'hr.employee', string='First Approval', readonly=True, copy=False, help= 'This area is automatically filled by the user who validate the leave', oldname='manager_id') second_approver_id = fields.Many2one( 'hr.employee', string='Second Approval', readonly=True, copy=False, oldname='manager_id2', help= 'This area is automaticly filled by the user who validate the leave with second level (If Leave type need second validation)' ) double_validation = fields.Boolean( 'Apply Double Validation', related='holiday_status_id.double_validation') can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') @api.multi @api.depends('number_of_days_temp', 'type') def _compute_number_of_days(self): for holiday in self: if holiday.type == 'remove': holiday.number_of_days = -holiday.number_of_days_temp else: holiday.number_of_days = holiday.number_of_days_temp @api.multi def _compute_can_reset(self): """ User can reset a leave request if it is its own leave request or if he is an Hr Manager. """ user = self.env.user group_hr_manager = self.env.ref( 'hr_holidays.group_hr_holidays_manager') for holiday in self: if group_hr_manager in user.groups_id or holiday.employee_id and holiday.employee_id.user_id == user: holiday.can_reset = True @api.constrains('date_from', 'date_to') def _check_date(self): for holiday in self: domain = [ ('date_from', '<=', holiday.date_to), ('date_to', '>=', holiday.date_from), ('employee_id', '=', holiday.employee_id.id), ('id', '!=', holiday.id), ('type', '=', holiday.type), ('state', 'not in', ['cancel', 'refuse']), ] nholidays = self.search_count(domain) if nholidays: raise ValidationError( _('You can not have 2 leaves that overlaps on same day!')) @api.constrains('state', 'number_of_days_temp', 'holiday_status_id') def _check_holidays(self): for holiday in self: if holiday.holiday_type != 'employee' or holiday.type != 'remove' or not holiday.employee_id or holiday.holiday_status_id.limit: continue leave_days = holiday.holiday_status_id.get_days( holiday.employee_id.id)[holiday.holiday_status_id.id] if float_compare(leave_days['remaining_leaves'], 0, precision_digits=2) == -1 or \ float_compare(leave_days['virtual_remaining_leaves'], 0, precision_digits=2) == -1: raise ValidationError( _('The number of remaining leaves is not sufficient for this leave type.\n' 'Please verify also the leaves waiting for validation.')) _sql_constraints = [ ('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or (holiday_type='category' AND category_id IS NOT NULL))", "The employee or employee category of this request is missing. Please make sure that your user login is linked to an employee." ), ('date_check2', "CHECK ( (type='add') OR (date_from <= date_to))", "The start date must be anterior to the end date."), ('date_check', "CHECK ( number_of_days_temp >= 0 )", "The number of days must be greater than 0."), ] @api.onchange('holiday_type') def _onchange_type(self): if self.holiday_type == 'employee' and not self.employee_id: self.employee_id = self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) elif self.holiday_type != 'employee': self.employee_id = None @api.onchange('employee_id') def _onchange_employee_id(self): self.manager_id = self.employee_id and self.employee_id.parent_id self.department_id = self.employee_id.department_id def _get_number_of_days(self, date_from, date_to, employee_id): """ Returns a float equals to the timedelta between two dates given as string.""" from_dt = fields.Datetime.from_string(date_from) to_dt = fields.Datetime.from_string(date_to) if employee_id: employee = self.env['hr.employee'].browse(employee_id) return employee.get_work_days_count(from_dt, to_dt) time_delta = to_dt - from_dt return math.ceil(time_delta.days + float(time_delta.seconds) / 86400) @api.onchange('date_from') def _onchange_date_from(self): """ If there are no date set for date_to, automatically set one 8 hours later than the date_from. Also update the number_of_days. """ date_from = self.date_from date_to = self.date_to # No date_to set so far: automatically compute one 8 hours later if date_from and not date_to: date_to_with_delta = fields.Datetime.from_string( date_from) + timedelta(hours=HOURS_PER_DAY) self.date_to = str(date_to_with_delta) # Compute and update the number of days if (date_to and date_from) and (date_from <= date_to): self.number_of_days_temp = self._get_number_of_days( date_from, date_to, self.employee_id.id) else: self.number_of_days_temp = 0 @api.onchange('date_to') def _onchange_date_to(self): """ Update the number_of_days. """ date_from = self.date_from date_to = self.date_to # Compute and update the number of days if (date_to and date_from) and (date_from <= date_to): self.number_of_days_temp = self._get_number_of_days( date_from, date_to, self.employee_id.id) else: self.number_of_days_temp = 0 #################################################### # ORM Overrides methods #################################################### @api.multi def name_get(self): res = [] for leave in self: if leave.type == 'remove': if self.env.context.get('short_name'): res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days_temp))) else: res.append( (leave.id, _("%s on %s : %.2f day(s)") % (leave.employee_id.name or leave.category_id.name, leave.holiday_status_id.name, leave.number_of_days_temp))) else: res.append( (leave.id, _("Allocation of %s : %.2f day(s) To %s") % (leave.holiday_status_id.name, leave.number_of_days_temp, leave.employee_id.name))) return res def _check_state_access_right(self, vals): if vals.get('state') and vals['state'] not in [ 'draft', 'confirm', 'cancel' ] and not self.env['res.users'].has_group( 'hr_holidays.group_hr_holidays_user'): return False return True @api.multi def add_follower(self, employee_id): employee = self.env['hr.employee'].browse(employee_id) if employee.user_id: self.message_subscribe_users(user_ids=employee.user_id.ids) @api.model def create(self, values): """ Override to avoid automatic logging of creation """ employee_id = values.get('employee_id', False) if not self._check_state_access_right(values): raise AccessError( _('You cannot set a leave request as \'%s\'. Contact a human resource manager.' ) % values.get('state')) if not values.get('department_id'): values.update({ 'department_id': self.env['hr.employee'].browse(employee_id).department_id.id }) holiday = super( Holidays, self.with_context(mail_create_nolog=True, mail_create_nosubscribe=True)).create(values) holiday.add_follower(employee_id) if 'employee_id' in values: holiday._onchange_employee_id() return holiday @api.multi def write(self, values): employee_id = values.get('employee_id', False) if not self._check_state_access_right(values): raise AccessError( _('You cannot set a leave request as \'%s\'. Contact a human resource manager.' ) % values.get('state')) result = super(Holidays, self).write(values) self.add_follower(employee_id) if 'employee_id' in values: self._onchange_employee_id() return result @api.multi def unlink(self): for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']): raise UserError( _('You cannot delete a leave which is in %s state.') % (holiday.state, )) return super(Holidays, self).unlink() #################################################### # Business methods #################################################### @api.multi def _create_resource_leave(self): """ This method will create entry in resource calendar leave object at the time of holidays validated """ for leave in self: self.env['resource.calendar.leaves'].create({ 'name': leave.name, 'date_from': leave.date_from, 'holiday_id': leave.id, 'date_to': leave.date_to, 'resource_id': leave.employee_id.resource_id.id, 'calendar_id': leave.employee_id.resource_calendar_id.id }) return True @api.multi def _remove_resource_leave(self): """ This method will create entry in resource calendar leave object at the time of holidays cancel/removed """ return self.env['resource.calendar.leaves'].search([ ('holiday_id', 'in', self.ids) ]).unlink() @api.multi def action_draft(self): if any(not holiday.can_reset for holiday in self): raise UserError( _('Only an HR Manager or the concerned employee can reset to draft.' )) if any(holiday.state not in ['confirm', 'refuse'] for holiday in self): raise UserError( _('Leave request state must be "Refused" or "To Approve" in order to reset to Draft.' )) self.write({ 'state': 'draft', 'first_approver_id': False, 'second_approver_id': False, }) linked_requests = self.mapped('linked_request_ids') if linked_requests: linked_requests.action_draft() linked_requests.unlink() return True @api.multi def action_confirm(self): if self.filtered(lambda holiday: holiday.state != 'draft'): raise UserError( _('Leave request must be in Draft state ("To Submit") in order to confirm it.' )) return self.write({'state': 'confirm'}) @api.multi def _check_security_action_approve(self): if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): raise UserError( _('Only an HR Officer or Manager can approve leave requests.')) @api.multi def action_approve(self): # if double_validation: this method is the first approval approval # if not double_validation: this method calls action_validate() below self._check_security_action_approve() current_employee = self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) for holiday in self: if holiday.state != 'confirm': raise UserError( _('Leave request must be confirmed ("To Approve") in order to approve it.' )) if holiday.double_validation: return holiday.write({ 'state': 'validate1', 'first_approver_id': current_employee.id }) else: holiday.action_validate() @api.multi def _prepare_create_by_category(self, employee): self.ensure_one() values = { 'name': self.name, 'type': self.type, 'holiday_type': 'employee', 'holiday_status_id': self.holiday_status_id.id, 'date_from': self.date_from, 'date_to': self.date_to, 'notes': self.notes, 'number_of_days_temp': self.number_of_days_temp, 'parent_id': self.id, 'employee_id': employee.id } return values @api.multi def _check_security_action_validate(self): if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): raise UserError( _('Only an HR Officer or Manager can approve leave requests.')) @api.multi def action_validate(self): self._check_security_action_validate() current_employee = self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) for holiday in self: if holiday.state not in ['confirm', 'validate1']: raise UserError( _('Leave request must be confirmed in order to approve it.' )) if holiday.state == 'validate1' and not holiday.env.user.has_group( 'hr_holidays.group_hr_holidays_manager'): raise UserError( _('Only an HR Manager can apply the second approval on leave requests.' )) holiday.write({'state': 'validate'}) if holiday.double_validation: holiday.write({'second_approver_id': current_employee.id}) else: holiday.write({'first_approver_id': current_employee.id}) if holiday.holiday_type == 'employee' and holiday.type == 'remove': holiday._validate_leave_request() elif holiday.holiday_type == 'category': leaves = self.env['hr.holidays'] for employee in holiday.category_id.employee_ids: values = holiday._prepare_create_by_category(employee) leaves += self.with_context( mail_notify_force_send=False).create(values) # TODO is it necessary to interleave the calls? leaves.action_approve() if leaves and leaves[0].double_validation: leaves.action_validate() return True def _validate_leave_request(self): """ Validate leave requests (holiday_type='employee' and holiday.type='remove') by creating a calendar event and a resource leaves. """ for holiday in self.filtered(lambda request: request.type == 'remove' and request.holiday_type == 'employee'): meeting_values = holiday._prepare_holidays_meeting_values() meeting = self.env['calendar.event'].with_context( no_mail_to_attendees=True).create(meeting_values) holiday.write({'meeting_id': meeting.id}) holiday._create_resource_leave() @api.multi def _prepare_holidays_meeting_values(self): self.ensure_one() meeting_values = { 'name': _("%s on Time Off : %.2f day(s)") % (self.employee_id.name or self.category_id.name, self.number_of_days_temp), 'categ_ids': [(6, 0, [self.holiday_status_id.categ_id.id])] if self.holiday_status_id.categ_id else [], 'duration': self.number_of_days_temp * HOURS_PER_DAY, 'description': self.notes, 'user_id': self.user_id.id, 'start': self.date_from, 'stop': self.date_to, 'allday': False, 'state': 'open', # to block that meeting date in the calendar 'privacy': 'confidential' } # Add the partner_id (if exist) as an attendee if self.user_id and self.user_id.partner_id: meeting_values['partner_ids'] = [(4, self.user_id.partner_id.id)] return meeting_values @api.multi def action_refuse(self): self._check_security_action_refuse() current_employee = self.env['hr.employee'].search( [('user_id', '=', self.env.uid)], limit=1) if any(holiday.state not in ['confirm', 'validate', 'validate1'] for holiday in self): raise UserError( _('Leave request must be confirmed or validated in order to refuse it.' )) validated_holidays = self.filtered( lambda hol: hol.state == 'validate1') validated_holidays.write({ 'state': 'refuse', 'first_approver_id': current_employee.id }) (self - validated_holidays).write({ 'state': 'refuse', 'second_approver_id': current_employee.id }) # Delete the meeting self.mapped('meeting_id').unlink() # If a category that created several holidays, cancel all related linked_requests = self.mapped('linked_request_ids') if linked_requests: linked_requests.action_refuse() self._remove_resource_leave() return True @api.multi def _check_security_action_refuse(self): if not self.env.user.has_group('hr_holidays.group_hr_holidays_user'): raise UserError( _('Only an HR Officer or Manager can refuse leave requests.')) #################################################### # Messaging methods #################################################### @api.multi def _track_subtype(self, init_values): if 'state' in init_values and self.state == 'validate': return 'hr_holidays.mt_holidays_approved' elif 'state' in init_values and self.state == 'validate1': return 'hr_holidays.mt_holidays_first_validated' elif 'state' in init_values and self.state == 'confirm': return 'hr_holidays.mt_holidays_confirmed' elif 'state' in init_values and self.state == 'refuse': return 'hr_holidays.mt_holidays_refused' return super(Holidays, self)._track_subtype(init_values) @api.multi def _notification_recipients(self, message, groups): """ Handle HR users and officers recipients that can validate or refuse holidays directly from email. """ groups = super(Holidays, self)._notification_recipients(message, groups) self.ensure_one() hr_actions = [] if self.state == 'confirm': app_action = self._notification_link_helper( 'controller', controller='/hr_holidays/validate') hr_actions += [{'url': app_action, 'title': _('Approve')}] if self.state in ['confirm', 'validate', 'validate1']: ref_action = self._notification_link_helper( 'controller', controller='/hr_holidays/refuse') hr_actions += [{'url': ref_action, 'title': _('Refuse')}] new_group = ('group_hr_holidays_user', lambda partner: bool(partner.user_ids) and any( user.has_group('hr_holidays.group_hr_holidays_user') for user in partner.user_ids), { 'actions': hr_actions, }) return [new_group] + groups @api.multi def _message_notification_recipients(self, message, recipients): result = super(Holidays, self)._message_notification_recipients( message, recipients) leave_type = self.env[message.model].browse(message.res_id).type title = _("See Leave") if leave_type == 'remove' else _( "See Allocation") for res in result: if result[res].get('button_access'): result[res]['button_access']['title'] = title return result
class PricelistItem(models.Model): _name = "product.pricelist.item" _description = "Pricelist Rule" _order = "applied_on, min_quantity desc, categ_id desc, id desc" _check_company_auto = True # 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. def _default_pricelist_id(self): return self.env['product.pricelist'].search([ '|', ('company_id', '=', False), ('company_id', '=', self.env.company.id) ], limit=1) product_tmpl_id = fields.Many2one( 'product.template', 'Product', ondelete='cascade', check_company=True, help= "Specify a template if this rule only applies to one product template. Keep empty otherwise." ) product_id = fields.Many2one( 'product.product', 'Product Variant', ondelete='cascade', check_company=True, 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.Float( 'Min. Quantity', default=0, digits="Product Unit Of Measure", 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', 'All Products'), ('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', 'Sales Price'), ('standard_price', 'Cost'), ('pricelist', 'Other Pricelist')], "Based on", default='list_price', required=True, help='Base price for computation.\n' 'Sales Price: The base price will be the Sales 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', check_company=True) pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', index=True, ondelete='cascade', required=True, default=_default_pricelist_id) price_surcharge = fields.Float( 'Price Surcharge', digits='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='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='Product Price', help='Specify the minimum amount of margin over the base price.') price_max_margin = fields.Float( 'Max. Price Margin', digits='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) active = fields.Boolean(readonly=True, related="pricelist_id.active", store=True) date_start = fields.Datetime( 'Start Date', help="Starting datetime for the pricelist item validation\n" "The displayed value depends on the timezone set in your preferences.") date_end = fields.Datetime( 'End Date', help="Ending datetime for the pricelist item validation\n" "The displayed value depends on the timezone set in your preferences.") compute_price = fields.Selection([('fixed', 'Fixed Price'), ('percentage', 'Percentage (discount)'), ('formula', 'Formula')], index=True, default='fixed', required=True) fixed_price = fields.Float('Fixed Price', digits='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.constrains('product_id', 'product_tmpl_id', 'categ_id') def _check_product_consistency(self): for item in self: if item.applied_on == "2_product_category" and not item.categ_id: raise ValidationError( _("Please specify the category for which this rule should be applied" )) elif item.applied_on == "1_product" and not item.product_tmpl_id: raise ValidationError( _("Please specify the product for which this rule should be applied" )) elif item.applied_on == "0_product_variant" and not item.product_id: raise ValidationError( _("Please specify the product variant for which this rule should be applied" )) @api.depends('applied_on', '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): for item in self: if item.categ_id and item.applied_on == '2_product_category': item.name = _("Category: %s") % (item.categ_id.display_name) elif item.product_tmpl_id and item.applied_on == '1_product': item.name = _("Product: %s") % ( item.product_tmpl_id.display_name) elif item.product_id and item.applied_on == '0_product_variant': item.name = _("Variant: %s") % (item.product_id.with_context( display_default_code=False).display_name) else: item.name = _("All Products") if item.compute_price == 'fixed': item.price = formatLang(item.env, item.fixed_price, monetary=True, dp="Product Price", currency_obj=item.currency_id) elif item.compute_price == 'percentage': item.price = _("%s %% discount", item.percent_price) else: item.price = _( "%(percentage)s %% discount and %(price)s surcharge", percentage=item.price_discount, price=item.price_surcharge) @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({ 'base': 'list_price', 'price_discount': 0.0, 'price_surcharge': 0.0, 'price_round': 0.0, 'price_min_margin': 0.0, 'price_max_margin': 0.0, }) @api.onchange('product_id') def _onchange_product_id(self): has_product_id = self.filtered('product_id') for item in has_product_id: item.product_tmpl_id = item.product_id.product_tmpl_id if self.env.context.get('default_applied_on', False) == '1_product': # If a product variant is specified, apply on variants instead # Reset if product variant is removed has_product_id.update({'applied_on': '0_product_variant'}) (self - has_product_id).update({'applied_on': '1_product'}) @api.onchange('product_tmpl_id') def _onchange_product_tmpl_id(self): has_tmpl_id = self.filtered('product_tmpl_id') for item in has_tmpl_id: if item.product_id and item.product_id.product_tmpl_id != item.product_tmpl_id: item.product_id = None @api.onchange('product_id', 'product_tmpl_id', 'categ_id') def _onchane_rule_content(self): if not self.user_has_groups( 'product.group_sale_pricelist') and not self.env.context.get( 'default_applied_on', False): # If advanced pricelists are disabled (applied_on field is not visible) # AND we aren't coming from a specific product template/variant. variants_rules = self.filtered('product_id') template_rules = (self - variants_rules).filtered('product_tmpl_id') variants_rules.update({'applied_on': '0_product_variant'}) template_rules.update({'applied_on': '1_product'}) (self - variants_rules - template_rules).update( {'applied_on': '3_global'}) @api.model_create_multi def create(self, vals_list): for values in vals_list: if values.get('applied_on', False): # Ensure item consistency for later searches. applied_on = values['applied_on'] if applied_on == '3_global': values.update( dict(product_id=None, product_tmpl_id=None, categ_id=None)) elif applied_on == '2_product_category': values.update(dict(product_id=None, product_tmpl_id=None)) elif applied_on == '1_product': values.update(dict(product_id=None, categ_id=None)) elif applied_on == '0_product_variant': values.update(dict(categ_id=None)) return super(PricelistItem, self).create(vals_list) def write(self, values): if values.get('applied_on', False): # Ensure item consistency for later searches. applied_on = values['applied_on'] if applied_on == '3_global': values.update( dict(product_id=None, product_tmpl_id=None, categ_id=None)) elif applied_on == '2_product_category': values.update(dict(product_id=None, product_tmpl_id=None)) elif applied_on == '1_product': values.update(dict(product_id=None, categ_id=None)) elif applied_on == '0_product_variant': values.update(dict(categ_id=None)) res = super(PricelistItem, self).write(values) # When the pricelist changes we need the product.template price # to be invalided and recomputed. self.env['product.template'].invalidate_cache(['price']) self.env['product.product'].invalidate_cache(['price']) 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 UoM(models.Model): _name = 'uom.uom' _description = 'Product Unit of Measure' _order = "name" name = fields.Char('Unit of Measure', required=True, translate=True) category_id = fields.Many2one( 'uom.category', 'Category', required=True, ondelete='cascade', help= "Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios." ) factor = fields.Float( 'Ratio', default=1.0, digits=0, required=True, # force NUMERIC with unlimited precision help= 'How much bigger or smaller this unit is compared to the reference Unit of Measure for this category: 1 * (reference unit) = ratio * (this unit)' ) factor_inv = fields.Float( 'Bigger Ratio', compute='_compute_factor_inv', digits=0, # force NUMERIC with unlimited precision readonly=True, required=True, help= 'How many times this Unit of Measure is bigger than the reference Unit of Measure in this category: 1 * (this unit) = ratio * (reference unit)' ) rounding = fields.Float( 'Rounding Precision', default=0.01, digits=0, required=True, help="The computed quantity will be a multiple of this value. " "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece." ) active = fields.Boolean( 'Active', default=True, help= "Uncheck the active field to disable a unit of measure without deleting it." ) uom_type = fields.Selection( [('bigger', 'Bigger than the reference Unit of Measure'), ('reference', 'Reference Unit of Measure for this category'), ('smaller', 'Smaller than the reference Unit of Measure')], 'Type', default='reference', required=1) _sql_constraints = [ ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!'), ('rounding_gt_zero', 'CHECK (rounding>0)', 'The rounding precision must be strictly positive.'), ('factor_reference_is_one', "CHECK((uom_type = 'reference' AND factor = 1.0) OR (uom_type != 'reference'))", "The reference unit must have a conversion factor equal to 1.") ] @api.depends('factor') def _compute_factor_inv(self): for uom in self: uom.factor_inv = uom.factor and (1.0 / uom.factor) or 0.0 @api.onchange('uom_type') def _onchange_uom_type(self): if self.uom_type == 'reference': self.factor = 1 @api.constrains('category_id', 'uom_type', 'active') def _check_category_reference_uniqueness(self): """ Force the existence of only one UoM reference per category NOTE: this is a constraint on the all table. This might not be a good practice, but this is not possible to do it in SQL directly. """ category_ids = self.mapped('category_id').ids self.env['uom.uom'].flush(['category_id', 'uom_type', 'active']) self._cr.execute( """ SELECT C.id AS category_id, count(U.id) AS uom_count FROM uom_category C LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference' AND U.active = 't' WHERE C.id IN %s GROUP BY C.id """, (tuple(category_ids), )) for uom_data in self._cr.dictfetchall(): if uom_data['uom_count'] == 0: raise ValidationError( _("UoM category %s should have a reference unit of measure. If you just created a new category, please record the 'reference' unit first." ) % (self.env['uom.category'].browse( uom_data['category_id']).name, )) if uom_data['uom_count'] > 1: raise ValidationError( _("UoM category %s should only have one reference unit of measure." ) % (self.env['uom.category'].browse( uom_data['category_id']).name, )) @api.constrains('category_id') def _validate_uom_category(self): for uom in self: reference_uoms = self.env['uom.uom'].search([ ('category_id', '=', uom.category_id.id), ('uom_type', '=', 'reference') ]) if len(reference_uoms) > 1: raise ValidationError( _("UoM category %s should only have one reference unit of measure." ) % (self.category_id.name)) @api.model_create_multi def create(self, vals_list): for values in vals_list: if 'factor_inv' in values: factor_inv = values.pop('factor_inv') values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0 return super(UoM, self).create(vals_list) def write(self, values): if 'factor_inv' in values: factor_inv = values.pop('factor_inv') values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0 return super(UoM, self).write(values) def unlink(self): uom_categ_unit = self.env.ref('uom.product_uom_categ_unit') uom_categ_wtime = self.env.ref('uom.uom_categ_wtime') uom_categ_kg = self.env.ref('uom.product_uom_categ_kgm') if any(uom.category_id.id in ( uom_categ_unit + uom_categ_wtime + uom_categ_kg).ids and uom.uom_type == 'reference' for uom in self): raise UserError( _("You cannot delete this UoM as it is used by the system. You should rather archive it." )) # UoM with external IDs shouldn't be deleted since they will most probably break the app somewhere else. # For example, in addons/product/models/product_template.py, cubic meters are used in `_get_volume_uom_id_from_ir_config_parameter()`, # meters in `_get_length_uom_id_from_ir_config_parameter()`, and so on. if self.env['ir.model.data'].search_count([('model', '=', self._name), ('res_id', 'in', self.ids) ]): raise UserError( _("You cannot delete this UoM as it is used by the system. You should rather archive it." )) return super(UoM, self).unlink() @api.model def name_create(self, name): """ The UoM category and factor are required, so we'll have to add temporary values for imported UoMs """ values = {self._rec_name: name, 'factor': 1} # look for the category based on the english name, i.e. no context on purpose! # TODO: should find a way to have it translated but not created until actually used if not self._context.get('default_category_id'): EnglishUoMCateg = self.env['uom.category'].with_context({}) misc_category = EnglishUoMCateg.search([ ('name', '=', 'Unsorted/Imported Units') ]) if misc_category: values['category_id'] = misc_category.id else: values['category_id'] = EnglishUoMCateg.name_create( 'Unsorted/Imported Units')[0] new_uom = self.create(values) return new_uom.name_get()[0] def _compute_quantity(self, qty, to_unit, round=True, rounding_method='UP', raise_if_failure=True): """ Convert the given quantity from the current UoM `self` into a given one :param qty: the quantity to convert :param to_unit: the destination UoM record (uom.uom) :param raise_if_failure: only if the conversion is not possible - if true, raise an exception if the conversion is not possible (different UoM category), - otherwise, return the initial quantity """ if not self or not qty: return qty self.ensure_one() if self != to_unit and self.category_id.id != to_unit.category_id.id: if raise_if_failure: raise UserError( _('The unit of measure %s defined on the order line doesn\'t belong to the same category as the unit of measure %s defined on the product. Please correct the unit of measure defined on the order line or on the product, they should belong to the same category.' ) % (self.name, to_unit.name)) else: return qty if self == to_unit: amount = qty else: amount = qty / self.factor if to_unit: amount = amount * to_unit.factor if to_unit and round: amount = tools.float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method) return amount def _compute_price(self, price, to_unit): self.ensure_one() if not self or not price or not to_unit or self == to_unit: return price if self.category_id.id != to_unit.category_id.id: return price amount = price * self.factor if to_unit: amount = amount / to_unit.factor return amount
class OpAdmission(models.Model): _name = 'op.admission' _inherit = 'mail.thread' _rec_name = 'application_number' _order = "application_number desc" _description = "Admission" name = fields.Char( 'First Name', size=128, required=True, states={'done': [('readonly', True)]}) middle_name = fields.Char( 'Middle Name', size=128, states={'done': [('readonly', True)]}) last_name = fields.Char( 'Last Name', size=128, required=True, states={'done': [('readonly', True)]}) title = fields.Many2one( 'res.partner.title', 'Title', states={'done': [('readonly', True)]}) application_number = fields.Char( 'Application Number', size=16, required=True, copy=False, states={'done': [('readonly', True)]}, default=lambda self: self.env['ir.sequence'].next_by_code('op.admission')) admission_date = fields.Date( 'Admission Date', copy=False, states={'done': [('readonly', True)]}) application_date = fields.Datetime( 'Application Date', required=True, copy=False, states={'done': [('readonly', True)]}, default=lambda self: fields.Datetime.now()) birth_date = fields.Date( 'Birth Date', required=True, states={'done': [('readonly', True)]}) course_id = fields.Many2one( 'op.course', 'Course', required=True, states={'done': [('readonly', True)]}) batch_id = fields.Many2one( 'op.batch', 'Batch', required=False, states={'done': [('readonly', True)], 'fees_paid': [('required', True)]}) street = fields.Char( 'Street', size=256, states={'done': [('readonly', True)]}) street2 = fields.Char( 'Street2', size=256, states={'done': [('readonly', True)]}) phone = fields.Char( 'Phone', size=16, states={'done': [('readonly', True)]}) mobile = fields.Char( 'Mobile', size=16, states={'done': [('readonly', True)]}) email = fields.Char( 'Email', size=256, required=True, states={'done': [('readonly', True)]}) city = fields.Char('City', size=64, states={'done': [('readonly', True)]}) zip = fields.Char('Zip', size=8, states={'done': [('readonly', True)]}) state_id = fields.Many2one( 'res.country.state', 'States', states={'done': [('readonly', True)]}) country_id = fields.Many2one( 'res.country', 'Country', states={'done': [('readonly', True)]}) fees = fields.Float('Fees', states={'done': [('readonly', True)]}) image = fields.Binary('image', states={'done': [('readonly', True)]}) state = fields.Selection( [('draft', 'Draft'), ('submit', 'Submitted'), ('confirm', 'Confirmed'), ('admission', 'Admission Confirm'), ('reject', 'Rejected'), ('pending', 'Pending'), ('cancel', 'Cancelled'), ('done', 'Done')], 'State', default='draft', track_visibility='onchange') due_date = fields.Date('Due Date', states={'done': [('readonly', True)]}) prev_institute_id = fields.Many2one( 'res.partner', 'Previous Institute', states={'done': [('readonly', True)]}) prev_course_id = fields.Many2one( 'op.course', 'Previous Course', states={'done': [('readonly', True)]}) prev_result = fields.Char( 'Previous Result', size=256, states={'done': [('readonly', True)]}) family_business = fields.Char( 'Family Business', size=256, states={'done': [('readonly', True)]}) family_income = fields.Float( 'Family Income', states={'done': [('readonly', True)]}) gender = fields.Selection( [('m', 'Male'), ('f', 'Female'), ('o', 'Other')], 'Gender', required=True, states={'done': [('readonly', True)]}) student_id = fields.Many2one( 'op.student', 'Student', states={'done': [('readonly', True)]}) nbr = fields.Integer('No of Admission', readonly=True) register_id = fields.Many2one( 'op.admission.register', 'Admission Register', required=True, states={'done': [('readonly', True)]}) partner_id = fields.Many2one('res.partner', 'Partner') is_student = fields.Boolean('Is Already Student') fees_term_id = fields.Many2one('op.fees.terms', 'Fees Term') @api.onchange('student_id', 'is_student') def onchange_student(self): if self.is_student and self.student_id: student = self.student_id self.title = student.title and student.title.id or False self.name = student.name self.middle_name = student.middle_name self.last_name = student.last_name self.birth_date = student.birth_date self.gender = student.gender self.image = student.image or False self.street = student.street or False self.street2 = student.street2 or False self.phone = student.phone or False self.mobile = student.mobile or False self.email = student.email or False self.zip = student.zip or False self.city = student.city or False self.country_id = student.country_id and \ student.country_id.id or False self.state_id = student.state_id and \ student.state_id.id or False self.partner_id = student.partner_id and \ student.partner_id.id or False else: self.title = '' self.name = '' self.middle_name = '' self.last_name = '' self.birth_date = '' self.gender = '' self.image = False self.street = '' self.street2 = '' self.phone = '' self.mobile = '' self.zip = '' self.city = '' self.country_id = False self.state_id = False self.partner_id = False @api.onchange('register_id') def onchange_register(self): self.course_id = self.register_id.course_id self.fees = self.register_id.product_id.lst_price @api.onchange('course_id') def onchange_course(self): self.batch_id = False term_id = False if self.course_id and self.course_id.fees_term_id: term_id = self.course_id.fees_term_id.id self.fees_term_id = term_id @api.multi @api.constrains('register_id', 'application_date') def _check_admission_register(self): for record in self: start_date = fields.Date.from_string(record.register_id.start_date) end_date = fields.Date.from_string(record.register_id.end_date) application_date = fields.Date.from_string(record.application_date) if application_date < start_date or application_date > end_date: raise ValidationError(_( "Application Date should be between Start Date & \ End Date of Admission Register.")) @api.multi @api.constrains('birth_date') def _check_birthdate(self): for record in self: if record.birth_date > fields.Date.today(): raise ValidationError(_( "Birth Date can't be greater than current date!")) @api.multi def submit_form(self): self.state = 'submit' @api.multi def admission_confirm(self): self.state = 'admission' @api.multi def confirm_in_progress(self): for record in self: if not record.batch_id: raise ValidationError(_('Please assign batch.')) if not record.partner_id: partner_id = self.env['res.partner'].create({ 'name': record.name }) record.partner_id = partner_id.id record.state = 'confirm' @api.multi def get_student_vals(self): for student in self: return { 'title': student.title and student.title.id or False, 'name': student.name, 'middle_name': student.middle_name, 'last_name': student.last_name, 'birth_date': student.birth_date, 'gender': student.gender, 'course_id': student.course_id and student.course_id.id or False, 'batch_id': student.batch_id and student.batch_id.id or False, 'image': student.image or False, 'street': student.street or False, 'street2': student.street2 or False, 'phone': student.phone or False, 'email': student.email or False, 'mobile': student.mobile or False, 'zip': student.zip or False, 'city': student.city or False, 'country_id': student.country_id and student.country_id.id or False, 'state_id': student.state_id and student.state_id.id or False, 'course_detail_ids': [[0, False, { 'date': fields.Date.today(), 'course_id': student.course_id and student.course_id.id or False, 'batch_id': student.batch_id and student.batch_id.id or False, }]], } @api.multi def enroll_student(self): for record in self: total_admission = self.env['op.admission'].search_count( [('register_id', '=', record.register_id.id), ('state', '=', 'done')]) if record.register_id.max_count: if not total_admission < record.register_id.max_count: msg = 'Max Admission In Admission Register :- (%s)' % ( record.register_id.max_count) raise ValidationError(_(msg)) if not record.student_id: vals = record.get_student_vals() vals.update({'partner_id': record.partner_id.id}) student_id = self.env['op.student'].create(vals).id else: student_id = record.student_id.id record.student_id.write({ 'course_detail_ids': [[0, False, { 'date': fields.Date.today(), 'course_id': record.course_id and record.course_id.id or False, 'batch_id': record.batch_id and record.batch_id.id or False, }]], }) if record.fees_term_id: val = [] product_id = record.register_id.product_id.id for line in record.fees_term_id.line_ids: no_days = line.due_days per_amount = line.value amount = (per_amount * record.fees) / 100 date = ( datetime.today() + relativedelta(days=no_days)).date() dict_val = { 'fees_line_id': line.id, 'amount': amount, 'date': date, 'product_id': product_id, 'state': 'draft', } val.append([0, False, dict_val]) self.env['op.student'].browse(student_id).write({ 'fees_detail_ids': val }) record.write({ 'nbr': 1, 'state': 'done', 'admission_date': fields.Date.today(), 'student_id': student_id, }) reg_id = self.env['op.subject.registration'].create({ 'student_id': student_id, 'batch_id': record.batch_id.id, 'course_id': record.course_id.id, 'min_unit_load': record.course_id.min_unit_load or 0.0, 'max_unit_load': record.course_id.max_unit_load or 0.0, 'state': 'draft', }) reg_id.get_subjects() @api.multi def confirm_rejected(self): self.state = 'reject' @api.multi def confirm_pending(self): self.state = 'pending' @api.multi def confirm_to_draft(self): self.state = 'draft' @api.multi def confirm_cancel(self): self.state = 'cancel' @api.multi def payment_process(self): self.state = 'fees_paid' @api.multi def open_student(self): form_view = self.env.ref('openeducat_core.view_op_student_form') tree_view = self.env.ref('openeducat_core.view_op_student_tree') value = { 'domain': str([('id', '=', self.student_id.id)]), 'view_type': 'form', 'view_mode': 'tree, form', 'res_model': 'op.student', 'view_id': False, 'views': [(form_view and form_view.id or False, 'form'), (tree_view and tree_view.id or False, 'tree')], 'type': 'ir.actions.act_window', 'res_id': self.student_id.id, 'target': 'current', 'nodestroy': True } self.state = 'done' return value @api.multi def create_invoice(self): """ Create invoice for fee payment process of student """ inv_obj = self.env['account.invoice'] partner_id = self.env['res.partner'].create({'name': self.name}) account_id = False product = self.register_id.product_id if product.id: account_id = product.property_account_income_id.id if not account_id: account_id = product.categ_id.property_account_income_categ_id.id if not account_id: raise UserError( _('There is no income account defined for this product: "%s". \ You may have to install a chart of account from Accounting \ app, settings menu.') % (product.name,)) if self.fees <= 0.00: raise UserError(_('The value of the deposit amount must be \ positive.')) else: amount = self.fees name = product.name invoice = inv_obj.create({ 'name': self.name, 'origin': self.application_number, 'type': 'out_invoice', 'reference': False, 'account_id': partner_id.property_account_receivable_id.id, 'partner_id': partner_id.id, 'invoice_line_ids': [(0, 0, { 'name': name, 'origin': self.application_number, 'account_id': account_id, 'price_unit': amount, 'quantity': 1.0, 'discount': 0.0, 'uom_id': self.register_id.product_id.uom_id.id, 'product_id': product.id, })], }) invoice.compute_taxes() form_view = self.env.ref('account.invoice_form') tree_view = self.env.ref('account.invoice_tree') value = { 'domain': str([('id', '=', invoice.id)]), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.invoice', 'view_id': False, 'views': [(form_view and form_view.id or False, 'form'), (tree_view and tree_view.id or False, 'tree')], 'type': 'ir.actions.act_window', 'res_id': invoice.id, 'target': 'current', 'nodestroy': True } self.partner_id = partner_id self.state = 'payment_process' return value
class MrpBom(models.Model): """ Defines bills of material for a product or a product template """ _name = 'mrp.bom' _description = 'Bill of Material' _inherit = ['mail.thread'] _rec_name = 'product_tmpl_id' _order = "sequence" _check_company_auto = True def _get_default_product_uom_id(self): return self.env['uom.uom'].search([], limit=1, order='id').id code = fields.Char('Reference') active = fields.Boolean( 'Active', default=True, help= "If the active field is set to False, it will allow you to hide the bills of material without removing it." ) type = fields.Selection([('normal', 'Manufacture this product'), ('phantom', 'Kit')], 'BoM Type', default='normal', required=True) product_tmpl_id = fields.Many2one( 'product.template', 'Product', check_company=True, index=True, domain= "[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", required=True) product_id = fields.Many2one( 'product.product', 'Product Variant', check_company=True, index=True, domain= "['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", help= "If a product variant is defined the BOM is available only for this product." ) bom_line_ids = fields.One2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True) byproduct_ids = fields.One2many('mrp.bom.byproduct', 'bom_id', 'By-products', copy=True) product_qty = fields.Float('Quantity', default=1.0, digits='Unit of Measure', required=True) product_uom_id = fields.Many2one( 'uom.uom', 'Unit of Measure', default=_get_default_product_uom_id, required=True, help= "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]") product_uom_category_id = fields.Many2one( related='product_tmpl_id.uom_id.category_id') sequence = fields.Integer( 'Sequence', help= "Gives the sequence order when displaying a list of bills of material." ) operation_ids = fields.One2many('mrp.routing.workcenter', 'bom_id', 'Operations', copy=True) ready_to_produce = fields.Selection( [('all_available', ' When all components are available'), ('asap', 'When components for 1st operation are available')], string='Manufacturing Readiness', default='asap', help= "Defines when a Manufacturing Order is considered as ready to be started", required=True) picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', domain= "[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]", check_company=True, help= u"When a procurement has a ‘produce’ route with a operation type set, it will try to create " "a Manufacturing Order for that product using a BoM of the same operation type. That allows " "to define stock rules which trigger different manufacturing orders with different BoMs." ) company_id = fields.Many2one('res.company', 'Company', index=True, default=lambda self: self.env.company) consumption = fields.Selection( [('flexible', 'Allowed'), ('warning', 'Allowed with warning'), ('strict', 'Blocked')], help= "Defines if you can consume more or less components than the quantity defined on the BoM:\n" " * Allowed: allowed for all manufacturing users.\n" " * Allowed with warning: allowed for all manufacturing users with summary of consumption differences when closing the manufacturing order.\n" " * Blocked: only a manager can close a manufacturing order when the BoM consumption is not respected.", default='warning', string='Flexible Consumption', required=True) _sql_constraints = [ ('qty_positive', 'check (product_qty > 0)', 'The quantity to produce must be positive!'), ] @api.onchange('product_id') def onchange_product_id(self): if self.product_id: for line in self.bom_line_ids: line.bom_product_template_attribute_value_ids = False @api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids') def _check_bom_lines(self): for bom in self: for bom_line in bom.bom_line_ids: if bom.product_id: same_product = bom.product_id == bom_line.product_id else: same_product = bom.product_tmpl_id == bom_line.product_id.product_tmpl_id if same_product: raise ValidationError( _("BoM line product %s should not be the same as BoM product." ) % bom.display_name) if bom.product_id and bom_line.bom_product_template_attribute_value_ids: raise ValidationError( _("BoM cannot concern product %s and have a line with attributes (%s) at the same time." ) % (bom.product_id.display_name, ", ".join([ ptav.display_name for ptav in bom_line.bom_product_template_attribute_value_ids ]))) for ptav in bom_line.bom_product_template_attribute_value_ids: if ptav.product_tmpl_id != bom.product_tmpl_id: raise ValidationError( _("The attribute value %(attribute)s set on product %(product)s does not match the BoM product %(bom_product)s.", attribute=ptav.display_name, product=ptav.product_tmpl_id.display_name, bom_product=bom_line.parent_product_tmpl_id. display_name)) @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} if not self.product_uom_id or not self.product_tmpl_id: return if self.product_uom_id.category_id.id != self.product_tmpl_id.uom_id.category_id.id: self.product_uom_id = self.product_tmpl_id.uom_id.id res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } return res @api.onchange('product_tmpl_id') def onchange_product_tmpl_id(self): if self.product_tmpl_id: self.product_uom_id = self.product_tmpl_id.uom_id.id if self.product_id.product_tmpl_id != self.product_tmpl_id: self.product_id = False for line in self.bom_line_ids: line.bom_product_template_attribute_value_ids = False def copy(self, default=None): res = super().copy(default) for bom_line in res.bom_line_ids: if bom_line.operation_id: operation = res.operation_ids.filtered( lambda op: op.name == bom_line.operation_id.name and op. workcenter_id == bom_line.operation_id.workcenter_id) bom_line.operation_id = operation return res @api.model def name_create(self, name): # prevent to use string as product_tmpl_id if isinstance(name, str): raise UserError( _("You cannot create a new Bill of Material from here.")) return super(MrpBom, self).name_create(name) def name_get(self): return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code or '', bom.product_tmpl_id.display_name)) for bom in self] @api.constrains('product_tmpl_id', 'product_id', 'type') def check_kit_has_not_orderpoint(self): product_ids = [ pid for bom in self.filtered(lambda bom: bom.type == "phantom") for pid in (bom.product_id.ids or bom.product_tmpl_id.product_variant_ids.ids) ] if self.env['stock.warehouse.orderpoint'].search( [('product_id', 'in', product_ids)], count=True): raise ValidationError( _("You can not create a kit-type bill of materials for products that have at least one reordering rule." )) def unlink(self): if self.env['mrp.production'].search( [('bom_id', 'in', self.ids), ('state', 'not in', ['done', 'cancel'])], limit=1): raise UserError( _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.' )) return super(MrpBom, self).unlink() @api.model def _bom_find_domain(self, product_tmpl=None, product=None, picking_type=None, company_id=False, bom_type=False): if product: if not product_tmpl: product_tmpl = product.product_tmpl_id domain = [ '|', ('product_id', '=', product.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl.id) ] elif product_tmpl: domain = [('product_tmpl_id', '=', product_tmpl.id)] else: # neither product nor template, makes no sense to search raise UserError( _('You should provide either a product or a product template to search a BoM' )) if picking_type: domain += [ '|', ('picking_type_id', '=', picking_type.id), ('picking_type_id', '=', False) ] if company_id or self.env.context.get('company_id'): domain = domain + [ '|', ('company_id', '=', False), ('company_id', '=', company_id or self.env.context.get('company_id')) ] if bom_type: domain += [('type', '=', bom_type)] # order to prioritize bom with product_id over the one without return domain @api.model def _bom_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False, bom_type=False): """ Finds BoM for particular product, picking and company """ if product and product.type == 'service' or product_tmpl and product_tmpl.type == 'service': return self.env['mrp.bom'] domain = self._bom_find_domain(product_tmpl=product_tmpl, product=product, picking_type=picking_type, company_id=company_id, bom_type=bom_type) if domain is False: return self.env['mrp.bom'] return self.search(domain, order='sequence, product_id', limit=1) @api.model def _get_product2bom(self, products, bom_type=False): """Optimized variant of _bom_find to work with recordset""" products = products.filtered(lambda product: product.type != 'service') if not products: return {} product_templates = products.mapped('product_tmpl_id') domain = [ '|', ('product_id', 'in', products.ids), '&', ('product_id', '=', False), ('product_tmpl_id', 'in', product_templates.ids) ] if self.env.context.get('company_id'): domain = domain + [ '|', ('company_id', '=', False), ('company_id', '=', self.env.context.get('company_id')) ] if bom_type: domain += [('type', '=', bom_type)] boms = self.search(domain, order='sequence, product_id') template2bom = {} variant2bom = {} for bom in boms: # Use "setdefault" to take only first bom if we have few ones for # the same product if bom.product_id: variant2bom.setdefault(bom.product_id, bom) else: template2bom.setdefault(bom.product_tmpl_id, bom) result = {} for p in products: bom = variant2bom.get(p) or template2bom.get(p.product_tmpl_id) if bom: result[p] = bom return result def explode(self, product, quantity, picking_type=False): """ Explodes the BoM and creates two lists with all the information you need: bom_done and line_done Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM and converted into its UoM """ from collections import defaultdict graph = defaultdict(list) V = set() def check_cycle(v, visited, recStack, graph): visited[v] = True recStack[v] = True for neighbour in graph[v]: if visited[neighbour] == False: if check_cycle(neighbour, visited, recStack, graph) == True: return True elif recStack[neighbour] == True: return True recStack[v] = False return False boms_done = [(self, { 'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False })] lines_done = [] V |= set([product.product_tmpl_id.id]) bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids] for bom_line in self.bom_line_ids: V |= set([bom_line.product_id.product_tmpl_id.id]) graph[product.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) while bom_lines: current_line, current_product, current_qty, parent_line = bom_lines[ 0] bom_lines = bom_lines[1:] if current_line._skip_bom_line(current_product): continue line_quantity = current_qty * current_line.product_qty bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id, bom_type='phantom') if bom: converted_line_quantity = current_line.product_uom_id._compute_quantity( line_quantity / bom.product_qty, bom.product_uom_id) bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines for bom_line in bom.bom_line_ids: graph[current_line.product_id.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) if bom_line.product_id.product_tmpl_id.id in V and check_cycle( bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph): raise UserError( _('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!' )) V |= set([bom_line.product_id.product_tmpl_id.id]) boms_done.append((bom, { 'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line })) else: # We round up here because the user expects that if he has to consume a little more, the whole UOM unit # should be consumed. rounding = current_line.product_uom_id.rounding line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP') lines_done.append((current_line, { 'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line })) return boms_done, lines_done @api.model def get_import_templates(self): return [{ 'label': _('Import Template for Bills of Materials'), 'template': '/mrp/static/xls/mrp_bom.xls' }]
class ReportStockForecat(models.Model): _name = 'report.stock.forecast' _auto = False date = fields.Date(string='Date') product_id = fields.Many2one('product.product', string='Product', readonly=True) product_tmpl_id = fields.Many2one('product.template', string='Product Template', related='product_id.product_tmpl_id', readonly=True) cumulative_quantity = fields.Float(string='Cumulative Quantity', readonly=True) quantity = fields.Float(readonly=True) @api.model_cr def init(self): tools.drop_view_if_exists(self._cr, 'report_stock_forecast') self._cr.execute( """CREATE or REPLACE VIEW report_stock_forecast AS (SELECT MIN(id) as id, product_id as product_id, date as date, sum(product_qty) AS quantity, sum(sum(product_qty)) OVER (PARTITION BY product_id ORDER BY date) AS cumulative_quantity FROM (SELECT MIN(id) as id, MAIN.product_id as product_id, SUB.date as date, CASE WHEN MAIN.date = SUB.date THEN sum(MAIN.product_qty) ELSE 0 END as product_qty FROM (SELECT MIN(sq.id) as id, sq.product_id, date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) as date, SUM(sq.quantity) AS product_qty FROM stock_quant as sq LEFT JOIN product_product ON product_product.id = sq.product_id LEFT JOIN stock_location location_id ON sq.location_id = location_id.id WHERE location_id.usage = 'internal' GROUP BY date, sq.product_id UNION ALL SELECT MIN(-sm.id) as id, sm.product_id, CASE WHEN sm.date_expected > CURRENT_DATE THEN date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD')) ELSE date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) END AS date, SUM(sm.product_qty) AS product_qty FROM stock_move as sm LEFT JOIN product_product ON product_product.id = sm.product_id LEFT JOIN stock_location dest_location ON sm.location_dest_id = dest_location.id LEFT JOIN stock_location source_location ON sm.location_id = source_location.id WHERE sm.state IN ('confirmed','assigned','waiting') and source_location.usage != 'internal' and dest_location.usage = 'internal' GROUP BY sm.date_expected,sm.product_id UNION ALL SELECT MIN(-sm.id) as id, sm.product_id, CASE WHEN sm.date_expected > CURRENT_DATE THEN date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD')) ELSE date_trunc('week', to_date(to_char(CURRENT_DATE, 'YYYY/MM/DD'), 'YYYY/MM/DD')) END AS date, SUM(-(sm.product_qty)) AS product_qty FROM stock_move as sm LEFT JOIN product_product ON product_product.id = sm.product_id LEFT JOIN stock_location source_location ON sm.location_id = source_location.id LEFT JOIN stock_location dest_location ON sm.location_dest_id = dest_location.id WHERE sm.state IN ('confirmed','assigned','waiting') and source_location.usage = 'internal' and dest_location.usage != 'internal' GROUP BY sm.date_expected,sm.product_id) as MAIN LEFT JOIN (SELECT DISTINCT date FROM ( SELECT date_trunc('week', CURRENT_DATE) AS DATE UNION ALL SELECT date_trunc('week', to_date(to_char(sm.date_expected, 'YYYY/MM/DD'), 'YYYY/MM/DD')) AS date FROM stock_move sm LEFT JOIN stock_location source_location ON sm.location_id = source_location.id LEFT JOIN stock_location dest_location ON sm.location_dest_id = dest_location.id WHERE sm.state IN ('confirmed','assigned','waiting') and sm.date_expected > CURRENT_DATE and ((dest_location.usage = 'internal' AND source_location.usage != 'internal') or (source_location.usage = 'internal' AND dest_location.usage != 'internal'))) AS DATE_SEARCH) SUB ON (SUB.date IS NOT NULL) GROUP BY MAIN.product_id,SUB.date, MAIN.date ) AS FINAL GROUP BY product_id,date)""")
class MrpBomLine(models.Model): _name = 'mrp.bom.line' _order = "sequence, id" _rec_name = "product_id" _description = 'Bill of Material Line' _check_company_auto = True def _get_default_product_uom_id(self): return self.env['uom.uom'].search([], limit=1, order='id').id product_id = fields.Many2one('product.product', 'Component', required=True, check_company=True) product_tmpl_id = fields.Many2one('product.template', 'Product Template', related='product_id.product_tmpl_id') company_id = fields.Many2one(related='bom_id.company_id', store=True, index=True, readonly=True) product_qty = fields.Float('Quantity', default=1.0, digits='Product Unit of Measure', required=True) product_uom_id = fields.Many2one( 'uom.uom', 'Product Unit of Measure', default=_get_default_product_uom_id, required=True, help= "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]") product_uom_category_id = fields.Many2one( related='product_id.uom_id.category_id') sequence = fields.Integer('Sequence', default=1, help="Gives the sequence order when displaying.") bom_id = fields.Many2one('mrp.bom', 'Parent BoM', index=True, ondelete='cascade', required=True) parent_product_tmpl_id = fields.Many2one('product.template', 'Parent Product Template', related='bom_id.product_tmpl_id') possible_bom_product_template_attribute_value_ids = fields.Many2many( 'product.template.attribute.value', compute='_compute_possible_bom_product_template_attribute_value_ids') bom_product_template_attribute_value_ids = fields.Many2many( 'product.template.attribute.value', string="Apply on Variants", ondelete='restrict', domain= "[('id', 'in', possible_bom_product_template_attribute_value_ids)]", help="BOM Product Variants needed to apply this line.") allowed_operation_ids = fields.Many2many( 'mrp.routing.workcenter', compute='_compute_allowed_operation_ids') operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Consumed in Operation', check_company=True, domain="[('id', 'in', allowed_operation_ids)]", help= "The operation where the components are consumed, or the finished products created." ) child_bom_id = fields.Many2one('mrp.bom', 'Sub BoM', compute='_compute_child_bom_id') child_line_ids = fields.One2many('mrp.bom.line', string="BOM lines of the referred bom", compute='_compute_child_line_ids') attachments_count = fields.Integer('Attachments Count', compute='_compute_attachments_count') _sql_constraints = [ ('bom_qty_zero', 'CHECK (product_qty>=0)', 'All product quantities must be greater or equal to 0.\n' 'Lines with 0 quantities can be used as optional lines. \n' 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !' ), ] @api.depends( 'parent_product_tmpl_id.attribute_line_ids.value_ids', 'parent_product_tmpl_id.attribute_line_ids.attribute_id.create_variant', 'parent_product_tmpl_id.attribute_line_ids.product_template_value_ids.ptav_active', ) def _compute_possible_bom_product_template_attribute_value_ids(self): for line in self: line.possible_bom_product_template_attribute_value_ids = line.parent_product_tmpl_id.valid_product_template_attribute_line_ids._without_no_variant_attributes( ).product_template_value_ids._only_active() @api.depends('product_id', 'bom_id') def _compute_child_bom_id(self): for line in self: if not line.product_id: line.child_bom_id = False else: line.child_bom_id = self.env['mrp.bom']._bom_find( product_tmpl=line.product_id.product_tmpl_id, product=line.product_id) @api.depends('product_id') def _compute_attachments_count(self): for line in self: nbr_attach = self.env['mrp.document'].search_count([ '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', line.product_id.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', line.product_id.product_tmpl_id.id) ]) line.attachments_count = nbr_attach @api.depends('child_bom_id') def _compute_child_line_ids(self): """ If the BOM line refers to a BOM, return the ids of the child BOM lines """ for line in self: line.child_line_ids = line.child_bom_id.bom_line_ids.ids or False @api.depends('bom_id') def _compute_allowed_operation_ids(self): for bom_line in self: if not bom_line.bom_id.operation_ids: bom_line.allowed_operation_ids = self.env[ 'mrp.routing.workcenter'] else: operation_domain = [ ('id', 'in', bom_line.bom_id.operation_ids.ids), '|', ('company_id', '=', bom_line.company_id.id), ('company_id', '=', False) ] bom_line.allowed_operation_ids = self.env[ 'mrp.routing.workcenter'].search(operation_domain) @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} if not self.product_uom_id or not self.product_id: return res if self.product_uom_id.category_id != self.product_id.uom_id.category_id: self.product_uom_id = self.product_id.uom_id.id res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } return res @api.onchange('product_id') def onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_id.id @api.model_create_multi def create(self, vals_list): for values in vals_list: if 'product_id' in values and 'product_uom_id' not in values: values['product_uom_id'] = self.env['product.product'].browse( values['product_id']).uom_id.id return super(MrpBomLine, self).create(vals_list) def _skip_bom_line(self, product): """ Control if a BoM line should be produced, can be inherited to add custom control. It currently checks that all variant values are in the product. If multiple values are encoded for the same attribute line, only one of them has to be found on the variant. """ self.ensure_one() if product._name == 'product.template': return False if self.bom_product_template_attribute_value_ids: for ptal, iter_ptav in groupby( self.bom_product_template_attribute_value_ids.sorted( 'attribute_line_id'), lambda ptav: ptav.attribute_line_id): if not any(ptav in product.product_template_attribute_value_ids for ptav in iter_ptav): return True return False def action_see_attachments(self): domain = [ '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', self.product_id.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', self.product_id.product_tmpl_id.id) ] attachment_view = self.env.ref('mrp.view_document_file_kanban_mrp') return { 'name': _('Attachments'), 'domain': domain, 'res_model': 'mrp.document', 'type': 'ir.actions.act_window', 'view_id': attachment_view.id, 'views': [(attachment_view.id, 'kanban'), (False, 'form')], 'view_mode': 'kanban,tree,form', 'help': _('''<p class="o_view_nocontent_smiling_face"> Upload files to your product </p><p> Use this feature to store any files, like drawings or specifications. </p>'''), 'limit': 80, 'context': "{'default_res_model': '%s','default_res_id': %d, 'default_company_id': %s}" % ('product.product', self.product_id.id, self.company_id.id) }
class AdjustmentLines(models.Model): _name = 'stock.valuation.adjustment.lines' _description = 'Valuation Adjustment Lines' name = fields.Char('Description', compute='_compute_name', store=True) cost_id = fields.Many2one('stock.landed.cost', 'Landed Cost', ondelete='cascade', required=True) cost_line_id = fields.Many2one('stock.landed.cost.lines', 'Cost Line', readonly=True) move_id = fields.Many2one('stock.move', 'Stock Move', readonly=True) product_id = fields.Many2one('product.product', 'Product', required=True) quantity = fields.Float('Quantity', default=1.0, digits=0, required=True) weight = fields.Float('Weight', default=1.0, digits='Stock Weight') volume = fields.Float('Volume', default=1.0, digits='Volume') former_cost = fields.Monetary('Original Value') additional_landed_cost = fields.Monetary('Additional Landed Cost') final_cost = fields.Monetary('New Value', compute='_compute_final_cost', store=True) currency_id = fields.Many2one('res.currency', related='cost_id.company_id.currency_id') @api.depends('cost_line_id.name', 'product_id.code', 'product_id.name') def _compute_name(self): for line in self: name = '%s - ' % (line.cost_line_id.name if line.cost_line_id else '') line.name = name + (line.product_id.code or line.product_id.name or '') @api.depends('former_cost', 'additional_landed_cost') def _compute_final_cost(self): for line in self: line.final_cost = line.former_cost + line.additional_landed_cost def _create_accounting_entries(self, move, qty_out): # TDE CLEANME: product chosen for computation ? cost_product = self.cost_line_id.product_id if not cost_product: return False accounts = self.product_id.product_tmpl_id.get_product_accounts() debit_account_id = accounts.get( 'stock_valuation') and accounts['stock_valuation'].id or False # If the stock move is dropshipped move we need to get the cost account instead the stock valuation account if self.move_id._is_dropshipped(): debit_account_id = accounts.get( 'expense') and accounts['expense'].id or False already_out_account_id = accounts['stock_output'].id credit_account_id = self.cost_line_id.account_id.id or cost_product.categ_id.property_stock_account_input_categ_id.id if not credit_account_id: raise UserError( _('Please configure Stock Expense Account for product: %s.') % (cost_product.name)) return self._create_account_move_line(move, credit_account_id, debit_account_id, qty_out, already_out_account_id) def _create_account_move_line(self, move, credit_account_id, debit_account_id, qty_out, already_out_account_id): """ Generate the account.move.line values to track the landed cost. Afterwards, for the goods that are already out of stock, we should create the out moves """ AccountMoveLine = [] base_line = { 'name': self.name, 'product_id': self.product_id.id, 'quantity': 0, } debit_line = dict(base_line, account_id=debit_account_id) credit_line = dict(base_line, account_id=credit_account_id) diff = self.additional_landed_cost if diff > 0: debit_line['debit'] = diff credit_line['credit'] = diff else: # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff AccountMoveLine.append([0, 0, debit_line]) AccountMoveLine.append([0, 0, credit_line]) # Create account move lines for quants already out of stock if qty_out > 0: debit_line = dict(base_line, name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=0, account_id=already_out_account_id) credit_line = dict(base_line, name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=0, account_id=debit_account_id) diff = diff * qty_out / self.quantity if diff > 0: debit_line['debit'] = diff credit_line['credit'] = diff else: # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff AccountMoveLine.append([0, 0, debit_line]) AccountMoveLine.append([0, 0, credit_line]) if self.env.company.anglo_saxon_accounting: expense_account_id = self.product_id.product_tmpl_id.get_product_accounts( )['expense'].id debit_line = dict(base_line, name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=0, account_id=expense_account_id) credit_line = dict(base_line, name=(self.name + ": " + str(qty_out) + _(' already out')), quantity=0, account_id=already_out_account_id) if diff > 0: debit_line['debit'] = diff credit_line['credit'] = diff else: # negative cost, reverse the entry debit_line['credit'] = -diff credit_line['debit'] = -diff AccountMoveLine.append([0, 0, debit_line]) AccountMoveLine.append([0, 0, credit_line]) return AccountMoveLine
class TrialBalanceReportAccount(models.TransientModel): _name = 'report_trial_balance_account' _inherit = 'account_financial_report_abstract' _order = 'sequence, code ASC, name' report_id = fields.Many2one(comodel_name='report_trial_balance', ondelete='cascade', index=True) hide_line = fields.Boolean(compute='_compute_hide_line') # Data fields, used to keep link with real object sequence = fields.Integer(index=True, default=1) level = fields.Integer(index=True, default=1) # Data fields, used to keep link with real object account_id = fields.Many2one('account.account', index=True) account_group_id = fields.Many2one('account.group', index=True) parent_id = fields.Many2one('account.group', index=True) child_account_ids = fields.Char(string="Accounts") compute_account_ids = fields.Many2many('account.account', string="Accounts", store=True) # Data fields, used for report display code = fields.Char() name = fields.Char() currency_id = fields.Many2one('res.currency') initial_balance = fields.Float(digits=(16, 2)) initial_balance_foreign_currency = fields.Float(digits=(16, 2)) debit = fields.Float(digits=(16, 2)) credit = fields.Float(digits=(16, 2)) period_balance = fields.Float(digits=(16, 2)) final_balance = fields.Float(digits=(16, 2)) final_balance_foreign_currency = fields.Float(digits=(16, 2)) # Data fields, used to browse report data partner_ids = fields.One2many(comodel_name='report_trial_balance_partner', inverse_name='report_account_id') @api.depends( 'currency_id', 'report_id', 'report_id.hide_account_at_0', 'report_id.limit_hierarchy_level', 'report_id.show_hierarchy_level', 'initial_balance', 'final_balance', 'debit', 'credit', ) def _compute_hide_line(self): for rec in self: report = rec.report_id r = (rec.currency_id or report.company_id.currency_id).rounding if report.hide_account_at_0 and ( float_is_zero(rec.initial_balance, precision_rounding=r) and float_is_zero(rec.final_balance, precision_rounding=r) and float_is_zero(rec.debit, precision_rounding=r) and float_is_zero(rec.credit, precision_rounding=r)): rec.hide_line = True elif report.limit_hierarchy_level and report.show_hierarchy_level: if report.hide_parent_hierarchy_level: distinct_level = rec.level != report.show_hierarchy_level if rec.account_group_id and distinct_level: rec.hide_line = True elif rec.level and distinct_level: rec.hide_line = True elif not report.hide_parent_hierarchy_level and \ rec.level > report.show_hierarchy_level: rec.hide_line = True
class Slide(models.Model): _name = 'slide.slide' _inherit = [ 'mail.thread', 'image.mixin', 'website.seo.metadata', 'website.published.mixin' ] _description = 'Slides' _mail_post_access = 'read' _order_by_strategy = { 'sequence': 'sequence asc, id asc', 'most_viewed': 'total_views desc', 'most_voted': 'likes desc', 'latest': 'date_published desc', } _order = 'sequence asc, is_category asc, id asc' # description name = fields.Char('Title', required=True, translate=True) active = fields.Boolean(default=True, tracking=100) sequence = fields.Integer('Sequence', default=0) user_id = fields.Many2one('res.users', string='Uploaded by', default=lambda self: self.env.uid) description = fields.Text('Description', translate=True) channel_id = fields.Many2one('slide.channel', string="Course", required=True) tag_ids = fields.Many2many('slide.tag', 'rel_slide_tag', 'slide_id', 'tag_id', string='Tags') is_preview = fields.Boolean( 'Allow Preview', default=False, help= "The course is accessible by anyone : the users don't need to join the channel to access the content of the course." ) is_new_slide = fields.Boolean('Is New Slide', compute='_compute_is_new_slide') completion_time = fields.Float( 'Duration', digits=(10, 4), help="The estimated completion time for this slide") # Categories is_category = fields.Boolean('Is a category', default=False) category_id = fields.Many2one('slide.slide', string="Section", compute="_compute_category_id", store=True) slide_ids = fields.One2many('slide.slide', "category_id", string="Slides") # subscribers partner_ids = fields.Many2many( 'res.partner', 'slide_slide_partner', 'slide_id', 'partner_id', string='Subscribers', groups='website_slides.group_website_slides_officer', copy=False) slide_partner_ids = fields.One2many( 'slide.slide.partner', 'slide_id', string='Subscribers information', groups='website_slides.group_website_slides_officer', copy=False) user_membership_id = fields.Many2one( 'slide.slide.partner', string="Subscriber information", compute='_compute_user_membership_id', compute_sudo=False, help="Subscriber information for the current logged in user") # Quiz related fields question_ids = fields.One2many("slide.question", "slide_id", string="Questions") questions_count = fields.Integer(string="Numbers of Questions", compute='_compute_questions_count') quiz_first_attempt_reward = fields.Integer("Reward: first attempt", default=10) quiz_second_attempt_reward = fields.Integer("Reward: second attempt", default=7) quiz_third_attempt_reward = fields.Integer( "Reward: third attempt", default=5, ) quiz_fourth_attempt_reward = fields.Integer( "Reward: every attempt after the third try", default=2) # content slide_type = fields.Selection( [('infographic', 'Infographic'), ('webpage', 'Web Page'), ('presentation', 'Presentation'), ('document', 'Document'), ('video', 'Video'), ('quiz', "Quiz")], string='Type', required=True, default='document', help= "The document type will be set automatically based on the document URL and properties (e.g. height and width for presentation and document)." ) datas = fields.Binary('Content', attachment=True) url = fields.Char('Document URL', help="Youtube or Google Document URL") document_id = fields.Char('Document ID', help="Youtube or Google Document ID") link_ids = fields.One2many('slide.slide.link', 'slide_id', string="External URL for this slide") slide_resource_ids = fields.One2many( 'slide.slide.resource', 'slide_id', string="Additional Resource for this slide") slide_resource_downloadable = fields.Boolean( 'Allow Download', default=True, help="Allow the user to download the content of the slide.") mime_type = fields.Char('Mime-type') html_content = fields.Html( "HTML Content", help="Custom HTML content for slides of type 'Web Page'.", translate=True, sanitize_form=False) # website website_id = fields.Many2one(related='channel_id.website_id', readonly=True) date_published = fields.Datetime('Publish Date', readonly=True, tracking=1) likes = fields.Integer('Likes', compute='_compute_user_info', store=True, compute_sudo=False) dislikes = fields.Integer('Dislikes', compute='_compute_user_info', store=True, compute_sudo=False) user_vote = fields.Integer('User vote', compute='_compute_user_info', compute_sudo=False) embed_code = fields.Text('Embed Code', readonly=True, compute='_compute_embed_code') # views embedcount_ids = fields.One2many('slide.embed', 'slide_id', string="Embed Count") slide_views = fields.Integer('# of Website Views', store=True, compute="_compute_slide_views") public_views = fields.Integer('# of Public Views', copy=False) total_views = fields.Integer("Views", default="0", compute='_compute_total', store=True) # comments comments_count = fields.Integer('Number of comments', compute="_compute_comments_count") # channel channel_type = fields.Selection(related="channel_id.channel_type", string="Channel type") channel_allow_comment = fields.Boolean(related="channel_id.allow_comment", string="Allows comment") # Statistics in case the slide is a category nbr_presentation = fields.Integer("Number of Presentations", compute='_compute_slides_statistics', store=True) nbr_document = fields.Integer("Number of Documents", compute='_compute_slides_statistics', store=True) nbr_video = fields.Integer("Number of Videos", compute='_compute_slides_statistics', store=True) nbr_infographic = fields.Integer("Number of Infographics", compute='_compute_slides_statistics', store=True) nbr_webpage = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True) nbr_quiz = fields.Integer("Number of Quizs", compute="_compute_slides_statistics", store=True) total_slides = fields.Integer(compute='_compute_slides_statistics', store=True) _sql_constraints = [( 'exclusion_html_content_and_url', "CHECK(html_content IS NULL OR url IS NULL)", "A slide is either filled with a document url or HTML content. Not both." )] @api.depends('date_published', 'is_published') def _compute_is_new_slide(self): for slide in self: slide.is_new_slide = slide.date_published > fields.Datetime.now( ) - relativedelta(days=7) if slide.is_published else False @api.depends('channel_id.slide_ids.is_category', 'channel_id.slide_ids.sequence') def _compute_category_id(self): """ Will take all the slides of the channel for which the index is higher than the index of this category and lower than the index of the next category. Lists are manually sorted because when adding a new browse record order will not be correct as the added slide would actually end up at the first place no matter its sequence.""" self.category_id = False # initialize whatever the state channel_slides = {} for slide in self: if slide.channel_id.id not in channel_slides: channel_slides[ slide.channel_id.id] = slide.channel_id.slide_ids for cid, slides in channel_slides.items(): current_category = self.env['slide.slide'] slide_list = list(slides) slide_list.sort(key=lambda s: (s.sequence, not s.is_category)) for slide in slide_list: if slide.is_category: current_category = slide elif slide.category_id != current_category: slide.category_id = current_category.id @api.depends('question_ids') def _compute_questions_count(self): for slide in self: slide.questions_count = len(slide.question_ids) @api.depends('website_message_ids.res_id', 'website_message_ids.model', 'website_message_ids.message_type') def _compute_comments_count(self): for slide in self: slide.comments_count = len(slide.website_message_ids) @api.depends('slide_views', 'public_views') def _compute_total(self): for record in self: record.total_views = record.slide_views + record.public_views @api.depends('slide_partner_ids.vote') @api.depends_context('uid') def _compute_user_info(self): default_stats = {'likes': 0, 'dislikes': 0, 'user_vote': False} if not self.ids: self.update(default_stats) return slide_data = dict.fromkeys(self.ids, default_stats) slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids) ]) for slide_partner in slide_partners: if slide_partner.vote == 1: slide_data[slide_partner.slide_id.id]['likes'] += 1 if slide_partner.partner_id == self.env.user.partner_id: slide_data[slide_partner.slide_id.id]['user_vote'] = 1 elif slide_partner.vote == -1: slide_data[slide_partner.slide_id.id]['dislikes'] += 1 if slide_partner.partner_id == self.env.user.partner_id: slide_data[slide_partner.slide_id.id]['user_vote'] = -1 for slide in self: slide.update(slide_data[slide.id]) @api.depends('slide_partner_ids.slide_id') def _compute_slide_views(self): # TODO awa: tried compute_sudo, for some reason it doesn't work in here... read_group_res = self.env['slide.slide.partner'].sudo().read_group( [('slide_id', 'in', self.ids)], ['slide_id'], groupby=['slide_id']) mapped_data = dict((res['slide_id'][0], res['slide_id_count']) for res in read_group_res) for slide in self: slide.slide_views = mapped_data.get(slide.id, 0) @api.depends('slide_ids.sequence', 'slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.is_category') def _compute_slides_statistics(self): # Do not use dict.fromkeys(self.ids, dict()) otherwise it will use the same dictionnary for all keys. # Therefore, when updating the dict of one key, it updates the dict of all keys. keys = [ 'nbr_%s' % slide_type for slide_type in self.env['slide.slide']._fields['slide_type'].get_values(self.env) ] default_vals = dict((key, 0) for key in keys + ['total_slides']) res = self.env['slide.slide'].read_group( [('is_published', '=', True), ('category_id', 'in', self.ids), ('is_category', '=', False)], ['category_id', 'slide_type'], ['category_id', 'slide_type'], lazy=False) type_stats = self._compute_slides_statistics_type(res) for record in self: record.update(type_stats.get(record._origin.id, default_vals)) def _compute_slides_statistics_type(self, read_group_res): """ Compute statistics based on all existing slide types """ slide_types = self.env['slide.slide']._fields['slide_type'].get_values( self.env) keys = ['nbr_%s' % slide_type for slide_type in slide_types] result = dict((cid, dict((key, 0) for key in keys + ['total_slides'])) for cid in self.ids) for res_group in read_group_res: cid = res_group['category_id'][0] slide_type = res_group.get('slide_type') if slide_type: slide_type_count = res_group.get('__count', 0) result[cid]['nbr_%s' % slide_type] = slide_type_count result[cid]['total_slides'] += slide_type_count return result @api.depends('slide_partner_ids.partner_id') @api.depends('uid') def _compute_user_membership_id(self): slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id), ]) for record in self: record.user_membership_id = next( (slide_partner for slide_partner in slide_partners if slide_partner.slide_id == record), self.env['slide.slide.partner']) @api.depends('document_id', 'slide_type', 'mime_type') def _compute_embed_code(self): base_url = request and request.httprequest.url_root or self.env[ 'ir.config_parameter'].sudo().get_param('web.base.url') if base_url[-1] == '/': base_url = base_url[:-1] for record in self: if record.datas and (not record.document_id or record.slide_type in ['document', 'presentation']): slide_url = base_url + url_for( '/slides/embed/%s?page=1' % record.id) record.embed_code = '<iframe src="%s" class="o_wslides_iframe_viewer" allowFullScreen="true" height="%s" width="%s" frameborder="0"></iframe>' % ( slide_url, 315, 420) elif record.slide_type == 'video' and record.document_id: if not record.mime_type: # embed youtube video query = urls.url_parse(record.url).query query = query + '&theme=light' if query else 'theme=light' record.embed_code = '<iframe src="//www.youtube-nocookie.com/embed/%s?%s" allowFullScreen="true" frameborder="0"></iframe>' % ( record.document_id, query) else: # embed google doc video record.embed_code = '<iframe src="//drive.google.com/file/d/%s/preview" allowFullScreen="true" frameborder="0"></iframe>' % ( record.document_id) else: record.embed_code = False @api.onchange('url') def _on_change_url(self): self.ensure_one() if self.url: res = self._parse_document_url(self.url) if res.get('error'): raise Warning(res.get('error')) values = res['values'] if not values.get('document_id'): raise Warning( _('Please enter valid Youtube or Google Doc URL')) for key, value in values.items(): self[key] = value @api.onchange('datas') def _on_change_datas(self): """ For PDFs, we assume that it takes 5 minutes to read a page. If the selected file is not a PDF, it is an image (You can only upload PDF or Image file) then the slide_type is changed into infographic and the uploaded dataS is transfered to the image field. (It avoids the infinite loading in PDF viewer)""" if self.datas: data = base64.b64decode(self.datas) if data.startswith(b'%PDF-'): pdf = PyPDF2.PdfFileReader(io.BytesIO(data), overwriteWarnings=False) self.completion_time = (5 * len(pdf.pages)) / 60 else: self.slide_type = 'infographic' self.image_1920 = self.datas self.datas = None @api.depends('name', 'channel_id.website_id.domain') def _compute_website_url(self): # TDE FIXME: clena this link.tracker strange stuff super(Slide, self)._compute_website_url() for slide in self: if slide.id: # avoid to perform a slug on a not yet saved record in case of an onchange. base_url = slide.channel_id.get_base_url() # link_tracker is not in dependencies, so use it to shorten url only if installed. if self.env.registry.get('link.tracker'): url = self.env['link.tracker'].sudo().create({ 'url': '%s/slides/slide/%s' % (base_url, slug(slide)), 'title': slide.name, }).short_url else: url = '%s/slides/slide/%s' % (base_url, slug(slide)) slide.website_url = url @api.depends('channel_id.can_publish') def _compute_can_publish(self): for record in self: record.can_publish = record.channel_id.can_publish @api.model def _get_can_publish_error_message(self): return _( "Publishing is restricted to the responsible of training courses or members of the publisher group for documentation courses" ) # --------------------------------------------------------- # ORM Overrides # --------------------------------------------------------- @api.model def create(self, values): # Do not publish slide if user has not publisher rights channel = self.env['slide.channel'].browse(values['channel_id']) if not channel.can_publish: # 'website_published' is handled by mixin values['date_published'] = False if values.get('slide_type' ) == 'infographic' and not values.get('image_1920'): values['image_1920'] = values['datas'] if values.get('is_category'): values['is_preview'] = True values['is_published'] = True if values.get('is_published') and not values.get('date_published'): values['date_published'] = datetime.datetime.now() if values.get('url') and not values.get('document_id'): doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.items(): values.setdefault(key, value) slide = super(Slide, self).create(values) if slide.is_published and not slide.is_category: slide._post_publication() return slide def write(self, values): if values.get('url') and values['url'] != self.url: doc_data = self._parse_document_url(values['url']).get( 'values', dict()) for key, value in doc_data.items(): values.setdefault(key, value) if values.get('is_category'): values['is_preview'] = True values['is_published'] = True res = super(Slide, self).write(values) if values.get('is_published'): self.date_published = datetime.datetime.now() self._post_publication() if 'is_published' in values or 'active' in values: # if the slide is published/unpublished, recompute the completion for the partners self.slide_partner_ids._set_completed_callback() return res @api.returns('self', lambda value: value.id) def copy(self, default=None): """Sets the sequence to zero so that it always lands at the beginning of the newly selected course as an uncategorized slide""" rec = super(Slide, self).copy(default) rec.sequence = 0 return rec def unlink(self): if self.question_ids and self.channel_id.channel_partner_ids: raise UserError( _("People already took this quiz. To keep course progression it should not be deleted." )) for category in self.filtered(lambda slide: slide.is_category): category.channel_id._move_category_slides(category, False) super(Slide, self).unlink() def toggle_active(self): # archiving/unarchiving a channel does it on its slides, too to_archive = self.filtered(lambda slide: slide.active) res = super(Slide, self).toggle_active() if to_archive: to_archive.filtered( lambda slide: not slide.is_category).is_published = False return res # --------------------------------------------------------- # Mail/Rating # --------------------------------------------------------- @api.returns('mail.message', lambda value: value.id) def message_post(self, *, message_type='notification', **kwargs): self.ensure_one() if message_type == 'comment' and not self.channel_id.can_comment: # user comments have a restriction on karma raise AccessError(_('Not enough karma to comment')) return super(Slide, self).message_post(message_type=message_type, **kwargs) def get_access_action(self, access_uid=None): """ Instead of the classic form view, redirect to website if it is published. """ self.ensure_one() if self.website_published: return { 'type': 'ir.actions.act_url', 'url': '%s' % self.website_url, 'target': 'self', 'target_type': 'public', 'res_id': self.id, } return super(Slide, self).get_access_action(access_uid) def _notify_get_groups(self, msg_vals=None): """ Add access button to everyone if the document is active. """ groups = super(Slide, self)._notify_get_groups(msg_vals=msg_vals) if self.website_published: for group_name, group_method, group_data in groups: group_data['has_button_access'] = True return groups # --------------------------------------------------------- # Business Methods # --------------------------------------------------------- def _post_publication(self): base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for slide in self.filtered(lambda slide: slide.website_published and slide.channel_id.publish_template_id): publish_template = slide.channel_id.publish_template_id html_body = publish_template.with_context( base_url=base_url)._render_field('body_html', slide.ids)[slide.id] subject = publish_template._render_field('subject', slide.ids)[slide.id] # We want to use the 'reply_to' of the template if set. However, `mail.message` will check # if the key 'reply_to' is in the kwargs before calling _get_reply_to. If the value is # falsy, we don't include it in the 'message_post' call. kwargs = {} reply_to = publish_template._render_field('reply_to', slide.ids)[slide.id] if reply_to: kwargs['reply_to'] = reply_to slide.channel_id.with_context( mail_create_nosubscribe=True).message_post( subject=subject, body=html_body, subtype_xmlid='website_slides.mt_channel_slide_published', email_layout_xmlid='mail.mail_notification_light', **kwargs, ) return True def _generate_signed_token(self, partner_id): """ Lazy generate the acces_token and return it signed by the given partner_id :rtype tuple (string, int) :return (signed_token, partner_id) """ if not self.access_token: self.write({'access_token': self._default_access_token()}) return self._sign_token(partner_id) def _send_share_email(self, email, fullscreen): # TDE FIXME: template to check mail_ids = [] base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for record in self: template = record.channel_id.share_template_id.with_context( user=self.env.user, email=email, base_url=base_url, fullscreen=fullscreen) email_values = {'email_to': email} if self.env.user.has_group('base.group_portal'): template = template.sudo() email_values[ 'email_from'] = self.env.company.catchall_formatted or self.env.company.email_formatted mail_ids.append( template.send_mail(record.id, notif_layout='mail.mail_notification_light', email_values=email_values)) return mail_ids def action_like(self): self.check_access_rights('read') self.check_access_rule('read') return self._action_vote(upvote=True) def action_dislike(self): self.check_access_rights('read') self.check_access_rule('read') return self._action_vote(upvote=False) def _action_vote(self, upvote=True): """ Private implementation of voting. It does not check for any real access rights; public methods should grant access before calling this method. :param upvote: if True, is a like; if False, is a dislike """ self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() slide_partners = SlidePartnerSudo.search([ ('slide_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id) ]) slide_id = slide_partners.mapped('slide_id') new_slides = self_sudo - slide_id channel = slide_id.channel_id karma_to_add = 0 for slide_partner in slide_partners: if upvote: new_vote = 0 if slide_partner.vote == -1 else 1 if slide_partner.vote != 1: karma_to_add += channel.karma_gen_slide_vote else: new_vote = 0 if slide_partner.vote == 1 else -1 if slide_partner.vote != -1: karma_to_add -= channel.karma_gen_slide_vote slide_partner.vote = new_vote for new_slide in new_slides: new_vote = 1 if upvote else -1 new_slide.write({ 'slide_partner_ids': [(0, 0, { 'vote': new_vote, 'partner_id': self.env.user.partner_id.id })] }) karma_to_add += new_slide.channel_id.karma_gen_slide_vote * ( 1 if upvote else -1) if karma_to_add: self.env.user.add_karma(karma_to_add) def action_set_viewed(self, quiz_attempts_inc=False): if any(not slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide as viewed if you are not among its members.' )) return bool( self._action_set_viewed(self.env.user.partner_id, quiz_attempts_inc=quiz_attempts_inc)) def _action_set_viewed(self, target_partner, quiz_attempts_inc=False): self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id)]) if quiz_attempts_inc and existing_sudo: sql.increment_field_skiplock(existing_sudo, 'quiz_attempts_count') SlidePartnerSudo.invalidate_cache(fnames=['quiz_attempts_count'], ids=existing_sudo.ids) new_slides = self_sudo - existing_sudo.mapped('slide_id') return SlidePartnerSudo.create([{ 'slide_id': new_slide.id, 'channel_id': new_slide.channel_id.id, 'partner_id': target_partner.id, 'quiz_attempts_count': 1 if quiz_attempts_inc else 0, 'vote': 0 } for new_slide in new_slides]) def action_set_completed(self): if any(not slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide as completed if you are not among its members.' )) return self._action_set_completed(self.env.user.partner_id) def _action_set_completed(self, target_partner): self_sudo = self.sudo() SlidePartnerSudo = self.env['slide.slide.partner'].sudo() existing_sudo = SlidePartnerSudo.search([('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id)]) existing_sudo.write({'completed': True}) new_slides = self_sudo - existing_sudo.mapped('slide_id') SlidePartnerSudo.create([{ 'slide_id': new_slide.id, 'channel_id': new_slide.channel_id.id, 'partner_id': target_partner.id, 'vote': 0, 'completed': True } for new_slide in new_slides]) return True def _action_set_quiz_done(self): if any(not slide.channel_id.is_member for slide in self): raise UserError( _('You cannot mark a slide quiz as completed if you are not among its members.' )) points = 0 for slide in self: user_membership_sudo = slide.user_membership_id.sudo() if not user_membership_sudo or user_membership_sudo.completed or not user_membership_sudo.quiz_attempts_count: continue gains = [ slide.quiz_first_attempt_reward, slide.quiz_second_attempt_reward, slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward ] points += gains[ user_membership_sudo.quiz_attempts_count - 1] if user_membership_sudo.quiz_attempts_count <= len( gains) else gains[-1] return self.env.user.sudo().add_karma(points) def _compute_quiz_info(self, target_partner, quiz_done=False): result = dict.fromkeys(self.ids, False) slide_partners = self.env['slide.slide.partner'].sudo().search([ ('slide_id', 'in', self.ids), ('partner_id', '=', target_partner.id) ]) slide_partners_map = dict( (sp.slide_id.id, sp) for sp in slide_partners) for slide in self: if not slide.question_ids: gains = [0] else: gains = [ slide.quiz_first_attempt_reward, slide.quiz_second_attempt_reward, slide.quiz_third_attempt_reward, slide.quiz_fourth_attempt_reward ] result[slide.id] = { 'quiz_karma_max': gains[0], # what could be gained if succeed at first try 'quiz_karma_gain': gains[0], # what would be gained at next test 'quiz_karma_won': 0, # what has been gained 'quiz_attempts_count': 0, # number of attempts } slide_partner = slide_partners_map.get(slide.id) if slide.question_ids and slide_partner: if slide_partner.quiz_attempts_count: result[slide.id]['quiz_karma_gain'] = gains[ slide_partner. quiz_attempts_count] if slide_partner.quiz_attempts_count < len( gains) else gains[-1] result[slide.id][ 'quiz_attempts_count'] = slide_partner.quiz_attempts_count if quiz_done or slide_partner.completed: result[slide.id]['quiz_karma_won'] = gains[ slide_partner.quiz_attempts_count - 1] if slide_partner.quiz_attempts_count < len( gains) else gains[-1] return result # -------------------------------------------------- # Parsing methods # -------------------------------------------------- @api.model def _fetch_data(self, base_url, params, content_type=False): result = {'values': dict()} try: response = requests.get(base_url, timeout=3, params=params) response.raise_for_status() if content_type == 'json': result['values'] = response.json() elif content_type in ('image', 'pdf'): result['values'] = base64.b64encode(response.content) else: result['values'] = response.content except requests.exceptions.HTTPError as e: result['error'] = e.response.content except requests.exceptions.ConnectionError as e: result['error'] = str(e) return result def _find_document_data_from_url(self, url): url_obj = urls.url_parse(url) if url_obj.ascii_host == 'youtu.be': return ('youtube', url_obj.path[1:] if url_obj.path else False) elif url_obj.ascii_host in ('youtube.com', 'www.youtube.com', 'm.youtube.com', 'www.youtube-nocookie.com'): v_query_value = url_obj.decode_query().get('v') if v_query_value: return ('youtube', v_query_value) split_path = url_obj.path.split('/') if len(split_path) >= 3 and split_path[1] in ('v', 'embed'): return ('youtube', split_path[2]) expr = re.compile( r'(^https:\/\/docs.google.com|^https:\/\/drive.google.com).*\/d\/([^\/]*)' ) arg = expr.match(url) document_id = arg and arg.group(2) or False if document_id: return ('google', document_id) return (None, False) def _parse_document_url(self, url, only_preview_fields=False): document_source, document_id = self._find_document_data_from_url(url) if document_source and hasattr(self, '_parse_%s_document' % document_source): return getattr(self, '_parse_%s_document' % document_source)( document_id, only_preview_fields) return {'error': _('Unknown document')} def _parse_youtube_document(self, document_id, only_preview_fields): """ If we receive a duration (YT video), we use it to determine the slide duration. The received duration is under a special format (e.g: PT1M21S15, meaning 1h 21m 15s). """ key = self.env['website'].get_current_website( ).website_slide_google_app_key fetch_res = self._fetch_data( 'https://www.googleapis.com/youtube/v3/videos', { 'id': document_id, 'key': key, 'part': 'snippet,contentDetails', 'fields': 'items(id,snippet,contentDetails)' }, 'json') if fetch_res.get('error'): return { 'error': self._extract_google_error_message(fetch_res.get('error')) } values = {'slide_type': 'video', 'document_id': document_id} items = fetch_res['values'].get('items') if not items: return {'error': _('Please enter valid Youtube or Google Doc URL')} youtube_values = items[0] youtube_duration = youtube_values.get('contentDetails', {}).get('duration') if youtube_duration: parsed_duration = re.search( r'^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$', youtube_duration) values['completion_time'] = (int(parsed_duration.group(1) or 0)) + \ (int(parsed_duration.group(2) or 0) / 60) + \ (int(parsed_duration.group(3) or 0) / 3600) if youtube_values.get('snippet'): snippet = youtube_values['snippet'] if only_preview_fields: values.update({ 'url_src': snippet['thumbnails']['high']['url'], 'title': snippet['title'], 'description': snippet['description'] }) return values values.update({ 'name': snippet['title'], 'image_1920': self._fetch_data(snippet['thumbnails']['high']['url'], {}, 'image')['values'], 'description': snippet['description'], 'mime_type': False, }) return {'values': values} def _extract_google_error_message(self, error): """ See here for Google error format https://developers.google.com/drive/api/v3/handle-errors """ try: error = json.loads(error) error = (error.get('error', {}).get('errors', []) or [{}])[0].get('reason') except json.decoder.JSONDecodeError: error = str(error) if error == 'keyInvalid': return _( 'Your Google API key is invalid, please update it in your settings.\nSettings > Website > Features > API Key' ) return _( 'Could not fetch data from url. Document or access right not available:\n%s', error) @api.model def _parse_google_document(self, document_id, only_preview_fields): def get_slide_type(vals): # TDE FIXME: WTF ?? slide_type = 'presentation' if vals.get('image_1920'): image = Image.open( io.BytesIO(base64.b64decode(vals['image_1920']))) width, height = image.size if height > width: return 'document' return slide_type # Google drive doesn't use a simple API key to access the data, but requires an access # token. However, this token is generated in module google_drive, which is not in the # dependencies of website_slides. We still keep the 'key' parameter just in case, but that # is probably useless. params = {} params['projection'] = 'BASIC' if 'google.drive.config' in self.env: access_token = self.env['google.drive.config'].get_access_token() if access_token: params['access_token'] = access_token if not params.get('access_token'): params['key'] = self.env['website'].get_current_website( ).website_slide_google_app_key fetch_res = self._fetch_data( 'https://www.googleapis.com/drive/v2/files/%s' % document_id, params, "json") if fetch_res.get('error'): return { 'error': self._extract_google_error_message(fetch_res.get('error')) } google_values = fetch_res['values'] if only_preview_fields: return { 'url_src': google_values['thumbnailLink'], 'title': google_values['title'], } values = { 'name': google_values['title'], 'image_1920': self._fetch_data( google_values['thumbnailLink'].replace('=s220', ''), {}, 'image')['values'], 'mime_type': google_values['mimeType'], 'document_id': document_id, } if google_values['mimeType'].startswith('video/'): values['slide_type'] = 'video' elif google_values['mimeType'].startswith('image/'): values['datas'] = values['image_1920'] values['slide_type'] = 'infographic' elif google_values['mimeType'].startswith( 'application/vnd.google-apps'): values['slide_type'] = get_slide_type(values) if 'exportLinks' in google_values: values['datas'] = self._fetch_data( google_values['exportLinks']['application/pdf'], params, 'pdf')['values'] elif google_values['mimeType'] == 'application/pdf': # TODO: Google Drive PDF document doesn't provide plain text transcript values['datas'] = self._fetch_data(google_values['webContentLink'], {}, 'pdf')['values'] values['slide_type'] = get_slide_type(values) return {'values': values} def _default_website_meta(self): res = super(Slide, self)._default_website_meta() res['default_opengraph']['og:title'] = res['default_twitter'][ 'twitter:title'] = self.name res['default_opengraph']['og:description'] = res['default_twitter'][ 'twitter:description'] = self.description res['default_opengraph']['og:image'] = res['default_twitter'][ 'twitter:image'] = self.env['website'].image_url( self, 'image_1024') res['default_meta_description'] = self.description return res # --------------------------------------------------------- # Data / Misc # --------------------------------------------------------- def get_backend_menu_id(self): return self.env.ref('website_slides.website_slides_menu_root').id
class StockMoveLine(models.Model): _name = "stock.move.line" _description = "Packing Operation" _order = "result_package_id desc, id" picking_id = fields.Many2one( 'stock.picking', 'Stock Picking', help='The stock operation where the packing has been made') move_id = fields.Many2one('stock.move', 'Stock Move', help="Change to a better name") product_id = fields.Many2one('product.product', 'Product', ondelete="cascade") product_uom_id = fields.Many2one('product.uom', 'Unit of Measure', required=True) product_qty = fields.Float('Real Reserved Quantity', digits=0, compute='_compute_product_qty', inverse='_set_product_qty', store=True) product_uom_qty = fields.Float( 'Reserved', default=0.0, digits=dp.get_precision('Product Unit of Measure'), required=True) ordered_qty = fields.Float( 'Ordered Quantity', digits=dp.get_precision('Product Unit of Measure')) qty_done = fields.Float('Done', default=0.0, digits=dp.get_precision('Product Unit of Measure'), copy=False) package_id = fields.Many2one('stock.quant.package', 'Source Package', ondelete='restrict') lot_id = fields.Many2one('stock.production.lot', 'Lot') lot_name = fields.Char('Lot/Serial Number') result_package_id = fields.Many2one( 'stock.quant.package', 'Destination Package', ondelete='restrict', required=False, help="If set, the operations are packed into this package") date = fields.Datetime('Date', default=fields.Datetime.now, required=True) owner_id = fields.Many2one('res.partner', 'Owner', help="Owner of the quants") location_id = fields.Many2one('stock.location', 'From', required=True) location_dest_id = fields.Many2one('stock.location', 'To', required=True) from_loc = fields.Char(compute='_compute_location_description') to_loc = fields.Char(compute='_compute_location_description') lots_visible = fields.Boolean(compute='_compute_lots_visible') state = fields.Selection(related='move_id.state', store=True) is_initial_demand_editable = fields.Boolean( related='move_id.is_initial_demand_editable') is_locked = fields.Boolean(related='move_id.is_locked', default=True, readonly=True) consume_line_ids = fields.Many2many( 'stock.move.line', 'stock_move_line_consume_rel', 'consume_line_id', 'produce_line_id', help="Technical link to see who consumed what. ") produce_line_ids = fields.Many2many( 'stock.move.line', 'stock_move_line_consume_rel', 'produce_line_id', 'consume_line_id', help="Technical link to see which line was produced with this. ") reference = fields.Char(related='move_id.reference', store=True) @api.one def _compute_location_description(self): for operation, operation_sudo in izip(self, self.sudo()): operation.from_loc = '%s%s' % ( operation_sudo.location_id.name, operation.product_id and operation_sudo.package_id.name or '') operation.to_loc = '%s%s' % (operation_sudo.location_dest_id.name, operation_sudo.result_package_id.name or '') @api.one @api.depends('picking_id.picking_type_id', 'product_id.tracking') def _compute_lots_visible(self): picking = self.picking_id if picking.picking_type_id and self.product_id.tracking != 'none': # TDE FIXME: not sure correctly migrated self.lots_visible = picking.picking_type_id.use_existing_lots or picking.picking_type_id.use_create_lots else: self.lots_visible = self.product_id.tracking != 'none' @api.one @api.depends('product_id', 'product_uom_id', 'product_uom_qty') def _compute_product_qty(self): self.product_qty = self.product_uom_id._compute_quantity( self.product_uom_qty, self.product_id.uom_id, rounding_method='HALF-UP') @api.one def _set_product_qty(self): """ The meaning of product_qty field changed lately and is now a functional field computing the quantity in the default product UoM. This code has been added to raise an error if a write is made given a value for `product_qty`, where the same write should set the `product_uom_qty` field instead, in order to detect errors. """ raise UserError( _('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.' )) @api.constrains('product_uom_qty') def check_reserved_done_quantity(self): for move_line in self: if move_line.state == 'done' and not float_is_zero( move_line.product_uom_qty, precision_rounding=self.env['decimal.precision']. precision_get('Product Unit of Measure')): raise ValidationError( _('A done move line should never have a reserved quantity.' )) @api.onchange('product_id', 'product_uom_id') def onchange_product_id(self): if self.product_id: self.lots_visible = self.product_id.tracking != 'none' if not self.product_uom_id or self.product_uom_id.category_id != self.product_id.uom_id.category_id: if self.move_id.product_uom: self.product_uom_id = self.move_id.product_uom.id else: self.product_uom_id = self.product_id.uom_id.id res = { 'domain': { 'product_uom_id': [('category_id', '=', self.product_uom_id.category_id.id)] } } else: res = {'domain': {'product_uom_id': []}} return res @api.onchange('lot_name', 'lot_id') def onchange_serial_number(self): res = {} if self.product_id.tracking == 'serial': self.qty_done = 1 move_lines_to_check = self._get_similar_move_lines() - self message = move_lines_to_check._check_for_duplicated_serial_numbers( ) if message: res['warning'] = {'title': _('Warning'), 'message': message} return res @api.constrains('qty_done') def _check_positive_qty_done(self): if any([ml.qty_done < 0 for ml in self]): raise ValidationError(_('You can not enter negative quantities!')) @api.constrains('lot_id', 'lot_name', 'qty_done') def _check_unique_serial_number(self): for ml in self.filtered(lambda ml: ml.move_id.product_id.tracking == 'serial' and (ml.lot_id or ml.lot_name)): move_lines_to_check = ml._get_similar_move_lines() message = move_lines_to_check._check_for_duplicated_serial_numbers( ) if message: raise ValidationError(message) if float_compare(ml.qty_done, 1.0, precision_rounding=ml.move_id.product_id.uom_id. rounding) == 1: raise UserError( _('You can only process 1.0 %s for products with unique serial number.' ) % ml.product_id.uom_id.name) if ml.lot_name: already_exist = self.env['stock.production.lot'].search([ ('name', '=', ml.lot_name), ('product_id', '=', ml.product_id.id) ]) if already_exist: return _( 'You have already assigned this serial number to this product. Please correct the serial numbers encoded.' ) def _get_similar_move_lines(self): self.ensure_one() lines = self.env['stock.move.line'] picking_id = self.move_id.picking_id if self.move_id else self.picking_id if picking_id: lines |= picking_id.move_line_ids.filtered( lambda ml: ml.product_id == self.product_id and (ml.lot_id or ml.lot_name)) return lines def _check_for_duplicated_serial_numbers(self): """ This method is used in _check_unique_serial_number and in onchange_serial_number to check that a same serial number is not used twice amongst the recordset passed. :return: an error message directed to the user if needed else False """ if self.mapped('lot_id'): lots_map = [(ml.product_id.id, ml.lot_id.name) for ml in self] recorded_serials_counter = Counter(lots_map) for (product_id, lot_id), occurrences in recorded_serials_counter.items(): if occurrences > 1 and lot_id is not False: return _( 'You cannot use the same serial number twice. Please correct the serial numbers encoded.' ) elif self.mapped('lot_name'): lots_map = [(ml.product_id.id, ml.lot_name) for ml in self] recorded_serials_counter = Counter(lots_map) for (product_id, lot_id), occurrences in recorded_serials_counter.items(): if occurrences > 1 and lot_id is not False: return _( 'You cannot use the same serial number twice. Please correct the serial numbers encoded.' ) return False @api.model def create(self, vals): vals['ordered_qty'] = vals.get('product_uom_qty') # If the move line is directly create on the picking view. # If this picking is already done we should generate an # associated done move. if 'picking_id' in vals and 'move_id' not in vals: picking = self.env['stock.picking'].browse(vals['picking_id']) if picking.state == 'done': product = self.env['product.product'].browse( vals['product_id']) new_move = self.env['stock.move'].create({ 'name': _('New Move:') + product.display_name, 'product_id': product.id, 'product_uom_qty': 'qty_done' in vals and vals['qty_done'] or 0, 'product_uom': vals['product_uom_id'], 'location_id': 'location_id' in vals and vals['location_id'] or picking.location_id.id, 'location_dest_id': 'location_dest_id' in vals and vals['location_dest_id'] or picking.location_dest_id.id, 'state': 'done', 'additional': True, 'picking_id': picking.id, }) vals['move_id'] = new_move.id ml = super(StockMoveLine, self).create(vals) if ml.state == 'done': if ml.product_id.type == 'product': Quant = self.env['stock.quant'] quantity = ml.product_uom_id._compute_quantity( ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') in_date = None available_qty, in_date = Quant._update_available_quantity( ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if available_qty < 0 and ml.lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity( ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) if untracked_qty: taken_from_untracked_qty = min(untracked_qty, abs(quantity)) Quant._update_available_quantity( ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity( ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity( ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date) next_moves = ml.move_id.move_dest_ids.filtered( lambda move: move.state not in ('done', 'cancel')) next_moves._do_unreserve() next_moves._action_assign() return ml def write(self, vals): """ Through the interface, we allow users to change the charateristics of a move line. If a quantity has been reserved for this move line, we impact the reservation directly to free the old quants and allocate the new ones. """ if self.env.context.get('bypass_reservation_update'): return super(StockMoveLine, self).write(vals) Quant = self.env['stock.quant'] precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') # We forbid to change the reserved quantity in the interace, but it is needed in the # case of stock.move's split. # TODO Move me in the update if 'product_uom_qty' in vals: for ml in self.filtered(lambda m: m.state in ('partially_available', 'assigned' ) and m.product_id.type == 'product'): if not ml.location_id.should_bypass_reservation(): qty_to_decrease = ml.product_qty - ml.product_uom_id._compute_quantity( vals['product_uom_qty'], ml.product_id.uom_id, rounding_method='HALF-UP') try: Quant._update_reserved_quantity( ml.product_id, ml.location_id, -qty_to_decrease, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) except UserError: if ml.lot_id: Quant._update_reserved_quantity( ml.product_id, ml.location_id, -qty_to_decrease, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) else: raise triggers = [('location_id', 'stock.location'), ('location_dest_id', 'stock.location'), ('lot_id', 'stock.production.lot'), ('package_id', 'stock.quant.package'), ('result_package_id', 'stock.quant.package'), ('owner_id', 'res.partner')] updates = {} for key, model in triggers: if key in vals: updates[key] = self.env[model].browse(vals[key]) if updates: for ml in self.filtered( lambda ml: ml.state in ['partially_available', 'assigned'] and ml.product_id.type == 'product'): if not ml.location_id.should_bypass_reservation(): try: Quant._update_reserved_quantity( ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) except UserError: if ml.lot_id: Quant._update_reserved_quantity( ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) else: raise if not updates.get('location_id', ml.location_id).should_bypass_reservation(): new_product_qty = 0 try: q = Quant._update_reserved_quantity( ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=updates.get('lot_id', ml.lot_id), package_id=updates.get('package_id', ml.package_id), owner_id=updates.get('owner_id', ml.owner_id), strict=True) new_product_qty = sum([x[1] for x in q]) except UserError: if updates.get('lot_id'): # If we were not able to reserve on tracked quants, we can use untracked ones. try: q = Quant._update_reserved_quantity( ml.product_id, updates.get('location_id', ml.location_id), ml.product_qty, lot_id=False, package_id=updates.get( 'package_id', ml.package_id), owner_id=updates.get( 'owner_id', ml.owner_id), strict=True) new_product_qty = sum([x[1] for x in q]) except UserError: pass if new_product_qty != ml.product_qty: new_product_uom_qty = self.product_id.uom_id._compute_quantity( new_product_qty, self.product_uom_id, rounding_method='HALF-UP') ml.with_context(bypass_reservation_update=True ).product_uom_qty = new_product_uom_qty # When editing a done move line, the reserved availability of a potential chained move is impacted. Take care of running again `_action_assign` on the concerned moves. next_moves = self.env['stock.move'] if updates or 'qty_done' in vals: for ml in self.filtered(lambda ml: ml.move_id.state == 'done' and ml.product_id.type == 'product'): # undo the original move line qty_done_orig = ml.move_id.product_uom._compute_quantity( ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') in_date = Quant._update_available_quantity( ml.product_id, ml.location_dest_id, -qty_done_orig, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id)[1] Quant._update_available_quantity(ml.product_id, ml.location_id, qty_done_orig, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, in_date=in_date) # move what's been actually done product_id = ml.product_id location_id = updates.get('location_id', ml.location_id) location_dest_id = updates.get('location_dest_id', ml.location_dest_id) qty_done = vals.get('qty_done', ml.qty_done) lot_id = updates.get('lot_id', ml.lot_id) package_id = updates.get('package_id', ml.package_id) result_package_id = updates.get('result_package_id', ml.result_package_id) owner_id = updates.get('owner_id', ml.owner_id) quantity = ml.move_id.product_uom._compute_quantity( qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') if not location_id.should_bypass_reservation(): ml._free_reservation(product_id, location_id, quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id) if not float_is_zero(quantity, precision_digits=precision): available_qty, in_date = Quant._update_available_quantity( product_id, location_id, -quantity, lot_id=lot_id, package_id=package_id, owner_id=owner_id) if available_qty < 0 and lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity( product_id, location_id, lot_id=False, package_id=package_id, owner_id=owner_id, strict=True) if untracked_qty: taken_from_untracked_qty = min( untracked_qty, abs(available_qty)) Quant._update_available_quantity( product_id, location_id, -taken_from_untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id) Quant._update_available_quantity( product_id, location_id, taken_from_untracked_qty, lot_id=lot_id, package_id=package_id, owner_id=owner_id) if not location_id.should_bypass_reservation(): ml._free_reservation(ml.product_id, location_id, untracked_qty, lot_id=False, package_id=package_id, owner_id=owner_id) Quant._update_available_quantity( product_id, location_dest_id, quantity, lot_id=lot_id, package_id=result_package_id, owner_id=owner_id, in_date=in_date) # Unreserve and reserve following move in order to have the real reserved quantity on move_line. next_moves |= ml.move_id.move_dest_ids.filtered( lambda move: move.state not in ('done', 'cancel')) # Log a note if ml.picking_id: ml._log_message(ml.picking_id, ml, 'stock.track_move_template', vals) res = super(StockMoveLine, self).write(vals) # Update scrap object linked to move_lines to the new quantity. if 'qty_done' in vals: for move in self.mapped('move_id'): if move.scrapped: move.scrap_ids.write({'scrap_qty': move.quantity_done}) # As stock_account values according to a move's `product_uom_qty`, we consider that any # done stock move should have its `quantity_done` equals to its `product_uom_qty`, and # this is what move's `action_done` will do. So, we replicate the behavior here. if updates or 'qty_done' in vals: moves = self.filtered( lambda ml: ml.move_id.state == 'done').mapped('move_id') for move in moves: move.product_uom_qty = move.quantity_done next_moves._do_unreserve() next_moves._action_assign() return res def unlink(self): precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') for ml in self: if ml.state in ('done', 'cancel'): raise UserError( _('You can not delete product moves if the picking is done. You can only correct the done quantities.' )) # Unlinking a move line should unreserve. if ml.product_id.type == 'product' and not ml.location_id.should_bypass_reservation( ) and not float_is_zero(ml.product_qty, precision_digits=precision): self.env['stock.quant']._update_reserved_quantity( ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) return super(StockMoveLine, self).unlink() def _action_done(self): """ This method is called during a move's `action_done`. It'll actually move a quant from the source location to the destination location, and unreserve if needed in the source location. This method is intended to be called on all the move lines of a move. This method is not intended to be called when editing a `done` move (that's what the override of `write` here is done. """ # First, we loop over all the move lines to do a preliminary check: `qty_done` should not # be negative and, according to the presence of a picking type or a linked inventory # adjustment, enforce some rules on the `lot_id` field. If `qty_done` is null, we unlink # the line. It is mandatory in order to free the reservation and correctly apply # `action_done` on the next move lines. ml_to_delete = self.env['stock.move.line'] for ml in self: qty_done_float_compared = float_compare( ml.qty_done, 0, precision_rounding=ml.product_uom_id.rounding) if qty_done_float_compared > 0: if ml.product_id.tracking != 'none': picking_type_id = ml.move_id.picking_type_id if picking_type_id: if picking_type_id.use_create_lots: # If a picking type is linked, we may have to create a production lot on # the fly before assigning it to the move line if the user checked both # `use_create_lots` and `use_existing_lots`. if ml.lot_name and not ml.lot_id: lot = self.env['stock.production.lot'].create({ 'name': ml.lot_name, 'product_id': ml.product_id.id }) ml.write({'lot_id': lot.id}) elif not picking_type_id.use_create_lots and not picking_type_id.use_existing_lots: # If the user disabled both `use_create_lots` and `use_existing_lots` # checkboxes on the picking type, he's allowed to enter tracked # products without a `lot_id`. continue elif ml.move_id.inventory_id: # If an inventory adjustment is linked, the user is allowed to enter # tracked products without a `lot_id`. continue if not ml.lot_id: raise UserError( _('You need to supply a lot/serial number for %s.') % ml.product_id.name) elif qty_done_float_compared < 0: raise UserError(_('No negative quantities allowed')) else: ml_to_delete |= ml ml_to_delete.unlink() # Now, we can actually move the quant. for ml in self - ml_to_delete: if ml.product_id.type == 'product': Quant = self.env['stock.quant'] rounding = ml.product_uom_id.rounding # if this move line is force assigned, unreserve elsewhere if needed if not ml.location_id.should_bypass_reservation( ) and float_compare(ml.qty_done, ml.product_qty, precision_rounding=rounding) > 0: extra_qty = ml.qty_done - ml.product_qty ml._free_reservation(ml.product_id, ml.location_id, extra_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) # unreserve what's been reserved if not ml.location_id.should_bypass_reservation( ) and ml.product_id.type == 'product' and ml.product_qty: try: Quant._update_reserved_quantity( ml.product_id, ml.location_id, -ml.product_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) except UserError: Quant._update_reserved_quantity( ml.product_id, ml.location_id, -ml.product_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) # move what's been actually done quantity = ml.product_uom_id._compute_quantity( ml.qty_done, ml.move_id.product_id.uom_id, rounding_method='HALF-UP') available_qty, in_date = Quant._update_available_quantity( ml.product_id, ml.location_id, -quantity, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) if available_qty < 0 and ml.lot_id: # see if we can compensate the negative quants with some untracked quants untracked_qty = Quant._get_available_quantity( ml.product_id, ml.location_id, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id, strict=True) if untracked_qty: taken_from_untracked_qty = min(untracked_qty, abs(quantity)) Quant._update_available_quantity( ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity( ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id) Quant._update_available_quantity( ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date) # Reset the reserved quantity as we just moved it to the destination location. (self - ml_to_delete).with_context( bypass_reservation_update=True).write({'product_uom_qty': 0.00}) def _log_message(self, record, move, template, vals): data = vals.copy() if 'lot_id' in vals and vals['lot_id'] != move.lot_id.id: data['lot_name'] = self.env['stock.production.lot'].browse( vals.get('lot_id')).name if 'location_id' in vals: data['location_name'] = self.env['stock.location_id'].browse( vals.get('location_id')).name if 'location_dest_id' in vals: data['location_dest_name'] = self.env['stock.location_id'].browse( vals.get('location_dest_id')).name if 'package_id' in vals and vals['package_id'] != move.package_id.id: data['package_name'] = self.env['stock.quant.package'].browse( vals.get('package_id')).name if 'package_result_id' in vals and vals[ 'package_result_id'] != move.package_result_id.id: data['result_package_name'] = self.env[ 'stock.quant.package'].browse( vals.get('result_package_id')).name if 'owner_id' in vals and vals['owner_id'] != move.owner_id.id: data['owner_name'] = self.env['res.partner'].browse( vals.get('owner_id')).name record.message_post_with_view( template, values={ 'move': move, 'vals': dict(vals, **data) }, subtype_id=self.env.ref('mail.mt_note').id) def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None): """ When editing a done move line or validating one with some forced quantities, it is possible to impact quants that were not reserved. It is therefore necessary to edit or unlink the move lines that reserved a quantity now unavailable. """ self.ensure_one() # Check the available quantity, with the `strict` kw set to `True`. If the available # quantity is greather than the quantity now unavailable, there is nothing to do. available_quantity = self.env['stock.quant']._get_available_quantity( product_id, location_id, lot_id=lot_id, package_id=package_id, owner_id=owner_id, strict=True) if quantity > available_quantity: # We now have to find the move lines that reserved our now unavailable quantity. We # take care to exclude ourselves and the move lines were work had already been done. oudated_move_lines_domain = [ ('move_id.state', 'not in', ['done', 'cancel']), ('product_id', '=', product_id.id), ('lot_id', '=', lot_id.id if lot_id else False), ('location_id', '=', location_id.id), ('owner_id', '=', owner_id.id if owner_id else False), ('package_id', '=', package_id.id if package_id else False), ('product_qty', '>', 0.0), ('qty_done', '=', 0.0), ('id', '!=', self.id), ] oudated_candidates = self.env['stock.move.line'].search( oudated_move_lines_domain) # As the move's state is not computed over the move lines, we'll have to manually # recompute the moves which we adapted their lines. move_to_recompute_state = self.env['stock.move'] rounding = self.product_uom_id.rounding for candidate in oudated_candidates: if float_compare(candidate.product_qty, quantity, precision_rounding=rounding) <= 0: quantity -= candidate.product_qty move_to_recompute_state |= candidate.move_id candidate.unlink() else: # split this move line and assign the new part to our extra move quantity_split = float_round( candidate.product_qty - quantity, precision_rounding=self.product_uom_id.rounding, rounding_method='UP') candidate.product_uom_qty = self.product_id.uom_id._compute_quantity( quantity_split, self.product_uom_id, rounding_method='HALF-UP') quantity -= quantity_split move_to_recompute_state |= candidate.move_id if quantity == 0.0: break move_to_recompute_state._recompute_state()
class ProductAttributevalue(models.Model): _name = "product.attribute.value" _order = 'sequence, attribute_id, id' name = fields.Char('Value', required=True, translate=True) sequence = fields.Integer('Sequence', help="Determine the display order") attribute_id = fields.Many2one('product.attribute', 'Attribute', ondelete='cascade', required=True) product_ids = fields.Many2many('product.product', string='Variants', readonly=True) price_extra = fields.Float( 'Attribute Price Extra', compute='_compute_price_extra', inverse='_set_price_extra', default=0.0, digits=dp.get_precision('Product Price'), help= "Price Extra: Extra price for the variant with this attribute value on sale price. eg. 200 price extra, 1000 + 200 = 1200." ) price_ids = fields.One2many('product.attribute.price', 'value_id', 'Attribute Prices', readonly=True) _sql_constraints = [('value_company_uniq', 'unique (name,attribute_id)', 'This attribute value already exists !')] @api.one def _compute_price_extra(self): if self._context.get('active_id'): price = self.price_ids.filtered(lambda price: price.product_tmpl_id .id == self._context['active_id']) self.price_extra = price.price_extra else: self.price_extra = 0.0 def _set_price_extra(self): if not self._context.get('active_id'): return AttributePrice = self.env['product.attribute.price'] prices = AttributePrice.search([('value_id', 'in', self.ids), ('product_tmpl_id', '=', self._context['active_id'])]) updated = prices.mapped('value_id') if prices: prices.write({'price_extra': self.price_extra}) else: for value in self - updated: AttributePrice.create({ 'product_tmpl_id': self._context['active_id'], 'value_id': value.id, 'price_extra': self.price_extra, }) @api.multi def name_get(self): if not self._context.get('show_attribute', True): # TDE FIXME: not used return super(ProductAttributevalue, self).name_get() return [(value.id, "%s: %s" % (value.attribute_id.name, value.name)) for value in self] @api.multi def unlink(self): linked_products = self.env['product.product'].with_context( active_test=False).search([('attribute_value_ids', 'in', self.ids) ]) if linked_products: raise UserError( _('The operation cannot be completed:\nYou are trying to delete an attribute value with a reference on a product variant.' )) return super(ProductAttributevalue, self).unlink() @api.multi def _variant_name(self, variable_attributes): return ", ".join( [v.name for v in self if v.attribute_id in variable_attributes])
class HolidaysAllocation(models.Model): """ Allocation Requests Access specifications: similar to leave requests """ _name = "hr.leave.allocation" _description = "Time Off Allocation" _order = "create_date desc" _inherit = ['mail.thread', 'mail.activity.mixin'] _mail_post_access = 'read' def _default_holiday_status_id(self): if self.user_has_groups('hr_holidays.group_hr_holidays_user'): domain = [('valid', '=', True)] else: domain = [('valid', '=', True), ('allocation_type', '=', 'fixed_allocation')] return self.env['hr.leave.type'].search(domain, limit=1) def _holiday_status_id_domain(self): if self.user_has_groups('hr_holidays.group_hr_holidays_manager'): return [('valid', '=', True), ('allocation_type', '!=', 'no')] return [('valid', '=', True), ('allocation_type', '=', 'fixed_allocation')] name = fields.Char('Description', compute='_compute_description', inverse='_inverse_description', search='_search_description', compute_sudo=False) private_name = fields.Char('Allocation Description', groups='hr_holidays.group_hr_holidays_user') state = fields.Selection( [('draft', 'To Submit'), ('cancel', 'Cancelled'), ('confirm', 'To Approve'), ('refuse', 'Refused'), ('validate1', 'Second Approval'), ('validate', 'Approved')], string='Status', readonly=True, tracking=True, copy=False, default='confirm', help= "The status is set to 'To Submit', when an allocation request is created." + "\nThe status is 'To Approve', when an allocation request is confirmed by user." + "\nThe status is 'Refused', when an allocation request is refused by manager." + "\nThe status is 'Approved', when an allocation request is approved by manager." ) date_from = fields.Datetime('Start Date', readonly=True, index=True, copy=False, default=fields.Date.context_today, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, tracking=True) date_to = fields.Datetime('End Date', compute='_compute_from_holiday_status_id', store=True, readonly=False, copy=False, tracking=True, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }) holiday_status_id = fields.Many2one("hr.leave.type", compute='_compute_from_employee_id', store=True, string="Time Off Type", required=True, readonly=False, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }, domain=_holiday_status_id_domain) employee_id = fields.Many2one('hr.employee', compute='_compute_from_holiday_type', store=True, string='Employee', index=True, readonly=False, ondelete="restrict", tracking=True, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }) manager_id = fields.Many2one('hr.employee', compute='_compute_from_employee_id', store=True, string='Manager') notes = fields.Text('Reasons', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }) # duration number_of_days = fields.Float( 'Number of Days', compute='_compute_from_holiday_status_id', store=True, readonly=False, tracking=True, default=1, help='Duration in days. Reference field to use when necessary.') number_of_days_display = fields.Float( 'Duration (days)', compute='_compute_number_of_days_display', states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= "If Accrual Allocation: Number of days allocated in addition to the ones you will get via the accrual' system." ) number_of_hours_display = fields.Float( 'Duration (hours)', compute='_compute_number_of_hours_display', help= "If Accrual Allocation: Number of hours allocated in addition to the ones you will get via the accrual' system." ) duration_display = fields.Char( 'Allocated (Days/Hours)', compute='_compute_duration_display', help= "Field allowing to see the allocation duration in days or hours depending on the type_request_unit" ) # details parent_id = fields.Many2one('hr.leave.allocation', string='Parent') linked_request_ids = fields.One2many('hr.leave.allocation', 'parent_id', string='Linked Requests') first_approver_id = fields.Many2one( 'hr.employee', string='First Approval', readonly=True, copy=False, help= 'This area is automatically filled by the user who validates the allocation' ) second_approver_id = fields.Many2one( 'hr.employee', string='Second Approval', readonly=True, copy=False, help= 'This area is automatically filled by the user who validates the allocation with second level (If allocation type need second validation)' ) validation_type = fields.Selection( string='Validation Type', related='holiday_status_id.allocation_validation_type', readonly=True) can_reset = fields.Boolean('Can reset', compute='_compute_can_reset') can_approve = fields.Boolean('Can Approve', compute='_compute_can_approve') type_request_unit = fields.Selection( related='holiday_status_id.request_unit', readonly=True) # mode holiday_type = fields.Selection( [('employee', 'By Employee'), ('company', 'By Company'), ('department', 'By Department'), ('category', 'By Employee Tag')], string='Allocation Mode', readonly=True, required=True, default='employee', states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= "Allow to create requests in batchs:\n- By Employee: for a specific employee" "\n- By Company: all employees of the specified company" "\n- By Department: all employees of the specified department" "\n- By Employee Tag: all employees of the specific employee group category" ) mode_company_id = fields.Many2one('res.company', compute='_compute_from_holiday_type', store=True, string='Company Mode', readonly=False, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }) department_id = fields.Many2one('hr.department', compute='_compute_department_id', store=True, string='Department', states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }) category_id = fields.Many2one('hr.employee.category', compute='_compute_from_holiday_type', store=True, string='Employee Tag', readonly=False, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }) # accrual configuration allocation_type = fields.Selection([('regular', 'Regular Allocation'), ('accrual', 'Accrual Allocation')], string="Allocation Type", default="regular", required=True, readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }) accrual_limit = fields.Integer( 'Balance limit', default=0, help="Maximum of allocation for accrual; 0 means no maximum.") number_per_interval = fields.Float( "Number of unit per interval", compute='_compute_from_holiday_status_id', store=True, readonly=False, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }) interval_number = fields.Integer("Number of unit between two intervals", compute='_compute_from_holiday_status_id', store=True, readonly=False, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }) unit_per_interval = fields.Selection( [('hours', 'Hours'), ('days', 'Days')], compute='_compute_from_holiday_status_id', store=True, string="Unit of time added at each interval", readonly=False, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }) interval_unit = fields.Selection( [('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months'), ('years', 'Years')], compute='_compute_from_holiday_status_id', store=True, string="Unit of time between two intervals", readonly=False, states={ 'cancel': [('readonly', True)], 'refuse': [('readonly', True)], 'validate1': [('readonly', True)], 'validate': [('readonly', True)] }) nextcall = fields.Date("Date of the next accrual allocation", default=False, readonly=True) max_leaves = fields.Float(compute='_compute_leaves') leaves_taken = fields.Float(compute='_compute_leaves') _sql_constraints = [ ('type_value', "CHECK( (holiday_type='employee' AND employee_id IS NOT NULL) or " "(holiday_type='category' AND category_id IS NOT NULL) or " "(holiday_type='department' AND department_id IS NOT NULL) or " "(holiday_type='company' AND mode_company_id IS NOT NULL))", "The employee, department, company or employee category of this request is missing. Please make sure that your user login is linked to an employee." ), ('duration_check', "CHECK ( number_of_days >= 0 )", "The number of days must be greater than 0."), ('number_per_interval_check', "CHECK(number_per_interval > 0)", "The number per interval should be greater than 0"), ('interval_number_check', "CHECK(interval_number > 0)", "The interval number should be greater than 0"), ] @api.model def _update_accrual(self): """ Method called by the cron task in order to increment the number_of_days when necessary. """ today = fields.Date.from_string(fields.Date.today()) holidays = self.search([('allocation_type', '=', 'accrual'), ('employee_id.active', '=', True), ('state', '=', 'validate'), ('holiday_type', '=', 'employee'), '|', ('date_to', '=', False), ('date_to', '>', fields.Datetime.now()), '|', ('nextcall', '=', False), ('nextcall', '<=', today)]) for holiday in holidays: values = {} delta = relativedelta(days=0) if holiday.interval_unit == 'days': delta = relativedelta(days=holiday.interval_number) if holiday.interval_unit == 'weeks': delta = relativedelta(weeks=holiday.interval_number) if holiday.interval_unit == 'months': delta = relativedelta(months=holiday.interval_number) if holiday.interval_unit == 'years': delta = relativedelta(years=holiday.interval_number) values['nextcall'] = (holiday.nextcall if holiday.nextcall else today) + delta period_start = datetime.combine(today, time(0, 0, 0)) - delta period_end = datetime.combine(today, time(0, 0, 0)) # We have to check when the employee has been created # in order to not allocate him/her too much leaves start_date = holiday.employee_id._get_date_start_work() # If employee is created after the period, we cancel the computation if period_end <= start_date or period_end < holiday.date_from: holiday.write(values) continue # If employee created during the period, taking the date at which he has been created if period_start <= start_date: period_start = start_date employee = holiday.employee_id worked = employee._get_work_days_data_batch( period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])[employee.id]['days'] left = employee._get_leave_days_data_batch( period_start, period_end, domain=[('holiday_id.holiday_status_id.unpaid', '=', True), ('time_type', '=', 'leave')])[employee.id]['days'] prorata = worked / (left + worked) if worked else 0 days_to_give = holiday.number_per_interval if holiday.unit_per_interval == 'hours': # As we encode everything in days in the database we need to convert # the number of hours into days for this we use the # mean number of hours set on the employee's calendar days_to_give = days_to_give / ( employee.resource_calendar_id.hours_per_day or HOURS_PER_DAY) values[ 'number_of_days'] = holiday.number_of_days + days_to_give * prorata if holiday.accrual_limit > 0: values['number_of_days'] = min(values['number_of_days'], holiday.accrual_limit) holiday.write(values) @api.depends_context('uid') def _compute_description(self): self.check_access_rights('read') self.check_access_rule('read') is_officer = self.env.user.has_group( 'hr_holidays.group_hr_holidays_user') for allocation in self: if is_officer or allocation.employee_id.user_id == self.env.user or allocation.manager_id == self.env.user: allocation.name = allocation.sudo().private_name else: allocation.name = '*****' def _inverse_description(self): is_officer = self.env.user.has_group( 'hr_holidays.group_hr_holidays_user') for allocation in self: if is_officer or allocation.employee_id.user_id == self.env.user or allocation.manager_id == self.env.user: allocation.sudo().private_name = allocation.name def _search_description(self, operator, value): is_officer = self.env.user.has_group( 'hr_holidays.group_hr_holidays_user') domain = [('private_name', operator, value)] if not is_officer: domain = expression.AND( [domain, [('employee_id.user_id', '=', self.env.user.id)]]) allocations = self.sudo().search(domain) return [('id', 'in', allocations.ids)] @api.depends('employee_id', 'holiday_status_id') def _compute_leaves(self): for allocation in self: leave_type = allocation.holiday_status_id.with_context( employee_id=allocation.employee_id.id) allocation.max_leaves = leave_type.max_leaves allocation.leaves_taken = leave_type.leaves_taken @api.depends('number_of_days') def _compute_number_of_days_display(self): for allocation in self: allocation.number_of_days_display = allocation.number_of_days @api.depends('number_of_days', 'employee_id') def _compute_number_of_hours_display(self): for allocation in self: if allocation.parent_id and allocation.parent_id.type_request_unit == "hour": allocation.number_of_hours_display = allocation.number_of_days * HOURS_PER_DAY elif allocation.number_of_days: allocation.number_of_hours_display = allocation.number_of_days * ( allocation.employee_id.sudo( ).resource_id.calendar_id.hours_per_day or HOURS_PER_DAY) else: allocation.number_of_hours_display = 0.0 @api.depends('number_of_hours_display', 'number_of_days_display') def _compute_duration_display(self): for allocation in self: allocation.duration_display = '%g %s' % ( (float_round(allocation.number_of_hours_display, precision_digits=2) if allocation.type_request_unit == 'hour' else float_round( allocation.number_of_days_display, precision_digits=2)), _('hours') if allocation.type_request_unit == 'hour' else _('days')) @api.depends('state', 'employee_id', 'department_id') def _compute_can_reset(self): for allocation in self: try: allocation._check_approval_update('draft') except (AccessError, UserError): allocation.can_reset = False else: allocation.can_reset = True @api.depends('state', 'employee_id', 'department_id') def _compute_can_approve(self): for allocation in self: try: if allocation.state == 'confirm' and allocation.validation_type == 'both': allocation._check_approval_update('validate1') else: allocation._check_approval_update('validate') except (AccessError, UserError): allocation.can_approve = False else: allocation.can_approve = True @api.depends('holiday_type') def _compute_from_holiday_type(self): for allocation in self: if allocation.holiday_type == 'employee': if not allocation.employee_id: allocation.employee_id = self.env.user.employee_id allocation.mode_company_id = False allocation.category_id = False if allocation.holiday_type == 'company': allocation.employee_id = False if not allocation.mode_company_id: allocation.mode_company_id = self.env.company allocation.category_id = False elif allocation.holiday_type == 'department': allocation.employee_id = False allocation.mode_company_id = False allocation.category_id = False elif allocation.holiday_type == 'category': allocation.employee_id = False allocation.mode_company_id = False elif not allocation.employee_id and not allocation._origin.employee_id: allocation.employee_id = self.env.context.get( 'default_employee_id') or self.env.user.employee_id @api.depends('holiday_type', 'employee_id') def _compute_department_id(self): for allocation in self: if allocation.holiday_type == 'employee': allocation.department_id = allocation.employee_id.department_id elif allocation.holiday_type == 'department': if not allocation.department_id: allocation.department_id = self.env.user.employee_id.department_id elif allocation.holiday_type == 'category': allocation.department_id = False @api.depends('employee_id') def _compute_from_employee_id(self): default_holiday_status_id = self._default_holiday_status_id() for holiday in self: holiday.manager_id = holiday.employee_id and holiday.employee_id.parent_id if holiday.employee_id.user_id != self.env.user and holiday._origin.employee_id != holiday.employee_id: holiday.holiday_status_id = False elif not holiday.holiday_status_id and not holiday._origin.holiday_status_id: holiday.holiday_status_id = default_holiday_status_id @api.depends('holiday_status_id', 'allocation_type', 'number_of_hours_display', 'number_of_days_display') def _compute_from_holiday_status_id(self): for allocation in self: allocation.number_of_days = allocation.number_of_days_display if allocation.type_request_unit == 'hour': allocation.number_of_days = allocation.number_of_hours_display / ( allocation.employee_id.sudo( ).resource_calendar_id.hours_per_day or HOURS_PER_DAY) # set default values if not allocation.interval_number and not allocation._origin.interval_number: allocation.interval_number = 1 if not allocation.number_per_interval and not allocation._origin.number_per_interval: allocation.number_per_interval = 1 if not allocation.unit_per_interval and not allocation._origin.unit_per_interval: allocation.unit_per_interval = 'hours' if not allocation.interval_unit and not allocation._origin.interval_unit: allocation.interval_unit = 'weeks' if allocation.holiday_status_id.validity_stop and allocation.date_to: new_date_to = datetime.combine( allocation.holiday_status_id.validity_stop, time.max) if new_date_to < allocation.date_to: allocation.date_to = new_date_to if allocation.allocation_type == 'accrual': if allocation.holiday_status_id.request_unit == 'hour': allocation.unit_per_interval = 'hours' else: allocation.unit_per_interval = 'days' else: allocation.interval_number = 1 allocation.interval_unit = 'weeks' allocation.number_per_interval = 1 allocation.unit_per_interval = 'hours' #################################################### # ORM Overrides methods #################################################### def name_get(self): res = [] for allocation in self: if allocation.holiday_type == 'company': target = allocation.mode_company_id.name elif allocation.holiday_type == 'department': target = allocation.department_id.name elif allocation.holiday_type == 'category': target = allocation.category_id.name else: target = allocation.employee_id.sudo().name res.append(( allocation.id, _("Allocation of %(allocation_name)s : %(duration).2f %(duration_type)s to %(person)s", allocation_name=allocation.holiday_status_id.sudo().name, duration=allocation.number_of_hours_display if allocation.type_request_unit == 'hour' else allocation.number_of_days, duration_type='hours' if allocation.type_request_unit == 'hour' else 'days', person=target))) return res def add_follower(self, employee_id): employee = self.env['hr.employee'].browse(employee_id) if employee.user_id: self.message_subscribe(partner_ids=employee.user_id.partner_id.ids) @api.constrains('holiday_status_id') def _check_leave_type_validity(self): for allocation in self: if allocation.holiday_status_id.validity_stop: vstop = allocation.holiday_status_id.validity_stop today = fields.Date.today() if vstop < today: raise ValidationError( _('You can allocate %(allocation_type)s only before %(date)s.', allocation_type=allocation.holiday_status_id. display_name, date=allocation.holiday_status_id.validity_stop)) @api.model def create(self, values): """ Override to avoid automatic logging of creation """ employee_id = values.get('employee_id', False) if not values.get('department_id'): values.update({ 'department_id': self.env['hr.employee'].browse(employee_id).department_id.id }) holiday = super( HolidaysAllocation, self.with_context(mail_create_nosubscribe=True)).create(values) holiday.add_follower(employee_id) if holiday.validation_type == 'hr': holiday.message_subscribe(partner_ids=( holiday.employee_id.parent_id.user_id.partner_id | holiday.employee_id.leave_manager_id.partner_id).ids) if not self._context.get('import_file'): holiday.activity_update() return holiday def write(self, values): employee_id = values.get('employee_id', False) if values.get('state'): self._check_approval_update(values['state']) result = super(HolidaysAllocation, self).write(values) self.add_follower(employee_id) return result def unlink(self): state_description_values = { elem[0]: elem[1] for elem in self._fields['state']._description_selection(self.env) } for holiday in self.filtered(lambda holiday: holiday.state not in ['draft', 'cancel', 'confirm']): raise UserError( _('You cannot delete an allocation request which is in %s state.' ) % (state_description_values.get(holiday.state), )) return super(HolidaysAllocation, self).unlink() def _get_mail_redirect_suggested_company(self): return self.holiday_status_id.company_id #################################################### # Business methods #################################################### def _prepare_holiday_values(self, employee): self.ensure_one() values = { 'name': self.name, 'holiday_type': 'employee', 'holiday_status_id': self.holiday_status_id.id, 'notes': self.notes, 'number_of_days': self.number_of_days, 'parent_id': self.id, 'employee_id': employee.id, 'allocation_type': self.allocation_type, 'date_from': self.date_from, 'date_to': self.date_to, 'interval_unit': self.interval_unit, 'interval_number': self.interval_number, 'number_per_interval': self.number_per_interval, 'unit_per_interval': self.unit_per_interval, } return values def action_draft(self): if any(holiday.state not in ['confirm', 'refuse'] for holiday in self): raise UserError( _('Allocation request state must be "Refused" or "To Approve" in order to be reset to Draft.' )) self.write({ 'state': 'draft', 'first_approver_id': False, 'second_approver_id': False, }) linked_requests = self.mapped('linked_request_ids') if linked_requests: linked_requests.action_draft() linked_requests.unlink() self.activity_update() return True def action_confirm(self): if self.filtered(lambda holiday: holiday.state != 'draft'): raise UserError( _('Allocation request must be in Draft state ("To Submit") in order to confirm it.' )) res = self.write({'state': 'confirm'}) self.activity_update() return res def action_approve(self): # if validation_type == 'both': this method is the first approval approval # if validation_type != 'both': this method calls action_validate() below if any(holiday.state != 'confirm' for holiday in self): raise UserError( _('Allocation request must be confirmed ("To Approve") in order to approve it.' )) current_employee = self.env.user.employee_id self.filtered(lambda hol: hol.validation_type == 'both').write({ 'state': 'validate1', 'first_approver_id': current_employee.id }) self.filtered( lambda hol: not hol.validation_type == 'both').action_validate() self.activity_update() def action_validate(self): current_employee = self.env.user.employee_id for holiday in self: if holiday.state not in ['confirm', 'validate1']: raise UserError( _('Allocation request must be confirmed in order to approve it.' )) holiday.write({'state': 'validate'}) if holiday.validation_type == 'both': holiday.write({'second_approver_id': current_employee.id}) else: holiday.write({'first_approver_id': current_employee.id}) holiday._action_validate_create_childs() self.activity_update() return True def _action_validate_create_childs(self): childs = self.env['hr.leave.allocation'] if self.state == 'validate' and self.holiday_type in [ 'category', 'department', 'company' ]: if self.holiday_type == 'category': employees = self.category_id.employee_ids elif self.holiday_type == 'department': employees = self.department_id.member_ids else: employees = self.env['hr.employee'].search([ ('company_id', '=', self.mode_company_id.id) ]) for employee in employees: childs += self.with_context( mail_notify_force_send=False, mail_activity_automation_skip=True).create( self._prepare_holiday_values(employee)) # TODO is it necessary to interleave the calls? childs.action_approve() if childs and self.validation_type == 'both': childs.action_validate() return childs def action_refuse(self): current_employee = self.env.user.employee_id if any(holiday.state not in ['confirm', 'validate', 'validate1'] for holiday in self): raise UserError( _('Allocation request must be confirmed or validated in order to refuse it.' )) validated_holidays = self.filtered( lambda hol: hol.state == 'validate1') validated_holidays.write({ 'state': 'refuse', 'first_approver_id': current_employee.id }) (self - validated_holidays).write({ 'state': 'refuse', 'second_approver_id': current_employee.id }) # If a category that created several holidays, cancel all related linked_requests = self.mapped('linked_request_ids') if linked_requests: linked_requests.action_refuse() self.activity_update() return True def _check_approval_update(self, state): """ Check if target state is achievable. """ if self.env.is_superuser(): return current_employee = self.env.user.employee_id if not current_employee: return is_officer = self.env.user.has_group( 'hr_holidays.group_hr_holidays_user') is_manager = self.env.user.has_group( 'hr_holidays.group_hr_holidays_manager') for holiday in self: val_type = holiday.holiday_status_id.sudo( ).allocation_validation_type if state == 'confirm': continue if state == 'draft': if holiday.employee_id != current_employee and not is_manager: raise UserError( _('Only a time off Manager can reset other people allocation.' )) continue if not is_officer and self.env.user != holiday.employee_id.leave_manager_id: raise UserError( _('Only a time off Officer/Responsible or Manager can approve or refuse time off requests.' )) if is_officer or self.env.user == holiday.employee_id.leave_manager_id: # use ir.rule based first access check: department, members, ... (see security.xml) holiday.check_access_rule('write') if holiday.employee_id == current_employee and not is_manager: raise UserError( _('Only a time off Manager can approve its own requests.')) if (state == 'validate1' and val_type == 'both') or (state == 'validate' and val_type == 'manager'): if self.env.user == holiday.employee_id.leave_manager_id and self.env.user != holiday.employee_id.user_id: continue manager = holiday.employee_id.parent_id or holiday.employee_id.department_id.manager_id if (manager != current_employee) and not is_manager: raise UserError( _('You must be either %s\'s manager or time off manager to approve this time off' ) % (holiday.employee_id.name)) if state == 'validate' and val_type == 'both': if not is_officer: raise UserError( _('Only a Time off Approver can apply the second approval on allocation requests.' )) # ------------------------------------------------------------ # Activity methods # ------------------------------------------------------------ def _get_responsible_for_approval(self): self.ensure_one() responsible = self.env.user if self.validation_type == 'manager' or (self.validation_type == 'both' and self.state == 'confirm'): if self.employee_id.leave_manager_id: responsible = self.employee_id.leave_manager_id elif self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'): if self.holiday_status_id.responsible_id: responsible = self.holiday_status_id.responsible_id return responsible def activity_update(self): to_clean, to_do = self.env['hr.leave.allocation'], self.env[ 'hr.leave.allocation'] for allocation in self: note = _( 'New Allocation Request created by %(user)s: %(count)s Days of %(allocation_type)s', user=allocation.create_uid.name, count=allocation.number_of_days, allocation_type=allocation.holiday_status_id.name) if allocation.state == 'draft': to_clean |= allocation elif allocation.state == 'confirm': allocation.activity_schedule( 'hr_holidays.mail_act_leave_allocation_approval', note=note, user_id=allocation.sudo()._get_responsible_for_approval( ).id or self.env.user.id) elif allocation.state == 'validate1': allocation.activity_feedback( ['hr_holidays.mail_act_leave_allocation_approval']) allocation.activity_schedule( 'hr_holidays.mail_act_leave_allocation_second_approval', note=note, user_id=allocation.sudo()._get_responsible_for_approval( ).id or self.env.user.id) elif allocation.state == 'validate': to_do |= allocation elif allocation.state == 'refuse': to_clean |= allocation if to_clean: to_clean.activity_unlink([ 'hr_holidays.mail_act_leave_allocation_approval', 'hr_holidays.mail_act_leave_allocation_second_approval' ]) if to_do: to_do.activity_feedback([ 'hr_holidays.mail_act_leave_allocation_approval', 'hr_holidays.mail_act_leave_allocation_second_approval' ]) #################################################### # Messaging methods #################################################### def _track_subtype(self, init_values): if 'state' in init_values and self.state == 'validate': allocation_notif_subtype_id = self.holiday_status_id.allocation_notif_subtype_id return allocation_notif_subtype_id or self.env.ref( 'hr_holidays.mt_leave_allocation') return super(HolidaysAllocation, self)._track_subtype(init_values) def _notify_get_groups(self, msg_vals=None): """ Handle HR users and officers recipients that can validate or refuse holidays directly from email. """ groups = super(HolidaysAllocation, self)._notify_get_groups(msg_vals=msg_vals) msg_vals = msg_vals or {} self.ensure_one() hr_actions = [] if self.state == 'confirm': app_action = self._notify_get_action_link( 'controller', controller='/allocation/validate', **msg_vals) hr_actions += [{'url': app_action, 'title': _('Approve')}] if self.state in ['confirm', 'validate', 'validate1']: ref_action = self._notify_get_action_link( 'controller', controller='/allocation/refuse', **msg_vals) hr_actions += [{'url': ref_action, 'title': _('Refuse')}] holiday_user_group_id = self.env.ref( 'hr_holidays.group_hr_holidays_user').id new_group = ('group_hr_holidays_user', lambda pdata: pdata['type'] == 'user' and holiday_user_group_id in pdata['groups'], { 'actions': hr_actions, }) return [new_group] + groups def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None): # due to record rule can not allow to add follower and mention on validated leave so subscribe through sudo if self.state in ['validate', 'validate1']: self.check_access_rights('read') self.check_access_rule('read') return super(HolidaysAllocation, self.sudo()).message_subscribe( partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids) return super(HolidaysAllocation, self).message_subscribe(partner_ids=partner_ids, channel_ids=channel_ids, subtype_ids=subtype_ids)
class AccountAssetAsset(models.Model): _name = 'account.asset.asset' _description = 'Asset/Revenue Recognition' _inherit = ['mail.thread'] entry_count = fields.Integer(compute='_entry_count', string='# Asset Entries') name = fields.Char(string='Asset Name', required=True, readonly=True, states={'draft': [('readonly', False)]}) code = fields.Char(string='Reference', size=32, readonly=True, states={'draft': [('readonly', False)]}) value = fields.Float(string='Gross Value', required=True, readonly=True, digits=0, states={'draft': [('readonly', False)]}, oldname='purchase_value') currency_id = fields.Many2one( 'res.currency', string='Currency', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env.user.company_id.currency_id.id) company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=lambda self: self.env['res.company']. _company_default_get('account.asset.asset')) note = fields.Text() category_id = fields.Many2one('account.asset.category', string='Category', required=True, change_default=True, readonly=True, states={'draft': [('readonly', False)]}) date = fields.Date(string='Date', required=True, readonly=True, states={'draft': [('readonly', False)]}, default=fields.Date.context_today, oldname="purchase_date") state = fields.Selection( [('draft', 'Draft'), ('open', 'Running'), ('close', 'Close')], 'Status', required=True, copy=False, default='draft', help="When an asset is created, the status is 'Draft'.\n" "If the asset is confirmed, the status goes in 'Running' and the depreciation lines can be posted in the accounting.\n" "You can manually close an asset when the depreciation is over. If the last line of depreciation is posted, the asset automatically goes in that status." ) active = fields.Boolean(default=True) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) method = fields.Selection( [('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, readonly=True, states={'draft': [('readonly', False)]}, default='linear', help= "Choose the method to use to compute the amount of depreciation lines.\n * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" " * Degressive: Calculated on basis of: Residual Value * Degressive Factor" ) method_number = fields.Integer( string='Number of Depreciations', readonly=True, states={'draft': [('readonly', False)]}, default=5, help="The number of depreciations needed to depreciate your asset") method_period = fields.Integer( string='Number of Months in a Period', required=True, readonly=True, default=12, states={'draft': [('readonly', False)]}, help="The amount of time between two depreciations, in months") method_end = fields.Date(string='Ending Date', readonly=True, states={'draft': [('readonly', False)]}) method_progress_factor = fields.Float( string='Degressive Factor', readonly=True, default=0.3, states={'draft': [('readonly', False)]}) value_residual = fields.Float(compute='_amount_residual', method=True, digits=0, string='Residual Value') method_time = fields.Selection( [('number', 'Number of Entries'), ('end', 'Ending Date')], string='Time Method', required=True, readonly=True, default='number', states={'draft': [('readonly', False)]}, help= "Choose the method to use to compute the dates and number of entries.\n" " * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n" " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond." ) prorata = fields.Boolean( string='Prorata Temporis', readonly=True, states={'draft': [('readonly', False)]}, help= 'Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first January / Start date of fiscal year' ) depreciation_line_ids = fields.One2many('account.asset.depreciation.line', 'asset_id', string='Depreciation Lines', readonly=True, states={ 'draft': [('readonly', False)], 'open': [('readonly', False)] }) salvage_value = fields.Float( string='Salvage Value', digits=0, readonly=True, states={'draft': [('readonly', False)]}, help="It is the amount you plan to have that you cannot depreciate.") invoice_id = fields.Many2one('account.invoice', string='Invoice', states={'draft': [('readonly', False)]}, copy=False) type = fields.Selection(related="category_id.type", string='Type', required=True) @api.multi def unlink(self): for asset in self: if asset.state in ['open', 'close']: raise UserError( _('You cannot delete a document is in %s state.') % (asset.state, )) for depreciation_line in asset.depreciation_line_ids: if depreciation_line.move_id: raise UserError( _('You cannot delete a document that contains posted entries.' )) return super(AccountAssetAsset, self).unlink() @api.multi def _get_last_depreciation_date(self): """ @param id: ids of a account.asset.asset objects @return: Returns a dictionary of the effective dates of the last depreciation entry made for given asset ids. If there isn't any, return the purchase date of this asset """ self.env.cr.execute( """ SELECT a.id as id, COALESCE(MAX(m.date),a.date) AS date FROM account_asset_asset a LEFT JOIN account_asset_depreciation_line rel ON (rel.asset_id = a.id) LEFT JOIN account_move m ON (rel.move_id = m.id) WHERE a.id IN %s GROUP BY a.id, m.date """, (tuple(self.ids), )) result = dict(self.env.cr.fetchall()) return result @api.model def _cron_generate_entries(self): self.compute_generated_entries(datetime.today()) @api.model def compute_generated_entries(self, date, asset_type=None): # Entries generated : one by grouped category and one by asset from ungrouped category created_move_ids = [] type_domain = [] if asset_type: type_domain = [('type', '=', asset_type)] ungrouped_assets = self.env['account.asset.asset'].search( type_domain + [('state', '=', 'open'), ('category_id.group_entries', '=', False)]) created_move_ids += ungrouped_assets._compute_entries( date, group_entries=False) for grouped_category in self.env['account.asset.category'].search( type_domain + [('group_entries', '=', True)]): assets = self.env['account.asset.asset'].search([ ('state', '=', 'open'), ('category_id', '=', grouped_category.id) ]) created_move_ids += assets._compute_entries(date, group_entries=True) return created_move_ids def _compute_board_amount(self, sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date): amount = 0 if sequence == undone_dotation_number: amount = residual_amount else: if self.method == 'linear': amount = amount_to_depr / (undone_dotation_number - len(posted_depreciation_line_ids)) if self.prorata: amount = amount_to_depr / self.method_number if sequence == 1: if self.method_period % 12 != 0: date = datetime.strptime(self.date, '%Y-%m-%d') month_days = calendar.monthrange( date.year, date.month)[1] days = month_days - date.day + 1 amount = (amount_to_depr / self.method_number) / month_days * days else: days = (self.company_id.compute_fiscalyear_dates( depreciation_date)['date_to'] - depreciation_date).days + 1 amount = (amount_to_depr / self.method_number) / total_days * days elif self.method == 'degressive': amount = residual_amount * self.method_progress_factor if self.prorata: if sequence == 1: if self.method_period % 12 != 0: date = datetime.strptime(self.date, '%Y-%m-%d') month_days = calendar.monthrange( date.year, date.month)[1] days = month_days - date.day + 1 amount = (residual_amount * self.method_progress_factor ) / month_days * days else: days = (self.company_id.compute_fiscalyear_dates( depreciation_date)['date_to'] - depreciation_date).days + 1 amount = (residual_amount * self.method_progress_factor ) / total_days * days return amount def _compute_board_undone_dotation_nb(self, depreciation_date, total_days): undone_dotation_number = self.method_number if self.method_time == 'end': end_date = datetime.strptime(self.method_end, DF).date() undone_dotation_number = 0 while depreciation_date <= end_date: depreciation_date = date( depreciation_date.year, depreciation_date.month, depreciation_date.day) + relativedelta( months=+self.method_period) undone_dotation_number += 1 if self.prorata: undone_dotation_number += 1 return undone_dotation_number @api.multi def compute_depreciation_board(self): self.ensure_one() posted_depreciation_line_ids = self.depreciation_line_ids.filtered( lambda x: x.move_check).sorted(key=lambda l: l.depreciation_date) unposted_depreciation_line_ids = self.depreciation_line_ids.filtered( lambda x: not x.move_check) # Remove old unposted depreciation lines. We cannot use unlink() with One2many field commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] if self.value_residual != 0.0: amount_to_depr = residual_amount = self.value_residual if self.prorata: # if we already have some previous validated entries, starting date is last entry + method perio if posted_depreciation_line_ids and posted_depreciation_line_ids[ -1].depreciation_date: last_depreciation_date = datetime.strptime( posted_depreciation_line_ids[-1].depreciation_date, DF).date() depreciation_date = last_depreciation_date + relativedelta( months=+self.method_period) else: depreciation_date = datetime.strptime( self._get_last_depreciation_date()[self.id], DF).date() else: # depreciation_date = 1st of January of purchase year if annual valuation, 1st of # purchase month in other cases if self.method_period >= 12: asset_date = datetime.strptime(self.date[:4] + '-01-01', DF).date() else: asset_date = datetime.strptime(self.date[:7] + '-01', DF).date() # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period if posted_depreciation_line_ids and posted_depreciation_line_ids[ -1].depreciation_date: last_depreciation_date = datetime.strptime( posted_depreciation_line_ids[-1].depreciation_date, DF).date() depreciation_date = last_depreciation_date + relativedelta( months=+self.method_period) else: depreciation_date = asset_date day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year total_days = (year % 4) and 365 or 366 undone_dotation_number = self._compute_board_undone_dotation_nb( depreciation_date, total_days) for x in range(len(posted_depreciation_line_ids), undone_dotation_number): sequence = x + 1 amount = self._compute_board_amount( sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date) amount = self.currency_id.round(amount) if float_is_zero(amount, precision_rounding=self.currency_id.rounding): continue residual_amount -= amount vals = { 'amount': amount, 'asset_id': self.id, 'sequence': sequence, 'name': (self.code or '') + '/' + str(sequence), 'remaining_value': residual_amount, 'depreciated_value': self.value - (self.salvage_value + residual_amount), 'depreciation_date': depreciation_date.strftime(DF), } commands.append((0, False, vals)) # Considering Depr. Period as months depreciation_date = date(year, month, day) + relativedelta( months=+self.method_period) day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year self.write({'depreciation_line_ids': commands}) return True @api.multi def validate(self): self.write({'state': 'open'}) fields = [ 'method', 'method_number', 'method_period', 'method_end', 'method_progress_factor', 'method_time', 'salvage_value', 'invoice_id', ] ref_tracked_fields = self.env['account.asset.asset'].fields_get(fields) for asset in self: tracked_fields = ref_tracked_fields.copy() if asset.method == 'linear': del (tracked_fields['method_progress_factor']) if asset.method_time != 'end': del (tracked_fields['method_end']) else: del (tracked_fields['method_number']) dummy, tracking_value_ids = asset._message_track( tracked_fields, dict.fromkeys(fields)) asset.message_post(subject=_('Asset created'), tracking_value_ids=tracking_value_ids) def _get_disposal_moves(self): move_ids = [] for asset in self: unposted_depreciation_line_ids = asset.depreciation_line_ids.filtered( lambda x: not x.move_check) if unposted_depreciation_line_ids: old_values = { 'method_end': asset.method_end, 'method_number': asset.method_number, } # Remove all unposted depr. lines commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] # Create a new depr. line with the residual amount and post it sequence = len(asset.depreciation_line_ids) - len( unposted_depreciation_line_ids) + 1 today = datetime.today().strftime(DF) vals = { 'amount': asset.value_residual, 'asset_id': asset.id, 'sequence': sequence, 'name': (asset.code or '') + '/' + str(sequence), 'remaining_value': 0, 'depreciated_value': asset.value - asset.salvage_value, # the asset is completely depreciated 'depreciation_date': today, } commands.append((0, False, vals)) asset.write({ 'depreciation_line_ids': commands, 'method_end': today, 'method_number': sequence }) tracked_fields = self.env['account.asset.asset'].fields_get( ['method_number', 'method_end']) changes, tracking_value_ids = asset._message_track( tracked_fields, old_values) if changes: asset.message_post(subject=_( 'Asset sold or disposed. Accounting entry awaiting for validation.' ), tracking_value_ids=tracking_value_ids) move_ids += asset.depreciation_line_ids[-1].create_move( post_move=False) return move_ids @api.multi def set_to_close(self): move_ids = self._get_disposal_moves() if move_ids: name = _('Disposal Move') view_mode = 'form' if len(move_ids) > 1: name = _('Disposal Moves') view_mode = 'tree,form' return { 'name': name, 'view_type': 'form', 'view_mode': view_mode, 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'target': 'current', 'res_id': move_ids[0], } @api.multi def set_to_draft(self): self.write({'state': 'draft'}) @api.one @api.depends('value', 'salvage_value', 'depreciation_line_ids.move_check', 'depreciation_line_ids.amount') def _amount_residual(self): total_amount = 0.0 for line in self.depreciation_line_ids: if line.move_check: total_amount += line.amount self.value_residual = self.value - total_amount - self.salvage_value @api.onchange('company_id') def onchange_company_id(self): self.currency_id = self.company_id.currency_id.id @api.multi @api.depends('depreciation_line_ids.move_id') def _entry_count(self): for asset in self: res = self.env['account.asset.depreciation.line'].search_count([ ('asset_id', '=', asset.id), ('move_id', '!=', False) ]) asset.entry_count = res or 0 @api.one @api.constrains('prorata', 'method_time') def _check_prorata(self): if self.prorata and self.method_time != 'number': raise ValidationError( _('Prorata temporis can be applied only for time method "number of depreciations".' )) @api.onchange('category_id') def onchange_category_id(self): vals = self.onchange_category_id_values(self.category_id.id) # We cannot use 'write' on an object that doesn't exist yet if vals: for k, v in vals['value'].items(): setattr(self, k, v) def onchange_category_id_values(self, category_id): if category_id: category = self.env['account.asset.category'].browse(category_id) return { 'value': { 'method': category.method, 'method_number': category.method_number, 'method_time': category.method_time, 'method_period': category.method_period, 'method_progress_factor': category.method_progress_factor, 'method_end': category.method_end, 'prorata': category.prorata, } } @api.onchange('method_time') def onchange_method_time(self): if self.method_time != 'number': self.prorata = False @api.multi def copy_data(self, default=None): if default is None: default = {} default['name'] = self.name + _(' (copy)') return super(AccountAssetAsset, self).copy_data(default) @api.multi def _compute_entries(self, date, group_entries=False): depreciation_ids = self.env['account.asset.depreciation.line'].search([ ('asset_id', 'in', self.ids), ('depreciation_date', '<=', date), ('move_check', '=', False) ]) if group_entries: return depreciation_ids.create_grouped_move() return depreciation_ids.create_move() @api.model def create(self, vals): asset = super(AccountAssetAsset, self.with_context(mail_create_nolog=True)).create(vals) asset.compute_depreciation_board() return asset @api.multi def write(self, vals): res = super(AccountAssetAsset, self).write(vals) if 'depreciation_line_ids' not in vals and 'state' not in vals: for rec in self: rec.compute_depreciation_board() return res @api.multi def open_entries(self): move_ids = [] for asset in self: for depreciation_line in asset.depreciation_line_ids: if depreciation_line.move_id: move_ids.append(depreciation_line.move_id.id) return { 'name': _('Journal Entries'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'account.move', 'view_id': False, 'type': 'ir.actions.act_window', 'domain': [('id', 'in', move_ids)], }
class FleetVehicle(models.Model): _inherit = 'fleet.vehicle' co2_fee = fields.Float(compute='_compute_co2_fee', string="CO2 Fee") total_depreciated_cost = fields.Float( compute='_compute_total_depreciated_cost', string="Total Cost (Depreciated)", track_visibility="onchange", help="This includes all the depreciated costs and the CO2 fee") total_cost = fields.Float( compute='_compute_total_cost', string="Total Cost", help="This include all the costs and the CO2 fee") fuel_type = fields.Selection(required=True, default='diesel') atn = fields.Float(compute='_compute_car_atn', string="ATN") acquisition_date = fields.Date(required=True) @api.depends('co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.recurring_cost_amount_depreciated') def _compute_total_depreciated_cost(self): for car in self: car.total_depreciated_cost = car.co2_fee + \ sum(car.log_contracts.filtered( lambda contract: contract.state == 'open' ).mapped('recurring_cost_amount_depreciated')) @api.depends('co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.cost_generated') def _compute_total_cost(self): for car in self: car.total_cost = car.co2_fee contracts = car.log_contracts.filtered( lambda contract: contract.state == 'open' and contract. cost_frequency != 'no') for contract in contracts: if contract.cost_frequency == "daily": car.total_cost += contract.cost_generated * 30.0 elif contract.cost_frequency == "weekly": car.total_cost += contract.cost_generated * 4.0 elif contract.cost_frequency == "monthly": car.total_cost += contract.cost_generated elif contract.cost_frequency == "yearly": car.total_cost += contract.cost_generated / 12.0 def _get_co2_fee(self, co2, fuel_type): fuel_coefficient = { 'diesel': 600, 'gasoline': 768, 'lpg': 990, 'electric': 0, 'hybrid': 600 } co2_fee = 0 if fuel_type and fuel_type != 'electric': if not co2: co2 = 165 if fuel_type in ['diesel', 'hybrid'] else 182 co2_fee = (((co2 * 9.0) - fuel_coefficient.get(fuel_type, 0)) * 144.97 / 114.08) / 12.0 return max(co2_fee, 26.47) @api.depends('co2', 'fuel_type') def _compute_co2_fee(self): for car in self: car.co2_fee = self._get_co2_fee(car.co2, car.fuel_type) @api.depends('fuel_type', 'car_value', 'acquisition_date', 'co2') def _compute_car_atn(self): for car in self: car.atn = car._get_car_atn(car.acquisition_date, car.car_value, car.fuel_type, car.co2) @api.depends('model_id', 'license_plate', 'log_contracts', 'acquisition_date', 'co2_fee', 'log_contracts', 'log_contracts.state', 'log_contracts.recurring_cost_amount_depreciated') def _compute_vehicle_name(self): super(FleetVehicle, self)._compute_vehicle_name() for vehicle in self: acquisition_date = vehicle._get_acquisition_date() vehicle.name += u" \u2022 " + str( round(vehicle.total_depreciated_cost, 2)) + u" \u2022 " + acquisition_date def _get_acquisition_date(self): self.ensure_one() return babel.dates.format_date( date=Datetime.from_string(self.acquisition_date), format='MMMM y', locale=self._context.get('lang') or 'en_US') def _get_car_atn(self, acquisition_date, car_value, fuel_type, co2): # Compute the correction coefficient from the age of the car now = Datetime.from_string(Datetime.now()) start = Datetime.from_string(acquisition_date) if start: number_of_month = ( now.year - start.year) * 12.0 + now.month - start.month + int( bool(now.day - start.day + 1)) if number_of_month <= 12: age_coefficient = 1.00 elif number_of_month <= 24: age_coefficient = 0.94 elif number_of_month <= 36: age_coefficient = 0.88 elif number_of_month <= 48: age_coefficient = 0.82 elif number_of_month <= 60: age_coefficient = 0.76 else: age_coefficient = 0.70 car_value = car_value * age_coefficient # Compute atn value from corrected car_value magic_coeff = 6.0 / 7.0 # Don't ask me why if fuel_type == 'electric': atn = 0.0 else: if fuel_type in ['diesel', 'hybrid']: reference = 88.0 else: reference = 107.0 if not co2: co2 = 195 if fuel_type in ['diesel', 'hybrid'] else 205 if co2 <= reference: atn = car_value * max(0.04, (0.055 - 0.001 * (reference - co2))) * magic_coeff else: atn = car_value * min(0.18, (0.055 + 0.001 * (co2 - reference))) * magic_coeff return max(1310, atn) / 12.0
class AccountAssetCategory(models.Model): _name = 'account.asset.category' _description = 'Asset category' active = fields.Boolean(default=True) name = fields.Char(required=True, index=True, string="Asset Type") account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') account_asset_id = fields.Many2one( 'account.account', string='Asset Account', required=True, domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)], help= "Account used to record the purchase of the asset at its original price." ) account_depreciation_id = fields.Many2one( 'account.account', string='Depreciation Entries: Asset Account', required=True, domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)], help= "Account used in the depreciation entries, to decrease the asset value." ) account_depreciation_expense_id = fields.Many2one( 'account.account', string='Depreciation Entries: Expense Account', required=True, domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)], oldname='account_income_recognition_id', help= "Account used in the periodical entries, to record a part of the asset as expense." ) journal_id = fields.Many2one('account.journal', string='Journal', required=True) company_id = fields.Many2one( 'res.company', string='Company', required=True, default=lambda self: self.env['res.company']._company_default_get( 'account.asset.category')) method = fields.Selection( [('linear', 'Linear'), ('degressive', 'Degressive')], string='Computation Method', required=True, default='linear', help= "Choose the method to use to compute the amount of depreciation lines.\n" " * Linear: Calculated on basis of: Gross Value / Number of Depreciations\n" " * Degressive: Calculated on basis of: Residual Value * Degressive Factor" ) method_number = fields.Integer( string='Number of Depreciations', default=5, help="The number of depreciations needed to depreciate your asset") method_period = fields.Integer( string='Period Length', default=1, help="State here the time between 2 depreciations, in months", required=True) method_progress_factor = fields.Float('Degressive Factor', default=0.3) method_time = fields.Selection( [('number', 'Number of Entries'), ('end', 'Ending Date')], string='Time Method', required=True, default='number', help= "Choose the method to use to compute the dates and number of entries.\n" " * Number of Entries: Fix the number of entries and the time between 2 depreciations.\n" " * Ending Date: Choose the time between 2 depreciations and the date the depreciations won't go beyond." ) method_end = fields.Date('Ending date') prorata = fields.Boolean( string='Prorata Temporis', help= 'Indicates that the first depreciation entry for this asset have to be done from the purchase date instead of the first of January' ) open_asset = fields.Boolean( string='Auto-confirm Assets', help= "Check this if you want to automatically confirm the assets of this category when created by invoices." ) group_entries = fields.Boolean( string='Group Journal Entries', help= "Check this if you want to group the generated entries by categories.") type = fields.Selection([('sale', 'Sale: Revenue Recognition'), ('purchase', 'Purchase: Asset')], required=True, index=True, default='purchase') @api.onchange('account_asset_id') def onchange_account_asset(self): if self.type == "purchase": self.account_depreciation_id = self.account_asset_id elif self.type == "sale": self.account_depreciation_expense_id = self.account_asset_id @api.onchange('type') def onchange_type(self): if self.type == 'sale': self.prorata = True self.method_period = 1 else: self.method_period = 12 @api.onchange('method_time') def _onchange_method_time(self): if self.method_time != 'number': self.prorata = False
class account_move(models.Model): _inherit = "account.move" def _get_document_data(self, cr, uid, ids, name, arg, context=None): """ TODO """ res = {} for record in self.browse(cr, uid, ids, context=context): document_number = False if record.model and record.res_id: document_number = self.pool[record.model].browse( cr, uid, record.res_id, context=context).document_number res[record.id] = document_number return res @api.depends( 'sii_document_number', 'name', 'document_class_id', 'document_class_id.doc_code_prefix', ) def _get_document_number(self): for r in self: if r.sii_document_number and r.document_class_id: document_number = (r.document_class_id.doc_code_prefix or '') + r.sii_document_number else: document_number = r.name r.document_number = document_number document_class_id = fields.Many2one( 'sii.document_class', string='Document Type', copy=False, readonly=True, states={'draft': [('readonly', False)]}, ) sii_document_number = fields.Char( string='Document Number', copy=False, readonly=True, states={'draft': [('readonly', False)]}, ) canceled = fields.Boolean( string="Canceled?", readonly=True, states={'draft': [('readonly', False)]}, ) iva_uso_comun = fields.Boolean( string="Iva Uso Común", readonly=True, states={'draft': [('readonly', False)]}, ) no_rec_code = fields.Selection( [('1', 'Compras destinadas a IVA a generar operaciones no gravados o exentas.' ), ('2', 'Facturas de proveedores registrados fuera de plazo.'), ('3', 'Gastos rechazados.'), ('4', 'Entregas gratuitas (premios, bonificaciones, etc.) recibidos.'), ('9', 'Otros.')], string="Código No recuperable", readonly=True, states={'draft': [('readonly', False)]}, ) # @TODO select 1 automático si es emisor 2Categoría document_number = fields.Char( compute='_get_document_number', string='Document Number', store=True, readonly=True, states={'draft': [('readonly', False)]}, ) sended = fields.Boolean( string="Enviado al SII", default=False, readonly=True, states={'draft': [('readonly', False)]}, ) factor_proporcionalidad = fields.Float( string="Factor proporcionalidad", default=0.00, readonly=True, states={'draft': [('readonly', False)]}, ) def _get_move_imps(self): imps = {} for l in self.line_ids: if l.tax_line_id: if l.tax_line_id: if not l.tax_line_id.id in imps: imps[l.tax_line_id.id] = { 'tax_id': l.tax_line_id.id, 'credit': 0, 'debit': 0, 'code': l.tax_line_id.sii_code } imps[l.tax_line_id.id]['credit'] += l.credit imps[l.tax_line_id.id]['debit'] += l.debit if l.tax_line_id.activo_fijo: ActivoFijo[1] += l.credit elif l.tax_ids and l.tax_ids[0].amount == 0: #caso monto exento if not l.tax_ids[0].id in imps: imps[l.tax_ids[0].id] = { 'tax_id': l.tax_ids[0].id, 'credit': 0, 'debit': 0, 'code': l.tax_ids[0].sii_code } imps[l.tax_ids[0].id]['credit'] += l.credit imps[l.tax_ids[0].id]['debit'] += l.debit return imps def totales_por_movimiento(self): move_imps = self._get_move_imps() imps = { 'iva': 0, 'exento': 0, 'otros_imps': 0, } for key, i in move_imps.items(): if i['code'] in [14]: imps['iva'] += (i['credit'] or i['debit']) elif i['code'] == 0: imps['exento'] += (i['credit'] or i['debit']) else: imps['otros_imps'] += (i['credit'] or i['debit']) imps['neto'] = self.amount - imps['otros_imps'] - imps[ 'exento'] - imps['iva'] return imps
class AccountAssetDepreciationLine(models.Model): _name = 'account.asset.depreciation.line' _description = 'Asset depreciation line' name = fields.Char(string='Depreciation Name', required=True, index=True) sequence = fields.Integer(required=True) asset_id = fields.Many2one('account.asset.asset', string='Asset', required=True, ondelete='cascade') parent_state = fields.Selection(related='asset_id.state', string='State of Asset') amount = fields.Float(string='Current Depreciation', digits=0, required=True) remaining_value = fields.Float(string='Next Period Depreciation', digits=0, required=True) depreciated_value = fields.Float(string='Cumulative Depreciation', required=True) depreciation_date = fields.Date('Depreciation Date', index=True) move_id = fields.Many2one('account.move', string='Depreciation Entry') move_check = fields.Boolean(compute='_get_move_check', string='Linked', track_visibility='always', store=True) move_posted_check = fields.Boolean(compute='_get_move_posted_check', string='Posted', track_visibility='always', store=True) @api.multi @api.depends('move_id') def _get_move_check(self): for line in self: line.move_check = bool(line.move_id) @api.multi @api.depends('move_id.state') def _get_move_posted_check(self): for line in self: line.move_posted_check = True if line.move_id and line.move_id.state == 'posted' else False @api.multi def create_move(self, post_move=True): created_moves = self.env['account.move'] prec = self.env['decimal.precision'].precision_get('Account') for line in self: category_id = line.asset_id.category_id depreciation_date = self.env.context.get( 'depreciation_date' ) or line.depreciation_date or fields.Date.context_today(self) company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id amount = current_currency.with_context( date=depreciation_date).compute(line.amount, company_currency) asset_name = line.asset_id.name + ' (%s/%s)' % ( line.sequence, len(line.asset_id.depreciation_line_ids)) move_line_1 = { 'name': asset_name, 'account_id': category_id.account_depreciation_id.id, 'debit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'credit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'journal_id': category_id.journal_id.id, 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, 'amount_currency': company_currency != current_currency and -1.0 * line.amount or 0.0, } move_line_2 = { 'name': asset_name, 'account_id': category_id.account_depreciation_expense_id.id, 'credit': 0.0 if float_compare(amount, 0.0, precision_digits=prec) > 0 else -amount, 'debit': amount if float_compare(amount, 0.0, precision_digits=prec) > 0 else 0.0, 'journal_id': category_id.journal_id.id, 'partner_id': line.asset_id.partner_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False, 'currency_id': company_currency != current_currency and current_currency.id or False, 'amount_currency': company_currency != current_currency and line.amount or 0.0, } move_vals = { 'ref': line.asset_id.code, 'date': depreciation_date or False, 'journal_id': category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } move = self.env['account.move'].create(move_vals) line.write({'move_id': move.id, 'move_check': True}) created_moves |= move if post_move and created_moves: created_moves.filtered(lambda m: any( m.asset_depreciation_ids.mapped( 'asset_id.category_id.open_asset'))).post() return [x.id for x in created_moves] @api.multi def create_grouped_move(self, post_move=True): if not self.exists(): return [] created_moves = self.env['account.move'] category_id = self[ 0].asset_id.category_id # we can suppose that all lines have the same category depreciation_date = self.env.context.get( 'depreciation_date') or fields.Date.context_today(self) amount = 0.0 for line in self: # Sum amount of all depreciation lines company_currency = line.asset_id.company_id.currency_id current_currency = line.asset_id.currency_id amount += current_currency.compute(line.amount, company_currency) name = category_id.name + _(' (grouped)') move_line_1 = { 'name': name, 'account_id': category_id.account_depreciation_id.id, 'debit': 0.0, 'credit': amount, 'journal_id': category_id.journal_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'sale' else False, } move_line_2 = { 'name': name, 'account_id': category_id.account_depreciation_expense_id.id, 'credit': 0.0, 'debit': amount, 'journal_id': category_id.journal_id.id, 'analytic_account_id': category_id.account_analytic_id.id if category_id.type == 'purchase' else False, } move_vals = { 'ref': category_id.name, 'date': depreciation_date or False, 'journal_id': category_id.journal_id.id, 'line_ids': [(0, 0, move_line_1), (0, 0, move_line_2)], } move = self.env['account.move'].create(move_vals) self.write({'move_id': move.id, 'move_check': True}) created_moves |= move if post_move and created_moves: self.post_lines_and_close_asset() created_moves.post() return [x.id for x in created_moves] @api.multi def post_lines_and_close_asset(self): # we re-evaluate the assets to determine whether we can close them for line in self: line.log_message_when_posted() asset = line.asset_id if asset.currency_id.is_zero(asset.value_residual): asset.message_post(body=_("Document closed.")) asset.write({'state': 'close'}) @api.multi def log_message_when_posted(self): def _format_message(message_description, tracked_values): message = '' if message_description: message = '<span>%s</span>' % message_description for name, values in tracked_values.items(): message += '<div> • <b>%s</b>: ' % name message += '%s</div>' % values return message for line in self: if line.move_id and line.move_id.state == 'draft': partner_name = line.asset_id.partner_id.name currency_name = line.asset_id.currency_id.name msg_values = { _('Currency'): currency_name, _('Amount'): line.amount } if partner_name: msg_values[_('Partner')] = partner_name msg = _format_message(_('Depreciation line posted.'), msg_values) line.asset_id.message_post(body=msg) @api.multi def unlink(self): for record in self: if record.move_check: if record.asset_id.category_id.type == 'purchase': msg = _("You cannot delete posted depreciation lines.") else: msg = _("You cannot delete posted installment lines.") raise UserError(msg) return super(AccountAssetDepreciationLine, self).unlink()
class SaleOrder(models.Model): _inherit = 'sale.order' timesheet_ids = fields.Many2many( 'account.analytic.line', compute='_compute_timesheet_ids', string='Timesheet activities associated to this sale') timesheet_count = fields.Float( string='Timesheet activities', compute='_compute_timesheet_ids', groups="hr_timesheet.group_hr_timesheet_user") # override domain project_id = fields.Many2one( domain= "['|', ('bill_type', '=', 'customer_task'), ('pricing_type', '=', 'fixed_rate'), ('analytic_account_id', '!=', False), ('company_id', '=', company_id)]" ) timesheet_encode_uom_id = fields.Many2one( 'uom.uom', related='company_id.timesheet_encode_uom_id') timesheet_total_duration = fields.Integer( "Timesheet Total Duration", compute='_compute_timesheet_total_duration', help= "Total recorded duration, expressed in the encoding UoM, and rounded to the unit" ) @api.depends('analytic_account_id.line_ids') def _compute_timesheet_ids(self): for order in self: if order.analytic_account_id: order.timesheet_ids = self.env['account.analytic.line'].search( [('so_line', 'in', order.order_line.ids), ('amount', '<=', 0.0), ('project_id', '!=', False)]) else: order.timesheet_ids = [] order.timesheet_count = len(order.timesheet_ids) @api.depends('timesheet_ids', 'company_id.timesheet_encode_uom_id') def _compute_timesheet_total_duration(self): for sale_order in self: timesheets = sale_order.timesheet_ids if self.user_has_groups( 'hr_timesheet.group_hr_timesheet_approver' ) else sale_order.timesheet_ids.filtered( lambda t: t.user_id.id == self.env.uid) total_time = 0.0 for timesheet in timesheets.filtered( lambda t: not t.non_allow_billable): # Timesheets may be stored in a different unit of measure, so first we convert all of them to the reference unit total_time += timesheet.unit_amount * timesheet.product_uom_id.factor_inv # Now convert to the proper unit of measure total_time *= sale_order.timesheet_encode_uom_id.factor sale_order.timesheet_total_duration = total_time def action_view_project_ids(self): self.ensure_one() # redirect to form or kanban view billable_projects = self.project_ids.filtered( lambda project: project.sale_line_id) if len(billable_projects) == 1 and self.env.user.has_group( 'project.group_project_manager'): action = billable_projects[0].action_view_timesheet_plan() else: action = super().action_view_project_ids() return action def action_view_timesheet(self): self.ensure_one() action = self.env["ir.actions.actions"]._for_xml_id( "sale_timesheet.timesheet_action_from_sales_order") action['context'] = { 'search_default_billable_timesheet': True } # erase default filters if self.timesheet_count > 0: action['domain'] = [('so_line', 'in', self.order_line.ids)] else: action = {'type': 'ir.actions.act_window_close'} return action def _create_invoices(self, grouped=False, final=False, date=None): """Link timesheets to the created invoices. Date interval is injected in the context in sale_make_invoice_advance_inv wizard. """ moves = super()._create_invoices(grouped=grouped, final=final, date=date) moves._link_timesheets_to_invoice( self.env.context.get("timesheet_start_date"), self.env.context.get("timesheet_end_date")) return moves
class biaya_registrasi(models.Model): _name = 'siswa_psb_ocb11.biaya_registrasi' name = fields.Char('Name', default=lambda self: str(self.tahunajaran_id.name) + " " + str(self.jenjang_id.name)) tahunajaran_id = fields.Many2one('siswa_ocb11.tahunajaran', string="Tahun Ajaran", required=True, ondelete="cascade") jenjang_id = fields.Many2one('siswa_ocb11.jenjang', string="Jenjang", required=True, ondelete="cascade") biaya_ta_jenjang_ids = fields.Many2many( 'siswa_keu_ocb11.biaya_ta_jenjang', relation='siswa_registrasi_biaya_ta_jenjang_rel', column1='biaya_registrasi_id', column2='biaya_ta_jenjang_id', string="Biaya") total_biaya = fields.Float('Total', default=0.0) total_biaya_alt = fields.Float('Total (alt)', default=0.0) @api.multi def write(self, vals): pprint(vals) if 'biaya_ta_jenjang_ids' in vals: by_ta_jj_ids = vals['biaya_ta_jenjang_ids'][0][2] get_biaya_ta_jenjang_ids = self.env[ 'siswa_keu_ocb11.biaya_ta_jenjang'].search([('id', 'in', by_ta_jj_ids)]) total_biaya_val = 0 total_biaya_alt_val = 0 for biaya in get_biaya_ta_jenjang_ids: total_biaya_val += biaya.harga total_biaya_alt_val += biaya.harga_alt vals['total_biaya'] = total_biaya_val vals['total_biaya_alt'] = total_biaya_alt_val # print('ada perubahan') # data_vals = vals['biaya_ta_jenjang_ids'][0][2] # for data in data_vals: # print(data) # # update total_biaya # print('Update total biaya') # biaya_reg_id = self.env['siswa_psb_ocb11.biaya_registrasi'].search([('id', '=', self.id)]) # for by_reg in biaya_reg_id: # if by_reg.biaya_ta_jenjang_ids: # total_biaya_val = 0 # total_biaya_alt_val = 0 # # for biaya in by_reg.biaya_ta_jenjang_ids: # total_biaya_val += biaya.harga # total_biaya_alt_val += biaya.harga_alt # print(biaya.biaya_id.name) # print(biaya.harga) # print(biaya.harga_alt) # print('------------------------------') # # print('Total Biaya : ' + str(total_biaya_val)) # print('Total Biaya : ' + str(total_biaya_alt_val)) # # vals['total_biaya'] = total_biaya_val # vals['total_biaya_alt'] = total_biaya_alt_val result = super(biaya_registrasi, self).write(vals) return result @api.model def create(self, vals): tahunajaran = self.env['siswa_ocb11.tahunajaran'] jenjang = self.env['siswa_ocb11.jenjang'] vals['name'] = str( tahunajaran.search( [('id', '=', vals['tahunajaran_id'])]).name) + " " + str( jenjang.search([('id', '=', vals['jenjang_id'])]).name) result = super(biaya_registrasi, self).create(vals) return result @api.onchange('tahunajaran_id') def pembayaran_id_onchange(self): return self.recompute_biaya_ta_jenjang() @api.onchange('jenjang_id') def jenjang_id_onchange(self): return self.recompute_biaya_ta_jenjang() def recompute_biaya_ta_jenjang(self): domain = { 'biaya_ta_jenjang_ids': [('tahunajaran_id', '=', self.tahunajaran_id.id), ('jenjang_id', '=', self.jenjang_id.id)] } return {'domain': domain, 'value': {'biaya_ta_jenjang_ids': []}} def generate_biaya_registrasi(self): print('Generate Biaya Registrasi') get_biaya_ta_jenjang_ids = self.env[ 'siswa_keu_ocb11.biaya_ta_jenjang'].search([ ('tahunajaran_id', '=', self.tahunajaran_id.id), ('jenjang_id', '=', self.jenjang_id.id), ]) # self.biaya_ta_jenjang_ids = [(0, 0, get_biaya_ta_jenjang_ids )] biaya_ids = [] for by in get_biaya_ta_jenjang_ids: biaya_ids.append(by.id) self.biaya_ta_jenjang_ids = [(6, 0, biaya_ids)] @api.model def generate_init_on_install(self): tahunajaran_ids = self.env['siswa_ocb11.tahunajaran'].search([ ('active', 'ilike', '%') ]) jenjang_ids = self.env['siswa_ocb11.jenjang'].search([('id', 'ilike', '%')]) print('---------------------------------------') print('GENERATE INIT BIAYA REGISTRASI ON INSTALLING MODULE ') print('---------------------------------------') for ta in tahunajaran_ids: for jj in jenjang_ids: # print(jj.name + ' ' + ta.name) biaya_registrasi_ids = self.env[ 'siswa_psb_ocb11.biaya_registrasi'].search([ ('tahunajaran_id', '=', ta.id), ('jenjang_id', '=', jj.id), ]) if not biaya_registrasi_ids: # create new data new_biaya_registrasi = self.env[ 'siswa_psb_ocb11.biaya_registrasi'].create({ 'tahunajaran_id': ta.id, 'jenjang_id': jj.id }) new_biaya_registrasi.recompute_biaya_ta_jenjang()