diff --git a/addons/zoo/__init__.py b/addons/zoo/__init__.py index 9a7e03e..fab5aef 100644 --- a/addons/zoo/__init__.py +++ b/addons/zoo/__init__.py @@ -1 +1,3 @@ -from . import models \ No newline at end of file +from . import models +from . import wizard +from . import controllers \ No newline at end of file diff --git a/addons/zoo/__manifest__.py b/addons/zoo/__manifest__.py index 3696fd5..b60f800 100644 --- a/addons/zoo/__manifest__.py +++ b/addons/zoo/__manifest__.py @@ -33,6 +33,11 @@ 'views/zoo_creature_views.xml', 'views/zoo_cage_views.xml', 'views/zoo_health_records.xml', + 'views/zoo_animal_meal_views.xml', 'views/zoo_diet_plans.xml', + 'wizard/toy_add_views.xml', + 'wizard/cage_update_views.xml', + 'wizard/animal_feeding_views.xml', + 'views/zoo_husbandry_task_views.xml' ], } \ No newline at end of file diff --git a/addons/zoo/__pycache__/__init__.cpython-312.pyc b/addons/zoo/__pycache__/__init__.cpython-312.pyc index e93f347..9680bce 100644 Binary files a/addons/zoo/__pycache__/__init__.cpython-312.pyc and b/addons/zoo/__pycache__/__init__.cpython-312.pyc differ diff --git a/addons/zoo/controllers/__init__.py b/addons/zoo/controllers/__init__.py new file mode 100644 index 0000000..deec4a8 --- /dev/null +++ b/addons/zoo/controllers/__init__.py @@ -0,0 +1 @@ +from . import main \ No newline at end of file diff --git a/addons/zoo/controllers/__pycache__/__init__.cpython-312.pyc b/addons/zoo/controllers/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..2c5adc9 Binary files /dev/null and b/addons/zoo/controllers/__pycache__/__init__.cpython-312.pyc differ diff --git a/addons/zoo/controllers/__pycache__/main.cpython-312.pyc b/addons/zoo/controllers/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..812362b Binary files /dev/null and b/addons/zoo/controllers/__pycache__/main.cpython-312.pyc differ diff --git a/addons/zoo/controllers/main.py b/addons/zoo/controllers/main.py new file mode 100644 index 0000000..6ba3f08 --- /dev/null +++ b/addons/zoo/controllers/main.py @@ -0,0 +1,33 @@ +import odoo +from odoo.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT +from odoo.http import request +import datetime +import json + +import logging +_logger = logging.getLogger(__name__) + +def convert_datetime(d): + if d: + return d.strftime(DEFAULT_SERVER_DATETIME_FORMAT) if isinstance(d, datetime.datetime) else d.strftime(DEFAULT_SERVER_DATE_FORMAT) + else: + return False + +class ZooAPI(odoo.http.Controller): + @odoo.http.route('/api/zoo/animal/', type='http', auth='none', cors='*', csrf=False) + def get_animal_by_id(self, id, **kw): + env = request.env + id = int(id) + model = "zoo.animal" + record = env[model].sudo().search([('id', '=', id)]) + if record: + res = { + "name": record.name, + "dob": convert_datetime(d=record.dob), + "gender": record.gender, + "feed_time": convert_datetime(d=record.feed_time), + } + _logger.warning(res) + return request.make_json_response(res, status=200) + else: + return request.make_json_response({}, status=200) \ No newline at end of file diff --git a/addons/zoo/data/product_food_import.csv b/addons/zoo/data/product_food_import.csv new file mode 100644 index 0000000..64bab00 --- /dev/null +++ b/addons/zoo/data/product_food_import.csv @@ -0,0 +1,21 @@ +name,default_code,type,list_price,standard_price,uom_id/id,categ_id/id,description +Beef (Raw),FOOD001,consu,15.50,12.00,uom.product_uom_kgm,product.product_category_all,Fresh raw beef for carnivores +Chicken (Raw),FOOD002,consu,8.75,6.50,uom.product_uom_kgm,product.product_category_all,Fresh raw chicken meat +Fish (Whole),FOOD003,consu,12.00,9.00,uom.product_uom_kgm,product.product_category_all,Fresh whole fish for aquatic animals +Pork (Raw),FOOD004,consu,10.50,8.00,uom.product_uom_kgm,product.product_category_all,Fresh pork meat +Deer Meat,FOOD005,consu,18.00,14.00,uom.product_uom_kgm,product.product_category_all,Premium deer meat for large carnivores +Grass Hay,FOOD006,consu,3.50,2.00,uom.product_uom_kgm,product.product_category_all,Dried grass hay for herbivores +Alfalfa,FOOD007,consu,4.25,2.50,uom.product_uom_kgm,product.product_category_all,High quality alfalfa +Fresh Vegetables,FOOD008,consu,5.00,3.50,uom.product_uom_kgm,product.product_category_all,Mixed fresh vegetables +Fruits (Mixed),FOOD009,consu,6.50,4.50,uom.product_uom_kgm,product.product_category_all,Assorted fresh fruits +Bamboo,FOOD010,consu,7.00,5.00,uom.product_uom_kgm,product.product_category_all,Fresh bamboo shoots and stalks +Fish Pellets,FOOD011,consu,4.50,3.00,uom.product_uom_kgm,product.product_category_all,Specialized fish food pellets +Bird Seed Mix,FOOD012,consu,3.00,2.00,uom.product_uom_kgm,product.product_category_all,Mixed seeds for birds +Insects (Live),FOOD013,consu,12.50,9.00,uom.product_uom_kgm,product.product_category_all,Live insects for insectivores +Rabbit (Whole),FOOD014,consu,9.00,7.00,uom.product_uom_kgm,product.product_category_all,Whole rabbit for medium carnivores +Squid,FOOD015,consu,11.00,8.50,uom.product_uom_kgm,product.product_category_all,Fresh squid for marine animals +Krill,FOOD016,consu,14.00,11.00,uom.product_uom_kgm,product.product_category_all,Frozen krill for aquatic animals +Lettuce,FOOD017,consu,2.50,1.50,uom.product_uom_kgm,product.product_category_all,Fresh lettuce leaves +Carrots,FOOD018,consu,2.00,1.20,uom.product_uom_kgm,product.product_category_all,Fresh carrots +Sweet Potato,FOOD019,consu,3.00,2.00,uom.product_uom_kgm,product.product_category_all,Fresh sweet potatoes +Apples,FOOD020,consu,4.00,3.00,uom.product_uom_kgm,product.product_category_all,Fresh apples diff --git a/addons/zoo/data/zoo_animal_import.csv b/addons/zoo/data/zoo_animal_import.csv new file mode 100644 index 0000000..d1f8908 --- /dev/null +++ b/addons/zoo/data/zoo_animal_import.csv @@ -0,0 +1,31 @@ +name,description,dob,gender,weight,nickname,is_alive,is_purchased,purchase_price,creature_id/name,cage_id/name +Simba,Majestic lion with golden mane,2018-05-15,male,190.5,King,True,True,15000.0,Lion,Savanna Habitat +Nala,Beautiful lioness and pride leader,2019-03-20,female,130.0,Queen,True,True,12000.0,Lion,Savanna Habitat +Shere Khan,Bengal tiger with striking orange stripes,2017-08-10,male,220.0,Khan,True,True,25000.0,Tiger,Rainforest Pavilion +Rajah,Playful tiger cub learning to hunt,2022-11-05,female,45.5,Raja,True,False,0.0,Tiger,Rainforest Pavilion +Dumbo,Gentle giant elephant with big ears,2015-01-12,male,5400.0,Big Ears,True,True,50000.0,Elephant,Safari Plains +Ellie,Young elephant loves to play in water,2020-06-30,female,2800.0,Ellie,True,False,0.0,Elephant,Safari Plains +Melman,Tall giraffe with beautiful spots,2016-07-22,male,1200.0,Mel,True,True,18000.0,Giraffe,Savanna Habitat +Gloria,Graceful giraffe mother,2018-09-15,female,950.0,Glori,True,True,16000.0,Giraffe,Savanna Habitat +Marty,Striped zebra always energetic,2019-04-08,male,350.0,Stripey,True,True,8000.0,Zebra,Safari Plains +Zara,Young zebra with perfect stripes,2021-12-20,female,280.0,Z,True,False,0.0,Zebra,Safari Plains +Skipper,Penguin leader of the colony,2020-02-14,male,5.5,Skip,True,True,3000.0,Penguin,Penguin Cove +Kowalski,Smart penguin loves fish,2020-02-14,male,5.2,Kowl,True,True,3000.0,Penguin,Penguin Cove +Private,Young penguin still learning,2021-08-01,male,4.8,Priv,True,False,0.0,Penguin,Penguin Cove +Rico,Tough penguin protects the group,2020-02-14,male,5.7,Ric,True,True,3000.0,Penguin,Penguin Cove +Aurora,Beautiful white polar bear female,2016-11-30,female,450.0,Rory,True,True,35000.0,Polar Bear,Polar Pavilion +Blizzard,Large male polar bear king of arctic,2015-09-18,male,550.0,Bliz,True,True,40000.0,Polar Bear,Arctic Zone +Flipper,Friendly dolphin loves to jump,2019-05-25,male,180.0,Flip,True,True,20000.0,Dolphin,Aquatic Center +Echo,Dolphin with beautiful voice,2020-07-12,female,165.0,Echi,True,True,18000.0,Dolphin,Tropical Reef +Jaws,Impressive great white shark,2014-03-03,male,900.0,Big J,True,True,45000.0,Shark,Aquatic Center +Marina,Graceful reef shark,2018-10-20,female,220.0,Mari,True,True,15000.0,Shark,Tropical Reef +Freedom,Majestic bald eagle symbol of liberty,2017-06-04,male,6.5,Free,True,True,5000.0,Eagle,Bird Sanctuary +Liberty,Female eagle partner of Freedom,2018-04-15,female,5.8,Libby,True,True,4500.0,Eagle,Bird Sanctuary +Zeus,Powerful golden eagle,2016-01-20,male,7.2,Z-man,True,True,6000.0,Eagle,Bird Sanctuary +Spirit,Young eagle learning to fly,2022-05-10,male,4.5,Spir,True,False,0.0,Eagle,Mountain Ridge +Leo,Strong second alpha lion,2019-08-22,male,185.0,Leo,True,True,14000.0,Lion,Savanna Habitat +Elsa,Brave lioness hunter,2020-10-11,female,125.0,Ice Queen,True,True,11000.0,Lion,Desert Dome +Tony,Young energetic tiger,2021-03-15,male,95.0,T-Rex,True,False,0.0,Tiger,Rainforest Pavilion +Shira,Rare beautiful white tiger,2018-12-25,female,140.0,Snow,True,True,30000.0,Tiger,Primate Paradise +Trunks,Adorable baby elephant,2023-02-28,male,800.0,Trunk,True,False,0.0,Elephant,Safari Plains +Patches,Cute giraffe calf with unique spots,2023-01-05,female,320.0,Patchy,True,False,0.0,Giraffe,Savanna Habitat diff --git a/addons/zoo/data/zoo_cage_import.csv b/addons/zoo/data/zoo_cage_import.csv new file mode 100644 index 0000000..4ecc23e --- /dev/null +++ b/addons/zoo/data/zoo_cage_import.csv @@ -0,0 +1,16 @@ +name,capacity,location,cage_type,area,description,active +Savanna Habitat,10,North Wing,outdoor,500.5,Large outdoor habitat for African animals,True +Rainforest Pavilion,8,East Wing,indoor,350.0,Climate-controlled rainforest environment,True +Arctic Zone,6,South Wing,outdoor,400.0,Cold climate habitat with ice features,True +Aquatic Center,15,West Wing,aquarium,600.0,Large aquarium for marine life,True +Bird Sanctuary,20,Central Area,aviary,450.0,Open-air aviary with netting,True +Desert Dome,5,North Wing,indoor,300.0,Heated desert environment,True +Primate Paradise,12,East Wing,outdoor,380.0,Multi-level jungle gym for primates,True +Reptile House,10,South Wing,indoor,250.0,Temperature-controlled reptile habitat,True +Polar Pavilion,4,West Wing,indoor,320.0,Frozen tundra simulation,True +Safari Plains,25,Central Area,outdoor,800.0,Large plains for grazing animals,True +Tropical Reef,20,East Wing,aquarium,550.0,Coral reef ecosystem,True +Mountain Ridge,8,North Wing,outdoor,420.0,Rocky mountain terrain,True +Nocturnal Gallery,6,South Wing,indoor,180.0,Dark environment for nocturnal animals,True +Penguin Cove,15,West Wing,outdoor,400.0,Icy pool for penguins,True +Butterfly Garden,30,Central Area,aviary,200.0,Glass dome for butterflies,True diff --git a/addons/zoo/data/zoo_creature_import.csv b/addons/zoo/data/zoo_creature_import.csv new file mode 100644 index 0000000..0147577 --- /dev/null +++ b/addons/zoo/data/zoo_creature_import.csv @@ -0,0 +1,11 @@ +name,environment,is_rare +Lion,ground,False +Tiger,forest,True +Elephant,ground,False +Giraffe,ground,False +Zebra,ground,False +Penguin,cool,False +Polar Bear,cool,True +Dolphin,ocean,False +Shark,sea,True +Eagle,sky,False diff --git a/addons/zoo/models/__init__.py b/addons/zoo/models/__init__.py index fcf0e96..09e5407 100644 --- a/addons/zoo/models/__init__.py +++ b/addons/zoo/models/__init__.py @@ -3,4 +3,6 @@ from . import zoo_creature from . import zoo_cage from . import zoo_health_record from . import zoo_diet_plan -from . import zoo_diet_line \ No newline at end of file +from . import zoo_diet_line +from . import zoo_animal_meal +from . import zoo_husbandry_task \ No newline at end of file diff --git a/addons/zoo/models/__pycache__/__init__.cpython-312.pyc b/addons/zoo/models/__pycache__/__init__.cpython-312.pyc index 0802721..cf9c976 100644 Binary files a/addons/zoo/models/__pycache__/__init__.cpython-312.pyc and b/addons/zoo/models/__pycache__/__init__.cpython-312.pyc differ diff --git a/addons/zoo/models/__pycache__/zoo_animal.cpython-312.pyc b/addons/zoo/models/__pycache__/zoo_animal.cpython-312.pyc index 56c0694..53ae767 100644 Binary files a/addons/zoo/models/__pycache__/zoo_animal.cpython-312.pyc and b/addons/zoo/models/__pycache__/zoo_animal.cpython-312.pyc differ diff --git a/addons/zoo/models/__pycache__/zoo_animal_meal.cpython-312.pyc b/addons/zoo/models/__pycache__/zoo_animal_meal.cpython-312.pyc new file mode 100644 index 0000000..6dae947 Binary files /dev/null and b/addons/zoo/models/__pycache__/zoo_animal_meal.cpython-312.pyc differ diff --git a/addons/zoo/models/__pycache__/zoo_cage.cpython-312.pyc b/addons/zoo/models/__pycache__/zoo_cage.cpython-312.pyc index 5d3e1c5..ac56f3b 100644 Binary files a/addons/zoo/models/__pycache__/zoo_cage.cpython-312.pyc and b/addons/zoo/models/__pycache__/zoo_cage.cpython-312.pyc differ diff --git a/addons/zoo/models/__pycache__/zoo_creature.cpython-312.pyc b/addons/zoo/models/__pycache__/zoo_creature.cpython-312.pyc index 63b578d..e54c863 100644 Binary files a/addons/zoo/models/__pycache__/zoo_creature.cpython-312.pyc and b/addons/zoo/models/__pycache__/zoo_creature.cpython-312.pyc differ diff --git a/addons/zoo/models/__pycache__/zoo_diet_plan.cpython-312.pyc b/addons/zoo/models/__pycache__/zoo_diet_plan.cpython-312.pyc index b543a88..c404c2b 100644 Binary files a/addons/zoo/models/__pycache__/zoo_diet_plan.cpython-312.pyc and b/addons/zoo/models/__pycache__/zoo_diet_plan.cpython-312.pyc differ diff --git a/addons/zoo/models/__pycache__/zoo_health_record.cpython-312.pyc b/addons/zoo/models/__pycache__/zoo_health_record.cpython-312.pyc index b6fa6fc..3cc01b4 100644 Binary files a/addons/zoo/models/__pycache__/zoo_health_record.cpython-312.pyc and b/addons/zoo/models/__pycache__/zoo_health_record.cpython-312.pyc differ diff --git a/addons/zoo/models/__pycache__/zoo_husbandry_task.cpython-312.pyc b/addons/zoo/models/__pycache__/zoo_husbandry_task.cpython-312.pyc new file mode 100644 index 0000000..0feec13 Binary files /dev/null and b/addons/zoo/models/__pycache__/zoo_husbandry_task.cpython-312.pyc differ diff --git a/addons/zoo/models/zoo_animal.py b/addons/zoo/models/zoo_animal.py index 6eeed05..5a8ffb4 100644 --- a/addons/zoo/models/zoo_animal.py +++ b/addons/zoo/models/zoo_animal.py @@ -8,47 +8,92 @@ class ZooAnimal(models.Model): _name = "zoo.animal" _description = "Animal in the zoo" - name = fields.Char('Animal Name', required=True) + name = fields.Char('Animal Name', + required=True) + description = fields.Text('Description') - dob = fields.Date('DOB', required=False) + + dob = fields.Date('DOB', + required=False) + gender = fields.Selection([ ('male', 'Male'), ('female', 'Female') - ], string='Gender', default='male', required=True) - feed_time = fields.Datetime('Feed Time', copy=False) - is_alive = fields.Boolean('Is Alive', default=True) - image = fields.Binary("Image", attachment=True, help="Animal Image") + ], + string='Gender', + default='male', + required=True) + + feed_time = fields.Datetime('Feed Time', + copy=False) + + is_alive = fields.Boolean('Is Alive', + default=True) + + image = fields.Binary("Image", + attachment=True, + help="Animal Image") + weight = fields.Float('Weight (kg)') + weight_pound = fields.Float('Weight (pounds)') + introduction = fields.Text('Introduction (EN)') + nickname = fields.Char('Nickname') + introduction_vn = fields.Html('Introduction (VI)') - is_purchased = fields.Boolean('Has Been Purchased', default=False) + + is_purchased = fields.Boolean('Has Been Purchased', + default=False) purchase_price = fields.Float('Purchase Price') - veterinarian_id = fields.Many2one(comodel_name='res.partner', string='Veterinarian') - - # thêm vào cuối: - - age = fields.Integer('Pet Age', compute='_compute_age') - number_of_children = fields.Integer('Number of Children', compute='_compute_number_of_children') - - # thêm các trường quan hệ sau: - mother_id = fields.Many2one(comodel_name='zoo.animal', string='Mother', ondelete='set null') # ondelete: 'set null', 'restrict', 'cascade' - mother_name = fields.Char('Mother Name', related='mother_id.name') - female_children_ids = fields.One2many(comodel_name='zoo.animal', inverse_name='mother_id', string='Female Children') - father_id = fields.Many2one(comodel_name='zoo.animal', string='Father', ondelete='set null') - father_name = fields.Char('Father Name', related='father_id.name') - male_children_ids = fields.One2many(comodel_name='zoo.animal', inverse_name='father_id', string='Male Children') + + veterinarian_id = fields.Many2one(comodel_name='res.partner', + string='Veterinarian') + + age = fields.Integer('Pet Age', + compute='_compute_age') + + number_of_children = fields.Integer('Number of Children', + compute='_compute_number_of_children') + + mother_id = fields.Many2one(comodel_name='zoo.animal', + string='Mother', + ondelete='set null') # ondelete: 'set null', 'restrict', 'cascade' + + mother_name = fields.Char('Mother Name', + related='mother_id.name') + + female_children_ids = fields.One2many(comodel_name='zoo.animal', + inverse_name='mother_id', + string='Female Children') + + father_id = fields.Many2one(comodel_name='zoo.animal', + string='Father', + ondelete='set null') + + father_name = fields.Char('Father Name', + related='father_id.name') + + male_children_ids = fields.One2many(comodel_name='zoo.animal', + inverse_name='father_id', + string='Male Children') - toy_ids = fields.Many2many(comodel_name='product.product', - string="Toys", - relation='animal_product_toy_rel', - column1='col_animal_id', - column2='col_product_id') + toy_ids = fields.Many2many(comodel_name='product.product', + string="Toys", + relation='animal_product_toy_rel', + column1='col_animal_id', + column2='col_product_id') - creature_id = fields.Many2one(comodel_name='zoo.creature', string='Creature') + creature_id = fields.Many2one(comodel_name='zoo.creature', + string='Creature') + cage_id = fields.Many2one(comodel_name='zoo.cage', + string='Cage', + ondelete='set null') + + # --- Các hàm tính toán (Compute Functions) --- @api.depends('dob') def _compute_age(self): now = datetime.datetime.now() @@ -65,17 +110,64 @@ class ZooAnimal(models.Model): record.age = False pass - @api.depends('male_children_ids') - def _compute_number_of_children(self): - for record in self: - record.number_of_children = len(record.male_children_ids) - + # --- Các hàm ràng buộc (Constraints) --- @api.constrains('dob') def _check_dob(self): for record in self: if record.dob and record.dob.year < 1900: raise ValidationError(_("Invalid DOB!")) + @api.depends('male_children_ids') + def _compute_number_of_children(self): + for record in self: + record.number_of_children = len(record.male_children_ids) + + + @api.constrains('father_id', 'mother_id') + def _check_parents(self): + """Validate parent relationships""" + for record in self: + # Kiểm tra cha != mẹ + if record.father_id and record.mother_id: + if record.father_id == record.mother_id: + raise ValidationError(_("Father and Mother cannot be the same animal!")) + + # Kiểm tra cha != record hiện tại + if record.father_id: + if record.father_id.id == record.id: + raise ValidationError(_("An animal cannot be its own father!")) + + # Kiểm tra mẹ != record hiện tại + if record.mother_id: + if record.mother_id.id == record.id: + raise ValidationError(_("An animal cannot be its own mother!")) + + @api.constrains('gender', 'female_children_ids', 'male_children_ids') + def _check_gender_children_consistency(self): + """Validate gender and children lists consistency""" + for record in self: + # Kiểm tra không đồng thời có cả female_children_ids và male_children_ids + if record.female_children_ids and record.male_children_ids: + raise ValidationError(_( + "An animal cannot have both female children list and male children list. " + "Please check the parent assignments." + )) + + # Giới tính đực (male) không được phép có female_children_ids + if record.gender == 'male' and record.female_children_ids: + raise ValidationError(_( + "A male animal cannot have female children in the female_children_ids field. " + "Male animals should only have children in male_children_ids (as father)." + )) + + # Giới tính cái (female) không được phép có male_children_ids + if record.gender == 'female' and record.male_children_ids: + raise ValidationError(_( + "A female animal cannot have male children in the male_children_ids field. " + "Female animals should only have children in female_children_ids (as mother)." + )) + + # --- Các hàm thay đổi (Onchange Functions) --- @api.onchange('weight') def _update_weight_pound(self): self.weight_pound = self.weight * 2.204623 diff --git a/addons/zoo/models/zoo_animal_meal.py b/addons/zoo/models/zoo_animal_meal.py new file mode 100644 index 0000000..9a9576f --- /dev/null +++ b/addons/zoo/models/zoo_animal_meal.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +from odoo import models, fields, api, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools import html2plaintext, format_date + +class ZooAnimalMeal(models.Model): + _name = "zoo.animal.meal" + _description = "Batch Feeding Record" + _inherit = ['mail.thread'] + + # Định nghĩa các field + record_name = fields.Char( + string='Batch Name', + compute='_compute_record_name', + store=True, + readonly=True + ) + + creature_id = fields.Many2one( + comodel_name="zoo.creature", + string="Species", + required=True, + help='Loài vật' + ) + + meal_date = fields.Datetime( + string="Meal Date", + required=True, + default=fields.Datetime.now, + help='Thời điểm cho ăn' + ) + + # domain logic: Chỉ chọn được Animal thuộc creature_id đã chọn + animal_ids = fields.Many2many( + comodel_name='zoo.animal', + relation='zoo_animal_meal_animal_rel', + column1='meal_id', + column2='animal_id', + string='Animals Fed', + domain="[('creature_id', '=', creature_id), ('is_alive', '=', True)]" + ) + + allowed_product_ids = fields.Many2many(comodel_name='product.product', + string='Allowed Products', + related='creature_id.allowed_product_ids') + + # Chọn product để cho ăn + product_id = fields.Many2one(comodel_name='product.product', + string='Food Item', + required=True, + domain="[('id', 'in', allowed_product_ids)]") + + uom_id = fields.Many2one(comodel_name='uom.uom', + related='product_id.uom_id', + string='Unit', + readonly=True, + required=True) + + qty_per_animal = fields.Float( + string='Qty per Animal', + default=1.0, + required=True, + help='Số lượng thức ăn cho mỗi con vật' + ) + + total_qty = fields.Float( + string='Total Qty', + compute='_compute_total_qty', + store=True, + help='Tổng số lượng = Qty per animal × Số động vật' + ) + + staff_id = fields.Many2one( + comodel_name="res.users", + string="Staff", + default=lambda self: self.env.user, + required=True, + help='Nhân viên phụ trách cho ăn' + ) + + state = fields.Selection( + [('draft', 'Draft'), ('done', 'Done')], + string='State', + default='draft', + tracking=True + ) + + meal_note = fields.Html( + string='Meal Note', + required=False, + help='Ghi chú về bữa ăn', + sanitize=True, + strip_style=False, + translate=False + ) + + # --- Validation --- + @api.constrains('meal_note') + def _check_meal_note_content(self): + for record in self: + if record.meal_note: + text_content = html2plaintext(record.meal_note).strip() + if not text_content: + raise ValidationError(_("Vui lòng nhập nội dung chi tiết về bữa ăn.")) + + # --- Computed Fields --- + @api.depends('creature_id', 'meal_date', 'product_id') + def _compute_record_name(self): + for record in self: + if record.creature_id and record.meal_date: + date_str = format_date(self.env, record.meal_date) + # Lấy tên món ăn + food_name = record.product_id.name if record.product_id else "..." + + # Format: Lion - Beef - 22/11/2025 + record.record_name = f"{record.creature_id.name} - {food_name} - {date_str}" + else: + record.record_name = 'New Meal' + + @api.depends('qty_per_animal', 'animal_ids') + def _compute_total_qty(self): + for record in self: + # Tổng = Định lượng * Số con + record.total_qty = record.qty_per_animal * len(record.animal_ids) + + # --- Actions --- + def action_load_all_animals(self): + """Load all animals of selected creature""" + for record in self: + if not record.creature_id: + raise UserError(_("Please select a creature first!")) + + # Tìm tất cả thú thuộc loài này + animals = self.env['zoo.animal'].search([ + ('creature_id', '=', record.creature_id.id), + ('is_alive', '=', True) + ]) + + if not animals: + raise UserError(_("No alive animals found for this creature!")) + + # Gán vào field Many2many (cú pháp replace: [(6, 0, ids)]) + record.animal_ids = [(6, 0, animals.ids)] + + return True + + def action_mark_done(self): + """Mark feeding as done""" + for record in self: + if not record.animal_ids: + raise UserError(_("Please select at least one animal!")) + if not record.product_id: + raise UserError(_("Please select a product!")) + + record.state = 'done' + + return True + + def action_reset_to_draft(self): + """Reset to draft""" + self.write({'state': 'draft'}) + return True + + # Định nghĩa các hàm Action: + def action_done(self): + for record in self: + # 1. Logic trừ kho (Inventory) sẽ viết ở đây + # ... + + # 2. Chuyển trạng thái + record.state = 'done' + + def action_draft(self): + for record in self: + record.state = 'draft' \ No newline at end of file diff --git a/addons/zoo/models/zoo_cage.py b/addons/zoo/models/zoo_cage.py index ceb24e6..26677e3 100644 --- a/addons/zoo/models/zoo_cage.py +++ b/addons/zoo/models/zoo_cage.py @@ -6,15 +6,51 @@ class ZooCage(models.Model): _description = 'Zoo Cage' _order = 'name' - name = fields.Char(string='Cage Name', required=True) - capacity = fields.Integer(string='Capacity', help='Maximum number of animals') + name = fields.Char(string='Cage Name', + required=True, + help='Name of the cage') + + capacity = fields.Integer(string='Capacity', + help='Maximum number of animals') + location = fields.Char(string='Location') + cage_type = fields.Selection([ ('indoor', 'Indoor'), ('outdoor', 'Outdoor'), ('aquarium', 'Aquarium'), ('aviary', 'Aviary'), - ], string='Cage Type', default='outdoor') - area = fields.Float(string='Area (m²)', help='Cage area in square meters') - description = fields.Text(string='Description') - active = fields.Boolean(string='Active', default=True) + ], + string='Cage Type', + default='outdoor', + required=True) + + area = fields.Float(string='Area (m²)', + help='Cage area in square meters') + + description = fields.Text(string='Description', + help='Description of the cage') + + active = fields.Boolean(string='Active', + default=True) + + # Định nghĩa một Standard checklist + checklist_template_ids = fields.One2many( + 'zoo.cage.checklist.template', + 'cage_id', + string='Standard Checklist' + ) + +# Định nghĩa một Standard checklist +class ZooCageChecklistTemplate(models.Model): + _name = 'zoo.cage.checklist.template' + _description = 'Cage Standard Task Template' + + cage_id = fields.Many2one(comodel_name='zoo.cage', + string='Cage') + + name = fields.Char(string='Task Description', + required=True) + + required = fields.Boolean(string='Required', + default=True) diff --git a/addons/zoo/models/zoo_creature.py b/addons/zoo/models/zoo_creature.py index 6f7e1c7..a22e32a 100644 --- a/addons/zoo/models/zoo_creature.py +++ b/addons/zoo/models/zoo_creature.py @@ -6,7 +6,9 @@ class ZooCreature(models.Model): _name = "zoo.creature" _description = "Creature" - name = fields.Char('Name', required=True) + name = fields.Char(string='Name', + required=True) + environment = fields.Selection([ ('water', 'Water'), ('ground', 'Ground'), @@ -20,7 +22,32 @@ class ZooCreature(models.Model): ('pond', 'Pond'), ('sea', 'Sea'), ('cool', 'Cool'), - ], string='Environment', default='ground') - is_rare = fields.Boolean('Is Rare', default=False) - - animal_ids = fields.One2many(comodel_name='zoo.animal', inverse_name='creature_id', string='Animals') \ No newline at end of file + ], + string='Environment', + default='ground') + + is_rare = fields.Boolean('Is Rare', + default=False) + + animal_ids = fields.One2many(comodel_name='zoo.animal', + inverse_name='creature_id', + string='Animals') + + animal_count = fields.Integer(string='Animal Count', + compute='_compute_animal_count', + store=True) + + allowed_product_ids = fields.Many2many( + comodel_name='product.product', + relation='zoo_creature_product_rel', + column1='creature_id', + column2='product_id', + string='Allowed Food Products', + help='List of food products that can be fed to this species' + ) + + # Định nghĩa các hàm tính toán + @api.depends('animal_ids') + def _compute_animal_count(self): + for record in self: + record.animal_count = len(record.animal_ids) \ No newline at end of file diff --git a/addons/zoo/models/zoo_health_record.py b/addons/zoo/models/zoo_health_record.py index fc4b9f8..402840e 100644 --- a/addons/zoo/models/zoo_health_record.py +++ b/addons/zoo/models/zoo_health_record.py @@ -82,7 +82,7 @@ class ZooHealthRecord(models.Model): ) related_cage_id = fields.Many2one(comodel_name='zoo.cage', - string='Related Cage ID', + string='Related Cage', required=False) # --- Các hàm tính toán --- diff --git a/addons/zoo/models/zoo_husbandry_task.py b/addons/zoo/models/zoo_husbandry_task.py new file mode 100644 index 0000000..25336ce --- /dev/null +++ b/addons/zoo/models/zoo_husbandry_task.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +from odoo import api, fields, models, tools, Command, _ +from odoo.exceptions import UserError, ValidationError +from odoo.tools import html2plaintext + +import datetime + +class ZooHusbandryTask(models.Model): + _name = 'zoo.husbandry.task' + _description = 'Daily Husbandry & Enrichment Task' + _inherit = ['mail.thread', 'mail.activity.mixin'] + _rec_name = 'display_name_custom' + + name = fields.Char(string='Reference', + required=True, + copy=False, + readonly=True, + default=lambda self: _('New')) + + # Ví dụ: "Lion Cage - 25/11/2025" + display_name_custom = fields.Char( + string='Subject', + compute='_compute_display_name_custom', + store=True + ) + + cage_id = fields.Many2one(comodel_name='zoo.cage', + domain="[('active', '=', True)]", + string='Cage/Enclosure', + required=True, + tracking=True) + + date = fields.Date(string='Date', + default=fields.Date.context_today, + required=True) + + user_id = fields.Many2one(comodel_name='res.users', + string='Assigned To', + default=lambda self: self.env.user, + required=True) + + task_type = fields.Selection([ + ('routine', 'Daily Routine'), + ('enrichment', 'Enrichment'), + ('maintenance', 'Minor Maintenance'), + ('vet_check', 'Visual Vet Check'), + ('observation', 'Observation'), + ('other', 'Other'), + ], + string='Task Type', + default='routine', + required=True) + + task_line_ids = fields.One2many(comodel_name='zoo.husbandry.task.line', + inverse_name='task_id', + string='Checklist', + required=True) + + keeper_note = fields.Html(string='Observations/Issues', + help='Ghi chú của Keeper', + sanitize=True, + strip_style=False, + translate=False) + + state = fields.Selection([ + ('draft', 'To Do'), + ('in_progress', 'In Progress'), + ('done', 'Done'), + ('cancel', 'Cancelled') + ], string='Status', + default='draft', + tracking=True, + group_expand='_expand_groups') + + # --- Logic đặt tên: Mã phiếu tự động + @api.model + def create(self, vals): + if vals.get('name', _('New')) == _('New'): + # HUSB là mã sequence chúng ta sẽ định nghĩa trong XML data + vals['name'] = self.env['ir.sequence'].next_by_code('zoo.husbandry.task') or _('New') + return super(ZooHusbandryTask, self).create(vals) + + # --- Logic đặt tên cho Display name + @api.depends('cage_id', 'date', 'task_type') + def _compute_display_name_custom(self): + for record in self: + cage_name = record.cage_id.name or 'Unknown Cage' + date_str = record.date.strftime('%d/%m') if record.date else '' + # Kết quả: "Lion Cage - 25/11" + record.display_name_custom = f"{cage_name} - {date_str}" + + # --- Kanban Grouping (Để Kanban hiển thị đủ cột Draft/Done dù không có data) --- + @api.model + def _expand_groups(self, states, domain, order=None): + """Force display all state columns in Kanban, even if empty""" + return ['draft', 'in_progress', 'done', 'cancel'] + + # --- ACTIONS --- + def action_start(self): + self.state = 'in_progress' + + def action_done(self): + # Validation: Không cho Done nếu chưa check hết các mục bắt buộc + unfinished_lines = self.task_line_ids.filtered(lambda l: l.required and not l.is_done) + if unfinished_lines: + raise ValidationError(_("You must complete all required checklist items before finishing!")) + self.state = 'done' + + def action_cancel(self): + self.state = 'cancel' + + def action_draft(self): + self.state = 'draft' + + # --- Validate kỹ hơn vì required=True của HTML đôi khi vẫn lọt lưới nếu chỉ nhập dấu cách + @api.constrains('keeper_note') + def _check_keeper_note_content(self): + for record in self: + if record.keeper_note: + # Chuyển HTML sang text thuần để kiểm tra độ dài thực + text_content = html2plaintext(record.keeper_note).strip() + if not text_content: + raise ValidationError("Vui lòng nhập nội dung chi tiết điều trị (không được để trống hoặc chỉ nhập khoảng trắng).") + + # Load template task mỗi khi chọn Chuồng + @api.onchange('cage_id') + def _onchange_cage_id(self): + """ + Khi chọn Chuồng: + 1. Xóa sạch checklist cũ (nếu có). + 2. Copy checklist mẫu từ cấu hình Chuồng sang Task này. + """ + if not self.cage_id: + return + + # Bước 1: Chuẩn bị danh sách lệnh + # Command.clear(): Xóa hết dòng cũ (tránh bị double khi user chọn lại chuồng) + lines_commands = [Command.clear()] + + # Bước 2: Lấy mẫu từ zoo.cage + if self.cage_id.checklist_template_ids: + for template in self.cage_id.checklist_template_ids: + # Command.create(values): Tạo dòng mới trong RAM (chưa lưu DB) + lines_commands.append(Command.create({ + 'name': template.name, + 'required': template.required, + 'is_done': False, # Mặc định chưa làm + })) + + # Bước 3: Gán vào field One2many của Task + self.task_line_ids = lines_commands + +# --- CHECKLIST --- +class ZooHusbandryTaskLine(models.Model): + _name = 'zoo.husbandry.task.line' + _description = 'Task Checklist' + + + task_id = fields.Many2one(comodel_name='zoo.husbandry.task', + string='Task', + required=True, + ondelete='cascade') + + name = fields.Char(string='Description', + required=True) + + is_done = fields.Boolean(string='Done', + default=False) + + required = fields.Boolean(string='Required', + default=True, + help="If checked, this item must be done to finish the task.") + + remark = fields.Char(string='Remark', + help="Quick note issues here (e.g. Broken lock)") \ No newline at end of file diff --git a/addons/zoo/security/ir.model.access.csv b/addons/zoo/security/ir.model.access.csv index 0b443f0..75395b7 100644 --- a/addons/zoo/security/ir.model.access.csv +++ b/addons/zoo/security/ir.model.access.csv @@ -4,4 +4,12 @@ access_zoo_creature,access_zoo_creature,model_zoo_creature,base.group_user,1,1,1 access_zoo_cage,access_zoo_cage,model_zoo_cage,base.group_user,1,1,1,1 access_zoo_health_record,access_zoo_health_record,model_zoo_health_record,base.group_user,1,1,1,1 access_zoo_diet_plan,access_zoo_diet_plan,model_zoo_diet_plan,base.group_user,1,1,1,1 -access_zoo_diet_line,access_zoo_diet_line,model_zoo_diet_line,base.group_user,1,1,1,1 \ No newline at end of file +access_zoo_diet_line,access_zoo_diet_line,model_zoo_diet_line,base.group_user,1,1,1,1 +access_zoo_toy_add_wizard,access_zoo_toy_add_wizard,model_zoo_toy_add_wizard,base.group_user,1,1,1,1 +access_zoo_cage_update_wizard,access_zoo_cage_update_wizard,model_zoo_cage_update_wizard,base.group_user,1,1,1,1 +access_zoo_animal_meal,access_zoo_animal_meal,model_zoo_animal_meal,base.group_user,1,1,1,1 +access_zoo_animal_feeding_wizard,access_zoo_animal_feeding_wizard,model_zoo_animal_feeding_wizard,base.group_user,1,1,1,1 +access_zoo_animal_feeding_wizard_line,access_zoo_animal_feeding_wizard_line,model_zoo_animal_feeding_wizard_line,base.group_user,1,1,1,1 +access_zoo_husbandry_task,access_zoo_husbandry_task,model_zoo_husbandry_task,base.group_user,1,1,1,1 +access_zoo_husbandry_task_line,access_zoo_husbandry_task_line,model_zoo_husbandry_task_line,base.group_user,1,1,1,1 +access_zoo_cage_checklist_template,access_zoo_cage_checklist_template,model_zoo_cage_checklist_template,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/addons/zoo/views/zoo_animal_meal_views.xml b/addons/zoo/views/zoo_animal_meal_views.xml new file mode 100644 index 0000000..af218d1 --- /dev/null +++ b/addons/zoo/views/zoo_animal_meal_views.xml @@ -0,0 +1,200 @@ + + + + + + zoo.animal.meal.form.view + zoo.animal.meal + +
+
+
+ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + + zoo.animal.meal.list.view + zoo.animal.meal + + + + + + + + + + + + + + + + + + + + + + + + zoo.animal.meal.kanban.view + zoo.animal.meal + + + + + + + + + + + + + + +
+
+
+ + + +
+
+ +
+
+ +
+
+
+ Species
+ +
+
+
+ +
+
+ Total Food
+ + + +
+
+ +
+
+
+
+ +
+
+
+
+
+
+
+
+ + + + Animal Meals + ir.actions.act_window + zoo.animal.meal + list,form,kanban + + +

+ Create a new Batch Feeding Record +

+

+ Track feeding sessions for multiple animals of the same species. +

+
+
+ + + + + +
+
\ No newline at end of file diff --git a/addons/zoo/views/zoo_animal_views.xml b/addons/zoo/views/zoo_animal_views.xml index 392dca9..25046d3 100644 --- a/addons/zoo/views/zoo_animal_views.xml +++ b/addons/zoo/views/zoo_animal_views.xml @@ -9,7 +9,7 @@
-