diff --git a/batch_picking_create_one_bill/__init__.py b/batch_picking_create_one_bill/__init__.py
new file mode 100644
index 0000000..862180d
--- /dev/null
+++ b/batch_picking_create_one_bill/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+from . import models
+from . import wizards
diff --git a/batch_picking_create_one_bill/__manifest__.py b/batch_picking_create_one_bill/__manifest__.py
new file mode 100644
index 0000000..44e921a
--- /dev/null
+++ b/batch_picking_create_one_bill/__manifest__.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'Batch Picking - Create One Bill',
+ 'version': '18.0.1.0.0',
+ 'category': 'Inventory/Purchase',
+ 'summary': 'Créer une seule facture fournisseur pour tous les bons de commande d\'un batch picking',
+ 'description': """
+ Ce module permet de générer une seule facture fournisseur pour tous les bons de commande
+ associés à un batch picking.
+
+ Fonctionnalités:
+ - Option pour initialiser les quantités à zéro lors de la création d'un batch
+ - Bouton pour créer une facture groupée à partir d'un batch de transferts
+ - Validation que tous les bons de commande proviennent du même fournisseur
+ - Suivi des factures créées directement depuis le batch
+ """,
+ 'author': 'Pneumac',
+ 'website': 'https://www.pneumac.ca',
+ 'depends': [
+ 'stock_picking_batch',
+ 'purchase',
+ 'purchase_stock',
+ 'account',
+ 'account_reports',
+ ],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'wizards/create_bill_wizard_views.xml',
+ 'wizards/merge_bill_wizard_views.xml',
+ 'views/stock_picking_batch_views.xml',
+ ],
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+ 'license': 'LGPL-3',
+}
diff --git a/batch_picking_create_one_bill/models/__init__.py b/batch_picking_create_one_bill/models/__init__.py
new file mode 100644
index 0000000..c238a7a
--- /dev/null
+++ b/batch_picking_create_one_bill/models/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+from . import stock_picking_batch
diff --git a/batch_picking_create_one_bill/models/stock_picking_batch.py b/batch_picking_create_one_bill/models/stock_picking_batch.py
new file mode 100644
index 0000000..eb46d5a
--- /dev/null
+++ b/batch_picking_create_one_bill/models/stock_picking_batch.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api
+from odoo.exceptions import ValidationError
+
+class StockPickingBatch(models.Model):
+ _inherit = 'stock.picking.batch'
+
+ zero_quantity_default = fields.Boolean(
+ string='Quantités à zéro par défaut',
+ default=True,
+ help='Initialiser les quantités à zéro lors de la création du batch'
+ )
+ invoice_ids = fields.Many2many(
+ 'account.move',
+ string='Factures associées',
+ compute='_compute_invoice_ids',
+ store=True
+ )
+ invoice_count = fields.Integer(
+ string='Nombre de factures',
+ compute='_compute_invoice_count',
+ store=True,
+ compute_sudo=True
+ )
+
+ def _prepare_move_line_vals(self, **kwargs):
+ vals = super()._prepare_move_line_vals(**kwargs)
+ if self.zero_quantity_default:
+ vals['quantity'] = 0.0
+ return vals
+
+ @api.depends('picking_ids.move_ids.purchase_line_id.order_id.invoice_ids')
+ def _compute_invoice_ids(self):
+ for batch in self:
+ purchase_orders = batch.picking_ids.move_ids.purchase_line_id.order_id
+ batch.invoice_ids = purchase_orders.invoice_ids
+
+ @api.depends('invoice_ids')
+ def _compute_invoice_count(self):
+ for batch in self:
+ batch.invoice_count = len(batch.invoice_ids)
+
+ def action_view_invoices(self):
+ self.ensure_one()
+ action = self.env['ir.actions.act_window']._for_xml_id(
+ 'account.action_move_in_invoice_type'
+ )
+ if self.invoice_count == 1:
+ action['views'] = [(False, 'form')]
+ action['res_id'] = self.invoice_ids.id
+ else:
+ action['domain'] = [('id', 'in', self.invoice_ids.ids)]
+ return action
+
+ @api.constrains('picking_ids')
+ def _check_purchase_orders(self):
+ for batch in self:
+ partners = batch.picking_ids.purchase_id.partner_id
+ if len(partners) > 1:
+ raise ValidationError(
+ 'Tous les bons de commande du batch doivent provenir du même fournisseur.'
+ )
diff --git a/batch_picking_create_one_bill/security/ir.model.access.csv b/batch_picking_create_one_bill/security/ir.model.access.csv
new file mode 100644
index 0000000..8647d7a
--- /dev/null
+++ b/batch_picking_create_one_bill/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_create_bill_wizard,create_bill_wizard_access,model_create_bill_wizard,base.group_user,1,1,1,0
+access_merge_bill_wizard,merge_bill_wizard_access,model_merge_bill_wizard,base.group_user,1,1,1,0
diff --git a/batch_picking_create_one_bill/specifications.md b/batch_picking_create_one_bill/specifications.md
new file mode 100644
index 0000000..e437f5d
--- /dev/null
+++ b/batch_picking_create_one_bill/specifications.md
@@ -0,0 +1,693 @@
+# Spécifications du module batch_picking_create_one_bill
+
+## Objectif
+Créer un module Odoo 18.0 permettant de générer une seule facture fournisseur (vendor bill) consolidée pour tous les bons de commande (Purchase Orders) associés à un batch picking, en utilisant la méthode standard de création de factures d'Odoo basée sur les réceptions.
+
+## Fonctionnalités principales
+
+### 1. Modification du comportement des Batch Pickings
+
+#### Héritage du modèle `stock.picking.batch`
+
+##### Champs ajoutés
+```python
+# Dans stock_picking_batch.py
+class StockPickingBatch(models.Model):
+ _inherit = 'stock.picking.batch'
+
+ zero_quantity_default = fields.Boolean(
+ string='Quantités à zéro par défaut',
+ default=True,
+ help='Initialiser les quantités à zéro lors de la création du batch'
+ )
+ invoice_count = fields.Integer(
+ compute='_compute_invoice_count',
+ string='Nombre de factures'
+ )
+ invoice_ids = fields.Many2many(
+ 'account.move',
+ string='Factures associées',
+ compute='_compute_invoice_ids',
+ store=True
+ )
+```
+
+##### Surcharge des méthodes
+
+###### Initialisation des quantités
+```python
+def _prepare_move_line_vals(self, **kwargs):
+ vals = super()._prepare_move_line_vals(**kwargs)
+ if self.zero_quantity_default:
+ vals['quantity'] = 0.0
+ return vals
+```
+
+###### Calcul des factures associées
+```python
+@api.depends('picking_ids', 'picking_ids.purchase_id.invoice_ids')
+def _compute_invoice_ids(self):
+ for batch in self:
+ invoices = batch.picking_ids.mapped('purchase_id.invoice_ids')
+ batch.invoice_ids = invoices
+ batch.invoice_count = len(invoices)
+```
+
+##### Actions et boutons
+```python
+def action_view_invoices(self):
+ self.ensure_one()
+ action = self.env['ir.actions.act_window']._for_xml_id(
+ 'account.action_move_in_invoice_type'
+ )
+ if self.invoice_count == 1:
+ action['views'] = [(False, 'form')]
+ action['res_id'] = self.invoice_ids.id
+ else:
+ action['domain'] = [('id', 'in', self.invoice_ids.ids)]
+ return action
+```
+
+#### Modification des vues
+
+##### Vue formulaire du batch picking
+```xml
+
+ stock.picking.batch.form.inherit
+ stock.picking.batch
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+#### Gestion des mouvements de stock
+
+##### Modification du comportement des move lines
+```python
+def _update_move_lines_values(self, moves):
+ res = super()._update_move_lines_values(moves)
+ if self.zero_quantity_default:
+ for move in moves:
+ for line in move.move_line_ids:
+ line.qty_done = 0.0
+ return res
+```
+
+##### Validation des quantités
+```python
+def _check_received_quantities(self):
+ self.ensure_one()
+ for move in self.move_lines:
+ if move.quantity_done > move.product_uom_qty:
+ raise ValidationError(
+ _('La quantité reçue ne peut pas être supérieure '
+ 'à la quantité commandée pour le produit %s')
+ % move.product_id.display_name
+ )
+```
+
+### 2. Critères de regroupement
+- Regroupement uniquement des PO du même fournisseur
+- Facturation basée uniquement sur les quantités réellement reçues dans le batch picking
+- Les lignes de PO non reçues ne seront pas incluses dans la facture
+
+### 2. Processus de création de facture
+- Un bouton dédié sera ajouté sur le batch picking après sa validation
+- Pour chaque PO du batch :
+ 1. Utilisation de la méthode standard d'Odoo `action_create_invoice` pour créer les factures basées sur les réceptions
+ 2. Les factures sont créées selon la politique 'Sur réception' (On received quantities)
+ 3. Création automatique de factures partielles pour les quantités reçues
+- Fusion automatique de toutes les factures créées en une seule facture
+ 1. Regroupement des lignes par produit
+ 2. Conservation des liens avec les PO d'origine
+ 3. Suppression des factures individuelles après fusion
+
+### 3. Gestion des cas particuliers
+- Vérification des quantités reçues vs commandées
+- Validation que toutes les réceptions sont bien effectuées avant la création de la facture
+- Gestion des écarts entre les quantités commandées et reçues
+
+### 4. Sécurité et droits d'accès
+- Restriction aux utilisateurs du groupe 'Invoicing user'
+- Vérification des droits d'accès sur les documents liés (PO, réceptions)
+
+### 5. Interface utilisateur
+- Ajout d'un bouton sur la vue form du batch picking
+- Rapports spécifiques pour le suivi des factures groupées [À PRÉCISER]
+- Messages de confirmation/erreur clairs pour l'utilisateur
+
+### 6. Aspects techniques
+- Pas de règles spécifiques pour la numérotation des factures (utilisation du système standard)
+- Intégration avec les modules stock_picking_batch et purchase
+- Respect des règles de facturation basées sur les réceptions
+
+## Workflow
+1. Création et traitement normal du batch picking
+2. Validation du batch picking
+3. Utilisation du nouveau bouton pour créer la facture groupée
+4. Pour chaque PO impliqué dans le batch picking :
+ - Utilisation de la méthode standard `action_create_invoice` d'Odoo
+ - Création automatique des factures basées sur les quantités reçues
+ - Les quantités non reçues restent en attente de facturation
+5. Fusion automatique de toutes les factures créées en une seule facture fournisseur
+ - Regroupement des lignes par produit
+ - Maintien des références aux PO d'origine
+ - Conservation des taxes et comptes analytiques
+ - Suppression des factures individuelles après fusion réussie
+6. Traitement normal de la facture fusionnée
+
+## Impact sur les processus existants
+- Les utilisateurs devront entrer manuellement les quantités reçues au lieu de les ajuster
+- Réduction des erreurs de réception dues aux quantités pré-remplies
+- Meilleure traçabilité des quantités réellement reçues vs commandées
+
+## Détails techniques d'implémentation
+
+### 1. Modification du comportement des Batch Pickings
+- Héritage du modèle `stock.picking.batch`
+- Surcharge de la méthode de création pour initialiser les quantités à 0
+- Modification des champs de quantité dans `stock.move.line`
+
+### 2. Gestion des factures
+
+#### Wizard de création de facture (`create_merged_bill.py`)
+
+##### 1. Modèles
+
+###### Modèle principal : `batch.picking.create.bill.wizard`
+- Champs principaux :
+ - `batch_id`: Many2one vers stock.picking.batch (readonly)
+ - Batch picking source pour la création de la facture
+ - `purchase_order_ids`: Many2many vers purchase.order (computed, readonly)
+ - Liste des PO associés au batch
+ - Calculé automatiquement depuis le batch
+ - `partner_id`: Many2one vers res.partner (computed, readonly)
+ - Fournisseur commun à tous les PO
+ - Vérification d'unicité du fournisseur
+ - `invoice_ids`: Many2many vers account.move (computed)
+ - Factures créées par le processus standard d'Odoo
+ - Utilisé temporairement avant la fusion
+ - `merged_invoice_id`: Many2one vers account.move
+ - Facture finale fusionnée
+ - `preview_available`: Boolean (computed)
+ - Indique si la prévisualisation est possible
+
+###### Modèle temporaire : `batch.picking.create.bill.line.preview`
+- Champs :
+ - `wizard_id`: Many2one vers le wizard principal
+ - `product_id`: Many2one vers product.product
+ - `description`: Char (nom du produit + description)
+ - `quantity`: Float (quantité reçue)
+ - `uom_id`: Many2one vers uom.uom
+ - `price_unit`: Float
+ - `price_subtotal`: Float
+ - `purchase_line_ids`: Many2many vers purchase.order.line
+ - `picking_ids`: Many2many vers stock.picking
+
+##### 2. Fonctions principales
+
+###### Calculs et vérifications
+- `_compute_purchase_orders()`:
+ ```python
+ def _compute_purchase_orders(self):
+ for wizard in self:
+ pickings = wizard.batch_id.picking_ids
+ purchase_orders = pickings.mapped('purchase_id')
+ wizard.purchase_order_ids = purchase_orders
+ ```
+
+- `_compute_partner()`:
+ ```python
+ def _compute_partner(self):
+ for wizard in self:
+ partners = wizard.purchase_order_ids.mapped('partner_id')
+ if len(partners) != 1:
+ raise ValidationError(_('Tous les PO doivent avoir le même fournisseur'))
+ wizard.partner_id = partners
+ ```
+
+###### Préparation des données
+- `_prepare_invoice_lines()`:
+ - Regroupe les lignes par produit
+ - Calcule les quantités reçues
+ - Vérifie la cohérence des prix
+ - Génère les lignes de facture
+
+- `_prepare_preview_lines()`:
+ - Crée les lignes de prévisualisation
+ - Affiche les détails des réceptions
+ - Calcule les sous-totaux
+
+###### Actions
+- `action_create_bill()`:
+ 1. Vérifications préalables
+ 2. Création des factures par PO
+ 3. Fusion des factures
+ 4. Liaison avec le batch picking
+ 5. Retour vers la facture créée
+
+##### 3. Interface utilisateur
+
+###### Vue formulaire principale
+```xml
+
+```
+
+##### 4. Messages et Validations
+
+###### Messages d'erreur
+- Fournisseur différent :
+ ```python
+ _('Les bons de commande sélectionnés ont des fournisseurs différents : %s')
+ ```
+- Batch non validé :
+ ```python
+ _('Le batch picking doit être validé avant de créer la facture')
+ ```
+- Quantités invalides :
+ ```python
+ _('Certaines lignes ont des quantités reçues invalides')
+ ```
+
+###### Validations automatiques
+- Vérification de l'état du batch
+- Contrôle des quantités reçues
+- Vérification des devises
+- Validation des droits d'accès
+
+### 3. Modifications de l'interface
+
+#### Modifications des vues principales
+
+##### 1. Vue formulaire du batch picking (`stock_picking_batch_views.xml`)
+```xml
+
+
+
+
+ stock.picking.batch.form.inherit
+ stock.picking.batch
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ stock.picking.batch.tree.inherit
+ stock.picking.batch
+
+
+
+
+
+
+
+
+
+
+ stock.picking.batch.search.inherit
+ stock.picking.batch
+
+
+
+
+
+
+
+
+
+```
+
+##### 2. Vue formulaire de la facture (`account_move_views.xml`)
+```xml
+
+
+
+ account.move.form.inherit
+ account.move
+
+
+
+
+
+
+
+
+
+```
+
+#### Actions et menus
+
+##### Actions du wizard
+```xml
+
+ Créer Facture Fournisseur
+ batch.picking.create.bill.wizard
+ form
+ new
+
+ form
+
+```
+
+#### Sécurité et droits d'accès
+
+##### Groupes et droits
+```xml
+
+ Création de factures depuis batch picking
+
+
+
+```
+
+##### Règles d'accès
+```csv
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_batch_bill_wizard,access.batch.picking.create.bill.wizard,model_batch_picking_create_bill_wizard,group_batch_invoice,1,1,1,0
+access_batch_bill_line_preview,access.batch.picking.create.bill.line.preview,model_batch_picking_create_bill_line_preview,group_batch_invoice,1,1,1,1
+```
+
+### 4. Points techniques clés
+
+#### Manifest du module
+```python
+{
+ 'name': 'Batch Picking - Create One Bill',
+ 'version': '18.0.1.0.0',
+ 'category': 'Inventory/Purchase',
+ 'summary': 'Créer une seule facture pour tous les PO d’un batch picking',
+ 'author': 'Pneumac',
+ 'website': 'https://www.pneumac.com',
+ 'license': 'LGPL-3',
+ 'depends': [
+ 'stock_picking_batch',
+ 'purchase',
+ 'account',
+ ],
+ 'data': [
+ 'security/security_groups.xml',
+ 'security/ir.model.access.csv',
+ 'views/stock_picking_batch_views.xml',
+ 'views/account_move_views.xml',
+ 'wizard/create_merged_bill_views.xml',
+ ],
+ 'demo': [],
+ 'installable': True,
+ 'auto_install': False,
+ 'application': False,
+}
+```
+
+#### Structure complète des fichiers
+```
+batch_picking_create_one_bill/
+├── __init__.py # Import des sous-modules
+├── __manifest__.py # Configuration du module
+├── models/ # Définition des modèles
+│ ├── __init__.py
+│ ├── stock_picking_batch.py # Extension du modèle batch
+│ └── account_move.py # Extension du modèle facture
+├── wizard/ # Assistant de création de facture
+│ ├── __init__.py
+│ ├── create_merged_bill.py # Logique du wizard
+│ └── create_merged_bill_views.xml # Vues du wizard
+├── views/ # Vues des modèles
+│ ├── stock_picking_batch_views.xml
+│ └── account_move_views.xml
+├── security/ # Sécurité et droits d'accès
+│ ├── ir.model.access.csv # Règles d'accès aux modèles
+│ └── security_groups.xml # Définition des groupes
+├── static/ # Ressources statiques
+│ └── description/
+│ └── icon.png # Icône du module
+└── i18n/ # Traductions
+ └── fr.po # Traduction française
+
+```
+
+#### Points techniques spécifiques
+
+##### 1. Gestion des hooks
+```python
+# Dans stock_picking_batch.py
+from odoo import models, fields, api, _
+
+class StockPickingBatch(models.Model):
+ _inherit = 'stock.picking.batch'
+
+ @api.model
+ def create(self, vals):
+ # Hook de création pour initialiser les quantités à 0
+ res = super().create(vals)
+ if res.zero_quantity_default:
+ res._reset_quantities()
+ return res
+
+ def write(self, vals):
+ # Hook d'écriture pour gérer les modifications
+ res = super().write(vals)
+ if 'zero_quantity_default' in vals:
+ self._handle_quantity_change()
+ return res
+```
+
+##### 2. Gestion des contraintes
+```python
+@api.constrains('picking_ids', 'picking_ids.purchase_id')
+def _check_purchase_orders(self):
+ for batch in self:
+ purchases = batch.picking_ids.mapped('purchase_id')
+ partners = purchases.mapped('partner_id')
+ if len(partners) > 1:
+ raise ValidationError(_(
+ 'Le batch picking ne peut contenir que des PO '
+ 'du même fournisseur.'
+ ))
+```
+
+##### 3. Performance et optimisation
+```python
+# Dans create_merged_bill.py
+from odoo.tools.profiler import profile
+
+@profile
+def _prepare_invoice_lines(self):
+ # Optimisation avec prefetch
+ products = self.env['product.product'].browse(
+ self.purchase_order_ids.mapped('order_line.product_id.id')
+ ).exists()
+ products.read(['name', 'type', 'invoice_policy'])
+
+ # Traitement par lots
+ moves_by_product = {}
+ for move in self.batch_id.move_lines:
+ if move.product_id not in moves_by_product:
+ moves_by_product[move.product_id] = self.env['stock.move']
+ moves_by_product[move.product_id] |= move
+```
+
+##### 4. Gestion des erreurs
+```python
+class BatchBillError(Exception):
+ """ Erreur spécifique pour la création de facture depuis batch """
+ pass
+
+def _handle_bill_creation_error(self, error):
+ if isinstance(error, BatchBillError):
+ # Erreur métier spécifique
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Erreur'),
+ 'message': str(error),
+ 'type': 'danger',
+ }
+ }
+ # Autres erreurs
+ raise error
+```
+
+##### 5. Tests unitaires
+```python
+# Dans tests/test_batch_bill.py
+from odoo.tests import tagged
+from odoo.tests.common import TransactionCase
+
+@tagged('post_install', '-at_install')
+class TestBatchBill(TransactionCase):
+
+ def setUp(self):
+ super().setUp()
+ # Préparation des données de test
+
+ def test_create_bill_from_batch(self):
+ # Test de création de facture
+
+ def test_zero_quantity_default(self):
+ # Test de l'initialisation des quantités
+
+ def test_merge_bills(self):
+ # Test de la fusion des factures
+```
+- Gestion des états des documents (draft, done, etc.)
+- Traçabilité complète via les champs related et stored
+- Gestion des droits d'accès via les groupes de sécurité
+
+### 5. Améliorations suggérées
+
+#### 1. Fonctionnalités supplémentaires
+- **Annulation en masse** :
+ - Possibilité d'annuler la facture créée et de revenir à l'état précédent
+ - Traçabilité des annulations dans l'historique
+
+- **Rapports et analyses** :
+ - Rapport de réconciliation PO/Réceptions/Factures
+
+#### 2. Améliorations techniques
+- **Cache et performance** :
+ - Mise en cache des calculs fréquents
+ - Optimisation des requêtes SQL
+ - Indexation des champs clés
+
+- **Gestion des erreurs avancée** :
+ - Journal d'erreurs détaillé
+ - Mécanisme de reprise sur erreur
+ - Notifications utilisateur améliorées
+
+- **Tests automatisés** :
+ - Tests d'intégration avec scénarios complexes
+ - Tests de performance
+ - Tests de régression
+
+#### 3. Améliorations UX
+- **Interface utilisateur** :
+ - Vue kanban pour les batch pickings avec statut de facturation
+ - Filtres et groupements avancés
+ - Aperçu rapide des informations clés
+
+- **Processus utilisateur** :
+ - Assistant de validation en plusieurs étapes
+ - Suggestions automatiques basées sur l'historique
+ - Messages d'aide contextuels
+
+#### 4. Personnalisation
+- **Configuration avancée** :
+ - Paramètres par entreprise
+
+### 5. Structure des fichiers
+```
+batch_picking_create_one_bill/
+├── __init__.py
+├── __manifest__.py
+├── models/
+│ ├── __init__.py
+│ ├── stock_picking_batch.py
+│ └── account_move.py
+├── wizard/
+│ ├── __init__.py
+│ └── create_merged_bill.py
+├── views/
+│ ├── stock_picking_batch_views.xml
+│ └── account_move_views.xml
+├── security/
+│ └── ir.model.access.csv
+└── data/
+ └── security_groups.xml
+```
+
+## Dépendances
+- stock_picking_batch
+- purchase
+- account
+
+## Version
+- Odoo 18.0
diff --git a/batch_picking_create_one_bill/views/stock_picking_batch_views.xml b/batch_picking_create_one_bill/views/stock_picking_batch_views.xml
new file mode 100644
index 0000000..bea2f0c
--- /dev/null
+++ b/batch_picking_create_one_bill/views/stock_picking_batch_views.xml
@@ -0,0 +1,30 @@
+
+
+
+ stock.picking.batch.form.inherit
+ stock.picking.batch
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/batch_picking_create_one_bill/wizards/__init__.py b/batch_picking_create_one_bill/wizards/__init__.py
new file mode 100644
index 0000000..7306402
--- /dev/null
+++ b/batch_picking_create_one_bill/wizards/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+from . import create_bill_wizard
+from . import merge_bill_wizard
diff --git a/batch_picking_create_one_bill/wizards/create_bill_wizard.py b/batch_picking_create_one_bill/wizards/create_bill_wizard.py
new file mode 100644
index 0000000..ffc42b2
--- /dev/null
+++ b/batch_picking_create_one_bill/wizards/create_bill_wizard.py
@@ -0,0 +1,155 @@
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+class CreateBillWizard(models.TransientModel):
+ _name = 'create.bill.wizard'
+ _description = 'Wizard pour créer une facture groupée'
+
+ batch_id = fields.Many2one(
+ 'stock.picking.batch',
+ required=True,
+ string='Batch de transferts',
+ default=lambda self: self._context.get('default_batch_id')
+ )
+
+ purchase_order_ids = fields.Many2many(
+ 'purchase.order',
+ string='Bons de commande',
+ compute='_compute_purchase_orders',
+ store=True
+ )
+
+ partner_id = fields.Many2one(
+ 'res.partner',
+ string='Fournisseur',
+ compute='_compute_partner',
+ store=True
+ )
+
+ invoice_ids = fields.Many2many(
+ 'account.move',
+ string='Factures temporaires',
+ compute='_compute_invoice_ids',
+ )
+
+ merged_invoice_id = fields.Many2one(
+ 'account.move',
+ string='Facture fusionnée'
+ )
+
+ @api.depends('batch_id')
+ def _compute_purchase_orders(self):
+ for wizard in self:
+ wizard.purchase_order_ids = wizard.batch_id.picking_ids.move_ids.purchase_line_id.order_id
+
+ @api.depends('purchase_order_ids')
+ def _compute_partner(self):
+ for wizard in self:
+ partners = wizard.purchase_order_ids.mapped('partner_id')
+ if len(partners) > 1:
+ raise ValidationError(_('Les bons de commande doivent provenir du même fournisseur'))
+ wizard.partner_id = partners and partners[0] or False
+
+ @api.depends('purchase_order_ids')
+ def _compute_invoice_ids(self):
+ for wizard in self:
+ wizard.invoice_ids = self.env['account.move'].search([
+ ('id', 'in', wizard.purchase_order_ids.mapped('invoice_ids').ids),
+ ('state', '=', 'draft')
+ ])
+
+ def _create_invoices_from_pos(self):
+ """Crée les factures pour chaque PO en utilisant la méthode standard d'Odoo"""
+ created_invoices = self.env['account.move']
+ for po in self.purchase_order_ids:
+ # Vérifie si le PO a des quantités reçues non facturées
+ if not any(line.qty_received > line.qty_invoiced for line in po.order_line):
+ continue
+
+ # Crée la facture en utilisant la méthode standard
+ invoice = po.action_create_invoice()
+ if isinstance(invoice, dict):
+ invoice = self.env['account.move'].browse(invoice.get('res_id'))
+ created_invoices |= invoice
+
+ return created_invoices
+
+ def _merge_invoices(self, invoices):
+ """Fusionne plusieurs factures en une seule"""
+ if not invoices:
+ return False
+
+ # Crée une nouvelle facture
+ merged_invoice = self.env['account.move'].create({
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_id.id,
+ 'invoice_date': fields.Date.context_today(self),
+ 'invoice_origin': ', '.join(self.purchase_order_ids.mapped('name')),
+ })
+
+ # Regroupe les lignes par produit
+ product_lines = {}
+ for invoice in invoices:
+ for line in invoice.invoice_line_ids:
+ key = (line.product_id.id, line.price_unit, tuple(line.tax_ids.ids))
+ if key not in product_lines:
+ product_lines[key] = {
+ 'product_id': line.product_id.id,
+ 'name': line.name,
+ 'quantity': 0,
+ 'price_unit': line.price_unit,
+ 'tax_ids': line.tax_ids.ids,
+ 'purchase_line_ids': [],
+ }
+ product_lines[key]['quantity'] += line.quantity
+ if line.purchase_line_id:
+ product_lines[key]['purchase_line_ids'].append(line.purchase_line_id.id)
+
+ # Crée les lignes dans la facture fusionnée
+ for line_vals in product_lines.values():
+ # Prépare les valeurs pour la ligne de facture
+ invoice_line_vals = {
+ 'move_id': merged_invoice.id,
+ 'product_id': line_vals['product_id'],
+ 'name': line_vals['name'],
+ 'quantity': line_vals['quantity'],
+ 'price_unit': line_vals['price_unit'],
+ 'tax_ids': [(6, 0, line_vals['tax_ids'])],
+ }
+ self.env['account.move.line'].create(invoice_line_vals)
+
+ # Supprime les factures originales
+ invoices.unlink()
+
+ return merged_invoice
+
+ def action_create_bill(self):
+ self.ensure_one()
+ if not self.purchase_order_ids:
+ raise ValidationError(_('Aucun bon de commande trouvé dans ce batch'))
+
+ # Vérifie que tous les PO ont le même fournisseur
+ partners = self.purchase_order_ids.mapped('partner_id')
+ if len(partners) > 1:
+ raise ValidationError(_(
+ 'Les bons de commande sélectionnés ont des fournisseurs différents:\n%s'
+ ) % '\n'.join(['- ' + p.name for p in partners]))
+
+ # Crée les factures individuelles
+ invoices = self._create_invoices_from_pos()
+ if not invoices:
+ raise ValidationError(_('Aucune quantité à facturer trouvée dans les bons de commande'))
+
+ # Ouvre le wizard de fusion
+ return {
+ 'name': _('Fusion des factures'),
+ 'type': 'ir.actions.act_window',
+ 'view_mode': 'form',
+ 'res_model': 'merge.bill.wizard',
+ 'target': 'new',
+ 'context': {
+ 'default_invoice_ids': [(6, 0, invoices.ids)],
+ 'default_partner_id': self.partner_id.id,
+ }
+ }
diff --git a/batch_picking_create_one_bill/wizards/create_bill_wizard_views.xml b/batch_picking_create_one_bill/wizards/create_bill_wizard_views.xml
new file mode 100644
index 0000000..5fd3805
--- /dev/null
+++ b/batch_picking_create_one_bill/wizards/create_bill_wizard_views.xml
@@ -0,0 +1,62 @@
+
+
+
+ create.bill.wizard.form
+ create.bill.wizard
+
+
+
+
+
+
+ Créer Facture Groupée
+ create.bill.wizard
+ form
+ new
+
+ form
+ {}
+
+
diff --git a/batch_picking_create_one_bill/wizards/merge_bill_wizard.py b/batch_picking_create_one_bill/wizards/merge_bill_wizard.py
new file mode 100644
index 0000000..75f7142
--- /dev/null
+++ b/batch_picking_create_one_bill/wizards/merge_bill_wizard.py
@@ -0,0 +1,109 @@
+# -*- coding: utf-8 -*-
+from odoo import models, fields, api, _
+from odoo.exceptions import ValidationError
+
+class MergeBillWizard(models.TransientModel):
+ _name = 'merge.bill.wizard'
+ _description = 'Assistant de fusion des factures'
+
+ invoice_ids = fields.Many2many(
+ 'account.move',
+ string='Factures à fusionner',
+ required=True
+ )
+ partner_id = fields.Many2one(
+ 'res.partner',
+ string='Fournisseur',
+ required=True
+ )
+ merge_invoices = fields.Boolean(
+ string='Fusionner les factures',
+ default=True,
+ help='Si coché, les factures seront fusionnées en une seule'
+ )
+
+ def _merge_invoices(self):
+ """Fusionne plusieurs factures en une seule"""
+ if not self.invoice_ids:
+ return False
+
+ # Collecte les informations des factures existantes
+ invoice_origin = ', '.join(self.invoice_ids.mapped('invoice_origin'))
+ journal = self.env['account.journal'].search([('type', '=', 'purchase')], limit=1)
+ if not journal:
+ raise ValidationError(_('Aucun journal de factures fournisseur trouvé'))
+
+ # Regroupe les lignes par produit
+ product_lines = {}
+ for invoice in self.invoice_ids:
+ for line in invoice.invoice_line_ids:
+ key = (line.product_id.id, line.price_unit, tuple(line.tax_ids.ids))
+ if key not in product_lines:
+ product_lines[key] = {
+ 'product_id': line.product_id.id,
+ 'name': line.name,
+ 'quantity': 0,
+ 'price_unit': line.price_unit,
+ 'tax_ids': line.tax_ids.ids,
+ 'purchase_line_ids': [],
+ }
+ product_lines[key]['quantity'] += line.quantity
+ if line.purchase_line_id:
+ product_lines[key]['purchase_line_ids'].append(line.purchase_line_id.id)
+
+ # Crée les lignes de la nouvelle facture
+ invoice_lines = []
+ for values in product_lines.values():
+ line_vals = {
+ 'product_id': values['product_id'],
+ 'name': values['name'],
+ 'quantity': values['quantity'],
+ 'price_unit': values['price_unit'],
+ 'tax_ids': [(6, 0, values['tax_ids'])],
+ }
+ if values['purchase_line_ids']:
+ line_vals['purchase_line_id'] = values['purchase_line_ids'][0]
+ invoice_lines.append((0, 0, line_vals))
+
+ # Supprime les factures originales
+ self.invoice_ids.unlink()
+
+ # Crée la nouvelle facture
+ vals = {
+ 'move_type': 'in_invoice',
+ 'partner_id': self.partner_id.id,
+ 'invoice_date': fields.Date.context_today(self),
+ 'invoice_origin': invoice_origin,
+ 'journal_id': journal.id,
+ 'state': 'draft',
+ 'invoice_line_ids': invoice_lines,
+ }
+
+ # Crée et valide la facture
+ merged_invoice = self.env['account.move'].create(vals)
+ merged_invoice.action_post()
+
+ return merged_invoice
+
+ def action_process(self):
+ """Traite les factures selon l'option choisie"""
+ self.ensure_one()
+
+ if self.merge_invoices and len(self.invoice_ids) > 1:
+ # Fusionne les factures et affiche la nouvelle facture
+ merged_invoice = self._merge_invoices()
+ if not merged_invoice:
+ raise ValidationError(_('Erreur lors de la fusion des factures'))
+ invoice = merged_invoice
+ else:
+ invoice = self.invoice_ids[0]
+
+ # Retourne la vue de la facture
+ return {
+ 'name': _('Facture fournisseur'),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'account.move',
+ 'res_id': invoice.id,
+ 'view_mode': 'form',
+ 'target': 'current',
+ }
diff --git a/batch_picking_create_one_bill/wizards/merge_bill_wizard_views.xml b/batch_picking_create_one_bill/wizards/merge_bill_wizard_views.xml
new file mode 100644
index 0000000..c343a43
--- /dev/null
+++ b/batch_picking_create_one_bill/wizards/merge_bill_wizard_views.xml
@@ -0,0 +1,27 @@
+
+
+
+ merge.bill.wizard.form
+ merge.bill.wizard
+
+
+
+
+
diff --git a/msg_attachments_to_mail_message/models/ir_attachment.py b/msg_attachments_to_mail_message/models/ir_attachment.py
index 1ff1d6e..c4e473e 100644
--- a/msg_attachments_to_mail_message/models/ir_attachment.py
+++ b/msg_attachments_to_mail_message/models/ir_attachment.py
@@ -447,7 +447,7 @@ class IrAttachment(models.Model):
strip_attachments=False,
thread_id=target_thread_id
)
-
+
# Return the EML content instead of True
return eml_content