trytond-farm/animal.py

1561 lines
59 KiB
Python

# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from datetime import date, datetime, timedelta
from decimal import Decimal
from trytond.rpc import RPC
from trytond.model import ModelView, ModelSQL, fields, UnionMixin, Unique
from trytond.pyson import Equal, Eval, Greater, Id, Not, Bool
from trytond.transaction import Transaction
from trytond.pool import Pool, PoolMeta
from trytond.wizard import Wizard, StateView, StateAction, Button, StateTransition
from trytond.exceptions import UserError
from trytond.i18n import gettext
from sql import Table
_STATES_MALE_FIELD = {
'invisible': Not(Equal(Eval('type'), 'male')),
}
_DEPENDS_MALE_FIELD = ['type']
_STATES_FEMALE_FIELD = {
'invisible': Not(Equal(Eval('type'), 'female')),
}
_DEPENDS_FEMALE_FIELD = ['type']
_STATES_INDIVIDUAL_FIELD = {
'invisible': Not(Equal(Eval('type'), 'individual')),
}
_DEPENDS_INDIVIDUAL_FIELD = ['type']
ANIMAL_ORIGIN = [
('purchased', 'Purchased'),
('raised', 'Raised'),
]
FEMALE_CICLE_STATES = [
('mated', 'Mated'),
('pregnant', 'Pregnant'),
('lactating', 'Lactating'),
('unmated', 'Unmated'),
]
class Tag(ModelSQL, ModelView):
'Farm Tags'
__name__ = 'farm.tag'
name = fields.Char('Name', required=True)
animals = fields.Many2Many('farm.animal-farm.tag', 'tag', 'animal',
'Animals')
animal_group = fields.Many2Many('farm.animal.group-farm.tag', 'tag',
'group', 'Groups')
@classmethod
def __setup__(cls):
super(Tag, cls).__setup__()
t = cls.__table__()
cls._sql_constraints += [
('name_uniq', Unique(t, t.name),
'farm.tag_must_be_unique'),
]
class AnimalMixin:
__slots__ = ()
feed_unit_digits = fields.Function(fields.Integer('Feed Unit Digits'),
'get_feed_unit_digits')
def get_feed_unit_digits(self, name):
Uom = Pool().get('product.uom')
weight_base_uom, = Uom.search([
('symbol', '=', 'kg'),
])
return weight_base_uom.id
@classmethod
def _create_and_done_first_stock_move(cls, records):
"""
It creates the first stock.move for animal's lot, and then confirms,
assigns and set done it to get stock in initial location (Farm).
"""
pool = Pool()
Move = pool.get('stock.move')
with Transaction().set_context(_check_access=False):
new_moves = []
for record in records:
move = record._get_first_move()
new_moves.append(move._save_values)
new_moves = Move.create(new_moves)
Move.assign(new_moves)
Move.do(new_moves)
return new_moves
def _get_first_move(self):
"""
Prepare values to create the first stock.move for animal's lot to
get stock in initial location (Farm).
"""
pool = Pool()
Company = pool.get('company.company')
Move = Pool().get('stock.move')
context = Transaction().context
company = Company(context['company'])
if self.origin == 'purchased':
from_location = company.party.supplier_location
if not from_location:
raise UserError(gettext('farm.missing_supplier_location',
party=company.party.rec_name))
else: # raised
from_location = self.initial_location.warehouse.production_location
if not from_location:
raise UserError(gettext('farm.missing_production_location',
location=self.initial_location.warehouse.rec_name))
move_date = self.arrival_date or date.today()
return Move(
product=self.lot.product,
unit=self.lot.product.default_uom,
quantity=getattr(self, 'initial_quantity', 1),
from_location=from_location,
to_location=self.initial_location,
planned_date=move_date,
effective_date=move_date,
company=company,
lot=self.lot,
unit_price=self.lot.product.cost_price,
currency=company.currency,
origin=self)
class Animal(ModelSQL, ModelView, AnimalMixin):
"Farm Animal"
__name__ = 'farm.animal'
type = fields.Selection([
('male', 'Male'),
('female', 'Female'),
('individual', 'Individual'),
], 'Type', required=True, states={
'readonly': True,
})
specie = fields.Many2One('farm.specie', 'Specie', required=True,
states={
'readonly': True,
})
breed = fields.Many2One('farm.specie.breed', 'Breed', required=True,
domain=[('specie', '=', Eval('specie'))], depends=['specie'])
lot = fields.Many2One('stock.lot', 'Lot',
readonly=True, domain=[
('animal_type', '=', Eval('type')),
], depends=['type'])
number = fields.Function(fields.Char('Number'),
'get_number', 'set_number', searcher='search_number')
# location is updated in do() of stock.move
location = fields.Many2One('stock.location', 'Current Location',
readonly=True, domain=[
('type', '!=', 'warehouse'),
('silo', '=', False),
], help='Indicates where the animal currently resides.')
farm = fields.Function(fields.Many2One('stock.location', 'Current Farm'),
'on_change_with_farm', searcher='search_farm')
origin = fields.Selection(ANIMAL_ORIGIN, 'Origin', required=True,
readonly=True,
help='Raised means that this animal was born in the farm. Otherwise, '
'it was purchased.')
arrival_date = fields.Date('Arrival Date', states={
'readonly': Greater(Eval('id', 0), 0),
}, depends=['id'],
help="The date this animal arrived (if it was purchased) or when it "
"was born.")
purchase_shipment = fields.Many2One('stock.shipment.in',
'Purchase Shipment', readonly=True,
states={'invisible': Not(Equal(Eval('origin'), 'purchased'))},
depends=['origin'])
initial_location = fields.Many2One('stock.location', 'Initial Location',
required=True, domain=[
('type', '=', 'storage'),
('silo', '=', False),
],
states={'readonly': Greater(Eval('id', 0), 0)}, depends=['id'],
context={'restrict_by_specie_animal_type': True},
help="The Location where the animal was reached or where it was "
"allocated when it was purchased.\nIt is used as historical "
"information and to get Serial Number.")
birthdate = fields.Date('Birthdate')
removal_date = fields.Date('Removal Date', readonly=True,
help='Get information from the corresponding removal event.')
removal_reason = fields.Many2One('farm.removal.reason', 'Removal Reason',
readonly=True)
weights = fields.One2Many('farm.animal.weight', 'animal',
'Weight Records', readonly=False, order=[('timestamp', 'DESC')])
current_weight = fields.Function(fields.Many2One('farm.animal.weight',
'Current Weight'),
'on_change_with_current_weight')
tags = fields.Many2Many('farm.animal-farm.tag', 'animal', 'tag', 'Tags')
notes = fields.Text('Notes')
active = fields.Boolean('Active')
consumed_feed = fields.Function(fields.Numeric('Consumed Feed (Kg)',
digits=(16, Eval('feed_unit_digits', 2)),
depends=['feed_unit_digits']),
'get_consumed_feed')
# Individual Fields
sex = fields.Selection([
('male', "Male"),
('female', "Female"),
('undetermined', "Undetermined"),
], 'Sex', required=True, states=_STATES_INDIVIDUAL_FIELD,
depends=_DEPENDS_INDIVIDUAL_FIELD)
purpose = fields.Selection([
(None, ''),
('sale', 'Sale'),
('replacement', 'Replacement'),
('unknown', 'Unknown'),
], 'Purpose', states=_STATES_INDIVIDUAL_FIELD,
depends=_DEPENDS_INDIVIDUAL_FIELD)
active = fields.Boolean('Active')
lots= fields.One2Many(
'stock.lot', 'animal', 'Lots', readonly=True)
# We can't use the 'required' attribute in field because it's
# checked on view before execute 'create()' function where this
# field is filled in.
@classmethod
def __register__(cls, module_name):
table = cls.__table_handler__(module_name)
sql_table = cls.__table__()
update_lot = False
if not table.column_exist('lot'):
update_lot = True
super().__register__(module_name)
table = cls.__table_handler__(module_name)
if update_lot:
sql_table_animal_lot = 'stock_lot-farm_animal'
if table.table_exist(sql_table_animal_lot):
sql_table_animal_lot = Table(sql_table_animal_lot)
cursor = Transaction().connection.cursor()
cursor.execute(*sql_table_animal_lot.select(
sql_table_animal_lot.animal, sql_table_animal_lot.lot))
for animal_id, lot_id in cursor.fetchall():
cursor.execute(*sql_table.update(columns=[sql_table.lot],
values=[lot_id], where=sql_table.id == animal_id))
@staticmethod
def default_specie():
return Transaction().context.get('specie')
@staticmethod
def default_breed():
pool = Pool()
Specie = pool.get('farm.specie')
context = Transaction().context
if 'specie' in context:
specie = Specie(context['specie'])
if len(specie.breeds) == 1:
return specie.breeds[0].id
@staticmethod
def default_type():
return Transaction().context.get('animal_type')
@staticmethod
def default_origin():
return 'purchased'
@staticmethod
def default_arrival_date():
return date.today()
@staticmethod
def default_sex():
sex = Transaction().context.get('animal_type', 'undetermined')
if sex in ('group', 'individual'):
sex = 'undetermined'
return sex
@staticmethod
def default_active():
return True
def get_rec_name(self, name):
if self.lot:
name = self.lot.number
if not self.active:
name += ' (*)'
return name
@classmethod
def search_rec_name(cls, name, clause):
return [('lot.number',) + tuple(clause[1:])]
@classmethod
def search_number(cls, name, clause):
return [('lot.number',) + tuple(clause[1:])]
def get_number(self, name):
if self.lot:
return self.lot.number
@classmethod
def set_number(cls, animals, name, value):
Lot = Pool().get('stock.lot')
lots = [animal.lot for animal in animals if animal.lot]
if lots:
Lot.write(lots, {
'number': value,
})
@fields.depends('location')
def on_change_with_farm(self, name=None):
return (self.location and self.location.warehouse and
self.location.warehouse.id or None)
@classmethod
def search_farm(cls, name, clause):
return [('location.warehouse',) + tuple(clause[1:])]
@fields.depends('weights')
def on_change_with_current_weight(self, name=None):
if self.weights:
return self.weights[0].id
def get_consumed_feed(self, name):
pool = Pool()
FeedEvent = pool.get('farm.feed.event')
Uom = pool.get('product.uom')
now = datetime.now()
feed_events = FeedEvent.search([
('animal_type', '=', self.type),
('animal', '=', self.id),
('state', 'in', ['provisional', 'validated']),
['OR', [
('start_date', '=', None),
('timestamp', '<=', now),
], [
('start_date', '<=', now.date()),
]],
])
kg, = Uom.search([
('symbol', '=', 'kg'),
])
consumed_feed = Decimal('0.0')
for event in feed_events:
# TODO: it uses compute_price() because quantity is a Decimal
# quantity in feed_product default uom. The method is not for
# this purpose but it works
event_feed_quantity = Uom.compute_price(kg, event.feed_quantity,
event.uom)
if event.timestamp > now:
event_feed_quantity /= (event.end_date - event.start_date).days
event_feed_quantity *= (now.date() - event.start_date).days
consumed_feed += event_feed_quantity
return consumed_feed
def check_in_location(self, location, timestamp):
Lot = Pool().get('stock.lot')
with Transaction().set_context(
locations=[location.id],
stock_date_end=timestamp.date()):
# Object must be reinstantiated in order for the context to take
# effect when computing lot's quantity.
lot = Lot(self.lot.id)
return lot.quantity == 1
def check_allowed_location(self, location, event_rec_name):
if not location.warehouse:
return
for farm_line in self.specie.farm_lines:
if farm_line.farm.id == location.warehouse.id:
if getattr(farm_line, 'has_%s' % self.type):
return
raise UserError(gettext('farm.invalid_animal_destination',
event=event_rec_name,
animal=self.rec_name,
location=location.rec_name,
))
@classmethod
def copy(cls, animals, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.update({
'lot': None,
'number': None,
'location': None,
'purchase_shipment': None,
'removal_reason': None,
'removal_date': None,
'weights': None,
})
return super(Animal, cls).copy(animals, default=default)
@classmethod
def create(cls, vlist):
pool = Pool()
Location = pool.get('stock.location')
Lot = pool.get('stock.lot')
context = Transaction().context
vlist = [x.copy() for x in vlist]
for vals in vlist:
if not vals.get('specie'):
vals['specie'] = cls.default_specie()
if not vals.get('type'):
vals['type'] = cls.default_type()
if vals['type'] in ('male', 'female'):
vals['sex'] = vals['type']
if not vals.get('number'):
location = Location(vals['initial_location'])
vals['number'] = cls._calc_number(vals['specie'],
location.warehouse.id, vals['type'])
new_animals = super(Animal, cls).create(vlist)
for animal, vals in zip(new_animals, vlist):
vals['id'] = animal.id
if vals.get('lot'):
lot = Lot(vals['lot'])
Lot.write([lot], cls._get_lot_values(vals, False))
animal.lot = lot
animal.save()
else:
new_lot, = Lot.create([cls._get_lot_values(vals, True)])
animal.lot = new_lot
animal.save()
if not context.get('no_create_stock_move'):
cls._create_and_done_first_stock_move(new_animals)
return new_animals
@classmethod
def _calc_number(cls, specie_id, farm_id, type):
pool = Pool()
FarmLine = pool.get('farm.specie.farm_line')
Location = pool.get('stock.location')
Specie = pool.get('farm.specie')
sequence_fieldname = '%s_sequence' % type
farm_lines = FarmLine.search([
('specie', '=', specie_id),
('farm', '=', farm_id),
('has_' + type, '=', True),
])
if not farm_lines:
raise UserError(gettext(
'farm.animal_no_farm_specie_farm_line_available',
farm=Location(farm_id).rec_name,
animal_type=type,
specie=Specie(specie_id).rec_name,
))
farm_line, = farm_lines
sequence = getattr(farm_line, sequence_fieldname, False)
if not sequence:
raise UserError(gettext('farm.no_sequence_in_farm_line',
sequence_field=getattr(FarmLine, sequence_fieldname).string,
farm_line=farm_line.rec_name,
))
return sequence.get()
@classmethod
def _get_lot_values(cls, animal_vals, create):
"""
Prepare values to create the stock.lot for the new animal.
animal_vals: dictionary with values to create farm.animal
It returns a dictionary with values to create stock.lot
"""
pool = Pool()
Specie = pool.get('farm.specie')
if not animal_vals:
return {}
specie = Specie(animal_vals['specie'])
product_fieldname = '%s_product' % animal_vals['type']
product = getattr(specie, product_fieldname, False)
if not product:
raise UserError(gettext('farm.no_product_in_specie',
product_field=getattr(Specie, product_fieldname).string,
specie=specie.rec_name,
))
res = {
'number': animal_vals['number'],
'product': product.id,
'animal_type': animal_vals['type'],
'animal': animal_vals['id']
}
return res
@classmethod
def delete(cls, animals):
pool = Pool()
Lot = pool.get('stock.lot')
lots = [a.lot for a in animals if a.lot is not None]
if lots:
Lot.write(lots, {'animal': None})
result = super(Animal, cls).delete(animals)
if lots:
Lot.delete(lots)
return result
class AnimalTag(ModelSQL):
'Animal - Tag'
__name__ = 'farm.animal-farm.tag'
animal = fields.Many2One('farm.animal', 'Animal', ondelete='CASCADE',
required=True)
tag = fields.Many2One('farm.tag', 'Tag', ondelete='CASCADE', required=True)
class AnimalWeight(ModelSQL, ModelView):
'Farm Animal Weight Record'
__name__ = 'farm.animal.weight'
_order = [('timestamp', 'DESC')]
animal = fields.Many2One('farm.animal', 'Animal',
required=True,
ondelete='CASCADE')
timestamp = fields.DateTime('Date & Time', required=True)
uom = fields.Many2One('product.uom', "UoM",
required=True,
domain=[('category', '=', Id('product', 'uom_cat_weight'))])
unit_digits = fields.Function(fields.Integer('Unit Digits'),
'on_change_with_unit_digits')
weight = fields.Numeric('Weight',
digits=(16, Eval('unit_digits', 2)),
required=True,
depends=['unit_digits'])
@staticmethod
def default_timestamp():
return datetime.now()
@staticmethod
def default_uom():
return Pool().get('ir.model.data').get_id('product', 'uom_kilogram')
@staticmethod
def default_unit_digits():
return 2
def get_rec_name(self, name):
return '%s %s (%s)' % (self.weight, self.uom.symbol, self.timestamp)
@classmethod
def search_rec_name(cls, name, clause):
operand = clause[2]
if isinstance(operand, str):
# May be None
operand = operand.replace('%', '')
try:
operand = Decimal(operand)
except:
return [('weight', '=', 0)]
operator = clause[1]
operator = operator.replace('ilike', '=').replace('like', '=')
return [('weight', operator, operand)]
@fields.depends('uom')
def on_change_with_unit_digits(self, name=None):
if self.uom:
return self.uom.digits
return 2
class Male(metaclass=PoolMeta):
__name__ = 'farm.animal'
extractions = fields.One2Many('farm.semen_extraction.event',
'animal', 'Semen Extractions', states=_STATES_MALE_FIELD,
depends=_DEPENDS_MALE_FIELD)
last_extraction = fields.Date('Last Extraction', readonly=True,
states=_STATES_MALE_FIELD, depends=_DEPENDS_MALE_FIELD)
def update_last_extraction(self, validated_event=None):
if not self.extractions:
self.last_extraction = None
self.save()
return None
last_extraction = None
reversed_extractions = list(self.extractions)
reversed_extractions.reverse()
for extraction_event in reversed_extractions:
if (extraction_event.state == 'validated' or
validated_event and extraction_event == validated_event):
last_extraction = extraction_event.timestamp.date()
break
self.last_extraction = last_extraction
self.save()
return last_extraction
class Female(metaclass=PoolMeta):
__name__ = 'farm.animal'
cycles = fields.One2Many('farm.animal.female_cycle', 'animal', 'Cycles',
readonly=True, order=[
('sequence', 'ASC'),
('ordination_date', 'ASC'),
],
states=_STATES_FEMALE_FIELD, depends=_DEPENDS_FEMALE_FIELD)
current_cycle = fields.Many2One('farm.animal.female_cycle',
'Current Cycle', readonly=True, states=_STATES_FEMALE_FIELD,
depends=_DEPENDS_FEMALE_FIELD)
current_cycle_state = fields.Selection([(None, '')] + FEMALE_CICLE_STATES,
'Current Cycle State', readonly=True, states=_STATES_FEMALE_FIELD,
depends=_DEPENDS_FEMALE_FIELD)
state = fields.Selection([
(None, ''),
('prospective', 'Prospective'),
('unmated', 'Unmated'),
('mated', 'Mated'),
('removed', 'Removed'),
],
'Status', readonly=True, states=_STATES_FEMALE_FIELD,
depends=_DEPENDS_FEMALE_FIELD,
help='According to NPPC Production and Financial Standards there are '
'four status for breeding sows. The status change is event driven: '
'arrival date, entry date mating event and removal event')
first_mating = fields.Function(fields.Date('First Mating',
states=_STATES_FEMALE_FIELD, depends=_DEPENDS_FEMALE_FIELD,
help='Date of first mating. This will change the status of the '
'sow to "mated"'),
'get_first_mating')
days_from_insemination = fields.Function(fields.Integer('Inseminated Days',
help='Number of days from last insemination. -1 if there isn\'t '
'any insemination.'),
'get_days_from_insemination', searcher='search_days_from_insemination')
last_produced_group = fields.Function(fields.Many2One('farm.animal.group',
'Last Produced Group', domain=[
('specie', '=', Eval('specie')),
], depends=['specie']),
'get_last_produced_group')
days_from_farrowing = fields.Function(fields.Integer('Unpregnant Days',
help='Number of days from last farrowing. -1 if there '
'isn\'t any farrowing.'),
'get_days_from_farrowing', searcher='search_days_from_farrowing')
farrowing_group = fields.Function(fields.Many2One('farm.animal.group',
'Farrowing Group'),
'get_farrowing_group')
events = fields.One2Many('farm.animal.cycle.events', 'animal', 'Events',
readonly=True)
@classmethod
def __setup__(cls):
super(Female, cls).__setup__()
cls._buttons.update({
'change_observation': {
'invisible': Not(Bool(Eval('cycles'))),
}
})
@staticmethod
def default_state():
'''
Specific for Female animals.
'''
if Transaction().context.get('animal_type') == 'female':
return 'prospective'
return None
@classmethod
@ModelView.button_action('farm.wizard_farm_cycle_observation_female')
def change_observation(cls, records):
pass
def is_lactating(self):
return (self.current_cycle and self.current_cycle.state == 'lactating'
or False)
# TODO: call when cycle is created, deleted or its ordination_date or
# sequence are modifyied
def update_current_cycle(self):
current_cycle = self.cycles and self.cycles[-1] or None
self.current_cycle = current_cycle
self.current_cycle_state = (current_cycle.state
if current_cycle else None)
self.save()
return current_cycle
def get_state(self):
if self.type != 'female':
return
if self.removal_date and self.removal_date <= date.today():
state = 'removed'
elif (not self.cycles or len(self.cycles) == 1 and
not self.cycles[0].weaning_event and
self.cycles[0].state == 'unmated'):
state = 'prospective'
elif self.current_cycle and self.current_cycle.state == 'unmated':
state = 'unmated'
else:
state = 'mated'
return state
# TODO: call in removal event, when cycle is added (but probably it's
# called from cycle)
def update_state(self):
self.state = self.get_state()
self.current_cycle_state = (self.current_cycle.state
if self.current_cycle else None)
self.save()
return self.state
def get_first_mating(self, name):
InseminationEvent = Pool().get('farm.insemination.event')
if self.type != 'female':
return None
first_inseminations = InseminationEvent.search([
('animal', '=', self.id),
], limit=1, order=[('timestamp', 'ASC')])
if not first_inseminations:
return None
first_insemination, = first_inseminations
return first_insemination.timestamp.date()
def get_days_from_insemination(self, name):
InseminationEvent = Pool().get('farm.insemination.event')
last_valid_insemination = InseminationEvent.search([
('animal', '=', self.id),
('state', '=', 'validated'),
], order=[('timestamp', 'DESC')], limit=1)
if not last_valid_insemination:
return -1
days_from_insemination = (date.today() -
last_valid_insemination[0].timestamp.date()).days
return days_from_insemination
@classmethod
def search_days_from_insemination(cls, name, clause):
InseminationEvent = Pool().get('farm.insemination.event')
event_filter, operator = cls._get_filter_search_days(name, clause)
animal_ids = set()
for event in InseminationEvent.search(event_filter):
animal_ids.add(event.animal.id)
return [
('type', '=', 'female'),
('id', operator, list(animal_ids)),
]
def get_last_produced_group(self, name):
FarrowingEvent = Pool().get('farm.farrowing.event')
last_farrowing_events = FarrowingEvent.search([
('animal', '=', self),
('state', '=', 'validated'),
('produced_group', '!=', None),
],
order=[
('timestamp', 'DESC'),
], limit=1)
if last_farrowing_events:
return last_farrowing_events[0].produced_group.id
return None
def get_days_from_farrowing(self, name):
FarrowingEvent = Pool().get('farm.farrowing.event')
last_valid_farrowing = FarrowingEvent.search([
('animal', '=', self.id),
('state', '=', 'validated'),
], order=[('timestamp', 'DESC')], limit=1)
if not last_valid_farrowing:
return -1
days_from_farrowing = (date.today() -
last_valid_farrowing[0].timestamp.date()).days
return days_from_farrowing
@classmethod
def search_days_from_farrowing(cls, name, clause):
FarrowingEvent = Pool().get('farm.farrowing.event')
event_filter, operator = cls._get_filter_search_days(name, clause)
animal_ids = set()
for event in FarrowingEvent.search(event_filter):
animal_ids.add(event.animal.id)
return [
('type', '=', 'female'),
('id', operator, list(animal_ids)),
]
@classmethod
def _get_filter_search_days(cls, name, clause):
event_filter = []
include_oposite = False
if isinstance(clause[2], bool) and clause[2] is False:
if clause[1] == '=':
include_oposite = True
# else: "!= False" => inseminated sometimes
elif isinstance(clause[2], int):
# third element is a number of days
operator = False
n_days = False
if clause[1] in ('<', '<='):
operator = '>'
if clause[1] == '<':
n_days = clause[2]
else:
n_days = clause[2] + 1
elif clause[1] in ('>', '>='):
operator = '<'
include_oposite = True
if clause[1] == '>':
n_days = clause[2] + 1
else:
n_days = clause[2]
elif clause[1] in ('=', '!='):
operator = clause[1]
n_days = clause[2]
if operator and n_days:
date_lim = date.today() - timedelta(days=n_days)
if operator == '=':
event_filter = ['AND', [
('timestamp', '>=',
date_lim.strftime('%Y-%m-%d 00:00:00')),
], [
('timestamp', '<=',
date_lim.strftime('%Y-%m-%d 23:59:59')),
], ]
elif operator == '!=':
include_oposite = True
event_filter = ['OR', [
('timestamp', '<',
date_lim.strftime('%Y-%m-%d 00:00:00')),
], [
('timestamp', '>',
date_lim.strftime('%Y-%m-%d 23:59:59')),
], ]
else:
event_filter = [
('timestamp', operator,
date_lim.strftime('%Y-%m-%d 23:59:59')),
]
op = 'in'
if include_oposite:
if event_filter:
event_filter.insert(0, 'NOT')
op = 'not in'
return event_filter, op
def get_farrowing_group(self, name):
'''
Return the farm.animal.group produced for current cycle
'''
if not self.current_cycle or self.current_cycle.state != 'lactating':
return None
return self.current_cycle.farrowing_event.produced_group.id
@classmethod
def create(cls, vlist):
pool = Pool()
Animal = pool.get('farm.animal')
Location = pool.get('stock.location')
for vals in vlist:
if vals.get('type', '') == 'female' and not vals.get('state'):
vals['state'] = 'prospective'
number = vals.get('number')
initial_location = vals.get('initial_location')
location = Location(initial_location)
if not location.warehouse:
raise UserError(gettext('farm.location_without_warehouse',
location=location))
duplicate = Animal.search([
('number', '=', number),
('farm', '=', location.warehouse.id),
('active', '=', True),
], limit=1)
if duplicate:
raise UserError(gettext('farm.duplicate_animal', number=number))
return super(Female, cls).create(vlist)
@classmethod
def write(cls, *args):
pool = Pool()
Animal = pool.get('farm.animal')
actions = iter(args)
for females, values in zip(actions, actions):
number = values.get('number')
if not number:
continue
for female in females:
farm = female.farm
duplicate = Animal.search([('number', '=', number),
('farm', '=', farm),
('active', '=', True),
('id', '!=', female.id)], limit=1)
if duplicate:
raise UserError('farm.duplicate_animal', number=number)
super(Female, cls).write(*args)
@classmethod
def copy(cls, females, default=None):
if default is None:
default = {}
else:
default = default.copy()
default['cycles'] = None
default['current_cycle'] = None
default['state'] = cls.default_state()
return super(Female, cls).copy(females, default)
class FemaleCycle(ModelSQL, ModelView):
'Farm Female Cycle'
__name__ = 'farm.animal.female_cycle'
_order = [
('animal', 'ASC'),
('sequence', 'ASC'),
('ordination_date', 'ASC'),
]
animal = fields.Many2One('farm.animal', 'Female', required=True,
ondelete='CASCADE', domain=[('type', '=', 'female')])
sequence = fields.Integer('Num. cycle', required=True)
ordination_date = fields.DateTime('Date for ordination', required=True,
readonly=True)
state = fields.Selection(FEMALE_CICLE_STATES, 'State', readonly=True,
required=True)
# Female events fields
insemination_events = fields.One2Many('farm.insemination.event',
'female_cycle', 'Inseminations')
days_between_weaning_and_insemination = fields.Function(
fields.Integer('Unmated Days', help='Number of days between previous '
'weaning and first insemination.'),
'get_days_between_weaning_and_insemination')
diagnosis_events = fields.One2Many('farm.pregnancy_diagnosis.event',
'female_cycle', 'Diagnosis')
pregnant = fields.Function(fields.Boolean('Pregnant',
help='A female will be considered pregnant if there are more than'
' one diagnosis and the last one has a positive result.'),
'on_change_with_pregnant')
abort_event = fields.One2One('farm.abort.event-farm.animal.female_cycle',
'cycle', 'event', string='Abort', readonly=True, domain=[
('animal', '=', Eval('animal')),
], depends=['animal'])
farrowing_event = fields.One2One(
'farm.farrowing.event-farm.animal.female_cycle', 'cycle', 'event',
string='Farrowing', readonly=True, domain=[
('animal', '=', Eval('animal')),
], depends=['animal'])
live = fields.Function(fields.Integer('Live'),
'get_farrowing_event_field')
dead = fields.Function(fields.Integer('Dead'),
'get_farrowing_event_field')
foster_events = fields.One2Many('farm.foster.event', 'female_cycle',
'Fosters')
fostered = fields.Function(fields.Integer('Fostered',
help='Diference between Fostered Input and Output. A negative '
'number means that he has given more than taken.'),
'on_change_with_fostered')
weaning_event = fields.One2One(
'farm.weaning.event-farm.animal.female_cycle', 'cycle', 'event',
string='Weaning', readonly=True, domain=[
('animal', '=', Eval('animal')),
], depends=['animal'])
weaned = fields.Function(fields.Integer('Weaned Quantity'),
'get_weaned')
removed = fields.Function(fields.Integer('Removed Quantity',
help='Number of removed animals from Produced Group. Diference '
'between born live and weaned, computing Fostered diference.'),
'get_removed')
days_between_farrowing_weaning = fields.Function(
fields.Integer('Lactating Days',
help='Number of days between Farrowing and Weaning.'),
'get_lactating_days')
observations = fields.Text('Observations')
@staticmethod
def default_sequence(animal_id=None):
'''
If 'animal_id' is not found in context it return 0.
Otherwise, if the last cycle is completed (has 'farrowing event'), it
returns its sequence plus one, if it's not completed it returns its
sequence.
'''
FemaleCycle = Pool().get('farm.animal.female_cycle')
animal_id = animal_id or Transaction().context.get('animal')
if not animal_id:
return 0
cycles = FemaleCycle.search([
('animal', '=', animal_id),
],
order=[
('sequence', 'DESC'),
('ordination_date', 'DESC'),
], limit=1)
if not cycles:
return 1
if cycles[0].farrowing_event:
return cycles[0].sequence + 1
return cycles[0].sequence
@staticmethod
def default_ordination_date():
return datetime.now()
@staticmethod
def default_state():
return 'unmated'
@fields.depends('_parent_animal.id', 'animal', 'ordination_date')
def on_change_ordination_date(self):
if not self.ordination_date or not self.animal:
return
past_date = self.animal.current_cycle.ordination_date.date()
current_date = self.ordination_date.date()
if past_date > current_date:
raise UserError(gettext('farm.cycle_invalid_date'))
def get_rec_name(self, name):
state_labels = dict(self.fields_get(['state'])['state']['selection'])
return "%s (%s)" % (self.sequence, state_labels[self.state])
# TODO: call in weaning, farrowing, abort, pregnancy_diagnosis and
# insemination event (in 'valid()' and 'cancel()')
def update_state(self, validated_event):
'''
Sorted rules:
- A cycle will be considered 'unmated'
if weaning_event_id != False and weaning_event.state == 'validated'
or if abort_event != False has abort_event.state == 'validated'
or has not any validated event in insemination_event_ids.
- A female will be considered 'lactating'
if farrowing_event_id!=False and farrowing_event.state=='validated'
- A female will be considered 'pregnant' if there are more than one
diagnosis in 'validated' state and the last one has a positive result
- A female will be considered 'mated' if there are any items in
insemination_event_ids with 'validated' state.
'''
def check_event(event_to_check):
return (type(event_to_check) == type(validated_event) and
event_to_check == validated_event or
event_to_check.state == 'validated')
state = 'unmated'
if (self.abort_event and check_event(self.abort_event) or
self.weaning_event and check_event(self.weaning_event)):
state = 'unmated'
elif self.farrowing_event and check_event(self.farrowing_event):
if self.farrowing_event.live > 0:
state = 'lactating'
else:
state = 'unmated'
elif self.pregnant:
state = 'pregnant'
else:
for insemination_event in self.insemination_events:
if check_event(insemination_event):
state = 'mated'
break
self.state = state
self.save()
self.animal.update_state()
return state
def get_days_between_weaning_and_insemination(self, name):
if not self.insemination_events:
return None
previous_cycles = self.search([
('animal', '=', self.animal.id),
('sequence', '<=', self.sequence),
('id', '!=', self.id)
],
order=[
('sequence', 'DESC'),
('ordination_date', 'DESC'),
], limit=1)
if not previous_cycles or (not previous_cycles[0].weaning_event and
not previous_cycles[0].abort_event):
return None
previous_date = (previous_cycles[0].weaning_event.timestamp.date()
if previous_cycles[0].weaning_event
else previous_cycles[0].abort_event.timestamp.date())
insemination_date = self.insemination_events[0].timestamp.date()
return (insemination_date - previous_date).days
@fields.depends('abort_event', 'diagnosis_events', 'farrowing_event')
def on_change_with_pregnant(self, name=None):
if self.abort_event:
return False
if not self.diagnosis_events:
return False
if self.farrowing_event:
return False
# relation to cycle is set on event validate so it's not required to
# check the state
return self.diagnosis_events[-1].result == 'positive'
def get_farrowing_event_field(self, name):
return (self.farrowing_event and getattr(self.farrowing_event, name)
or 0)
@fields.depends('foster_events')
def on_change_with_fostered(self, name=None):
return sum(e.quantity for e in self.foster_events)
def get_weaned(self, name):
return self.weaning_event and self.weaning_event.quantity or 0
def get_removed(self, name):
if not self.weaning_event:
return None
return self.live + self.fostered - self.weaned
def get_lactating_days(self, name):
if not self.farrowing_event or not self.weaning_event:
return None
return (self.weaning_event.timestamp.date() -
self.farrowing_event.timestamp.date()).days
@classmethod
def create(cls, vlist):
vlist = [v.copy() for v in vlist]
for vals in vlist:
if not vals.get('sequence') and vals.get('animal'):
vals['sequence'] = cls.default_sequence(vals['animal'])
return super(FemaleCycle, cls).create(vlist)
class EventUnion(UnionMixin, ModelSQL, ModelView):
'Union between female cycles and events'
__name__ = 'farm.animal.cycle.events'
timestamp = fields.DateTime('Date & Time', required=True)
cycle = fields.Function(fields.Many2One('farm.animal.female_cycle',
'Cycle'), 'get_cycle')
event_type = fields.Function(fields.Char('Event Type'), 'get_event_type')
event_link = fields.Function(fields.Reference('Event',
selection='get_fieldname'), 'get_event')
animal = fields.Many2One('farm.animal', 'Animal')
@classmethod
def __setup__(cls):
super(EventUnion, cls).__setup__()
cls.__rpc__.update({
'get_fieldname': RPC(),
})
cls._order.insert(0, ('timestamp', 'ASC'))
@classmethod
def _get_fieldname(cls):
return ['farm.abort.event', 'farm.event.order',
'farm.farrowing.event', 'farm.feed.event',
'farm.foster.event', 'farm.insemination.event',
'farm.medication.event', 'farm.move.event',
'farm.pregnancy_diagnosis.event', 'farm.removal.event',
'farm.semen_extraction.event', 'farm.transformation.event',
'farm.weaning.event']
@classmethod
def get_fieldname(cls):
pool = Pool()
Model = pool.get('ir.model')
models = cls._get_fieldname()
models = Model.search([
('model', 'in', models),
])
return [('', '')] + [(m.model, m.name) for m in models]
def _get_event(self):
cls = self.__class__
model = cls.union_unshard(self.id)
return model
def get_event_type(self, name=None):
pool = Pool()
Model = pool.get('ir.model')
model, = Model.search([
('model', '=', self._get_event().__class__.__name__),
], limit=1)
return model.name
def get_event(self, name=None):
return str(self._get_event())
def get_cycle(self, name=None):
model = self._get_event()
if hasattr(model, 'female_cycle') and model.female_cycle:
return model.female_cycle.id
return None
@staticmethod
def union_models():
res = super(EventUnion, EventUnion).union_models()
models = ['farm.abort.event', 'farm.event.order',
'farm.farrowing.event', 'farm.feed.event',
'farm.foster.event', 'farm.insemination.event',
'farm.medication.event', 'farm.move.event',
'farm.pregnancy_diagnosis.event', 'farm.removal.event',
'farm.semen_extraction.event', 'farm.transformation.event',
'farm.weaning.event']
return res + models
class ChangeCycleObservationStart(ModelView):
'Sets the value of the observation field of a cycle'
__name__ = 'female.cycle.observation.start'
cycle = fields.Many2One('farm.animal.female_cycle', 'Cycle',
domain=[
('animal', '=', Eval('animal'))
],
states={
'required': True,
},
depends=['animal'],
)
observation = fields.Text('Observation', required=True)
animal = fields.Many2One('farm.animal', 'Current animal',
readonly=True, states={
'invisible': True,
})
@staticmethod
def default_cycle():
Animal = Pool().get('farm.animal')
active_id = Transaction().context.get('active_id')
if active_id:
active_animal = Animal(active_id)
last_cycle = active_animal.cycles[-1]
return last_cycle.id
@staticmethod
def default_animal():
return Transaction().context.get('active_id')
class ChangeCycleObservation(Wizard):
'Sets the value of the observation field of a cycle'
__name__ = 'female.cycle.observation'
start = StateView('female.cycle.observation.start',
'farm.farm_cycle_observation_start_view', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create', 'result', 'tryton-ok', default=True),
])
result = StateTransition()
def transition_result(self):
cycle = self.start.cycle
cycle.observations = self.start.observation
cycle.save()
return 'end'
class CreateFemaleStart(ModelView):
'Create Female Start'
__name__ = 'farm.create_female.start'
number = fields.Char('Number')
origin = fields.Selection(ANIMAL_ORIGIN, 'Origin', required=True)
arrival_date = fields.Date('Arrival Date', required=True)
birthdate = fields.Date('Birthdate',
states={
'readonly': Eval('origin') == 'raised',
},
depends=['origin'])
initial_location = fields.Many2One('stock.location', 'Current Location',
required=True,
domain=[
('type', '=', 'storage'),
('silo', '=', False),
],
context={
'restrict_by_specie_animal_type': True,
})
specie = fields.Many2One('farm.specie', 'Specie', required=True,
readonly=True)
breed = fields.Many2One('farm.specie.breed', 'Breed', required=True,
domain=[
('specie', '=', Eval('specie')),
],
depends=['specie'])
cycles = fields.One2Many('farm.create_female.line', 'start', 'Cycles',
order=[('insemination_date', 'ASC')])
last_cycle_active = fields.Boolean('Last cycle active',
help='If marked the moves for the last cycle will be created.')
@staticmethod
def default_specie():
context = Transaction().context
if context.get('active_model') == 'ir.ui.menu':
pool = Pool()
Menu = pool.get('ir.ui.menu')
return Menu(context.get('active_id')).specie.id
return context.get('specie')
@staticmethod
def default_origin():
return 'raised'
@fields.depends('origin', 'arrival_date')
def on_change_with_birthdate(self):
if self.origin == 'raised':
return self.arrival_date
return None
class CreateFemaleLine(ModelView):
'Create Female Line'
__name__ = 'farm.create_female.line'
start = fields.Many2One('farm.create_female.start', 'Start', required=True)
insemination_date = fields.Date('Insemination Date', required=True)
second_insemination_date = fields.Date('Second Insemination Date')
third_insemination_date = fields.Date('Third Insemination Date')
abort = fields.Boolean('Aborted?')
abort_date = fields.Date('Abort Date', states={
'invisible': ~Eval('abort', False),
}, depends=['abort'])
farrowing_date = fields.Date('Farrowing Date', states={
'required': Bool(Eval('weaning_date')),
'invisible': Bool(Eval('abort')),
}, depends=['weaning_date', 'abort'])
live = fields.Integer('Live', states={
'required': Bool(Eval('farrowing_date')),
'invisible': Bool(Eval('abort')),
}, depends=['farrowing_date', 'abort'])
stillborn = fields.Integer('Stillborn', states={
'invisible': Bool(Eval('abort')),
}, depends=['abort'])
mummified = fields.Integer('Mummified', states={
'invisible': Bool(Eval('abort')),
}, depends=['abort'])
fostered = fields.Integer('Fostered', states={
'invisible': Bool(Eval('abort')),
}, depends=['abort'])
to_weaning_quantity = fields.Function(
fields.Integer('To Weaning Quantity'),
'on_change_with_to_weaning_quantity')
weaning_date = fields.Date('Weaning Date', states={
'invisible': (Eval('abort', False) |
(Eval('to_weaning_quantity', 0) == 0)),
}, depends=['abort', 'to_weaning_quantity'])
weaned_quantity = fields.Integer('Weaned Quantity', states={
'required': Bool(Eval('weaning_date')),
'invisible': (Eval('abort', False) |
(Eval('to_weaning_quantity', 0) == 0)),
}, depends=['weaning_date', 'abort', 'to_weaning_quantity'])
@fields.depends('live', 'fostered')
def on_change_with_to_weaning_quantity(self, name=None):
return (self.live or 0) + (self.fostered or 0)
class CreateFemale(Wizard):
'Create Female'
__name__ = 'farm.create_female'
start = StateView('farm.create_female.start',
'farm.farm_create_female_start_view', [
Button('Cancel', 'end', 'tryton-cancel'),
Button('Create', 'result', 'tryton-ok', default=True),
])
result = StateAction('farm.act_farm_animal_female')
def do_result(self, action):
pool = Pool()
Abort = pool.get('farm.abort.event')
Animal = pool.get('farm.animal')
Cycle = pool.get('farm.animal.female_cycle')
Farrowing = pool.get('farm.farrowing.event')
Foster = pool.get('farm.foster.event')
Insemination = pool.get('farm.insemination.event')
Weaning = pool.get('farm.weaning.event')
events_time = datetime.now().time()
if (self.start.birthdate and self.start.birthdate >
self.start.arrival_date):
raise UserError(gettext('farm.birthdate_after_arrival',
birth=self.start.birthdate,
arrival=self.start.arrival_date))
female = Animal()
female.type = 'female'
female.specie = self.start.specie
female.breed = self.start.breed
female.number = self.start.number
female.arrival_date = self.start.arrival_date
female.birthdate = self.start.birthdate
female.origin = self.start.origin
female.initial_location = self.start.initial_location
female.save()
female.cycles = []
farm = female.initial_location.warehouse
for sequence, line in enumerate(self.start.cycles):
for field in ('live', 'stillborn', 'mummified', 'weaned_quantity'):
value = getattr(line, field)
if value and value < 0:
raise UserError(gettext('farm.greather_than_zero',
line=line.insemination_date))
cycle = Cycle()
cycle.sequence = sequence + 1
cycle.animal = female
cycle.ordination_date = datetime.combine(line.insemination_date,
events_time)
insemination_events = []
last_insemination_date = None
for insemination_date in (line.insemination_date,
line.second_insemination_date,
line.third_insemination_date):
if not insemination_date:
continue
if (not last_insemination_date or
last_insemination_date < insemination_date):
last_insemination_date = insemination_date
if insemination_date < self.start.arrival_date:
raise UserError(gettext('farm.insemination_before_arrival',
insemination=insemination_date,
line=line.insemination_date,
arrival=self.start.arrival_date))
if (line.farrowing_date and insemination_date >
line.farrowing_date):
raise UserError(gettext(
'farm.farrowing_before_insemination',
farrowing=line.farrowing_date,
line=line.insemination_date,
insemination=insemination_date))
if line.abort_date and insemination_date > line.abort_date:
raise UserError(gettext('farm.abort_before_insemination',
abort=line.abort_date,
line=line.insemination_date,
insemination=insemination_date))
insemination = Insemination()
insemination.imported = True
insemination.animal = female
insemination.animal_type = 'female'
insemination.farm = farm
insemination.specie = self.start.specie
insemination.timestamp = datetime.combine(insemination_date,
events_time)
insemination.state = 'validated'
insemination_events.append(insemination)
cycle.insemination_events = insemination_events
if line.abort:
abort = Abort()
abort.female_cycle = cycle
abort.imported = True
abort.animal = female
abort.animal_type = 'female'
abort.farm = farm
abort.specie = self.start.specie
abort.timestamp = datetime.combine(line.abort_date
if line.abort_date else last_insemination_date,
events_time)
abort.state = 'validated'
abort.save()
cycle.abort_event = abort
elif line.farrowing_date:
cycle.save()
farrowing = Farrowing()
farrowing.female_cycle = cycle
farrowing.imported = True
farrowing.animal = female
farrowing.animal_type = 'female'
farrowing.farm = farm
farrowing.specie = self.start.specie
farrowing.timestamp = datetime.combine(line.farrowing_date,
events_time)
farrowing.live = line.live
farrowing.stillborn = line.stillborn
farrowing.mummified = line.mummified
farrowing.state = 'validated'
cycle.farrowing_event = farrowing
if line.fostered:
if line.fostered < 0 and abs(line.fostered) > line.live:
raise UserError(gettext('farm.more_fostered_than_live',
line=line.insemination_date))
foster = Foster()
foster.imported = True
foster.female_cycle = cycle
foster.animal = female
foster.animal_type = 'female'
foster.farm = farm
foster.specie = self.start.specie
foster.timestamp = datetime.combine(line.farrowing_date,
events_time)
foster.quantity = line.fostered
foster.state = 'validated'
cycle.foster_events = [foster]
if line.weaning_date:
if line.weaning_date < line.farrowing_date:
raise UserError(gettext('farm.weaning_before_farrowing',
weaning=line.weaning_date,
line=line.insemination_date,
farrowing=line.farrowing_date))
if line.weaned_quantity > line.to_weaning_quantity:
raise UserError(gettext('farm.more_weaned_than_live',
line=line.insemination_date))
weaning = Weaning()
weaning.imported = True
weaning.female_cycle = cycle
weaning.animal = female
weaning.animal_type = 'female'
weaning.farm = farm
weaning.specie = self.start.specie
weaning.timestamp = datetime.combine(line.weaning_date,
events_time)
weaning.quantity = line.weaned_quantity
weaning.female_to_location = female.initial_location
weaning.weaned_to_location = female.initial_location
weaning.state = 'validated'
cycle.weaning_event = weaning
elif (not self.start.last_cycle_active or
line != self.start.cycles[-1]):
if (line.live + (line.fostered or 0) -
(line.stillborn or 0) -
(line.mummified or 0) > 0):
raise UserError(gettext('farm.missing_weaning',
line=line.insemination_date))
cycle.save()
cycle.update_state(None)
female = Animal(female.id)
female.update_current_cycle()
if self.start.last_cycle_active:
cycle = female.current_cycle
if cycle:
if cycle.farrowing_event:
Farrowing.write([cycle.farrowing_event], {
'state': 'draft',
})
Farrowing.validate_event([cycle.farrowing_event])
Farrowing.write([cycle.farrowing_event], {
'imported': False,
})
if cycle.foster_events:
Foster.write(list(cycle.foster_events), {
'state': 'draft',
})
Foster.validate_event(cycle.foster_events)
Foster.write(list(cycle.foster_events), {
'imported': False,
})
if cycle.weaning_event:
Weaning.write([cycle.weaning_event], {
'state': 'draft',
})
Weaning.validate_event([cycle.weaning_event])
Weaning.write([cycle.weaning_event], {
'imported': False,
})
action['views'].reverse()
return action, {'res_id': [female.id]}