diff --git a/l10n_it_delivery_note/README.rst b/l10n_it_delivery_note/README.rst index 7f3277f4de77..e60b16a4af18 100644 --- a/l10n_it_delivery_note/README.rst +++ b/l10n_it_delivery_note/README.rst @@ -1,7 +1,3 @@ -.. image:: https://odoo-community.org/readme-banner-image - :target: https://odoo-community.org/get-involved?utm_source=readme - :alt: Odoo Community Association - ============================ ITA - Documento di trasporto ============================ @@ -17,7 +13,7 @@ ITA - Documento di trasporto .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png :target: https://odoo-community.org/page/development-status :alt: Beta -.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fl10n--italy-lightgray.png?logo=github @@ -37,8 +33,16 @@ ITA - Documento di trasporto This module manage the Italian DDT (Delivery note). From a picking is possible to generate a Delivery Note and group more -picking in one delivery note. It's also possible to invoice from the -delivery note form. +picking in one delivery note. It's also possible to invoice directly +from the delivery note form, with configurable options to use DN data +(product names, prices) instead of sale order data when generating +invoices. + +This is particularly useful when: + +- Products are substituted at delivery time +- Prices are negotiated during delivery +- Detailed descriptions need to be added in the DN This module is alternative to ``l10n_it_ddt``, it follows the Odoo way to process sale orders, pickings and invoices. @@ -48,15 +52,24 @@ installed together. There are two available settings: -- Base (default): one picking, one DN. -- Advanced: more picking in one DN. +- Base (default): one picking, one DN. +- Advanced: more picking in one DN. **Italiano** Questo modulo consente di gestire i DDT. Da un prelievo è possibile generare un DDT e raggruppare più prelievi in -un DDT. È anche possibile fatturare dalla scheda del DDT. +un DDT. È anche possibile fatturare direttamente dalla scheda del DDT, +con opzioni configurabili per utilizzare i dati del DDT (nomi prodotti, +prezzi) invece dei dati dell'ordine di vendita nella generazione delle +fatture. + +Questo è particolarmente utile quando: + +- I prodotti vengono sostituiti al momento della consegna +- I prezzi vengono negoziati durante la consegna +- È necessario aggiungere descrizioni dettagliate nel DDT Questo modulo è un alternativa al modulo ``l10n_it_ddt``, segue la modalità Odoo di gestire ordini di vendita, prelievi e fatture. @@ -66,8 +79,8 @@ Non è possibile avere installati contemporaneamente ``l10n_it_ddt`` e Ci sono due impostazioni possibili. -- Base (predefinita): un prelievo, un DDT. -- Avanzata: più prelievi in un DDT. +- Base (predefinita): un prelievo, un DDT. +- Avanzata: più prelievi in un DDT. **Table of contents** @@ -94,17 +107,32 @@ To configure this module, go to: Checking 'Display Delivery Method in Delivery Note Report' enables in report field 'Delivery Method'. + **Invoice Generation from Delivery Notes:** + + - Checking 'Use Delivery Note Product Name in Invoice' makes the + invoice use the product description from the delivery note instead + of the sale order. This is useful when you modify product + descriptions in the DN to reflect what was actually delivered. + + - Checking 'Use Delivery Note Price Unit in Invoice' makes the + invoice use the unit price from the delivery note instead of the + sale order. This is useful for price negotiations at delivery time + or when substituting products with different prices. + 2. *Inventory → Configuration → Warehouse Management → Delivery Note Types* In delivery note type you can specify if the product price have to be printed in the delivery note report/slip. - - *Inventory → Configuration → Delivery Notes → Conditions of - Transport* - - *Inventory → Configuration → Delivery Notes → Appearances of Goods* - - *Inventory → Configuration → Delivery Notes → Reasons of Transport* - - *Inventory → Configuration → Delivery Notes → Methods of Transport* + - *Inventory → Configuration → Delivery Notes → Conditions of + Transport* + - *Inventory → Configuration → Delivery Notes → Appearances of + Goods* + - *Inventory → Configuration → Delivery Notes → Reasons of + Transport* + - *Inventory → Configuration → Delivery Notes → Methods of + Transport* 3. *Settings → User & Companies → Users* @@ -142,10 +170,10 @@ Funzionalità avanzata Vengono attivate varie funzionalità aggiuntive: -- più prelievi per un DDT -- selezione multipla di prelievi e generazione dei DDT -- aggiunta righe nota e righe sezione descrittive. -- lista dei DDT. +- più prelievi per un DDT +- selezione multipla di prelievi e generazione dei DDT +- aggiunta righe nota e righe sezione descrittive. +- lista dei DDT. Il report DDT stampa in righe aggiuntive i lotti/seriali e le scadenze del prodotto. @@ -157,6 +185,50 @@ permessi dell'utente. Le fatture generate dai DDT contengono i riferimenti al DDT stesso nelle righe nota. +Fatturazione da DN +------------------ + +E' possibile creare una fattura selezionando una o più DN dello stesso +partner dalla tree view tramite il wizard "crea fattura". Si può +scegliere se includere anche i servizi non ancora fatturati dell'ordine +di vendita correlato o considerare solo le righe nei DN. In maniera +predefinita vengono dedotti gli eventuali anticipi fatturati. + +Utilizzo dei dati dal DDT nelle fatture +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Dalle impostazioni (*Inventario → Configurazione → Impostazioni - +Documenti di Trasporto*) è possibile configurare se la fattura deve +utilizzare i dati dal DDT anziché dall'ordine di vendita: + +- **Usa Nome Prodotto da DDT nelle Fatture**: Quando attivo, la + descrizione del prodotto nella fattura viene presa dal DDT invece che + dall'ordine di vendita. Utile quando si modificano le descrizioni nel + DDT per riflettere ciò che è stato effettivamente consegnato. + +- **Usa Prezzo Unitario da DDT nelle Fatture**: Quando attivo, il + prezzo unitario nella fattura viene preso dal DDT invece che + dall'ordine di vendita. Utile per negoziazioni di prezzo al momento + della consegna o quando si sostituiscono prodotti con prezzi diversi. + +**Esempi pratici:** + +1. **Prodotto sostituito**: Se ordini "Scrivania Modello A - €500" ma + consegni "Scrivania Modello B - €450", modificando DDT e attivando + entrambe le opzioni, la fattura rifletterà automaticamente il + prodotto e prezzo reale consegnato. + +2. **Negoziazione alla consegna**: Se il cliente nota un difetto e + negoziate uno sconto, modificando il prezzo nel DDT con l'opzione + attiva, la fattura sarà corretta senza bisogno di note credito. + +3. **Descrizioni dettagliate**: Se nel DDT specifichi "3 sacchi cemento + CEM II, 5 pannelli isolanti" invece di "Materiale edile vario", con + l'opzione attiva la fattura mostrerà i dettagli completi. + +**Nota**: Queste opzioni sono disabilitate per default per mantenere la +retrocompatibilità. Attivarle solo se si desidera questo comportamento. + Accesso da portale ------------------ @@ -187,39 +259,39 @@ Authors Contributors ------------ -- Riccardo Bellanova +- Riccardo Bellanova -- Matteo Bilotta +- Matteo Bilotta -- Giuseppe Borruso +- Giuseppe Borruso -- Marco Calcagni +- Marco Calcagni -- Marco Colombo +- Marco Colombo -- Gianmarco Conte +- Gianmarco Conte -- Letizia Freda +- Letizia Freda -- Andrea Piovesana +- Andrea Piovesana -- Alex Comba +- Alex Comba -- `Ooops `__: +- `Ooops `__: - - Giovanni Serra - - Foresti Francesco + - Giovanni Serra + - Foresti Francesco -- Nextev Srl +- Nextev Srl -- `PyTech-SRL `__: +- `PyTech-SRL `__: - - Alessandro Uffreduzzi - - Sebastiano Picchi + - Alessandro Uffreduzzi + - Sebastiano Picchi -- `Aion Tech `__: +- `Aion Tech `__: - - Simone Rubino + - Simone Rubino Maintainers ----------- diff --git a/l10n_it_delivery_note/models/account_invoice.py b/l10n_it_delivery_note/models/account_invoice.py index 335a5a5e2a84..29e3123ecd24 100644 --- a/l10n_it_delivery_note/models/account_invoice.py +++ b/l10n_it_delivery_note/models/account_invoice.py @@ -4,6 +4,7 @@ # @author: Gianmarco Conte # Copyright (c) 2019, Link IT Europe Srl # @author: Matteo Bilotta +# Copyright (c) 2024, Nextev Srl # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import fields, models @@ -147,14 +148,11 @@ def update_delivery_note_lines(self): def unlink(self): # Ripristino il valore delle delivery note # per poterle rifatturare - inv_lines = self.mapped("invoice_line_ids") - all_dnls = inv_lines.mapped("sale_line_ids").mapped("delivery_note_line_ids") - inv_dnls = self.mapped("delivery_note_ids").mapped("line_ids") - dnls_to_unlink = all_dnls & inv_dnls + inv_dnls = self.invoice_line_ids.delivery_note_line_id res = super().unlink() - dnls_to_unlink.sync_invoice_status() - dnls_to_unlink.mapped("delivery_note_id")._compute_invoice_status() - for dn in dnls_to_unlink.mapped("delivery_note_id"): + inv_dnls.sync_invoice_status() + inv_dnls.delivery_note_id._compute_invoice_status() + for dn in inv_dnls.delivery_note_id: dn.state = "confirm" return res diff --git a/l10n_it_delivery_note/models/account_invoice_line.py b/l10n_it_delivery_note/models/account_invoice_line.py index f13171454345..54e40ae9371d 100644 --- a/l10n_it_delivery_note/models/account_invoice_line.py +++ b/l10n_it_delivery_note/models/account_invoice_line.py @@ -12,4 +12,10 @@ class AccountInvoiceLine(models.Model): delivery_note_id = fields.Many2one( "stock.delivery.note", string="Delivery Note", readonly=True, copy=False ) + delivery_note_line_id = fields.Many2one( + "stock.delivery.note.line", + string="Delivery Note Line", + readonly=True, + copy=False, + ) note_dn = fields.Boolean(string="Note DN") diff --git a/l10n_it_delivery_note/models/res_company.py b/l10n_it_delivery_note/models/res_company.py index ec55b7355f89..47d1cd5a10a2 100644 --- a/l10n_it_delivery_note/models/res_company.py +++ b/l10n_it_delivery_note/models/res_company.py @@ -7,6 +7,15 @@ class ResCompany(models.Model): _inherit = "res.company" + use_dn_product_name_in_invoice = fields.Boolean( + string="Use Delivery Note Product Name in Invoice", + default=False, + ) + use_dn_price_unit_in_invoice = fields.Boolean( + string="Use Delivery Note Price Unit in Invoice", + default=False, + ) + display_ref_order_dn_report = fields.Boolean( "Display Ref. Order in Delivery Note Report", default=False, diff --git a/l10n_it_delivery_note/models/res_config_settings.py b/l10n_it_delivery_note/models/res_config_settings.py index f7df6fed79e6..016e9dd01ef4 100644 --- a/l10n_it_delivery_note/models/res_config_settings.py +++ b/l10n_it_delivery_note/models/res_config_settings.py @@ -24,6 +24,17 @@ def _default_virtual_locations_root(self): config_parameter="stock.location.virtual_root", ) + use_dn_product_name_in_invoice = fields.Boolean( + string="Use Delivery Note Product Name in Invoice", + related="company_id.use_dn_product_name_in_invoice", + readonly=False, + ) + use_dn_price_unit_in_invoice = fields.Boolean( + string="Use Delivery Note Price Unit in Invoice", + related="company_id.use_dn_price_unit_in_invoice", + readonly=False, + ) + display_ref_order_dn_report = fields.Boolean( string="Display Ref. Order in Delivery Note Report", related="company_id.display_ref_order_dn_report", diff --git a/l10n_it_delivery_note/models/sale_order.py b/l10n_it_delivery_note/models/sale_order.py index 6e38f6d95423..f291985c29b1 100644 --- a/l10n_it_delivery_note/models/sale_order.py +++ b/l10n_it_delivery_note/models/sale_order.py @@ -122,12 +122,21 @@ def _generate_delivery_note_lines(self, invoice_ids): invoices.update_delivery_note_lines() def _create_invoices(self, grouped=False, final=False, date=None): - invoice_ids = super()._create_invoices(grouped=grouped, final=final, date=date) - - self._assign_delivery_notes_invoices(invoice_ids.ids) - self._generate_delivery_note_lines(invoice_ids.ids) - - return invoice_ids + # TODO: Consider adding an 'invoice_policy' selection field on sale.order + # to allow choosing between 'delivered' (standard) and 'delivery_note' modes. + # In 'delivery_note' mode: + # - invoice_status should be computed based on delivery notes, not delivered qty + # - "Create Invoice" button should appear only when new delivery notes exist + # This would avoid bypassing standard invoicing when delivery notes exist + # but user wants to invoice directly (e.g., for multi-delivery orders). + if any(dn.invoice_status == "to invoice" for dn in self.delivery_note_ids): + self.delivery_note_ids.action_invoice( + invoice_method="service", final=final, sale_orders=self, grouped=grouped + ) + # Assign delivery notes to invoices and update statuses + self._assign_delivery_notes_invoices(self.invoice_ids.ids) + return self.invoice_ids + return super()._create_invoices(grouped=grouped, final=final, date=date) def goto_delivery_notes(self, **kwargs): delivery_notes = self.mapped("delivery_note_ids") diff --git a/l10n_it_delivery_note/models/stock_delivery_note.py b/l10n_it_delivery_note/models/stock_delivery_note.py index e6b5e5cab48d..28450d76511b 100644 --- a/l10n_it_delivery_note/models/stock_delivery_note.py +++ b/l10n_it_delivery_note/models/stock_delivery_note.py @@ -1,12 +1,14 @@ # Copyright (c) 2019, Link IT Europe Srl # @author: Matteo Bilotta # Copyright 2023 Simone Rubino - Aion Tech +# Copyright (c) 2024, Nextev Srl # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). import datetime +from itertools import groupby -from odoo import api, fields, models -from odoo.exceptions import UserError +from odoo import Command, api, fields, models +from odoo.exceptions import AccessError, UserError from ..mixins.delivery_mixin import ( _default_volume_uom, @@ -667,6 +669,21 @@ def action_confirm(self): else: note._action_confirm() + def action_invoice_wizard(self): + self.ensure_one() + + return { + "name": self.env._("Create invoices"), + "type": "ir.actions.act_window", + "res_model": "stock.delivery.note.invoice.wizard", + "view_mode": "form", + "target": "new", + "context": { + "active_ids": self.ids, + "active_model": "stock.delivery.note", + }, + } + def _check_delivery_notes_before_invoicing(self): for delivery_note_id in self: if not delivery_note_id.sale_ids: @@ -674,127 +691,340 @@ def _check_delivery_notes_before_invoicing(self): delivery_note_id.env._("%s hasn't sale order!") % delivery_note_id.display_name ) - if ( - len( - delivery_note_id.mapped("sale_ids.picking_ids.picking_type_id.code") + + def _prepare_invoice(self, sale_orders): + """Create invoice header data""" + invoice_vals = ( + sale_orders[0] + .with_company(sale_orders[0].company_id) + .with_context(lang=sale_orders[0].partner_invoice_id.lang) + ._prepare_invoice() + ) + return invoice_vals + + def _build_dn_map(self, sale_orders): + """Map delivery note lines by sale_line_id.""" + dn_map = {} + for dn in self.sorted(key=lambda d: (d.date, d.name)): + for line in dn.line_ids: + if line.sale_line_id and ( + not sale_orders or line.sale_line_id.order_id in sale_orders + ): + dn_map.setdefault(line.sale_line_id.id, []).append((dn, line)) + return dn_map + + def _append_dn_linked_lines(self, vals_list, dn_map, sequence, current_dn): + """Append product lines coming from delivery notes, grouped by DN.""" + account_move = self.env["account.move"] + + for dn, dn_line in dn_map: + if current_dn != dn: + vals_list.append( + Command.create(account_move._prepare_note_dn_value(sequence, dn)) ) - > 1 - ): - raise UserError( - delivery_note_id.env._( - "Sale orders related to %s have return! " - "For invoicing, go to sale orders." + sequence += 1 + current_dn = dn + + if returned_moves := dn_line.mapped("move_id.returned_move_ids"): + return_qty = sum(returned_moves.mapped("quantity")) + product_qty = dn_line.product_qty - return_qty + else: + product_qty = dn_line.product_qty + + vals = dn_line._prepare_invoice_line( + sequence=sequence, + quantity=product_qty, + ) + vals_list.append(Command.create(vals)) + sequence += 1 + + return sequence, current_dn + + def _append_sale_order_lines(self, vals_list, sale_orders, dn_map, sequence): + """Append invoice lines from sale orders preserving SO line order.""" + current_dn = None + for order in sale_orders: + so_lines = order.order_line.sorted(key=lambda ln: ln.sequence) + + for line in so_lines: + # Add SO lines notes/sections + if line.display_type and not line.is_downpayment: + vals = line._prepare_invoice_line(sequence=sequence) + vals_list.append(Command.create(vals)) + sequence += 1 + continue + + if line.id in dn_map and line.order_id == order: + # Add delivery note lines + sequence, current_dn = self._append_dn_linked_lines( + vals_list=vals_list, + dn_map=dn_map[line.id], + sequence=sequence, + current_dn=current_dn, ) - % delivery_note_id.display_name - ) - if delivery_note_id.invoice_status == "invoiced": - raise UserError( - delivery_note_id.env._("%s is already invoiced!") - % delivery_note_id.display_name - ) - if delivery_note_id.state == "draft": - raise UserError( - delivery_note_id.env._("%s is in draft!") - % delivery_note_id.display_name - ) - for line in delivery_note_id.line_ids: - if line.product_id.invoice_policy == "order": - raise UserError( - delivery_note_id.env._( - "In %(ddt_name)s there is %(product_name)s" - " with invoicing policy 'order'" + else: + # Add remaining SO lines not in delivery notes + if ( + line.product_id.type != "service" + and line.qty_to_invoice > 0 + and not line.is_downpayment + ): + vals = line._prepare_invoice_line(sequence=sequence) + vals["sale_line_ids"] = [Command.link(line.id)] + vals_list.append(Command.create(vals)) + sequence += 1 + + return sequence + + def _prepare_invoice_lines(self, sale_orders, invoice_method, final): + """Creates invoice lines from delivery note lines, + sorting delivery notes by date and name. + For each delivery note adds: + - A section line with delivery note data + - Product lines from the delivery note + If sale_orders is passed: + - Only includes lines related to those sale orders + - Adds order lines not yet in delivery notes + If requested (invoice_method == "service"): + - Adds service type lines from orders + If it's the final invoice (final == True): + - Adds down payment lines + Sets the created lines in the invoice values dictionary""" + vals_list = [] + sequence = 1 + + dn_map = self._build_dn_map(sale_orders) + + if sale_orders: + sequence = self._append_sale_order_lines( + vals_list=vals_list, + sale_orders=sale_orders, + dn_map=dn_map, + sequence=sequence, + ) + + # Add service lines if requested + if invoice_method == "service": + service_lines = sale_orders.mapped("order_line").filtered( + lambda ol: ol.product_id.type == "service" + and ol.qty_to_invoice > 0 + and not ol.is_downpayment + ) + for line in service_lines: + vals = line._prepare_invoice_line(sequence=sequence) + vals["sale_line_ids"] = [Command.link(line.id)] + vals_list.append(Command.create(vals)) + sequence += 1 + + # Add downpayment lines if final invoice + if final: + downpayment_lines = sale_orders.mapped("order_line").filtered( + lambda ol: not ol.display_type + and ol.is_downpayment + and ol.qty_to_invoice < 0 + ) + if downpayment_lines: + # Add downpayment section + vals_list.append( + Command.create( + sale_orders[0]._prepare_down_payment_section_line( + sequence=sequence ) - % { - "ddt_name": delivery_note_id.display_name, - "product_name": line.product_id.name, - } + ), + ) + sequence += 1 + # Add downpayment lines + for line in downpayment_lines: + vals_list.append( + Command.create(line._prepare_invoice_line(sequence=sequence)) ) + sequence += 1 - def _fix_quantities_to_invoice(self, lines, invoice_method): - cache = {} + return vals_list + + def _update_invoice_statuses(self, invoices, sale_orders): + """Update invoice statuses after invoice creation""" + # Get all delivery note lines with sale orders + delivery_note_lines = self.mapped("line_ids").filtered( + lambda line: line.sale_line_id and line.is_invoiceable + ) - pickings_lines = lines.retrieve_pickings_lines(self.picking_ids) - other_lines = lines - pickings_lines + # If sale_orders specified, filter only those lines + if sale_orders: + delivery_note_lines = delivery_note_lines.filtered( + lambda line: line.sale_line_id.order_id in sale_orders + ) - if not invoice_method or invoice_method == "dn": - for line in other_lines: - cache[line] = line.fix_qty_to_invoice() - elif invoice_method == "service": - for line in other_lines: - if line.product_id.type != "service": - cache[line] = line.fix_qty_to_invoice() + # Mark lines as invoiced + delivery_note_lines.write({"invoice_status": DOMAIN_INVOICE_STATUSES[2]}) - pickings_move_ids = self.mapped("picking_ids.move_ids") - for line in pickings_lines.filtered(lambda line: len(line.move_ids) > 1): - move_ids = line.move_ids & pickings_move_ids - qty_to_invoice = sum(move_ids.mapped("quantity")) + # Link invoice to delivery notes + for delivery_note in self: + delivery_note.write( + {"invoice_ids": [Command.link(invoice.id) for invoice in invoices]} + ) - if qty_to_invoice < line.qty_to_invoice: - cache[line] = line.fix_qty_to_invoice(qty_to_invoice) + # Recompute overall invoice status + self._compute_invoice_status() - return cache + def _build_invoice_vals_list( + self, sale_orders, delivery_notes, invoice_method=False, final=False + ): + invoice_vals_list = [] - def action_invoice(self, invoice_method=False): - self._check_delivery_notes_before_invoicing() + orders = delivery_notes.sale_ids & sale_orders + invoice_vals = delivery_notes._prepare_invoice(orders) + invoice_vals["invoice_line_ids"] = delivery_notes._prepare_invoice_lines( + orders, invoice_method, final + ) + invoice_vals_list.append(invoice_vals) - payment_term_ids = [self.env["account.payment.term"]] - payment_term_ids += [ - payment_term_id - for payment_term_id in self.mapped("sale_ids.payment_term_id") - ] - for payment_term_id in payment_term_ids: - sale_ids = self.mapped("sale_ids").filtered( - lambda s, pay_term_id=payment_term_id: s.payment_term_id == pay_term_id + return invoice_vals_list + + def _group_invoice_vals(self, invoice_vals_list, sale_orders): + new_invoice_vals_list = [] + invoice_grouping_keys = sale_orders._get_invoice_grouping_keys() + + invoice_vals_list = sorted( + invoice_vals_list, + key=lambda vals: [vals.get(k) for k in invoice_grouping_keys], + ) + + for _keys, invoices in groupby( + invoice_vals_list, + key=lambda vals: [vals.get(k) for k in invoice_grouping_keys], + ): + grouped_vals = None + origins = set() + payment_refs = set() + refs = set() + + for invoice_vals in invoices: + if not grouped_vals: + grouped_vals = invoice_vals + else: + grouped_vals["invoice_line_ids"] += invoice_vals["invoice_line_ids"] + + if invoice_vals.get("invoice_origin"): + origins.add(invoice_vals["invoice_origin"]) + if invoice_vals.get("payment_reference"): + payment_refs.add(invoice_vals["payment_reference"]) + if invoice_vals.get("ref"): + refs.add(invoice_vals["ref"]) + + grouped_vals.update( + { + "ref": ", ".join(refs)[:2000], + "invoice_origin": ", ".join(origins), + "payment_reference": payment_refs.pop() + if len(payment_refs) == 1 + else False, + } ) - if not sale_ids: + new_invoice_vals_list.append(grouped_vals) + + return new_invoice_vals_list + + def action_invoice( + self, invoice_method=False, final=False, sale_orders=None, grouped=False + ): + delivery_note_ids = self.filtered( + lambda dn: dn.state == "confirm" and dn.invoice_status == "to invoice" + ) + delivery_note_ids._check_delivery_notes_before_invoicing() + + # If not passed explicitly, get all sale orders from delivery notes + sale_orders = sale_orders or delivery_note_ids.sale_ids + if not sale_orders: + return self.env["account.move"] + + # Check if the user has access rights to create invoices + if not self.env["account.move"].check_access("create"): + try: + self.check_access("write") + except AccessError: + return self.env["account.move"] + + moves = self.env["account.move"] + + # Group by payment term (include empty payment term) + payment_terms = sale_orders.payment_term_id or [False] + for payment_term_id in payment_terms: + # Filter sale orders by payment term and invoice status + if payment_term_id: + filtered_sales = sale_orders.filtered( + lambda so, pt=payment_term_id: so.payment_term_id == pt + and so.invoice_status == "to invoice" + ) + else: + # No payment term filter when payment_term_id is False + filtered_sales = sale_orders.filtered( + lambda so: not so.payment_term_id + and so.invoice_status == "to invoice" + ) + if not filtered_sales: continue - orders_lines = sale_ids.mapped("order_line").filtered( - lambda l: l.product_id # noqa: E741 - ) - downpayment_lines = orders_lines.filtered(lambda l: l.is_downpayment) # noqa: E741 - invoiceable_lines = orders_lines.filtered(lambda l: l.is_invoiceable) # noqa: E741 + # Filter delivery notes related to the sale orders + filter_delivery_notes = delivery_note_ids.filtered( + lambda dn, so=filtered_sales: all( + so_id in so.ids for so_id in dn.sale_ids.ids + ) + or all(so_id in dn.sale_ids.ids for so_id in so.ids) + ) - cache = self._fix_quantities_to_invoice( - invoiceable_lines - downpayment_lines, invoice_method + # prepare invoice lines for every SO + invoice_vals_list = self._build_invoice_vals_list( + filtered_sales, filter_delivery_notes, invoice_method, final ) + if not invoice_vals_list: + continue - for downpayment in downpayment_lines: - order = downpayment.order_id - order_lines = order.order_line.filtered( - lambda l: l.product_id and not l.is_downpayment # noqa: E741 + # GROUPING + if not grouped: + invoice_vals_list = self._group_invoice_vals( + invoice_vals_list, sale_orders ) - if order_lines.filtered(lambda l: l.need_to_be_invoiced): # noqa: E741 - cache[downpayment] = downpayment.fix_qty_to_invoice() - - invoice_ids = sale_ids.filtered( - lambda o: o.invoice_status == DOMAIN_INVOICE_STATUSES[1] - )._create_invoices(final=True) - - for line, vals in cache.items(): - line.write(vals) - - orders_lines._compute_qty_to_invoice() - - for line in self.mapped("line_ids"): - line.write({"invoice_status": "invoiced"}) - for delivery_note in self: - ready_invoice_ids = [ - invoice_id - for invoice_id in delivery_note.sale_ids.mapped("invoice_ids").ids - if invoice_id in invoice_ids.ids - ] - delivery_note.write( - { - "invoice_ids": [ - (4, invoice_id) for invoice_id in ready_invoice_ids - ] - } + for invoice_vals in invoice_vals_list: + sequence = 1 + for line in invoice_vals.get("invoice_line_ids", []): + if len(line) >= 3 and isinstance(line[2], dict): + line[2]["sequence"] = self.env[ + "sale.order.line" + ]._get_invoice_line_sequence( + new=sequence, + old=line[2].get("sequence"), + ) + sequence += 1 + + # CREATE invoices + moves = self.env["account.move"] + + for invoice_vals in invoice_vals_list: + move = ( + self.env["account.move"] + .sudo() + .with_context(default_move_type="out_invoice") + .create(invoice_vals) ) - self._compute_invoice_status() - invoices = self.env["account.move"].browse(invoice_ids.ids) - invoices.update_delivery_note_lines() + moves |= move + + # update delivery notes status + filter_delivery_notes._update_invoice_statuses(moves, sale_orders) + + # Some moves might actually be refunds: convert them if the total amount is + # negative. + # We do this after the moves have been created since we need taxes, etc. + # to know if the total is actually negative or not + if final and ( + moves_to_switch := moves.sudo().filtered(lambda m: m.amount_total < 0) + ): + with self.env.protecting([moves._fields["team_id"]], moves_to_switch): + moves_to_switch.action_switch_move_type() + sale_orders.invoice_ids._set_reversed_entry(moves_to_switch) + + return moves def action_done(self): self.write({"state": DOMAIN_DELIVERY_NOTE_STATES[3]}) @@ -872,7 +1102,7 @@ def _create_detail_lines(self, move_ids): moves = self.env["stock.move"].browse(move_ids) lines_vals = self.env["stock.delivery.note.line"]._prepare_detail_lines(moves) - self.write({"line_ids": [(0, False, vals) for vals in lines_vals]}) + self.write({"line_ids": [Command.create(vals) for vals in lines_vals]}) def _delete_detail_lines(self, move_ids): if not move_ids: @@ -882,7 +1112,7 @@ def _delete_detail_lines(self, move_ids): [("move_id", "in", move_ids)] ) - self.write({"line_ids": [(2, line.id, False) for line in lines]}) + self.write({"line_ids": [Command.delete(line.id) for line in lines]}) def update_detail_lines(self): for note in self: diff --git a/l10n_it_delivery_note/models/stock_delivery_note_line.py b/l10n_it_delivery_note/models/stock_delivery_note_line.py index d65c8f00d449..0b847bea9419 100644 --- a/l10n_it_delivery_note/models/stock_delivery_note_line.py +++ b/l10n_it_delivery_note/models/stock_delivery_note_line.py @@ -1,9 +1,10 @@ # Copyright 2022 Dinamiche Aziendali srl # (http://www.dinamicheaziendali.it/) # @author: Giuseppe Borruso +# Copyright 2024 Nextev srl # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo import api, fields, models +from odoo import Command, api, fields, models from odoo.exceptions import UserError DATE_FORMAT = "%d/%m/%Y" @@ -197,3 +198,61 @@ def sync_invoice_status(self): if invoice_status == "upselling" else invoice_status ) + + def _prepare_invoice_line(self, **optional_values): + """Prepare the values to create the new invoice line for a DN line. + + :param optional_values: any parameter that should be added to the + returned invoice line + :rtype: dict + """ + self.ensure_one() + + res = { + "display_type": self.display_type or "product", + "delivery_note_line_id": self.id, + } + + if self.display_type: + res.update( + { + "name": self.name, + "quantity": 0, + "account_id": False, + } + ) + else: + product_name = self.sale_line_id.name + if self.company_id.use_dn_product_name_in_invoice: + product_name = self.name + + price_unit = self.sale_line_id.price_unit + if self.company_id.use_dn_price_unit_in_invoice: + price_unit = self.price_unit + + res.update( + { + "name": product_name, + "product_id": self.product_id.id, + "product_uom_id": self.product_uom_id.id, + "quantity": self.product_qty, + "discount": self.discount, + "price_unit": price_unit, + "tax_ids": [Command.set(self.tax_ids.ids)], + "sale_line_ids": [Command.link(self.sale_line_id.id)], + } + ) + if optional_values.get("quantity"): + res["quantity"] = optional_values["quantity"] + del optional_values["quantity"] + if ( + self.sale_line_id.analytic_distribution + and not self.sale_line_id.display_type + ): + res["analytic_distribution"] = self.sale_line_id.analytic_distribution + + if optional_values.get("sequence"): + res["sequence"] = optional_values["sequence"] + if optional_values: + res.update(optional_values) + return res diff --git a/l10n_it_delivery_note/models/stock_picking.py b/l10n_it_delivery_note/models/stock_picking.py index ab34d94d80cd..ef160db8d84a 100644 --- a/l10n_it_delivery_note/models/stock_picking.py +++ b/l10n_it_delivery_note/models/stock_picking.py @@ -239,7 +239,17 @@ def action_delivery_note_confirm(self): def action_delivery_note_invoice(self): self.ensure_one() - return self.delivery_note_id.action_invoice() + return { + "name": self.env._("Create invoices"), + "type": "ir.actions.act_window", + "res_model": "stock.delivery.note.invoice.wizard", + "view_mode": "form", + "target": "new", + "context": { + "active_ids": self.delivery_note_id.ids, + "active_model": "stock.delivery.note", + }, + } def action_delivery_note_done(self): self.ensure_one() diff --git a/l10n_it_delivery_note/readme/CONFIGURE.md b/l10n_it_delivery_note/readme/CONFIGURE.md index 6f57149dd61e..230f3d768fea 100644 --- a/l10n_it_delivery_note/readme/CONFIGURE.md +++ b/l10n_it_delivery_note/readme/CONFIGURE.md @@ -11,6 +11,12 @@ To configure this module, go to: Checking 'Display Delivery Method in Delivery Note Report' enables in report field 'Delivery Method'. + **Invoice Generation from Delivery Notes:** + + - Checking 'Use Delivery Note Product Name in Invoice' makes the invoice use the product description from the delivery note instead of the sale order. This is useful when you modify product descriptions in the DN to reflect what was actually delivered. + + - Checking 'Use Delivery Note Price Unit in Invoice' makes the invoice use the unit price from the delivery note instead of the sale order. This is useful for price negotiations at delivery time or when substituting products with different prices. + 2. *Inventory → Configuration → Warehouse Management → Delivery Note Types* diff --git a/l10n_it_delivery_note/readme/DESCRIPTION.md b/l10n_it_delivery_note/readme/DESCRIPTION.md index 79f82628ffab..20c6aa68b421 100644 --- a/l10n_it_delivery_note/readme/DESCRIPTION.md +++ b/l10n_it_delivery_note/readme/DESCRIPTION.md @@ -3,8 +3,14 @@ This module manage the Italian DDT (Delivery note). From a picking is possible to generate a Delivery Note and group more -picking in one delivery note. It's also possible to invoice from the -delivery note form. +picking in one delivery note. It's also possible to invoice directly from the +delivery note form, with configurable options to use DN data (product names, prices) +instead of sale order data when generating invoices. + +This is particularly useful when: +- Products are substituted at delivery time +- Prices are negotiated during delivery +- Detailed descriptions need to be added in the DN This module is alternative to `l10n_it_ddt`, it follows the Odoo way to process sale orders, pickings and invoices. @@ -22,7 +28,14 @@ There are two available settings: Questo modulo consente di gestire i DDT. Da un prelievo è possibile generare un DDT e raggruppare più prelievi in -un DDT. È anche possibile fatturare dalla scheda del DDT. +un DDT. È anche possibile fatturare direttamente dalla scheda del DDT, +con opzioni configurabili per utilizzare i dati del DDT (nomi prodotti, prezzi) +invece dei dati dell'ordine di vendita nella generazione delle fatture. + +Questo è particolarmente utile quando: +- I prodotti vengono sostituiti al momento della consegna +- I prezzi vengono negoziati durante la consegna +- È necessario aggiungere descrizioni dettagliate nel DDT Questo modulo è un alternativa al modulo `l10n_it_ddt`, segue la modalità Odoo di gestire ordini di vendita, prelievi e fatture. diff --git a/l10n_it_delivery_note/readme/USAGE.md b/l10n_it_delivery_note/readme/USAGE.md index 34ca0b4bb553..ca971e4c1d0b 100644 --- a/l10n_it_delivery_note/readme/USAGE.md +++ b/l10n_it_delivery_note/readme/USAGE.md @@ -39,6 +39,47 @@ permessi dell'utente. Le fatture generate dai DDT contengono i riferimenti al DDT stesso nelle righe nota. +## Fatturazione da DN + +E' possibile creare una fattura selezionando una o più DN dello stesso partner +dalla tree view tramite il wizard "crea fattura". +Si può scegliere se includere anche i servizi non ancora fatturati dell'ordine +di vendita correlato o considerare solo le righe nei DN. +In maniera predefinita vengono dedotti gli eventuali anticipi fatturati. + +### Utilizzo dei dati dal DDT nelle fatture + +Dalle impostazioni (*Inventario → Configurazione → Impostazioni - Documenti di Trasporto*) +è possibile configurare se la fattura deve utilizzare i dati dal DDT anziché +dall'ordine di vendita: + +- **Usa Nome Prodotto da DDT nelle Fatture**: Quando attivo, la descrizione del + prodotto nella fattura viene presa dal DDT invece che dall'ordine di vendita. + Utile quando si modificano le descrizioni nel DDT per riflettere ciò che è + stato effettivamente consegnato. + +- **Usa Prezzo Unitario da DDT nelle Fatture**: Quando attivo, il prezzo unitario + nella fattura viene preso dal DDT invece che dall'ordine di vendita. + Utile per negoziazioni di prezzo al momento della consegna o quando si + sostituiscono prodotti con prezzi diversi. + +**Esempi pratici:** + +1. **Prodotto sostituito**: Se ordini "Scrivania Modello A - €500" ma consegni + "Scrivania Modello B - €450", modificando DDT e attivando entrambe le opzioni, + la fattura rifletterà automaticamente il prodotto e prezzo reale consegnato. + +2. **Negoziazione alla consegna**: Se il cliente nota un difetto e negoziate uno + sconto, modificando il prezzo nel DDT con l'opzione attiva, la fattura sarà + corretta senza bisogno di note credito. + +3. **Descrizioni dettagliate**: Se nel DDT specifichi "3 sacchi cemento CEM II, + 5 pannelli isolanti" invece di "Materiale edile vario", con l'opzione attiva + la fattura mostrerà i dettagli completi. + +**Nota**: Queste opzioni sono disabilitate per default per mantenere la +retrocompatibilità. Attivarle solo se si desidera questo comportamento. + ## Accesso da portale Gli utenti portal hanno la possibilità di scaricare i report dei DDT di cui loro o la loro azienda padre sono impostati come destinatari o indirizzo di spedizione. diff --git a/l10n_it_delivery_note/static/description/index.html b/l10n_it_delivery_note/static/description/index.html index 5f7d5ef5739b..ee56ad44f3f1 100644 --- a/l10n_it_delivery_note/static/description/index.html +++ b/l10n_it_delivery_note/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +ITA - Documento di trasporto -
+
+

ITA - Documento di trasporto

- - -Odoo Community Association - -
-

ITA - Documento di trasporto

-

Beta License: AGPL-3 OCA/l10n-italy Translate me on Weblate Try me on Runboat

+

Beta License: AGPL-3 OCA/l10n-italy Translate me on Weblate Try me on Runboat

English

This module manage the Italian DDT (Delivery note).

From a picking is possible to generate a Delivery Note and group more -picking in one delivery note. It’s also possible to invoice from the -delivery note form.

+picking in one delivery note. It’s also possible to invoice directly +from the delivery note form, with configurable options to use DN data +(product names, prices) instead of sale order data when generating +invoices.

+

This is particularly useful when:

+
    +
  • Products are substituted at delivery time
  • +
  • Prices are negotiated during delivery
  • +
  • Detailed descriptions need to be added in the DN
  • +

This module is alternative to l10n_it_ddt, it follows the Odoo way to process sale orders, pickings and invoices.

You can’t have both l10n_it_ddt and l10n_it_delivery_note @@ -392,7 +395,16 @@

ITA - Documento di trasporto

Italiano

Questo modulo consente di gestire i DDT.

Da un prelievo è possibile generare un DDT e raggruppare più prelievi in -un DDT. È anche possibile fatturare dalla scheda del DDT.

+un DDT. È anche possibile fatturare direttamente dalla scheda del DDT, +con opzioni configurabili per utilizzare i dati del DDT (nomi prodotti, +prezzi) invece dei dati dell’ordine di vendita nella generazione delle +fatture.

+

Questo è particolarmente utile quando:

+
    +
  • I prodotti vengono sostituiti al momento della consegna
  • +
  • I prezzi vengono negoziati durante la consegna
  • +
  • È necessario aggiungere descrizioni dettagliate nel DDT
  • +

Questo modulo è un alternativa al modulo l10n_it_ddt, segue la modalità Odoo di gestire ordini di vendita, prelievi e fatture.

Non è possibile avere installati contemporaneamente l10n_it_ddt e @@ -409,20 +421,24 @@

ITA - Documento di trasporto

  • Usage
  • -
  • Bug Tracker
  • -
  • Credits
  • -

    Configuration

    +

    Configuration

    To configure this module, go to:

    1. Inventory → Configuration → Settings - Delivery Notes

      @@ -435,6 +451,17 @@

      Configuration

      field ‘Carrier’.

      Checking ‘Display Delivery Method in Delivery Note Report’ enables in report field ‘Delivery Method’.

      +

      Invoice Generation from Delivery Notes:

      +
        +
      • Checking ‘Use Delivery Note Product Name in Invoice’ makes the +invoice use the product description from the delivery note instead +of the sale order. This is useful when you modify product +descriptions in the DN to reflect what was actually delivered.
      • +
      • Checking ‘Use Delivery Note Price Unit in Invoice’ makes the +invoice use the unit price from the delivery note instead of the +sale order. This is useful for price negotiations at delivery time +or when substituting products with different prices.
      • +
    2. Inventory → Configuration → Warehouse Management → Delivery Note Types

      @@ -443,9 +470,12 @@

      Configuration

      • Inventory → Configuration → Delivery Notes → Conditions of Transport
      • -
      • Inventory → Configuration → Delivery Notes → Appearances of Goods
      • -
      • Inventory → Configuration → Delivery Notes → Reasons of Transport
      • -
      • Inventory → Configuration → Delivery Notes → Methods of Transport
      • +
      • Inventory → Configuration → Delivery Notes → Appearances of +Goods
      • +
      • Inventory → Configuration → Delivery Notes → Reasons of +Transport
      • +
      • Inventory → Configuration → Delivery Notes → Methods of +Transport
    3. Settings → User & Companies → Users

      @@ -455,9 +485,9 @@

      Configuration

    -

    Usage

    +

    Usage

    -

    Funzionalità base

    +

    Funzionalità base

    Quando un prelievo viene validato compare una scheda DDT.

    Nella scheda fare clic su “Crea nuovo”, si apre un procedura guidata dove scegliere il tipo di DDT, quindi confermare. Immettere i dati @@ -474,7 +504,7 @@

    Funzionalità base

    e la data.

    -

    Funzionalità avanzata

    +

    Funzionalità avanzata

    Vengono attivate varie funzionalità aggiuntive:

    • più prelievi per un DDT
    • @@ -490,15 +520,54 @@

      Funzionalità avanzata

      Le fatture generate dai DDT contengono i riferimenti al DDT stesso nelle righe nota.

    +
    +

    Fatturazione da DN

    +

    E’ possibile creare una fattura selezionando una o più DN dello stesso +partner dalla tree view tramite il wizard “crea fattura”. Si può +scegliere se includere anche i servizi non ancora fatturati dell’ordine +di vendita correlato o considerare solo le righe nei DN. In maniera +predefinita vengono dedotti gli eventuali anticipi fatturati.

    +
    +

    Utilizzo dei dati dal DDT nelle fatture

    +

    Dalle impostazioni (Inventario → Configurazione → Impostazioni - +Documenti di Trasporto) è possibile configurare se la fattura deve +utilizzare i dati dal DDT anziché dall’ordine di vendita:

    +
      +
    • Usa Nome Prodotto da DDT nelle Fatture: Quando attivo, la +descrizione del prodotto nella fattura viene presa dal DDT invece che +dall’ordine di vendita. Utile quando si modificano le descrizioni nel +DDT per riflettere ciò che è stato effettivamente consegnato.
    • +
    • Usa Prezzo Unitario da DDT nelle Fatture: Quando attivo, il +prezzo unitario nella fattura viene preso dal DDT invece che +dall’ordine di vendita. Utile per negoziazioni di prezzo al momento +della consegna o quando si sostituiscono prodotti con prezzi diversi.
    • +
    +

    Esempi pratici:

    +
      +
    1. Prodotto sostituito: Se ordini “Scrivania Modello A - €500” ma +consegni “Scrivania Modello B - €450”, modificando DDT e attivando +entrambe le opzioni, la fattura rifletterà automaticamente il +prodotto e prezzo reale consegnato.
    2. +
    3. Negoziazione alla consegna: Se il cliente nota un difetto e +negoziate uno sconto, modificando il prezzo nel DDT con l’opzione +attiva, la fattura sarà corretta senza bisogno di note credito.
    4. +
    5. Descrizioni dettagliate: Se nel DDT specifichi “3 sacchi cemento +CEM II, 5 pannelli isolanti” invece di “Materiale edile vario”, con +l’opzione attiva la fattura mostrerà i dettagli completi.
    6. +
    +

    Nota: Queste opzioni sono disabilitate per default per mantenere la +retrocompatibilità. Attivarle solo se si desidera questo comportamento.

    +
    +
    -

    Accesso da portale

    +

    Accesso da portale

    Gli utenti portal hanno la possibilità di scaricare i report dei DDT di cui loro o la loro azienda padre sono impostati come destinatari o indirizzo di spedizione.

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed @@ -506,9 +575,9 @@

    Bug Tracker

    Do not contact contributors directly about support or help with technical issues.

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Marco Calcagni
    • Gianmarco Conte
    • @@ -516,7 +585,7 @@

      Authors

    -

    Contributors

    +

    Contributors

    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -577,6 +646,5 @@

    Maintainers

    -
    diff --git a/l10n_it_delivery_note/tests/test_stock_delivery_note_invoicing.py b/l10n_it_delivery_note/tests/test_stock_delivery_note_invoicing.py index eddfde83a99f..a1eaf734f72a 100644 --- a/l10n_it_delivery_note/tests/test_stock_delivery_note_invoicing.py +++ b/l10n_it_delivery_note/tests/test_stock_delivery_note_invoicing.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta +from odoo.fields import Command from odoo.tests import Form from .delivery_note_common import StockDeliveryNoteCommon @@ -61,7 +62,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note.state, "confirm") self.assertEqual(delivery_note.invoice_status, "to invoice") - delivery_note.action_invoice() + delivery_note.action_invoice(final=True) self.assertEqual(len(delivery_note.line_ids), 4) self.assertEqual(delivery_note.state, "invoiced") self.assertEqual(delivery_note.invoice_status, "invoiced") @@ -74,7 +75,7 @@ def test_complete_invoicing_single_so(self): final_invoice = invoices[0] # in sale.advance.payment.inv the method create_invoices uses the field - # deduct_down_payments (default True) that includes selection lines: + # deduct_down_payments that includes selection lines: # so 4 product lines, 1 ddt note, 1 down_payment and 1 selection line self.assertEqual(len(final_invoice.invoice_line_ids), 7) self.assertEqual(final_invoice.delivery_note_ids, delivery_note) @@ -99,7 +100,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[0] + invoice_line = final_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -121,7 +122,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = final_invoice.invoice_line_ids[1] + invoice_line = final_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) @@ -143,7 +144,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 11) - invoice_line = final_invoice.invoice_line_ids[2] + invoice_line = final_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 11) @@ -165,7 +166,7 @@ def test_complete_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[3] + invoice_line = final_invoice.invoice_line_ids[4] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -188,18 +189,18 @@ def test_complete_invoicing_single_so(self): self.assertEqual(len(delivery_note_line), 0) - invoice_line = final_invoice.invoice_line_ids[4] + invoice_line = final_invoice.invoice_line_ids[-2] self.assertEqual(invoice_line.display_type, "line_section") self.assertEqual(invoice_line.name, "Down Payments") - invoice_line = final_invoice.invoice_line_ids[5] + invoice_line = final_invoice.invoice_line_ids[-1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, -1) # # Fattura - Linea 7 (DdT in fattura) # - invoice_line = final_invoice.invoice_line_ids[6] + invoice_line = final_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, delivery_note) @@ -315,7 +316,7 @@ def test_partial_invoicing_single_so(self): # # Ordine - Linea 1 - # Fattura - Linea 1 + # Fattura - Linea 2 # order_line = sales_order.order_line[0] self.assertEqual(order_line.invoice_status, "to invoice") @@ -331,13 +332,13 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = partial_invoice.invoice_line_ids[0] + invoice_line = partial_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) # # Ordine - Linea 2 - # Fattura - Linea 2 + # Fattura - Linea 3 # order_line = sales_order.order_line[1] self.assertEqual(order_line.invoice_status, "invoiced") @@ -353,13 +354,13 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = partial_invoice.invoice_line_ids[1] + invoice_line = partial_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) # # Ordine - Linea 3 - # Fattura - Linea 3 + # Fattura - Linea 4 # order_line = sales_order.order_line[2] self.assertEqual(order_line.invoice_status, "to invoice") @@ -375,13 +376,13 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 6) - invoice_line = partial_invoice.invoice_line_ids[2] + invoice_line = partial_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 6) # # Ordine - Linea 4 - # Fattura - Linea 4 + # Fattura - Linea 5 # order_line = sales_order.order_line[3] self.assertEqual(order_line.invoice_status, "to invoice") @@ -397,14 +398,14 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 3) - invoice_line = partial_invoice.invoice_line_ids[3] + invoice_line = partial_invoice.invoice_line_ids[4] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 3) # - # Fattura - Linea 5 (DdT in fattura) + # Fattura - Linea 1 (DdT in fattura) # - invoice_line = partial_invoice.invoice_line_ids[4] + invoice_line = partial_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, first_delivery_note) @@ -413,7 +414,7 @@ def test_partial_invoicing_single_so(self): # = = - = = - = = - = = # - second_delivery_note.action_invoice() + second_delivery_note.action_invoice(final=True) self.assertEqual(len(second_delivery_note.line_ids), 3) self.assertEqual(second_delivery_note.state, "invoiced") self.assertEqual(second_delivery_note.invoice_status, "invoiced") @@ -448,7 +449,7 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[0] + invoice_line = final_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -470,7 +471,7 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 5) - invoice_line = final_invoice.invoice_line_ids[1] + invoice_line = final_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 5) @@ -492,7 +493,7 @@ def test_partial_invoicing_single_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = final_invoice.invoice_line_ids[2] + invoice_line = final_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) @@ -516,18 +517,18 @@ def test_partial_invoicing_single_so(self): self.assertEqual(len(delivery_note_line), 0) - invoice_line = final_invoice.invoice_line_ids[3] + invoice_line = final_invoice.invoice_line_ids[-2] self.assertEqual(invoice_line.display_type, "line_section") self.assertEqual(invoice_line.name, "Down Payments") - invoice_line = final_invoice.invoice_line_ids[4] + invoice_line = final_invoice.invoice_line_ids[-1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, -1) # # Fattura - Linea 6 (DdT in fattura) # - invoice_line = final_invoice.invoice_line_ids[5] + invoice_line = final_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, second_delivery_note) @@ -617,7 +618,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note.state, "confirm") self.assertEqual(delivery_note.invoice_status, "to invoice") - delivery_note.action_invoice() + delivery_note.action_invoice(final=True) self.assertEqual(len(delivery_note.line_ids), 6) self.assertEqual(delivery_note.state, "invoiced") self.assertEqual(delivery_note.invoice_status, "invoiced") @@ -633,7 +634,7 @@ def test_complete_invoicing_multiple_so(self): invoices = sales_orders.mapped("invoice_ids") self.assertEqual(len(invoices), 2) - final_invoice = invoices[0] + final_invoice = invoices.sorted("id", reverse=True)[0] self.assertEqual(len(final_invoice.invoice_line_ids), 9) self.assertEqual(final_invoice.delivery_note_ids, delivery_note) @@ -657,7 +658,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[0] + invoice_line = final_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -679,7 +680,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 3) - invoice_line = final_invoice.invoice_line_ids[1] + invoice_line = final_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 3) @@ -701,7 +702,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = final_invoice.invoice_line_ids[2] + invoice_line = final_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) @@ -724,11 +725,11 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(len(delivery_note_line), 0) - invoice_line = final_invoice.invoice_line_ids[3] + invoice_line = final_invoice.invoice_line_ids[7] self.assertEqual(invoice_line.display_type, "line_section") self.assertEqual(invoice_line.name, "Down Payments") - invoice_line = final_invoice.invoice_line_ids[4] + invoice_line = final_invoice.invoice_line_ids[-1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, -1) @@ -750,7 +751,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 11) - invoice_line = final_invoice.invoice_line_ids[5] + invoice_line = final_invoice.invoice_line_ids[4] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 11) @@ -772,7 +773,7 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 5) - invoice_line = final_invoice.invoice_line_ids[6] + invoice_line = final_invoice.invoice_line_ids[5] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 5) @@ -794,14 +795,14 @@ def test_complete_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[7] + invoice_line = final_invoice.invoice_line_ids[6] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) # # Fattura - Linea 9 (DdT in fattura) # - invoice_line = final_invoice.invoice_line_ids[8] + invoice_line = final_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, delivery_note) @@ -909,6 +910,8 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(first_delivery_note.state, "confirm") self.assertEqual(first_delivery_note.invoice_status, "to invoice") + # Create invoices from both sale orders separately + # Each SO passes itself as sale_orders parameter to filter only its lines first_sales_order._create_invoices() self.assertEqual(len(first_sales_order.order_line), 5) self.assertEqual(first_sales_order.invoice_status, "no") @@ -980,7 +983,7 @@ def test_partial_invoicing_multiple_so(self): # # Ordine 1 - Linea 1 - # Fattura 1 - Linea 1 + # Fattura 1 - Linea 2 # order_line = first_sales_order.order_line[0] self.assertEqual(order_line.invoice_status, "invoiced") @@ -996,13 +999,13 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = first_partial_invoice.invoice_line_ids[0] + invoice_line = first_partial_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) # # Ordine 1 - Linea 2 - # Fattura 1 - Linea 2 + # Fattura 1 - Linea 3 # order_line = first_sales_order.order_line[1] self.assertEqual(order_line.invoice_status, "to invoice") @@ -1018,13 +1021,13 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = first_partial_invoice.invoice_line_ids[1] + invoice_line = first_partial_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) # # Ordine 1 - Linea 3 - # Fattura 1 - Linea 3 + # Fattura 1 - Linea 4 # order_line = first_sales_order.order_line[2] self.assertEqual(order_line.invoice_status, "invoiced") @@ -1040,21 +1043,21 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = first_partial_invoice.invoice_line_ids[2] + invoice_line = first_partial_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) # - # Fattura 1 - Linea 4 (DdT in fattura) + # Fattura 1 - Linea 1 (DdT in fattura) # - invoice_line = first_partial_invoice.invoice_line_ids[3] + invoice_line = first_partial_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, first_delivery_note) # # Ordine 2 - Linea 1 - # Fattura 2 - Linea 1 + # Fattura 2 - Linea 2 # order_line = second_sales_order.order_line[0] self.assertEqual(order_line.invoice_status, "invoiced") @@ -1070,13 +1073,13 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 3) - invoice_line = second_partial_invoice.invoice_line_ids[0] + invoice_line = second_partial_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 3) # # Ordine 2 - Linea 2 - # Fattura 2 - Linea 2 + # Fattura 2 - Linea 3 # order_line = second_sales_order.order_line[1] self.assertEqual(order_line.invoice_status, "to invoice") @@ -1092,13 +1095,13 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 3) - invoice_line = second_partial_invoice.invoice_line_ids[1] + invoice_line = second_partial_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 3) # # Ordine 2 - Linea 3 - # Fattura 2 - Linea 3 + # Fattura 2 - Linea 4 # order_line = second_sales_order.order_line[2] self.assertEqual(order_line.invoice_status, "to invoice") @@ -1114,14 +1117,14 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 3) - invoice_line = second_partial_invoice.invoice_line_ids[2] + invoice_line = second_partial_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 3) # - # Fattura 2 - Linea 4 (DdT in fattura) + # Fattura 2 - Linea 1 (DdT in fattura) # - invoice_line = second_partial_invoice.invoice_line_ids[3] + invoice_line = second_partial_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, first_delivery_note) @@ -1130,7 +1133,7 @@ def test_partial_invoicing_multiple_so(self): # = = - = = - = = - = = # - second_delivery_note.action_invoice() + second_delivery_note.action_invoice(final=True) self.assertEqual(len(second_delivery_note.line_ids), 3) self.assertEqual(second_delivery_note.state, "invoiced") self.assertEqual(second_delivery_note.invoice_status, "invoiced") @@ -1168,7 +1171,7 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 1) - invoice_line = final_invoice.invoice_line_ids[0] + invoice_line = final_invoice.invoice_line_ids[1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 1) @@ -1192,11 +1195,11 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(len(delivery_note_line), 0) - invoice_line = final_invoice.invoice_line_ids[1] + invoice_line = final_invoice.invoice_line_ids[4] self.assertEqual(invoice_line.display_type, "line_section") self.assertEqual(invoice_line.name, "Down Payments") - invoice_line = final_invoice.invoice_line_ids[2] + invoice_line = final_invoice.invoice_line_ids[-1] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, -1) @@ -1218,7 +1221,7 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 8) - invoice_line = final_invoice.invoice_line_ids[3] + invoice_line = final_invoice.invoice_line_ids[2] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 8) @@ -1240,14 +1243,14 @@ def test_partial_invoicing_multiple_so(self): self.assertEqual(delivery_note_line.sale_line_id, order_line) self.assertEqual(delivery_note_line.product_qty, 2) - invoice_line = final_invoice.invoice_line_ids[4] + invoice_line = final_invoice.invoice_line_ids[3] self.assertEqual(invoice_line.sale_line_ids, order_line) self.assertEqual(invoice_line.quantity, 2) # # Fattura 3 - Linea 6 (DdT in fattura) # - invoice_line = final_invoice.invoice_line_ids[5] + invoice_line = final_invoice.invoice_line_ids[0] self.assertEqual(invoice_line.display_type, "line_note") self.assertEqual(invoice_line.quantity, 0) self.assertEqual(invoice_line.delivery_note_id, second_delivery_note) @@ -1268,8 +1271,10 @@ def test_delivery_note_to_draft_from_create(self): picking.move_ids[0].quantity = 1 picking.button_validate() sales_order._create_invoices() - wizard = Form.from_action( - self.env, picking.action_delivery_note_create() + wizard = Form( + self.env["stock.delivery.note.create.wizard"].with_context( + active_ids=picking.ids, active_model="stock.picking" + ) ).save() result = wizard.confirm() delivery_note = self.env["stock.delivery.note"].browse(result["res_id"]) @@ -1278,3 +1283,648 @@ def test_delivery_note_to_draft_from_create(self): delivery_note.action_draft() self.assertEqual(delivery_note.invoice_status, "no") self.assertEqual(delivery_note.state, "draft") + + # ⇒ "DdT multipli: fatturazione completa" + def test_complete_invoicing_multiple_dn(self): + # + # - Picking -- DdT ┐ + # | | + # SO - ├ Fattura + # | | + # - Picking -- DdT ┘ + # + + # Activate advanced setting to allow more picking in one DN + self.env["ir.config_parameter"].sudo().set_param( + "l10n_it_delivery_note.group_use_advanced_delivery_notes", True + ) + + # SO + sales_order = self.create_sales_order( + [ + self.desk_combination_line, + self.customizable_desk_line, + ] + ) + self.assertEqual(len(sales_order.order_line), 2) + + sales_order.action_confirm() + + # 1° Picking + first_picking = sales_order.picking_ids + self.assertEqual(len(first_picking), 1) + self.assertEqual(len(first_picking.move_ids), 2) + + first_picking.move_ids.quantity = False + first_picking.move_ids[0].quantity = 1 + first_picking.move_ids[1].quantity = 1 + + result = first_picking.button_validate() + self.assertTrue(result) + + wizard = Form( + self.env[(result.get("res_model"))].with_context(**result["context"]) + ).save() + self.assertEqual(wizard._name, "stock.backorder.confirmation") + wizard.process() + + # 1° DdT + wizard = Form( + self.env["stock.delivery.note.create.wizard"].with_context( + active_ids=first_picking.ids, active_model="stock.picking" + ) + ).save() + result = wizard.confirm() + first_delivery_note = self.env["stock.delivery.note"].browse(result["res_id"]) + first_delivery_note.action_confirm() + self.assertEqual(len(first_delivery_note.line_ids), 2) + self.assertEqual(first_delivery_note.invoice_status, "to invoice") + + # 2° Picking + backorder = self.env["stock.picking"].search( + [("backorder_id", "=", first_picking.id)] + ) + self.assertEqual(len(backorder), 1) + self.assertEqual(len(backorder.move_ids), 1) + + backorder.move_ids.quantity = False + backorder.move_ids[0].quantity = 2 + + result = backorder.button_validate() + self.assertTrue(result) + + # 2° DdT + second_delivery_note = self.create_delivery_note() + second_delivery_note.transport_datetime = datetime.now() + timedelta( + days=1, hours=3 + ) + second_delivery_note.picking_ids = backorder + second_delivery_note.action_confirm() + + self.assertEqual(len(second_delivery_note.line_ids), 1) + self.assertEqual(second_delivery_note.invoice_status, "to invoice") + + # Create invoice + delivery_notes = first_delivery_note | second_delivery_note + delivery_notes.action_invoice() + + self.assertEqual(first_delivery_note.state, "invoiced") + self.assertEqual(second_delivery_note.state, "invoiced") + + invoices = delivery_notes.mapped("invoice_ids") + self.assertEqual(len(invoices), 1) + + self.assertEqual(invoices.delivery_note_ids, delivery_notes) + self.assertEqual(len(invoices.invoice_line_ids), 5) + + # Check invoice lines + lines_note = invoices.invoice_line_ids.filtered( + lambda line: line.display_type == "line_note" + ) + lines_product = invoices.invoice_line_ids.filtered( + lambda line: line.display_type == "product" + ) + # Note 1 DdT 1: 'Delivery Note "DDT/C1/00001" of ...' + line_note_dn_1 = lines_note.filtered(lambda line: line.sequence == 1) + self.assertEqual( + line_note_dn_1.name, + f'Delivery Note "{first_delivery_note.name}" of ' + f'{first_delivery_note.date.strftime("%d/%m/%Y")}', + ) + # Product Line 1 DdT 1 + line_product_dn_1 = lines_product.filtered(lambda line: line.sequence == 2) + self.assertEqual( + line_product_dn_1.product_id, first_delivery_note.line_ids[0].product_id + ) + self.assertEqual( + line_product_dn_1.quantity, first_delivery_note.line_ids[0].product_qty + ) + # Product Line 2 DdT 1 + line_product_dn_2 = lines_product.filtered(lambda line: line.sequence == 3) + self.assertEqual( + line_product_dn_2.product_id, first_delivery_note.line_ids[1].product_id + ) + self.assertEqual( + line_product_dn_2.quantity, first_delivery_note.line_ids[1].product_qty + ) + # Note 1 DdT 2: 'Delivery Note "DDT/C1/00002" of ...' + line_note_dn_2 = lines_note.filtered(lambda line: line.sequence == 4) + self.assertEqual( + line_note_dn_2.name, + f'Delivery Note "{second_delivery_note.name}" of ' + f'{second_delivery_note.date.strftime("%d/%m/%Y")}', + ) + # Product Line 1 DdT 2 + line_product_dn_3 = lines_product.filtered(lambda line: line.sequence == 5) + self.assertEqual( + line_product_dn_3.product_id, second_delivery_note.line_ids[0].product_id + ) + self.assertEqual( + line_product_dn_3.quantity, second_delivery_note.line_ids[0].product_qty + ) + + def test_analytic_distribution_from_sale_order_line(self): + """Test that analytic_distribution is correctly copied from sale order line.""" + # Create analytic accounts + analytic_account_1 = self.env["account.analytic.account"].create( + { + "name": "Test Analytic Account 1", + "plan_id": self.env.ref("analytic.analytic_plan_projects").id, + } + ) + analytic_account_2 = self.env["account.analytic.account"].create( + { + "name": "Test Analytic Account 2", + "plan_id": self.env.ref("analytic.analytic_plan_projects").id, + } + ) + + # Create sales order with analytic distribution on lines + sales_order = self.create_sales_order( + [ + self.desk_combination_line, + self.large_desk_line, + ] + ) + self.assertEqual(len(sales_order.order_line), 2) + + # Set different analytic distributions on each line + sales_order.order_line[0].write( + { + "analytic_distribution": { + str(analytic_account_1.id): 60.0, + str(analytic_account_2.id): 40.0, + } + } + ) + sales_order.order_line[1].write( + {"analytic_distribution": {str(analytic_account_1.id): 100.0}} + ) + + sales_order.action_confirm() + + picking = sales_order.picking_ids + self.assertEqual(len(picking), 1) + + # Complete the picking + picking.move_ids.quantity = False + picking.move_ids[0].quantity = 1 + picking.move_ids[1].quantity = 1 + + result = picking.button_validate() + self.assertTrue(result) + + delivery_note = self.create_delivery_note() + delivery_note.picking_ids = picking + delivery_note.action_confirm() + + delivery_note.action_invoice() + + invoices = sales_order.invoice_ids + self.assertEqual(len(invoices), 1) + + # Get invoice lines (excluding display types) + invoice_lines = invoices.invoice_line_ids.filtered( + lambda line: line.display_type == "product" + ) + self.assertEqual(len(invoice_lines), 2) + + # Verify analytic distributions are correctly copied + # First line should have 60/40 distribution + line_1 = invoice_lines.filtered( + lambda line: line.product_id == sales_order.order_line[0].product_id + ) + self.assertEqual( + line_1.analytic_distribution, + { + str(analytic_account_1.id): 60.0, + str(analytic_account_2.id): 40.0, + }, + ) + + # Second line should have 100% on account 1 + line_2 = invoice_lines.filtered( + lambda line: line.product_id == sales_order.order_line[1].product_id + ) + self.assertEqual( + line_2.analytic_distribution, + {str(analytic_account_1.id): 100.0}, + ) + + def test_dn_product_name_and_price_in_invoice(self): + """Test configuration options to use DN product name and price in invoice.""" + # Create sales order with 2 lines + sales_order = self.create_sales_order( + [ + self.desk_combination_line, + self.large_desk_line, + ] + ) + self.assertEqual(len(sales_order.order_line), 2) + + # Store original sale order line data + so_line_1_name = sales_order.order_line[0].name + so_line_1_price = sales_order.order_line[0].price_unit + so_line_2_name = sales_order.order_line[1].name + so_line_2_price = sales_order.order_line[1].price_unit + + sales_order.action_confirm() + + picking = sales_order.picking_ids + self.assertEqual(len(picking), 1) + + # Complete the picking + picking.move_ids.quantity = False + picking.move_ids[0].quantity = 1 + picking.move_ids[1].quantity = 1 + + result = picking.button_validate() + self.assertTrue(result) + + # Create delivery note + delivery_note = self.create_delivery_note() + delivery_note.picking_ids = picking + delivery_note.action_confirm() + + # Modify delivery note lines with different name and price + dn_line_1_name = "Custom DN Product Name 1" + dn_line_1_price = 999.99 + dn_line_2_name = "Custom DN Product Name 2" + dn_line_2_price = 777.77 + + delivery_note.line_ids[0].write( + {"name": dn_line_1_name, "price_unit": dn_line_1_price} + ) + delivery_note.line_ids[1].write( + {"name": dn_line_2_name, "price_unit": dn_line_2_price} + ) + + # Test 1: Default behavior (use sale order data) + delivery_note.action_invoice() + + invoices = sales_order.invoice_ids + self.assertEqual(len(invoices), 1) + + invoice_lines = invoices.invoice_line_ids.filtered( + lambda line: line.display_type == "product" + ) + self.assertEqual(len(invoice_lines), 2) + + # Verify sale order data is used by default + inv_line_1 = invoice_lines.filtered( + lambda line: line.product_id == sales_order.order_line[0].product_id + ) + self.assertEqual(inv_line_1.name, so_line_1_name) + self.assertEqual(inv_line_1.price_unit, so_line_1_price) + + inv_line_2 = invoice_lines.filtered( + lambda line: line.product_id == sales_order.order_line[1].product_id + ) + self.assertEqual(inv_line_2.name, so_line_2_name) + self.assertEqual(inv_line_2.price_unit, so_line_2_price) + + # Delete the invoice to test again with configuration enabled + invoices.button_cancel() + invoices.unlink() + + # Reset delivery note status + delivery_note.line_ids.write({"invoice_status": "to invoice"}) + delivery_note._compute_invoice_status() + + # Test 2: Enable configuration to use DN product name + self.env.company.use_dn_product_name_in_invoice = True + + delivery_note.action_invoice() + + invoices = sales_order.invoice_ids + self.assertEqual(len(invoices), 1) + + invoice_lines = invoices.invoice_line_ids.filtered( + lambda line: line.display_type == "product" + ) + self.assertEqual(len(invoice_lines), 2) + + # Verify DN product name is used + inv_line_1 = invoice_lines.filtered( + lambda line: line.product_id == sales_order.order_line[0].product_id + ) + self.assertEqual(inv_line_1.name, dn_line_1_name) + self.assertEqual(inv_line_1.price_unit, so_line_1_price) # Still from SO + + inv_line_2 = invoice_lines.filtered( + lambda line: line.product_id == sales_order.order_line[1].product_id + ) + self.assertEqual(inv_line_2.name, dn_line_2_name) + self.assertEqual(inv_line_2.price_unit, so_line_2_price) # Still from SO + + # Delete the invoice again + invoices.button_cancel() + invoices.unlink() + + # Reset delivery note status + delivery_note.line_ids.write({"invoice_status": "to invoice"}) + delivery_note._compute_invoice_status() + + # Test 3: Enable both configurations (DN name and price) + self.env.company.use_dn_product_name_in_invoice = True + self.env.company.use_dn_price_unit_in_invoice = True + + delivery_note.action_invoice() + + invoices = sales_order.invoice_ids + self.assertEqual(len(invoices), 1) + + invoice_lines = invoices.invoice_line_ids.filtered( + lambda line: line.display_type == "product" + ) + self.assertEqual(len(invoice_lines), 2) + + # Verify both DN product name and price are used + inv_line_1 = invoice_lines.filtered( + lambda line: line.product_id == sales_order.order_line[0].product_id + ) + self.assertEqual(inv_line_1.name, dn_line_1_name) + self.assertEqual(inv_line_1.price_unit, dn_line_1_price) + + inv_line_2 = invoice_lines.filtered( + lambda line: line.product_id == sales_order.order_line[1].product_id + ) + self.assertEqual(inv_line_2.name, dn_line_2_name) + self.assertEqual(inv_line_2.price_unit, dn_line_2_price) + + # Cleanup: disable configurations + self.env.company.use_dn_product_name_in_invoice = False + self.env.company.use_dn_price_unit_in_invoice = False + + def test_return_picking_from_single_so(self): + # + # SO ┐ ┌ DdT + # └ Picking ┤ + # └ Return + + sales_order = self.create_sales_order( + [ + self.desk_combination_line, + self.large_cabinet_line, + ] + ) + self.assertEqual(len(sales_order.order_line), 2) + + sales_order.action_confirm() + + picking = sales_order.picking_ids + self.assertEqual(len(picking), 1) + self.assertEqual(len(picking.move_ids), 2) + + picking.move_ids.quantity = False + picking.move_ids[0].quantity = 1 + picking.move_ids[1].quantity = 11 + + result = picking.button_validate() + self.assertTrue(result) + + delivery_note = self.create_delivery_note() + delivery_note.transport_datetime = datetime.now() + timedelta(days=1, hours=3) + delivery_note.picking_ids = picking + delivery_note.action_confirm() + self.assertEqual(len(delivery_note.line_ids), 2) + self.assertEqual(delivery_note.state, "confirm") + self.assertEqual(delivery_note.invoice_status, "to invoice") + + # create a return picking + stock_return_picking_form = Form( + self.env["stock.return.picking"].with_context( + active_ids=picking.ids, + active_id=picking.id, + active_model="stock.picking", + ) + ) + stock_return_picking = stock_return_picking_form.save() + stock_return_picking.product_return_moves[0].quantity = 0 + stock_return_picking.product_return_moves[1].quantity = 6 + stock_return_picking_action = stock_return_picking.action_create_returns() + return_picking = self.env["stock.picking"].browse( + stock_return_picking_action["res_id"] + ) + return_picking.button_validate() + self.assertEqual( + return_picking.move_ids[0].partner_id.id, picking.partner_id.id + ) + + delivery_note.action_invoice(final=True) + self.assertEqual(len(delivery_note.line_ids), 2) + self.assertEqual(delivery_note.state, "invoiced") + self.assertEqual(delivery_note.invoice_status, "invoiced") + + self.assertEqual(len(sales_order.order_line), 2) + self.assertEqual(sales_order.invoice_status, "invoiced") + + invoices = sales_order.invoice_ids + self.assertEqual(len(invoices), 1) + + final_invoice = invoices[0] + self.assertEqual(len(final_invoice.invoice_line_ids), 3) + self.assertEqual(final_invoice.delivery_note_ids, delivery_note) + + self.assertEqual(delivery_note.invoice_ids, final_invoice) + + # + # Ordine - Linea 1 + # Fattura - Linea 1 + # + + order_line = sales_order.order_line[0] + self.assertEqual(order_line.invoice_status, "invoiced") + self.assertEqual(order_line.qty_to_invoice, 0) + self.assertEqual(order_line.qty_invoiced, 1) + + move = order_line.move_ids + self.assertEqual(len(move), 1) + self.assertEqual(move.quantity, 1) + + delivery_note_line = delivery_note.line_ids[0] + self.assertEqual(delivery_note_line.invoice_status, "invoiced") + self.assertEqual(delivery_note_line.sale_line_id, order_line) + self.assertEqual(delivery_note_line.product_qty, 1) + + invoice_line = final_invoice.invoice_line_ids[1] + self.assertEqual(invoice_line.sale_line_ids, order_line) + self.assertEqual(invoice_line.quantity, 1) + + # + # Ordine - Linea 2 + # Fattura - Linea 2 + # + order_line = sales_order.order_line[1] + self.assertEqual(order_line.invoice_status, "invoiced") + self.assertEqual(order_line.qty_to_invoice, 0) + self.assertEqual(order_line.qty_invoiced, 5) + + moves = order_line.move_ids + self.assertEqual(len(moves), 2) + + delivery_note_line = delivery_note.line_ids[1] + self.assertEqual(delivery_note_line.invoice_status, "invoiced") + self.assertEqual(delivery_note_line.sale_line_id, order_line) + self.assertEqual(delivery_note_line.product_qty, 11) + + invoice_line = final_invoice.invoice_line_ids[2] + self.assertEqual(invoice_line.sale_line_ids, order_line) + self.assertEqual(invoice_line.quantity, 5) + + # + # Fattura - Linea 3 (DdT in fattura) + # + invoice_line = final_invoice.invoice_line_ids[0] + self.assertEqual(invoice_line.display_type, "line_note") + self.assertEqual(invoice_line.quantity, 0) + self.assertEqual(invoice_line.delivery_note_id, delivery_note) + + def test_notes_in_invoice_from_single_so(self): + # + # SO ┐ ┌ DdT + # └ Picking ┘ + # + + sales_order = self.create_sales_order( + [ + self.desk_combination_line, + Command.create( + { + "display_type": "line_note", + "product_uom_qty": 0, + "name": "desk combination note line", + } + ), + self.large_cabinet_line, + Command.create( + { + "display_type": "line_note", + "product_uom_qty": 0, + "name": "large cabinet note line", + } + ), + ] + ) + + self.assertEqual(len(sales_order.order_line), 4) + + sales_order.action_confirm() + + picking = sales_order.picking_ids + self.assertEqual(len(picking), 1) + self.assertEqual(len(picking.move_ids), 2) + + picking.move_ids.quantity = False + picking.move_ids[0].quantity = 1 + picking.move_ids[1].quantity = 11 + + result = picking.button_validate() + self.assertTrue(result) + + delivery_note = self.create_delivery_note() + delivery_note.transport_datetime = datetime.now() + timedelta(days=1, hours=3) + delivery_note.picking_ids = picking + delivery_note.action_confirm() + + self.assertEqual(len(delivery_note.line_ids), 2) + self.assertEqual(delivery_note.state, "confirm") + self.assertEqual(delivery_note.invoice_status, "to invoice") + + sales_order._create_invoices() + self.assertEqual(len(delivery_note.line_ids), 2) + self.assertEqual(delivery_note.state, "invoiced") + self.assertEqual(delivery_note.invoice_status, "invoiced") + + self.assertEqual(len(sales_order.order_line), 4) + self.assertEqual(sales_order.invoice_status, "invoiced") + + invoices = sales_order.invoice_ids + self.assertEqual(len(invoices), 1) + + final_invoice = invoices[0] + + self.assertEqual(len(final_invoice.invoice_line_ids), 5) + self.assertEqual(final_invoice.delivery_note_ids, delivery_note) + + self.assertEqual(delivery_note.invoice_ids, final_invoice) + + # + # Ordine - Linea 1 + # Fattura - Linea 1 + # + + order_line = sales_order.order_line[0] + self.assertEqual(order_line.invoice_status, "invoiced") + self.assertEqual(order_line.qty_to_invoice, 0) + self.assertEqual(order_line.qty_invoiced, 1) + + move = order_line.move_ids + self.assertEqual(len(move), 1) + self.assertEqual(move.quantity, 1) + + delivery_note_line = delivery_note.line_ids[0] + self.assertEqual(delivery_note_line.invoice_status, "invoiced") + self.assertEqual(delivery_note_line.sale_line_id, order_line) + self.assertEqual(delivery_note_line.product_qty, 1) + + invoice_line = final_invoice.invoice_line_ids[1] + self.assertEqual(invoice_line.sale_line_ids, order_line) + self.assertEqual(invoice_line.quantity, 1) + + # + # Ordine - Linea 2(Note) + # Fattura - Linea 2(Note) + # + + order_line = sales_order.order_line[1] + self.assertEqual(order_line.display_type, "line_note") + self.assertEqual(order_line.qty_invoiced, 0) + + invoice_line = final_invoice.invoice_line_ids[2] + self.assertEqual(invoice_line.display_type, "line_note") + self.assertEqual(invoice_line.quantity, 0) + self.assertEqual(invoice_line.sale_line_ids, order_line) + + # + # Ordine - Linea 3 + # Fattura - Linea 3 + # + order_line = sales_order.order_line[2] + self.assertEqual(order_line.invoice_status, "invoiced") + self.assertEqual(order_line.qty_to_invoice, 0) + self.assertEqual(order_line.qty_invoiced, 11) + + move = order_line.move_ids + self.assertEqual(len(move), 1) + self.assertEqual(move.quantity, 11) + + delivery_note_line = delivery_note.line_ids[1] + self.assertEqual(delivery_note_line.invoice_status, "invoiced") + self.assertEqual(delivery_note_line.sale_line_id, order_line) + self.assertEqual(delivery_note_line.product_qty, 11) + + invoice_line = final_invoice.invoice_line_ids[3] + self.assertEqual(invoice_line.sale_line_ids, order_line) + self.assertEqual(invoice_line.quantity, 11) + + # + # Ordine - Linea 4(Note) + # Fattura - Linea 4(Note) + # + order_line = sales_order.order_line[3] + self.assertEqual(order_line.display_type, "line_note") + self.assertEqual(order_line.qty_invoiced, 0) + + invoice_line = final_invoice.invoice_line_ids[4] + self.assertEqual(invoice_line.display_type, "line_note") + self.assertEqual(invoice_line.quantity, 0) + self.assertEqual(invoice_line.sale_line_ids, order_line) + + # + # Fattura - Linea 5 (DdT in fattura) + # + invoice_line = final_invoice.invoice_line_ids[0] + self.assertEqual(invoice_line.display_type, "line_note") + self.assertEqual(invoice_line.quantity, 0) + self.assertEqual(invoice_line.delivery_note_id, delivery_note) diff --git a/l10n_it_delivery_note/views/res_config_settings.xml b/l10n_it_delivery_note/views/res_config_settings.xml index cac1e1cd8c5b..f7f9c12fadb7 100644 --- a/l10n_it_delivery_note/views/res_config_settings.xml +++ b/l10n_it_delivery_note/views/res_config_settings.xml @@ -35,6 +35,12 @@ + + + + + +
    diff --git a/l10n_it_delivery_note/views/stock_delivery_note.xml b/l10n_it_delivery_note/views/stock_delivery_note.xml index 44dc26795ce3..3a9b84899305 100644 --- a/l10n_it_delivery_note/views/stock_delivery_note.xml +++ b/l10n_it_delivery_note/views/stock_delivery_note.xml @@ -33,7 +33,7 @@ string="Validate" />