2013-08-06 09:52:20 +02:00
|
|
|
#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
|
|
|
|
import logging
|
|
|
|
|
|
|
|
from trytond.model import ModelView, ModelSQL, fields
|
|
|
|
from trytond.pyson import Equal, Eval, Greater, Id, Not
|
|
|
|
from trytond.transaction import Transaction
|
|
|
|
from trytond.pool import Pool, PoolMeta
|
|
|
|
|
|
|
|
__all__ = ['Tag', 'Animal', 'AnimalTag', 'AnimalWeight', 'Male', 'Female',
|
|
|
|
'FemaleCycle']
|
|
|
|
__metaclass__ = PoolMeta
|
|
|
|
|
|
|
|
_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']
|
|
|
|
|
|
|
|
|
|
|
|
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__()
|
|
|
|
cls._sql_constraints += [
|
|
|
|
('name_uniq', 'UNIQUE (name)',
|
|
|
|
'The Name of the Tag must be unique.'),
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
class AnimalMixin:
|
|
|
|
@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).
|
|
|
|
"""
|
|
|
|
Move = Pool().get('stock.move')
|
|
|
|
|
|
|
|
new_moves = []
|
|
|
|
for record in records:
|
|
|
|
move = record._get_first_move()
|
|
|
|
move.save()
|
|
|
|
new_moves.append(move)
|
|
|
|
|
|
|
|
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:
|
|
|
|
self.raise_user_error('missing_supplier_location',
|
|
|
|
company.party.rec_name)
|
|
|
|
else: # raised
|
|
|
|
from_location = self.initial_location.warehouse.production_location
|
|
|
|
if not from_location:
|
|
|
|
self.raise_user_error('missing_production_location',
|
|
|
|
self.initial_location.warehouse.rec_name)
|
|
|
|
|
|
|
|
move_date = self.arrival_date or date.today()
|
|
|
|
return Move(
|
|
|
|
product=self.lot.product,
|
|
|
|
uom=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,
|
|
|
|
origin=self)
|
|
|
|
|
|
|
|
|
|
|
|
class Animal(ModelSQL, ModelView, AnimalMixin):
|
|
|
|
"Farm Animal"
|
|
|
|
__name__ = 'farm.animal'
|
|
|
|
|
|
|
|
type = fields.Selection([
|
|
|
|
('male', 'Male'),
|
|
|
|
('female', 'Female'),
|
|
|
|
('individual', 'Individual'),
|
|
|
|
], 'Type', required=True, readonly=True, select=True)
|
|
|
|
specie = fields.Many2One('farm.specie', 'Specie', required=True,
|
|
|
|
readonly=True, select=True)
|
|
|
|
breed = fields.Many2One('farm.specie.breed', 'Breed', required=True,
|
|
|
|
domain=[('specie', '=', Eval('specie'))], depends=['specie'])
|
|
|
|
lot = fields.One2One('stock.lot-farm.animal', 'animal', 'lot',
|
|
|
|
string='Lot', required=True, readonly=True, domain=[
|
|
|
|
('animal_type', '=', Eval('type')),
|
|
|
|
], depends=['type'])
|
|
|
|
number = fields.Function(fields.Char('Number'),
|
|
|
|
'get_number', 'set_number')
|
|
|
|
# location is updated in do() of stock.move
|
2014-01-28 09:50:45 +01:00
|
|
|
location = fields.Many2One('stock.location', 'Current Location',
|
|
|
|
readonly=True, domain=[
|
|
|
|
('type', '!=', 'warehouse'),
|
|
|
|
], help='Indicates where the animal currently resides.')
|
2013-08-06 09:52:20 +02:00
|
|
|
farm = fields.Function(fields.Many2One('stock.location', 'Current Farm',
|
|
|
|
on_change_with=['location'], depends=['location']),
|
|
|
|
'on_change_with_farm', searcher='search_farm')
|
|
|
|
origin = fields.Selection([
|
|
|
|
('purchased', 'Purchased'),
|
|
|
|
('raised', 'Raised'),
|
|
|
|
], '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')],
|
|
|
|
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=['weights']),
|
|
|
|
'on_change_with_current_weight')
|
|
|
|
tags = fields.Many2Many('farm.animal-farm.tag', 'animal', 'tag', 'Tags')
|
|
|
|
notes = fields.Text('Notes')
|
|
|
|
active = fields.Boolean('Active')
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
# 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 __setup__(cls):
|
|
|
|
super(Animal, cls).__setup__()
|
|
|
|
cls._error_messages.update({
|
|
|
|
'missing_supplier_location': ('Supplier Location of '
|
|
|
|
'company\'s party "%s" is empty but it is required to '
|
|
|
|
'create the arrival stock move for a new animal.'),
|
|
|
|
'missing_production_location': ('The warehouse location "%s" '
|
|
|
|
'doesn\'t have set production location, but it is '
|
|
|
|
'required to create the arrival stock move for a new '
|
|
|
|
'animal.'),
|
|
|
|
'no_farm_specie_farm_line_available': ('The specified farm '
|
|
|
|
'"%(farm)s" is not configured as farm with '
|
|
|
|
'"%(animal_type)s" for the specie "%(specie)s"'),
|
|
|
|
'no_sequence_in_farm_line': ('The required sequence '
|
|
|
|
'"%(sequence_field)s" is not set in the farm line '
|
|
|
|
'"%(farm_line)s".'),
|
|
|
|
'invalid_animal_destination': ('The event "%(event)s" is '
|
|
|
|
'trying to move the animal "%(animal)s" to location '
|
|
|
|
'"%(location)s", but the location\'s warehouse is not '
|
|
|
|
'configured as a farm for this kind of animals.'),
|
|
|
|
'no_product_in_specie': ('The required product '
|
|
|
|
'"%(product_field)s" is not set in the farm "%(farm)s".'),
|
|
|
|
})
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def default_specie():
|
|
|
|
return Transaction().context.get('specie')
|
|
|
|
|
|
|
|
@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):
|
|
|
|
name = self.lot.number
|
|
|
|
if not self.active:
|
|
|
|
name += ' (*)'
|
|
|
|
return name
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def search_rec_name(cls, name, clause):
|
2013-09-27 13:47:11 +02:00
|
|
|
return [('lot.number',) + tuple(clause[1:])]
|
2013-08-06 09:52:20 +02:00
|
|
|
|
|
|
|
def get_number(self, name):
|
|
|
|
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,
|
|
|
|
})
|
|
|
|
|
|
|
|
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:])]
|
|
|
|
|
|
|
|
def on_change_with_current_weight(self, name=None):
|
|
|
|
if self.weights:
|
|
|
|
return self.weights[0].id
|
|
|
|
|
|
|
|
def check_in_location(self, location, timestamp):
|
|
|
|
with Transaction().set_context(
|
|
|
|
locations=[location.id],
|
|
|
|
stock_date_end=timestamp.date()):
|
|
|
|
return self.lot.quantity == 1
|
|
|
|
|
|
|
|
def check_allowed_location(self, location, event_rec_name):
|
|
|
|
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
|
|
|
|
self.raise_user_error('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:
|
|
|
|
logging.getLogger(cls.__name__).debug("Create vals: %s" % vals)
|
|
|
|
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'])
|
|
|
|
if vals.get('lot'):
|
|
|
|
lot = Lot(vals['lot'])
|
|
|
|
Lot.write([lot], cls._get_lot_values(vals, False))
|
|
|
|
else:
|
|
|
|
new_lot, = Lot.create([cls._get_lot_values(vals, True)])
|
|
|
|
vals['lot'] = new_lot.id
|
|
|
|
new_animals = super(Animal, cls).create(vlist)
|
|
|
|
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')
|
|
|
|
Sequence = pool.get('ir.sequence')
|
|
|
|
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:
|
|
|
|
cls.raise_user_error('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:
|
|
|
|
cls.raise_user_error('no_sequence_in_farm_line', {
|
|
|
|
'sequence_field': getattr(FarmLine,
|
|
|
|
sequence_fieldname).string,
|
|
|
|
'farm_line': farm_line.rec_name,
|
|
|
|
})
|
|
|
|
return Sequence.get_id(sequence.id)
|
|
|
|
|
|
|
|
@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
|
|
|
|
"""
|
2014-04-09 11:34:47 +02:00
|
|
|
pool = Pool()
|
|
|
|
Lot = pool.get('stock.lot')
|
|
|
|
Specie = pool.get('farm.specie')
|
|
|
|
|
2013-08-06 09:52:20 +02:00
|
|
|
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:
|
|
|
|
cls.raise_user_error('no_product_in_specie', {
|
|
|
|
'product_field': getattr(Specie, product_fieldname).string,
|
|
|
|
'specie': specie.rec_name,
|
|
|
|
})
|
2014-04-09 11:34:47 +02:00
|
|
|
|
|
|
|
lot_tmp = Lot(product=product)
|
|
|
|
cost_lines = lot_tmp._on_change_product_cost_lines()
|
2013-08-06 09:52:20 +02:00
|
|
|
return {
|
|
|
|
'number': animal_vals['number'],
|
|
|
|
'product': product.id,
|
|
|
|
'animal_type': animal_vals['type'],
|
2014-04-09 11:34:47 +02:00
|
|
|
'cost_lines': [('create', cost_lines.get('add', []))],
|
2013-08-06 09:52:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def delete(cls, animals):
|
|
|
|
pool = Pool()
|
|
|
|
Lot = pool.get('stock.lot')
|
|
|
|
|
|
|
|
lots = [a.lot for a in animals]
|
|
|
|
result = super(Animal, cls).delete(animals)
|
|
|
|
if lots:
|
|
|
|
Lot.delete(lots)
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
class AnimalTag(ModelSQL):
|
|
|
|
'Animal - Tag'
|
|
|
|
__name__ = 'farm.animal-farm.tag'
|
2013-09-12 14:26:09 +02:00
|
|
|
animal = fields.Many2One('farm.animal', 'Animal', ondelete='CASCADE',
|
|
|
|
required=True, select=True)
|
|
|
|
tag = fields.Many2One('farm.tag', 'Tag', ondelete='CASCADE', required=True,
|
|
|
|
select=True)
|
2013-08-06 09:52:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
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=['uom']),
|
|
|
|
'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]
|
|
|
|
operand = operand.replace('%', '')
|
|
|
|
try:
|
|
|
|
operand = Decimal(operand)
|
|
|
|
except:
|
|
|
|
return [('weight', '=', 0)]
|
|
|
|
operator = clause[1]
|
|
|
|
operator = operator.replace('ilike', '=').replace('like', '=')
|
|
|
|
return [('weight', operator, operand)]
|
|
|
|
|
|
|
|
def on_change_with_unit_digits(self, name=None):
|
|
|
|
if self.uom:
|
|
|
|
return self.uom.digits
|
|
|
|
return 2
|
|
|
|
|
|
|
|
|
|
|
|
class Male:
|
|
|
|
__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:
|
|
|
|
__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)
|
|
|
|
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 Event. -1 if there '
|
|
|
|
'isn\'t any Insemination Event.'),
|
|
|
|
'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 Event. -1 if there '
|
|
|
|
'isn\'t any Farrowing Event.'),
|
|
|
|
'get_days_from_farrowing', searcher='search_days_from_farrowing')
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def default_state():
|
|
|
|
'''
|
|
|
|
Specific for Female animals.
|
|
|
|
'''
|
|
|
|
if Transaction().context.get('animal_type') == 'female':
|
|
|
|
return 'prospective'
|
|
|
|
return None
|
|
|
|
|
|
|
|
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].id or None
|
|
|
|
self.current_cycle = current_cycle
|
|
|
|
self.save()
|
|
|
|
return current_cycle
|
|
|
|
|
2014-04-09 11:34:47 +02:00
|
|
|
def get_state(self):
|
2013-08-06 09:52:20 +02:00
|
|
|
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.state == 'unmated':
|
|
|
|
state = 'unmated'
|
|
|
|
else:
|
|
|
|
state = 'mated'
|
|
|
|
return state
|
|
|
|
|
2014-04-09 11:34:47 +02:00
|
|
|
# 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.save()
|
|
|
|
return self.state
|
|
|
|
|
2013-08-06 09:52:20 +02:00
|
|
|
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'),
|
2013-09-12 14:26:09 +02:00
|
|
|
('produced_group', '!=', None),
|
2013-08-06 09:52:20 +02:00
|
|
|
],
|
|
|
|
order=[
|
|
|
|
('timestamp', 'DESC'),
|
|
|
|
], limit=1)
|
|
|
|
if last_farrowing_events:
|
2013-09-12 14:26:09 +02:00
|
|
|
return last_farrowing_events[0].produced_group.id
|
2013-08-06 09:52:20 +02:00
|
|
|
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
|
|
|
|
|
2014-04-09 11:34:47 +02:00
|
|
|
@classmethod
|
|
|
|
def create(cls, vlist):
|
|
|
|
for vals in vlist:
|
|
|
|
if vals.get('type', '') == 'female' and not vals.get('state'):
|
|
|
|
vals['state'] = 'prospective'
|
|
|
|
return super(Female, cls).create(vlist)
|
|
|
|
|
2013-08-06 09:52:20 +02:00
|
|
|
@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'
|
|
|
|
_rec_name = 'sequence'
|
|
|
|
_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([
|
|
|
|
('mated', 'Mated'),
|
|
|
|
('pregnant', 'Pregnant'),
|
|
|
|
('lactating', 'Lactating'),
|
|
|
|
('unmated', 'Unmated'),
|
|
|
|
], 'State', readonly=True, required=True)
|
|
|
|
# Female events fields
|
|
|
|
insemination_events = fields.One2Many('farm.insemination.event',
|
|
|
|
'female_cycle', 'Insemination Events')
|
|
|
|
days_between_weaning_and_insemination = fields.Function(
|
|
|
|
fields.Integer('Unmated Days', help='Number of days between previous '
|
|
|
|
'Weaning Event and first Insemination Event.'),
|
|
|
|
'get_days_between_weaning_and_insemination')
|
|
|
|
diagnosis_events = fields.One2Many('farm.pregnancy_diagnosis.event',
|
|
|
|
'female_cycle', 'Diagnosis Events')
|
|
|
|
pregnant = fields.Function(fields.Boolean('Pregnant',
|
|
|
|
on_change_with=['diagnosis_events', 'abort_event'],
|
|
|
|
depends=['diagnosis_events', 'abort_event'],
|
|
|
|
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 Event', readonly=True, domain=[
|
|
|
|
('animal', '=', Eval('animal')),
|
|
|
|
], depends=['animal'])
|
|
|
|
farrowing_event = fields.One2One(
|
|
|
|
'farm.farrowing.event-farm.animal.female_cycle', 'cycle', 'event',
|
|
|
|
string='Farrowing Event', 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',
|
|
|
|
'Foster Events')
|
|
|
|
fostered = fields.Function(fields.Integer('Fostered',
|
|
|
|
on_change_with=['foster_events'], depends=['foster_events'],
|
|
|
|
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 Event', 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')
|
|
|
|
|
|
|
|
@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'
|
|
|
|
|
|
|
|
def get_rec_name(self, name):
|
2013-09-27 13:47:11 +02:00
|
|
|
state_labels = [label for (n, label) in FemaleCycle.state.selection
|
|
|
|
if n == self.state]
|
|
|
|
if state_labels:
|
|
|
|
return "%s (%s)" % (self.sequence, state_labels[0])
|
|
|
|
return "%s" % self.sequence
|
2013-08-06 09:52:20 +02:00
|
|
|
|
|
|
|
# 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),
|
|
|
|
('ordination_date', '<', self.ordination_date)
|
|
|
|
],
|
|
|
|
order=[
|
|
|
|
('sequence', 'DESC'),
|
|
|
|
('ordination_date', 'DESC'),
|
|
|
|
], limit=1)
|
|
|
|
if not previous_cycles or not previous_cycles[0].weaning_event:
|
|
|
|
return None
|
|
|
|
weaning_date = previous_cycles[0].weaning_event.timestamp.date()
|
|
|
|
insemination_date = self.insemination_events[0].timestamp.date()
|
|
|
|
return (insemination_date - weaning_date).days
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
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):
|
|
|
|
return self.live + self.fostered + self.weaned
|
|
|
|
|
2013-09-27 13:47:11 +02:00
|
|
|
def get_lactating_days(self, name):
|
2013-08-06 09:52:20 +02:00
|
|
|
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)
|