class ResourceResource(models.Model): _name = "resource.resource" _description = "Resources" @api.model def default_get(self, fields): res = super(ResourceResource, self).default_get(fields) if not res.get('calendar_id') and res.get('company_id'): company = self.env['res.company'].browse(res['company_id']) res['calendar_id'] = company.resource_calendar_id.id return res name = fields.Char(required=True) active = fields.Boolean( 'Active', default=True, tracking=True, help= "If the active field is set to False, it will allow you to hide the resource record without removing it." ) company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.company) resource_type = fields.Selection([('user', 'Human'), ('material', 'Material')], string='Resource Type', default='user', required=True) user_id = fields.Many2one( 'res.users', string='User', help='Related user name for the resource to manage its access.') time_efficiency = fields.Float( 'Efficiency Factor', default=100, required=True, help= "This field is used to calculate the the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100%, then the expected duration will be one hour. If the efficiency factor is 200%, however the expected duration will be 30 minutes." ) calendar_id = fields.Many2one( "resource.calendar", string='Working Time', default=lambda self: self.env.company.resource_calendar_id, required=True, help="Define the schedule of resource") tz = fields.Selection( _tz_get, string='Timezone', required=True, default=lambda self: self._context.get('tz' ) or self.env.user.tz or 'UTC', help= "This field is used in order to define in which timezone the resources will work." ) _sql_constraints = [ ('check_time_efficiency', 'CHECK(time_efficiency>0)', 'Time efficiency must be strictly positive'), ] @api.constrains('time_efficiency') def _check_time_efficiency(self): for record in self: if record.time_efficiency == 0: raise ValidationError( _('The efficiency factor cannot be equal to 0.')) @api.model def create(self, values): if values.get('company_id') and not values.get('calendar_id'): values['calendar_id'] = self.env['res.company'].browse( values['company_id']).resource_calendar_id.id if not values.get('tz'): # retrieve timezone on user or calendar tz = (self.env['res.users'].browse(values.get('user_id')).tz or self.env['resource.calendar'].browse( values.get('calendar_id')).tz) if tz: values['tz'] = tz return super(ResourceResource, self).create(values) @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() if default is None: default = {} if not default.get('name'): default.update(name=_('%s (copy)') % (self.name)) return super(ResourceResource, self).copy(default) @api.onchange('company_id') def _onchange_company_id(self): if self.company_id: self.calendar_id = self.company_id.resource_calendar_id.id @api.onchange('user_id') def _onchange_user_id(self): if self.user_id: self.tz = self.user_id.tz def _get_work_interval(self, start, end): """ Return interval's start datetime for interval closest to start. And interval's end datetime for interval closest to end. If none is found return None Note: this method is used in enterprise (forecast and planning) :start: datetime :end: datetime :return: (datetime|None, datetime|None) """ start_datetime = timezone_datetime(start) end_datetime = timezone_datetime(end) resource_mapping = {} for resource in self: work_intervals = sorted(resource.calendar_id._work_intervals( start_datetime, end_datetime, resource), key=lambda x: x[0]) if work_intervals: resource_mapping[resource.id] = ( work_intervals[0][0].astimezone(utc), work_intervals[-1][1].astimezone(utc)) else: resource_mapping[resource.id] = (None, None) return resource_mapping def _get_unavailable_intervals(self, start, end): """ Compute the intervals during which employee is unavailable with hour granularity between start and end Note: this method is used in enterprise (forecast and planning) """ start_datetime = timezone_datetime(start) end_datetime = timezone_datetime(end) resource_mapping = {} for resource in self: calendar = resource.calendar_id resource_work_intervals = calendar._work_intervals( start_datetime, end_datetime, resource) resource_work_intervals = [ (start, stop) for start, stop, meta in resource_work_intervals ] # start + flatten(intervals) + end resource_work_intervals = [start_datetime] + list( chain.from_iterable(resource_work_intervals)) + [end_datetime] # put it back to UTC resource_work_intervals = list( map(lambda dt: dt.astimezone(utc), resource_work_intervals)) # pick groups of two resource_work_intervals = list( zip(resource_work_intervals[0::2], resource_work_intervals[1::2])) resource_mapping[resource.id] = resource_work_intervals return resource_mapping
class AccountFrFec(models.TransientModel): _name = 'account.fr.fec' _description = 'Ficher Echange Informatise' date_from = fields.Date(string='Start Date', required=True) date_to = fields.Date(string='End Date', required=True) fec_data = fields.Binary('FEC File', readonly=True) filename = fields.Char(string='Filename', size=256, readonly=True) export_type = fields.Selection([ ('official', 'Official FEC report (posted entries only)'), ('nonofficial', 'Non-official FEC report (posted and unposted entries)'), ], string='Export Type', required=True, default='official') def do_query_unaffected_earnings(self): ''' Compute the sum of ending balances for all accounts that are of a type that does not bring forward the balance in new fiscal years. This is needed because we have to display only one line for the initial balance of all expense/revenue accounts in the FEC. ''' sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, '120/129' AS CompteNum, 'Benefice (perte) reporte(e)' AS CompteLib, '' AS CompAuxNum, '' AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN COALESCE(sum(aml.balance), 0) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN COALESCE(sum(aml.balance), 0) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 'f' AND (aml.debit != 0 OR aml.credit != 0) ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' company = self.env.user.company_id formatted_date_from = fields.Date.to_string(self.date_from).replace( '-', '') date_from = self.date_from formatted_date_year = date_from.year self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) listrow = [] row = self._cr.fetchone() listrow = list(row) return listrow def _get_company_legal_data(self, company): """ Dom-Tom are excluded from the EU's fiscal territory Those regions do not have SIREN sources: https://www.service-public.fr/professionnels-entreprises/vosdroits/F23570 http://www.douane.gouv.fr/articles/a11024-tva-dans-les-dom """ dom_tom_group = self.env.ref('l10n_fr.dom-tom') is_dom_tom = company.country_id.code in dom_tom_group.country_ids.mapped( 'code') if not is_dom_tom and not company.vat: raise Warning( _("Missing VAT number for company %s") % company.name) if not is_dom_tom and company.vat[0:2] != 'FR': raise Warning(_("FEC is for French companies only !")) return { 'siren': company.vat[4:13] if not is_dom_tom else '', } @api.multi def generate_fec(self): self.ensure_one() # We choose to implement the flat file instead of the XML # file for 2 reasons : # 1) the XSD file impose to have the label on the account.move # but Eagle has the label on the account.move.line, so that's a # problem ! # 2) CSV files are easier to read/use for a regular accountant. # So it will be easier for the accountant to check the file before # sending it to the fiscal administration company = self.env.user.company_id company_legal_data = self._get_company_legal_data(company) header = [ u'JournalCode', # 0 u'JournalLib', # 1 u'EcritureNum', # 2 u'EcritureDate', # 3 u'CompteNum', # 4 u'CompteLib', # 5 u'CompAuxNum', # 6 We use partner.id u'CompAuxLib', # 7 u'PieceRef', # 8 u'PieceDate', # 9 u'EcritureLib', # 10 u'Debit', # 11 u'Credit', # 12 u'EcritureLet', # 13 u'DateLet', # 14 u'ValidDate', # 15 u'Montantdevise', # 16 u'Idevise', # 17 ] rows_to_write = [header] # INITIAL BALANCE unaffected_earnings_xml_ref = self.env.ref( 'account.data_unaffected_earnings') unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once if unaffected_earnings_xml_ref: #compute the benefit/loss of last year to add in the initial balance of the current year earnings account unaffected_earnings_results = self.do_query_unaffected_earnings() unaffected_earnings_line = False sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(replace(MIN(aa.name), '|', '/'), '\t', '') AS CompteLib, '' AS CompAuxNum, '' AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 't' AND (aml.debit != 0 OR aml.credit != 0) ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' GROUP BY aml.account_id, aat.type HAVING round(sum(aml.balance), %s) != 0 AND aat.type not in ('receivable', 'payable') ''' formatted_date_from = fields.Date.to_string(self.date_from).replace( '-', '') date_from = self.date_from formatted_date_year = date_from.year currency_digits = 2 self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id, currency_digits)) for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() if not unaffected_earnings_line: account = self.env['account.account'].browse(account_id) if account.user_type_id.id == self.env.ref( 'account.data_unaffected_earnings').id: #add the benefit/loss of previous fiscal year to the first unaffected earnings account found. unaffected_earnings_line = True current_amount = float(listrow[11].replace( ',', '.')) - float(listrow[12].replace(',', '.')) unaffected_earnings_amount = float( unaffected_earnings_results[11].replace( ',', '.')) - float( unaffected_earnings_results[12].replace( ',', '.')) listrow_amount = current_amount + unaffected_earnings_amount if float_is_zero(listrow_amount, precision_digits=currency_digits): continue if listrow_amount > 0: listrow[11] = str(listrow_amount).replace('.', ',') listrow[12] = '0,00' else: listrow[11] = '0,00' listrow[12] = str(-listrow_amount).replace('.', ',') rows_to_write.append(listrow) #if the unaffected earnings account wasn't in the selection yet: add it manually if (not unaffected_earnings_line and unaffected_earnings_results and (unaffected_earnings_results[11] != '0,00' or unaffected_earnings_results[12] != '0,00')): #search an unaffected earnings account unaffected_earnings_account = self.env['account.account'].search( [('user_type_id', '=', self.env.ref('account.data_unaffected_earnings').id)], limit=1) if unaffected_earnings_account: unaffected_earnings_results[ 4] = unaffected_earnings_account.code unaffected_earnings_results[ 5] = unaffected_earnings_account.name rows_to_write.append(unaffected_earnings_results) # INITIAL BALANCE - receivable/payable sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(MIN(aa.name), '|', '/') AS CompteLib, CASE WHEN rp.ref IS null OR rp.ref = '' THEN COALESCE('ID ' || rp.id, '') ELSE replace(rp.ref, '|', '/') END AS CompAuxNum, COALESCE(replace(rp.name, '|', '/'), '') AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 't' AND (aml.debit != 0 OR aml.credit != 0) ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' GROUP BY aml.account_id, aat.type, rp.ref, rp.id HAVING round(sum(aml.balance), %s) != 0 AND aat.type in ('receivable', 'payable') ''' self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id, currency_digits)) for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() rows_to_write.append(listrow) # LINES sql_query = ''' SELECT replace(replace(aj.code, '|', '/'), '\t', '') AS JournalCode, replace(replace(aj.name, '|', '/'), '\t', '') AS JournalLib, replace(replace(am.name, '|', '/'), '\t', '') AS EcritureNum, TO_CHAR(am.date, 'YYYYMMDD') AS EcritureDate, aa.code AS CompteNum, replace(replace(aa.name, '|', '/'), '\t', '') AS CompteLib, CASE WHEN rp.ref IS null OR rp.ref = '' THEN COALESCE('ID ' || rp.id, '') ELSE replace(rp.ref, '|', '/') END AS CompAuxNum, COALESCE(replace(replace(rp.name, '|', '/'), '\t', ''), '') AS CompAuxLib, CASE WHEN am.ref IS null OR am.ref = '' THEN '-' ELSE replace(replace(am.ref, '|', '/'), '\t', '') END AS PieceRef, TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' WHEN aml.name SIMILAR TO '[\t|\s|\n]*' THEN '/' ELSE replace(replace(replace(replace(aml.name, '|', '/'), '\t', ''), '\n', ''), '\r', '') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet, TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate, CASE WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN '' ELSE replace(to_char(aml.amount_currency, '000000000000000D99'), '.', ',') END AS Montantdevise, CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_journal aj ON aj.id = am.journal_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN res_currency rc ON rc.id = aml.currency_id LEFT JOIN account_full_reconcile rec ON rec.id = aml.full_reconcile_id WHERE am.date >= %s AND am.date <= %s AND am.company_id = %s AND (aml.debit != 0 OR aml.credit != 0) ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' ORDER BY am.date, am.name, aml.id ''' self._cr.execute(sql_query, (self.date_from, self.date_to, company.id)) for row in self._cr.fetchall(): rows_to_write.append(list(row)) fecvalue = self._csv_write_rows(rows_to_write) end_date = fields.Date.to_string(self.date_to).replace('-', '') suffix = '' if self.export_type == "nonofficial": suffix = '-NONOFFICIAL' self.write({ 'fec_data': base64.encodestring(fecvalue), # Filename = <siren>FECYYYYMMDD where YYYMMDD is the closing date 'filename': '%sFEC%s%s.csv' % (company_legal_data['siren'], end_date, suffix), }) action = { 'name': 'FEC', 'type': 'ir.actions.act_url', 'url': "web/content/?model=account.fr.fec&id=" + str(self.id) + "&filename_field=filename&field=fec_data&download=true&filename=" + self.filename, 'target': 'self', } return action def _csv_write_rows(self, rows, lineterminator=u'\r\n'): """ Write FEC rows into a file It seems that Bercy's bureaucracy is not too happy about the empty new line at the End Of File. @param {list(list)} rows: the list of rows. Each row is a list of strings @param {unicode string} [optional] lineterminator: effective line terminator Has nothing to do with the csv writer parameter The last line written won't be terminated with it @return the value of the file """ fecfile = io.BytesIO() writer = pycompat.csv_writer(fecfile, delimiter='|', lineterminator='') rows_length = len(rows) for i, row in enumerate(rows): if not i == rows_length - 1: row[-1] += lineterminator writer.writerow(row) fecvalue = fecfile.getvalue() fecfile.close() return fecvalue
class PurchaseOrder(models.Model): _name = "purchase.order" _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] _description = "Purchase Order" _order = 'date_order desc, id desc' @api.depends('order_line.price_total') def _amount_all(self): for order in self: amount_untaxed = amount_tax = 0.0 for line in order.order_line: amount_untaxed += line.price_subtotal amount_tax += line.price_tax order.update({ 'amount_untaxed': order.currency_id.round(amount_untaxed), 'amount_tax': order.currency_id.round(amount_tax), 'amount_total': amount_untaxed + amount_tax, }) @api.depends('state', 'order_line.qty_invoiced', 'order_line.qty_received', 'order_line.product_qty') def _get_invoiced(self): precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') for order in self: if order.state not in ('purchase', 'done'): order.invoice_status = 'no' continue if any(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) == -1 for line in order.order_line): order.invoice_status = 'to invoice' elif all(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) >= 0 for line in order.order_line) and order.invoice_ids: order.invoice_status = 'invoiced' else: order.invoice_status = 'no' @api.depends('order_line.invoice_lines.move_id') def _compute_invoice(self): for order in self: invoices = order.mapped('order_line.invoice_lines.move_id') order.invoice_ids = invoices order.invoice_count = len(invoices) READONLY_STATES = { 'purchase': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)], } name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New') origin = fields.Char('Source Document', copy=False, help="Reference of the document that generated this purchase order " "request (e.g. a sales order)") partner_ref = fields.Char('Vendor Reference', copy=False, help="Reference of the sales order or bid sent by the vendor. " "It's used to do the matching when you receive the " "products as this reference is usually written on the " "delivery order sent by your vendor.") date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\ help="Depicts the date where the Quotation should be validated and converted into a purchase order.") date_approve = fields.Datetime('Confirmation Date', readonly=1, index=True, copy=False) partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, tracking=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="You can find a vendor by its Name, TIN, Email or Internal Reference.") dest_address_id = fields.Many2one('res.partner', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", string='Drop Ship Address', states=READONLY_STATES, help="Put an address if you want to deliver directly from the vendor to the customer. " "Otherwise, keep empty to deliver to your own company.") currency_id = fields.Many2one('res.currency', 'Currency', required=True, states=READONLY_STATES, default=lambda self: self.env.company.currency_id.id) state = fields.Selection([ ('draft', 'RFQ'), ('sent', 'RFQ Sent'), ('to approve', 'To Approve'), ('purchase', 'Purchase Order'), ('done', 'Locked'), ('cancel', 'Cancelled') ], string='Status', readonly=True, index=True, copy=False, default='draft', tracking=True) order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True) notes = fields.Text('Terms and Conditions') invoice_count = fields.Integer(compute="_compute_invoice", string='Bill Count', copy=False, default=0, store=True) invoice_ids = fields.Many2many('account.move', compute="_compute_invoice", string='Bills', copy=False, store=True) invoice_status = fields.Selection([ ('no', 'Nothing to Bill'), ('to invoice', 'Waiting Bills'), ('invoiced', 'Fully Billed'), ], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no') # There is no inverse function on purpose since the date may be different on each line date_planned = fields.Datetime(string='Receipt Date', index=True) amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', tracking=True) amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all') amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all') fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") payment_term_id = fields.Many2one('account.payment.term', 'Payment Terms', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]") incoterm_id = fields.Many2one('account.incoterms', 'Incoterm', states={'done': [('readonly', True)]}, help="International Commercial Terms are a series of predefined commercial terms used in international transactions.") product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product', readonly=False) user_id = fields.Many2one( 'res.users', string='Purchase Representative', index=True, tracking=True, default=lambda self: self.env.user, check_company=True) company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.company.id) currency_rate = fields.Float("Currency Rate", compute='_compute_currency_rate', compute_sudo=True, store=True, readonly=True, help='Ratio between the purchase order currency and the company currency') @api.constrains('company_id', 'order_line') def _check_order_line_company_id(self): for order in self: companies = order.order_line.product_id.company_id if companies and companies != order.company_id: bad_products = order.order_line.product_id.filtered(lambda p: p.company_id and p.company_id != order.company_id) raise ValidationError((_("Your quotation contains products from company %s whereas your quotation belongs to company %s. \n Please change the company of your quotation or remove the products from other companies (%s).") % (', '.join(companies.mapped('display_name')), order.company_id.display_name, ', '.join(bad_products.mapped('display_name'))))) def _compute_access_url(self): super(PurchaseOrder, self)._compute_access_url() for order in self: order.access_url = '/my/purchase/%s' % (order.id) @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): args = args or [] domain = [] if name: domain = ['|', ('name', operator, name), ('partner_ref', operator, name)] purchase_order_ids = self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid) return models.lazy_name_get(self.browse(purchase_order_ids).with_user(name_get_uid)) @api.depends('date_order', 'currency_id', 'company_id', 'company_id.currency_id') def _compute_currency_rate(self): for order in self: order.currency_rate = self.env['res.currency']._get_conversion_rate(order.company_id.currency_id, order.currency_id, order.company_id, order.date_order) @api.depends('name', 'partner_ref') def name_get(self): result = [] for po in self: name = po.name if po.partner_ref: name += ' (' + po.partner_ref + ')' if self.env.context.get('show_total_amount') and po.amount_total: name += ': ' + formatLang(self.env, po.amount_total, currency_obj=po.currency_id) result.append((po.id, name)) return result @api.model def create(self, vals): if vals.get('name', 'New') == 'New': seq_date = None if 'date_order' in vals: seq_date = fields.Datetime.context_timestamp(self, fields.Datetime.to_datetime(vals['date_order'])) vals['name'] = self.env['ir.sequence'].next_by_code('purchase.order', sequence_date=seq_date) or '/' return super(PurchaseOrder, self).create(vals) def write(self, vals): res = super(PurchaseOrder, self).write(vals) if vals.get('date_planned'): self.order_line.filtered(lambda line: not line.display_type).date_planned = vals['date_planned'] return res def unlink(self): for order in self: if not order.state == 'cancel': raise UserError(_('In order to delete a purchase order, you must cancel it first.')) return super(PurchaseOrder, self).unlink() def copy(self, default=None): new_po = super(PurchaseOrder, self).copy(default=default) for line in new_po.order_line: if new_po.date_planned: line.date_planned = new_po.date_planned elif line.product_id: seller = line.product_id._select_seller( partner_id=line.partner_id, quantity=line.product_qty, date=line.order_id.date_order and line.order_id.date_order.date(), uom_id=line.product_uom) line.date_planned = line._get_date_planned(seller) return new_po def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'purchase': return self.env.ref('purchase.mt_rfq_approved') elif 'state' in init_values and self.state == 'to approve': return self.env.ref('purchase.mt_rfq_confirmed') elif 'state' in init_values and self.state == 'done': return self.env.ref('purchase.mt_rfq_done') return super(PurchaseOrder, self)._track_subtype(init_values) @api.onchange('partner_id', 'company_id') def onchange_partner_id(self): # Ensures all properties and fiscal positions # are taken with the company of the order # if not defined, force_company doesn't change anything. self = self.with_context(force_company=self.company_id.id) if not self.partner_id: self.fiscal_position_id = False self.payment_term_id = False self.currency_id = self.env.company.currency_id.id else: self.fiscal_position_id = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id) self.payment_term_id = self.partner_id.property_supplier_payment_term_id.id self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.company.currency_id.id return {} @api.onchange('fiscal_position_id') def _compute_tax_id(self): """ Trigger the recompute of the taxes if the fiscal position is changed on the PO. """ for order in self: order.order_line._compute_tax_id() @api.onchange('partner_id') def onchange_partner_id_warning(self): if not self.partner_id or not self.env.user.has_group('purchase.group_warning_purchase'): return warning = {} title = False message = False partner = self.partner_id # If partner has no warning, check its company if partner.purchase_warn == 'no-message' and partner.parent_id: partner = partner.parent_id if partner.purchase_warn and partner.purchase_warn != 'no-message': # Block if partner only has warning but parent company is blocked if partner.purchase_warn != 'block' and partner.parent_id and partner.parent_id.purchase_warn == 'block': partner = partner.parent_id title = _("Warning for %s") % partner.name message = partner.purchase_warn_msg warning = { 'title': title, 'message': message } if partner.purchase_warn == 'block': self.update({'partner_id': False}) return {'warning': warning} return {} def action_rfq_send(self): ''' This function opens a window to compose an email, with the edi purchase template message loaded by default ''' self.ensure_one() ir_model_data = self.env['ir.model.data'] try: if self.env.context.get('send_rfq', False): template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase')[1] else: template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase_done')[1] except ValueError: template_id = False try: compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1] except ValueError: compose_form_id = False ctx = dict(self.env.context or {}) ctx.update({ 'default_model': 'purchase.order', 'active_model': 'purchase.order', 'active_id': self.ids[0], 'default_res_id': self.ids[0], 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_composition_mode': 'comment', 'custom_layout': "mail.mail_notification_paynow", 'force_email': True, 'mark_rfq_as_sent': True, }) # In the case of a RFQ or a PO, we want the "View..." button in line with the state of the # object. Therefore, we pass the model description in the context, in the language in which # the template is rendered. lang = self.env.context.get('lang') if {'default_template_id', 'default_model', 'default_res_id'} <= ctx.keys(): template = self.env['mail.template'].browse(ctx['default_template_id']) if template and template.lang: lang = template._render_template(template.lang, ctx['default_model'], ctx['default_res_id']) self = self.with_context(lang=lang) if self.state in ['draft', 'sent']: ctx['model_description'] = _('Request for Quotation') else: ctx['model_description'] = _('Purchase Order') return { 'name': _('Compose Email'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form_id, 'form')], 'view_id': compose_form_id, 'target': 'new', 'context': ctx, } @api.returns('mail.message', lambda value: value.id) def message_post(self, **kwargs): if self.env.context.get('mark_rfq_as_sent'): self.filtered(lambda o: o.state == 'draft').write({'state': 'sent'}) return super(PurchaseOrder, self.with_context(mail_post_autofollow=True)).message_post(**kwargs) def print_quotation(self): self.write({'state': "sent"}) return self.env.ref('purchase.report_purchase_quotation').report_action(self) def button_approve(self, force=False): self.write({'state': 'purchase', 'date_approve': fields.Date.context_today(self)}) self.filtered(lambda p: p.company_id.po_lock == 'lock').write({'state': 'done'}) return {} def button_draft(self): self.write({'state': 'draft'}) return {} def button_confirm(self): for order in self: if order.state not in ['draft', 'sent']: continue order._add_supplier_to_product() # Deal with double validation process if order.company_id.po_double_validation == 'one_step'\ or (order.company_id.po_double_validation == 'two_step'\ and order.amount_total < self.env.company.currency_id._convert( order.company_id.po_double_validation_amount, order.currency_id, order.company_id, order.date_order or fields.Date.today()))\ or order.user_has_groups('purchase.group_purchase_manager'): order.button_approve() else: order.write({'state': 'to approve'}) return True def button_cancel(self): for order in self: for inv in order.invoice_ids: if inv and inv.state not in ('cancel', 'draft'): raise UserError(_("Unable to cancel this purchase order. You must first cancel the related vendor bills.")) self.write({'state': 'cancel'}) def button_unlock(self): self.write({'state': 'purchase'}) def button_done(self): self.write({'state': 'done'}) def _add_supplier_to_product(self): # Add the partner in the supplier list of the product if the supplier is not registered for # this product. We limit to 10 the number of suppliers for a product to avoid the mess that # could be caused for some generic products ("Miscellaneous"). for line in self.order_line: # Do not add a contact as a supplier partner = self.partner_id if not self.partner_id.parent_id else self.partner_id.parent_id if line.product_id and partner not in line.product_id.seller_ids.mapped('name') and len(line.product_id.seller_ids) <= 10: # Convert the price in the right currency. currency = partner.property_purchase_currency_id or self.env.company.currency_id price = self.currency_id._convert(line.price_unit, currency, line.company_id, line.date_order or fields.Date.today(), round=False) # Compute the price for the template's UoM, because the supplier's UoM is related to that UoM. if line.product_id.product_tmpl_id.uom_po_id != line.product_uom: default_uom = line.product_id.product_tmpl_id.uom_po_id price = line.product_uom._compute_price(price, default_uom) supplierinfo = { 'name': partner.id, 'sequence': max(line.product_id.seller_ids.mapped('sequence')) + 1 if line.product_id.seller_ids else 1, 'min_qty': 0.0, 'price': price, 'currency_id': currency.id, 'delay': 0, } # In case the order partner is a contact address, a new supplierinfo is created on # the parent company. In this case, we keep the product name and code. seller = line.product_id._select_seller( partner_id=line.partner_id, quantity=line.product_qty, date=line.order_id.date_order and line.order_id.date_order.date(), uom_id=line.product_uom) if seller: supplierinfo['product_name'] = seller.product_name supplierinfo['product_code'] = seller.product_code vals = { 'seller_ids': [(0, 0, supplierinfo)], } try: line.product_id.write(vals) except AccessError: # no write access rights -> just ignore break def action_view_invoice(self): ''' This function returns an action that display existing vendor bills of given purchase order ids. When only one found, show the vendor bill immediately. ''' action = self.env.ref('account.action_move_in_invoice_type') result = action.read()[0] create_bill = self.env.context.get('create_bill', False) # override the context to get rid of the default filtering result['context'] = { 'default_type': 'in_invoice', 'default_company_id': self.company_id.id, 'default_purchase_id': self.id, } # choose the view_mode accordingly if len(self.invoice_ids) > 1 and not create_bill: result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]" else: res = self.env.ref('account.view_move_form', False) form_view = [(res and res.id or False, 'form')] if 'views' in result: result['views'] = form_view + [(state,view) for state,view in action['views'] if view != 'form'] else: result['views'] = form_view # Do not set an invoice_id if we want to create a new bill. if not create_bill: result['res_id'] = self.invoice_ids.id or False result['context']['default_origin'] = self.name result['context']['default_reference'] = self.partner_ref return result
class EventRegistration(models.Model): _name = 'event.registration' _description = 'Event Registration' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'name, create_date desc' # event origin = fields.Char( string='Source Document', readonly=True, help= "Reference of the document that created the registration, for example a sales order" ) event_id = fields.Many2one('event.event', string='Event', required=True, readonly=True, states={'draft': [('readonly', False)]}) # attendee partner_id = fields.Many2one('res.partner', string='Contact', states={'done': [('readonly', True)]}) name = fields.Char(string='Attendee Name', index=True) email = fields.Char(string='Email') phone = fields.Char(string='Phone') mobile = fields.Char(string='Mobile') # organization date_open = fields.Datetime(string='Registration Date', readonly=True, default=lambda self: fields.Datetime.now() ) # weird crash is directly now date_closed = fields.Datetime(string='Attended Date', readonly=True) event_begin_date = fields.Datetime(string="Event Start Date", related='event_id.date_begin', readonly=True) event_end_date = fields.Datetime(string="Event End Date", related='event_id.date_end', readonly=True) company_id = fields.Many2one('res.company', string='Company', related='event_id.company_id', store=True, readonly=True, states={'draft': [('readonly', False)]}) state = fields.Selection([('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('open', 'Confirmed'), ('done', 'Attended')], string='Status', default='draft', readonly=True, copy=False, tracking=True) @api.constrains('event_id', 'state') def _check_seats_limit(self): for registration in self: if registration.event_id.seats_availability == 'limited' and registration.event_id.seats_max and registration.event_id.seats_available < ( 1 if registration.state == 'draft' else 0): raise ValidationError( _('No more seats available for this event.')) def _check_auto_confirmation(self): if self._context.get('registration_force_draft'): return False if any(registration.event_id.state != 'confirm' or not registration.event_id.auto_confirm or ( not registration.event_id.seats_available and registration.event_id.seats_availability == 'limited') for registration in self): return False return True @api.model def create(self, vals): registration = super(EventRegistration, self).create(vals) if registration._check_auto_confirmation(): registration.sudo().confirm_registration() return registration @api.model def _prepare_attendee_values(self, registration): """ Method preparing the values to create new attendees based on a sales order line. It takes some registration data (dict-based) that are optional values coming from an external input like a web page. This method is meant to be inherited in various addons that sell events. """ partner_id = registration.pop('partner_id', self.env.user.partner_id) event_id = registration.pop('event_id', False) data = { 'name': registration.get('name', partner_id.name), 'phone': registration.get('phone', partner_id.phone), 'mobile': registration.get('mobile', partner_id.mobile), 'email': registration.get('email', partner_id.email), 'partner_id': partner_id.id, 'event_id': event_id and event_id.id or False, } data.update({ key: value for key, value in registration.items() if key in self._fields }) return data def do_draft(self): self.write({'state': 'draft'}) def confirm_registration(self): self.write({'state': 'open'}) # auto-trigger after_sub (on subscribe) mail schedulers, if needed onsubscribe_schedulers = self.event_id.event_mail_ids.filtered( lambda s: s.interval_type == 'after_sub') onsubscribe_schedulers.execute() def button_reg_close(self): """ Close Registration """ for registration in self: today = fields.Datetime.now() if registration.event_id.date_begin <= today and registration.event_id.state == 'confirm': registration.write({'state': 'done', 'date_closed': today}) elif registration.event_id.state == 'draft': raise UserError( _("You must wait the event confirmation before doing this action." )) else: raise UserError( _("You must wait the event starting day before doing this action." )) def button_reg_cancel(self): self.write({'state': 'cancel'}) @api.onchange('partner_id') def _onchange_partner(self): if self.partner_id: contact_id = self.partner_id.address_get().get('contact', False) if contact_id: contact = self.env['res.partner'].browse(contact_id) self.name = contact.name or self.name self.email = contact.email or self.email self.phone = contact.phone or self.phone self.mobile = contact.mobile or self.mobile def _message_get_suggested_recipients(self): recipients = super(EventRegistration, self)._message_get_suggested_recipients() public_users = self.env['res.users'].sudo() public_groups = self.env.ref("base.group_public", raise_if_not_found=False) if public_groups: public_users = public_groups.sudo().with_context( active_test=False).mapped("users") try: for attendee in self: is_public = attendee.sudo().with_context( active_test=False ).partner_id.user_ids in public_users if public_users else False if attendee.partner_id and not is_public: attendee._message_add_suggested_recipient( recipients, partner=attendee.partner_id, reason=_('Customer')) elif attendee.email: attendee._message_add_suggested_recipient( recipients, email=attendee.email, reason=_('Customer Email')) except AccessError: # no read access rights -> ignore suggested recipients pass return recipients def _message_get_default_recipients(self): # Prioritize registration email over partner_id, which may be shared when a single # partner booked multiple seats return { r.id: { 'partner_ids': [], 'email_to': r.email, 'email_cc': False } for r in self } def _message_post_after_hook(self, message, msg_vals): if self.email and not self.partner_id: # we consider that posting a message with a specified recipient (not a follower, a specific one) # on a document without customer means that it was created through the chatter using # suggested recipients. This heuristic allows to avoid ugly hacks in JS. new_partner = message.partner_ids.filtered( lambda partner: partner.email == self.email) if new_partner: self.search([ ('partner_id', '=', False), ('email', '=', new_partner.email), ('state', 'not in', ['cancel']), ]).write({'partner_id': new_partner.id}) return super(EventRegistration, self)._message_post_after_hook(message, msg_vals) def action_send_badge_email(self): """ Open a window to compose an email, with the template - 'event_badge' message loaded by default """ self.ensure_one() template = self.env.ref('event.event_registration_mail_template_badge') compose_form = self.env.ref('mail.email_compose_message_wizard_form') ctx = dict( default_model='event.registration', default_res_id=self.id, default_use_template=bool(template), default_template_id=template.id, default_composition_mode='comment', custom_layout="mail.mail_notification_light", ) return { 'name': _('Compose Email'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form.id, 'form')], 'view_id': compose_form.id, 'target': 'new', 'context': ctx, } def get_date_range_str(self): self.ensure_one() today = fields.Datetime.now() event_date = self.event_begin_date diff = (event_date.date() - today.date()) if diff.days <= 0: return _('today') elif diff.days == 1: return _('tomorrow') elif (diff.days < 7): return _('in %d days') % (diff.days, ) elif (diff.days < 14): return _('next week') elif event_date.month == (today + relativedelta(months=+1)).month: return _('next month') else: return _('on ') + format_datetime(self.env, self.event_begin_date, tz=self.event_id.date_tz, dt_format='medium') def summary(self): self.ensure_one() return {'information': []}
class User(models.Model): _inherit = ['res.users'] # note: a user can only be linked to one employee per company (see sql constraint in ´hr.employee´) employee_ids = fields.One2many('hr.employee', 'user_id', string='Related employee') employee_id = fields.Many2one('hr.employee', string="Company employee", compute='_compute_company_employee', search='_search_company_employee', store=False) job_title = fields.Char(related='employee_id.job_title', readonly=False) work_phone = fields.Char(related='employee_id.work_phone', readonly=False) mobile_phone = fields.Char(related='employee_id.mobile_phone', readonly=False) employee_phone = fields.Char(related='employee_id.phone', readonly=False, related_sudo=False) work_email = fields.Char(related='employee_id.work_email', readonly=False, related_sudo=False) category_ids = fields.Many2many(related='employee_id.category_ids', string="Employee Tags", readonly=False, related_sudo=False) department_id = fields.Many2one(related='employee_id.department_id', readonly=False, related_sudo=False) address_id = fields.Many2one(related='employee_id.address_id', readonly=False, related_sudo=False) work_location = fields.Char(related='employee_id.work_location', readonly=False, related_sudo=False) employee_parent_id = fields.Many2one(related='employee_id.parent_id', related_sudo=False) coach_id = fields.Many2one(related='employee_id.coach_id', readonly=False, related_sudo=False) address_home_id = fields.Many2one(related='employee_id.address_home_id', readonly=False, related_sudo=False) is_address_home_a_company = fields.Boolean( related='employee_id.is_address_home_a_company', readonly=False, related_sudo=False) private_email = fields.Char(related='address_home_id.email', string="Private Email", readonly=False) km_home_work = fields.Integer(related='employee_id.km_home_work', readonly=False, related_sudo=False) # res.users already have a field bank_account_id and country_id from the res.partner inheritance: don't redefine them employee_bank_account_id = fields.Many2one( related='employee_id.bank_account_id', string="Employee's Bank Account Number", related_sudo=False, readonly=False) employee_country_id = fields.Many2one(related='employee_id.country_id', string="Employee's Country", readonly=False, related_sudo=False) identification_id = fields.Char(related='employee_id.identification_id', readonly=False, related_sudo=False) passport_id = fields.Char(related='employee_id.passport_id', readonly=False, related_sudo=False) gender = fields.Selection(related='employee_id.gender', readonly=False, related_sudo=False) birthday = fields.Date(related='employee_id.birthday', readonly=False, related_sudo=False) place_of_birth = fields.Char(related='employee_id.place_of_birth', readonly=False, related_sudo=False) country_of_birth = fields.Many2one(related='employee_id.country_of_birth', readonly=False, related_sudo=False) marital = fields.Selection(related='employee_id.marital', readonly=False, related_sudo=False) spouse_complete_name = fields.Char( related='employee_id.spouse_complete_name', readonly=False, related_sudo=False) spouse_birthdate = fields.Date(related='employee_id.spouse_birthdate', readonly=False, related_sudo=False) children = fields.Integer(related='employee_id.children', readonly=False, related_sudo=False) emergency_contact = fields.Char(related='employee_id.emergency_contact', readonly=False, related_sudo=False) emergency_phone = fields.Char(related='employee_id.emergency_phone', readonly=False, related_sudo=False) visa_no = fields.Char(related='employee_id.visa_no', readonly=False, related_sudo=False) permit_no = fields.Char(related='employee_id.permit_no', readonly=False, related_sudo=False) visa_expire = fields.Date(related='employee_id.visa_expire', readonly=False, related_sudo=False) additional_note = fields.Text(related='employee_id.additional_note', readonly=False, related_sudo=False) barcode = fields.Char(related='employee_id.barcode', readonly=False, related_sudo=False) pin = fields.Char(related='employee_id.pin', readonly=False, related_sudo=False) certificate = fields.Selection(related='employee_id.certificate', readonly=False, related_sudo=False) study_field = fields.Char(related='employee_id.study_field', readonly=False, related_sudo=False) study_school = fields.Char(related='employee_id.study_school', readonly=False, related_sudo=False) employee_count = fields.Integer(compute='_compute_employee_count') hr_presence_state = fields.Selection( related='employee_id.hr_presence_state') last_activity = fields.Date(related='employee_id.last_activity') last_activity_time = fields.Char(related='employee_id.last_activity_time') can_edit = fields.Boolean(compute='_compute_can_edit') def _compute_can_edit(self): can_edit = self.env['ir.config_parameter'].sudo().get_param( 'hr.hr_employee_self_edit') or self.env.user.has_group( 'hr.group_hr_user') for user in self: user.can_edit = can_edit @api.depends('employee_ids') def _compute_employee_count(self): for user in self.with_context(active_test=False): user.employee_count = len(user.employee_ids) def __init__(self, pool, cr): """ Override of __init__ to add access rights. Access rights are disabled by default, but allowed on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS. """ hr_readable_fields = [ 'active', 'child_ids', 'employee_id', 'employee_ids', 'employee_parent_id', 'hr_presence_state', 'last_activity', 'last_activity_time', 'can_edit', ] hr_writable_fields = [ 'additional_note', 'address_home_id', 'address_id', 'barcode', 'birthday', 'category_ids', 'children', 'coach_id', 'country_of_birth', 'department_id', 'display_name', 'emergency_contact', 'emergency_phone', 'employee_bank_account_id', 'employee_country_id', 'gender', 'identification_id', 'is_address_home_a_company', 'job_title', 'private_email', 'km_home_work', 'marital', 'mobile_phone', 'notes', 'employee_parent_id', 'passport_id', 'permit_no', 'employee_phone', 'pin', 'place_of_birth', 'spouse_birthdate', 'spouse_complete_name', 'visa_expire', 'visa_no', 'work_email', 'work_location', 'work_phone', 'certificate', 'study_field', 'study_school', ] init_res = super(User, self).__init__(pool, cr) # duplicate list to avoid modifying the original reference type(self).SELF_READABLE_FIELDS = type( self ).SELF_READABLE_FIELDS + hr_readable_fields + hr_writable_fields type(self).SELF_WRITEABLE_FIELDS = type( self).SELF_WRITEABLE_FIELDS + hr_writable_fields return init_res @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): # When the front-end loads the views it gets the list of available fields # for the user (according to its access rights). Later, when the front-end wants to # populate the view with data, it only asks to read those available fields. # However, in this case, we want the user to be able to read/write its own data, # even if they are protected by groups. # We make the front-end aware of those fields by sending all field definitions. # Note: limit the `sudo` to the only action of "editing own profile" action in order to # avoid breaking `groups` mecanism on res.users form view. profile_view = self.env.ref("hr.res_users_view_form_profile") if profile_view and view_id == profile_view.id: self = self.with_user(SUPERUSER_ID) return super(User, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) def write(self, vals): """ Synchronize user and its related employee and check access rights if employees are not allowed to update their own data (otherwise sudo is applied for self data). """ hr_fields = { field for field_name, field in self._fields.items() if field.related_field and field.related_field.model_name == 'hr.employee' and field_name in vals } can_edit_self = self.env['ir.config_parameter'].sudo().get_param( 'hr.hr_employee_self_edit') or self.env.user.has_group( 'hr.group_hr_user') if hr_fields and not can_edit_self: # Raise meaningful error message raise AccessError( _("You are only allowed to update your preferences. Please contact a HR officer to update other informations." )) result = super(User, self).write(vals) employee_values = {} for fname in [ f for f in ['name', 'email', 'image_1920', 'tz'] if f in vals ]: employee_values[fname] = vals[fname] if employee_values: if 'email' in employee_values: employee_values['work_email'] = employee_values.pop('email') if 'image_1920' in vals: without_image = self.env['hr.employee'].sudo().search([ ('user_id', 'in', self.ids), ('image_1920', '=', False) ]) with_image = self.env['hr.employee'].sudo().search([ ('user_id', 'in', self.ids), ('image_1920', '!=', False) ]) without_image.write(employee_values) if not can_edit_self: employee_values.pop('image_1920') with_image.write(employee_values) else: self.env['hr.employee'].sudo().search([ ('user_id', 'in', self.ids) ]).write(employee_values) return result @api.model def action_get(self): if self.env.user.employee_id: return self.sudo().env.ref('hr.res_users_action_my').read()[0] return super(User, self).action_get() @api.depends('employee_ids') @api.depends_context('force_company') def _compute_company_employee(self): for user in self: user.employee_id = self.env['hr.employee'].search( [('id', 'in', user.employee_ids.ids), ('company_id', '=', self.env.company.id)], limit=1) def _search_company_employee(self, operator, value): employees = self.env['hr.employee'].search( [('name', operator, value), '|', ('company_id', '=', self.env.company.id), ('company_id', '=', False)], order='company_id ASC') return [('id', 'in', employees.mapped('user_id').ids)] def action_create_employee(self): self.ensure_one() self.env['hr.employee'].create( dict(user_id=self.id, name=self.name, **self.env['hr.employee']._sync_user(self)))
class Lead2OpportunityMassConvert(models.TransientModel): _name = 'crm.lead2opportunity.partner.mass' _description = 'Convert Lead to Opportunity (in mass)' _inherit = 'crm.lead2opportunity.partner' @api.model def default_get(self, fields): res = super(Lead2OpportunityMassConvert, self).default_get(fields) if 'partner_id' in fields: # avoid forcing the partner of the first lead as default res['partner_id'] = False if 'action' in fields: res['action'] = 'each_exist_or_create' if 'name' in fields: res['name'] = 'convert' if 'opportunity_ids' in fields: res['opportunity_ids'] = False return res user_ids = fields.Many2many('res.users', string='Salesmen') team_id = fields.Many2one('crm.team', 'Sales Team', index=True, oldname='section_id') deduplicate = fields.Boolean( 'Apply deduplication', default=True, help='Merge with existing leads/opportunities of each partner') action = fields.Selection( [('each_exist_or_create', 'Use existing partner or create'), ('nothing', 'Do not link to a customer')], 'Related Customer', required=True) force_assignation = fields.Boolean( 'Force assignation', help= 'If unchecked, this will leave the salesman of duplicated opportunities' ) @api.onchange('action') def _onchange_action(self): if self.action != 'exist': self.partner_id = False @api.onchange('deduplicate') def _onchange_deduplicate(self): active_leads = self.env['crm.lead'].browse(self._context['active_ids']) partner_ids = [ (lead.partner_id.id, lead.partner_id and lead.partner_id.email or lead.email_from) for lead in active_leads ] partners_duplicated_leads = {} for partner_id, email in partner_ids: duplicated_leads = self._get_duplicated_leads(partner_id, email) if len(duplicated_leads) > 1: partners_duplicated_leads.setdefault( (partner_id, email), []).extend(duplicated_leads) leads_with_duplicates = [] for lead in active_leads: lead_tuple = (lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from) if len(partners_duplicated_leads.get(lead_tuple, [])) > 1: leads_with_duplicates.append(lead.id) self.opportunity_ids = self.env['crm.lead'].browse( leads_with_duplicates) @api.multi def _convert_opportunity(self, vals): """ When "massively" (more than one at a time) converting leads to opportunities, check the salesteam_id and salesmen_ids and update the values before calling super. """ self.ensure_one() salesteam_id = self.team_id.id if self.team_id else False salesmen_ids = [] if self.user_ids: salesmen_ids = self.user_ids.ids vals.update({'user_ids': salesmen_ids, 'team_id': salesteam_id}) return super(Lead2OpportunityMassConvert, self)._convert_opportunity(vals) @api.multi def mass_convert(self): self.ensure_one() if self.name == 'convert' and self.deduplicate: merged_lead_ids = set() remaining_lead_ids = set() lead_selected = self._context.get('active_ids', []) for lead_id in lead_selected: if lead_id not in merged_lead_ids: lead = self.env['crm.lead'].browse(lead_id) duplicated_leads = self._get_duplicated_leads( lead.partner_id.id, lead.partner_id.email if lead.partner_id else lead.email_from) if len(duplicated_leads) > 1: lead = duplicated_leads.merge_opportunity() merged_lead_ids.update(duplicated_leads.ids) remaining_lead_ids.add(lead.id) active_ids = set(self._context.get('active_ids', {})) active_ids = (active_ids - merged_lead_ids) | remaining_lead_ids self = self.with_context(active_ids=list( active_ids)) # only update active_ids when there are set no_force_assignation = self._context.get('no_force_assignation', not self.force_assignation) return self.with_context( no_force_assignation=no_force_assignation).action_apply()
class EventEvent(models.Model): """Event""" _name = 'event.event' _description = 'Event' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'date_begin' name = fields.Char(string='Event', translate=True, required=True, readonly=False, states={'done': [('readonly', True)]}) active = fields.Boolean(default=True) user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user, tracking=True, readonly=False, states={'done': [('readonly', True)]}) company_id = fields.Many2one('res.company', string='Company', change_default=True, default=lambda self: self.env.company, required=False, readonly=False, states={'done': [('readonly', True)]}) organizer_id = fields.Many2one( 'res.partner', string='Organizer', tracking=True, default=lambda self: self.env.company.partner_id, domain= "['|', ('company_id', '=', False), ('company_id', '=', company_id)]") event_type_id = fields.Many2one('event.type', string='Category', readonly=False, states={'done': [('readonly', True)]}) color = fields.Integer('Kanban Color Index') event_mail_ids = fields.One2many('event.mail', 'event_id', string='Mail Schedule', copy=True) # Seats and computation seats_max = fields.Integer( string='Maximum Attendees Number', readonly=True, states={ 'draft': [('readonly', False)], 'confirm': [('readonly', False)] }, help= "For each event you can define a maximum registration of seats(number of attendees), above this numbers the registrations are not accepted." ) seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')], 'Maximum Attendees', required=True, default='unlimited') seats_min = fields.Integer( string='Minimum Attendees', help= "For each event you can define a minimum reserved seats (number of attendees), if it does not reach the mentioned registrations the event can not be confirmed (keep 0 to ignore this rule)" ) seats_reserved = fields.Integer(string='Reserved Seats', store=True, readonly=True, compute='_compute_seats') seats_available = fields.Integer(string='Available Seats', store=True, readonly=True, compute='_compute_seats') seats_unconfirmed = fields.Integer(string='Unconfirmed Seat Reservations', store=True, readonly=True, compute='_compute_seats') seats_used = fields.Integer(string='Number of Participants', store=True, readonly=True, compute='_compute_seats') seats_expected = fields.Integer(string='Number of Expected Attendees', compute_sudo=True, readonly=True, compute='_compute_seats') # Registration fields registration_ids = fields.One2many('event.registration', 'event_id', string='Attendees', readonly=False, states={'done': [('readonly', True)]}) # Date fields date_tz = fields.Selection('_tz_get', string='Timezone', required=True, default=lambda self: self.env.user.tz or 'UTC') date_begin = fields.Datetime(string='Start Date', required=True, tracking=True, states={'done': [('readonly', True)]}) date_end = fields.Datetime(string='End Date', required=True, tracking=True, states={'done': [('readonly', True)]}) date_begin_located = fields.Char(string='Start Date Located', compute='_compute_date_begin_tz') date_end_located = fields.Char(string='End Date Located', compute='_compute_date_end_tz') is_one_day = fields.Boolean(compute='_compute_field_is_one_day') state = fields.Selection( [('draft', 'Unconfirmed'), ('cancel', 'Cancelled'), ('confirm', 'Confirmed'), ('done', 'Done')], string='Status', default='draft', readonly=True, required=True, copy=False, help= "If event is created, the status is 'Draft'. If event is confirmed for the particular dates the status is set to 'Confirmed'. If the event is over, the status is set to 'Done'. If event is cancelled the status is set to 'Cancelled'." ) auto_confirm = fields.Boolean(string='Autoconfirm Registrations') is_online = fields.Boolean('Online Event') address_id = fields.Many2one( 'res.partner', string='Location', default=lambda self: self.env.company.partner_id, readonly=False, states={'done': [('readonly', True)]}, domain= "['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True) country_id = fields.Many2one('res.country', 'Country', related='address_id.country_id', store=True, readonly=False) twitter_hashtag = fields.Char('Twitter Hashtag') description = fields.Html(string='Description', translate=html_translate, sanitize_attributes=False, readonly=False, states={'done': [('readonly', True)]}) # badge fields badge_front = fields.Html(string='Badge Front') badge_back = fields.Html(string='Badge Back') badge_innerleft = fields.Html(string='Badge Inner Left') badge_innerright = fields.Html(string='Badge Inner Right') event_logo = fields.Html(string='Event Logo') @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 for event in self: event.seats_unconfirmed = event.seats_reserved = event.seats_used = event.seats_available = 0 # aggregate registrations by event and by state if self.ids: state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT event_id, state, count(event_id) FROM event_registration WHERE event_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_id, state """ self.env['event.registration'].flush(['event_id', 'state']) self._cr.execute(query, (tuple(self.ids), )) for event_id, state, num in self._cr.fetchall(): event = self.browse(event_id) event[state_field[state]] += num # compute seats_available for event in self: if event.seats_max > 0: event.seats_available = event.seats_max - ( event.seats_reserved + event.seats_used) event.seats_expected = event.seats_unconfirmed + event.seats_reserved + event.seats_used @api.model def _tz_get(self): return [(x, x) for x in pytz.all_timezones] @api.depends('date_tz', 'date_begin') def _compute_date_begin_tz(self): for event in self: if event.date_begin: event.date_begin_located = format_datetime(self.env, event.date_begin, tz=event.date_tz, dt_format='medium') else: event.date_begin_located = False @api.depends('date_tz', 'date_end') def _compute_date_end_tz(self): for event in self: if event.date_end: event.date_end_located = format_datetime(self.env, event.date_end, tz=event.date_tz, dt_format='medium') else: event.date_end_located = False @api.depends('date_begin', 'date_end', 'date_tz') def _compute_field_is_one_day(self): for event in self: # Need to localize because it could begin late and finish early in # another timezone event = event.with_context(tz=event.date_tz) begin_tz = fields.Datetime.context_timestamp( event, event.date_begin) end_tz = fields.Datetime.context_timestamp(event, event.date_end) event.is_one_day = (begin_tz.date() == end_tz.date()) @api.onchange('is_online') def _onchange_is_online(self): if self.is_online: self.address_id = False @api.onchange('event_type_id') def _onchange_type(self): if self.event_type_id: self.seats_min = self.event_type_id.default_registration_min self.seats_max = self.event_type_id.default_registration_max if self.event_type_id.default_registration_max: self.seats_availability = 'limited' if self.event_type_id.auto_confirm: self.auto_confirm = self.event_type_id.auto_confirm if self.event_type_id.use_hashtag: self.twitter_hashtag = self.event_type_id.default_hashtag if self.event_type_id.use_timezone: self.date_tz = self.event_type_id.default_timezone self.is_online = self.event_type_id.is_online if self.event_type_id.event_type_mail_ids: self.event_mail_ids = [(5, 0, 0)] + [(0, 0, { attribute_name: line[attribute_name] for attribute_name in self.env['event.type.mail']. _get_event_mail_fields_whitelist() }) for line in self.event_type_id.event_type_mail_ids] @api.constrains('seats_min', 'seats_max', 'seats_availability') def _check_seats_min_max(self): if any(event.seats_availability == 'limited' and event.seats_min > event.seats_max for event in self): raise ValidationError( _('Maximum attendees number should be greater than minimum attendees number.' )) @api.constrains('seats_max', 'seats_available') def _check_seats_limit(self): if any(event.seats_availability == 'limited' and event.seats_max and event.seats_available < 0 for event in self): raise ValidationError(_('No more available seats.')) @api.constrains('date_begin', 'date_end') def _check_closing_date(self): for event in self: if event.date_end < event.date_begin: raise ValidationError( _('The closing date cannot be earlier than the beginning date.' )) @api.depends('name', 'date_begin', 'date_end') def name_get(self): result = [] for event in self: date_begin = fields.Datetime.from_string(event.date_begin) date_end = fields.Datetime.from_string(event.date_end) dates = [ fields.Date.to_string( fields.Datetime.context_timestamp(event, dt)) for dt in [date_begin, date_end] if dt ] dates = sorted(set(dates)) result.append( (event.id, '%s (%s)' % (event.name, ' - '.join(dates)))) return result @api.model def create(self, vals): res = super(EventEvent, self).create(vals) if res.organizer_id: res.message_subscribe([res.organizer_id.id]) if res.auto_confirm: res.button_confirm() return res def write(self, vals): res = super(EventEvent, self).write(vals) if vals.get('organizer_id'): self.message_subscribe([vals['organizer_id']]) return res @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_("%s (copy)") % (self.name)) return super(EventEvent, self).copy(default) def button_draft(self): self.write({'state': 'draft'}) def button_cancel(self): if any('done' in event.mapped('registration_ids.state') for event in self): raise UserError( _("There are already attendees who attended this event. Please reset it to draft if you want to cancel this event." )) self.registration_ids.write({'state': 'cancel'}) self.state = 'cancel' def button_done(self): self.write({'state': 'done'}) def button_confirm(self): self.write({'state': 'confirm'}) def mail_attendees(self, template_id, force_send=False, filter_func=lambda self: self.state != 'cancel'): for event in self: for attendee in event.registration_ids.filtered(filter_func): self.env['mail.template'].browse(template_id).send_mail( attendee.id, force_send=force_send) def _is_event_registrable(self): return self.date_end > fields.Datetime.now() def _get_ics_file(self): """ Returns iCalendar file for the event invitation. :returns a dict of .ics file content for each event """ result = {} if not vobject: return result for event in self: cal = vobject.iCalendar() cal_event = cal.add('vevent') cal_event.add('created').value = fields.Datetime.now().replace( tzinfo=pytz.timezone('UTC')) cal_event.add('dtstart').value = fields.Datetime.from_string( event.date_begin).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtend').value = fields.Datetime.from_string( event.date_end).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('summary').value = event.name if event.address_id: cal_event.add( 'location').value = event.sudo().address_id.contact_address result[event.id] = cal.serialize().encode('utf-8') return result
class OpMediaQueue(models.Model): _name = 'op.media.queue' _inherit = 'mail.thread' _rec_name = 'user_id' _description = 'Media Queue Request' name = fields.Char("Sequence No", readonly=True, copy=False, default='/') partner_id = fields.Many2one('res.partner', 'Student/Faculty') media_id = fields.Many2one('op.media', 'Media', required=True, track_visibility='onchange') date_from = fields.Date('From Date', required=True, default=fields.Date.today()) date_to = fields.Date('To Date', required=True) user_id = fields.Many2one('res.users', 'User', readonly=True, default=lambda self: self.env.uid) state = fields.Selection([('request', 'Request'), ('accept', 'Accepted'), ('reject', 'Rejected')], 'Status', copy=False, default='request', track_visibility='onchange') @api.onchange('user_id') def onchange_user(self): self.partner_id = self.user_id.partner_id.id @api.constrains('date_from', 'date_to') def _check_date(self): if self.date_from > self.date_to: raise ValidationError(_('To Date cannot be set before From Date.')) @api.model def create(self, vals): if self.env.user.child_ids: raise Warning( _('Invalid Action!\n Parent can not create \ Media Queue Requests!')) if vals.get('name', '/') == '/': vals['name'] = self.env['ir.sequence'].next_by_code( 'op.media.queue') or '/' return super(OpMediaQueue, self).create(vals) @api.multi def write(self, vals): if self.env.user.child_ids: raise Warning( _('Invalid Action!\n Parent can not edit \ Media Queue Requests!')) return super(OpMediaQueue, self).write(vals) @api.multi def do_reject(self): self.state = 'reject' @api.multi def do_accept(self): self.state = 'accept' @api.multi def do_request_again(self): self.state = 'request'
class StockScrap(models.Model): _name = 'stock.scrap' _order = 'id desc' _description = 'Scrap' def _get_default_scrap_location_id(self): return self.env['stock.location'].search( [('scrap_location', '=', True), ('company_id', 'in', [self.env.user.company_id.id, False])], limit=1).id def _get_default_location_id(self): company_user = self.env.user.company_id warehouse = self.env['stock.warehouse'].search( [('company_id', '=', company_user.id)], limit=1) if warehouse: return warehouse.lot_stock_id.id return None name = fields.Char('Reference', default=lambda self: _('New'), copy=False, readonly=True, required=True, states={'done': [('readonly', True)]}) origin = fields.Char(string='Source Document') product_id = fields.Many2one('product.product', 'Product', domain=[('type', 'in', ['product', 'consu'])], required=True, states={'done': [('readonly', True)]}) product_uom_id = fields.Many2one('uom.uom', 'Unit of Measure', required=True, states={'done': [('readonly', True)]}) tracking = fields.Selection('Product Tracking', readonly=True, related="product_id.tracking") lot_id = fields.Many2one('stock.production.lot', 'Lot', states={'done': [('readonly', True)]}, domain="[('product_id', '=', product_id)]") package_id = fields.Many2one('stock.quant.package', 'Package', states={'done': [('readonly', True)]}) owner_id = fields.Many2one('res.partner', 'Owner', states={'done': [('readonly', True)]}) move_id = fields.Many2one('stock.move', 'Scrap Move', readonly=True) picking_id = fields.Many2one('stock.picking', 'Picking', states={'done': [('readonly', True)]}) location_id = fields.Many2one('stock.location', 'Location', domain="[('usage', '=', 'internal')]", required=True, states={'done': [('readonly', True)]}, default=_get_default_location_id) scrap_location_id = fields.Many2one( 'stock.location', 'Scrap Location', default=_get_default_scrap_location_id, domain="[('scrap_location', '=', True)]", required=True, states={'done': [('readonly', True)]}) scrap_qty = fields.Float('Quantity', default=1.0, required=True, states={'done': [('readonly', True)]}) state = fields.Selection([('draft', 'Draft'), ('done', 'Done')], string='Status', default="draft") date_expected = fields.Datetime('Expected Date', default=fields.Datetime.now) @api.onchange('picking_id') def _onchange_picking_id(self): if self.picking_id: self.location_id = ( self.picking_id.state == 'done' ) and self.picking_id.location_dest_id.id or self.picking_id.location_id.id @api.onchange('product_id') def onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_id.id @api.model def create(self, vals): if 'name' not in vals or vals['name'] == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code( 'stock.scrap') or _('New') scrap = super(StockScrap, self).create(vals) return scrap def unlink(self): if 'done' in self.mapped('state'): raise UserError(_('You cannot delete a scrap which is done.')) return super(StockScrap, self).unlink() def _get_origin_moves(self): return self.picking_id and self.picking_id.move_lines.filtered( lambda x: x.product_id == self.product_id) def _prepare_move_values(self): self.ensure_one() return { 'name': self.name, 'origin': self.origin or self.picking_id.name or self.name, 'product_id': self.product_id.id, 'product_uom': self.product_uom_id.id, 'product_uom_qty': self.scrap_qty, 'location_id': self.location_id.id, 'scrapped': True, 'location_dest_id': self.scrap_location_id.id, 'move_line_ids': [(0, 0, { 'product_id': self.product_id.id, 'product_uom_id': self.product_uom_id.id, 'qty_done': self.scrap_qty, 'location_id': self.location_id.id, 'location_dest_id': self.scrap_location_id.id, 'package_id': self.package_id.id, 'owner_id': self.owner_id.id, 'lot_id': self.lot_id.id, })], # 'restrict_partner_id': self.owner_id.id, 'picking_id': self.picking_id.id } @api.multi def do_scrap(self): for scrap in self: move = self.env['stock.move'].create(scrap._prepare_move_values()) # master: replace context by cancel_backorder move.with_context(is_scrap=True)._action_done() scrap.write({'move_id': move.id, 'state': 'done'}) return True def action_get_stock_picking(self): action = self.env.ref('stock.action_picking_tree_all').read([])[0] action['domain'] = [('id', '=', self.picking_id.id)] return action def action_get_stock_move_lines(self): action = self.env.ref('stock.stock_move_line_action').read([])[0] action['domain'] = [('move_id', '=', self.move_id.id)] return action def action_validate(self): self.ensure_one() if self.product_id.type != 'product': return self.do_scrap() precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') available_qty = sum(self.env['stock.quant']._gather( self.product_id, self.location_id, self.lot_id, self.package_id, self.owner_id, strict=True).mapped('quantity')) scrap_qty = self.product_uom_id._compute_quantity( self.scrap_qty, self.product_id.uom_id) if float_compare(available_qty, scrap_qty, precision_digits=precision) >= 0: return self.do_scrap() else: return { 'name': _('Insufficient Quantity'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.warn.insufficient.qty.scrap', 'view_id': self.env.ref( 'stock.stock_warn_insufficient_qty_scrap_form_view').id, 'type': 'ir.actions.act_window', 'context': { 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_scrap_id': self.id }, 'target': 'new' }
class ResourceResource(models.Model): _name = "resource.resource" _description = "Resources" @api.model def default_get(self, fields): res = super(ResourceResource, self).default_get(fields) if not res.get('calendar_id') and res.get('company_id'): company = self.env['res.company'].browse(res['company_id']) res['calendar_id'] = company.resource_calendar_id.id return res name = fields.Char(required=True) active = fields.Boolean( 'Active', default=True, track_visibility='onchange', help= "If the active field is set to False, it will allow you to hide the resource record without removing it." ) company_id = fields.Many2one( 'res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) resource_type = fields.Selection([('user', 'Human'), ('material', 'Material')], string='Resource Type', default='user', required=True) user_id = fields.Many2one( 'res.users', string='User', help='Related user name for the resource to manage its access.') time_efficiency = fields.Float( 'Efficiency Factor', default=100, required=True, help= "This field is used to calculate the the expected duration of a work order at this work center. For example, if a work order takes one hour and the efficiency factor is 100%, then the expected duration will be one hour. If the efficiency factor is 200%, however the expected duration will be 30 minutes." ) calendar_id = fields.Many2one("resource.calendar", string='Working Time', default=lambda self: self.env['res.company']. _company_default_get().resource_calendar_id, required=True, help="Define the schedule of resource") tz = fields.Selection( _tz_get, string='Timezone', required=True, default=lambda self: self._context.get('tz' ) or self.env.user.tz or 'UTC', help= "This field is used in order to define in which timezone the resources will work." ) _sql_constraints = [ ('check_time_efficiency', 'CHECK(time_efficiency>0)', 'Time efficiency must be strictly positive'), ] @api.multi @api.constrains('time_efficiency') def _check_time_efficiency(self): for record in self: if record.time_efficiency == 0: raise ValidationError( _('The efficiency factor cannot be equal to 0.')) @api.model def create(self, values): if values.get('company_id') and not values.get('calendar_id'): values['calendar_id'] = self.env['res.company'].browse( values['company_id']).resource_calendar_id.id if not values.get('tz'): # retrieve timezone on user or calendar tz = (self.env['res.users'].browse(values.get('user_id')).tz or self.env['resource.calendar'].browse( values.get('calendar_id')).tz) if tz: values['tz'] = tz return super(ResourceResource, self).create(values) @api.multi @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() if default is None: default = {} if not default.get('name'): default.update(name=_('%s (copy)') % (self.name)) return super(ResourceResource, self).copy(default) @api.onchange('company_id') def _onchange_company_id(self): if self.company_id: self.calendar_id = self.company_id.resource_calendar_id.id @api.onchange('user_id') def _onchange_user_id(self): if self.user_id: self.tz = self.user_id.tz
class StockRule(models.Model): _inherit = 'stock.rule' action = fields.Selection(selection_add=[('manufacture', 'Manufacture')]) def _get_message_dict(self): message_dict = super(StockRule, self)._get_message_dict() source, destination, operation = self._get_message_values() manufacture_message = _( 'When products are needed in <b>%s</b>, <br/> a manufacturing order is created to fulfill the need.' ) % (destination) if self.location_src_id: manufacture_message += _( ' <br/><br/> The components will be taken from <b>%s</b>.') % ( source) message_dict.update({'manufacture': manufacture_message}) return message_dict @api.onchange('action') def _onchange_action_operation(self): domain = {'picking_type_id': []} if self.action == 'manufacture': domain = {'picking_type_id': [('code', '=', 'mrp_operation')]} return {'domain': domain} @api.multi def _run_manufacture(self, product_id, product_qty, product_uom, location_id, name, origin, values): Production = self.env['mrp.production'] ProductionSudo = Production.sudo().with_context( force_company=values['company_id'].id) bom = self._get_matching_bom(product_id, values) if not bom: msg = _( 'There is no Bill of Material found for the product %s. Please define a Bill of Material for this product.' ) % (product_id.display_name, ) raise UserError(msg) # create the MO as SUPERUSER because the current user may not have the rights to do it (mto product launched by a sale for example) production = ProductionSudo.create( self._prepare_mo_vals(product_id, product_qty, product_uom, location_id, name, origin, values, bom)) origin_production = values.get('move_dest_ids') and values[ 'move_dest_ids'][0].raw_material_production_id or False orderpoint = values.get('orderpoint_id') if orderpoint: production.message_post_with_view( 'mail.message_origin_link', values={ 'self': production, 'origin': orderpoint }, subtype_id=self.env.ref('mail.mt_note').id) if origin_production: production.message_post_with_view( 'mail.message_origin_link', values={ 'self': production, 'origin': origin_production }, subtype_id=self.env.ref('mail.mt_note').id) return True @api.multi def _get_matching_bom(self, product_id, values): if values.get('bom_id', False): return values['bom_id'] return self.env['mrp.bom'].with_context( company_id=values['company_id'].id, force_company=values['company_id'].id)._bom_find( product=product_id, picking_type=self.picking_type_id ) # TDE FIXME: context bullshit def _prepare_mo_vals(self, product_id, product_qty, product_uom, location_id, name, origin, values, bom): return { 'origin': origin, 'product_id': product_id.id, 'product_qty': product_qty, 'product_uom_id': product_uom.id, 'location_src_id': self.location_src_id.id or self.picking_type_id.default_location_src_id.id or location_id.id, 'location_dest_id': location_id.id, 'bom_id': bom.id, 'date_planned_start': fields.Datetime.to_string( self._get_date_planned(product_id, values)), 'date_planned_finished': values['date_planned'], 'procurement_group_id': False, 'propagate': self.propagate, 'picking_type_id': self.picking_type_id.id or values['warehouse_id'].manu_type_id.id, 'company_id': values['company_id'].id, 'move_dest_ids': values.get('move_dest_ids') and [(4, x.id) for x in values['move_dest_ids']] or False, } def _get_date_planned(self, product_id, values): format_date_planned = fields.Datetime.from_string( values['date_planned']) date_planned = format_date_planned - relativedelta( days=product_id.produce_delay or 0.0) date_planned = date_planned - relativedelta( days=values['company_id'].manufacturing_lead) return date_planned def _push_prepare_move_copy_values(self, move_to_copy, new_date): new_move_vals = super(StockRule, self)._push_prepare_move_copy_values( move_to_copy, new_date) new_move_vals['production_id'] = False return new_move_vals
class ResourceCalendar(models.Model): """ Calendar model for a resource. It has - attendance_ids: list of resource.calendar.attendance that are a working interval in a given weekday. - leave_ids: list of leaves linked to this calendar. A leave can be general or linked to a specific resource, depending on its resource_id. All methods in this class use intervals. An interval is a tuple holding (begin_datetime, end_datetime). A list of intervals is therefore a list of tuples, holding several intervals of work or leaves. """ _name = "resource.calendar" _description = "Resource Working Time" @api.model def default_get(self, fields): res = super(ResourceCalendar, self).default_get(fields) if not res.get('name') and res.get('company_id'): res['name'] = _( 'Working Hours of %s') % self.env['res.company'].browse( res['company_id']).name return res def _get_default_attendance_ids(self): return [(0, 0, { 'name': _('Monday Morning'), 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Monday Evening'), 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' }), (0, 0, { 'name': _('Tuesday Morning'), 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Tuesday Evening'), 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' }), (0, 0, { 'name': _('Wednesday Morning'), 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Wednesday Evening'), 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' }), (0, 0, { 'name': _('Thursday Morning'), 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Thursday Evening'), 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' }), (0, 0, { 'name': _('Friday Morning'), 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Friday Evening'), 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' })] name = fields.Char(required=True) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get()) attendance_ids = fields.One2many('resource.calendar.attendance', 'calendar_id', 'Working Time', copy=True, default=_get_default_attendance_ids) leave_ids = fields.One2many('resource.calendar.leaves', 'calendar_id', 'Leaves') global_leave_ids = fields.One2many('resource.calendar.leaves', 'calendar_id', 'Global Leaves', domain=[('resource_id', '=', False)]) hours_per_day = fields.Float( "Average hour per day", default=HOURS_PER_DAY, help= "Average hours per day a resource is supposed to work with this calendar." ) tz = fields.Selection( _tz_get, string='Timezone', required=True, default=lambda self: self._context.get('tz' ) or self.env.user.tz or 'UTC', help= "This field is used in order to define in which timezone the resources will work." ) @api.onchange('attendance_ids') def _onchange_hours_per_day(self): attendances = self.attendance_ids.filtered( lambda attendance: not attendance.date_from and not attendance. date_to) hour_count = 0.0 for attendance in attendances: hour_count += attendance.hour_to - attendance.hour_from if attendances: self.hours_per_day = float_round( hour_count / float(len(set(attendances.mapped('dayofweek')))), precision_digits=2) # -------------------------------------------------- # Computation API # -------------------------------------------------- def _attendance_intervals(self, start_dt, end_dt, resource=None): """ Return the attendance intervals in the given datetime range. The returned intervals are expressed in the resource's timezone. """ assert start_dt.tzinfo and end_dt.tzinfo combine = datetime.combine # express all dates and times in the resource's timezone tz = timezone((resource or self).tz) start_dt = start_dt.astimezone(tz) end_dt = end_dt.astimezone(tz) # for each attendance spec, generate the intervals in the date range result = [] for attendance in self.attendance_ids: start = start_dt.date() if attendance.date_from: start = max(start, attendance.date_from) until = end_dt.date() if attendance.date_to: until = min(until, attendance.date_to) weekday = int(attendance.dayofweek) for day in rrule(DAILY, start, until=until, byweekday=weekday): # attendance hours are interpreted in the resource's timezone dt0 = tz.localize( combine(day, float_to_time(attendance.hour_from))) dt1 = tz.localize( combine(day, float_to_time(attendance.hour_to))) result.append((max(start_dt, dt0), min(end_dt, dt1), attendance)) return Intervals(result) def _leave_intervals(self, start_dt, end_dt, resource=None, domain=None): """ Return the leave intervals in the given datetime range. The returned intervals are expressed in the calendar's timezone. """ assert start_dt.tzinfo and end_dt.tzinfo self.ensure_one() # for the computation, express all datetimes in UTC resource_ids = [resource.id, False] if resource else [False] if domain is None: domain = [('time_type', '=', 'leave')] domain = domain + [ ('calendar_id', '=', self.id), ('resource_id', 'in', resource_ids), ('date_from', '<=', datetime_to_string(end_dt)), ('date_to', '>=', datetime_to_string(start_dt)), ] # retrieve leave intervals in (start_dt, end_dt) tz = timezone((resource or self).tz) start_dt = start_dt.astimezone(tz) end_dt = end_dt.astimezone(tz) result = [] for leave in self.env['resource.calendar.leaves'].search(domain): dt0 = string_to_datetime(leave.date_from).astimezone(tz) dt1 = string_to_datetime(leave.date_to).astimezone(tz) result.append((max(start_dt, dt0), min(end_dt, dt1), leave)) return Intervals(result) def _work_intervals(self, start_dt, end_dt, resource=None, domain=None): """ Return the effective work intervals between the given datetimes. """ return (self._attendance_intervals(start_dt, end_dt, resource) - self._leave_intervals(start_dt, end_dt, resource, domain)) # -------------------------------------------------- # External API # -------------------------------------------------- @api.multi def get_work_hours_count(self, start_dt, end_dt, compute_leaves=True, domain=None): """ `compute_leaves` controls whether or not this method is taking into account the global leaves. `domain` controls the way leaves are recognized. None means default value ('time_type', '=', 'leave') Counts the number of work hours between two datetimes. """ # Set timezone in UTC if no timezone is explicitly given if not start_dt.tzinfo: start_dt = start_dt.replace(tzinfo=utc) if not end_dt.tzinfo: end_dt = end_dt.replace(tzinfo=utc) if compute_leaves: intervals = self._work_intervals(start_dt, end_dt, domain=domain) else: intervals = self._attendance_intervals(start_dt, end_dt) return sum((stop - start).total_seconds() / 3600 for start, stop, meta in intervals) @api.multi def plan_hours(self, hours, day_dt, compute_leaves=False, domain=None, resource=None): """ `compute_leaves` controls whether or not this method is taking into account the global leaves. `domain` controls the way leaves are recognized. None means default value ('time_type', '=', 'leave') Return datetime after having planned hours """ day_dt, revert = make_aware(day_dt) # which method to use for retrieving intervals if compute_leaves: get_intervals = partial(self._work_intervals, domain=domain, resource=resource) else: get_intervals = self._attendance_intervals if hours >= 0: delta = timedelta(days=14) for n in range(100): dt = day_dt + delta * n for start, stop, meta in get_intervals(dt, dt + delta): interval_hours = (stop - start).total_seconds() / 3600 if hours <= interval_hours: return revert(start + timedelta(hours=hours)) hours -= interval_hours return False else: hours = abs(hours) delta = timedelta(days=14) for n in range(100): dt = day_dt - delta * n for start, stop, meta in reversed(get_intervals( dt - delta, dt)): interval_hours = (stop - start).total_seconds() / 3600 if hours <= interval_hours: return revert(stop - timedelta(hours=hours)) hours -= interval_hours return False @api.multi def plan_days(self, days, day_dt, compute_leaves=False, domain=None): """ `compute_leaves` controls whether or not this method is taking into account the global leaves. `domain` controls the way leaves are recognized. None means default value ('time_type', '=', 'leave') Returns the datetime of a days scheduling. """ day_dt, revert = make_aware(day_dt) # which method to use for retrieving intervals if compute_leaves: get_intervals = partial(self._work_intervals, domain=domain) else: get_intervals = self._attendance_intervals if days > 0: found = set() delta = timedelta(days=14) for n in range(100): dt = day_dt + delta * n for start, stop, meta in get_intervals(dt, dt + delta): found.add(start.date()) if len(found) == days: return revert(stop) return False elif days < 0: days = abs(days) found = set() delta = timedelta(days=14) for n in range(100): dt = day_dt - delta * n for start, stop, meta in reversed(get_intervals( dt - delta, dt)): found.add(start.date()) if len(found) == days: return revert(start) return False else: return revert(day_dt)
class OpAssignmentSubLine(models.Model): _name = "op.assignment.sub.line" _inherit = "mail.thread" _rec_name = "assignment_id" _description = "Assignment Submission" _order = "submission_date DESC" def _compute_get_user_group(self): for user in self: if self.env.user.has_group( 'openeagleedu_core.group_op_back_office_admin') or \ self.env.user.has_group( 'openeagleedu_core.group_op_back_office') or \ self.env.user.has_group( 'openeagleedu_core.group_op_faculty'): user.user_boolean = True else: user.user_boolean = False assignment_id = fields.Many2one( 'op.assignment', 'Assignment', required=True) student_id = fields.Many2one( 'op.student', 'Student', default=lambda self: self.env['op.student'].search( [('user_id', '=', self.env.uid)]), required=True) description = fields.Text('Description', track_visibility='onchange') state = fields.Selection([ ('draft', 'Draft'), ('submit', 'Submitted'), ('reject', 'Rejected'), ('change', 'Change Req.'), ('accept', 'Accepted')], basestring='State', default='draft', track_visibility='onchange') submission_date = fields.Datetime( 'Submission Date', readonly=True, default=lambda self: fields.Datetime.now(), required=True) marks = fields.Float('Marks', track_visibility='onchange') note = fields.Text('Note') user_id = fields.Many2one( 'res.users', related='student_id.user_id', string='User') faculty_user_id = fields.Many2one( 'res.users', related='assignment_id.faculty_id.user_id', string='Faculty User') user_boolean = fields.Boolean(string='Check user', compute='_compute_get_user_group') def act_draft(self): result = self.state = 'draft' return result and result or False def act_submit(self): result = self.state = 'submit' return result and result or False def act_accept(self): result = self.state = 'accept' return result and result or False def act_change_req(self): result = self.state = 'change' return result and result or False def act_reject(self): result = self.state = 'reject' return result and result or False def unlink(self): for record in self: if not record.state == 'draft' and not self.env.user.has_group( 'openeagleedu_core.group_op_faculty'): raise ValidationError( _("You can't delete none draft submissions!")) res = super(OpAssignmentSubLine, self).unlink() return res @api.model def create(self, vals): if self.env.user.child_ids: raise Warning(_('Invalid Action!\n Parent can not \ create Assignment Submissions!')) return super(OpAssignmentSubLine, self).create(vals) def write(self, vals): if self.env.user.child_ids: raise Warning(_('Invalid Action!\n Parent can not edit \ Assignment Submissions!')) return super(OpAssignmentSubLine, self).write(vals)
class AcquirerAdyen(models.Model): _inherit = 'payment.acquirer' provider = fields.Selection(selection_add=[('adyen', 'Adyen')]) adyen_merchant_account = fields.Char('Merchant Account', required_if_provider='adyen', groups='base.group_user') adyen_skin_code = fields.Char('Skin Code', required_if_provider='adyen', groups='base.group_user') adyen_skin_hmac_key = fields.Char('Skin HMAC Key', required_if_provider='adyen', groups='base.group_user') @api.model def _adyen_convert_amount(self, amount, currency): """ Adyen requires the amount to be multiplied by 10^k, where k depends on the currency code. """ k = CURRENCY_CODE_MAPS.get(currency.name, 2) paymentAmount = int(tools.float_round(amount, k) * (10**k)) return paymentAmount @api.model def _get_adyen_urls(self, environment): """ Adyen URLs: yhpp: hosted payment page: pay.shtml for single, select.shtml for multiple """ return { 'adyen_form_url': 'https://%s.adyen.com/hpp/pay.shtml' % ('live' if environment == 'prod' else environment), } @api.multi def _adyen_generate_merchant_sig_sha256(self, inout, values): """ Generate the shasign for incoming or outgoing communications., when using the SHA-256 signature. :param string inout: 'in' (eagle contacting ogone) or 'out' (adyen contacting eagle). In this last case only some fields should be contained (see e-Commerce basic) :param dict values: transaction values :return string: shasign """ def escapeVal(val): return val.replace('\\', '\\\\').replace(':', '\\:') def signParams(parms): signing_string = ':'.join( escapeVal(v) for v in chain(parms.keys(), parms.values())) hm = hmac.new(hmac_key, signing_string.encode('utf-8'), hashlib.sha256) return base64.b64encode(hm.digest()) assert inout in ('in', 'out') assert self.provider == 'adyen' if inout == 'in': # All the fields sent to Adyen must be included in the signature. ALL the f*****g # fields, despite what is claimed in the documentation. For example, in # https://docs.adyen.com/developers/hpp-manual, it is stated: "The resURL parameter does # not need to be included in the signature." It's a trap, it must be included as well! keys = [ 'merchantReference', 'paymentAmount', 'currencyCode', 'shipBeforeDate', 'skinCode', 'merchantAccount', 'sessionValidity', 'merchantReturnData', 'shopperEmail', 'shopperReference', 'allowedMethods', 'blockedMethods', 'offset', 'shopperStatement', 'recurringContract', 'billingAddressType', 'deliveryAddressType', 'brandCode', 'countryCode', 'shopperLocale', 'orderData', 'offerEmail', 'resURL', ] else: keys = [ 'authResult', 'merchantReference', 'merchantReturnData', 'paymentMethod', 'pspReference', 'shopperLocale', 'skinCode', ] hmac_key = binascii.a2b_hex(self.adyen_skin_hmac_key.encode('ascii')) raw_values = {k: values.get(k, '') for k in keys if k in values} raw_values_ordered = OrderedDict( sorted(raw_values.items(), key=lambda t: t[0])) return signParams(raw_values_ordered) @api.multi def _adyen_generate_merchant_sig(self, inout, values): """ Generate the shasign for incoming or outgoing communications, when using the SHA-1 signature (deprecated by Adyen). :param string inout: 'in' (eagle contacting ogone) or 'out' (adyen contacting eagle). In this last case only some fields should be contained (see e-Commerce basic) :param dict values: transaction values :return string: shasign """ assert inout in ('in', 'out') assert self.provider == 'adyen' if inout == 'in': keys = "paymentAmount currencyCode shipBeforeDate merchantReference skinCode merchantAccount sessionValidity shopperEmail shopperReference recurringContract allowedMethods blockedMethods shopperStatement merchantReturnData billingAddressType deliveryAddressType offset".split( ) else: keys = "authResult pspReference merchantReference skinCode merchantReturnData".split( ) def get_value(key): if values.get(key): return values[key] return '' sign = ''.join('%s' % get_value(k) for k in keys).encode('ascii') key = self.adyen_skin_hmac_key.encode('ascii') return base64.b64encode(hmac.new(key, sign, hashlib.sha1).digest()) @api.multi def adyen_form_generate_values(self, values): base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') # tmp import datetime from dateutil import relativedelta paymentAmount = self._adyen_convert_amount(values['amount'], values['currency']) if self.provider == 'adyen' and len(self.adyen_skin_hmac_key) == 64: tmp_date = datetime.datetime.today() + relativedelta.relativedelta( days=1) values.update({ 'merchantReference': values['reference'], 'paymentAmount': '%d' % paymentAmount, 'currencyCode': values['currency'] and values['currency'].name or '', 'shipBeforeDate': tmp_date.strftime('%Y-%m-%d'), 'skinCode': self.adyen_skin_code, 'merchantAccount': self.adyen_merchant_account, 'shopperLocale': values.get('partner_lang', ''), 'sessionValidity': tmp_date.isoformat('T')[:19] + "Z", 'resURL': urls.url_join(base_url, AdyenController._return_url), 'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url', '') else False, 'shopperEmail': values.get('partner_email') or values.get('billing_partner_email') or '', }) values['merchantSig'] = self._adyen_generate_merchant_sig_sha256( 'in', values) else: tmp_date = datetime.date.today() + relativedelta.relativedelta( days=1) values.update({ 'merchantReference': values['reference'], 'paymentAmount': '%d' % paymentAmount, 'currencyCode': values['currency'] and values['currency'].name or '', 'shipBeforeDate': tmp_date, 'skinCode': self.adyen_skin_code, 'merchantAccount': self.adyen_merchant_account, 'shopperLocale': values.get('partner_lang'), 'sessionValidity': tmp_date, 'resURL': urls.url_join(base_url, AdyenController._return_url), 'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url') else False, }) values['merchantSig'] = self._adyen_generate_merchant_sig( 'in', values) return values @api.multi def adyen_get_form_action_url(self): return self._get_adyen_urls(self.environment)['adyen_form_url']
class MailMessage(models.Model): """ Override MailMessage class in order to add a new type: SMS messages. Those messages comes with their own notification method, using SMS gateway. """ _inherit = 'mail.message' message_type = fields.Selection(selection_add=[('sms', 'SMS')]) has_sms_error = fields.Boolean( 'Has SMS error', compute='_compute_has_sms_error', search='_search_has_sms_error', help='Has error') def _compute_has_sms_error(self): sms_error_from_notification = self.env['mail.notification'].sudo().search([ ('notification_type', '=', 'sms'), ('mail_message_id', 'in', self.ids), ('notification_status', '=', 'exception')]).mapped('mail_message_id') for message in self: message.has_sms_error = message in sms_error_from_notification def _search_has_sms_error(self, operator, operand): if operator == '=' and operand: return ['&', ('notification_ids.notification_status', '=', 'exception'), ('notification_ids.notification_type', '=', 'sms')] raise NotImplementedError() def _format_mail_failures(self): """ A shorter message to notify a SMS delivery failure update TDE FIXME: should be cleaned """ res = super(MailMessage, self)._format_mail_failures() # prepare notifications computation in batch all_notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', self.ids) ]) msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo()) for notif in all_notifications: msgid_to_notif[notif.mail_message_id.id] += notif for message in self: notifications = msgid_to_notif[message.id] if not any(notification.notification_type == 'sms' for notification in notifications): continue info = dict(message._get_mail_failure_dict(), failure_type='sms', notifications=dict((notif.res_partner_id.id, (notif.notification_status, notif.res_partner_id.name)) for notif in notifications if notif.notification_type == 'sms'), module_icon='/sms/static/img/sms_failure.svg' ) res.append(info) return res def _notify_sms_update(self): """ Send bus notifications to update status of notifications in chatter. Purpose is to send the updated status per author. TDE FIXME: author_id strategy seems curious, check with JS """ messages = self.env['mail.message'] for message in self: # YTI FIXME: check allowed_company_ids if necessary if message.model and message.res_id: record = self.env[message.model].browse(message.res_id) try: record.check_access_rights('read') record.check_access_rule('read') except exceptions.AccessError: continue else: messages |= message """ Notify channels after update of SMS status """ updates = [[ (self._cr.dbname, 'res.partner', author.id), {'type': 'sms_update', 'elements': self.env['mail.message'].concat(*author_messages)._format_mail_failures()} ] for author, author_messages in groupby(messages, itemgetter('author_id'))] self.env['bus.bus'].sendmany(updates) def message_format(self): """ Override in order to retrieves data about SMS (recipient name and SMS status) TDE FIXME: clean the overall message_format thingy """ message_values = super(MailMessage, self).message_format() all_sms_notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', 'in', [r['id'] for r in message_values]), ('notification_type', '=', 'sms') ]) msgid_to_notif = defaultdict(lambda: self.env['mail.notification'].sudo()) for notif in all_sms_notifications: msgid_to_notif[notif.mail_message_id.id] += notif for message in message_values: customer_sms_data = [(notif.id, notif.res_partner_id.display_name or notif.sms_number, notif.notification_status) for notif in msgid_to_notif.get(message['id'], [])] message['sms_ids'] = customer_sms_data return message_values
def inverse_fn(records): pass MODELS = [ ('boolean', fields.Boolean()), ('integer', fields.Integer()), ('float', fields.Float()), ('decimal', fields.Float(digits=(16, 3))), ('string.bounded', fields.Char(size=16)), ('string.required', fields.Char(size=None, required=True)), ('string', fields.Char(size=None)), ('date', fields.Date()), ('datetime', fields.Datetime()), ('text', fields.Text()), ('selection', fields.Selection([(1, "Foo"), (2, "Bar"), (3, "Qux"), (4, '')])), ('selection.function', fields.Selection(selection_fn)), # just relate to an integer ('many2one', fields.Many2one('export.integer')), ('one2many', fields.One2many('export.one2many.child', 'parent_id')), ('many2many', fields.Many2many('export.many2many.other')), ('function', fields.Integer(compute=compute_fn, inverse=inverse_fn)), # related: specialization of fields.function, should work the same way # TODO: reference ] for name, field in MODELS: class NewModel(models.Model): _name = 'export.%s' % name _description = 'Export: %s' % name _rec_name = 'value'
class AccountClosing(models.Model): """ This object holds an interval total and a grand total of the accounts of type receivable for a company, as well as the last account_move that has been counted in a previous object It takes its earliest brother to infer from when the computation needs to be done in order to compute its own data. """ _name = 'account.sale.closing' _order = 'date_closing_stop desc, sequence_number desc' _description = "Sale Closing" name = fields.Char(help="Frequency and unique sequence number", required=True) company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True) date_closing_stop = fields.Datetime( string="Closing Date", help='Date to which the values are computed', readonly=True, required=True) date_closing_start = fields.Datetime( string="Starting Date", help='Date from which the total interval is computed', readonly=True, required=True) frequency = fields.Selection(string='Closing Type', selection=[('daily', 'Daily'), ('monthly', 'Monthly'), ('annually', 'Annual')], readonly=True, required=True) total_interval = fields.Monetary( string="Period Total", help= 'Total in receivable accounts during the interval, excluding overlapping periods', readonly=True, required=True) cumulative_total = fields.Monetary( string="Cumulative Grand Total", help='Total in receivable accounts since the beginnig of times', readonly=True, required=True) sequence_number = fields.Integer('Sequence #', readonly=True, required=True) last_order_id = fields.Many2one( 'pos.order', string='Last Pos Order', help='Last Pos order included in the grand total', readonly=True) last_order_hash = fields.Char( string='Last Order entry\'s inalteralbility hash', readonly=True) currency_id = fields.Many2one('res.currency', string='Currency', help="The company's currency", readonly=True, related='company_id.currency_id', store=True) def _query_for_aml(self, company, first_move_sequence_number, date_start): params = {'company_id': company.id} query = '''WITH aggregate AS (SELECT m.id AS move_id, aml.balance AS balance, aml.id as line_id FROM account_move_line aml JOIN account_journal j ON aml.journal_id = j.id JOIN account_account acc ON acc.id = aml.account_id JOIN account_account_type t ON (t.id = acc.user_type_id AND t.type = 'receivable') JOIN account_move m ON m.id = aml.move_id WHERE j.type = 'sale' AND aml.company_id = %(company_id)s AND m.state = 'posted' ''' if first_move_sequence_number is not False and first_move_sequence_number is not None: params['first_move_sequence_number'] = first_move_sequence_number query += '''AND m.secure_sequence_number > %(first_move_sequence_number)s''' elif date_start: #the first time we compute the closing, we consider only from the installation of the module params['date_start'] = date_start query += '''AND m.date >= %(date_start)s''' query += " ORDER BY m.secure_sequence_number DESC) " query += '''SELECT array_agg(move_id) AS move_ids, array_agg(line_id) AS line_ids, sum(balance) AS balance FROM aggregate''' self.env.cr.execute(query, params) return self.env.cr.dictfetchall()[0] def _compute_amounts(self, frequency, company): """ Method used to compute all the business data of the new object. It will search for previous closings of the same frequency to infer the move from which account move lines should be fetched. @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually) frequencies are literal (daily means 24 hours and so on) @param {recordset} company: the company for which the closing is done @return {dict} containing {field: value} for each business field of the object """ interval_dates = self._interval_dates(frequency, company) previous_closing = self.search([('frequency', '=', frequency), ('company_id', '=', company.id)], limit=1, order='sequence_number desc') first_order = self.env['pos.order'] date_start = interval_dates['interval_from'] cumulative_total = 0 if previous_closing: first_order = previous_closing.last_order_id date_start = previous_closing.create_date cumulative_total += previous_closing.cumulative_total domain = [('company_id', '=', company.id), ('state', 'in', ('paid', 'done', 'invoiced'))] if first_order.l10n_fr_secure_sequence_number is not False and first_order.l10n_fr_secure_sequence_number is not None: domain = AND([ domain, [('l10n_fr_secure_sequence_number', '>', first_order.l10n_fr_secure_sequence_number)] ]) elif date_start: #the first time we compute the closing, we consider only from the installation of the module domain = AND([domain, [('date_order', '>=', date_start)]]) orders = self.env['pos.order'].search(domain, order='date_order desc') total_interval = sum(orders.mapped('amount_total')) cumulative_total += total_interval # We keep the reference to avoid gaps (like daily object during the weekend) last_order = first_order if orders: last_order = orders[0] return { 'total_interval': total_interval, 'cumulative_total': cumulative_total, 'last_order_id': last_order.id, 'last_order_hash': last_order.l10n_fr_secure_sequence_number, 'date_closing_stop': interval_dates['date_stop'], 'date_closing_start': date_start, 'name': interval_dates['name_interval'] + ' - ' + interval_dates['date_stop'][:10] } def _interval_dates(self, frequency, company): """ Method used to compute the theoretical date from which account move lines should be fetched @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually) frequencies are literal (daily means 24 hours and so on) @param {recordset} company: the company for which the closing is done @return {dict} the theoretical date from which account move lines are fetched. date_stop date to which the move lines are fetched, always now() the dates are in their Eagle Database string representation """ date_stop = datetime.utcnow() interval_from = None name_interval = '' if frequency == 'daily': interval_from = date_stop - timedelta(days=1) name_interval = _('Daily Closing') elif frequency == 'monthly': month_target = date_stop.month > 1 and date_stop.month - 1 or 12 year_target = month_target < 12 and date_stop.year or date_stop.year - 1 interval_from = date_stop.replace(year=year_target, month=month_target) name_interval = _('Monthly Closing') elif frequency == 'annually': year_target = date_stop.year - 1 interval_from = date_stop.replace(year=year_target) name_interval = _('Annual Closing') return { 'interval_from': FieldDateTime.to_string(interval_from), 'date_stop': FieldDateTime.to_string(date_stop), 'name_interval': name_interval } def write(self, vals): raise UserError( _('Sale Closings are not meant to be written or deleted under any circumstances.' )) def unlink(self): raise UserError( _('Sale Closings are not meant to be written or deleted under any circumstances.' )) @api.model def _automated_closing(self, frequency='daily'): """To be executed by the CRON to create an object of the given frequency for each company that needs it @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually) frequencies are literal (daily means 24 hours and so on) @return {recordset} all the objects created for the given frequency """ res_company = self.env['res.company'].search([]) account_closings = self.env['account.sale.closing'] for company in res_company.filtered( lambda c: c._is_accounting_unalterable()): new_sequence_number = company.l10n_fr_closing_sequence_id.next_by_id( ) values = self._compute_amounts(frequency, company) values['frequency'] = frequency values['company_id'] = company.id values['sequence_number'] = new_sequence_number account_closings |= account_closings.create(values) return account_closings
class SelectionWithDefault(models.Model): _name = 'export.selection.withdefault' _description = 'Export Selection With Default' const = fields.Integer(default=4) value = fields.Selection([(1, "Foo"), (2, "Bar")], default=2)
class Lead2OpportunityPartner(models.TransientModel): _name = 'crm.lead2opportunity.partner' _description = 'Convert Lead to Opportunity (not in mass)' _inherit = 'crm.partner.binding' @api.model def default_get(self, fields): """ Default get for name, opportunity_ids. If there is an exisitng partner link to the lead, find all existing opportunities links with this partner to merge all information together """ result = super(Lead2OpportunityPartner, self).default_get(fields) if self._context.get('active_id'): tomerge = {int(self._context['active_id'])} partner_id = result.get('partner_id') lead = self.env['crm.lead'].browse(self._context['active_id']) email = lead.partner_id.email if lead.partner_id else lead.email_from tomerge.update( self._get_duplicated_leads(partner_id, email, include_lost=True).ids) if 'action' in fields and not result.get('action'): result['action'] = 'exist' if partner_id else 'create' if 'partner_id' in fields: result['partner_id'] = partner_id if 'name' in fields: result['name'] = 'merge' if len(tomerge) >= 2 else 'convert' if 'opportunity_ids' in fields and len(tomerge) >= 2: result['opportunity_ids'] = list(tomerge) if lead.user_id: result['user_id'] = lead.user_id.id if lead.team_id: result['team_id'] = lead.team_id.id if not partner_id and not lead.contact_name: result['action'] = 'nothing' return result name = fields.Selection([('convert', 'Convert to opportunity'), ('merge', 'Merge with existing opportunities')], 'Conversion Action', required=True) opportunity_ids = fields.Many2many('crm.lead', string='Opportunities') user_id = fields.Many2one('res.users', 'Salesperson', index=True) team_id = fields.Many2one('crm.team', 'Sales Team', oldname='section_id', index=True) @api.onchange('action') def onchange_action(self): if self.action == 'exist': self.partner_id = self._find_matching_partner() else: self.partner_id = False @api.onchange('user_id') def _onchange_user(self): """ When changing the user, also set a team_id or restrict team id to the ones user_id is member of. """ if self.user_id: if self.team_id: user_in_team = self.env['crm.team'].search_count([ ('id', '=', self.team_id.id), '|', ('user_id', '=', self.user_id.id), ('member_ids', '=', self.user_id.id) ]) else: user_in_team = False if not user_in_team: values = self.env['crm.lead']._onchange_user_values( self.user_id.id if self.user_id else False) self.team_id = values.get('team_id', False) @api.model def _get_duplicated_leads(self, partner_id, email, include_lost=False): """ Search for opportunities that have the same partner and that arent done or cancelled """ return self.env['crm.lead']._get_duplicated_leads_by_emails( partner_id, email, include_lost=include_lost) # NOTE JEM : is it the good place to test this ? @api.model def view_init(self, fields): """ Check some preconditions before the wizard executes. """ for lead in self.env['crm.lead'].browse( self._context.get('active_ids', [])): if lead.probability == 100: raise UserError( _("Closed/Dead leads cannot be converted into opportunities." )) return False @api.multi def _convert_opportunity(self, vals): self.ensure_one() res = False leads = self.env['crm.lead'].browse(vals.get('lead_ids')) for lead in leads: self_def_user = self.with_context(default_user_id=self.user_id.id) partner_id = self_def_user._create_partner( lead.id, self.action, vals.get('partner_id') or lead.partner_id.id) res = lead.convert_opportunity(partner_id, [], False) user_ids = vals.get('user_ids') leads_to_allocate = leads if self._context.get('no_force_assignation'): leads_to_allocate = leads_to_allocate.filtered( lambda lead: not lead.user_id) if user_ids: leads_to_allocate.allocate_salesman(user_ids, team_id=(vals.get('team_id'))) return res @api.multi def action_apply(self): """ Convert lead to opportunity or merge lead and opportunity and open the freshly created opportunity view. """ self.ensure_one() values = { 'team_id': self.team_id.id, } if self.partner_id: values['partner_id'] = self.partner_id.id if self.name == 'merge': leads = self.with_context( active_test=False).opportunity_ids.merge_opportunity() if not leads.active: leads.write({ 'active': True, 'activity_type_id': False, 'lost_reason': False }) if leads.type == "lead": values.update({ 'lead_ids': leads.ids, 'user_ids': [self.user_id.id] }) self.with_context( active_ids=leads.ids)._convert_opportunity(values) elif not self._context.get( 'no_force_assignation') or not leads.user_id: values['user_id'] = self.user_id.id leads.write(values) else: leads = self.env['crm.lead'].browse( self._context.get('active_ids', [])) values.update({ 'lead_ids': leads.ids, 'user_ids': [self.user_id.id] }) self._convert_opportunity(values) return leads[0].redirect_opportunity_view() def _create_partner(self, lead_id, action, partner_id): """ Create partner based on action. :return dict: dictionary organized as followed: {lead_id: partner_assigned_id} """ #TODO this method in only called by Lead2OpportunityPartner #wizard and would probably diserve to be refactored or at least #moved to a better place if action == 'each_exist_or_create': partner_id = self.with_context( active_id=lead_id)._find_matching_partner() action = 'create' result = self.env['crm.lead'].browse( lead_id).handle_partner_assignation(action, partner_id) return result.get(lead_id)
class OpSubjectRegistration(models.Model): _name = "op.subject.registration" _description = "Subject Registration Details" _inherit = ["mail.thread"] name = fields.Char('Name', readonly=True, default='New') student_id = fields.Many2one('op.student', 'Student', required=True, track_visibility='onchange') course_id = fields.Many2one('op.course', 'Course', required=True, track_visibility='onchange') batch_id = fields.Many2one('op.batch', 'Batch', required=True, track_visibility='onchange') compulsory_subject_ids = fields.Many2many( 'op.subject', 'subject_compulsory_rel', 'register_id', 'subject_id', string="Compulsory Subjects", readonly=True) elective_subject_ids = fields.Many2many( 'op.subject', string="Elective Subjects") state = fields.Selection([ ('draft', 'Draft'), ('submitted', 'Submitted'), ('approved', 'Approved'), ('rejected', 'Rejected')], default='draft', string='state', copy=False, track_visibility='onchange') max_unit_load = fields.Float('Maximum Unit Load', track_visibility='onchange') min_unit_load = fields.Float('Minimum Unit Load', track_visibility='onchange') def action_reset_draft(self): self.state = 'draft' def action_reject(self): self.state = 'rejected' def action_approve(self): for record in self: subject_ids = [] for sub in record.compulsory_subject_ids: subject_ids.append(sub.id) for sub in record.elective_subject_ids: subject_ids.append(sub.id) course_id = self.env['op.student.course'].search([ ('student_id', '=', record.student_id.id), ('course_id', '=', record.course_id.id) ], limit=1) if course_id: course_id.write({ 'subject_ids': [[6, 0, list(set(subject_ids))]] }) record.state = 'approved' else: raise ValidationError( _("Course not found on student's admission!")) def action_submitted(self): self.state = 'submitted' @api.model def create(self, vals): if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code( 'op.subject.registration') or '/' return super(OpSubjectRegistration, self).create(vals) def get_subjects(self): for record in self: subject_ids = [] if record.course_id and record.course_id.subject_ids: for subject in record.course_id.subject_ids: if subject.subject_type == 'compulsory': subject_ids.append(subject.id) record.compulsory_subject_ids = [(6, 0, subject_ids)]
class EventType(models.Model): _name = 'event.type' _description = 'Event Category' _order = 'sequence, id' @api.model def _get_default_event_type_mail_ids(self): return [(0, 0, { 'notification_type': 'mail', 'interval_unit': 'now', 'interval_type': 'after_sub', 'template_id': self.env.ref('event.event_subscription').id, }), (0, 0, { 'notification_type': 'mail', 'interval_nbr': 1, 'interval_unit': 'days', 'interval_type': 'before_event', 'template_id': self.env.ref('event.event_reminder').id, }), (0, 0, { 'notification_type': 'mail', 'interval_nbr': 10, 'interval_unit': 'days', 'interval_type': 'before_event', 'template_id': self.env.ref('event.event_reminder').id, })] name = fields.Char('Event Category', required=True, translate=True) sequence = fields.Integer() # registration has_seats_limitation = fields.Boolean('Limited Seats', default=False) default_registration_min = fields.Integer( 'Minimum Registrations', default=0, help= "It will select this default minimum value when you choose this event") default_registration_max = fields.Integer( 'Maximum Registrations', default=0, help= "It will select this default maximum value when you choose this event") auto_confirm = fields.Boolean( 'Automatically Confirm Registrations', default=True, help="Events and registrations will automatically be confirmed " "upon creation, easing the flow for simple events.") # location is_online = fields.Boolean( 'Online Event', help= 'Online events like webinars do not require a specific location and are hosted online.' ) use_timezone = fields.Boolean('Use Default Timezone') default_timezone = fields.Selection('_tz_get', string='Timezone', default=lambda self: self.env.user.tz) # communication use_hashtag = fields.Boolean('Use Default Hashtag') default_hashtag = fields.Char('Twitter Hashtag') use_mail_schedule = fields.Boolean('Automatically Send Emails', default=True) event_type_mail_ids = fields.One2many( 'event.type.mail', 'event_type_id', string='Mail Schedule', copy=False, default=lambda self: self._get_default_event_type_mail_ids()) @api.onchange('has_seats_limitation') def _onchange_has_seats_limitation(self): if not self.has_seats_limitation: self.default_registration_min = 0 self.default_registration_max = 0 @api.model def _tz_get(self): return [(x, x) for x in pytz.all_timezones]
class SaleCouponReward(models.Model): _name = 'sale.coupon.reward' _description = "Sales Coupon Reward" _rec_name = 'reward_description' # VFE FIXME multi company """Rewards are not restricted to a company... You could have a reward_product_id limited to a specific company A. But still use this reward as reward of a program of company B... """ reward_description = fields.Char('Reward Description') reward_type = fields.Selection( [ ('discount', 'Discount'), ('product', 'Free Product'), ], string='Reward Type', default='discount', help="Discount - Reward will be provided as discount.\n" + "Free Product - Free product will be provide as reward \n" + "Free Shipping - Free shipping will be provided as reward (Need delivery module)" ) # Product Reward reward_product_id = fields.Many2one('product.product', string="Free Product", help="Reward Product") reward_product_quantity = fields.Integer(string="Quantity", default=1, help="Reward product quantity") # Discount Reward discount_type = fields.Selection( [('percentage', 'Percentage'), ('fixed_amount', 'Fixed Amount')], default="percentage", help="Percentage - Entered percentage discount will be provided\n" + "Amount - Entered fixed amount discount will be provided") discount_percentage = fields.Float( string="Discount", default=10, help='The discount in percentage, between 1 to 100') discount_apply_on = fields.Selection( [('on_order', 'On Order'), ('cheapest_product', 'On Cheapest Product'), ('specific_products', 'On Specific Products')], default="on_order", help="On Order - Discount on whole order\n" + "Cheapest product - Discount on cheapest product of the order\n" + "Specific products - Discount on selected specific products") discount_specific_product_ids = fields.Many2many( 'product.product', string="Products", help= "Products that will be discounted if the discount is applied on specific products" ) discount_max_amount = fields.Float( default=0, help="Maximum amount of discount that can be provided") discount_fixed_amount = fields.Float(string="Fixed Amount", help='The discount in fixed amount') reward_product_uom_id = fields.Many2one( related='reward_product_id.product_tmpl_id.uom_id', string='Unit of Measure', readonly=True) discount_line_product_id = fields.Many2one( 'product.product', string='Reward Line Product', copy=False, help= "Product used in the sales order to apply the discount. Each coupon program has its own reward product for reporting purpose" ) @api.constrains('discount_percentage') def _check_discount_percentage(self): if self.filtered(lambda reward: reward.discount_type == 'percentage' and (reward.discount_percentage < 0 or reward. discount_percentage > 100)): raise ValidationError( _('Discount percentage should be between 1-100')) def name_get(self): """ Returns a complete description of the reward """ result = [] for reward in self: reward_string = "" if reward.reward_type == 'product': reward_string = _("Free Product - %s" % (reward.reward_product_id.name)) elif reward.reward_type == 'discount': if reward.discount_type == 'percentage': reward_percentage = str(reward.discount_percentage) if reward.discount_apply_on == 'on_order': reward_string = _("%s%% discount on total amount" % (reward_percentage)) elif reward.discount_apply_on == 'specific_products': if len(reward.discount_specific_product_ids) > 1: reward_string = _("%s%% discount on products" % (reward_percentage)) else: reward_string = _( "%s%% discount on %s" % (reward_percentage, reward.discount_specific_product_ids.name)) elif reward.discount_apply_on == 'cheapest_product': reward_string = _("%s%% discount on cheapest product" % (reward_percentage)) elif reward.discount_type == 'fixed_amount': program = self.env['sale.coupon.program'].search([ ('reward_id', '=', reward.id) ]) reward_string = _("%s %s discount on total amount" % (str(reward.discount_fixed_amount), program.currency_id.name)) result.append((reward.id, reward_string)) return result
class AccountAnalyticLine(models.Model): _inherit = 'account.analytic.line' def _default_sale_line_domain(self): domain = super(AccountAnalyticLine, self)._default_sale_line_domain() return expression.OR( [domain, [('qty_delivered_method', '=', 'timesheet')]]) timesheet_invoice_type = fields.Selection( [('billable_time', 'Billed on Timesheets'), ('billable_fixed', 'Billed at a Fixed price'), ('non_billable', 'Non Billable Tasks'), ('non_billable_project', 'No task found')], string="Billable Type", compute='_compute_timesheet_invoice_type', compute_sudo=True, store=True, readonly=True) timesheet_invoice_id = fields.Many2one( 'account.move', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet") @api.depends('so_line.product_id', 'project_id', 'task_id') def _compute_timesheet_invoice_type(self): for timesheet in self: if timesheet.project_id: # AAL will be set to False invoice_type = 'non_billable_project' if not timesheet.task_id else 'non_billable' if timesheet.task_id and timesheet.so_line.product_id.type == 'service': if timesheet.so_line.product_id.invoice_policy == 'delivery': if timesheet.so_line.product_id.service_type == 'timesheet': invoice_type = 'billable_time' else: invoice_type = 'billable_fixed' elif timesheet.so_line.product_id.invoice_policy == 'order': invoice_type = 'billable_fixed' timesheet.timesheet_invoice_type = invoice_type else: timesheet.timesheet_invoice_type = False @api.onchange('employee_id') def _onchange_task_id_employee_id(self): if self.project_id: # timesheet only if self.task_id.billable_type == 'task_rate': self.so_line = self.task_id.sale_line_id elif self.task_id.billable_type == 'employee_rate': self.so_line = self._timesheet_determine_sale_line( self.task_id, self.employee_id) else: self.so_line = False @api.constrains('so_line', 'project_id') def _check_sale_line_in_project_map(self): for timesheet in self: if timesheet.project_id and timesheet.so_line: # billed timesheet if timesheet.so_line not in timesheet.project_id.mapped( 'sale_line_employee_ids.sale_line_id' ) | timesheet.task_id.sale_line_id | timesheet.project_id.sale_line_id: raise ValidationError( _("This timesheet line cannot be billed: there is no Sale Order Item defined on the task, nor on the project. Please define one to save your timesheet line." )) def write(self, values): # prevent to update invoiced timesheets if one line is of type delivery self._check_can_write(values) result = super(AccountAnalyticLine, self).write(values) return result def _check_can_write(self, values): if self.sudo().filtered( lambda aal: aal.so_line.product_id.invoice_policy == "delivery" ) and self.filtered(lambda timesheet: timesheet.timesheet_invoice_id): if any([ field_name in values for field_name in [ 'unit_amount', 'employee_id', 'project_id', 'task_id', 'so_line', 'amount', 'date' ] ]): raise UserError( _('You can not modify already invoiced timesheets (linked to a Sales order items invoiced on Time and material).' )) @api.model def _timesheet_preprocess(self, values): values = super(AccountAnalyticLine, self)._timesheet_preprocess(values) # task implies so line (at create) if 'task_id' in values and not values.get('so_line') and values.get( 'employee_id'): task = self.env['project.task'].sudo().browse(values['task_id']) employee = self.env['hr.employee'].sudo().browse( values['employee_id']) values['so_line'] = self._timesheet_determine_sale_line( task, employee).id return values def _timesheet_postprocess_values(self, values): result = super(AccountAnalyticLine, self)._timesheet_postprocess_values(values) # (re)compute the sale line if any([ field_name in values for field_name in ['task_id', 'employee_id'] ]): for timesheet in self: result[timesheet.id].update({ 'so_line': timesheet._timesheet_determine_sale_line( timesheet.task_id, timesheet.employee_id).id, }) return result @api.model def _timesheet_determine_sale_line(self, task, employee): """ Deduce the SO line associated to the timesheet line: 1/ timesheet on task rate: the so line will be the one from the task 2/ timesheet on employee rate task: find the SO line in the map of the project (even for subtask), or fallback on the SO line of the task, or fallback on the one on the project NOTE: this have to be consistent with `_compute_billable_type` on project.task. """ if task.billable_type != 'no': if task.billable_type == 'employee_rate': map_entry = self.env['project.sale.line.employee.map'].search([ ('project_id', '=', task.project_id.id), ('employee_id', '=', employee.id) ]) if map_entry: return map_entry.sale_line_id if task.sale_line_id: return task.sale_line_id return task.project_id.sale_line_id elif task.billable_type == 'task_rate': return task.sale_line_id return self.env['sale.order.line'] def _timesheet_get_portal_domain(self): """ Only the timesheets with a product invoiced on delivered quantity are concerned. since in ordered quantity, the timesheet quantity is not invoiced, thus there is no meaning of showing invoice with ordered quantity. """ domain = super(AccountAnalyticLine, self)._timesheet_get_portal_domain() return expression.AND([ domain, [('timesheet_invoice_type', 'in', ['billable_time', 'non_billable'])] ])
class MailMailStats(models.Model): """ MailMailStats models the statistics collected about emails. Those statistics are stored in a separated model and table to avoid bloating the mail_mail table with statistics values. This also allows to delete emails send with mass mailing without loosing the statistics about them. """ _name = 'mail.mail.statistics' _description = 'Email Statistics' _rec_name = 'message_id' _order = 'message_id' mail_mail_id = fields.Many2one('mail.mail', string='Mail', index=True) mail_mail_id_int = fields.Integer( string='Mail ID (tech)', help= 'ID of the related mail_mail. This field is an integer field because ' 'the related mail_mail can be deleted separately from its statistics. ' 'However the ID is needed for several action and controllers.', index=True, ) message_id = fields.Char(string='Message-ID') model = fields.Char(string='Document model') res_id = fields.Integer(string='Document ID') # campaign / wave data mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing', index=True) mass_mailing_campaign_id = fields.Many2one( related='mass_mailing_id.mass_mailing_campaign_id', string='Mass Mailing Campaign', store=True, readonly=True, index=True) # Bounce and tracking ignored = fields.Datetime( help='Date when the email has been invalidated. ' 'Invalid emails are blacklisted, opted-out or invalid email format') scheduled = fields.Datetime(help='Date when the email has been created', default=fields.Datetime.now) sent = fields.Datetime(help='Date when the email has been sent') exception = fields.Datetime( help='Date of technical error leading to the email not being sent') opened = fields.Datetime( help='Date when the email has been opened the first time') replied = fields.Datetime( help='Date when this email has been replied for the first time.') bounced = fields.Datetime(help='Date when this email has bounced.') # Link tracking links_click_ids = fields.One2many('link.tracker.click', 'mail_stat_id', string='Links click') clicked = fields.Datetime( help='Date when customer clicked on at least one tracked link') # Status state = fields.Selection(compute="_compute_state", selection=[('outgoing', 'Outgoing'), ('exception', 'Exception'), ('sent', 'Sent'), ('opened', 'Opened'), ('replied', 'Replied'), ('bounced', 'Bounced'), ('ignored', 'Ignored')], store=True) state_update = fields.Datetime(compute="_compute_state", string='State Update', help='Last state update of the mail', store=True) email = fields.Char(string="Recipient email address") @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception', 'ignored') def _compute_state(self): self.update({'state_update': fields.Datetime.now()}) for stat in self: if stat.ignored: stat.state = 'ignored' elif stat.exception: stat.state = 'exception' elif stat.opened or stat.clicked: stat.state = 'opened' elif stat.replied: stat.state = 'replied' elif stat.bounced: stat.state = 'bounced' elif stat.sent: stat.state = 'sent' else: stat.state = 'outgoing' @api.model def create(self, values): if 'mail_mail_id' in values: values['mail_mail_id_int'] = values['mail_mail_id'] res = super(MailMailStats, self).create(values) return res def _get_records(self, mail_mail_ids=None, mail_message_ids=None, domain=None): if not self.ids and mail_mail_ids: base_domain = [('mail_mail_id_int', 'in', mail_mail_ids)] elif not self.ids and mail_message_ids: base_domain = [('message_id', 'in', mail_message_ids)] else: base_domain = [('id', 'in', self.ids)] if domain: base_domain = ['&'] + domain + base_domain return self.search(base_domain) def set_opened(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('opened', '=', False)]) statistics.write({'opened': fields.Datetime.now(), 'bounced': False}) return statistics def set_clicked(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)]) statistics.write({'clicked': fields.Datetime.now()}) return statistics def set_replied(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)]) statistics.write({'replied': fields.Datetime.now()}) return statistics def set_bounced(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('bounced', '=', False), ('opened', '=', False)]) statistics.write({'bounced': fields.Datetime.now()}) return statistics
class MrpUnbuild(models.Model): _name = "mrp.unbuild" _description = "Unbuild Order" _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'id desc' name = fields.Char('Reference', copy=False, readonly=True, default=lambda x: _('New')) product_id = fields.Many2one( 'product.product', 'Product', check_company=True, domain="[('bom_ids', '!=', False), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", required=True, states={'done': [('readonly', True)]}) company_id = fields.Many2one( 'res.company', 'Company', default=lambda s: s.env.company, required=True, index=True, states={'done': [('readonly', True)]}) product_qty = fields.Float( 'Quantity', default=1.0, required=True, states={'done': [('readonly', True)]}) product_uom_id = fields.Many2one( 'uom.uom', 'Unit of Measure', required=True, states={'done': [('readonly', True)]}) bom_id = fields.Many2one( 'mrp.bom', 'Bill of Material', domain="""[ '|', ('product_id', '=', product_id), '&', ('product_tmpl_id.product_variant_ids', '=', product_id), ('product_id','=',False), ('type', '=', 'normal'), '|', ('company_id', '=', company_id), ('company_id', '=', False) ] """, required=True, states={'done': [('readonly', True)]}, check_company=True) mo_id = fields.Many2one( 'mrp.production', 'Manufacturing Order', domain="[('state', 'in', ['done', 'cancel']), ('company_id', '=', company_id)]", states={'done': [('readonly', True)]}, check_company=True) lot_id = fields.Many2one( 'stock.production.lot', 'Lot/Serial Number', domain="[('product_id', '=', product_id), ('company_id', '=', company_id)]", check_company=True, states={'done': [('readonly', True)]}, help="Lot/Serial Number of the product to unbuild.") has_tracking=fields.Selection(related='product_id.tracking', readonly=True) location_id = fields.Many2one( 'stock.location', 'Source Location', domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", check_company=True, required=True, states={'done': [('readonly', True)]}, help="Location where the product you want to unbuild is.") location_dest_id = fields.Many2one( 'stock.location', 'Destination Location', domain="[('usage','=','internal'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", check_company=True, required=True, states={'done': [('readonly', True)]}, help="Location where you want to send the components resulting from the unbuild order.") consume_line_ids = fields.One2many( 'stock.move', 'consume_unbuild_id', readonly=True, string='Consumed Disassembly Lines') produce_line_ids = fields.One2many( 'stock.move', 'unbuild_id', readonly=True, string='Processed Disassembly Lines') state = fields.Selection([ ('draft', 'Draft'), ('done', 'Done')], string='Status', default='draft', index=True) @api.onchange('company_id') def _onchange_company_id(self): if self.company_id: warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.company_id.id)], limit=1) self.location_id = warehouse.lot_stock_id self.location_dest_id = warehouse.lot_stock_id else: self.location_id = False self.location_dest_id = False @api.onchange('mo_id') def _onchange_mo_id(self): if self.mo_id: self.product_id = self.mo_id.product_id.id self.product_qty = self.mo_id.product_qty @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: self.bom_id = self.env['mrp.bom']._bom_find(product=self.product_id, company_id=self.company_id.id) self.product_uom_id = self.product_id.uom_id.id if self.company_id: return {'domain': {'mo_id': [('state', '=', 'done'), ('product_id', '=', self.product_id.id), ('company_id', '=', self.company_id.id)]}} @api.constrains('product_qty') def _check_qty(self): if self.product_qty <= 0: raise ValueError(_('Unbuild Order product quantity has to be strictly positive.')) @api.model def create(self, vals): if not vals.get('name') or vals['name'] == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code('mrp.unbuild') or _('New') return super(MrpUnbuild, self).create(vals) def unlink(self): if 'done' in self.mapped('state'): raise UserError(_("You cannot delete an unbuild order if the state is 'Done'.")) return super(MrpUnbuild, self).unlink() def action_unbuild(self): self.ensure_one() self._check_company() if self.product_id.tracking != 'none' and not self.lot_id.id: raise UserError(_('You should provide a lot number for the final product.')) if self.mo_id: if self.mo_id.state != 'done': raise UserError(_('You cannot unbuild a undone manufacturing order.')) consume_moves = self._generate_consume_moves() consume_moves._action_confirm() produce_moves = self._generate_produce_moves() finished_move = consume_moves.filtered(lambda m: m.product_id == self.product_id) consume_moves -= finished_move if any(produce_move.has_tracking != 'none' and not self.mo_id for produce_move in produce_moves): raise UserError(_('Some of your components are tracked, you have to specify a manufacturing order in order to retrieve the correct components.')) if any(consume_move.has_tracking != 'none' and not self.mo_id for consume_move in consume_moves): raise UserError(_('Some of your byproducts are tracked, you have to specify a manufacturing order in order to retrieve the correct byproducts.')) if finished_move.has_tracking != 'none': self.env['stock.move.line'].create({ 'move_id': finished_move.id, 'lot_id': self.lot_id.id, 'qty_done': finished_move.product_uom_qty, 'product_id': finished_move.product_id.id, 'product_uom_id': finished_move.product_uom.id, 'location_id': finished_move.location_id.id, 'location_dest_id': finished_move.location_dest_id.id, }) else: finished_move.quantity_done = finished_move.product_uom_qty # TODO: Will fail if user do more than one unbuild with lot on the same MO. Need to check what other unbuild has aready took for move in produce_moves | consume_moves: if move.has_tracking != 'none': original_move = move in produce_moves and self.mo_id.move_raw_ids or self.mo_id.move_finished_ids original_move = original_move.filtered(lambda m: m.product_id == move.product_id) needed_quantity = move.product_qty moves_lines = original_move.mapped('move_line_ids') if move in produce_moves and self.lot_id: moves_lines = moves_lines.filtered(lambda ml: self.lot_id in ml.lot_produced_ids) for move_line in moves_lines: # Iterate over all move_lines until we unbuilded the correct quantity. taken_quantity = min(needed_quantity, move_line.qty_done) if taken_quantity: self.env['stock.move.line'].create({ 'move_id': move.id, 'lot_id': move_line.lot_id.id, 'qty_done': taken_quantity, 'product_id': move.product_id.id, 'product_uom_id': move_line.product_uom_id.id, 'location_id': move.location_id.id, 'location_dest_id': move.location_dest_id.id, }) needed_quantity -= taken_quantity else: move.quantity_done = move.product_uom_qty finished_move._action_done() consume_moves._action_done() produce_moves._action_done() produced_move_line_ids = produce_moves.mapped('move_line_ids').filtered(lambda ml: ml.qty_done > 0) consume_moves.mapped('move_line_ids').write({'produce_line_ids': [(6, 0, produced_move_line_ids.ids)]}) return self.write({'state': 'done'}) def _generate_consume_moves(self): moves = self.env['stock.move'] for unbuild in self: if unbuild.mo_id: finished_moves = unbuild.mo_id.move_finished_ids.filtered(lambda move: move.state == 'done') factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id) for finished_move in finished_moves: moves += unbuild._generate_move_from_existing_move(finished_move, factor, finished_move.location_dest_id, finished_move.location_id) else: factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty moves += unbuild._generate_move_from_bom_line(self.product_id, self.product_uom_id, unbuild.product_qty) for byproduct in unbuild.bom_id.byproduct_ids: quantity = byproduct.product_qty * factor moves += unbuild._generate_move_from_bom_line(byproduct.product_id, byproduct.product_uom_id, quantity, byproduct_id=byproduct.id) return moves def _generate_produce_moves(self): moves = self.env['stock.move'] for unbuild in self: if unbuild.mo_id: raw_moves = unbuild.mo_id.move_raw_ids.filtered(lambda move: move.state == 'done') factor = unbuild.product_qty / unbuild.mo_id.product_uom_id._compute_quantity(unbuild.mo_id.product_qty, unbuild.product_uom_id) for raw_move in raw_moves: moves += unbuild._generate_move_from_existing_move(raw_move, factor, raw_move.location_dest_id, self.location_dest_id) else: factor = unbuild.product_uom_id._compute_quantity(unbuild.product_qty, unbuild.bom_id.product_uom_id) / unbuild.bom_id.product_qty boms, lines = unbuild.bom_id.explode(unbuild.product_id, factor, picking_type=unbuild.bom_id.picking_type_id) for line, line_data in lines: moves += unbuild._generate_move_from_bom_line(line.product_id, line.product_uom_id, line_data['qty'], bom_line_id=line.id) return moves def _generate_move_from_existing_move(self, move, factor, location_id, location_dest_id): return self.env['stock.move'].create({ 'name': self.name, 'date': self.create_date, 'product_id': move.product_id.id, 'product_uom_qty': move.product_uom_qty * factor, 'product_uom': move.product_uom.id, 'procure_method': 'make_to_stock', 'location_dest_id': location_dest_id.id, 'location_id': location_id.id, 'warehouse_id': location_dest_id.get_warehouse().id, 'unbuild_id': self.id, 'company_id': move.company_id.id, }) def _generate_move_from_bom_line(self, product, product_uom, quantity, bom_line_id=False, byproduct_id=False): location_id = bom_line_id and product.property_stock_production or self.location_id location_dest_id = bom_line_id and self.location_dest_id or product.with_context(force_company=self.company_id.id).property_stock_production warehouse = location_dest_id.get_warehouse() return self.env['stock.move'].create({ 'name': self.name, 'date': self.create_date, 'bom_line_id': bom_line_id, 'byproduct_id': byproduct_id, 'product_id': product.id, 'product_uom_qty': quantity, 'product_uom': product_uom.id, 'procure_method': 'make_to_stock', 'location_dest_id': location_dest_id.id, 'location_id': location_id.id, 'warehouse_id': warehouse.id, 'unbuild_id': self.id, 'company_id': self.company_id.id, }) def action_validate(self): self.ensure_one() precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') available_qty = self.env['stock.quant']._get_available_quantity(self.product_id, self.location_id, self.lot_id, strict=True) if float_compare(available_qty, self.product_qty, precision_digits=precision) >= 0: return self.action_unbuild() else: return { 'name': _('Insufficient Quantity'), 'view_mode': 'form', 'res_model': 'stock.warn.insufficient.qty.unbuild', 'view_id': self.env.ref('mrp.stock_warn_insufficient_qty_unbuild_form_view').id, 'type': 'ir.actions.act_window', 'context': { 'default_product_id': self.product_id.id, 'default_location_id': self.location_id.id, 'default_unbuild_id': self.id }, 'target': 'new' }
class EagleeduExamValuation(models.Model): _name = 'eagleedu.exam.valuation' name = fields.Char(string='Name', default='New') exam_id = fields.Many2one('eagleedu.exam', string='Exam', required=True, domain=[('state', '=', 'ongoing')]) class_id = fields.Many2one('eagleedu.class', string='Class', required=True) division_id = fields.Many2one('eagleedu.class.division', string='Division', required=True) subject_id = fields.Many2one('eagleedu.syllabus', string='Subject', required=True) teachers_id = fields.Many2one('eagleedu.faculty', string='Evaluator') mark = fields.Integer(string='Max Mark', related='subject_id.total_mark') pass_mark = fields.Integer(string='Pass Mark', related='subject_id.pass_mark') tut_mark = fields.Integer('Tutorial Mark', related='subject_id.tut_mark') tut_pass_mark = fields.Integer('Tutorial Pass Mark', related='subject_id.tut_pass') subj_mark = fields.Integer('Subjective Mark', related='subject_id.subj_mark') subj_pass_mark = fields.Integer('Subjective Pass Mark', related='subject_id.subj_pass') obj_mark = fields.Integer('Objective Mark', related='subject_id.obj_mark') obj_pass_mark = fields.Integer('Objective Pass Mark', related='subject_id.obj_pass') prac_mark = fields.Integer('Practical Mark', related='subject_id.prac_mark') prac_pass_mark = fields.Integer('Practical Pass Mark', related='subject_id.prac_pass') state = fields.Selection([('draft', 'Draft'), ('completed', 'Completed'), ('cancel', 'Canceled')], default='draft') valuation_line = fields.One2many('exam.valuation.line', 'valuation_id', string='Students') mark_sheet_created = fields.Boolean(string='Mark sheet Created') date = fields.Date(string='Date', default=fields.Date.today) academic_year = fields.Many2one('eagleedu.academic.year', string='Academic Year', related='division_id.academic_year_id', store=True) company_id = fields.Many2one( 'res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get()) highest = fields.Integer('Highest mark Obtained') # @api.multi # def get_highest(self): # for rec in self: # evlauation_line=self.env['exam.valuation.line'].search([('valuation_id','=',rec.id)],order='mark_scored asc',limit=1) # # rec.highest=evlauation_line.mark_scored @api.onchange('exam_id', 'division_id') def domain4subject(self): domain = [] for rec in self: if rec.division_id.id: result_created = self.env['eagleedu.exam.valuation'].search([ ('exam_id.id', '=', rec.exam_id.id), ('division_id.id', '=', rec.division_id.id) ]) for res in result_created: domain.append(res.subject_id.id) return {'domain': {'subject_id': [('id', '!=', domain)]}} @api.onchange('tut_mark', 'tut_pass_mark', 'subj_mark', 'subj_pass_mark', 'obj_mark', 'obj_pass_mark', 'prac_mark', 'prac_pass_mark') def calculate_marks(self): for rec in self: rec.mark = rec.tut_mark + rec.subj_mark + rec.obj_mark + rec.prac_mark rec.pass_mark = rec.tut_pass_mark + rec.subj_pass_mark + rec.obj_pass_mark + rec.prac_pass_mark @api.onchange('class_id') def onchange_class_id(self): domain = [] if self.division_id.class_id != self.class_id: self.division_id = '' if self.class_id: domain = [('class_id', '=', self.class_id.id)] return {'domain': {'division_id': domain}} # @api.onchange('pass_mark') # def onchange_pass_mark(self): # if self.pass_mark > self.mark: # raise UserError(_('Pass mark must be less than Max Mark')) # for records in self.valuation_line: # if records.mark_scored >= self.pass_mark: # records.pass_or_fail = True # else: # records.pass_or_fail = False @api.onchange('exam_id', 'subject_id') def onchange_exam_id(self): if self.exam_id: if self.exam_id.division_id: self.class_id = self.exam_id.class_id self.division_id = self.exam_id.division_id elif self.exam_id.class_id: self.class_id = self.exam_id.class_id else: self.class_id = '' self.division_id = '' self.mark = '' if self.subject_id: for sub in self.exam_id.subject_line: if sub.subject_id.id == self.subject_id.id: if sub.mark: self.mark = sub.mark domain = [] subjects = self.exam_id.subject_line for items in subjects: domain.append(items.subject_id.id) return {'domain': {'subject_id': [('id', 'in', domain)]}} # @api.model def create_mark_sheet(self): valuation_line_obj = self.env['exam.valuation.line'] history = self.env['eagleedu.class.history'].search([ ('class_id', '=', self.division_id.id), '|', ('compulsory_subjects', '=', self.subject_id.id), '|', ('selective_subjects', '=', self.subject_id.id), ('optional_subjects', '=', self.subject_id.id) ]) #division_id.student_ids if len(history) < 1: raise UserError(_('There are no students in this Division')) for student in history: data = { 'student_id': student.student_id.id, 'student_name': student.student_id.name, 'valuation_id': self.id, 'tut_mark': 0, 'subj_mark': 0, 'obj_mark': 0, 'letter_grade': 'F', 'prac_mark': 0, 'grade_point': 0, } valuation_line_obj.create(data) self.mark_sheet_created = True @api.model def create(self, vals): res = super(EagleeduExamValuation, self).create(vals) valuation_obj = self.env['eagleedu.exam.valuation'] search_valuation = valuation_obj.search([ ('exam_id', '=', res.exam_id.id), ('division_id', '=', res.division_id.id), ('subject_id', '=', res.subject_id.id), ('state', '!=', 'cancel') ]) if len(search_valuation) > 1: raise UserError( _('Valuation Sheet for \n Subject --> %s \nDivision --> %s \nExam --> %s \n is already created' ) % (res.subject_id.name, res.division_id.name, res.exam_id.name)) return res # @api.multi def valuation_completed(self): self.name = str(self.exam_id.exam_type.name) + '-' + str( self.exam_id.start_date)[0:10] + ' (' + str( self.division_id.name) + ')' result_obj = self.env['eagleedu.exam.results'] result_line_obj = self.env['results.subject.line'] for students in self.valuation_line: search_result = result_obj.search([ ('exam_id', '=', self.exam_id.id), ('division_id', '=', self.division_id.id), ('student_id', '=', students.student_id.id) ]) if len(search_result) < 1: result_data = { 'name': self.name, 'exam_id': self.exam_id.id, 'class_id': self.class_id.id, 'division_id': self.division_id.id, 'student_id': students.student_id.id, 'student_name': students.student_name, } result = result_obj.create(result_data) result_line_data = { 'name': self.name, 'tut_obt': students.tut_mark, 'tut_pr': students.tut_pr, 'obj_obt': students.obj_mark, 'obj_pr': students.obj_pr, 'subj_obt': students.subj_mark, 'subj_pr': students.subj_pr, 'prac_obt': students.prac_mark, 'prac_pr': students.prac_pr, 'subject_id': self.subject_id.id, 'max_mark': self.mark, 'pass_mark': self.pass_mark, 'mark_scored': students.mark_scored, 'pass_or_fail': students.pass_or_fail, 'result_id': result.id, 'exam_id': self.exam_id.id, 'class_id': self.class_id.id, 'division_id': self.division_id.id, 'student_id': students.student_id.id, 'student_name': students.student_name, 'letter_grade': students.letter_grade, 'grade_point': students.grade_point, } result_line_obj.create(result_line_data) else: result_line_data = { 'subject_id': self.subject_id.id, 'max_mark': self.mark, 'pass_mark': self.pass_mark, 'tut_obt': students.tut_mark, 'tut_pr': students.tut_pr, 'obj_obt': students.obj_mark, 'obj_pr': students.obj_pr, 'subj_obt': students.subj_mark, 'subj_pr': students.subj_pr, 'prac_obt': students.prac_mark, 'prac_pr': students.prac_pr, 'mark_scored': students.mark_scored, 'pass_or_fail': students.pass_or_fail, 'result_id': search_result.id, 'exam_id': self.exam_id.id, 'class_id': self.class_id.id, 'division_id': self.division_id.id, 'student_id': students.student_id.id, 'student_name': students.student_name, 'letter_grade': students.letter_grade, 'grade_point': students.grade_point, } result_line_obj.create(result_line_data) self.state = 'completed' # @api.multi def set_to_draft(self): result_line_obj = self.env['results.subject.line'] result_obj = self.env['eagleedu.exam.results'] for students in self.valuation_line: search_result = result_obj.search([ ('exam_id', '=', self.exam_id.id), ('division_id', '=', self.division_id.id), ('student_id', '=', students.student_id.id) ]) for rec in search_result: rec.state = 'draft' search_result_line = result_line_obj.search([ ('result_id', '=', search_result.id), ('subject_id', '=', self.subject_id.id) ]) search_result_line.unlink() self.state = 'draft' # @api.multi def valuation_canceled(self): self.state = 'cancel'
class MailActivityType(models.Model): _inherit = "mail.activity.type" category = fields.Selection(selection_add=[('reminder', 'Reminder')])
class fee_month(models.Model): _name = 'fee.month' _rec_name = 'name' List_Of_Month = [ (1,'January'), (2,'February'), (3,'March'), (4,'April'), (5,'May'), (6,'June'), (7,'July'), (8,'August'), (9,'September'), (10,'October'), (11,'November'), (12,'December'), ] code= fields.Char(string='Code') name= fields.Selection(List_Of_Month,string='Month') year = fields.Char(string="Year") batch_id = fields.Many2one('batch',string='Batch') alt_month = fields.Boolean('Alternate Month') quater_month = fields.Boolean('Quater Month') # generate_month = fields.Boolean('Month Calculated') qtr_month = fields.Boolean('Quater Month') leave_month = fields.Boolean('Leave Month') @api.multi def name_get(self): res = [] def _month(month): val = { 1:'January', 2:'February', 3:'March', 4:'April', 5:'May', 6:'June', 7:'July', 8:'August', 9:'September', 10:'October', 11:'November', 12:'December', } for i in val: if val.get(month): return val[month] for record in self: name = _month(record.name) res.append((record.id, name)) return res @api.model def allocation_alt_qtr_half_month_false(self, batch_id): for month_id in self.batch_id.month_ids.search([('batch_id','=',batch_id)]): month_id.write({'alt_month': False, 'quater_month': False, 'qtr_month': False}) @api.model def allocation_alt_qtr_half_month(self, batch_id): code = 0 total_month = self.batch_id.month_ids.search_count([('batch_id','=',batch_id),('leave_month','=',False)]) if total_month % 2 == 0: half_month = total_month / 2 else: total_month += 1 half_month = total_month / 2 for month_id in self.batch_id.month_ids.search([('batch_id','=',batch_id)]): if month_id.leave_month == False: code += 1 # half year if code in [1,half_month+1]: month_id.quater_month = True else: month_id.quater_month = False # Quater month if code % 3 == 1: month_id.qtr_month = True else: month_id.qtr_month = False # Alter month if code % 2 == 0: month_id.alt_month = False else: month_id.alt_month = True @api.multi def make_it_leave_month(self): self.leave_month = True batch_id = self.batch_id.id self.allocation_alt_qtr_half_month_false(batch_id) self.allocation_alt_qtr_half_month(batch_id) @api.multi def make_it_unleave_month(self): self.leave_month = False batch_id = self.batch_id.id self.allocation_alt_qtr_half_month_false(batch_id) self.allocation_alt_qtr_half_month(batch_id)
class PurchaseOrderLine(models.Model): _name = 'purchase.order.line' _description = 'Purchase Order Line' _order = 'order_id, sequence, id' name = fields.Text(string='Description', required=True) sequence = fields.Integer(string='Sequence', default=10) product_qty = fields.Float(string='Quantity', digits='Product Unit of Measure', required=True) product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True) date_planned = fields.Datetime(string='Scheduled Date', index=True) taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) product_uom = fields.Many2one('uom.uom', string='Unit of Measure', domain="[('category_id', '=', product_uom_category_id)]") product_uom_category_id = fields.Many2one(related='product_id.uom_id.category_id') product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True) product_type = fields.Selection(related='product_id.type', readonly=True) price_unit = fields.Float(string='Unit Price', required=True, digits='Product Price') price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True) price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True) price_tax = fields.Float(compute='_compute_amount', string='Tax', store=True) order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade') account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True) state = fields.Selection(related='order_id.state', store=True, readonly=False) invoice_lines = fields.One2many('account.move.line', 'purchase_line_id', string="Bill Lines", readonly=True, copy=False) # Replace by invoiced Qty qty_invoiced = fields.Float(compute='_compute_qty_invoiced', string="Billed Qty", digits='Product Unit of Measure', store=True) qty_received_method = fields.Selection([('manual', 'Manual')], string="Received Qty Method", compute='_compute_qty_received_method', store=True, help="According to product configuration, the recieved quantity can be automatically computed by mechanism :\n" " - Manual: the quantity is set manually on the line\n" " - Stock Moves: the quantity comes from confirmed pickings\n") qty_received = fields.Float("Received Qty", compute='_compute_qty_received', inverse='_inverse_qty_received', compute_sudo=True, store=True, digits='Product Unit of Measure') qty_received_manual = fields.Float("Manual Received Qty", digits='Product Unit of Measure', copy=False) partner_id = fields.Many2one('res.partner', related='order_id.partner_id', string='Partner', readonly=True, store=True) currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True) date_order = fields.Datetime(related='order_id.date_order', string='Order Date', readonly=True) display_type = fields.Selection([ ('line_section', "Section"), ('line_note', "Note")], default=False, help="Technical field for UX purpose.") _sql_constraints = [ ('accountable_required_fields', "CHECK(display_type IS NOT NULL OR (product_id IS NOT NULL AND product_uom IS NOT NULL AND date_planned IS NOT NULL))", "Missing required fields on accountable purchase order line."), ('non_accountable_null_fields', "CHECK(display_type IS NULL OR (product_id IS NULL AND price_unit = 0 AND product_uom_qty = 0 AND product_uom IS NULL AND date_planned is NULL))", "Forbidden values on non-accountable purchase order line"), ] @api.depends('product_qty', 'price_unit', 'taxes_id') def _compute_amount(self): for line in self: vals = line._prepare_compute_all_values() taxes = line.taxes_id.compute_all( vals['price_unit'], vals['currency_id'], vals['product_qty'], vals['product'], vals['partner']) line.update({ 'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])), 'price_total': taxes['total_included'], 'price_subtotal': taxes['total_excluded'], }) def _prepare_compute_all_values(self): # Hook method to returns the different argument values for the # compute_all method, due to the fact that discounts mechanism # is not implemented yet on the purchase orders. # This method should disappear as soon as this feature is # also introduced like in the sales module. self.ensure_one() return { 'price_unit': self.price_unit, 'currency_id': self.order_id.currency_id, 'product_qty': self.product_qty, 'product': self.product_id, 'partner': self.order_id.partner_id, } def _compute_tax_id(self): for line in self: fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.with_context(force_company=line.company_id.id).property_account_position_id # If company_id is set, always filter taxes by the company taxes = line.product_id.supplier_taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id) line.taxes_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_id) if fpos else taxes @api.depends('invoice_lines.move_id.state', 'invoice_lines.quantity') def _compute_qty_invoiced(self): for line in self: qty = 0.0 for inv_line in line.invoice_lines: if inv_line.move_id.state not in ['cancel']: if inv_line.move_id.type == 'in_invoice': qty += inv_line.product_uom_id._compute_quantity(inv_line.quantity, line.product_uom) elif inv_line.move_id.type == 'in_refund': qty -= inv_line.product_uom_id._compute_quantity(inv_line.quantity, line.product_uom) line.qty_invoiced = qty @api.depends('product_id') def _compute_qty_received_method(self): for line in self: if line.product_id and line.product_id.type in ['consu', 'service']: line.qty_received_method = 'manual' else: line.qty_received_method = False @api.depends('qty_received_method', 'qty_received_manual') def _compute_qty_received(self): for line in self: if line.qty_received_method == 'manual': line.qty_received = line.qty_received_manual or 0.0 else: line.qty_received = 0.0 @api.onchange('qty_received') def _inverse_qty_received(self): """ When writing on qty_received, if the value should be modify manually (`qty_received_method` = 'manual' only), then we put the value in `qty_received_manual`. Otherwise, `qty_received_manual` should be False since the received qty is automatically compute by other mecanisms. """ for line in self: if line.qty_received_method == 'manual': line.qty_received_manual = line.qty_received else: line.qty_received_manual = 0.0 @api.model def create(self, values): if values.get('display_type', self.default_get(['display_type'])['display_type']): values.update(product_id=False, price_unit=0, product_uom_qty=0, product_uom=False, date_planned=False) order_id = values.get('order_id') if 'date_planned' not in values: order = self.env['purchase.order'].browse(order_id) if order.date_planned: values['date_planned'] = order.date_planned line = super(PurchaseOrderLine, self).create(values) if line.order_id.state == 'purchase': msg = _("Extra line with %s ") % (line.product_id.display_name,) line.order_id.message_post(body=msg) return line def write(self, values): if 'display_type' in values and self.filtered(lambda line: line.display_type != values.get('display_type')): raise UserError("You cannot change the type of a purchase order line. Instead you should delete the current line and create a new line of the proper type.") if 'product_qty' in values: for line in self: if line.order_id.state == 'purchase': line.order_id.message_post_with_view('purchase.track_po_line_template', values={'line': line, 'product_qty': values['product_qty']}, subtype_id=self.env.ref('mail.mt_note').id) return super(PurchaseOrderLine, self).write(values) def unlink(self): for line in self: if line.order_id.state in ['purchase', 'done']: raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') % (line.state,)) return super(PurchaseOrderLine, self).unlink() @api.model def _get_date_planned(self, seller, po=False): """Return the datetime value to use as Schedule Date (``date_planned``) for PO Lines that correspond to the given product.seller_ids, when ordered at `date_order_str`. :param Model seller: used to fetch the delivery delay (if no seller is provided, the delay is 0) :param Model po: purchase.order, necessary only if the PO line is not yet attached to a PO. :rtype: datetime :return: desired Schedule Date for the PO line """ date_order = po.date_order if po else self.order_id.date_order if date_order: return date_order + relativedelta(days=seller.delay if seller else 0) else: return datetime.today() + relativedelta(days=seller.delay if seller else 0) @api.onchange('product_id') def onchange_product_id(self): if not self.product_id: return # Reset date, price and quantity since _onchange_quantity will provide default values self.date_planned = datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT) self.price_unit = self.product_qty = 0.0 self._product_id_change() self._suggest_quantity() self._onchange_quantity() def _product_id_change(self): if not self.product_id: return self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id product_lang = self.product_id.with_context( lang=self.partner_id.lang, partner_id=self.partner_id.id, company_id=self.company_id.id, ) self.name = self._get_product_purchase_description(product_lang) self._compute_tax_id() @api.onchange('product_id') def onchange_product_id_warning(self): if not self.product_id or not self.env.user.has_group('purchase.group_warning_purchase'): return warning = {} title = False message = False product_info = self.product_id if product_info.purchase_line_warn != 'no-message': title = _("Warning for %s") % product_info.name message = product_info.purchase_line_warn_msg warning['title'] = title warning['message'] = message if product_info.purchase_line_warn == 'block': self.product_id = False return {'warning': warning} return {} @api.onchange('product_qty', 'product_uom') def _onchange_quantity(self): if not self.product_id: return params = {'order_id': self.order_id} seller = self.product_id._select_seller( partner_id=self.partner_id, quantity=self.product_qty, date=self.order_id.date_order and self.order_id.date_order.date(), uom_id=self.product_uom, params=params) if seller or not self.date_planned: self.date_planned = self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT) if not seller: if self.product_id.seller_ids.filtered(lambda s: s.name.id == self.partner_id.id): self.price_unit = 0.0 return price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, self.product_id.supplier_taxes_id, self.taxes_id, self.company_id) if seller else 0.0 if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id: price_unit = seller.currency_id._convert( price_unit, self.order_id.currency_id, self.order_id.company_id, self.date_order or fields.Date.today()) if seller and self.product_uom and seller.product_uom != self.product_uom: price_unit = seller.product_uom._compute_price(price_unit, self.product_uom) self.price_unit = price_unit @api.depends('product_uom', 'product_qty', 'product_id.uom_id') def _compute_product_uom_qty(self): for line in self: if line.product_id and line.product_id.uom_id != line.product_uom: line.product_uom_qty = line.product_uom._compute_quantity(line.product_qty, line.product_id.uom_id) else: line.product_uom_qty = line.product_qty def _suggest_quantity(self): ''' Suggest a minimal quantity based on the seller ''' if not self.product_id: return seller_min_qty = self.product_id.seller_ids\ .filtered(lambda r: r.name == self.order_id.partner_id and (not r.product_id or r.product_id == self.product_id))\ .sorted(key=lambda r: r.min_qty) if seller_min_qty: self.product_qty = seller_min_qty[0].min_qty or 1.0 self.product_uom = seller_min_qty[0].product_uom else: self.product_qty = 1.0 def _get_product_purchase_description(self, product_lang): self.ensure_one() name = product_lang.display_name if product_lang.description_purchase: name += '\n' + product_lang.description_purchase return name def _prepare_account_move_line(self, move): self.ensure_one() if self.product_id.purchase_method == 'purchase': qty = self.product_qty - self.qty_invoiced else: qty = self.qty_received - self.qty_invoiced if float_compare(qty, 0.0, precision_rounding=self.product_uom.rounding) <= 0: qty = 0.0 if self.currency_id == move.company_id.currency_id: currency = False else: currency = move.currency_id return { 'name': '%s: %s' % (self.order_id.name, self.name), 'move_id': move.id, 'currency_id': currency and currency.id or False, 'purchase_line_id': self.id, 'date_maturity': move.invoice_date_due, 'product_uom_id': self.product_uom.id, 'product_id': self.product_id.id, 'price_unit': self.price_unit, 'quantity': qty, 'partner_id': move.partner_id.id, 'analytic_account_id': self.account_analytic_id.id, 'analytic_tag_ids': [(6, 0, self.analytic_tag_ids.ids)], 'tax_ids': [(6, 0, self.taxes_id.ids)], 'display_type': self.display_type, }
class ResourceCalendar(models.Model): """ Calendar model for a resource. It has - attendance_ids: list of resource.calendar.attendance that are a working interval in a given weekday. - leave_ids: list of leaves linked to this calendar. A leave can be general or linked to a specific resource, depending on its resource_id. All methods in this class use intervals. An interval is a tuple holding (begin_datetime, end_datetime). A list of intervals is therefore a list of tuples, holding several intervals of work or leaves. """ _name = "resource.calendar" _description = "Resource Working Time" @api.model def default_get(self, fields): res = super(ResourceCalendar, self).default_get(fields) if not res.get('name') and res.get('company_id'): res['name'] = _( 'Working Hours of %s') % self.env['res.company'].browse( res['company_id']).name if 'attendance_ids' in fields and not res.get('attendance_ids'): res['attendance_ids'] = [(0, 0, { 'name': _('Monday Morning'), 'dayofweek': '0', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Monday Afternoon'), 'dayofweek': '0', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' }), (0, 0, { 'name': _('Tuesday Morning'), 'dayofweek': '1', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Tuesday Afternoon'), 'dayofweek': '1', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' }), (0, 0, { 'name': _('Wednesday Morning'), 'dayofweek': '2', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Wednesday Afternoon'), 'dayofweek': '2', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' }), (0, 0, { 'name': _('Thursday Morning'), 'dayofweek': '3', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Thursday Afternoon'), 'dayofweek': '3', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' }), (0, 0, { 'name': _('Friday Morning'), 'dayofweek': '4', 'hour_from': 8, 'hour_to': 12, 'day_period': 'morning' }), (0, 0, { 'name': _('Friday Afternoon'), 'dayofweek': '4', 'hour_from': 13, 'hour_to': 17, 'day_period': 'afternoon' })] return res name = fields.Char(required=True) company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company) attendance_ids = fields.One2many('resource.calendar.attendance', 'calendar_id', 'Working Time', copy=True) leave_ids = fields.One2many('resource.calendar.leaves', 'calendar_id', 'Time Off') global_leave_ids = fields.One2many('resource.calendar.leaves', 'calendar_id', 'Global Time Off', domain=[('resource_id', '=', False)]) hours_per_day = fields.Float( "Average Hour per Day", default=HOURS_PER_DAY, help= "Average hours per day a resource is supposed to work with this calendar." ) tz = fields.Selection( _tz_get, string='Timezone', required=True, default=lambda self: self._context.get('tz' ) or self.env.user.tz or 'UTC', help= "This field is used in order to define in which timezone the resources will work." ) two_weeks_calendar = fields.Boolean(string="Calendar in 2 weeks mode") two_weeks_explanation = fields.Char( 'Explanation', compute="_compute_two_weeks_explanation") @api.depends('two_weeks_calendar') def _compute_two_weeks_explanation(self): today = fields.Date.today() week_type = _("odd") if int( math.floor((today.toordinal() - 1) / 7) % 2) else _("even") first_day = date_utils.start_of(today, 'week') last_day = date_utils.end_of(today, 'week') self.two_weeks_explanation = "This week (from %s to %s) is an %s week." % ( first_day, last_day, week_type) def _get_global_attendances(self): return self.attendance_ids.filtered( lambda attendance: not attendance.date_from and not attendance. date_to and not attendance.resource_id and not attendance. display_type) def _compute_hours_per_day(self, attendances): if not attendances: return 0 hour_count = 0.0 for attendance in attendances: hour_count += attendance.hour_to - attendance.hour_from if self.two_weeks_calendar: number_of_days = len( set( attendances.filtered( lambda cal: cal.week_type == '1').mapped('dayofweek'))) number_of_days += len( set( attendances.filtered( lambda cal: cal.week_type == '0').mapped('dayofweek'))) else: number_of_days = len(set(attendances.mapped('dayofweek'))) return float_round(hour_count / float(number_of_days), precision_digits=2) @api.onchange('attendance_ids', 'two_weeks_calendar') def _onchange_hours_per_day(self): attendances = self._get_global_attendances() self.hours_per_day = self._compute_hours_per_day(attendances) def switch_calendar_type(self): if not self.two_weeks_calendar: self.attendance_ids.unlink() self.attendance_ids = [ (0, 0, { 'name': 'Even week', 'dayofweek': '0', 'sequence': '0', 'hour_from': 0, 'day_period': 'morning', 'week_type': '0', 'hour_to': 0, 'display_type': 'line_section' }), (0, 0, { 'name': 'Odd week', 'dayofweek': '0', 'sequence': '25', 'hour_from': 0, 'day_period': 'morning', 'week_type': '1', 'hour_to': 0, 'display_type': 'line_section' }), ] self.two_weeks_calendar = True default_attendance = self.default_get( 'attendance_ids')['attendance_ids'] for idx, att in enumerate(default_attendance): att[2]["week_type"] = '0' att[2]["sequence"] = idx + 1 self.attendance_ids = default_attendance for idx, att in enumerate(default_attendance): att[2]["week_type"] = '1' att[2]["sequence"] = idx + 26 self.attendance_ids = default_attendance else: self.two_weeks_calendar = False self.attendance_ids.unlink() self.attendance_ids = self.default_get( 'attendance_ids')['attendance_ids'] self._onchange_hours_per_day() @api.onchange('attendance_ids') def _onchange_attendance_ids(self): if not self.two_weeks_calendar: return even_week_seq = self.attendance_ids.filtered( lambda att: att.display_type == 'line_section' and att.week_type == '0') odd_week_seq = self.attendance_ids.filtered( lambda att: att.display_type == 'line_section' and att.week_type == '1') if len(even_week_seq) != 1 or len(odd_week_seq) != 1: raise ValidationError(_("You can't delete section between weeks.")) even_week_seq = even_week_seq.sequence odd_week_seq = odd_week_seq.sequence for line in self.attendance_ids.filtered( lambda att: att.display_type is False): if even_week_seq > odd_week_seq: line.week_type = '1' if even_week_seq > line.sequence else '0' else: line.week_type = '0' if odd_week_seq > line.sequence else '1' def _check_overlap(self, attendance_ids): """ attendance_ids correspond to attendance of a week, will check for each day of week that there are no superimpose. """ result = [] for attendance in attendance_ids.filtered( lambda att: not att.date_from and not att.date_to): # 0.000001 is added to each start hour to avoid to detect two contiguous intervals as superimposing. # Indeed Intervals function will join 2 intervals with the start and stop hour corresponding. result.append( (int(attendance.dayofweek) * 24 + attendance.hour_from + 0.000001, int(attendance.dayofweek) * 24 + attendance.hour_to, attendance)) if len(Intervals(result)) != len(result): raise ValidationError(_("Attendances can't overlap.")) @api.constrains('attendance_ids') def _check_attendance(self): # Avoid superimpose in attendance for calendar in self: attendance_ids = calendar.attendance_ids.filtered( lambda attendance: not attendance.resource_id and attendance. display_type is False) if calendar.two_weeks_calendar: calendar._check_overlap( attendance_ids.filtered( lambda attendance: attendance.week_type == '0')) calendar._check_overlap( attendance_ids.filtered( lambda attendance: attendance.week_type == '1')) else: calendar._check_overlap(attendance_ids) # -------------------------------------------------- # Computation API # -------------------------------------------------- def _attendance_intervals(self, start_dt, end_dt, resource=None, domain=None): """ Return the attendance intervals in the given datetime range. The returned intervals are expressed in the resource's timezone. """ assert start_dt.tzinfo and end_dt.tzinfo combine = datetime.combine resource_ids = [resource.id, False] if resource else [False] domain = domain if domain is not None else [] domain = expression.AND([ domain, [ ('calendar_id', '=', self.id), ('resource_id', 'in', resource_ids), ('display_type', '=', False), ] ]) # express all dates and times in the resource's timezone tz = timezone((resource or self).tz) start_dt = start_dt.astimezone(tz) end_dt = end_dt.astimezone(tz) # for each attendance spec, generate the intervals in the date range result = [] for attendance in self.env['resource.calendar.attendance'].search( domain): start = start_dt.date() if attendance.date_from: start = max(start, attendance.date_from) until = end_dt.date() if attendance.date_to: until = min(until, attendance.date_to) if attendance.week_type: start_week_type = int( math.floor((start.toordinal() - 1) / 7) % 2) if start_week_type != int(attendance.week_type): # start must be the week of the attendance # if it's not the case, we must remove one week start = start + relativedelta(weeks=-1) weekday = int(attendance.dayofweek) if self.two_weeks_calendar and attendance.week_type: days = rrule(WEEKLY, start, interval=2, until=until, byweekday=weekday) else: days = rrule(DAILY, start, until=until, byweekday=weekday) for day in days: # attendance hours are interpreted in the resource's timezone dt0 = tz.localize( combine(day, float_to_time(attendance.hour_from))) dt1 = tz.localize( combine(day, float_to_time(attendance.hour_to))) result.append((max(start_dt, dt0), min(end_dt, dt1), attendance)) return Intervals(result) def _leave_intervals(self, start_dt, end_dt, resource=None, domain=None): """ Return the leave intervals in the given datetime range. The returned intervals are expressed in the calendar's timezone. """ assert start_dt.tzinfo and end_dt.tzinfo self.ensure_one() # for the computation, express all datetimes in UTC resource_ids = [resource.id, False] if resource else [False] if domain is None: domain = [('time_type', '=', 'leave')] domain = domain + [ ('calendar_id', '=', self.id), ('resource_id', 'in', resource_ids), ('date_from', '<=', datetime_to_string(end_dt)), ('date_to', '>=', datetime_to_string(start_dt)), ] # retrieve leave intervals in (start_dt, end_dt) tz = timezone((resource or self).tz) start_dt = start_dt.astimezone(tz) end_dt = end_dt.astimezone(tz) result = [] for leave in self.env['resource.calendar.leaves'].search(domain): dt0 = string_to_datetime(leave.date_from).astimezone(tz) dt1 = string_to_datetime(leave.date_to).astimezone(tz) result.append((max(start_dt, dt0), min(end_dt, dt1), leave)) return Intervals(result) def _work_intervals(self, start_dt, end_dt, resource=None, domain=None): """ Return the effective work intervals between the given datetimes. """ return (self._attendance_intervals(start_dt, end_dt, resource) - self._leave_intervals(start_dt, end_dt, resource, domain)) # -------------------------------------------------- # Private Methods / Helpers # -------------------------------------------------- def _get_days_data(self, intervals, day_total): """ helper function to compute duration of `intervals` expressed in days and hours. `day_total` is a dict {date: n_hours} with the number of hours for each day. """ day_hours = defaultdict(float) for start, stop, meta in intervals: day_hours[start.date()] += (stop - start).total_seconds() / 3600 # compute number of days as quarters days = sum( float_utils.round(ROUNDING_FACTOR * day_hours[day] / day_total[day]) / ROUNDING_FACTOR for day in day_hours) return { 'days': days, 'hours': sum(day_hours.values()), } def _get_day_total(self, from_datetime, to_datetime, resource=None): """ @return dict with hours of attendance in each day between `from_datetime` and `to_datetime` """ self.ensure_one() # total hours per day: retrieve attendances with one extra day margin, # in order to compute the total hours on the first and last days from_full = from_datetime - timedelta(days=1) to_full = to_datetime + timedelta(days=1) intervals = self._attendance_intervals(from_full, to_full, resource=resource) day_total = defaultdict(float) for start, stop, meta in intervals: day_total[start.date()] += (stop - start).total_seconds() / 3600 return day_total # -------------------------------------------------- # External API # -------------------------------------------------- def get_work_hours_count(self, start_dt, end_dt, compute_leaves=True, domain=None): """ `compute_leaves` controls whether or not this method is taking into account the global leaves. `domain` controls the way leaves are recognized. None means default value ('time_type', '=', 'leave') Counts the number of work hours between two datetimes. """ # Set timezone in UTC if no timezone is explicitly given if not start_dt.tzinfo: start_dt = start_dt.replace(tzinfo=utc) if not end_dt.tzinfo: end_dt = end_dt.replace(tzinfo=utc) if compute_leaves: intervals = self._work_intervals(start_dt, end_dt, domain=domain) else: intervals = self._attendance_intervals(start_dt, end_dt) return sum((stop - start).total_seconds() / 3600 for start, stop, meta in intervals) def get_work_duration_data(self, from_datetime, to_datetime, compute_leaves=True, domain=None): """ Get the working duration (in days and hours) for a given period, only based on the current calendar. This method does not use resource to compute it. `domain` is used in order to recognise the leaves to take, None means default value ('time_type', '=', 'leave') Returns a dict {'days': n, 'hours': h} containing the quantity of working time expressed as days and as hours. """ # naive datetimes are made explicit in UTC from_datetime, dummy = make_aware(from_datetime) to_datetime, dummy = make_aware(to_datetime) day_total = self._get_day_total(from_datetime, to_datetime) # actual hours per day if compute_leaves: intervals = self._work_intervals(from_datetime, to_datetime, domain=domain) else: intervals = self._attendance_intervals(from_datetime, to_datetime) return self._get_days_data(intervals, day_total) def plan_hours(self, hours, day_dt, compute_leaves=False, domain=None, resource=None): """ `compute_leaves` controls whether or not this method is taking into account the global leaves. `domain` controls the way leaves are recognized. None means default value ('time_type', '=', 'leave') Return datetime after having planned hours """ day_dt, revert = make_aware(day_dt) # which method to use for retrieving intervals if compute_leaves: get_intervals = partial(self._work_intervals, domain=domain, resource=resource) else: get_intervals = self._attendance_intervals if hours >= 0: delta = timedelta(days=14) for n in range(100): dt = day_dt + delta * n for start, stop, meta in get_intervals(dt, dt + delta): interval_hours = (stop - start).total_seconds() / 3600 if hours <= interval_hours: return revert(start + timedelta(hours=hours)) hours -= interval_hours return False else: hours = abs(hours) delta = timedelta(days=14) for n in range(100): dt = day_dt - delta * n for start, stop, meta in reversed(get_intervals( dt - delta, dt)): interval_hours = (stop - start).total_seconds() / 3600 if hours <= interval_hours: return revert(stop - timedelta(hours=hours)) hours -= interval_hours return False def plan_days(self, days, day_dt, compute_leaves=False, domain=None): """ `compute_leaves` controls whether or not this method is taking into account the global leaves. `domain` controls the way leaves are recognized. None means default value ('time_type', '=', 'leave') Returns the datetime of a days scheduling. """ day_dt, revert = make_aware(day_dt) # which method to use for retrieving intervals if compute_leaves: get_intervals = partial(self._work_intervals, domain=domain) else: get_intervals = self._attendance_intervals if days > 0: found = set() delta = timedelta(days=14) for n in range(100): dt = day_dt + delta * n for start, stop, meta in get_intervals(dt, dt + delta): found.add(start.date()) if len(found) == days: return revert(stop) return False elif days < 0: days = abs(days) found = set() delta = timedelta(days=14) for n in range(100): dt = day_dt - delta * n for start, stop, meta in reversed(get_intervals( dt - delta, dt)): found.add(start.date()) if len(found) == days: return revert(start) return False else: return revert(day_dt)