batch_picking_create_one_bill
This commit is contained in:
parent
4d06664928
commit
020ed0e99c
13 changed files with 1186 additions and 1 deletions
3
batch_picking_create_one_bill/__init__.py
Normal file
3
batch_picking_create_one_bill/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import models
|
||||
from . import wizards
|
||||
36
batch_picking_create_one_bill/__manifest__.py
Normal file
36
batch_picking_create_one_bill/__manifest__.py
Normal file
|
|
@ -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',
|
||||
}
|
||||
2
batch_picking_create_one_bill/models/__init__.py
Normal file
2
batch_picking_create_one_bill/models/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import stock_picking_batch
|
||||
62
batch_picking_create_one_bill/models/stock_picking_batch.py
Normal file
62
batch_picking_create_one_bill/models/stock_picking_batch.py
Normal file
|
|
@ -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.'
|
||||
)
|
||||
|
|
@ -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
|
||||
|
693
batch_picking_create_one_bill/specifications.md
Normal file
693
batch_picking_create_one_bill/specifications.md
Normal file
|
|
@ -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
|
||||
<record id="view_picking_batch_form_inherit" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.form.inherit</field>
|
||||
<field name="model">stock.picking.batch</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_batch_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="%(action_create_batch_bill_wizard)d"
|
||||
string="Créer Facture"
|
||||
type="action"
|
||||
class="btn-primary"
|
||||
attrs="{'invisible': [('state', '!=', 'done')]}"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button class="oe_stat_button"
|
||||
name="action_view_invoices"
|
||||
type="object"
|
||||
icon="fa-pencil-square-o">
|
||||
<field name="invoice_count" widget="statinfo" string="Factures"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//group[@name='group_misc']" position="inside">
|
||||
<field name="zero_quantity_default"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
#### 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
|
||||
<form>
|
||||
<header>
|
||||
<button name="action_create_bill"
|
||||
string="Créer Facture"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
attrs="{'invisible': [('preview_available', '=', False)]}"/>
|
||||
<button special="cancel" string="Annuler" class="btn-secondary"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="batch_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="currency_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="amount_total"/>
|
||||
<field name="preview_available" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Ordres d'achat">
|
||||
<field name="purchase_order_ids"/>
|
||||
</page>
|
||||
<page string="Prévisualisation des lignes">
|
||||
<field name="preview_line_ids">
|
||||
<tree>
|
||||
<field name="product_id"/>
|
||||
<field name="description"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="price_subtotal"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
```
|
||||
|
||||
##### 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
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Héritage de la vue form du batch picking -->
|
||||
<record id="view_picking_batch_form_inherit" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.form.inherit</field>
|
||||
<field name="model">stock.picking.batch</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_batch_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Ajout du bouton de création de facture -->
|
||||
<xpath expr="//header" position="inside">
|
||||
<button name="%(action_create_batch_bill_wizard)d"
|
||||
string="Créer Facture"
|
||||
type="action"
|
||||
class="btn-primary"
|
||||
attrs="{'invisible': [('state', '!=', 'done')]}"/>
|
||||
</xpath>
|
||||
<!-- Ajout du smart button pour les factures -->
|
||||
<div name="button_box" position="inside">
|
||||
<button class="oe_stat_button"
|
||||
name="action_view_invoices"
|
||||
type="object"
|
||||
icon="fa-pencil-square-o">
|
||||
<field name="invoice_count" widget="statinfo" string="Factures"/>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Ajout de l'option quantités à zéro -->
|
||||
<group name="group_misc" position="inside">
|
||||
<field name="zero_quantity_default"/>
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vue tree des batch pickings -->
|
||||
<record id="view_picking_batch_tree_inherit" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.tree.inherit</field>
|
||||
<field name="model">stock.picking.batch</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_batch_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="state" position="after">
|
||||
<field name="invoice_count"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Vue search des batch pickings -->
|
||||
<record id="view_picking_batch_search_inherit" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.search.inherit</field>
|
||||
<field name="model">stock.picking.batch</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_batch_search"/>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="to_process" position="after">
|
||||
<filter string="Non facturé"
|
||||
name="not_invoiced"
|
||||
domain="[('invoice_count', '=', 0)]"/>
|
||||
<filter string="Facturé"
|
||||
name="invoiced"
|
||||
domain="[('invoice_count', '>', 0)]"/>
|
||||
</filter>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
```
|
||||
|
||||
##### 2. Vue formulaire de la facture (`account_move_views.xml`)
|
||||
```xml
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_move_form_inherit" model="ir.ui.view">
|
||||
<field name="name">account.move.form.inherit</field>
|
||||
<field name="model">account.move</field>
|
||||
<field name="inherit_id" ref="account.view_move_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Ajout des références aux batch pickings -->
|
||||
<xpath expr="//field[@name='invoice_origin']" position="after">
|
||||
<field name="batch_picking_ids"
|
||||
widget="many2many_tags"
|
||||
readonly="1"
|
||||
attrs="{'invisible': [('move_type', '!=', 'in_invoice')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
```
|
||||
|
||||
#### Actions et menus
|
||||
|
||||
##### Actions du wizard
|
||||
```xml
|
||||
<record id="action_create_batch_bill_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Créer Facture Fournisseur</field>
|
||||
<field name="res_model">batch.picking.create.bill.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="model_stock_picking_batch"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
#### Sécurité et droits d'accès
|
||||
|
||||
##### Groupes et droits
|
||||
```xml
|
||||
<record id="group_batch_invoice" model="res.groups">
|
||||
<field name="name">Création de factures depuis batch picking</field>
|
||||
<field name="category_id" ref="base.module_category_inventory"/>
|
||||
<field name="implied_ids" eval="[(4, ref('account.group_account_invoice'))]"/>
|
||||
</record>
|
||||
```
|
||||
|
||||
##### 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
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_picking_batch_form_inherit" model="ir.ui.view">
|
||||
<field name="name">stock.picking.batch.form.inherit</field>
|
||||
<field name="model">stock.picking.batch</field>
|
||||
<field name="inherit_id" ref="stock_picking_batch.stock_picking_batch_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//header" position="inside">
|
||||
<field name="id" invisible="1"/>
|
||||
<button name="%(batch_picking_create_one_bill.action_create_batch_bill_wizard)d"
|
||||
string="Créer Facture"
|
||||
type="action"
|
||||
class="btn-primary"
|
||||
context="{'default_batch_id': id}"
|
||||
invisible="state != 'done'"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<button class="oe_stat_button"
|
||||
name="action_view_invoices"
|
||||
type="object"
|
||||
icon="fa-pencil-square-o">
|
||||
<field name="invoice_count" widget="statinfo" string="Factures"/>
|
||||
</button>
|
||||
</xpath>
|
||||
<xpath expr="//group[@id='batch_delivery_data']" position="inside">
|
||||
<field name="zero_quantity_default"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
3
batch_picking_create_one_bill/wizards/__init__.py
Normal file
3
batch_picking_create_one_bill/wizards/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from . import create_bill_wizard
|
||||
from . import merge_bill_wizard
|
||||
155
batch_picking_create_one_bill/wizards/create_bill_wizard.py
Normal file
155
batch_picking_create_one_bill/wizards/create_bill_wizard.py
Normal file
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_create_bill_wizard_form" model="ir.ui.view">
|
||||
<field name="name">create.bill.wizard.form</field>
|
||||
<field name="model">create.bill.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Créer une facture groupée">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="batch_id" readonly="1"/>
|
||||
<field name="partner_id" readonly="1"/>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Bons de commande" name="purchase_orders">
|
||||
<field
|
||||
name="purchase_order_ids"
|
||||
readonly="1"
|
||||
widget="many2many"
|
||||
options="{'no_create': True}">
|
||||
</field>
|
||||
</page>
|
||||
<page string="Factures temporaires" name="temp_invoices">
|
||||
<field
|
||||
name="invoice_ids"
|
||||
readonly="1"
|
||||
widget="many2many"
|
||||
options="{'no_create': True}"
|
||||
invisible="not invoice_ids">
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
<group>
|
||||
<field
|
||||
name="merged_invoice_id"
|
||||
readonly="1"
|
||||
invisible="not merged_invoice_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button name="action_create_bill"
|
||||
string="Créer et Fusionner les Factures"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
invisible="merged_invoice_id"/>
|
||||
<button special="cancel"
|
||||
string="Fermer"
|
||||
class="btn-secondary"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_create_batch_bill_wizard" model="ir.actions.act_window">
|
||||
<field name="name">Créer Facture Groupée</field>
|
||||
<field name="res_model">create.bill.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="stock_picking_batch.model_stock_picking_batch"/>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="context">{}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
109
batch_picking_create_one_bill/wizards/merge_bill_wizard.py
Normal file
109
batch_picking_create_one_bill/wizards/merge_bill_wizard.py
Normal file
|
|
@ -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',
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_merge_bill_wizard_form" model="ir.ui.view">
|
||||
<field name="name">merge.bill.wizard.form</field>
|
||||
<field name="model">merge.bill.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Fusion des factures">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1><field name="partner_id" readonly="1"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="merge_invoices"/>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Factures à fusionner"/>
|
||||
<field name="invoice_ids" readonly="1" nolabel="1" options="{'no_create': True}" context="{'tree_view_ref': 'account.view_in_invoice_tree'}"/>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button string="Confirmer" name="action_process" type="object" class="btn-primary"/>
|
||||
<button string="Annuler" class="btn-secondary" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue