# Tác Vụ Tạo Addon Odoo

Tạo addon Odoo mới dựa trên yêu cầu nghiệp vụ và đặc tả kỹ thuật. Nhiệm vụ bao gồm dựng cấu trúc đầy đủ, tuân thủ chuẩn OCA và thực hành tốt nhất của Odoo.

## Mục tiêu
### Chính
- Tạo cấu trúc addon đầy đủ với mọi file cần thiết
- Triển khai logic nghiệp vụ theo đặc tả
- Tuân thủ chuẩn và guideline phát triển OCA
- Đảm bảo tương thích phiên bản Odoo mục tiêu
- Cung cấp tài liệu và kiểm thử đầy đủ

### Tiêu chí thành công
- Cài đặt không lỗi trên phiên bản mục tiêu
- Đáp ứng đủ yêu cầu chức năng
- Code vượt kiểm tra chất lượng OCA (flake8, pylint-odoo)
- Có tối thiểu unit test
- Tài liệu đầy đủ, chính xác

## Điều kiện tiên quyết
### Thông tin bắt buộc
- **Yêu cầu nghiệp vụ**: đặc tả chức năng rõ ràng
- **Kiến trúc kỹ thuật**: mô hình dữ liệu, thiết kế tích hợp
- **Phiên bản Odoo mục tiêu**: 13.0–18.0
- **Dependencies module**: module chuẩn và OCA cần thiết
- **Điểm tích hợp**: hệ thống/ API ngoài cần kết nối

### Môi trường phát triển
- Môi trường Odoo
- Có template/công cụ module OCA
- Repo Git để quản lý mã
- DB và môi trường kiểm thử

## Quy trình triển khai

### 1. Tạo cấu trúc addon
#### File manifest (`__manifest__.py`)
```python
{
    'name': 'Module Name',
    'version': '17.0.1.0.0',
    'category': 'Category',
    'summary': 'Brief module description',
    'description': """
Long description...
Include: key features, use cases, integrations, config requirements
    """,
    'author': 'Your Company, Odoo Community Association (OCA)',
     'website': 'https://kytoc.vn/',
    'license': 'AGPL-3',
    'depends': ['base', 'web'],
    'data': [
        'security/ir.model.access.csv',
        'security/security.xml',
        'views/model_views.xml',
        'views/menu_items.xml',
        'data/initial_data.xml',
    ],
    'demo': ['demo/demo_data.xml'],
    'installable': True,
    'auto_install': False,
    'application': False,
    'external_dependencies': {'python': [], 'bin': []},
}
```

#### Cấu trúc thư mục
```
addon_name/
├── __init__.py
├── __manifest__.py
├── models/
│   ├── __init__.py
│   └── model_name.py
├── views/
│   ├── model_views.xml
│   └── menu_items.xml
├── security/
│   ├── ir.model.access.csv
│   └── security.xml
├── data/
│   └── initial_data.xml
├── demo/
│   └── demo_data.xml
├── tests/
│   ├── __init__.py
│   └── test_model.py
├── static/
│   ├── description/
│   │   ├── icon.png
│   │   └── index.html
│   └── src/js/
├── wizards/
│   ├── __init__.py
│   └── wizard_name.py
└── README.rst
```

### 2. Triển khai model
```python
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError, UserError

class CustomModel(models.Model):
    _name = 'custom.model'
    _description = 'Custom Model Description'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _order = 'sequence, name'
    _rec_name = 'name'

    name = fields.Char(string='Name', required=True, translate=True, tracking=True)
    description = fields.Text(string='Description', translate=True)
    sequence = fields.Integer(string='Sequence', default=10, help='Used to order records')
    active = fields.Boolean(string='Active', default=True, tracking=True)

    state = fields.Selection([
        ('draft', 'Draft'),
        ('confirmed', 'Confirmed'),
        ('done', 'Done'),
        ('cancelled', 'Cancelled')
    ], string='Status', default='draft', tracking=True)

    company_id = fields.Many2one('res.company', string='Company',
                                 default=lambda self: self.env.company, required=True)
    user_id = fields.Many2one('res.users', string='Responsible',
                              default=lambda self: self.env.user, tracking=True)

    @api.depends('field1', 'field2')
    def _compute_calculated_field(self):
        for record in self:
            record.calculated_field = record.field1 + record.field2

    calculated_field = fields.Float(string='Calculated Field',
                                    compute='_compute_calculated_field', store=True)

    @api.constrains('field1')
    def _check_field1(self):
        for record in self:
            if record.field1 < 0:
                raise ValidationError(_('Field1 must be positive'))

    @api.model
    def create(self, vals):
        if 'sequence' not in vals:
            vals['sequence'] = self._get_next_sequence()
        return super().create(vals)

    def write(self, vals):
        if 'state' in vals and vals['state'] == 'confirmed':
            self._validate_confirmation()
        return super().write(vals)

    def unlink(self):
        if any(rec.state == 'confirmed' for rec in self):
            raise UserError(_('Cannot delete confirmed records'))
        return super().unlink()

    def action_confirm(self):
        self.ensure_one()
        if self.state != 'draft':
            raise UserError(_('Only draft records can be confirmed'))
        self.state = 'confirmed'
        self.message_post(body=_('Record confirmed'))

    def action_cancel(self):
        self.ensure_one()
        if self.state == 'done':
            raise UserError(_('Cannot cancel completed records'))
        self.state = 'cancelled'
        self.message_post(body=_('Record cancelled'))

    def _get_next_sequence(self):
        last_record = self.search([], order='sequence desc', limit=1)
        return (last_record.sequence or 0) + 10

    def _validate_confirmation(self):
        if not self.name:
            raise ValidationError(_('Name is required for confirmation'))
```

### 3. Triển khai view
**Form View**
```xml
<odoo>
  <record id="view_custom_model_form" model="ir.ui.view">
    <field name="name">custom.model.form</field>
    <field name="model">custom.model</field>
    <field name="arch" type="xml">
      <form string="Custom Model">
        <header>
          <button name="action_confirm" type="object"
                  string="Confirm" class="oe_highlight"
                  invisible="state != 'draft'"/>
          <button name="action_cancel" type="object"
                  string="Cancel"
                  invisible="state in ('done', 'cancelled')"/>
          <field name="state" widget="statusbar"
                 statusbar_visible="draft,confirmed,done"/>
        </header>
        <sheet>
          <div class="oe_title">
            <h1><field name="name" placeholder="Enter name..."/></h1>
          </div>
          <group>
            <group>
              <field name="user_id"/>
              <field name="company_id" groups="base.group_multi_company"/>
            </group>
            <group>
              <field name="sequence"/>
              <field name="active"/>
            </group>
          </group>
          <notebook>
            <page string="Description">
              <field name="description" nolabel="1"/>
            </page>
            <page string="Additional Info">
              <group>
                <field name="calculated_field"/>
              </group>
            </page>
          </notebook>
        </sheet>
        <div class="oe_chatter">
          <field name="message_follower_ids"/>
          <field name="activity_ids"/>
          <field name="message_ids"/>
        </div>
      </form>
    </field>
  </record>

  <!-- Tree View -->
  <record id="view_custom_model_tree" model="ir.ui.view">
    <field name="name">custom.model.tree</field>
    <field name="model">custom.model</field>
    <field name="arch" type="xml">
      <tree string="Custom Models"
            decoration-info="state=='draft'"
            decoration-success="state=='done'"
            decoration-muted="state=='cancelled'">
        <field name="sequence" widget="handle"/>
        <field name="name"/>
        <field name="user_id"/>
        <field name="calculated_field"/>
        <field name="state"/>
        <field name="company_id" groups="base.group_multi_company"/>
      </tree>
    </field>
  </record>

  <!-- Search View -->
  <record id="view_custom_model_search" model="ir.ui.view">
    <field name="name">custom.model.search</field>
    <field name="model">custom.model</field>
    <field name="arch" type="xml">
      <search string="Search Custom Models">
        <field name="name"/>
        <field name="user_id"/>
        <field name="company_id" groups="base.group_multi_company"/>
        <separator/>
        <filter name="my_records" string="My Records"
                domain="[('user_id', '=', uid)]"/>
        <filter name="active" string="Active"
                domain="[('active', '=', True)]"/>
        <separator/>
        <filter name="draft" string="Draft"
                domain="[('state', '=', 'draft')]"/>
        <filter name="confirmed" string="Confirmed"
                domain="[('state', '=', 'confirmed')]"/>
        <group expand="0" string="Group By">
          <filter string="Status" name="group_state"
                  context="{'group_by': 'state'}"/>
          <filter string="Responsible" name="group_user"
                  context="{'group_by': 'user_id'}"/>
          <filter string="Company" name="group_company"
                  context="{'group_by': 'company_id'}"
                  groups="base.group_multi_company"/>
        </group>
      </search>
    </field>
  </record>
</odoo>
```

### 4. Bảo mật
**Access Rights** (`security/ir.model.access.csv`)
```csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_custom_model_user,custom.model.user,model_custom_model,base.group_user,1,1,1,1
access_custom_model_manager,custom.model.manager,model_custom_model,base.group_system,1,1,1,1
```

**Security Groups** (`security/security.xml`)
```xml
<odoo>
  <record model="res.groups" id="group_custom_model_user">
    <field name="name">Custom Model User</field>
    <field name="category_id" ref="base.module_category_operations"/>
    <field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
  </record>

  <record model="res.groups" id="group_custom_model_manager">
    <field name="name">Custom Model Manager</field>
    <field name="category_id" ref="base.module_category_operations"/>
    <field name="implied_ids" eval="[(4, ref('group_custom_model_user'))]"/>
  </record>

  <record id="custom_model_rule_user" model="ir.rule">
    <field name="name">Custom Model: User Access</field>
    <field name="model_id" ref="model_custom_model"/>
    <field name="domain_force">[
      '|',
      ('user_id', '=', user.id),
      ('company_id', 'in', user.company_ids.ids)
    ]</field>
    <field name="groups" eval="[(4, ref('group_custom_model_user'))]"/>
  </record>

  <record id="custom_model_rule_manager" model="ir.rule">
    <field name="name">Custom Model: Manager Access</field>
    <field name="model_id" ref="model_custom_model"/>
    <field name="domain_force">[(1, '=', 1)]</field>
    <field name="groups" eval="[(4, ref('group_custom_model_manager'))]"/>
  </record>
</odoo>
```

### 5. Menu & Actions
```xml
<odoo>
  <record id="action_custom_model" model="ir.actions.act_window">
    <field name="name">Custom Models</field>
    <field name="res_model">custom.model</field>
    <field name="view_mode">tree,form</field>
    <field name="context">{}</field>
    <field name="domain">[]</field>
    <field name="help" type="html">
      <p class="o_view_nocontent_smiling_face">Create your first custom model!</p>
      <p>Manage processes efficiently with powerful features & integrations.</p>
    </field>
  </record>

  <menuitem id="menu_custom_root" name="Custom Module" sequence="10"/>
  <menuitem id="menu_custom_models" name="Custom Models"
            parent="menu_custom_root" action="action_custom_model" sequence="10"/>
</odoo>
```

### 6. Kiểm thử
**Unit tests (`tests/test_model.py`)**
```python
from odoo.tests import TransactionCase
from odoo.exceptions import ValidationError, UserError

class TestCustomModel(TransactionCase):
    def setUp(self):
        super().setUp()
        self.CustomModel = self.env['custom.model']
        self.user = self.env.ref('base.user_demo')

    def test_create_model(self):
        model = self.CustomModel.create({'name': 'Test Model', 'user_id': self.user.id})
        self.assertEqual(model.state, 'draft')
        self.assertTrue(model.active)

    def test_confirm_action(self):
        model = self.CustomModel.create({'name': 'Test Model', 'user_id': self.user.id})
        model.action_confirm()
        self.assertEqual(model.state, 'confirmed')
        with self.assertRaises(UserError):
            model.action_confirm()

    def test_validation_constraints(self):
        with self.assertRaises(ValidationError):
            self.CustomModel.create({'name': 'Test Model', 'field1': -1})

    def test_computed_fields(self):
        model = self.CustomModel.create({'name': 'Test Model', 'field1': 10, 'field2': 20})
        self.assertEqual(model.calculated_field, 30)

    def test_sequence_generation(self):
        m1 = self.CustomModel.create({'name': 'Model 1'})
        m2 = self.CustomModel.create({'name': 'Model 2'})
        self.assertGreater(m2.sequence, m1.sequence)
```

### 7. Tài liệu
**README.rst** (ví dụ)
```rst
============
Custom Model
============

.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   !! Generated by oca-gen-addon-readme, do not edit !!
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
    :target: https://odoo-community.org/page/development-status
    :alt: Beta

|badge1|

Module này cung cấp custom model để quản lý quy trình nghiệp vụ.

**Table of contents**

.. contents::
   :local:

Features
========
* Custom model & workflow
* Multi-company support
* Chatter & activities
* Security đầy đủ

Usage
=====
1. Mở menu Custom Module
2. Tạo bản ghi mới
3. Điền thông tin bắt buộc
4. Dùng actions để chuyển trạng thái
```

## Đảm bảo chất lượng
**Pre-commit hooks (ví dụ)**
```yaml
repos:
  - repo: https://kytoc.vn/
    rev: v1.0.0
    hooks:
      - id: oca-checks-odoo-module
      - id: oca-checks-po
  - repo: https://github.com/psf/black
    rev: 22.3.0
    hooks:
      - id: black
  - repo: https://github.com/pycqa/flake8
    rev: 4.0.1
    hooks:
      - id: flake8
  - repo: https://github.com/pycqa/isort
    rev: 5.10.1
    hooks:
      - id: isort
```

**Checklist kiểm thử**
- [ ] Cài đặt module không lỗi
- [ ] View hiển thị đúng
- [ ] Quy tắc bảo mật hoạt động
- [ ] Unit tests pass
- [ ] Lint pass
- [ ] Tài liệu đầy đủ
- [ ] Demo data load đúng

## Triển khai
**Cài đặt**
1. Copy addon vào đường dẫn addons
2. Cập nhật danh sách: `invoke install --modules=custom_model`
3. Cài từ Apps
4. Cấu hình nhóm bảo mật nếu cần
5. Import dữ liệu khởi tạo nếu cần

**Sau cài đặt**
1. Xác minh chức năng
2. Đào tạo người dùng
3. Giám sát hiệu suất & usage
4. Thiết lập backup

Nhớ: Đây là nền tảng cho phát triển addon Odoo. Ưu tiên chất lượng code, bảo mật và trải nghiệm người dùng, tuân thủ chuẩn OCA để tương thích cộng đồng.