animal.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. # -*- coding: utf-8 -*-
  2. # @authors: Alexander Ezquevo <alexander@acysos.com>
  3. # Copyright (C) 2015 Acysos S.L.
  4. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
  5. from openerp import models, fields, api, _
  6. from openerp.exceptions import Warning
  7. from datetime import datetime
  8. ANIMAL_ORIGIN = [('purchased', 'Purchased'),
  9. ('raised', 'Raised'), ]
  10. ANIMAL_TYPE = [('male', 'Male'), ('female', 'Female'),
  11. ('individual', 'Individual'), ]
  12. DFORMAT = "%Y-%m-%d %H:%M:%S"
  13. class Tag(models.Model):
  14. _name = 'farm.tags'
  15. _order = 'name DESC'
  16. name = fields.Char(string='Name', required=True)
  17. animals = fields.Many2many(
  18. comodel_name='farm.animal', inverse_name='tags',
  19. string='Animals')
  20. animal_group = fields.Many2many(
  21. comodel_name='farm.animal.group', inverse_name='tags',
  22. string='Groups')
  23. class Animal(models.Model):
  24. _name = 'farm.animal'
  25. _rec_name = 'number'
  26. type = fields.Selection(selection=ANIMAL_TYPE, string='Type',
  27. default='individual')
  28. specie = fields.Many2one(comodel_name='farm.specie',
  29. string='Specie', required=True, select=True)
  30. breed = fields.Many2one(comodel_name='farm.specie.breed', string='Breed')
  31. tags = fields.Many2many(
  32. comodel_name='farm.tags', inverse_name='animals',
  33. string='Tags')
  34. lot = fields.One2many(comodel_name='stock.lot_farm.animal',
  35. inverse_name='animal', colum1='lot',
  36. string='lot')
  37. number = fields.Char(string='Number',
  38. related='lot.lot.name')
  39. location = fields.Many2one(
  40. comodel_name='stock.location', string='Current Location',
  41. domain=[
  42. ('silo', '=', False),
  43. ], help='Indicates where the animal currently resides.')
  44. farm = fields.Many2one(comodel_name='stock.location',
  45. domain=[('usage', '=', 'view'), ],
  46. string='Current Farm')
  47. origin = fields.Selection(selection=ANIMAL_ORIGIN, string='Origin',
  48. required=True)
  49. arrival_date = fields.Date(string='Arrival Date',
  50. default=fields.Date.today(),
  51. help="The date this animal arrived (if it"
  52. "was purchased) or when it was born.")
  53. initial_location = fields.Many2one(
  54. comodel_name='stock.location', string='Initial Location',
  55. required=True,
  56. domain=[('usage', '=', 'internal'), ('silo', '=', False), ],
  57. help="The Location where the animal was reached or where it was "
  58. "allocated when it was purchased.\nIt is used as historical "
  59. "information and to get Serial Number.")
  60. birthdate = fields.Date(string='Birthdate', default=fields.Date.today())
  61. removal_date = fields.Date(
  62. string='Removal Date', readonly=True,
  63. defaulft=fields.Date.today(),
  64. help='Get information from the corresponding removal event.')
  65. removal_reason = fields.Many2one(comodel_name='farm.removal.reason',
  66. readonly=True)
  67. weight = fields.One2many(comodel_name='farm.animal.weight',
  68. inverse_name='animal', column1='tag', string='Weigth records',)
  69. current_weight = fields.Many2one(comodel_name='farm.animal.weight',
  70. string='Current Weight',
  71. compute='get_current_weight')
  72. notes = fields.Text(string='Notes')
  73. active = fields.Boolean(string='Active', default=True)
  74. consumed_feed = fields.Float(string='Consumed Feed (kg)')
  75. sex = fields.Selection([('male', "Male"),
  76. ('female', "Female"),
  77. ('undetermined', "Undetermined"),
  78. ], string='Sex', required=True)
  79. purpose = fields.Selection([('sale', 'Sale'),
  80. ('replacement', 'Replacement'),
  81. ('unknown', 'Unknown'),
  82. ], string='Purpose', default='unknown')
  83. account = fields.Many2one(comodel_name='account.analytic.account',
  84. string='Analytic Account')
  85. @api.multi
  86. def create_first_move(self, res):
  87. moves_obj = self.env['stock.move']
  88. quant_obj = self.env['stock.quant']
  89. for record in res:
  90. quant = quant_obj.search([(
  91. 'lot_id', '=', record.lot.lot.id)])
  92. if record.origin == 'raised':
  93. if not quant:
  94. raise_location = self.env['stock.location'].search(
  95. [('usage', '=', 'production')])
  96. product_tmpt = record.lot.lot.product_id.product_tmpl_id
  97. new_move = moves_obj.create({
  98. 'origin': res.origin,
  99. 'name': 'raise-' + record.lot.lot.name,
  100. 'create_date': fields.Date.today(),
  101. 'date': record.arrival_date,
  102. 'product_id': record.lot.lot.product_id.id,
  103. 'product_uom_qty': 1,
  104. 'product_uom': product_tmpt.uom_id.id,
  105. 'location_id': raise_location.id,
  106. 'location_dest_id': record.initial_location.id,
  107. 'company_id': record.farm.company_id.id,
  108. })
  109. new_move.action_done()
  110. new_move.quant_ids.lot_id = record.lot.lot.id
  111. else:
  112. raise Warning(
  113. _('this lot is in use, please create new lot'))
  114. elif len(self.lot) > 0:
  115. if not quant:
  116. raise Warning(
  117. _('no product in farms for this lot'))
  118. elif quant.location_id != record.initial_location:
  119. raise Warning(
  120. _('animal location and product location are diferent'))
  121. @api.model
  122. @api.returns('self', lambda value: value.id)
  123. def create(self, vals):
  124. res = super(Animal, self).create(vals)
  125. self.create_first_move(res)
  126. res.location = res.initial_location
  127. analy_ac_obj = self.env['account.analytic.account']
  128. top_account = analy_ac_obj.search([
  129. ('name', '=', res.farm.name)])
  130. if not top_account:
  131. gen_account = analy_ac_obj.search([
  132. ('name', '=', 'General Account')])
  133. if not gen_account:
  134. gen_account = analy_ac_obj.create({'name': 'General Account'})
  135. top_account = analy_ac_obj.create({'name': res.farm.name,
  136. 'parent_id': gen_account.id})
  137. new_account = analy_ac_obj.create({
  138. 'name': 'AA-'+res.type+'-'+res.number,
  139. 'parent_id': top_account.id})
  140. res.account = new_account
  141. if res.type == 'female':
  142. nom_eti = '-unmated'
  143. else:
  144. nom_eti = '-fathers'
  145. tags_obj = self.env['farm.tags']
  146. new_tag = tags_obj.search([
  147. ('name', '=', res.farm.name + nom_eti)])
  148. if len(new_tag) == 0:
  149. new_tag = tags_obj.create({'name': res.farm.name + nom_eti, })
  150. res.tags = [(6, 0, [new_tag.id, ])]
  151. return res
  152. @api.one
  153. def get_current_weight(self):
  154. if self.weight:
  155. self.current_weight = self.weight[0].id
  156. @api.multi
  157. def name_get(self):
  158. result = ''
  159. displayName = []
  160. for animal in self:
  161. if animal.tags:
  162. result = ''
  163. for tag in animal.tags:
  164. result = result + tag.name + '/'
  165. displayName.append(
  166. (animal.id, animal.number + '-' + result))
  167. else:
  168. displayName.append((animal.id, animal.number))
  169. return displayName
  170. class AnimalWeight(models.Model):
  171. _name = 'farm.animal.weight'
  172. _order = 'timestamp DESC'
  173. _rec_name = 'weight'
  174. animal = fields.Many2one(comodel_name='farm.animal', string='Animal',
  175. required=True, ondelete='CASCADE')
  176. timestamp = fields.Datetime(string='Date & Time', required=True,
  177. defaulft=fields.Date.today())
  178. uom = fields.Many2one(comodel_name='product.uom', string='Uom',
  179. requiered=True, ondelete='CASCADE')
  180. weight = fields.Float(string='Weight', digits=(16, 2))
  181. class Male(models.Model):
  182. _inherit = 'farm.animal'
  183. extractions = fields.One2many(comodel_name='farm.semen_extraction.event',
  184. inverse_name='animal',
  185. string='Semen Extractions')
  186. last_extraction = fields.Date(string='Last Extraction', readonly=True,
  187. compute='get_last_extraction')
  188. @api.one
  189. def get_last_extraction(self):
  190. if not self.extractions:
  191. return False
  192. extraction = self.extractions.search(
  193. [('animal', '=', self.id)],
  194. order='timestamp DESC')[0]
  195. self.last_extraction = extraction.timestamp
  196. class Female(models.Model):
  197. _inherit = 'farm.animal'
  198. cycles = fields.One2many(comodel_name='farm.animal.female_cycle',
  199. inverse_name='animal', string='Cycles)')
  200. current_cycle = fields.Many2one(comodel_name='farm.animal.female_cycle',
  201. string='Current Cycle', readonly=True,
  202. compute='get_current_cycle')
  203. state = fields.Selection(selection=[('initial', ''),
  204. ('prospective', 'Prospective'),
  205. ('unmated', 'Unmated'),
  206. ('mated', 'Mated'),
  207. ('removed', 'Removed'), ],
  208. readonly=True, default='prospective',
  209. help='According to NPPC Production and Financial'
  210. 'Standards there are four status for breeding'
  211. 'sows The status change is event driven:arrival'
  212. 'date, entry date mating event and removal event')
  213. first_mating = fields.Date(string='First Mating',
  214. compute='get_first_mating')
  215. days_from_insemination = fields.Integer(
  216. string='Insemination Days',
  217. compute='get_days_from_insemination')
  218. last_produced_group = fields.Many2one(comodel_name='farm.animal.group',
  219. string='Last produced group',
  220. compute='get_last_produced_group')
  221. days_from_farrowing = fields.Integer(string='Unpregnat Days',
  222. compute='get_days_from_farrowing')
  223. def is_lactating(self):
  224. return (self.current_cycle and
  225. self.current_cycle.state == 'lactating' or False)
  226. @api.multi
  227. def get_current_cycle(self):
  228. for res in self:
  229. if len(res.cycles) > 0:
  230. res.current_cycle = res.cycles[-1]
  231. @api.one
  232. def update_state(self):
  233. if self.type != 'female':
  234. self.state = 'initial'
  235. else:
  236. if self.removal_date and self.removal_date <= fields.Date.today():
  237. state = 'removed'
  238. elif (not self.cycles or len(self.cycles) == 1 and
  239. not self.cycles[0].weaning_event and
  240. self.cycles[0].state == 'unmated'):
  241. state = 'prospective'
  242. elif self.current_cycle and self.current_cycle.state == 'unmated':
  243. state = 'unmated'
  244. else:
  245. state = 'mated'
  246. self.state = state
  247. @api.one
  248. def get_first_mating(self):
  249. InseminationEvent = self.env['farm.insemination.event']
  250. if self.type != 'female':
  251. self.first_mating = False
  252. else:
  253. first_inseminations = InseminationEvent.search([
  254. ('animal', '=', self.id),
  255. ], limit=1, order='timestamp ASC')
  256. if not first_inseminations:
  257. self.first_mating = False
  258. else:
  259. first_insemination, = first_inseminations
  260. self.first_mating = first_insemination.timestamp
  261. @api.one
  262. def get_days_from_insemination(self):
  263. InseminationEvent = self.env['farm.insemination.event']
  264. last_valid_insemination = InseminationEvent.search([
  265. ('animal', '=', self.id),
  266. ('state', '=', 'validated'),
  267. ], order='timestamp DESC', limit=1)
  268. if not last_valid_insemination:
  269. self.days_from_insemination = False
  270. else:
  271. val_in = datetime.strptime(
  272. last_valid_insemination[0].timestamp, DFORMAT)
  273. days_from_insemination = (
  274. datetime.today() - val_in).days
  275. self.days_from_insemination = days_from_insemination
  276. @api.one
  277. def get_last_produced_group(self):
  278. FarrowingEvent = self.env['farm.farrowing.event']
  279. last_farrowing_events = FarrowingEvent.search([
  280. ('animal', '=', self.id),
  281. ('state', '=', 'validated'),
  282. ('produced_group', '!=', None),
  283. ], order='timestamp DESC', limit=1)
  284. if last_farrowing_events:
  285. self.last_produced_group = \
  286. last_farrowing_events[0].produced_group.id
  287. else:
  288. self.last_produced_group = False
  289. @api.one
  290. def get_days_from_farrowing(self):
  291. FarrowingEvent = self.env['farm.farrowing.event']
  292. last_valid_farrowing = FarrowingEvent.search([
  293. ('animal', '=', self.id),
  294. ('state', '=', 'validated'),
  295. ], order='timestamp DESC', limit=1)
  296. if not last_valid_farrowing:
  297. self.days_from_farrowing = False
  298. else:
  299. last_val_farrow = datetime.strptime(
  300. last_valid_farrowing[0].timestamp, DFORMAT)
  301. days_from_farrowing = (
  302. datetime.today() - last_val_farrow).days
  303. self.days_from_farrowing = days_from_farrowing
  304. class FemaleCycle(models.Model):
  305. _name = 'farm.animal.female_cycle'
  306. _order = 'ordination_date ASC'
  307. _rec_name = 'sequence'
  308. animal = fields.Many2one(comodel_name='farm.animal', string='Female',
  309. required=True, domain=['type', '=', 'female'])
  310. sequence = fields.Integer(string='Nun. cycle')
  311. ordination_date = fields.Datetime('Date for ordination', requiered=True,
  312. default=fields.Datetime.now())
  313. state = fields.Selection(selection=[
  314. ('mated', 'Mated'), ('pregnat', 'Pregnat'),
  315. ('lactating', 'Lactating'), ('unmated', 'Unmated')],
  316. readonly=True, required=True, default='unmated')
  317. insemination_events = fields.One2many(
  318. comodel_name='farm.insemination.event',
  319. inverse_name='female_cycle', string='Inseminations')
  320. days_between_wearing_and_insemination = fields.Integer(
  321. string='Unmated Days',
  322. compute='get_days_between_weaning_and_insemination')
  323. diagnosis_events = fields.One2many(
  324. comodel_name='farm.pregnancy_diagnosis.event',
  325. inverse_name='female_cycle', string='Diagnosis')
  326. pregnant = fields.Boolean(string='Pregnat',
  327. compute='on_change_whith_pregnant')
  328. abort_event = fields.One2many(comodel_name='farm.abort.event',
  329. inverse_name='female_cycle',
  330. string='Abort')
  331. farrowing_event = fields.One2many(
  332. comodel_name='farm.farrowing.event_female_cycle',
  333. inverse_name='cycle', column1='female_cycle.event', string='Farrowing',
  334. readonly=True)
  335. live = fields.Integer(string='Live', compute='get_live')
  336. dead = fields.Integer(string='Dead', compute='get_dead')
  337. foster_events = fields.One2many(comodel_name='farm.foster.event',
  338. inverse_name='female_cycle',
  339. string='Fosters')
  340. fostered = fields.Integer(string='Fostered',
  341. compute='on_change_with_fostered',
  342. store=True)
  343. weaning_event = fields.One2many(
  344. comodel_name='farm.weaning.event_female_cycle',
  345. inverse_name='cycle', column1='event', readonly=True)
  346. weared = fields.Integer(string='Weared', compute='get_weaned')
  347. removed = fields.Integer(
  348. string='Removed Quantity',
  349. help='Number of removed animals from Produced Group. Diference '
  350. 'between born live and weaned, computing Fostered diference.',
  351. compute='get_removed')
  352. days_between_farrowing_weaning = fields.Integer(
  353. string='Lactating Days',
  354. help='Number of days between Farrowing and Weaning.',
  355. compute='get_lactating_days')
  356. @api.one
  357. def update_state(self, validated_event):
  358. '''
  359. Sorted rules:
  360. - A cycle will be considered 'unmated'
  361. if weaning_event_id != False and weaning_event.state == 'validated'
  362. or if abort_event != False has abort_event.state == 'validated'
  363. or has not any validated event in insemination_event_ids.
  364. - A female will be considered 'lactating'
  365. if farrowing_event_id!=False and farrowing_event.state=='validated'
  366. - A female will be considered 'pregnant' if there are more than one
  367. diagnosis in 'validated' state and the last one has a positive result
  368. - A female will be considered 'mated' if there are any items in
  369. insemination_event_ids with 'validated' state.
  370. '''
  371. def check_event(event_to_check):
  372. return(type(event_to_check) == type(validated_event) and
  373. event_to_check == validated_event or
  374. event_to_check.state == 'validated')
  375. state = 'unmated'
  376. if (self.abort_event and check_event(self.abort_event) or
  377. self.weaning_event and check_event(self.weaning_event.event)):
  378. state = 'unmated'
  379. elif self.farrowing_event and check_event(self.farrowing_event.event):
  380. if self.farrowing_event.event.live > 0:
  381. state = 'lactating'
  382. else:
  383. state = 'unmated'
  384. elif self.pregnant:
  385. state = 'pregnat'
  386. else:
  387. for insemination_event in self.insemination_events:
  388. if check_event(insemination_event):
  389. state = 'mated'
  390. break
  391. self.state = state
  392. self.animal.update_state()
  393. self.state = state
  394. @api.model
  395. @api.returns('self', lambda value: value.id)
  396. def create(self, vals):
  397. res = super(FemaleCycle, self).create(vals)
  398. femaleCycle_obj = self.env['farm.animal.female_cycle']
  399. cycles = femaleCycle_obj.search([
  400. ('animal.id', '=', res.animal.id), ])
  401. secuence = 1
  402. for cycle in cycles:
  403. if len(cycle.farrowing_event) != 0:
  404. secuence += 1
  405. res.sequence = secuence
  406. return res
  407. @api.one
  408. def get_days_between_weaning_and_insemination(self):
  409. if not self.insemination_events:
  410. return False
  411. previous_cycles = self.search([
  412. ('animal', '=', self.animal.id),
  413. ('sequence', '<=', self.sequence),
  414. ('id', '!=', self.id)
  415. ],
  416. order='sequence, ordination_date DESC', limit=1)
  417. if len(previous_cycles) < 1 or (
  418. not previous_cycles[0].weaning_event and
  419. not previous_cycles[0].abort_event):
  420. self.days_between_wearing_and_insemination = 0
  421. else:
  422. previous_date = (
  423. datetime.strptime(
  424. previous_cycles[0].weaning_event.event.timestamp, DFORMAT)
  425. if previous_cycles[0].weaning_event
  426. else datetime.strptime(
  427. previous_cycles[0].abort_event.timestamp, DFORMAT))
  428. insemination_date = \
  429. datetime.strptime(
  430. self.insemination_events[0].timestamp, DFORMAT)
  431. self.days_between_wearing_and_insemination = \
  432. (insemination_date - previous_date).days
  433. @api.one
  434. def get_live(self):
  435. if self.farrowing_event:
  436. self.live = self.farrowing_event[-1].cycle.live
  437. else:
  438. self.live = False
  439. @api.one
  440. def get_dead(self):
  441. if self.farrowing_event:
  442. self.live = self.farrowing_event[-1].cycle.dead
  443. else:
  444. self.live = False
  445. @api.one
  446. def on_change_with_fostered(self):
  447. self.fostered = sum(e.quantity for e in self.foster_events)
  448. @api.one
  449. def get_weaned(self):
  450. self.weared = self.weaning_event and \
  451. self.weaning_event.event.quantity or 0
  452. @api.one
  453. def get_removed(self):
  454. self.removed = self.live + self.fostered - self.weared
  455. @api.one
  456. def get_lactating_days(self):
  457. if not self.farrowing_event or not self.weaning_event:
  458. return None
  459. w_e = datetime.strptime(
  460. self.weaning_event[-1].event.timestamp, DFORMAT)
  461. f_e = datetime.strptime(
  462. self.farrowing_event[-1].event.timestamp, DFORMAT)
  463. self.days_between_farrowing_weaning = (w_e-f_e).days
  464. @api.one
  465. @api.onchange('abort_event', 'diagnosis_events', 'farrowing_event')
  466. def on_change_whith_pregnant(self):
  467. if self.abort_event:
  468. self.pregnant = False
  469. elif not self.diagnosis_events:
  470. self.pregnant = False
  471. elif self.farrowing_event:
  472. self.pregnant = False
  473. else:
  474. self.pregnant = \
  475. self.diagnosis_events[-1].result == 'positive'
  476. @api.one
  477. @api.onchange('pregnant')
  478. def on_change_pregnant(self):
  479. if self.pregnant:
  480. self.state = 'pregnat'
  481. def get_last_produced_group(self):
  482. farrowingEvent_obj = self.env['farm.farrowing.event']
  483. last_farrowing_events = farrowingEvent_obj.search([
  484. ('animal', '=', self),
  485. ('state', '=', 'validated'),
  486. ('produced_group', '!=', None),
  487. ],
  488. order='timestamp DESC', limit=1)
  489. if last_farrowing_events:
  490. return last_farrowing_events[0].produced_group.id
  491. return None