diff --git a/__init__.py b/__init__.py index c8d1b24..f107064 100644 --- a/__init__.py +++ b/__init__.py @@ -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, diff --git a/animal.py b/animal.py index 7fc866a..4940868 100644 --- a/animal.py +++ b/animal.py @@ -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') diff --git a/events/__init__.py b/events/__init__.py index ce48ffa..12eba14 100644 --- a/events/__init__.py +++ b/events/__init__.py @@ -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'] diff --git a/events/farrowing_event.py b/events/farrowing_event.py index 019189a..7607bc2 100644 --- a/events/farrowing_event.py +++ b/events/farrowing_event.py @@ -17,6 +17,7 @@ _INVISIBLE_NOT_GROUP = { } + class FarrowingProblem(ModelSQL, ModelView): '''Farrowing Event Problem''' __name__ = 'farm.farrowing.problem' diff --git a/events/reclassification_event.py b/events/reclassification_event.py new file mode 100644 index 0000000..284849f --- /dev/null +++ b/events/reclassification_event.py @@ -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) diff --git a/events/reclassification_event.xml b/events/reclassification_event.xml new file mode 100644 index 0000000..b5a90ee --- /dev/null +++ b/events/reclassification_event.xml @@ -0,0 +1,116 @@ + + + + + + + farm.reclassification.event + form + farm_reclassification_event_form + + + + farm.reclassification.event + tree + farm_reclassification_event_list + + + + Draft + draft + + + + + + + + + + + + + + + + + + + + + + + + + Validate + validate_event + + Are you sure to validate this event? + + + + + + + + + + + + + + + + + + + + + + + + Reclassification + farm.reclassification.event + + + + + + + + + + + + + Draft + + + + + + + All + + + + + + + + + + + + + + + + + + + + + + diff --git a/events/weaning_event.py b/events/weaning_event.py index a2d4f91..b5e16f4 100644 --- a/events/weaning_event.py +++ b/events/weaning_event.py @@ -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 diff --git a/message.xml b/message.xml index c5dafd3..aef7705 100644 --- a/message.xml +++ b/message.xml @@ -301,5 +301,12 @@ this repository contains the full copyright notices and license terms. --> In Transformation Events, the quantity must be 1 for Animals (not Groups). + + + In reclassification event "%(event)s", the product "%(product)s" is already set to the animal + + + Reclassification Event "%(event)s" already has the related stock moves: IN: "%(in_move)s", OUT: "%(out_move)s" + diff --git a/specie.py b/specie.py index c122981..743be55 100644 --- a/specie.py +++ b/specie.py @@ -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) diff --git a/specie_menu_template.xml b/specie_menu_template.xml index c6ddab9..5d31625 100644 --- a/specie_menu_template.xml +++ b/specie_menu_template.xml @@ -88,6 +88,10 @@ + +