batch_picking_create_one_bill: major rework

Fixes an issue where creating the bills from the purchase orders could
create a bill with quantities vastly different from the received batch
quantities. This would happen when the purchase orders had lines with
quantities to invoice that did not match the receipt quantities.

Instead of creating the bills from the POs, we now generate the bill
and its line values directly from the quantities on the move lines
related to the batch.
This commit is contained in:
Marc Durepos 2025-03-12 17:49:37 -04:00
parent d16304e57b
commit 99e3ed03a7
10 changed files with 246 additions and 555 deletions

View file

@ -24,9 +24,6 @@
"account_reports",
],
"data": [
"security/ir.model.access.csv",
"wizard/create_bill_wizard_views.xml",
"wizard/merge_bill_wizard_views.xml",
"wizard/stock_picking_to_batch_views.xml",
"views/stock_picking_batch_views.xml",
],

View file

@ -1,11 +1,22 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo import models, fields, api, Command, _
from odoo.exceptions import ValidationError
class StockPickingBatch(models.Model):
_inherit = "stock.picking.batch"
purchase_order_ids = fields.Many2many(
"purchase.order",
string="Bons de commande",
compute="_compute_purchase_orders",
store=True,
)
partner_ids = fields.Many2many(
"res.partner", string="Fournisseur", compute="_compute_partner", store=True
)
@api.model_create_multi
def create(self, vals_list):
# Handle zero_quantity_default from context if not explicitly set in vals
@ -37,16 +48,30 @@ class StockPickingBatch(models.Model):
compute_sudo=True,
)
# Field computation methods
@api.depends("picking_ids.move_ids.purchase_line_id.order_id")
def _compute_purchase_orders(self):
for batch in self:
batch.purchase_order_ids = (
batch.picking_ids.move_ids.purchase_line_id.order_id
)
@api.depends("purchase_order_ids")
def _compute_partner(self):
for wizard in self:
wizard.partner_ids = wizard.purchase_order_ids.mapped("partner_id")
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")
@api.depends("move_line_ids.move_id.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
purchase_orders = batch.move_line_ids.move_id.purchase_line_id.order_id
batch.invoice_ids = purchase_orders.invoice_ids
@api.depends("invoice_ids")
@ -54,6 +79,8 @@ class StockPickingBatch(models.Model):
for batch in self:
batch.invoice_count = len(batch.invoice_ids)
# Actions
def action_view_invoices(self):
self.ensure_one()
action = self.env["ir.actions.act_window"]._for_xml_id(
@ -66,17 +93,72 @@ class StockPickingBatch(models.Model):
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."
)
def action_confirm(self):
"""Override to set zero quantity on move lines at confirmation of the batch"""
res = super().action_confirm()
self.filtered("zero_quantity_default").move_line_ids.write({"quantity": 0})
return res
def action_create_bill(self):
self.ensure_one()
if not self.purchase_order_ids:
raise ValidationError(_("No purchase orders found in this batch."))
if len(self.partner_ids) > 1:
raise ValidationError(_("The batch must have only one supplier."))
bill = self.env["account.move"].create(self._get_bill_values())
return self._get_view_bill_action(bill)
# Helpers
def _get_view_bill_action(self, bill):
action = self.env["ir.actions.act_window"]._for_xml_id(
"account.action_move_in_invoice_type"
)
action["views"] = [(False, "form")]
action["res_id"] = self.invoice_ids[-1].id
return action
def _get_currency_id(self):
currency_id = self.purchase_order_ids.mapped("currency_id")
if len(currency_id) > 1:
raise UserError(_("The selected receipts do not have the same currency."))
return currency_id.id
def _get_bill_values(self):
"""Build a dictionary of the values for the vendor bill to create."""
company_id = self.company_id.id
partner_id = self.partner_ids.id
invoice_date = self.scheduled_date
invoice_origin = ", ".join(self.purchase_order_ids.mapped("name"))
move_line_ids = self._get_line_values()
currency_id = self._get_currency_id()
return {
"company_id": company_id,
"partner_id": partner_id,
"move_type": "in_invoice",
"invoice_date": invoice_date,
"invoice_origin": invoice_origin,
"currency_id": currency_id,
"line_ids": move_line_ids,
}
def _get_line_values(self):
"""For each of the stock.move.line in the batch, build a dictionary of values for the invoice line to match."""
line_vals = []
for move_line in self.move_line_ids:
purchase_line_id = move_line.move_id.purchase_line_id
line_vals.append(
Command.create(
{
"product_id": move_line.product_id.id,
"quantity": move_line.quantity,
"price_unit": purchase_line_id.price_unit,
"discount": purchase_line_id.discount,
"purchase_line_id": purchase_line_id.id,
}
)
)
return line_vals

View file

@ -1,3 +0,0 @@
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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_create_bill_wizard create_bill_wizard_access model_create_bill_wizard base.group_user 1 1 1 0
3 access_merge_bill_wizard merge_bill_wizard_access model_merge_bill_wizard base.group_user 1 1 1 0

View file

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from odoo.tests import tagged
from odoo.tests.common import TransactionCase
from odoo.exceptions import AccessError
from odoo.exceptions import AccessError, ValidationError
from odoo import fields
@ -118,201 +118,169 @@ class TestBatchPickingBill(TransactionCase):
for move in picking.move_ids:
move.quantity = move.product_qty
def test_01_warehouse_user_flow(self):
"""Test the complete flow with warehouse user"""
# Switch to warehouse user
self.env = self.env(user=self.warehouse_user)
# Create batch picking
batch = self.env["stock.picking.batch"].create(
# Create a batch picking with the pickings
cls.batch = cls.env["stock.picking.batch"].create(
{
"picking_ids": [
(6, 0, (self.po1.picking_ids + self.po2.picking_ids).ids)
],
"zero_quantity_default": True,
"name": "Test Batch",
"company_id": cls.env.company.id,
"scheduled_date": fields.Date.today(),
"picking_ids": [(6, 0, (cls.po1 + cls.po2).picking_ids.ids)],
"zero_quantity_default": False,
}
)
# Check batch was created successfully
self.assertTrue(batch, "Batch picking should be created")
self.assertEqual(len(batch.picking_ids), 2, "Batch should have 2 pickings")
# Process the pickings
for picking in batch.picking_ids:
for move_line in picking.move_line_ids:
move_line.quantity = move_line.move_id.product_qty
# Validate batch
batch.action_done()
self.assertEqual(batch.state, "done", "Batch should be done")
# Try to create bill (should work)
wizard = (
self.env["create.bill.wizard"]
.with_context(default_batch_id=batch.id)
.create({})
)
# Execute the create bill action
action = wizard.action_create_bill()
self.assertTrue(action, "Should get action to merge bills")
self.assertEqual(
action["res_model"], "merge.bill.wizard", "Should open merge bill wizard"
)
def test_02_bill_creation_and_access(self):
"""Test bill creation and access rights"""
# Create and process batch as warehouse user
self.env = self.env(user=self.warehouse_user)
batch = self.env["stock.picking.batch"].create(
# Create a batch with multiple vendors for testing validation
cls.vendor2 = cls.env["res.partner"].create(
{
"picking_ids": [
(6, 0, (self.po1.picking_ids + self.po2.picking_ids).ids)
],
"zero_quantity_default": True,
}
)
# Process pickings as warehouse user
for picking in batch.picking_ids:
for move_line in picking.move_line_ids:
move_line.quantity = move_line.move_id.product_qty
batch.action_done()
# Create bill as warehouse user
wizard = (
self.env["create.bill.wizard"]
.with_context(default_batch_id=batch.id)
.create({})
)
action = wizard.action_create_bill()
# Execute merge wizard as warehouse user
merge_wizard = (
self.env["merge.bill.wizard"].with_context(**action["context"]).create({})
)
draft_invoices = merge_wizard.invoice_ids
self.assertTrue(draft_invoices, "Invoices should be created")
bill = self.env["account.move"].browse(merge_wizard.action_process()["res_id"])
self.assertTrue(bill, "Bill should be created")
self.assertEqual(
bill.mapped("state"),
["draft"],
"Bill should be draft",
)
# Validate invoice as accountant (should work)
bill.with_user(self.accountant).invoice_date = fields.Date.today()
bill.with_user(self.accountant).action_post()
self.assertEqual(
bill.mapped("state"),
["posted"],
"Bill should be posted",
)
def test_03_multi_vendor_constraint(self):
"""Test constraint preventing batch with multiple vendors"""
# Create another vendor and PO
other_vendor = self.env["res.partner"].create(
{
"name": "Other Vendor",
"email": "other@test.com",
"name": "Second Vendor",
"email": "vendor2@test.com",
"supplier_rank": 1,
}
)
other_po = self.env["purchase.order"].create(
# Create a purchase order with a different vendor
po_vals_vendor2 = {
"partner_id": cls.vendor2.id,
"order_line": [
(
0,
0,
{
"product_id": cls.product_a.id,
"name": cls.product_a.name,
"product_qty": 2.0,
"product_uom": cls.product_a.uom_po_id.id,
"price_unit": 100.0,
},
),
],
}
cls.po3 = cls.env["purchase.order"].create(po_vals_vendor2)
cls.po3.button_confirm()
# Process the picking for the second vendor
for picking in cls.po3.picking_ids:
for move in picking.move_ids:
move.quantity = move.product_qty
# Create a batch with multiple vendors
cls.multi_vendor_batch = cls.env["stock.picking.batch"].create(
{
"partner_id": other_vendor.id,
"order_line": [
(
0,
0,
{
"product_id": self.product_a.id,
"product_qty": 1,
"price_unit": 100,
},
)
],
}
)
other_po.button_confirm()
# Try to create batch with multiple vendors (should fail)
with self.assertRaises(Exception):
self.env["stock.picking.batch"].create(
{
"picking_ids": [
(6, 0, (self.po1.picking_ids + other_po.picking_ids).ids)
],
}
)
def test_zero_quantity_default_from_wizard(self):
"""Test that zero_quantity_default is properly set when creating a batch from wizard"""
# Switch to warehouse user like in the parent test
self.env = self.env(user=self.warehouse_user)
pickings = (self.po1 + self.po2).picking_ids
self.assertTrue(pickings, "Purchase orders should have created pickings")
# Create batch through wizard
wizard = (
self.env["stock.picking.to.batch"]
.with_context(active_ids=pickings.ids, active_model="stock.picking")
.create(
{
"mode": "new",
"zero_quantity_default": True,
}
)
)
result = wizard.attach_pickings()
batch = self.env["stock.picking.batch"].browse(result["res_id"])
# Check that zero_quantity_default was properly set on the batch
self.assertTrue(
batch.zero_quantity_default,
"zero_quantity_default should be True on the created batch",
)
# Check that all move lines have quantity 0
for move_line in batch.move_line_ids:
self.assertEqual(
move_line.quantity,
0.0,
f"Move line for {move_line.product_id.name} should have quantity 0",
)
def test_zero_quantity_default_from_picking(self):
"""Test that zero_quantity_default works when adding picking to existing batch"""
# Switch to warehouse user like in the parent test
self.env = self.env(user=self.warehouse_user)
pickings = (self.po1 + self.po2).picking_ids
# Create batch directly
batch = self.env["stock.picking.batch"].create(
{
"zero_quantity_default": True,
"name": "Multi Vendor Batch",
"company_id": cls.env.company.id,
"scheduled_date": fields.Date.today(),
"picking_ids": [(6, 0, (cls.po1 + cls.po3).picking_ids.ids)],
"zero_quantity_default": False,
}
)
# Add pickings to batch
pickings.write({"batch_id": batch.id})
def test_action_create_bill_success(self):
"""Test that a bill is successfully created from a batch picking"""
# Ensure the batch has no invoices initially
self.assertEqual(len(self.batch.invoice_ids), 0)
self.assertEqual(self.batch.invoice_count, 0)
# Confirm the batch to get the move lines set up
batch.action_confirm()
# Execute the action to create a bill
action = self.batch.action_create_bill()
# Check that all move lines have quantity 0
for move_line in batch.move_line_ids:
self.assertEqual(
move_line.quantity,
0.0,
f"Move line for {move_line.product_id.name} should have quantity 0",
# Verify that an invoice was created
self.assertEqual(len(self.batch.invoice_ids), 1)
self.assertEqual(self.batch.invoice_count, 1)
# Verify the action returns the correct view
self.assertEqual(action.get("res_id"), self.batch.invoice_ids[-1].id)
self.assertEqual(action.get("views")[0][1], "form")
# Verify the bill content
bill = self.batch.invoice_ids[-1]
self.assertEqual(bill.partner_id, self.vendor)
self.assertEqual(bill.move_type, "in_invoice")
self.assertEqual(bill.invoice_date, self.batch.scheduled_date.date())
# Verify that the bill contains the correct lines
expected_products = self.batch.move_line_ids.mapped("product_id")
bill_products = bill.invoice_line_ids.mapped("product_id")
self.assertEqual(set(bill_products.ids), set(expected_products.ids))
# Verify the quantities match
for line in bill.invoice_line_ids:
# Find matching move lines
move_lines = self.batch.move_line_ids.filtered(
lambda ml: ml.product_id == line.product_id
)
self.assertEqual(line.quantity, sum(move_lines.mapped("quantity")))
# Verify price from purchase order line
purchase_line = move_lines[0].move_id.purchase_line_id
self.assertEqual(line.price_unit, purchase_line.price_unit)
def test_action_create_bill_no_purchase_orders(self):
"""Test that an error is raised when there are no purchase orders"""
# Create an empty batch
empty_batch = self.env["stock.picking.batch"].create(
{
"name": "Empty Batch",
"company_id": self.env.company.id,
"scheduled_date": fields.Date.today(),
}
)
# Try to create a bill and expect a ValidationError
with self.assertRaises(ValidationError):
empty_batch.action_create_bill()
def test_action_create_bill_multiple_vendors(self):
"""Test that an error is raised when there are multiple vendors"""
# Try to create a bill from a batch with multiple vendors
with self.assertRaises(ValidationError):
self.multi_vendor_batch.action_create_bill()
def test_action_create_bill_access_rights(self):
"""Test access rights for creating bills from batch pickings"""
# Test with warehouse user (should have access)
self.batch.with_user(self.warehouse_user).action_create_bill()
# Verify that a bill was created
self.assertEqual(len(self.batch.invoice_ids), 1)
# Create a user without invoice creation rights
no_invoice_user = self.env["res.users"].create(
{
"name": "No Invoice User",
"login": "no_invoice_user",
"email": "no_invoice@test.com",
"groups_id": [(6, 0, [self.env.ref("stock.group_stock_user").id])],
}
)
# Create a new batch for testing with the limited user
new_batch = self.env["stock.picking.batch"].create(
{
"name": "New Test Batch",
"company_id": self.env.company.id,
"scheduled_date": fields.Date.today(),
"picking_ids": [(6, 0, self.po2.picking_ids.ids)],
}
)
# Try to create a bill with a user that doesn't have invoice creation rights
with self.assertRaises(AccessError):
new_batch.with_user(no_invoice_user).action_create_bill()
def test_bill_values_calculation(self):
"""Test the helper methods that calculate bill values"""
# Test _get_bill_values method
bill_values = self.batch._get_bill_values()
self.assertEqual(bill_values["company_id"], self.batch.company_id.id)
self.assertEqual(bill_values["partner_id"], self.batch.partner_ids.id)
self.assertEqual(bill_values["move_type"], "in_invoice")
self.assertEqual(bill_values["invoice_date"], self.batch.scheduled_date)
# The invoice_origin should contain the purchase order names
for po_name in self.batch.purchase_order_ids.mapped("name"):
self.assertIn(po_name, bill_values["invoice_origin"])
# Test currency consistency
currency_id = self.batch._get_currency_id()
self.assertEqual(currency_id, self.batch.purchase_order_ids[0].currency_id.id)

View file

@ -7,7 +7,7 @@
<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'"/>
<button name="action_create_bill" string="Create Bill" type="object" 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">

View file

@ -1,4 +1,2 @@
# -*- coding: utf-8 -*-
from . import create_bill_wizard
from . import merge_bill_wizard
from . import stock_picking_to_batch

View file

@ -1,155 +0,0 @@
# -*- 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,
}
}

View file

@ -1,62 +0,0 @@
<?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>

View file

@ -1,107 +0,0 @@
# -*- 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 la facture
merged_invoice = self.env["account.move"].create(vals)
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",
}

View file

@ -1,27 +0,0 @@
<?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>