046624 | Add Reclassification event

This commit is contained in:
jared-nan 2021-09-29 14:01:14 +02:00 committed by Jared Esparza
parent 4e13165de3
commit a19eb45557
16 changed files with 464 additions and 18 deletions

View File

@ -20,6 +20,7 @@ def register():
specie.UIMenu,
specie.ActionActWindow,
specie.ActionWizard,
specie.SpecieProduct,
animal.Tag,
events.removal_event.RemovalType,
events.removal_event.RemovalReason,
@ -39,7 +40,6 @@ def register():
animal_group.AnimalGroupWeight,
stock.Location,
stock.LocationSiloLocation,
stock.LotAnimal,
stock.LotAnimalGroup,
stock.Lot,
stock.LotCostLine,
@ -72,6 +72,7 @@ def register():
events.weaning_event.WeaningEvent,
events.weaning_event.WeaningEventAnimal,
events.weaning_event.WeaningEventFemaleCycle,
events.reclassification_event.ReclassficationEvent,
stock.Move,
production.BOM,
quality.QualityTest,

View File

@ -10,6 +10,8 @@ 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 trytond import backend
from sql import Table
_STATES_MALE_FIELD = {
'invisible': Not(Equal(Eval('type'), 'male')),
@ -142,8 +144,8 @@ class Animal(ModelSQL, ModelView, AnimalMixin):
})
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=[
lot = fields.Many2One('stock.lot', 'Lot',
readonly=True, domain=[
('animal_type', '=', Eval('type')),
], depends=['type'])
number = fields.Function(fields.Char('Number'),
@ -211,11 +213,34 @@ class Animal(ModelSQL, ModelView, AnimalMixin):
], '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):
TableHandler = backend.get('TableHandler')
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 TableHandler.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')
@ -388,13 +413,19 @@ class Animal(ModelSQL, ModelView, AnimalMixin):
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)])
vals['lot'] = new_lot.id
new_animals = super(Animal, cls).create(vlist)
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
@ -455,6 +486,7 @@ class Animal(ModelSQL, ModelView, AnimalMixin):
'number': animal_vals['number'],
'product': product.id,
'animal_type': animal_vals['type'],
'animal': animal_vals['id']
}
if Transaction().context.get('create_cost_lines', True):
cost_lines = lot_tmp._on_change_product_cost_lines().get('add')

View File

@ -15,6 +15,7 @@ from . import abort_event
from . import farrowing_event
from . import foster_event
from . import weaning_event
from . import reclassification_event
from . import event_order
@ -22,4 +23,4 @@ __all__ = ['abstract_event', 'move_event', 'feed_event', 'feed_inventory',
'medication_event', 'transformation_event', 'removal_event',
'semen_extraction_event', 'insemination_event',
'pregnancy_diagnosis_event', 'abort_event', 'farrowing_event',
'foster_event', 'weaning_event', 'event_order']
'foster_event', 'weaning_event', 'reclassification_event', 'event_order']

View File

@ -17,6 +17,7 @@ _INVISIBLE_NOT_GROUP = {
}
class FarrowingProblem(ModelSQL, ModelView):
'''Farrowing Event Problem'''
__name__ = 'farm.farrowing.problem'

View File

@ -0,0 +1,198 @@
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from trytond.exceptions import UserError
from trytond.model import fields, ModelView, Workflow
from trytond.pyson import Equal, Eval, If, Not
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.i18n import gettext
from .abstract_event import AbstractEvent, _STATES_VALIDATED_ADMIN, \
_DEPENDS_VALIDATED_ADMIN
class ReclassficationEvent(AbstractEvent):
'''Farm Reclassification Event'''
__name__ = 'farm.reclassification.event'
_table = 'farm_reclassification_event'
reclassification_product = fields.Many2One(
'product.product', "Reclassification Product", required=True,
domain=[('id', 'in', Eval('valid_products'))],
states={
'readonly': Not(Equal(Eval('state'), 'draft')),
}, depends=['animal_type', 'state', 'valid_products'])
valid_products = fields.Function(fields.Many2Many(
'product.product', None, None, 'Valid Products'),
'on_change_with_valid_products')
in_move = fields.Many2One(
'stock.move', 'Input Stock Move', readonly=True,
states=_STATES_VALIDATED_ADMIN, depends=_DEPENDS_VALIDATED_ADMIN)
out_move = fields.Many2One(
'stock.move', 'Output Stock Move',
readonly=True, states=_STATES_VALIDATED_ADMIN,
depends=_DEPENDS_VALIDATED_ADMIN)
to_location = fields.Many2One('stock.location', 'Destination',
required=True, states={
'readonly': Not(Equal(Eval('state'), 'draft')),
}, domain=[
('type', '=', 'storage'),
('silo', '=', False),
], depends=['state'])
@classmethod
def __setup__(cls):
super().__setup__()
cls.animal.domain += [
('farm', '=', Eval('farm')),
('location.type', '=', 'storage'),
('type', '=', 'individual'),
]
if 'farm' not in cls.animal.depends:
cls.animal.depends.append('farm')
cls._buttons.update({
'draft': {
'invisible': True,
},
})
@fields.depends('animal')
def on_change_with_valid_products(self, name=None):
if self.animal and self.animal.specie:
return [p.id for p in self.animal.specie.reclassification_products]
return []
@classmethod
@ModelView.button
@Workflow.transition('validated')
def validate_event(cls, events):
"""
Create the input and output stock moves.
"""
pool = Pool()
Move = pool.get('stock.move')
for reclass_event in events:
if reclass_event.in_move and reclass_event.out_move:
raise UserError(gettext(
'farm.related_stock_moves',
event=reclass_event.id,
in_move=reclass_event.in_move.id,
out_move=reclass_event.out_move.id
))
if (reclass_event.animal.lot.product ==
reclass_event.reclassification_product):
raise UserError(gettext(
'farm.invalid_reclassification_product',
event=reclass_event.id,
product=reclass_event.reclassification_product
))
new_in_move = reclass_event._get_event_input_move()
new_in_move.save()
Move.assign([new_in_move])
Move.do([new_in_move])
new_out_move = reclass_event._get_event_output_move()
new_out_move.save()
Move.assign([new_out_move])
Move.do([new_out_move])
reclass_event.in_move = new_in_move
reclass_event.out_move = new_out_move
reclass_event.save()
@fields.depends('animal', 'valid_products', 'to_location')
def on_change_animal(self):
super().on_change_animal()
if not self.animal:
return
self.to_location = self.animal.location
def _get_new_lot_values(self):
"""
Prepare values to create the new stock.lot for the reclassificated
animal. It returns a dictionary with values to create stock.lot
"""
pool = Pool()
Lot = pool.get('stock.lot')
if not self.animal:
return {}
product = self.reclassification_product
lot_tmp = Lot(product=product)
# TODO Improve the manage of animal/lot number, currently using
# the animal number to create the new lot
res = {
'number': self.animal.number,
'product': product.id,
'animal_type': self.animal.type,
'animal': self.animal,
}
if Transaction().context.get('create_cost_lines', True):
cost_lines = lot_tmp._on_change_product_cost_lines().get('add')
if cost_lines:
res['cost_lines'] = [('create', [cl[1] for cl in cost_lines])]
return res
def _get_event_input_move(self):
pool = Pool()
Move = pool.get('stock.move')
context = Transaction().context
if self.animal_type == 'group':
lot = self.animal_group.lot
else:
lot = self.animal.lot
production_location = self.farm.production_location
return Move(
product=lot.product,
uom=lot.product.default_uom,
quantity=1,
from_location=self.animal.location,
to_location=production_location,
planned_date=self.timestamp.date(),
effective_date=self.timestamp.date(),
company=context.get('company'),
lot=lot,
origin=self,
)
def _get_event_output_move(self):
pool = Pool()
Move = pool.get('stock.move')
Lot = pool.get('stock.lot')
context = Transaction().context
lots = Lot.create([self._get_new_lot_values()])
if lots:
lot, = lots
lot.save()
self.animal.lot = lot
self.animal.save()
production_location = self.farm.production_location
return Move(
product=lot.product,
uom=lot.product.default_uom,
quantity=1,
from_location=production_location,
to_location=self.to_location,
planned_date=self.timestamp.date(),
effective_date=self.timestamp.date(),
company=context.get('company'),
lot=lot,
unit_price=lot.product.cost_price,
origin=self,
)
@classmethod
def copy(cls, records, default=None):
if default is None:
default = {}
else:
default = default.copy()
default.update({
'reclassification_product': None,
'to_location': None,
'in_move': None,
'out_move': None,
})
return super().copy(records, default=default)

View File

@ -0,0 +1,116 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<tryton>
<data>
<!-- farm.reclassification.event -->
<record model="ir.ui.view" id="farm_reclassification_event_form_view">
<field name="model">farm.reclassification.event</field>
<field name="type">form</field>
<field name="name">farm_reclassification_event_form</field>
</record>
<record model="ir.ui.view" id="farm_reclassification_event_list_view">
<field name="model">farm.reclassification.event</field>
<field name="type">tree</field>
<field name="name">farm_reclassification_event_list</field>
</record>
<record model="ir.model.button" id="draft_reclassification_event_button">
<field name="string">Draft</field>
<field name="name">draft</field>
<field name="model" search="[('model', '=', 'farm.reclassification.event')]"/>
</record>
<record model="ir.model.button-res.group" id="draft_reclassification_event_button_group_farm_admin">
<field name="button" ref="draft_reclassification_event_button"/>
<field name="group" ref="group_farm_admin"/>
</record>
<record model="ir.model.button-res.group" id="draft_reclassification_event_button_group_farm_males">
<field name="button" ref="draft_reclassification_event_button"/>
<field name="group" ref="group_farm_males"/>
</record>
<record model="ir.model.button-res.group" id="draft_reclassification_event_button_group_farm_females">
<field name="button" ref="draft_reclassification_event_button"/>
<field name="group" ref="group_farm_females"/>
</record>
<record model="ir.model.button-res.group" id="draft_reclassification_event_button_group_farm_individuals">
<field name="button" ref="draft_reclassification_event_button"/>
<field name="group" ref="group_farm_individuals"/>
</record>
<record model="ir.model.button-res.group" id="draft_reclassification_event_button_group_farm_groups">
<field name="button" ref="draft_reclassification_event_button"/>
<field name="group" ref="group_farm_groups"/>
</record>
<record model="ir.model.button" id="validate_reclassification_event_button">
<field name="string">Validate</field>
<field name="name">validate_event</field>
<field name="model" search="[('model', '=', 'farm.reclassification.event')]"/>
<field name="confirm">Are you sure to validate this event?</field>
</record>
<record model="ir.model.button-res.group" id="validate_reclassification_event_button_group_farm_admin">
<field name="button" ref="validate_reclassification_event_button"/>
<field name="group" ref="group_farm_admin"/>
</record>
<record model="ir.model.button-res.group" id="validate_reclassification_event_button_group_farm_males">
<field name="button" ref="validate_reclassification_event_button"/>
<field name="group" ref="group_farm_males"/>
</record>
<record model="ir.model.button-res.group" id="validate_reclassification_event_button_group_farm_females">
<field name="button" ref="validate_reclassification_event_button"/>
<field name="group" ref="group_farm_females"/>
</record>
<record model="ir.model.button-res.group" id="validate_reclassification_event_button_group_farm_individuals">
<field name="button" ref="validate_reclassification_event_button"/>
<field name="group" ref="group_farm_individuals"/>
</record>
<record model="ir.model.button-res.group" id="validate_reclassification_event_button_group_farm_groups">
<field name="button" ref="validate_reclassification_event_button"/>
<field name="group" ref="group_farm_groups"/>
</record>
<record model="ir.action.act_window" id="act_farm_reclassification_event">
<field name="name">Reclassification</field>
<field name="res_model">farm.reclassification.event</field>
</record>
<record model="ir.action.act_window.view" id="act_farm_reclassification_event_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="farm_reclassification_event_list_view"/>
<field name="act_window" ref="act_farm_reclassification_event"/>
</record>
<record model="ir.action.act_window.view" id="act_farm_reclassification_event_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="farm_reclassification_event_form_view"/>
<field name="act_window" ref="act_farm_reclassification_event"/>
</record>
<record model="ir.action.act_window.domain" id="act_farm_reclassification_event_domain_draft">
<field name="name">Draft</field>
<field name="sequence" eval="10"/>
<field name="domain" eval="[('state', '=', 'draft')]" pyson="1"/>
<field name="count" eval="True"/>
<field name="act_window" ref="act_farm_reclassification_event"/>
</record>
<record model="ir.action.act_window.domain" id="act_farm_reclassification_event_domain_all">
<field name="name">All</field>
<field name="sequence" eval="20"/>
<field name="act_window" ref="act_farm_reclassification_event"/>
</record>
<!-- Permissions -->
<record model="ir.model.access" id="access_farm_reclassification_event">
<field name="model" search="[('model', '=', 'farm.reclassification.event')]"/>
<field name="perm_read" eval="False"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.model.access" id="access_farm_reclassification_event_farm">
<field name="model" search="[('model', '=', 'farm.reclassification.event')]"/>
<field name="group" ref="group_farm"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_delete" eval="True"/>
</record>
</data>
</tryton>

View File

@ -26,6 +26,7 @@ __all__ = ['WeaningEvent', 'WeaningEventFemaleCycle']
_INVISIBLE_NOT_GROUP = {
'invisible': ~Equal(Eval('produced_animal_type'), 'group')
}
_REQUIRED_IF_GROUP = {'required': Equal(Eval('produced_animal_type'), 'group')}
@ -320,6 +321,7 @@ class WeaningEvent(AbstractEvent, ImportedEventMixin):
if weaning_event.produced_animal_type == 'individual':
to_save = []
for animal in weaning_event.farrowing_animals:
animalMove = AnimalMove()
animalMove.event = weaning_event

View File

@ -301,5 +301,12 @@ this repository contains the full copyright notices and license terms. -->
<field name="text">In Transformation Events, the quantity must be 1 for Animals (not Groups).</field>
</record>
<!-- events/reclassification_event.py -->
<record model="ir.message" id="invalid_reclassification_product">
<field name="text">In reclassification event "%(event)s", the product "%(product)s" is already set to the animal</field>
</record>
<record model="ir.message" id="related_stock_moves">
<field name="text">Reclassification Event "%(event)s" already has the related stock moves: IN: "%(in_move)s", OUT: "%(out_move)s"</field>
</record>
</data>
</tryton>

View File

@ -97,6 +97,9 @@ class Specie(ModelSQL, ModelView):
('individual', 'Individual'),
('group', 'Group'),
], 'Produced Animal Type')
reclassification_products = fields.Many2Many(
'farm.specie-product.product', 'specie', 'product',
'Reclassification Products')
@classmethod
def __setup__(cls):
@ -403,6 +406,10 @@ class Specie(ModelSQL, ModelView):
'farm.weaning.event': Menu(ModelData.get_id(MODULE_NAME,
'menu_farm_weaning_event')),
},
'individual': {
'farm.reclassification.event': Menu(ModelData.get_id(MODULE_NAME,
'menu_farm_reclassification_event')),
}
}
def _duplicate_menu(self, original_menu, parent_menu, sequence,
@ -644,3 +651,10 @@ class ActionActWindow(metaclass=PoolMeta):
class ActionWizard(metaclass=PoolMeta):
__name__ = 'ir.action.wizard'
specie = fields.Many2One('farm.specie', 'Specie', ondelete='CASCADE')
class SpecieProduct(ModelSQL):
'Specie - Product'
__name__ = 'farm.specie-product.product'
specie = fields.Many2One('farm.specie', 'Specie', ondelete='CASCADE',
required=True)
product = fields.Many2One('product.product', 'Product', required=True)

View File

@ -88,6 +88,10 @@
<field name="group" ref="group_farm_individuals"/>
</record>
<menuitem id="menu_farm_reclassification_event"
action="act_farm_reclassification_event"
parent="menu_farm_animal_individuals" sequence="20"/>
<!-- Groups -->
<menuitem id="menu_farm_animal_groups"
action="act_farm_animal_group"

View File

@ -8,6 +8,8 @@ from trytond.model import ModelView, ModelSQL, fields, Workflow
from trytond.pyson import Equal, Eval, Not
from trytond.pool import Pool, PoolMeta
from trytond.transaction import Transaction
from trytond import backend
from sql import Table
class Lot(metaclass=PoolMeta):
@ -20,8 +22,7 @@ class Lot(metaclass=PoolMeta):
('individual', 'Individual'),
('group', 'Group'),
], 'Animal Type', readonly=True)
animal = fields.One2One('stock.lot-farm.animal', 'lot', 'animal',
string='Animal', readonly=True,
animal = fields.Many2One('farm.animal', 'Animal', readonly=True,
states={'invisible': Equal(Eval('animal_type'), 'group')},
depends=['animal_type'])
animal_group = fields.One2One('stock.lot-farm.animal.group', 'lot',
@ -42,6 +43,27 @@ class Lot(metaclass=PoolMeta):
# Consider making configurable per specie if that constraint should
# apply to 'group' too but with more than one unit.
@classmethod
def __register__(cls, module_name):
TableHandler = backend.get('TableHandler')
table = cls.__table_handler__(module_name)
sql_table = cls.__table__()
update_animal = False
if not table.column_exist('animal'):
update_animal = True
super().__register__(module_name)
table = cls.__table_handler__(module_name)
if update_animal:
sql_table_animal_lot = 'stock_lot-farm_animal'
if TableHandler.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.animal],
values=[animal_id], where=sql_table.id == lot_id))
@staticmethod
def default_animal_type():
return ''
@ -132,16 +154,6 @@ class Lot(metaclass=PoolMeta):
return res
class LotAnimal(ModelSQL):
"Lot - Animal"
__name__ = 'stock.lot-farm.animal'
lot = fields.Many2One('stock.lot', 'Lot', required=True,
ondelete='RESTRICT', select=True)
animal = fields.Many2One('farm.animal', 'Animal', required=True,
ondelete='RESTRICT', select=True)
class LotAnimalGroup(ModelSQL):
"Lot - Animal Group"
__name__ = 'stock.lot-farm.animal.group'
@ -376,6 +388,7 @@ class Move(metaclass=PoolMeta):
'farm.farrowing.event',
'farm.foster.event',
'farm.weaning.event',
'farm.reclassification.event'
]
return models
@ -404,5 +417,6 @@ class LotCostLine(metaclass=PoolMeta):
'farm.transformation.event',
'farm.farrowing.event',
'farm.weaning.event',
'farm.reclassification.event'
]
return models

View File

@ -35,4 +35,5 @@ xml:
events/weaning_event.xml
events/feed_inventory.xml
events/event_order.xml
events/reclassification_event.xml
specie_menu_template.xml

View File

@ -35,6 +35,8 @@
<field name="initial_location" colspan="3"/>
<label name="location"/>
<field name="location" colspan="3"/>
<label name="lot"/>
<field name="lot"/>
<!--
<label name="lot"/>
<field name="lot"/>
@ -76,6 +78,9 @@
<label name="current_weight"/>
<field name="current_weight"/>
<field name="weights" colspan="4"/>
</page>
<page name="lots">
<field name="lots" colspan="4"/>
</page>
<page string="Notes" id="notes">
<field name="notes"/>

View File

@ -0,0 +1,37 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<form>
<group id="header" colspan="4" col="6">
<label name="farm"/>
<field name="farm"/>
<label name="timestamp"/>
<group col="2" id="timestamp">
<field name="timestamp" widget="date"/>
<field name="timestamp" widget="time"/>
</group>
</group>
<group id="animal_or_group" colspan="2" col="2">
<label name="animal"/>
<field name="animal"/>
</group>
<label name="employee"/>
<field name="employee"/>
<label name="reclassification_product"/>
<field name="reclassification_product"/>
<label name="to_location"/>
<field name="to_location"/>
<newline/>
<label name="in_move"/>
<field name="in_move"/>
<label name="out_move"/>
<field name="out_move"/>
<separator name="notes" colspan="4"/>
<field name="notes" colspan="4"/>
<group id="state_and_buttons" colspan="4" col="12">
<label name="state"/>
<field name="state"/>
<button name="draft"/>
<button name="validate_event"/>
</group>
</form>

View File

@ -0,0 +1,12 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<tree>
<field name="animal"/>
<field name="animal_group"/>
<field name="timestamp" widget="date"/>
<field name="timestamp" widget="time"/>
<field name="employee"/>
<field name="farm"/>
<field name="state"/>
</tree>

View File

@ -42,6 +42,7 @@
<label name="feed_lost_found_location"/>
<field name="feed_lost_found_location"/>
</group>
<field name="reclassification_products" colspan="4"/>
<field name="farm_lines" colspan="4"/>
</page>
<page string="Menus &amp; Actions" id="actions">