class Configuration( ModelSingleton, ModelSQL, ModelView, CompanyMultiValueMixin): 'Contract Configuration' __name__ = 'contract.configuration' contract_sequence = fields.MultiValue(fields.Many2One( 'ir.sequence', "Contract Reference Sequence", required=True, domain=[ ('company', 'in', [Eval('context', {}).get('company', -1), None]), ('code', '=', 'contract'), ])) journal = fields.MultiValue(fields.Many2One( 'account.journal', "Journal", required=True, domain=[ ('type', '=', 'revenue'), ])) default_months_renewal = fields.Integer('Review Months Renewal') default_review_limit_date = fields.TimeDelta('Limit Date', help="The deadline date on which the actions should be performed.") default_review_alarm = fields.TimeDelta('Alarm Date', help="The date when actions related to reviews should start to be managed.") @classmethod def multivalue_model(cls, field): pool = Pool() if field == 'contract_sequence': return pool.get('contract.configuration.sequence') elif field == 'journal': return pool.get('contract.configuration.account') return super(Configuration, cls).multivalue_model(field) @classmethod def default_contract_sequence(cls, **pattern): return cls.multivalue_model( 'contract_sequence').default_contract_sequence()
class ExportData(ModelSQL): "Export Data" __name__ = 'test.export_data' boolean = fields.Boolean('Boolean') integer = fields.Integer('Integer') float = fields.Float('Float') numeric = fields.Numeric('Numeric') char = fields.Char('Char') text = fields.Text('Text') date = fields.Date('Date') datetime = fields.DateTime('DateTime') timedelta = fields.TimeDelta('TimeDelta') selection = fields.Selection([ (None, ''), ('select1', 'Select 1'), ('select2', 'Select 2'), ], 'Selection') many2one = fields.Many2One('test.export_data.target', 'Many2One') many2many = fields.Many2Many('test.export_data.relation', 'many2many', 'target', 'Many2Many') one2many = fields.One2Many('test.export_data.target', 'one2many', 'One2Many') reference = fields.Reference('Reference', [ (None, ''), ('test.export_data.target', 'Target'), ])
class HoursEmployeeWeekly(ModelSQL, ModelView): 'Hours per Employee per Week' __name__ = 'timesheet.hours_employee_weekly' year = fields.Integer("Year") week = fields.Integer("Week") employee = fields.Many2One('company.employee', 'Employee') duration = fields.TimeDelta('Duration', 'company_work_time') @classmethod def __setup__(cls): super(HoursEmployeeWeekly, cls).__setup__() cls._order.insert(0, ('year', 'DESC')) cls._order.insert(1, ('week', 'DESC')) cls._order.insert(2, ('employee', 'ASC')) @classmethod def table_query(cls): pool = Pool() Line = pool.get('timesheet.line') line = Line.__table__() year_column = Extract('YEAR', line.date).as_('year') week_column = Extract('WEEK', line.date).as_('week') return line.select(Max( Extract('WEEK', line.date) + Extract('YEAR', line.date) * 100 + line.employee * 1000000).as_('id'), Max(line.create_uid).as_('create_uid'), Max(line.create_date).as_('create_date'), Max(line.write_uid).as_('write_uid'), Max(line.write_date).as_('write_date'), year_column, week_column, line.employee, Sum(line.duration).as_('duration'), group_by=(year_column, week_column, line.employee))
class HoursEmployee(ModelSQL, ModelView): 'Hours per Employee' __name__ = 'timesheet.hours_employee' employee = fields.Many2One('company.employee', 'Employee') duration = fields.TimeDelta('Duration', 'company_work_time') @staticmethod def table_query(): pool = Pool() Line = pool.get('timesheet.line') line = Line.__table__() where = Literal(True) if Transaction().context.get('start_date'): where &= line.date >= Transaction().context['start_date'] if Transaction().context.get('end_date'): where &= line.date <= Transaction().context['end_date'] return line.select(line.employee.as_('id'), Max(line.create_uid).as_('create_uid'), Max(line.create_date).as_('create_date'), Max(line.write_uid).as_('write_uid'), Max(line.write_date).as_('write_date'), line.employee, Sum(line.duration).as_('duration'), where=where, group_by=line.employee)
class Level(sequence_ordered(), ModelSQL, ModelView): 'Account Dunning Level' __name__ = 'account.dunning.level' procedure = fields.Many2One('account.dunning.procedure', 'Procedure', required=True, select=True) overdue = fields.TimeDelta('Overdue', help="When the level should be applied.") @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() table = cls.__table_handler__(module_name) sql_table = cls.__table__() super(Level, cls).__register__(module_name) # Migration from 4.0: change days into timedelta overdue if table.column_exist('days'): cursor.execute(*sql_table.select( sql_table.id, sql_table.days, where=sql_table.days != Null)) for id_, days in cursor.fetchall(): overdue = datetime.timedelta(days) cursor.execute(*sql_table.update( [sql_table.overdue], [overdue], where=sql_table.id == id_)) table.drop_column('days') def get_rec_name(self, name): return '%s@%s' % (self.procedure.levels.index(self), self.procedure.rec_name) def test(self, line, date): if self.overdue is not None: return (date - line.maturity_date) >= self.overdue
class Service(DeactivableMixin, ModelSQL, ModelView): "Subscription Service" __name__ = 'sale.subscription.service' product = fields.Many2One('product.product', "Product", required=True, domain=[ ('type', '=', 'service'), ]) consumption_recurrence = fields.Many2One( 'sale.subscription.recurrence.rule.set', "Consumption Recurrence") consumption_delay = fields.TimeDelta("Consumption Delay", states={ 'invisible': ~Eval('consumption_recurrence'), }, depends=['consumption_recurrence']) def get_rec_name(self, name): return self.product.rec_name @classmethod def search_rec_name(cls, name, clause): return [('product.rec_name', ) + tuple(clause[1:])]
class Configuration(ModelSingleton, ModelSQL, ModelView, CompanyMultiValueMixin): 'Sale Configuration' __name__ = 'sale.configuration' sale_sequence = fields.MultiValue( fields.Many2One('ir.sequence', "Sale Sequence", required=True, domain=[ ('company', 'in', [Eval('context', {}).get('company', -1), None]), ('code', '=', 'sale.sale'), ])) sale_invoice_method = fields.MultiValue(sale_invoice_method) get_sale_invoice_methods = get_sale_methods('invoice_method') sale_shipment_method = fields.MultiValue(sale_shipment_method) get_sale_shipment_methods = get_sale_methods('shipment_method') sale_process_after = fields.TimeDelta( "Process Sale after", help="The grace period during which confirmed sale " "can still be reset to draft.\n" "Applied if a worker queue is activated.") @classmethod def multivalue_model(cls, field): pool = Pool() if field in {'sale_invoice_method', 'sale_shipment_method'}: return pool.get('sale.configuration.sale_method') if field == 'sale_sequence': return pool.get('sale.configuration.sequence') return super(Configuration, cls).multivalue_model(field) default_sale_sequence = default_func('sale_sequence') default_sale_invoice_method = default_func('sale_invoice_method') default_sale_shipment_method = default_func('sale_shipment_method')
class LocationLeadTime(sequence_ordered(), ModelSQL, ModelView, MatchMixin): 'Location Lead Time' __name__ = 'stock.location.lead_time' warehouse_from = fields.Many2One('stock.location', 'Warehouse From', ondelete='CASCADE', domain=[ ('type', '=', 'warehouse'), ]) warehouse_to = fields.Many2One('stock.location', 'Warehouse To', ondelete='CASCADE', domain=[ ('type', '=', 'warehouse'), ]) lead_time = fields.TimeDelta( 'Lead Time', help="The time it takes to move stock between the warehouses.") @classmethod def get_lead_time(cls, pattern): for record in cls.search([]): if record.match(pattern): return record.lead_time
class ProductionLeadTime(sequence_ordered(), ModelSQL, ModelView, MatchMixin): 'Production Lead Time' __name__ = 'production.lead_time' product = fields.Many2One('product.product', 'Product', ondelete='CASCADE', select=True, required=True, domain=[ ('type', '!=', 'service'), ]) bom = fields.Many2One('production.bom', 'BOM', ondelete='CASCADE', domain=[ ('output_products', '=', If(Bool(Eval('product')), Eval('product', -1), Get(Eval('_parent_product', {}), 'id', 0))), ], depends=['product']) lead_time = fields.TimeDelta('Lead Time') @classmethod def __setup__(cls): super(ProductionLeadTime, cls).__setup__() cls._order.insert(0, ('product', 'ASC'))
class Sheet(ModelSQL, ModelView): "Attendance Sheet" __name__ = 'attendance.sheet' company = fields.Many2One('company.company', "Company") employee = fields.Many2One('company.employee', "Employee") duration = fields.TimeDelta("Duration") date = fields.Date("Date") lines = fields.One2Many('attendance.sheet.line', 'sheet', "Lines") @classmethod def __setup__(cls): super().__setup__() cls._order.insert(0, ('date', 'DESC')) @classmethod def table_query(cls): pool = Pool() Line = pool.get('attendance.sheet.line') line = Line.__table__() return line.select( (Min(line.id * 2)).as_('id'), Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'), cls.write_date.sql_cast(Literal(Null)).as_('write_date'), line.company.as_('company'), line.employee.as_('employee'), Sum(line.duration).as_('duration'), line.date.as_('date'), group_by=[line.company, line.employee, line.date])
class TimeDeltaDefault(ModelSQL): 'TimeDelta Default' __name__ = 'test.timedelta_default' timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta', required=False) @staticmethod def default_timedelta(): return datetime.timedelta(seconds=3600)
class AdvancePaymentTermLine(ModelView, ModelSQL, CompanyMultiValueMixin): "Advance Payment Term Line" __name__ = 'sale.advance_payment_term.line' _rec_name = 'description' advance_payment_term = fields.Many2One( 'sale.advance_payment_term', "Advance Payment Term", required=True, ondelete='CASCADE', select=True) description = fields.Char( "Description", required=True, translate=True, help="Used as description for the invoice line.") account = fields.MultiValue( fields.Many2One('account.account', "Account", required=True, domain=[ ('kind', '=', 'revenue'), ], help="Used for the line of advance payment invoice.")) accounts = fields.One2Many( 'sale.advance_payment_term.line.account', 'line', "Accounts") block_supply = fields.Boolean( "Block Supply", help="Check to prevent any supply request before advance payment.") block_shipping = fields.Boolean( "Block Shipping", help="Check to prevent the packing of the shipment " "before advance payment.") invoice_delay = fields.TimeDelta( "Invoice Delay", help="Delta to apply on the sale date for the date of " "the advance payment invoice.") formula = fields.Char('Formula', required=True, help="A python expression used to compute the advance payment amount " "that will be evaluated with:\n" "- total_amount: The total amount of the sale.\n" "- untaxed_amount: The total untaxed amount of the sale.") @classmethod def __setup__(cls): super(AdvancePaymentTermLine, cls).__setup__() cls._error_messages.update({ 'invalid_formula': ('Invalid formula "%(formula)s" with' ' exception "%(exception)s".'), }) @fields.depends('formula') def pre_validate(self, **names): super(AdvancePaymentTermLine, self).pre_validate() names['total_amount'] = names['untaxed_amount'] = 0 try: if not isinstance(self.compute_amount(**names), Decimal): raise Exception('The formula does not return a Decimal') except Exception, exception: self.raise_user_error('invalid_formula', { 'formula': self.formula, 'exception': exception, })
class ActivityType(sequence_ordered(), ModelSQL, ModelView): 'Activity Type' __name__ = "activity.type" name = fields.Char('Name', required=True, translate=True) active = fields.Boolean('Active') color = fields.Char('Color') default_duration = fields.TimeDelta('Default Duration') @staticmethod def default_active(): return True
class OTEmployeeLog(ModelSQL, ModelView): 'OT Employee Log' __name__ = 'ot.employee.log' date = fields.Date('Date') time_from = fields.Time('Time From') time_to = fields.Time('Time To') hours = fields.TimeDelta('Hours') vehicle_no = fields.Char('Vehicle No.') ot_employee_details = fields.Many2One('ot.employee.details', 'OT Employee Details')
class SupplierLeadTime(ModelSQL, CompanyValueMixin): "Supplier Lead Time" __name__ = 'party.party.supplier_lead_time' party = fields.Many2One('party.party', "Party", ondelete='CASCADE', select=True, context={ 'company': Eval('company', -1), }, depends={'company'}) supplier_lead_time = fields.TimeDelta("Lead Time")
class HoursEmployeeMonthly(ModelSQL, ModelView): 'Hours per Employee per Month' __name__ = 'timesheet.hours_employee_monthly' year = fields.Char('Year') month_internal = fields.Char('Month') month = fields.Function(fields.Char('Month'), 'get_month', searcher='search_month') employee = fields.Many2One('company.employee', 'Employee') duration = fields.TimeDelta('Duration', 'company_work_time') @classmethod def __setup__(cls): super(HoursEmployeeMonthly, cls).__setup__() cls._order.insert(0, ('year', 'DESC')) cls._order.insert(1, ('month', 'DESC')) cls._order.insert(2, ('employee', 'ASC')) @classmethod def table_query(cls): pool = Pool() Line = pool.get('timesheet.line') line = Line.__table__() type_name = cls.year.sql_type().base year_column = Extract('YEAR', line.date).cast(type_name).as_('year') type_name = cls.month_internal.sql_type().base month_column = Extract('MONTH', line.date).cast(type_name).as_('month_internal') return line.select(Max( Extract('MONTH', line.date) + Extract('YEAR', line.date) * 100 + line.employee * 1000000).as_('id'), Max(line.create_uid).as_('create_uid'), Max(line.create_date).as_('create_date'), Max(line.write_uid).as_('write_uid'), Max(line.write_date).as_('write_date'), year_column, month_column, line.employee, Sum(line.duration).as_('duration'), group_by=(year_column, month_column, line.employee)) def get_month(self, name): return '%02i' % int(self.month_internal) @classmethod def search_month(self, name, domain): return [('month_internal', ) + tuple(domain[1:])] @classmethod def order_month(cls, tables): table, _ = tables[None] return [CharLength(table.month_internal), table.month_internal]
class Configuration(metaclass=PoolMeta): __name__ = 'product.configuration' default_lead_time = fields.MultiValue( fields.TimeDelta( "Default Lead Time", help="The time from confirming the sales order to sending the " "products.\n" "Used for products without a lead time.")) @classmethod def default_default_lead_time(cls, **pattern): return datetime.timedelta(0)
class WorkInvoicedProgress(ModelView, ModelSQL): 'Work Invoiced Progress' __name__ = 'project.work.invoiced_progress' work = fields.Many2One('project.work', 'Work', ondelete='RESTRICT', select=True) effort_duration = fields.TimeDelta('Effort', 'company_work_time') invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line', ondelete='CASCADE') @property def effort_hours(self): if not self.effort_duration: return 0 return self.effort_duration.total_seconds() / 60 / 60
class Journal(metaclass=PoolMeta): __name__ = 'account.payment.journal' clearing_account = fields.Many2One('account.account', 'Clearing Account', domain=[ ('type', '!=', None), ('closed', '!=', True), ('party_required', '=', False), ], states={ 'required': Bool(Eval('clearing_journal')), }, depends=['clearing_journal']) clearing_journal = fields.Many2One('account.journal', 'Clearing Journal', states={ 'required': Bool(Eval('clearing_account')), }, depends=['clearing_account']) clearing_posting_delay = fields.TimeDelta( "Clearing Posting Delay", help="Post automatically the clearing moves after the delay.\n" "Leave empty for no posting.") @classmethod def cron_post_clearing_moves(cls, date=None): pool = Pool() Date = pool.get('ir.date') Move = pool.get('account.move') if date is None: date = Date.today() moves = [] journals = cls.search([ ('clearing_posting_delay', '!=', None), ]) for journal in journals: move_date = date - journal.clearing_posting_delay moves.extend( Move.search([ ('date', '<=', move_date), ('origin.journal.id', '=', journal.id, 'account.payment'), ('state', '=', 'draft'), ('company', '=', Transaction().context.get('company')), ])) Move.post(moves)
class Party(CompanyMultiValueMixin, metaclass=PoolMeta): __name__ = 'party.party' customer_code = fields.MultiValue( fields.Char( 'Customer Code', help="The code the party as supplier has assigned to the company" " as customer.")) customer_codes = fields.One2Many('party.party.customer_code', 'party', "Customer Codes") supplier_lead_time = fields.MultiValue( fields.TimeDelta( "Lead Time", help="The time from confirming the purchase order to receiving " "the goods from the party when used as a supplier.\n" "Used if no lead time is set on the product supplier.")) supplier_lead_times = fields.One2Many('party.party.supplier_lead_time', 'party', "Lead Times") supplier_currency = fields.MultiValue(supplier_currency) supplier_currencies = fields.One2Many('party.party.supplier_currency', 'party', "Supplier Currencies")
class Configuration(ModelSingleton, ModelSQL, ModelView, CompanyMultiValueMixin): 'Purchase Configuration' __name__ = 'purchase.configuration' purchase_sequence = fields.MultiValue( fields.Many2One('ir.sequence', "Purchase Sequence", required=True, domain=[ ('company', 'in', [Eval('context', {}).get('company', -1), None]), ('sequence_type', '=', Id('purchase', 'sequence_type_purchase')), ])) purchase_invoice_method = fields.MultiValue(purchase_invoice_method) get_purchase_invoice_method = get_purchase_methods('invoice_method') purchase_process_after = fields.TimeDelta( "Process Purchase after", help="The grace period during which confirmed purchase " "can still be reset to draft.\n" "Applied only if a worker queue is activated.") @classmethod def multivalue_model(cls, field): pool = Pool() if field == 'purchase_invoice_method': return pool.get('purchase.configuration.purchase_method') if field == 'purchase_sequence': return pool.get('purchase.configuration.sequence') return super(Configuration, cls).multivalue_model(field) @classmethod def default_purchase_sequence(cls, **pattern): return cls.multivalue_model( 'purchase_sequence').default_purchase_sequence() @classmethod def default_purchase_invoice_method(cls, **pattern): return cls.multivalue_model( 'purchase_invoice_method').default_purchase_invoice_method()
class HoursEmployeeMonthly(ModelSQL, ModelView): 'Hours per Employee per Month' __name__ = 'timesheet.hours_employee_monthly' year = fields.Char('Year') month = fields.Many2One('ir.calendar.month', "Month") employee = fields.Many2One('company.employee', 'Employee') duration = fields.TimeDelta('Duration', 'company_work_time') @classmethod def __setup__(cls): super(HoursEmployeeMonthly, cls).__setup__() cls._order.insert(0, ('year', 'DESC')) cls._order.insert(1, ('month.index', 'DESC')) cls._order.insert(2, ('employee', 'ASC')) @classmethod def table_query(cls): pool = Pool() Line = pool.get('timesheet.line') Month = pool.get('ir.calendar.month') line = Line.__table__() month = Month.__table__() type_name = cls.year.sql_type().base year_column = Extract('YEAR', line.date).cast(type_name).as_('year') month_index = Extract('MONTH', line.date) return line.join(month, condition=month_index == month.id).select( Max( Extract('MONTH', line.date) + Extract('YEAR', line.date) * 100 + line.employee * 1000000).as_('id'), Max(line.create_uid).as_('create_uid'), Max(line.create_date).as_('create_date'), Max(line.write_uid).as_('write_uid'), Max(line.write_date).as_('write_date'), year_column, month.id.as_('month'), line.employee, Sum(line.duration).as_('duration'), group_by=(year_column, month.id, line.employee))
class Sheet_Timesheet(metaclass=PoolMeta): __name__ = 'attendance.sheet' timesheet_duration = fields.TimeDelta("Timesheet Duration") @classmethod def table_query(cls): pool = Pool() Timesheet = pool.get('timesheet.line') line = Timesheet.__table__() timesheet = line.select( Min(line.id * 2 + 1).as_('id'), line.company.as_('company'), line.employee.as_('employee'), Sum(line.duration).as_('duration'), line.date.as_('date'), group_by=[line.company, line.employee, line.date]) attendance = super().table_query() return (attendance.join( timesheet, 'FULL' if backend.name != 'sqlite' else 'LEFT', condition=(attendance.company == timesheet.company) & (attendance.employee == timesheet.employee) & (attendance.date == timesheet.date)).select( Coalesce(attendance.id, timesheet.id).as_('id'), Literal(0).as_('create_uid'), CurrentTimestamp().as_('create_date'), cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'), cls.write_date.sql_cast(Literal(Null)).as_('write_date'), Coalesce(attendance.company, timesheet.company).as_('company'), Coalesce(attendance.employee, timesheet.employee).as_('employee'), attendance.duration.as_('duration'), timesheet.duration.as_('timesheet_duration'), Coalesce(attendance.date, timesheet.date).as_('date'), ))
class Work(sequence_ordered(), tree(separator='\\'), ModelSQL, ModelView): 'Work Effort' __name__ = 'project.work' name = fields.Char('Name', required=True, select=True) type = fields.Selection([ ('project', 'Project'), ('task', 'Task') ], 'Type', required=True, select=True) company = fields.Many2One('company.company', 'Company', required=True, select=True) party = fields.Many2One('party.party', 'Party', states={ 'invisible': Eval('type') != 'project', }, context={ 'company': Eval('company', -1), }, depends={'company'}) party_address = fields.Many2One('party.address', 'Contact Address', domain=[('party', '=', Eval('party'))], states={ 'invisible': Eval('type') != 'project', }) timesheet_works = fields.One2Many( 'timesheet.work', 'origin', 'Timesheet Works', readonly=True, size=1) timesheet_available = fields.Function( fields.Boolean('Available on timesheets'), 'get_timesheet_available', setter='set_timesheet_available') timesheet_start_date = fields.Function(fields.Date('Timesheet Start', states={ 'invisible': ~Eval('timesheet_available'), }), 'get_timesheet_date', setter='set_timesheet_date') timesheet_end_date = fields.Function(fields.Date('Timesheet End', states={ 'invisible': ~Eval('timesheet_available'), }), 'get_timesheet_date', setter='set_timesheet_date') timesheet_duration = fields.Function(fields.TimeDelta('Duration', 'company_work_time', help="Total time spent on this work and the sub-works."), 'get_total') effort_duration = fields.TimeDelta('Effort', 'company_work_time', help="Estimated Effort for this work.") total_effort = fields.Function(fields.TimeDelta('Total Effort', 'company_work_time', help="Estimated total effort for this work and the sub-works."), 'get_total') progress = fields.Float('Progress', domain=['OR', ('progress', '=', None), [ ('progress', '>=', 0), ('progress', '<=', 1), ], ], help='Estimated progress for this work.') total_progress = fields.Function(fields.Float('Total Progress', digits=(16, 4), help='Estimated total progress for this work and the sub-works.', states={ 'invisible': ( Eval('total_progress', None) == None), # noqa: E711 }), 'get_total') comment = fields.Text('Comment') parent = fields.Many2One( 'project.work', 'Parent', path='path', ondelete='RESTRICT', domain=[ ('company', '=', Eval('company', -1)), ]) path = fields.Char("Path", select=True) children = fields.One2Many('project.work', 'parent', 'Children', domain=[ ('company', '=', Eval('company', -1)), ]) status = fields.Many2One( 'project.work.status', "Status", required=True, select=True, domain=[If(Bool(Eval('type')), ('types', 'in', Eval('type')), ())]) @staticmethod def default_type(): return 'task' @classmethod def default_company(cls): return Transaction().context.get('company') @classmethod def default_status(cls): pool = Pool() WorkStatus = pool.get('project.work.status') return WorkStatus.get_default_status(cls.default_type()) @classmethod def __register__(cls, module_name): TimesheetWork = Pool().get('timesheet.work') transaction = Transaction() cursor = transaction.connection.cursor() update = transaction.connection.cursor() table_project_work = cls.__table_handler__(module_name) project = cls.__table__() timesheet = TimesheetWork.__table__() work_exist = table_project_work.column_exist('work') add_parent = (not table_project_work.column_exist('parent') and work_exist) add_company = (not table_project_work.column_exist('company') and work_exist) add_name = (not table_project_work.column_exist('name') and work_exist) super(Work, cls).__register__(module_name) # Migration from 3.4: change effort into timedelta effort_duration if table_project_work.column_exist('effort'): cursor.execute(*project.select(project.id, project.effort, where=project.effort != Null)) for id_, effort in cursor: duration = datetime.timedelta(hours=effort) update.execute(*project.update( [project.effort_duration], [duration], where=project.id == id_)) table_project_work.drop_column('effort') # Migration from 3.6: add parent, company, drop required on work, # fill name if add_parent: second_project = cls.__table__() query = project.join(timesheet, condition=project.work == timesheet.id ).join(second_project, condition=timesheet.parent == second_project.work ).select(project.id, second_project.id) cursor.execute(*query) for id_, parent in cursor: update.execute(*project.update( [project.parent], [parent], where=project.id == id_)) cls._rebuild_tree('parent', None, 0) if add_company: cursor.execute(*project.join(timesheet, condition=project.work == timesheet.id ).select(project.id, timesheet.company)) for id_, company in cursor: update.execute(*project.update( [project.company], [company], where=project.id == id_)) table_project_work.not_null_action('work', action='remove') if add_name: cursor.execute(*project.join(timesheet, condition=project.work == timesheet.id ).select(project.id, timesheet.name)) for id_, name in cursor: update.execute(*project.update( [project.name], [name], where=project.id == id_)) # Migration from 4.0: remove work if work_exist: table_project_work.drop_constraint('work_uniq') cursor.execute(*project.select(project.id, project.work, where=project.work != Null)) for project_id, work_id in cursor: update.execute(*timesheet.update( [timesheet.origin, timesheet.name], ['%s,%s' % (cls.__name__, project_id), Null], where=timesheet.id == work_id)) table_project_work.drop_column('work') # Migration from 5.4: replace state by status table_project_work.not_null_action('state', action='remove') # Migration from 6.0: remove left and right table_project_work.drop_column('left') table_project_work.drop_column('right') @fields.depends('type', 'status') def on_change_type(self): pool = Pool() WorkState = pool.get('project.work.status') if (self.type and (not self.status or self.type not in self.status.types)): self.status = WorkState.get_default_status(self.type) @fields.depends('status', 'progress') def on_change_status(self): if (self.status and self.status.progress is not None and self.status.progress > (self.progress or -1.0)): self.progress = self.status.progress @classmethod def index_set_field(cls, name): index = super(Work, cls).index_set_field(name) if name in {'timesheet_start_date', 'timesheet_end_date'}: index = cls.index_set_field('timesheet_available') + 1 return index @classmethod def validate(cls, works): super(Work, cls).validate(works) for work in works: work.check_work_progress() def check_work_progress(self): pool = Pool() progress = -1 if self.progress is None else self.progress if (self.status.progress is not None and progress < self.status.progress): Lang = pool.get('ir.lang') lang = Lang.get() raise WorkProgressValidationError( gettext('project.msg_work_invalid_progress_status', work=self.rec_name, progress=lang.format('%.2f%%', self.status.progress * 100), status=self.status.rec_name)) if (self.status.progress == 1.0 and not all(c.progress == 1.0 for c in self.children)): raise WorkProgressValidationError( gettext('project.msg_work_children_progress', work=self.rec_name, status=self.status.rec_name)) if (self.parent and self.parent.progress == 1.0 and not self.progress == 1.0): raise WorkProgressValidationError( gettext('project.msg_work_parent_progress', work=self.rec_name, parent=self.parent.rec_name)) @property def effort_hours(self): if not self.effort_duration: return 0 return self.effort_duration.total_seconds() / 60 / 60 @property def total_effort_hours(self): if not self.total_effort: return 0 return self.total_effort.total_seconds() / 60 / 60 @property def timesheet_duration_hours(self): if not self.timesheet_duration: return 0 return self.timesheet_duration.total_seconds() / 60 / 60 @classmethod def default_timesheet_available(cls): return False def get_timesheet_available(self, name): return bool(self.timesheet_works) @classmethod def set_timesheet_available(cls, projects, name, value): pool = Pool() Timesheet = pool.get('timesheet.work') to_create = [] to_delete = [] for project in projects: if not project.timesheet_works and value: to_create.append({ 'origin': str(project), 'company': project.company.id, }) elif project.timesheet_works and not value: to_delete.extend(project.timesheet_works) if to_create: Timesheet.create(to_create) if to_delete: Timesheet.delete(to_delete) def get_timesheet_date(self, name): if self.timesheet_works: func = { 'timesheet_start_date': min, 'timesheet_end_date': max, }[name] return func(getattr(w, name) for w in self.timesheet_works) @classmethod def set_timesheet_date(cls, projects, name, value): pool = Pool() Timesheet = pool.get('timesheet.work') timesheets = [w for p in projects for w in p.timesheet_works] if timesheets: Timesheet.write(timesheets, { name: value, }) @classmethod def sum_tree(cls, works, values, parents): result = values.copy() works = set((w.id for w in works)) leafs = works - set(parents.values()) while leafs: for work in leafs: works.remove(work) parent = parents.get(work) if parent in result: result[parent] += result[work] next_leafs = set(works) for work in works: parent = parents.get(work) if not parent: continue if parent in next_leafs and parent in works: next_leafs.remove(parent) leafs = next_leafs return result @classmethod def get_total(cls, works, names): cursor = Transaction().connection.cursor() table = cls.__table__() works = cls.search([ ('parent', 'child_of', [w.id for w in works]), ]) work_ids = [w.id for w in works] parents = {} for sub_ids in grouped_slice(work_ids): where = reduce_ids(table.id, sub_ids) cursor.execute(*table.select(table.id, table.parent, where=where)) parents.update(cursor) if 'total_progress' in names and 'total_effort' not in names: names = list(names) names.append('total_effort') result = {} for name in names: values = getattr(cls, '_get_%s' % name)(works) result[name] = cls.sum_tree(works, values, parents) if 'total_progress' in names: digits = cls.total_progress.digits[1] total_progress = result['total_progress'] total_effort = result['total_effort'] for work in works: if total_effort[work.id]: total_progress[work.id] = round(total_progress[work.id] / (total_effort[work.id].total_seconds() / 60 / 60), digits) else: total_effort[work.id] = None return result @classmethod def _get_total_effort(cls, works): return {w.id: w.effort_duration or datetime.timedelta() for w in works} @classmethod def _get_timesheet_duration(cls, works): durations = {} for work in works: value = datetime.timedelta() for timesheet_work in work.timesheet_works: if timesheet_work.duration: value += timesheet_work.duration durations[work.id] = value return durations @classmethod def _get_total_progress(cls, works): return {w.id: w.effort_hours * (w.progress or 0) for w in works} @classmethod def copy(cls, project_works, default=None): pool = Pool() WorkStatus = pool.get('project.work.status') if default is None: default = {} else: default = default.copy() default.setdefault('children', None) default.setdefault('progress', None) default.setdefault( 'status', lambda data: WorkStatus.get_default_status(data['type'])) return super().copy(project_works, default=default) @classmethod def delete(cls, project_works): TimesheetWork = Pool().get('timesheet.work') # Get the timesheet works linked to the project works timesheet_works = [ w for pw in project_works for w in pw.timesheet_works] super(Work, cls).delete(project_works) if timesheet_works: with Transaction().set_context(_check_access=False): TimesheetWork.delete(timesheet_works) @classmethod def search_global(cls, text): for record, rec_name, icon in super(Work, cls).search_global(text): icon = icon or 'tryton-project' yield record, rec_name, icon
class Email(ModelSQL, ModelView): "Email Notification" __name__ = 'notification.email' from_ = fields.Char( "From", translate=True, help="Leave empty for the value defined in the configuration file.") subject = fields.Char("Subject", translate=True, help="The Genshi syntax can be used " "with 'record' in the evaluation context.\n" "If empty the report name will be used.") recipients = fields.Many2One( 'ir.model.field', "Recipients", domain=[ ('model.model', '=', Eval('model')), ], depends=['model'], help="The field that contains the recipient(s).") fallback_recipients = fields.Many2One( 'res.user', "Recipients Fallback User", domain=[ ('email', '!=', None), ], states={ 'invisible': ~Eval('recipients'), }, depends=['recipients'], help="User notified when no recipients e-mail is found") recipients_secondary = fields.Many2One( 'ir.model.field', "Secondary Recipients", domain=[ ('model.model', '=', Eval('model')), ], depends=['model'], help="The field that contains the secondary recipient(s).") fallback_recipients_secondary = fields.Many2One( 'res.user', "Secondary Recipients Fallback User", domain=[ ('email', '!=', None), ], states={ 'invisible': ~Eval('recipients_secondary'), }, depends=['recipients'], help="User notified when no secondary recipients e-mail is found") recipients_hidden = fields.Many2One( 'ir.model.field', "Hidden Recipients", domain=[ ('model.model', '=', Eval('model')), ], depends=['model'], help="The field that contains the hidden recipient(s).") fallback_recipients_hidden = fields.Many2One( 'res.user', "Hidden Recipients Fallback User", domain=[ ('email', '!=', None), ], states={ 'invisible': ~Eval('recipients_hidden'), }, depends=['recipients_hidden'], help="User notified when no hidden recipients e-mail is found") contact_mechanism = fields.Selection( 'get_contact_mechanisms', "Contact Mechanism", help="Define which email to use from the party's contact mechanisms") content = fields.Many2One('ir.action.report', "Content", required=True, domain=[('template_extension', 'in', ['txt', 'html', 'xhtml'])], help="The report used as email template.") attachments = fields.Many2Many('notification.email.attachment', 'notification', 'report', "Attachments", domain=[ ('model', '=', Eval('model')), ], depends=['model'], help="The reports used as attachments.") triggers = fields.One2Many('ir.trigger', 'notification_email', "Triggers", domain=[('model.model', '=', Eval('model'))], depends=['model'], help="Add a trigger for the notification.") send_after = fields.TimeDelta( "Send After", help="The delay after which the email must be sent.\n" "Applied if a worker queue is activated.") model = fields.Function(fields.Char("Model"), 'on_change_with_model', searcher='search_model') @classmethod def __setup__(cls): pool = Pool() EmailTemplate = pool.get('ir.email.template') super().__setup__() for field in [ 'recipients', 'recipients_secondary', 'recipients_hidden', ]: field = getattr(cls, field) field.domain.append([ 'OR', ('relation', 'in', EmailTemplate.email_models()), [ ('model.model', 'in', EmailTemplate.email_models()), ('name', '=', 'id'), ], ]) def get_rec_name(self, name): return self.content.rec_name @classmethod def search_rec_name(cls, name, clause): return [('content', ) + tuple(clause[1:])] @classmethod def get_contact_mechanisms(cls): pool = Pool() try: ContactMechanism = pool.get('party.contact_mechanism') except KeyError: return [(None, "")] return ContactMechanism.usages() @fields.depends('content') def on_change_with_model(self, name=None): if self.content: return self.content.model @classmethod def search_model(cls, name, clause): return [('content.model', ) + tuple(clause[1:])] def _get_addresses(self, value): pool = Pool() EmailTemplate = pool.get('ir.email.template') with Transaction().set_context(usage=self.contact_mechanism): return EmailTemplate.get_addresses(value) def _get_languages(self, value): pool = Pool() EmailTemplate = pool.get('ir.email.template') with Transaction().set_context(usage=self.contact_mechanism): return EmailTemplate.get_languages(value) def get_email(self, record, sender, to, cc, bcc, languages): pool = Pool() Attachment = pool.get('notification.email.attachment') # TODO order languages to get default as last one for title content, title = get_email(self.content, record, languages) language = list(languages)[-1] from_ = sender with Transaction().set_context(language=language.code): notification = self.__class__(self.id) if notification.from_: from_ = notification.from_ if self.subject: title = (TextTemplate( notification.subject).generate(record=record).render()) if self.attachments: msg = MIMEMultipart('mixed') msg.attach(content) for report in self.attachments: msg.attach(Attachment.get_mime(report, record, language.code)) else: msg = content set_from_header(msg, sender, from_) msg['To'] = ', '.join(to) msg['Cc'] = ', '.join(cc) msg['Subject'] = Header(title, 'utf-8') msg['Auto-Submitted'] = 'auto-generated' return msg def get_log(self, record, trigger, msg, bcc=None): return { 'recipients': msg['To'], 'recipients_secondary': msg['Cc'], 'recipients_hidden': bcc, 'resource': str(record), 'notification': trigger.notification_email.id, 'trigger': trigger.id, } @classmethod def trigger(cls, records, trigger): "Action function for the triggers" notification_email = trigger.notification_email if not notification_email: raise ValueError( 'Trigger "%s" is not related to any email notification' % trigger.rec_name) if notification_email.send_after: with Transaction().set_context( queue_name='notification_email', queue_scheduled_at=trigger.notification_email.send_after): notification_email.__class__.__queue__._send_email_queued( notification_email, [r.id for r in records], trigger.id) else: notification_email.send_email(records, trigger) def _send_email_queued(self, ids, trigger_id): pool = Pool() Model = pool.get(self.model) Trigger = pool.get('ir.trigger') records = Model.browse(ids) trigger = Trigger(trigger_id) self.send_email(records, trigger) def send_email(self, records, trigger): pool = Pool() Log = pool.get('notification.email.log') datamanager = SMTPDataManager() Transaction().join(datamanager) from_ = (config.get('notification_email', 'from') or config.get('email', 'from')) logs = [] for record in records: to, to_languages = self._get_to(record) cc, cc_languages = self._get_cc(record) bcc, bcc_languages = self._get_bcc(record) languagues = to_languages | cc_languages | bcc_languages to_addrs = [e for _, e in getaddresses(to + cc + bcc)] if to_addrs: msg = self.get_email(record, from_, to, cc, bcc, languagues) sendmail_transactional(from_, to_addrs, msg, datamanager=datamanager) logs.append( self.get_log(record, trigger, msg, bcc=', '.join(bcc))) if logs: Log.create(logs) def _get_to(self, record): to = [] languagues = set() if self.recipients: recipients = getattr(record, self.recipients.name, None) if recipients: languagues.update(self._get_languages(recipients)) to = self._get_addresses(recipients) if not to and self.fallback_recipients: languagues.update(self._get_languages(self.fallback_recipients)) to = self._get_addresses(self.fallback_recipients) return to, languagues def _get_cc(self, record): cc = [] languagues = set() if self.recipients_secondary: recipients_secondary = getattr(record, self.recipients_secondary.name, None) if recipients_secondary: languagues.update(self._get_languages(recipients_secondary)) cc = self._get_addresses(recipients_secondary) if not cc and self.fallback_recipients_secondary: languagues.update( self._get_languages(self.fallback_recipients_secondary)) cc = self._get_addresses(self.fallback_recipients_secondary) return cc, languagues def _get_bcc(self, record): bcc = [] languagues = set() if self.recipients_hidden: recipients_hidden = getattr(record, self.recipients_hidden.name, None) if recipients_hidden: languagues.update(self._get_languages(recipients_hidden)) bcc = self._get_addresses(recipients_hidden) if not bcc and self.fallback_recipients_hidden: languagues.update( self._get_languages(self.fallback_recipients_hidden)) bcc = self._get_addresses(self.fallback_recipients_hidden) return bcc, languagues @classmethod def validate(cls, notifications): super().validate(notifications) for notification in notifications: notification.check_subject() def check_subject(self): if not self.subject: return try: TextTemplate(self.subject) except Exception as exception: raise TemplateError( gettext( 'notification_email.' 'msg_notification_invalid_subject', notification=self.rec_name, exception=exception)) from exception
class Work: __name__ = 'project.work' predecessors = fields.Many2Many('project.predecessor_successor', 'successor', 'predecessor', 'Predecessors', domain=[ ('parent', '=', Eval('parent')), ('id', '!=', Eval('id')), ], depends=['parent', 'id']) successors = fields.Many2Many('project.predecessor_successor', 'predecessor', 'successor', 'Successors', domain=[ ('parent', '=', Eval('parent')), ('id', '!=', Eval('id')), ], depends=['parent', 'id']) leveling_delay = fields.Float("Leveling Delay", required=True) back_leveling_delay = fields.Float("Back Leveling Delay", required=True) allocations = fields.One2Many('project.allocation', 'work', 'Allocations', states={ 'invisible': Eval('type') != 'task', }, depends=['type']) duration = fields.Function( fields.TimeDelta('Duration', 'company_work_time'), 'get_function_fields') early_start_time = fields.DateTime("Early Start Time", readonly=True) late_start_time = fields.DateTime("Late Start Time", readonly=True) early_finish_time = fields.DateTime("Early Finish Time", readonly=True) late_finish_time = fields.DateTime("Late Finish Time", readonly=True) actual_start_time = fields.DateTime("Actual Start Time") actual_finish_time = fields.DateTime("Actual Finish Time") constraint_start_time = fields.DateTime("Constraint Start Time") constraint_finish_time = fields.DateTime("Constraint Finish Time") early_start_date = fields.Function(fields.Date('Early Start'), 'get_function_fields') late_start_date = fields.Function(fields.Date('Late Start'), 'get_function_fields') early_finish_date = fields.Function(fields.Date('Early Finish'), 'get_function_fields') late_finish_date = fields.Function(fields.Date('Late Finish'), 'get_function_fields') actual_start_date = fields.Function(fields.Date('Actual Start'), 'get_function_fields', setter='set_function_fields') actual_finish_date = fields.Function(fields.Date('Actual Finish'), 'get_function_fields', setter='set_function_fields') constraint_start_date = fields.Function(fields.Date('Constraint Start', depends=['type']), 'get_function_fields', setter='set_function_fields') constraint_finish_date = fields.Function(fields.Date('Constraint Finish', depends=['type']), 'get_function_fields', setter='set_function_fields') @classmethod def __setup__(cls): super(Work, cls).__setup__() @classmethod def validate(cls, works): super(Work, cls).validate(works) cls.check_recursion(works, parent='successors') @staticmethod def default_leveling_delay(): return 0.0 @staticmethod def default_back_leveling_delay(): return 0.0 @classmethod def get_function_fields(cls, works, names): ''' Function to compute function fields ''' res = {} ids = [w.id for w in works] if 'duration' in names: all_works = cls.search([('parent', 'child_of', ids), ('active', '=', True)]) + works all_works = set(all_works) durations = {} id2work = {} leafs = set() for work in all_works: id2work[work.id] = work if not work.children: leafs.add(work.id) total_allocation = 0 if not work.allocations or not work.effort_duration: durations[work.id] = work.effort_duration continue for allocation in work.allocations: total_allocation += allocation.percentage durations[work.id] = datetime.timedelta( seconds=work.effort_duration.total_seconds() / (total_allocation / 100.0)) while leafs: for work_id in leafs: work = id2work[work_id] all_works.remove(work) if not work.active: continue if work.parent and work.parent.id in durations: durations[work.parent.id] += durations[work_id] next_leafs = set(w.id for w in all_works) for work in all_works: if not work.parent: continue if work.parent.id in next_leafs and work.parent in works: next_leafs.remove(work.parent.id) leafs = next_leafs res['duration'] = durations fun_fields = ('early_start_date', 'early_finish_date', 'late_start_date', 'late_finish_date', 'actual_start_date', 'actual_finish_date', 'constraint_start_date', 'constraint_finish_date') db_fields = ('early_start_time', 'early_finish_time', 'late_start_time', 'late_finish_time', 'actual_start_time', 'actual_finish_time', 'constraint_start_time', 'constraint_finish_time') for fun_field, db_field in zip(fun_fields, db_fields): if fun_field in names: values = {} for work in works: values[work.id] = getattr(work, db_field) \ and getattr(work, db_field).date() or None res[fun_field] = values return res @classmethod def set_function_fields(cls, works, name, value): fun_fields = ('actual_start_date', 'actual_finish_date', 'constraint_start_date', 'constraint_finish_date') db_fields = ('actual_start_time', 'actual_finish_time', 'constraint_start_time', 'constraint_finish_time') for fun_field, db_field in zip(fun_fields, db_fields): if fun_field == name: cls.write( works, { db_field: (value and datetime.datetime.combine(value, datetime.time()) or None), }) break @property def hours(self): if not self.duration: return 0 return self.duration.total_seconds() / 60 / 60 @classmethod def add_minutes(cls, company, date, minutes): minutes = int(round(minutes)) minutes = date.minute + minutes hours = minutes // 60 if hours: date = cls.add_hours(company, date, hours) minutes = minutes % 60 date = datetime.datetime(date.year, date.month, date.day, date.hour, minutes, date.second) return date @classmethod def add_hours(cls, company, date, hours): while hours: if hours != intfloor(hours): minutes = (hours - intfloor(hours)) * 60 date = cls.add_minutes(company, date, minutes) hours = intfloor(hours) hours = date.hour + hours days = hours // company.hours_per_work_day if days: date = cls.add_days(company, date, days) hours = hours % company.hours_per_work_day date = datetime.datetime(date.year, date.month, date.day, intfloor(hours), date.minute, date.second) hours = hours - intfloor(hours) return date @classmethod def add_days(cls, company, date, days): day_per_week = company.hours_per_work_week / company.hours_per_work_day while days: if days != intfloor(days): hours = (days - intfloor(days)) * company.hours_per_work_day date = cls.add_hours(company, date, hours) days = intfloor(days) days = date.weekday() + days weeks = days // day_per_week days = days % day_per_week if weeks: date = cls.add_weeks(company, date, weeks) date += datetime.timedelta(days=-date.weekday() + intfloor(days)) days = days - intfloor(days) return date @classmethod def add_weeks(cls, company, date, weeks): day_per_week = company.hours_per_work_week / company.hours_per_work_day if weeks != intfloor(weeks): days = (weeks - intfloor(weeks)) * day_per_week if days: date = cls.add_days(company, date, days) date += datetime.timedelta(days=7 * intfloor(weeks)) return date def compute_dates(self): values = {} get_early_finish = lambda work: values.get(work, {}).get( 'early_finish_time', work.early_finish_time) get_late_start = lambda work: values.get(work, {}).get( 'late_start_time', work.late_start_time) maxdate = lambda x, y: x and y and max(x, y) or x or y mindate = lambda x, y: x and y and min(x, y) or x or y # propagate constraint_start_time constraint_start = reduce(maxdate, (pred.early_finish_time for pred in self.predecessors), None) if constraint_start is None and self.parent: constraint_start = self.parent.early_start_time constraint_start = maxdate(constraint_start, self.constraint_start_time) works = deque([(self, constraint_start)]) work2children = {} parent = None while works or parent: if parent: work = parent parent = None # Compute early_finish if work.children: early_finish_time = reduce( maxdate, map(get_early_finish, work.children), None) else: early_finish_time = None if values[work]['early_start_time']: early_finish_time = self.add_hours( work.company, values[work]['early_start_time'], work.hours) values[work]['early_finish_time'] = early_finish_time # Propagate constraint_start on successors for w in work.successors: works.append((w, early_finish_time)) if not work.parent: continue # housecleaning work2children if work.parent not in work2children: work2children[work.parent] = set() work2children[work.parent].update(work.successors) if work in work2children[work.parent]: work2children[work.parent].remove(work) # if no sibling continue to walk up the tree if not work2children.get(work.parent): if work.parent not in values: values[work.parent] = {} parent = work.parent continue work, constraint_start = works.popleft() # take constraint define on the work into account constraint_start = maxdate(constraint_start, work.constraint_start_time) if constraint_start: early_start = self.add_hours(work.company, constraint_start, work.leveling_delay) else: early_start = None # update values if work not in values: values[work] = {} values[work]['early_start_time'] = early_start # Loop on children if they exist if work.children and work not in work2children: work2children[work] = set(work.children) # Propagate constraint_start on children for w in work.children: if w.predecessors: continue works.append((w, early_start)) else: parent = work # propagate constraint_finish_time constraint_finish = reduce(mindate, (succ.late_start_time for succ in self.successors), None) if constraint_finish is None and self.parent: constraint_finish = self.parent.late_finish_time constraint_finish = mindate(constraint_finish, self.constraint_finish_time) works = deque([(self, constraint_finish)]) work2children = {} parent = None while works or parent: if parent: work = parent parent = None # Compute late_start if work.children: reduce(mindate, map(get_late_start, work.children), None) else: late_start_time = None if values[work]['late_finish_time']: late_start_time = self.add_hours( work.company, values[work]['late_finish_time'], -work.hours) values[work]['late_start_time'] = late_start_time # Propagate constraint_finish on predecessors for w in work.predecessors: works.append((w, late_start_time)) if not work.parent: continue # housecleaning work2children if work.parent not in work2children: work2children[work.parent] = set() work2children[work.parent].update(work.predecessors) if work in work2children[work.parent]: work2children[work.parent].remove(work) # if no sibling continue to walk up the tree if not work2children.get(work.parent): if work.parent not in values: values[work.parent] = {} parent = work.parent continue work, constraint_finish = works.popleft() # take constraint define on the work into account constraint_finish = mindate(constraint_finish, work.constraint_finish_time) if constraint_finish: late_finish = self.add_hours(work.company, constraint_finish, -work.back_leveling_delay) else: late_finish = None # update values if work not in values: values[work] = {} values[work]['late_finish_time'] = late_finish # Loop on children if they exist if work.children and work not in work2children: work2children[work] = set(work.children) # Propagate constraint_start on children for w in work.children: if w.successors: continue works.append((w, late_finish)) else: parent = work # write values write_fields = ('early_start_time', 'early_finish_time', 'late_start_time', 'late_finish_time') for work, val in values.iteritems(): write_cond = False for field in write_fields: if field in val and getattr(work, field) != val[field]: write_cond = True break if write_cond: self.write([work], val) def reset_leveling(self): get_key = lambda w: (set(p.id for p in w.predecessors), set(s.id for s in w.successors)) parent_id = self.parent and self.parent.id or None siblings = self.search([('parent', '=', parent_id)]) to_clean = [] ref_key = get_key(self) for sibling in siblings: if sibling.leveling_delay == sibling.back_leveling_delay == 0: continue if get_key(sibling) == ref_key: to_clean.append(sibling) if to_clean: self.write(to_clean, { 'leveling_delay': 0, 'back_leveling_delay': 0, }) def create_leveling(self): # define some helper functions get_key = lambda w: (set(p.id for p in w.predecessors), set(s.id for s in w.successors)) over_alloc = lambda current_alloc, work: (reduce( lambda res, alloc: (res or (current_alloc[alloc.employee.id] + alloc.percentage) > 100 ), work.allocations, False)) def sum_allocs(current_alloc, work): res = defaultdict(float) for alloc in work.allocations: empl = alloc.employee.id res[empl] = current_alloc[empl] + alloc.percentage return res def compute_delays(siblings): # time_line is a list [[end_delay, allocations], ...], this # mean that allocations is valid between the preceding end_delay # (or 0 if it doesn't exist) and the current end_delay. timeline = [] for sibling in siblings: delay = 0 ignored = [] overloaded = [] item = None while timeline: # item is [end_delay, allocations] item = heappop(timeline) if over_alloc(item[1], sibling): ignored.extend(overloaded) ignored.append(item) delay = item[0] continue elif item[1] >= delay + sibling.duration: overloaded.append(item) else: # Succes! break heappush(timeline, [ delay + sibling.duration, sum_allocs(defaultdict(float), sibling), sibling.id ]) for i in ignored: heappush(timeline, i) for i in overloaded: i[1] = sum_allocs(i[1], sibling) heappush(timeline, i) yield sibling, delay siblings = self.search([('parent', '=', self.parent.id if self.parent else None)]) refkey = get_key(self) siblings = [s for s in siblings if get_key(s) == refkey] for sibling, delay in compute_delays(siblings): self.write([sibling], { 'leveling_delay': delay, }) siblings.reverse() for sibling, delay in compute_delays(siblings): self.write([sibling], { 'back_leveling_delay': delay, }) if self.parent: self.parent.compute_dates() @classmethod def write(cls, *args): super(Work, cls).write(*args) actions = iter(args) for works, values in zip(actions, actions): if 'effort' in values: for work in works: work.reset_leveling() fields = ('constraint_start_time', 'constraint_finish_time', 'effort') if reduce(lambda x, y: x or y in values, fields, False): for work in works: work.compute_dates() @classmethod def create(cls, vlist): works = super(Work, cls).create(vlist) for work in works: work.reset_leveling() work.compute_dates() return works @classmethod def delete(cls, works): to_update = set() for work in works: if work.parent and work.parent not in works: to_update.add(work.parent) to_update.update(c for c in work.parent.children if c not in works) super(Work, cls).delete(works) for work in to_update: work.reset_leveling() work.compute_dates()
class Surgery(ModelSQL, ModelView): 'Surgery' __name__ = 'gnuhealth.surgery' def surgery_duration(self, name): if (self.surgery_end_date and self.surgery_date): return self.surgery_end_date - self.surgery_date else: return None def patient_age_at_surgery(self, name): if (self.patient.name.dob and self.surgery_date): rdelta = relativedelta(self.surgery_date.date(), self.patient.name.dob) years_months_days = str(rdelta.years) + 'y ' \ + str(rdelta.months) + 'm ' \ + str(rdelta.days) + 'd' return years_months_days else: return None patient = fields.Many2One('gnuhealth.patient', 'Patient', required=True) admission = fields.Many2One('gnuhealth.appointment', 'Admission') operating_room = fields.Many2One('gnuhealth.hospital.or', 'Operating Room') code = fields.Char('Code', readonly=True, help="Health Center code / sequence") procedures = fields.One2Many( 'gnuhealth.operation', 'name', 'Procedures', help="List of the procedures in the surgery. Please enter the first " "one as the main procedure") supplies = fields.One2Many( 'gnuhealth.surgery_supply', 'name', 'Supplies', help="List of the supplies required for the surgery") pathology = fields.Many2One('gnuhealth.pathology', 'Condition', help="Base Condition / Reason") classification = fields.Selection([ (None, ''), ('o', 'Optional'), ('r', 'Required'), ('u', 'Urgent'), ('e', 'Emergency'), ], 'Urgency', help="Urgency level for this surgery", sort=False) surgeon = fields.Many2One('gnuhealth.healthprofessional', 'Surgeon', help="Surgeon who did the procedure") anesthetist = fields.Many2One('gnuhealth.healthprofessional', 'Anesthetist', help="Anesthetist in charge") surgery_date = fields.DateTime('Date', help="Start of the Surgery") surgery_end_date = fields.DateTime( 'End', states={ 'required': Equal(Eval('state'), 'done'), }, help="Automatically set when the surgery is done." "It is also the estimated end time when confirming the surgery.") surgery_length = fields.Function( fields.TimeDelta('Duration', states={ 'invisible': And(Not(Equal(Eval('state'), 'done')), Not(Equal(Eval('state'), 'signed'))) }, help="Length of the surgery"), 'surgery_duration') state = fields.Selection([ ('draft', 'Draft'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('in_progress', 'In Progress'), ('done', 'Done'), ('signed', 'Signed'), ], 'State', readonly=True, sort=False) signed_by = fields.Many2One( 'gnuhealth.healthprofessional', 'Signed by', readonly=True, states={'invisible': Not(Equal(Eval('state'), 'signed'))}, help="Health Professional that signed this surgery document") # age is deprecated in GNU Health 2.0 age = fields.Char('Estimative Age', help="Use this field for historical purposes, \ when no date of surgery is given") computed_age = fields.Function( fields.Char('Age', help="Computed patient age at the moment of the surgery"), 'patient_age_at_surgery') gender = fields.Function(fields.Selection([ (None, ''), ('m', 'Male'), ('f', 'Female'), ('f-m', 'Female -> Male'), ('m-f', 'Male -> Female'), ], 'Gender'), 'get_patient_gender', searcher='search_patient_gender') description = fields.Char('Description') preop_mallampati = fields.Selection([ (None, ''), ('Class 1', 'Class 1: Full visibility of tonsils, uvula and soft ' 'palate'), ('Class 2', 'Class 2: Visibility of hard and soft palate, ' 'upper portion of tonsils and uvula'), ('Class 3', 'Class 3: Soft and hard palate and base of the uvula are ' 'visible'), ('Class 4', 'Class 4: Only Hard Palate visible'), ], 'Mallampati Score', sort=False) preop_bleeding_risk = fields.Boolean( 'Risk of Massive bleeding', help="Patient has a risk of losing more than 500 " "ml in adults of over 7ml/kg in infants. If so, make sure that " "intravenous access and fluids are available") preop_oximeter = fields.Boolean('Pulse Oximeter in place', help="Pulse oximeter is in place " "and functioning") preop_site_marking = fields.Boolean( 'Surgical Site Marking', help="The surgeon has marked the surgical incision") preop_antibiotics = fields.Boolean( 'Antibiotic Prophylaxis', help="Prophylactic antibiotic treatment within the last 60 minutes") preop_sterility = fields.Boolean( 'Sterility confirmed', help="Nursing team has confirmed sterility of the devices and room") preop_asa = fields.Selection([ (None, ''), ('ps1', 'PS 1 : Normal healthy patient'), ('ps2', 'PS 2 : Patients with mild systemic disease'), ('ps3', 'PS 3 : Patients with severe systemic disease'), ('ps4', 'PS 4 : Patients with severe systemic disease that is' ' a constant threat to life '), ('ps5', 'PS 5 : Moribund patients who are not expected to' ' survive without the operation'), ('ps6', 'PS 6 : A declared brain-dead patient who organs are' ' being removed for donor purposes'), ], 'ASA PS', help="ASA pre-operative Physical Status", sort=False) preop_rcri = fields.Many2One( 'gnuhealth.rcri', 'RCRI', help='Patient Revised Cardiac Risk Index\n' 'Points 0: Class I Very Low (0.4% complications)\n' 'Points 1: Class II Low (0.9% complications)\n' 'Points 2: Class III Moderate (6.6% complications)\n' 'Points 3 or more : Class IV High (>11% complications)') surgical_wound = fields.Selection([ (None, ''), ('I', 'Clean . Class I'), ('II', 'Clean-Contaminated . Class II'), ('III', 'Contaminated . Class III'), ('IV', 'Dirty-Infected . Class IV'), ], 'Surgical wound', sort=False) extra_info = fields.Text('Extra Info') anesthesia_report = fields.Text('Anesthesia Report') institution = fields.Many2One('gnuhealth.institution', 'Institution') report_surgery_date = fields.Function(fields.Date('Surgery Date'), 'get_report_surgery_date') report_surgery_time = fields.Function(fields.Time('Surgery Time'), 'get_report_surgery_time') surgery_team = fields.One2Many( 'gnuhealth.surgery_team', 'name', 'Team Members', help="Professionals Involved in the surgery") postoperative_dx = fields.Many2One( 'gnuhealth.pathology', 'Post-op dx', states={ 'invisible': And(Not(Equal(Eval('state'), 'done')), Not(Equal(Eval('state'), 'signed'))) }, help="Post-operative diagnosis") @staticmethod def default_institution(): HealthInst = Pool().get('gnuhealth.institution') institution = HealthInst.get_institution() return institution @staticmethod def default_surgery_date(): return datetime.now() @staticmethod def default_surgeon(): pool = Pool() HealthProf = pool.get('gnuhealth.healthprofessional') surgeon = HealthProf.get_health_professional() return surgeon @staticmethod def default_state(): return 'draft' def get_patient_gender(self, name): return self.patient.gender @classmethod def search_patient_gender(cls, name, clause): res = [] value = clause[2] res.append(('patient.name.gender', clause[1], value)) return res # Show the gender and age upon entering the patient # These two are function fields (don't exist at DB level) @fields.depends('patient') def on_change_patient(self): gender = None age = '' self.gender = self.patient.gender self.computed_age = self.patient.age @classmethod def create(cls, vlist): Sequence = Pool().get('ir.sequence') Config = Pool().get('gnuhealth.sequences') vlist = [x.copy() for x in vlist] for values in vlist: if not values.get('code'): config = Config(1) values['code'] = Sequence.get_id( config.surgery_code_sequence.id) return super(Surgery, cls).create(vlist) @classmethod def __setup__(cls): super(Surgery, cls).__setup__() cls._error_messages.update({ 'end_date_before_start': 'End time "%(end_date)s" BEFORE ' 'surgery date "%(surgery_date)s"', 'or_is_not_available': 'Operating Room is not available' }) cls._order.insert(0, ('surgery_date', 'DESC')) cls._buttons.update({ 'confirmed': { 'invisible': And(Not(Equal(Eval('state'), 'draft')), Not(Equal(Eval('state'), 'cancelled'))), }, 'cancel': { 'invisible': Not(Equal(Eval('state'), 'confirmed')), }, 'start': { 'invisible': Not(Equal(Eval('state'), 'confirmed')), }, 'done': { 'invisible': Not(Equal(Eval('state'), 'in_progress')), }, 'signsurgery': { 'invisible': Not(Equal(Eval('state'), 'done')), }, }) @classmethod def validate(cls, surgeries): super(Surgery, cls).validate(surgeries) for surgery in surgeries: surgery.validate_surgery_period() def validate_surgery_period(self): Lang = Pool().get('ir.lang') language, = Lang.search([ ('code', '=', Transaction().language), ]) if (self.surgery_end_date and self.surgery_date): if (self.surgery_end_date < self.surgery_date): self.raise_user_error( 'end_date_before_start', { 'surgery_date': Lang.strftime(self.surgery_date, language.code, language.date), 'end_date': Lang.strftime(self.surgery_end_date, language.code, language.date), }) @classmethod def write(cls, surgeries, vals): # Don't allow to write the record if the surgery has been signed if surgeries[0].state == 'signed': cls.raise_user_error( "This surgery is at state Done and has been signed\n" "You can no longer modify it.") return super(Surgery, cls).write(surgeries, vals) ## Method to check for availability and make the Operating Room reservation # for the associated surgery @classmethod @ModelView.button def confirmed(cls, surgeries): surgery_id = surgeries[0] Operating_room = Pool().get('gnuhealth.hospital.or') cursor = Transaction().connection.cursor() # Operating Room and end surgery time check if (not surgery_id.operating_room or not surgery_id.surgery_end_date): cls.raise_user_error("Operating Room and estimated end time " "are needed in order to confirm the surgery") or_id = surgery_id.operating_room.id cursor.execute( "SELECT COUNT(*) \ FROM gnuhealth_surgery \ WHERE (surgery_date::timestamp,surgery_end_date::timestamp) \ OVERLAPS (timestamp %s, timestamp %s) \ AND (state = %s or state = %s) \ AND operating_room = CAST(%s AS INTEGER) ", (surgery_id.surgery_date, surgery_id.surgery_end_date, 'confirmed', 'in_progress', str(or_id))) res = cursor.fetchone() if (surgery_id.surgery_end_date < surgery_id.surgery_date): cls.raise_user_error("The Surgery end date must later than the \ Start") if res[0] > 0: cls.raise_user_error('or_is_not_available') else: cls.write(surgeries, {'state': 'confirmed'}) # Cancel the surgery and set it to draft state # Free the related Operating Room @classmethod @ModelView.button def cancel(cls, surgeries): surgery_id = surgeries[0] Operating_room = Pool().get('gnuhealth.hospital.or') cls.write(surgeries, {'state': 'cancelled'}) # Start the surgery @classmethod @ModelView.button def start(cls, surgeries): surgery_id = surgeries[0] Operating_room = Pool().get('gnuhealth.hospital.or') cls.write( surgeries, { 'state': 'in_progress', 'surgery_date': datetime.now(), 'surgery_end_date': datetime.now() }) Operating_room.write([surgery_id.operating_room], {'state': 'occupied'}) # Finnish the surgery # Free the related Operating Room @classmethod @ModelView.button def done(cls, surgeries): surgery_id = surgeries[0] Operating_room = Pool().get('gnuhealth.hospital.or') cls.write(surgeries, { 'state': 'done', 'surgery_end_date': datetime.now() }) Operating_room.write([surgery_id.operating_room], {'state': 'free'}) # Sign the surgery document, and the surgical act. @classmethod @ModelView.button def signsurgery(cls, surgeries): surgery_id = surgeries[0] # Sign, change the state of the Surgery to "Signed" # and write the name of the signing health professional signing_hp = Pool().get( 'gnuhealth.healthprofessional').get_health_professional() if not signing_hp: cls.raise_user_error( "No health professional associated to this user !") cls.write(surgeries, {'state': 'signed', 'signed_by': signing_hp}) def get_report_surgery_date(self, name): Company = Pool().get('company.company') timezone = None company_id = Transaction().context.get('company') if company_id: company = Company(company_id) if company.timezone: timezone = pytz.timezone(company.timezone) dt = self.surgery_date return datetime.astimezone(dt.replace(tzinfo=pytz.utc), timezone).date() def get_report_surgery_time(self, name): Company = Pool().get('company.company') timezone = None company_id = Transaction().context.get('company') if company_id: company = Company(company_id) if company.timezone: timezone = pytz.timezone(company.timezone) dt = self.surgery_date return datetime.astimezone(dt.replace(tzinfo=pytz.utc), timezone).time() @classmethod def search_rec_name(cls, name, clause): if clause[1].startswith('!') or clause[1].startswith('not '): bool_op = 'AND' else: bool_op = 'OR' return [ bool_op, ('patient', ) + tuple(clause[1:]), ('code', ) + tuple(clause[1:]), ]
class TimeDeltaRequired(ModelSQL): 'TimeDelta Required' __name__ = 'test.timedelta_required' timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta', required=True)
class TimeDelta(ModelSQL): 'TimeDelta' __name__ = 'test.timedelta' timedelta = fields.TimeDelta(string='TimeDelta', help='Test timedelta', required=False)
class Trigger(DeactivableMixin, ModelSQL, ModelView): "Trigger" __name__ = 'ir.trigger' name = fields.Char('Name', required=True, translate=True) model = fields.Many2One('ir.model', 'Model', required=True, select=True) on_time = fields.Boolean('On Time', select=True, states={ 'invisible': (Eval('on_create', False) | Eval('on_write', False) | Eval('on_delete', False)), }, depends=['on_create', 'on_write', 'on_delete']) on_create = fields.Boolean('On Create', select=True, states={ 'invisible': Eval('on_time', False), }, depends=['on_time']) on_write = fields.Boolean('On Write', select=True, states={ 'invisible': Eval('on_time', False), }, depends=['on_time']) on_delete = fields.Boolean('On Delete', select=True, states={ 'invisible': Eval('on_time', False), }, depends=['on_time']) condition = fields.Char('Condition', required=True, help='A PYSON statement evaluated with record represented by ' '"self"\nIt triggers the action if true.') limit_number = fields.Integer('Limit Number', required=True, help='Limit the number of call to "Action Function" by records.\n' '0 for no limit.') minimum_time_delay = fields.TimeDelta('Minimum Delay', help='Set a minimum time delay between call to "Action Function" ' 'for the same record.\n' 'empty for no delay.') action = fields.Selection([], "Action", required=True) _get_triggers_cache = Cache('ir_trigger.get_triggers') @classmethod def __setup__(cls): super(Trigger, cls).__setup__() t = cls.__table__() cls._sql_constraints += [ ('on_exclusive', Check(t, ~((t.on_time == Literal(True)) & ((t.on_create == Literal(True)) | (t.on_write == Literal(True)) | (t.on_delete == Literal(True))))), '"On Time" and others are mutually exclusive!'), ] cls._order.insert(0, ('name', 'ASC')) @classmethod def __register__(cls, module_name): cursor = Transaction().connection.cursor() table = cls.__table_handler__(cls, module_name) sql_table = cls.__table__() super(Trigger, cls).__register__(module_name) table_h = cls.__table_handler__(module_name) # Migration from 3.4: # change minimum_delay into timedelta minimum_time_delay if table.column_exist('minimum_delay'): cursor.execute(*sql_table.select( sql_table.id, sql_table.minimum_delay, where=sql_table.minimum_delay != Null)) for id_, delay in cursor: delay = datetime.timedelta(hours=delay) cursor.execute(*sql_table.update( [sql_table.minimum_time_delay], [delay], where=sql_table.id == id_)) table.drop_column('minimum_delay') # Migration from 5.4: merge action if (table_h.column_exist('action_model') and table_h.column_exist('action_function')): pool = Pool() Model = pool.get('ir.model') model = Model.__table__() action_model = model.select( model.model, where=model.id == sql_table.action_model) cursor.execute(*sql_table.update( [sql_table.action], [Concat(action_model, Concat( '|', sql_table.action_function))])) table_h.drop_column('action_model') table_h.drop_column('action_function') @classmethod def validate_fields(cls, triggers, field_names): super().validate_fields(triggers, field_names) cls.check_condition(triggers, field_names) @classmethod def check_condition(cls, triggers, field_names=None): ''' Check condition ''' if field_names and 'condition' not in field_names: return for trigger in triggers: try: PYSONDecoder(noeval=True).decode(trigger.condition) except Exception: raise ConditionError( gettext('ir.msg_trigger_invalid_condition', condition=trigger.condition, trigger=trigger.rec_name)) @staticmethod def default_limit_number(): return 0 @fields.depends('on_time') def on_change_on_time(self): if self.on_time: self.on_create = False self.on_write = False self.on_delete = False @fields.depends('on_create') def on_change_on_create(self): if self.on_create: self.on_time = False @fields.depends('on_write') def on_change_on_write(self): if self.on_write: self.on_time = False @fields.depends('on_delete') def on_change_on_delete(self): if self.on_delete: self.on_time = False @classmethod def get_triggers(cls, model_name, mode): """ Return triggers for a model and a mode """ assert mode in ['create', 'write', 'delete', 'time'], \ 'Invalid trigger mode' if Transaction().context.get('_no_trigger'): return [] key = (model_name, mode) trigger_ids = cls._get_triggers_cache.get(key) if trigger_ids is not None: return cls.browse(trigger_ids) triggers = cls.search([ ('model.model', '=', model_name), ('on_%s' % mode, '=', True), ]) cls._get_triggers_cache.set(key, list(map(int, triggers))) return triggers def eval(self, record): """ Evaluate the condition of trigger """ env = {} env['current_date'] = datetime.datetime.today() env['time'] = time env['context'] = Transaction().context env['self'] = EvalEnvironment(record, record.__class__) return bool(PYSONDecoder(env).decode(self.condition)) def queue_trigger_action(self, records): trigger_records = Transaction().trigger_records[self.id] ids = {r.id for r in records if self.eval(r)} - trigger_records if ids: self.__class__.__queue__.trigger_action(self, list(ids)) trigger_records.update(ids) def trigger_action(self, ids): """ Trigger the action define on trigger for the records """ pool = Pool() TriggerLog = pool.get('ir.trigger.log') Model = pool.get(self.model.model) model, method = self.action.split('|') ActionModel = pool.get(model) cursor = Transaction().connection.cursor() trigger_log = TriggerLog.__table__() ids = [r.id for r in Model.browse(ids) if self.eval(r)] # Filter on limit_number if self.limit_number: new_ids = [] for sub_ids in grouped_slice(ids): sub_ids = list(sub_ids) red_sql = reduce_ids(trigger_log.record_id, sub_ids) cursor.execute(*trigger_log.select( trigger_log.record_id, Count(Literal(1)), where=red_sql & (trigger_log.trigger == self.id), group_by=trigger_log.record_id)) number = dict(cursor) for record_id in sub_ids: if record_id not in number: new_ids.append(record_id) continue if number[record_id] < self.limit_number: new_ids.append(record_id) ids = new_ids def cast_datetime(value): datepart, timepart = value.split(" ") year, month, day = map(int, datepart.split("-")) timepart_full = timepart.split(".") hours, minutes, seconds = map( int, timepart_full[0].split(":")) if len(timepart_full) == 2: microseconds = int(timepart_full[1]) else: microseconds = 0 return datetime.datetime( year, month, day, hours, minutes, seconds, microseconds) # Filter on minimum_time_delay if self.minimum_time_delay: new_ids = [] # Use now from the transaction to compare with create_date timestamp_cast = self.__class__.create_date.sql_cast cursor.execute(*Select([timestamp_cast(CurrentTimestamp())])) now, = cursor.fetchone() if isinstance(now, str): now = cast_datetime(now) for sub_ids in grouped_slice(ids): sub_ids = list(sub_ids) red_sql = reduce_ids(trigger_log.record_id, sub_ids) cursor.execute(*trigger_log.select( trigger_log.record_id, Max(trigger_log.create_date), where=(red_sql & (trigger_log.trigger == self.id)), group_by=trigger_log.record_id)) delay = dict(cursor) for record_id in sub_ids: if record_id not in delay: new_ids.append(record_id) continue # SQLite return string for MAX if isinstance(delay[record_id], str): delay[record_id] = cast_datetime(delay[record_id]) if now - delay[record_id] >= self.minimum_time_delay: new_ids.append(record_id) ids = new_ids records = Model.browse(ids) if records: getattr(ActionModel, method)(records, self) if self.limit_number or self.minimum_time_delay: to_create = [] for record in records: to_create.append({ 'trigger': self.id, 'record_id': record.id, }) if to_create: TriggerLog.create(to_create) @classmethod def trigger_time(cls): ''' Trigger time actions ''' pool = Pool() triggers = cls.search([ ('on_time', '=', True), ]) for trigger in triggers: Model = pool.get(trigger.model.model) # TODO add a domain records = Model.search([]) trigger.trigger_action(records) @classmethod def create(cls, vlist): res = super(Trigger, cls).create(vlist) # Restart the cache on the get_triggers method of ir.trigger cls._get_triggers_cache.clear() return res @classmethod def write(cls, triggers, values, *args): super(Trigger, cls).write(triggers, values, *args) # Restart the cache on the get_triggers method of ir.trigger cls._get_triggers_cache.clear() @classmethod def delete(cls, records): super(Trigger, cls).delete(records) # Restart the cache on the get_triggers method of ir.trigger cls._get_triggers_cache.clear()