From f1774c46b3449cd3c8bf656a190c64b767c02a7c Mon Sep 17 00:00:00 2001 From: Dorin Hongu Date: Fri, 6 Feb 2026 21:30:23 +0200 Subject: [PATCH] =?UTF-8?q?[ADD]=20Introduce=20new=20module:=20Vendor=20De?= =?UTF-8?q?livery=20Document=20=E2=86=92=20Wave=20(#2346)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added a new module to simplify supplier delivery handling by generating batch picking (waves) from delivery lines. Includes features for creating delivery documents, assigning open pickings, and generating wave batches with support for excess quantity handling. Updated manifest and added tests, documentation, and essential views to enhance usability. --- README.md | 6 +- deltatech_delivery_wave_doc/README.rst | 151 ++++++++++++++ deltatech_delivery_wave_doc/__init__.py | 2 + deltatech_delivery_wave_doc/__manifest__.py | 19 ++ deltatech_delivery_wave_doc/data/sequence.xml | 10 + .../models/__init__.py | 1 + .../models/delivery_vendor_document.py | 195 ++++++++++++++++++ deltatech_delivery_wave_doc/pyproject.toml | 3 + .../readme/DESCRIPTION.md | 57 +++++ .../security/ir.model.access.csv | 3 + .../static/description/icon.png | Bin 0 -> 11702 bytes .../static/description/index.html | 0 .../static/description/logo-terrabit.png | Bin 0 -> 3047 bytes .../static/description/main_screenshot.png | Bin 0 -> 13007 bytes deltatech_delivery_wave_doc/tests/__init__.py | 1 + .../tests/test_delivery_wave_doc.py | 145 +++++++++++++ .../views/delivery_vendor_document_views.xml | 121 +++++++++++ .../wizard/__init__.py | 1 + .../wizard/import_lines.py | 161 +++++++++++++++ deltatech_putaway_strategy/README.rst | 17 +- .../static/description/index.html | 17 +- 21 files changed, 902 insertions(+), 8 deletions(-) create mode 100644 deltatech_delivery_wave_doc/README.rst create mode 100644 deltatech_delivery_wave_doc/__init__.py create mode 100644 deltatech_delivery_wave_doc/__manifest__.py create mode 100644 deltatech_delivery_wave_doc/data/sequence.xml create mode 100644 deltatech_delivery_wave_doc/models/__init__.py create mode 100644 deltatech_delivery_wave_doc/models/delivery_vendor_document.py create mode 100644 deltatech_delivery_wave_doc/pyproject.toml create mode 100644 deltatech_delivery_wave_doc/readme/DESCRIPTION.md create mode 100644 deltatech_delivery_wave_doc/security/ir.model.access.csv create mode 100644 deltatech_delivery_wave_doc/static/description/icon.png create mode 100644 deltatech_delivery_wave_doc/static/description/index.html create mode 100644 deltatech_delivery_wave_doc/static/description/logo-terrabit.png create mode 100644 deltatech_delivery_wave_doc/static/description/main_screenshot.png create mode 100644 deltatech_delivery_wave_doc/tests/__init__.py create mode 100644 deltatech_delivery_wave_doc/tests/test_delivery_wave_doc.py create mode 100644 deltatech_delivery_wave_doc/views/delivery_vendor_document_views.xml create mode 100644 deltatech_delivery_wave_doc/wizard/__init__.py create mode 100644 deltatech_delivery_wave_doc/wizard/import_lines.py diff --git a/README.md b/README.md index fd3f29a7e0..7cff809420 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ addon | version | maintainers | summary [deltatech_data_sheet_website](deltatech_data_sheet_website/) | 17.0.1.0.0 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Data Sheet [deltatech_dc](deltatech_dc/) | 17.0.1.0.7 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Print Declaration of Conformity [deltatech_delivery_status](deltatech_delivery_status/) | 17.0.2.1.3 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Carrier status on picking +[deltatech_delivery_wave_doc](deltatech_delivery_wave_doc/) | 17.0.1.0.0 | | Document furnizor cu linii ce generează Batch/Wave pe recepții existente [deltatech_download](deltatech_download/) | 17.0.0.1.1 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Generare fisier [deltatech_dropshipping](deltatech_dropshipping/) | 17.0.1.0.0 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Delivery address in picking [deltatech_dummy_queue_job](deltatech_dummy_queue_job/) | 17.0.1.0.0 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Dummy Queue Job @@ -69,6 +70,7 @@ addon | version | maintainers | summary [deltatech_invoice_report](deltatech_invoice_report/) | 17.0.1.0.7 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Invoice Report [deltatech_invoice_to_draft](deltatech_invoice_to_draft/) | 17.0.2.0.1 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Restricted access to reset account move to draft [deltatech_invoice_weight](deltatech_invoice_weight/) | 17.0.1.0.2 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Invoice Weight +[deltatech_kit_price](deltatech_kit_price/) | 17.0.0.0.1 | [![danila12](https://github.com/danila12.png?size=30px)](https://github.com/danila12) | Compute product cost price in sale order line based on kit [deltatech_ledger](deltatech_ledger/) | 17.0.0.0.1 | [![VoicuStefan2001](https://github.com/VoicuStefan2001.png?size=30px)](https://github.com/VoicuStefan2001) | Deltatech Ledger [deltatech_list_view](deltatech_list_view/) | 17.0.1.0.1 | | List View Select Text [deltatech_logistic_docs](deltatech_logistic_docs/) | 17.0.1.0.3 | | Logistic Documents @@ -135,7 +137,7 @@ addon | version | maintainers | summary [deltatech_purchase_stock](deltatech_purchase_stock/) | 17.0.1.0.2 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Purchase Stock [deltatech_purchase_ubl](deltatech_purchase_ubl/) | 17.0.0.0.1 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Import UBL XML vendor invoices to update prices, validate receipts, and create vendor bills [deltatech_purchase_xls](deltatech_purchase_xls/) | 17.0.1.0.9 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Import/export purchase line from/to Excel -[deltatech_putaway_strategy](deltatech_putaway_strategy/) | 17.0.1.0.2 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Location capacities and enhanced putaway strategy for Inventory +[deltatech_putaway_strategy](deltatech_putaway_strategy/) | 17.0.1.0.5 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Location capacities and enhanced putaway strategy for Inventory [deltatech_queue_job](deltatech_queue_job/) | 17.0.1.0.4 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Deltatech Queue Job [deltatech_ral](deltatech_ral/) | 17.0.1.0.3 | | RAL [deltatech_reccurent_task_activity](deltatech_reccurent_task_activity/) | 17.0.0.0.0 | [![VoicuStefan2001](https://github.com/VoicuStefan2001.png?size=30px)](https://github.com/VoicuStefan2001) | Will automatically create an activity at the creation of the recurring task @@ -190,7 +192,7 @@ addon | version | maintainers | summary [deltatech_stock_inventory_product_display](deltatech_stock_inventory_product_display/) | 17.0.0.0.0 | [![VoicuStefan2001](https://github.com/VoicuStefan2001.png?size=30px)](https://github.com/VoicuStefan2001) | Adds product display button on sales and invoices to see the stock of the products in the order [deltatech_stock_negative](deltatech_stock_negative/) | 17.0.2.0.5 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Negative stocks are not allowed [deltatech_stock_picking_activity_report](deltatech_stock_picking_activity_report/) | 17.0.0.0.1 | [![VoicuStefan2001](https://github.com/VoicuStefan2001.png?size=30px)](https://github.com/VoicuStefan2001) | Tracks activities and changes on stock pickings -[deltatech_stock_removal_priority](deltatech_stock_removal_priority/) | 17.0.1.0.0 | | Stock Removal Location by Priority +[deltatech_stock_removal_priority](deltatech_stock_removal_priority/) | 17.0.1.0.3 | | Stock Removal Location by Priority [deltatech_stock_report](deltatech_stock_report/) | 17.0.1.0.3 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Report with positions from picking lists [deltatech_stock_reseller](deltatech_stock_reseller/) | 17.0.1.0.1 | [![dhongu](https://github.com/dhongu.png?size=30px)](https://github.com/dhongu) | Report report reseller [deltatech_stock_sn](deltatech_stock_sn/) | 17.0.1.0.0 | | Stock Serial Number diff --git a/deltatech_delivery_wave_doc/README.rst b/deltatech_delivery_wave_doc/README.rst new file mode 100644 index 0000000000..f3bec11ed7 --- /dev/null +++ b/deltatech_delivery_wave_doc/README.rst @@ -0,0 +1,151 @@ +================================ +Vendor Delivery Document to Wave +================================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:236508e10d4210c25aaf4ac1291ffc70a92eb102c44796a0749f45acf523e4c1 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-dhongu%2Fdeltatech-lightgray.png?logo=github + :target: https://github.com/dhongu/deltatech/tree/17.0/deltatech_delivery_wave_doc + :alt: dhongu/deltatech + +|badge1| |badge2| |badge3| + +Titlu: Vendor Delivery Document → Wave (Batch Picking) + +Scop + +- Modulul permite înregistrarea unei „note de livrare” de la furnizor + (document cu linii: produs, cantitate, preț) și generarea automată a + unui Batch/Wave care reunește recepțiile (pickings) deschise provenite + din comenzi de achiziție (PO) existente pentru acel furnizor. + +Problemă adresată + +- Furnizorul livrează în timp, parțial și amestecat, pe baza unui + document propriu (număr/serie). +- În standard, recepțiile trebuie procesate per PO; acest modul + accelerează operațional recepțiile grupându-le într-un singur Wave pe + baza documentului de livrare. + +Caracteristici cheie + +- Obiect nou: „Vendor Delivery Document” cu câmpuri: Furnizor, Dată, + Număr document, Linii (Produs, Cantitate, UoM, Preț – informativ), + Responsabil, Tip de operație (opțional), „Allow excess”. +- Buton „Generate Wave” pe document: + + - caută recepțiile de tip „incoming” pentru același furnizor și + produsele din document, în stări confirmed/assigned; + - alocă cantitățile din document peste cantitățile „deschise” din + mișcările existente, în ordine cronologică (data programată a + recepțiilor); + - creează automat unul sau mai multe Wave-uri (un wave per combinație + Operation Type × Companie) și atașează liniile de mișcare folosind + API-ul standard ``stock.move.line._add_to_wave``; + - dacă documentul depășește cantitățile deschise din recepții: + + - blochează cu eroare (implicit), sau + - doar loghează diferențele în chatter dacă este bifat „Allow + excess”. + +- Trasabilitate: mesaj în chatter cu lista wave-urilor create și, dacă e + cazul, cu cantitățile neacoperite. +- Fără efecte în Purchase: nu se creează PO-uri sau recepții noi – se + folosește exclusiv ceea ce există deja din PO-urile confirmate. + +Navigare și UI + +- Inventory → Vendor Deliveries → Delivery Documents. +- Formularele includ tab „Lines” pentru produse și tab „Waves” + (read-only) cu valurile create. + +Instalare (Odoo 17) + +- Dependențe: ``mail``, ``stock``, ``stock_picking_batch``, + ``purchase_stock``. +- După instalare, utilizatorii din grupul „Inventory / User” pot crea și + procesa documente. + +Utilizare – pași rapizi + +1) Creați un „Delivery Document” și completați: Furnizor, Dată, Număr + document; opțional: Operation Type (Receipts), Responsabil, Allow + excess. +2) Adăugați linii cu produse și cantități (UoM). Prețul este informativ + și nu influențează recepția. +3) Apăsați „Generate Wave”. Modulul: + + - identifică recepțiile deschise (din PO-uri existente) pentru + furnizor și produsele din document; + - alocă cantitățile și creează Wave-ul/Wave-urile necesare; + - atașează recepțiile la Wave pentru procesare rapidă. + +4) Continuați procesarea valului/valurilor din Inventory (recepție, + validare, etc.). + +Note și limitări + +- Toate transferurile dintr-un Wave trebuie să aparțină aceleiași + companii și aceluiași Operation Type (regulă standard Odoo). Modulul + va crea mai multe Wave-uri dacă este necesar. +- Dacă „Allow excess” nu este bifat, orice cantitate neacoperită de + recepțiile deschise va bloca generarea Wave-ului cu un mesaj explicit + pe produs. +- Modulul nu setează automat ``qty_done``; atașează mișcările în Wave + pentru execuție. (Se poate extinde ulterior pentru precompletare.) +- Prețurile din document sunt doar pentru referință; evaluarea stocului + urmează regulile standard (pe baza PO/valuării configurate). + +Compatibilitate + +- Odoo 17.0 (Enterprise/Community) cu modulele listate la Dependențe. + +Rulare rapidă pentru test (fără HTTP) + +:: + + ./odoo/odoo-bin -c odoo17.conf -d devtest_wave_doc \ + --stop-after-init --no-http \ + -i stock_picking_batch,deltatech_delivery_wave_doc \ + --log-level=info + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `Terrabit Issues `_. +In case of trouble, please check there if your issue has already been reported. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Terrabit +* Dorin Hongu + +Maintainers +----------- + +This module is part of the `dhongu/deltatech `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/deltatech_delivery_wave_doc/__init__.py b/deltatech_delivery_wave_doc/__init__.py new file mode 100644 index 0000000000..9b4296142f --- /dev/null +++ b/deltatech_delivery_wave_doc/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/deltatech_delivery_wave_doc/__manifest__.py b/deltatech_delivery_wave_doc/__manifest__.py new file mode 100644 index 0000000000..c132b212a7 --- /dev/null +++ b/deltatech_delivery_wave_doc/__manifest__.py @@ -0,0 +1,19 @@ +# © 2026 Deltatech +# See README.rst file on addons root folder for license details + +{ + "name": "Vendor Delivery Document to Wave", + "version": "17.0.1.0.0", + "summary": "Document furnizor cu linii ce generează Batch/Wave pe recepții existente", + "author": "Terrabit, Dorin Hongu", + "website": "https://www.terrabit.ro", + "license": "LGPL-3", + "depends": ["mail", "stock", "stock_picking_batch", "purchase_stock"], + "data": [ + "security/ir.model.access.csv", + "data/sequence.xml", + "views/delivery_vendor_document_views.xml", + ], + "installable": True, + "development_status": "Beta", +} diff --git a/deltatech_delivery_wave_doc/data/sequence.xml b/deltatech_delivery_wave_doc/data/sequence.xml new file mode 100644 index 0000000000..d6f42c259b --- /dev/null +++ b/deltatech_delivery_wave_doc/data/sequence.xml @@ -0,0 +1,10 @@ + + + + Delivery Vendor Document + delivery.vendor.document + 5 + DVD/ + + + diff --git a/deltatech_delivery_wave_doc/models/__init__.py b/deltatech_delivery_wave_doc/models/__init__.py new file mode 100644 index 0000000000..c5d3ca54e9 --- /dev/null +++ b/deltatech_delivery_wave_doc/models/__init__.py @@ -0,0 +1 @@ +from . import delivery_vendor_document diff --git a/deltatech_delivery_wave_doc/models/delivery_vendor_document.py b/deltatech_delivery_wave_doc/models/delivery_vendor_document.py new file mode 100644 index 0000000000..147d23b3e7 --- /dev/null +++ b/deltatech_delivery_wave_doc/models/delivery_vendor_document.py @@ -0,0 +1,195 @@ +# © 2026 Deltatech +# See README.rst file on addons root folder for license details + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class DeliveryVendorDocument(models.Model): + _name = "delivery.vendor.document" + _description = "Vendor Delivery Document" + _order = "id desc" + _inherit = ["mail.thread", "mail.activity.mixin"] + + name = fields.Char(default="New", readonly=True) + partner_id = fields.Many2one("res.partner", required=True, domain=[("supplier_rank", ">", 0)]) + date = fields.Date(required=True, default=fields.Date.context_today) + document_no = fields.Char(required=True) + currency_id = fields.Many2one("res.currency", default=lambda self: self.env.company.currency_id.id) + line_ids = fields.One2many("delivery.vendor.document.line", "document_id") + company_id = fields.Many2one("res.company", default=lambda self: self.env.company.id, required=True) + picking_type_id = fields.Many2one( + "stock.picking.type", domain="[('code', '=', 'incoming'), ('company_id', '=', company_id)]" + ) + responsible_id = fields.Many2one("res.users") + wave_id = fields.Many2one("stock.picking.batch", readonly=True) + state = fields.Selection([("draft", "Draft"), ("processed", "Processed")], default="draft") + allow_excess = fields.Boolean(string="Allow excess (log only)") + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if vals.get("name") in (False, "New"): + vals["name"] = self.env["ir.sequence"].next_by_code("delivery.vendor.document") or "New" + records = super().create(vals_list) + return records + + def _moves_domain(self, product_ids): + self.ensure_one() + domain = [ + ("picking_id.partner_id", "=", self.partner_id.id), + ("company_id", "=", self.company_id.id), + ("product_id", "in", product_ids), + ("state", "in", ("confirmed", "assigned")), + ("picking_id.state", "in", ("confirmed", "assigned")), + ("picking_id.picking_type_id.code", "=", "incoming"), + ] + if self.picking_type_id: + domain.append(("picking_id.picking_type_id", "=", self.picking_type_id.id)) + return domain + + def action_generate_wave(self): + self.ensure_one() + if not self.line_ids: + raise UserError(_("There are no lines on the document.")) + if any(l.quantity <= 0 for l in self.line_ids): + raise UserError(_("All quantities must be > 0.")) + + products = self.line_ids.product_id + moves = self.env["stock.move"].search(self._moves_domain(products.ids)) + # sortează cronologic după data programată sau data mișcării + moves = moves.sorted(key=lambda m: (m.picking_id.scheduled_date or m.date, m.picking_id.id, m.id)) + + # open qty în UoM de produs + open_qty = { + m.id: ( + m.product_uom._compute_quantity(m.product_uom_qty, m.product_id.uom_id) + - m.product_uom._compute_quantity(m.quantity_done, m.product_id.uom_id) + ) + for m in moves + } + + allocations = {} # move_id -> qty (in product UoM) + not_covered = [] + + for line in self.line_ids: + need = line.product_uom._compute_quantity(line.quantity, line.product_id.uom_id) + for m in (mv for mv in moves if mv.product_id == line.product_id and open_qty.get(mv.id, 0) > 0): + take = min(need, open_qty[m.id]) + if take <= 0: + continue + allocations[m.id] = allocations.get(m.id, 0) + take + open_qty[m.id] -= take + need -= take + if need <= 0: + break + if need > 0: + if not self.allow_excess: + # compute the remaining qty in the line UoM for the message + rest_in_line_uom = line.product_id.uom_id._compute_quantity(need, line.product_uom) + raise UserError( + _( + f"Product {line.product_id.display_name}: quantity {rest_in_line_uom} {line.product_uom.name} exceeds the open quantity in receipts." + ) + ) + not_covered.append((line, need)) + + if not allocations: + raise UserError(_("No open receipts found for the selected vendor/products.")) + + mls_to_add = self.env["stock.move.line"] + for move in self.env["stock.move"].browse(list(allocations.keys())): + # creează un move line minim dacă nu există niciunul nefinalizat + pending_mls = move.move_line_ids.filtered(lambda ml: ml.state != "done") + if pending_mls: + mls_to_add |= pending_mls + else: + ml = self.env["stock.move.line"].create( + { + "move_id": move.id, + "product_id": move.product_id.id, + "product_uom_id": move.product_uom.id, + "qty_done": 0.0, + "location_id": move.location_id.id, + "location_dest_id": move.location_dest_id.id, + } + ) + mls_to_add |= ml + + # Validations for a single Operation Type and a single company + if not mls_to_add: + raise UserError(_("There are no move lines to add to the wave.")) + picking_types = mls_to_add.picking_id.picking_type_id + companies = mls_to_add.company_id + if len(companies) > 1: + raise UserError( + _( + "The selected operations belong to multiple companies. Please restrict the document to a single company." + ) + ) + if self.picking_type_id and any(pt != self.picking_type_id for pt in picking_types): + raise UserError( + _("There are receipts with a different Operation Type than the one selected on the document.") + ) + # dacă nu e setat pe document, deducem unicitatea + if len(picking_types) > 1: + raise UserError( + _( + "The identified receipts have different Operation Types. Set the Operation Type field on the document to narrow the selection." + ) + ) + + # Creează un singur wave conform cerinței și atașează toate liniile + target_pt = self.picking_type_id or picking_types[0] + target_company = self.company_id or companies[0] + wave = self.env["stock.picking.batch"].create( + { + "is_wave": True, + "picking_type_id": target_pt.id, + "company_id": target_company.id, + "user_id": self.responsible_id.id if self.responsible_id else False, + } + ) + mls_to_add.with_context(active_owner_id=self.responsible_id.id if self.responsible_id else False)._add_to_wave( + wave + ) + + body = _("Generated wave: %s") % (wave.name) + if not_covered: + lines = "\n".join( + f"- {l.product_id.display_name}: missing {l.product_id.uom_id._compute_quantity(need_qty, l.product_uom)} {l.product_uom.name}" + for l, need_qty in not_covered + ) + body += "\n" + _("Quantities not covered by open receipts:") + "\n" + lines + self.message_post(body=body) + self.write({"state": "processed", "wave_id": wave.id}) + + action = self.env["ir.actions.act_window"]._for_xml_id("stock_picking_batch.action_picking_batch") + action.update({"res_id": wave.id, "view_mode": "form", "domain": [("id", "=", wave.id)]}) + return action + + def action_open_import_wizard(self): + self.ensure_one() + action = self.env.ref("deltatech_delivery_wave_doc.action_delivery_document_import_wizard").read()[0] + action["context"] = dict(self.env.context, active_id=self.id, default_file_type="xlsx") + return action + + +class DeliveryVendorDocumentLine(models.Model): + _name = "delivery.vendor.document.line" + _description = "Vendor Delivery Document Line" + + document_id = fields.Many2one("delivery.vendor.document", required=True, ondelete="cascade") + product_id = fields.Many2one("product.product", required=True) + name = fields.Char() + price_unit = fields.Monetary() + quantity = fields.Float(required=True) + product_uom = fields.Many2one("uom.uom", required=True) + currency_id = fields.Many2one(related="document_id.currency_id", store=True, readonly=True) + + @api.onchange("product_id") + def _onchange_product(self): + for l in self: + if l.product_id: + l.name = l.product_id.display_name + l.product_uom = l.product_id.uom_id diff --git a/deltatech_delivery_wave_doc/pyproject.toml b/deltatech_delivery_wave_doc/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/deltatech_delivery_wave_doc/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/deltatech_delivery_wave_doc/readme/DESCRIPTION.md b/deltatech_delivery_wave_doc/readme/DESCRIPTION.md new file mode 100644 index 0000000000..dab4131059 --- /dev/null +++ b/deltatech_delivery_wave_doc/readme/DESCRIPTION.md @@ -0,0 +1,57 @@ + + +Titlu: Vendor Delivery Document → Wave (Batch Picking) + +Scop +- Modulul permite înregistrarea unei „note de livrare” de la furnizor (document cu linii: produs, cantitate, preț) și generarea automată a unui Batch/Wave care reunește recepțiile (pickings) deschise provenite din comenzi de achiziție (PO) existente pentru acel furnizor. + +Problemă adresată +- Furnizorul livrează în timp, parțial și amestecat, pe baza unui document propriu (număr/serie). +- În standard, recepțiile trebuie procesate per PO; acest modul accelerează operațional recepțiile grupându-le într-un singur Wave pe baza documentului de livrare. + +Caracteristici cheie +- Obiect nou: „Vendor Delivery Document” cu câmpuri: Furnizor, Dată, Număr document, Linii (Produs, Cantitate, UoM, Preț – informativ), Responsabil, Tip de operație (opțional), „Allow excess”. +- Buton „Generate Wave” pe document: + - caută recepțiile de tip „incoming” pentru același furnizor și produsele din document, în stări confirmed/assigned; + - alocă cantitățile din document peste cantitățile „deschise” din mișcările existente, în ordine cronologică (data programată a recepțiilor); + - creează automat unul sau mai multe Wave-uri (un wave per combinație Operation Type × Companie) și atașează liniile de mișcare folosind API-ul standard `stock.move.line._add_to_wave`; + - dacă documentul depășește cantitățile deschise din recepții: + - blochează cu eroare (implicit), sau + - doar loghează diferențele în chatter dacă este bifat „Allow excess”. +- Trasabilitate: mesaj în chatter cu lista wave-urilor create și, dacă e cazul, cu cantitățile neacoperite. +- Fără efecte în Purchase: nu se creează PO-uri sau recepții noi – se folosește exclusiv ceea ce există deja din PO-urile confirmate. + +Navigare și UI +- Inventory → Vendor Deliveries → Delivery Documents. +- Formularele includ tab „Lines” pentru produse și tab „Waves” (read-only) cu valurile create. + +Instalare (Odoo 17) +- Dependențe: `mail`, `stock`, `stock_picking_batch`, `purchase_stock`. +- După instalare, utilizatorii din grupul „Inventory / User” pot crea și procesa documente. + +Utilizare – pași rapizi +1) Creați un „Delivery Document” și completați: Furnizor, Dată, Număr document; opțional: Operation Type (Receipts), Responsabil, Allow excess. +2) Adăugați linii cu produse și cantități (UoM). Prețul este informativ și nu influențează recepția. +3) Apăsați „Generate Wave”. Modulul: + - identifică recepțiile deschise (din PO-uri existente) pentru furnizor și produsele din document; + - alocă cantitățile și creează Wave-ul/Wave-urile necesare; + - atașează recepțiile la Wave pentru procesare rapidă. +4) Continuați procesarea valului/valurilor din Inventory (recepție, validare, etc.). + +Note și limitări +- Toate transferurile dintr-un Wave trebuie să aparțină aceleiași companii și aceluiași Operation Type (regulă standard Odoo). Modulul va crea mai multe Wave-uri dacă este necesar. +- Dacă „Allow excess” nu este bifat, orice cantitate neacoperită de recepțiile deschise va bloca generarea Wave-ului cu un mesaj explicit pe produs. +- Modulul nu setează automat `qty_done`; atașează mișcările în Wave pentru execuție. (Se poate extinde ulterior pentru precompletare.) +- Prețurile din document sunt doar pentru referință; evaluarea stocului urmează regulile standard (pe baza PO/valuării configurate). + +Compatibilitate +- Odoo 17.0 (Enterprise/Community) cu modulele listate la Dependențe. + +Rulare rapidă pentru test (fără HTTP) +``` +./odoo/odoo-bin -c odoo17.conf -d devtest_wave_doc \ + --stop-after-init --no-http \ + -i stock_picking_batch,deltatech_delivery_wave_doc \ + --log-level=info +``` + diff --git a/deltatech_delivery_wave_doc/security/ir.model.access.csv b/deltatech_delivery_wave_doc/security/ir.model.access.csv new file mode 100644 index 0000000000..11697d6a2b --- /dev/null +++ b/deltatech_delivery_wave_doc/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_delivery_vendor_document_user,delivery.vendor.document,model_delivery_vendor_document,stock.group_stock_user,1,1,1,1 +access_delivery_vendor_document_line_user,delivery.vendor.document.line,model_delivery_vendor_document_line,stock.group_stock_user,1,1,1,1 diff --git a/deltatech_delivery_wave_doc/static/description/icon.png b/deltatech_delivery_wave_doc/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ad6e34df7767d24c64e50edaab009a68d3320915 GIT binary patch literal 11702 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 literal 0 HcmV?d00001 diff --git a/deltatech_delivery_wave_doc/static/description/main_screenshot.png b/deltatech_delivery_wave_doc/static/description/main_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..4292c67867d7109f0b442e4aa89e044ea3784027 GIT binary patch literal 13007 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 amlP94zL)6v#Q creates incoming picking with moves + self.po = self.env["purchase.order"].create( + { + "partner_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "product_id": self.product_a.id, + "name": "A", + "product_qty": 5, + "product_uom": self.uom_unit.id, + "price_unit": 10, + }, + ), + ( + 0, + 0, + { + "product_id": self.product_b.id, + "name": "B", + "product_qty": 3, + "product_uom": self.uom_unit.id, + "price_unit": 20, + }, + ), + ], + } + ) + self.po.button_confirm() + + # Find the created incoming picking + self.picking = self.env["stock.picking"].search( + [ + ("origin", "=", self.po.name), + ("picking_type_id.code", "=", "incoming"), + ], + limit=1, + ) + self.assertTrue(self.picking, "Incoming picking should have been created after PO confirmation") + + def test_generate_wave_single(self): + Doc = self.env["delivery.vendor.document"] + doc = Doc.create( + { + "partner_id": self.partner.id, + "document_no": "DN-001", + "company_id": self.company.id, + "picking_type_id": self.picking_type_in.id, + "line_ids": [ + (0, 0, {"product_id": self.product_a.id, "quantity": 2, "product_uom": self.uom_unit.id}), + (0, 0, {"product_id": self.product_b.id, "quantity": 1, "product_uom": self.uom_unit.id}), + ], + } + ) + + # Action should create a single wave and set wave_id + action = doc.action_generate_wave() + self.assertTrue(action, "Action should return an act_window") + self.assertTrue(doc.wave_id, "A wave should be created and linked on the document") + self.assertEqual(doc.state, "processed") + + # The wave should contain our picking in draft/in_progress state + batch = doc.wave_id + self.assertEqual(batch.picking_type_id.code, "incoming") + self.assertIn(self.picking, batch.picking_ids, "The PO receipt picking should be part of the wave") + + def test_import_wizard_adds_lines(self): + # Prepare a small CSV in-memory + content = """product,quantity,uom,price_unit\nPROD_A,1,Unit(s),9.5\nPROD_B,2,Unit(s),19\n""" + file_b64 = self.env["ir.binary"]._encode_bytes(content.encode("utf-8")) + + doc = self.env["delivery.vendor.document"].create( + { + "partner_id": self.partner.id, + "document_no": "DN-CSV", + "company_id": self.company.id, + "picking_type_id": self.picking_type_in.id, + } + ) + + wiz = ( + self.env["delivery.vendor.document.import.wizard"] + .with_context(active_id=doc.id) + .create( + { + "file": file_b64, + "filename": "lines.csv", + "file_type": "csv", + "product_match_by": "default_code", + } + ) + ) + wiz.action_import() + + self.assertEqual(len(doc.line_ids), 2, "Two lines should be imported from CSV") + qtys = {l.product_id.default_code: l.quantity for l in doc.line_ids} + self.assertEqual(qtys.get("PROD_A"), 1.0) + self.assertEqual(qtys.get("PROD_B"), 2.0) diff --git a/deltatech_delivery_wave_doc/views/delivery_vendor_document_views.xml b/deltatech_delivery_wave_doc/views/delivery_vendor_document_views.xml new file mode 100644 index 0000000000..721adf4805 --- /dev/null +++ b/deltatech_delivery_wave_doc/views/delivery_vendor_document_views.xml @@ -0,0 +1,121 @@ + + + + delivery.vendor.document.form + delivery.vendor.document + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + delivery.vendor.document.tree + delivery.vendor.document + + + + + + + + + + + + + Vendor Delivery Documents + delivery.vendor.document + tree,form + + + + + delivery.vendor.document.import.wizard.form + delivery.vendor.document.import.wizard + +
+ + + + + + + + +
+
+ + + Import Lines + delivery.vendor.document.import.wizard + form + new + + + + +
diff --git a/deltatech_delivery_wave_doc/wizard/__init__.py b/deltatech_delivery_wave_doc/wizard/__init__.py new file mode 100644 index 0000000000..71ca6d33fa --- /dev/null +++ b/deltatech_delivery_wave_doc/wizard/__init__.py @@ -0,0 +1 @@ +from . import import_lines diff --git a/deltatech_delivery_wave_doc/wizard/import_lines.py b/deltatech_delivery_wave_doc/wizard/import_lines.py new file mode 100644 index 0000000000..c2cab055db --- /dev/null +++ b/deltatech_delivery_wave_doc/wizard/import_lines.py @@ -0,0 +1,161 @@ +# © 2026 Deltatech +# See README.rst file on addons root folder for license details + +import base64 +import csv +from io import BytesIO, StringIO + +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class DeliveryVendorDocumentImportWizard(models.TransientModel): + _name = "delivery.vendor.document.import.wizard" + _description = "Import Delivery Document Lines" + + file = fields.Binary(string="File", required=True, help="Upload an Excel (.xlsx) or CSV file") + filename = fields.Char(string="Filename") + file_type = fields.Selection( + [ + ("xlsx", "Excel (.xlsx)"), + ("csv", "CSV"), + ], + string="File Type", + default="xlsx", + required=True, + ) + product_match_by = fields.Selection( + [ + ("default_code", "Internal Reference"), + ("barcode", "Barcode"), + ("name", "Name"), + ], + string="Match Product By", + default="default_code", + required=True, + ) + delimiter = fields.Char(string="CSV Delimiter", help="Used only for CSV; leave empty to auto-detect", size=1) + + def action_import(self): + self.ensure_one() + doc = self.env["delivery.vendor.document"].browse(self.env.context.get("active_id")) + if not doc: + raise UserError(_("No active delivery document found.")) + if doc.state != "draft": + raise UserError(_("You can import lines only in Draft state.")) + + rows = self._read_rows() + if not rows: + raise UserError(_("The file does not contain any rows.")) + + # expected columns: product, quantity, (optional) uom, (optional) price_unit, (optional) name + errors = [] + for index, row in enumerate(rows, start=2): # assume header at row 1 + try: + product_key = (row.get("product") or "").strip() + qty_val = row.get("quantity") + uom_key = (row.get("uom") or "").strip() + price_val = row.get("price_unit") + name = (row.get("name") or "").strip() + + if not product_key: + raise UserError(_("Row %s: product is required.") % index) + if qty_val in (None, ""): + raise UserError(_("Row %s: quantity is required.") % index) + try: + qty = float(qty_val) + except Exception as err: + raise UserError(_("Row %s: quantity is not a number.") % index) from err + if qty <= 0: + raise UserError(_("Row %s: quantity must be > 0.") % index) + + product = self._match_product(product_key) + uom = product.uom_id + if uom_key: + uom = self.env["uom.uom"].search([("name", "=", uom_key)], limit=1) or uom + + price_unit = None + if price_val not in (None, ""): + try: + price_unit = float(price_val) + except Exception as err: + raise UserError(_("Row %s: price_unit is not a number.") % index) from err + + vals = { + "document_id": doc.id, + "product_id": product.id, + "name": name or product.display_name, + "quantity": qty, + "product_uom": uom.id, + } + if price_unit is not None: + vals["price_unit"] = price_unit + + self.env["delivery.vendor.document.line"].create(vals) + except Exception as e: + errors.append(str(e)) + + if errors: + raise UserError("\n".join(errors)) + + return { + "type": "ir.actions.act_window", + "res_model": "delivery.vendor.document", + "res_id": doc.id, + "view_mode": "form", + "target": "current", + } + + def _read_rows(self): + if self.file_type == "xlsx": + try: + import openpyxl # noqa: F401 + except Exception as err: + raise UserError(_('XLSX support requires the "openpyxl" Python package.')) from err + content = base64.b64decode(self.file or b"") + from openpyxl import load_workbook + + wb = load_workbook(filename=BytesIO(content), read_only=True) + ws = wb.active + rows_iter = ws.iter_rows(values_only=True) + headers = next(rows_iter) + headers = [h.strip() if isinstance(h, str) else h for h in headers] + result = [] + for r in rows_iter: + row = {} + for i, h in enumerate(headers): + if not h: + continue + val = r[i] if i < len(r) else None + row[h] = ( + val + if val is None or isinstance(val, str) + else (str(val) if isinstance(val, int | float) else val) + ) + result.append(row) + return result + # CSV + data = base64.b64decode(self.file or b"") + text = data.decode("utf-8") + if self.delimiter: + reader = csv.DictReader(StringIO(text), delimiter=self.delimiter) + else: + try: + dialect = csv.Sniffer().sniff(text.splitlines()[0]) + reader = csv.DictReader(StringIO(text), dialect=dialect) + except Exception: + reader = csv.DictReader(StringIO(text)) + return list(reader) + + def _match_product(self, key): + key = str(key).strip() + Product = self.env["product.product"] + if self.product_match_by == "default_code": + product = Product.search([("default_code", "=", key)], limit=1) + elif self.product_match_by == "barcode": + product = Product.search([("barcode", "=", key)], limit=1) + else: + product = Product.search([("name", "=", key)], limit=1) + if not product: + raise UserError(_('Product "%s" not found.') % key) + return product diff --git a/deltatech_putaway_strategy/README.rst b/deltatech_putaway_strategy/README.rst index c9cc204b06..bf83ed53fd 100644 --- a/deltatech_putaway_strategy/README.rst +++ b/deltatech_putaway_strategy/README.rst @@ -37,18 +37,27 @@ Key features - ``Max products``: computed capacity for any location (sum of children for non‑leaf locations). - ``Current quantity``: computed on‑hand quantity per location. + - ``Planned quantity``: computed incoming quantity per location based + on pending moves. - ``Occupancy``: computed ratio ``current/max`` (clamped to [0, 1]). - Optimized computation for large hierarchies: - Single batched ``read_group`` on ``stock.quant`` for all leaf locations in the batch. + - Batched ``read_group`` on ``stock.move.line`` for planned + quantities. - Bottom‑up in‑memory aggregation for parent locations. - Smarter putaway: - Respects capacity on leaf locations when suggesting destinations. - - Prefers empty child locations when possible. + - Automatically splits move lines if a destination location reaches + its maximum capacity. + - Prefers empty child locations when possible (if search sublocation + is enabled). + - Optimized rule lookup with database indexes on ``product_id`` and + ``sequence`` for ``stock.putaway.rule``. - Keeps full compatibility with Odoo’s storage category rules (max weight, product/pack capacities, allow new product rules, etc.). @@ -84,11 +93,13 @@ Compatibility Tests ----- -- Includes minimal TransactionCase tests that validate: +- Includes TransactionCase tests that validate: - capacity enforcement via ``_check_can_be_used``, - putaway preference for empty child locations via - ``_get_putaway_strategy``. + ``_get_putaway_strategy``, + - automatic splitting of move lines when capacity is reached, + - planned quantity calculations. Screenshots ----------- diff --git a/deltatech_putaway_strategy/static/description/index.html b/deltatech_putaway_strategy/static/description/index.html index bbbc5b8a81..70eca8a3cf 100644 --- a/deltatech_putaway_strategy/static/description/index.html +++ b/deltatech_putaway_strategy/static/description/index.html @@ -382,18 +382,27 @@

Key features

  • Max products: computed capacity for any location (sum of children for non‑leaf locations).
  • Current quantity: computed on‑hand quantity per location.
  • +
  • Planned quantity: computed incoming quantity per location based +on pending moves.
  • Occupancy: computed ratio current/max (clamped to [0, 1]).
  • Optimized computation for large hierarchies:
    • Single batched read_group on stock.quant for all leaf locations in the batch.
    • +
    • Batched read_group on stock.move.line for planned +quantities.
    • Bottom‑up in‑memory aggregation for parent locations.
  • Smarter putaway:
    • Respects capacity on leaf locations when suggesting destinations.
    • -
    • Prefers empty child locations when possible.
    • +
    • Automatically splits move lines if a destination location reaches +its maximum capacity.
    • +
    • Prefers empty child locations when possible (if search sublocation +is enabled).
    • +
    • Optimized rule lookup with database indexes on product_id and +sequence for stock.putaway.rule.
    • Keeps full compatibility with Odoo’s storage category rules (max weight, product/pack capacities, allow new product rules, etc.).
    @@ -435,10 +444,12 @@

    Compatibility

    Tests

      -
    • Includes minimal TransactionCase tests that validate:
        +
      • Includes TransactionCase tests that validate:
        • capacity enforcement via _check_can_be_used,
        • putaway preference for empty child locations via -_get_putaway_strategy.
        • +_get_putaway_strategy, +
        • automatic splitting of move lines when capacity is reached,
        • +
        • planned quantity calculations.