1561 lines
59 KiB
Python
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]}
|