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