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()
Beispiel #2
0
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'),
    ])
Beispiel #3
0
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))
Beispiel #4
0
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)
Beispiel #5
0
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
Beispiel #6
0
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:])]
Beispiel #7
0
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')
Beispiel #8
0
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
Beispiel #9
0
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'))
Beispiel #10
0
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])
Beispiel #11
0
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)
Beispiel #12
0
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,
                    })
Beispiel #13
0
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
Beispiel #14
0
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')
Beispiel #15
0
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")
Beispiel #16
0
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]
Beispiel #17
0
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)
Beispiel #18
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)
Beispiel #20
0
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")
Beispiel #21
0
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()
Beispiel #22
0
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))
Beispiel #23
0
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'),
            ))
Beispiel #24
0
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
Beispiel #25
0
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
Beispiel #26
0
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()
Beispiel #27
0
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:]),
        ]
Beispiel #28
0
class TimeDeltaRequired(ModelSQL):
    'TimeDelta Required'
    __name__ = 'test.timedelta_required'
    timedelta = fields.TimeDelta(string='TimeDelta',
                                 help='Test timedelta',
                                 required=True)
Beispiel #29
0
class TimeDelta(ModelSQL):
    'TimeDelta'
    __name__ = 'test.timedelta'
    timedelta = fields.TimeDelta(string='TimeDelta',
                                 help='Test timedelta',
                                 required=False)
Beispiel #30
0
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()