diff --git a/purchase_container/README.rst b/purchase_container/README.rst new file mode 100644 index 00000000000..5ae4e5e5cac --- /dev/null +++ b/purchase_container/README.rst @@ -0,0 +1,92 @@ +================== +Purchase Container +================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:baa8fdb1bf7c768b597127f43206886bc305180a2ea9536616b333d388fdd394 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-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%2Fpurchase--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/purchase-workflow/tree/16.0/purchase_container + :alt: OCA/purchase-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/purchase-workflow-16-0/purchase-workflow-16-0-purchase_container + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/purchase-workflow&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Add containers to purchase orders and stock pickings. + +|Purchase container image| + +.. |Purchase container image| image:: https://raw.githubusercontent.com/OCA/purchase-workflow/16.0/purchase_container/static/description/img.png + +**Table of contents** + +.. contents:: + :local: + +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 +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion +* Kencove + +Contributors +------------ + +- Akretion + + - Olivier Nibart + - Mathieu Delva + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +.. |maintainer-nayatec| image:: https://github.com/nayatec.png?size=40px + :target: https://github.com/nayatec + :alt: nayatec + +Current `maintainer `__: + +|maintainer-nayatec| + +This module is part of the `OCA/purchase-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_container/__init__.py b/purchase_container/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/purchase_container/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/purchase_container/__manifest__.py b/purchase_container/__manifest__.py new file mode 100644 index 00000000000..15b032d3bd3 --- /dev/null +++ b/purchase_container/__manifest__.py @@ -0,0 +1,28 @@ +{ + "name": "Purchase Container", + "summary": """Add containers to purchase orders and stock pickings.""", + "version": "16.0.1.3.0", + "license": "AGPL-3", + "maintainers": ["nayatec"], + "author": "Akretion, Kencove, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/purchase-workflow", + "depends": [ + "delivery", + "purchase_stock", + "stock", + "stock_landed_costs", + ], + "data": [ + "security/ir.model.access.csv", + "data/container.type.csv", + "data/container_document_type.xml", + "data/cron_data.xml", + "views/container_cost_line.xml", + "views/container_document.xml", + "views/container_line.xml", + "views/container_type.xml", + "views/purchase.xml", + "views/purchase_container.xml", + "views/stock_picking.xml", + ], +} diff --git a/purchase_container/data/container.type.csv b/purchase_container/data/container.type.csv new file mode 100644 index 00000000000..1a166b474e3 --- /dev/null +++ b/purchase_container/data/container.type.csv @@ -0,0 +1,6 @@ +id,name +container_20,20' +container_40,40' +container_40HC,40' HC +container_lcl,LCL +container_lcl_air,LCL Air diff --git a/purchase_container/data/container_document_type.xml b/purchase_container/data/container_document_type.xml new file mode 100644 index 00000000000..dcc3efe1423 --- /dev/null +++ b/purchase_container/data/container_document_type.xml @@ -0,0 +1,91 @@ + + + + + Commercial Invoice + 10 + True + Commercial invoice for customs clearance showing value and description of goods. + + + + Packing List + 20 + True + Detailed list of contents, cartons, weights, and dimensions. + + + + Bill of Lading (B/L) + 30 + True + Original bill of lading from carrier - document of title for cargo. + + + + Certificate of Origin + 40 + False + Document certifying country of origin for preferential duty rates. + + + + Customs Entry + 50 + False + Customs entry documentation and duty payment confirmation. + + + + Insurance Certificate + 60 + False + Marine cargo insurance certificate. + + + + ISF (10+2) + 70 + False + Importer Security Filing for US-bound shipments. + + + + Arrival Notice + 80 + False + Notification from carrier of vessel arrival at port. + + + + Delivery Order + 90 + False + Order authorizing release of cargo from terminal. + + + + Inspection Report + 100 + False + Pre-shipment or upon arrival inspection report. + + diff --git a/purchase_container/data/cron_data.xml b/purchase_container/data/cron_data.xml new file mode 100644 index 00000000000..048e8733fcd --- /dev/null +++ b/purchase_container/data/cron_data.xml @@ -0,0 +1,14 @@ + + + Recompute purchase container state + + + 1 + days + +records = model.search([("state", "in", ["waiting", "transit"])]) +if records: + records._compute_state() + + + diff --git a/purchase_container/i18n/it.po b/purchase_container/i18n/it.po new file mode 100644 index 00000000000..91554974299 --- /dev/null +++ b/purchase_container/i18n/it.po @@ -0,0 +1,510 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_container +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-11-10 10:44+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.container_type_view_form +msgid "40HC" +msgstr "40HC" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_ata +msgid "ATA Date" +msgstr "Data ATA" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_atd +msgid "ATD Date" +msgstr "Data ATD" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_needaction +msgid "Action Needed" +msgstr "Azione richiesta" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_ids +msgid "Activities" +msgstr "Attività" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decorazione eccezione attività" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_state +msgid "Activity State" +msgstr "Stato attività" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_type_icon +msgid "Activity Type Icon" +msgstr "Icona tipo attività" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_ata +msgid "Actual Time Of Arrival" +msgstr "Ora di arrivo attuale" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_atd +msgid "Actual Time Of Departure" +msgstr "Ora di partenza attuale" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Arrival" +msgstr "Arrivo" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__arrival_location_id +msgid "Arrival Location" +msgstr "Località di arrivo" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__arrived +msgid "Arrived" +msgstr "Arrivato" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_attachment_count +msgid "Attachment Count" +msgstr "Conteggio allegati" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__bill_of_lading_ref +msgid "Bill Of Lading No." +msgstr "N° della bolla di carico" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__weight +msgid "Bruto Weight" +msgstr "Peso lordo" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_stock_picking__container_id +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Container" +msgstr "Contenitore" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__code +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Container Reference" +msgstr "Riferimento contenitore" + +#. module: purchase_container +#: model:ir.actions.act_window,name:purchase_container.container_type_action +#: model:ir.ui.menu,name:purchase_container.menu_container_type +msgid "Container Types" +msgstr "Tipi contenitore" + +#. module: purchase_container +#: model:ir.actions.act_window,name:purchase_container.purchase_container_action +#: model:ir.model.fields,field_description:purchase_container.field_purchase_order__container_ids +#: model:ir.ui.menu,name:purchase_container.menu_procurement_management_container +msgid "Containers" +msgstr "Contenitori" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__cost +msgid "Cost" +msgstr "Costo" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__cost_currency_id +msgid "Cost Currency" +msgstr "Valuta costo" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__create_uid +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__create_uid +msgid "Created by" +msgstr "Creato da" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__create_date +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__create_date +msgid "Created on" +msgstr "Creato il" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Departure" +msgstr "Partenza" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__departure_location_id +msgid "Departure Location" +msgstr "Località di partenza" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__description +msgid "Description" +msgstr "Descrizione" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__display_name +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__display_name +msgid "Display Name" +msgstr "Nome visualizzato" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__displayed_incoterm_id +msgid "Displayed Incoterm" +msgstr "Incoterm visualizzati" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_eta +msgid "ETA Date" +msgstr "Data ETA" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_etd +msgid "ETD Date" +msgstr "Data ETD" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_ett +msgid "ETT Date" +msgstr "Data ETT" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_eta +msgid "Estimated Time Of Arrival" +msgstr "Ora di arrivo stimata" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_etd +msgid "Estimated Time Of Departure" +msgstr "Ora di partenza stimata" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_ett +msgid "Estimated Time Of Travel" +msgstr "Tempo di viaggio stimato" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_follower_ids +msgid "Followers" +msgstr "Seguito da" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_partner_ids +msgid "Followers (Partners)" +msgstr "Seguito da (partner)" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "Icona Font Awesome es. fa-tasks" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_search +msgid "Group By" +msgstr "Raggruppa per" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__has_message +msgid "Has Message" +msgstr "Ha un messaggio" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__id +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__id +msgid "ID" +msgstr "ID" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_exception_icon +msgid "Icon" +msgstr "Icona" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "Icona per indicare un'attività eccezione." + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_needaction +msgid "If checked, new messages require your attention." +msgstr "Se selezionata, nuovi messaggi richiedono attenzione." + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna." + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__incoterm_id +msgid "Incoterm" +msgstr "Incoterm" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_is_follower +msgid "Is Follower" +msgstr "Segue" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__is_locked +msgid "Is Locked" +msgstr "È bloccato" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__write_uid +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__write_uid +msgid "Last Updated by" +msgstr "Ultimo aggiornamento di" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__write_date +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__write_date +msgid "Last Updated on" +msgstr "Ultimo aggiornamento il" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Lock" +msgstr "Blocco" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__locked +msgid "Locked" +msgstr "Bloccato" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__manual_incoterm_id +msgid "Manual Incoterm" +msgstr "Incoterm manuale" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_has_error +msgid "Message Delivery error" +msgstr "Errore di consegna messaggio" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_ids +msgid "Messages" +msgstr "Messaggi" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "Scadenza mia attività" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__name +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__name +msgid "Name" +msgstr "Nome" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "Scadenza prossima attività" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_summary +msgid "Next Activity Summary" +msgstr "Riepilogo prossima attività" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_type_id +msgid "Next Activity Type" +msgstr "Tipo prossima attività" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_needaction_counter +msgid "Number of Actions" +msgstr "Numero di azioni" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_has_error_counter +msgid "Number of errors" +msgstr "Numero di errori" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "Numero di messaggi che richiedono un'azione" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "Numero di messaggi con errore di consegna" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_order_view_form_container +msgid "Open" +msgstr "Apri" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__package_qty +msgid "Package Qty" +msgstr "Q.tà collo" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_purchase_order +msgid "Purchase Order" +msgstr "Ordine di acquisto" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Purchase Orders" +msgstr "Ordini di acquisto" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_purchase_container +msgid "Purchase order related container" +msgstr "Contenitore relativo all'ordine di acquisto" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Purchases" +msgstr "Acquisti" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_rfq_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "RFQ" +msgstr "RdP" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__picking_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Receipts" +msgstr "Ricevute" + +#. module: purchase_container +#: model:ir.actions.server,name:purchase_container.ir_cron_recompute_purchase_container_state_ir_actions_server +msgid "Recompute purchase container state" +msgstr "Ricalcola stato container acquisto" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__picking_ids +msgid "Related Pickings" +msgstr "Prelievi correlati" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_ids +msgid "Related Purchases" +msgstr "Acquisti correlati" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_user_id +msgid "Responsible User" +msgstr "Utente responsabile" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__shipping_agent_id +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_search +msgid "Shipping Agent" +msgstr "Agente spedizione" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__state +msgid "State" +msgstr "Stato" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" +"Stato in base alle attività\n" +"Scaduto: la data richiesta è trascorsa\n" +"Oggi: la data attività è oggi\n" +"Pianificato: attività future." + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_stock_picking +msgid "Transfer" +msgstr "Trasferimento" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Transfers" +msgstr "Trasferimenti" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__transit +msgid "Transit" +msgstr "Transito" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__type_id +msgid "Type" +msgstr "Tipo" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo di attività eccezione sul record." + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Unlock" +msgstr "Sblocca" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_container_type +msgid "Usual containers" +msgstr "Contenitori consueto" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__volume +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Volume" +msgstr "Volume" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__volume_uom_id +msgid "Volume Units of Measure" +msgstr "Unità di misura del volume" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__waiting +msgid "Waiting" +msgstr "Attesa" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__website_message_ids +msgid "Website Messages" +msgstr "Messaggi sito web" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__website_message_ids +msgid "Website communication history" +msgstr "Cronologia comunicazioni sito web" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__weight_uom_id +msgid "Weight Unit of Measure" +msgstr "Unità di misura del peso" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__weight_uom_id +msgid "Weight Units of Measure" +msgstr "Unità di misura del peso" + +#~ msgid "SMS Delivery error" +#~ msgstr "Errore consegna SMS" diff --git a/purchase_container/i18n/pt_BR.po b/purchase_container/i18n/pt_BR.po new file mode 100644 index 00000000000..506b6c99030 --- /dev/null +++ b/purchase_container/i18n/pt_BR.po @@ -0,0 +1,503 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_container +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2025-09-03 19:43+0000\n" +"Last-Translator: Douglas Custódio \n" +"Language-Team: none\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 5.10.4\n" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.container_type_view_form +msgid "40HC" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_ata +msgid "ATA Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_atd +msgid "ATD Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_ids +msgid "Activities" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "Decoração de Exceção de Atividade" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_state +msgid "Activity State" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_type_icon +msgid "Activity Type Icon" +msgstr "Ícone do Tipo de Atividade" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_ata +msgid "Actual Time Of Arrival" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_atd +msgid "Actual Time Of Departure" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Arrival" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__arrival_location_id +msgid "Arrival Location" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__arrived +msgid "Arrived" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__bill_of_lading_ref +msgid "Bill Of Lading No." +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__weight +msgid "Bruto Weight" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_stock_picking__container_id +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Container" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__code +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Container Reference" +msgstr "" + +#. module: purchase_container +#: model:ir.actions.act_window,name:purchase_container.container_type_action +#: model:ir.ui.menu,name:purchase_container.menu_container_type +msgid "Container Types" +msgstr "" + +#. module: purchase_container +#: model:ir.actions.act_window,name:purchase_container.purchase_container_action +#: model:ir.model.fields,field_description:purchase_container.field_purchase_order__container_ids +#: model:ir.ui.menu,name:purchase_container.menu_procurement_management_container +msgid "Containers" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__cost +msgid "Cost" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__cost_currency_id +msgid "Cost Currency" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__create_uid +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__create_uid +msgid "Created by" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__create_date +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__create_date +msgid "Created on" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Departure" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__departure_location_id +msgid "Departure Location" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__description +msgid "Description" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__display_name +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__display_name +msgid "Display Name" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__displayed_incoterm_id +msgid "Displayed Incoterm" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_eta +msgid "ETA Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_etd +msgid "ETD Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_ett +msgid "ETT Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_eta +msgid "Estimated Time Of Arrival" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_etd +msgid "Estimated Time Of Departure" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_ett +msgid "Estimated Time Of Travel" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_search +msgid "Group By" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__has_message +msgid "Has Message" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__id +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__id +msgid "ID" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__incoterm_id +msgid "Incoterm" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__is_locked +msgid "Is Locked" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__write_uid +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__write_date +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__write_date +msgid "Last Updated on" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Lock" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__locked +msgid "Locked" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__manual_incoterm_id +msgid "Manual Incoterm" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_ids +msgid "Messages" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__name +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__name +msgid "Name" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_order_view_form_container +msgid "Open" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__package_qty +msgid "Package Qty" +msgstr "" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_purchase_order +msgid "Purchase Order" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Purchase Orders" +msgstr "" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_purchase_container +msgid "Purchase order related container" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Purchases" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_rfq_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "RFQ" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__picking_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Receipts" +msgstr "" + +#. module: purchase_container +#: model:ir.actions.server,name:purchase_container.ir_cron_recompute_purchase_container_state_ir_actions_server +msgid "Recompute purchase container state" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__picking_ids +msgid "Related Pickings" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_ids +msgid "Related Purchases" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__shipping_agent_id +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_search +msgid "Shipping Agent" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__state +msgid "State" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Transfers" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__transit +msgid "Transit" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__type_id +msgid "Type" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "Tipo da atividade de exceção no registro." + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Unlock" +msgstr "" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_container_type +msgid "Usual containers" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__volume +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Volume" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__volume_uom_id +msgid "Volume Units of Measure" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__waiting +msgid "Waiting" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__weight_uom_id +msgid "Weight Unit of Measure" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__weight_uom_id +msgid "Weight Units of Measure" +msgstr "" diff --git a/purchase_container/i18n/purchase_container.pot b/purchase_container/i18n/purchase_container.pot new file mode 100644 index 00000000000..c2768cf114a --- /dev/null +++ b/purchase_container/i18n/purchase_container.pot @@ -0,0 +1,500 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_container +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.container_type_view_form +msgid "40HC" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_ata +msgid "ATA Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_atd +msgid "ATD Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_ids +msgid "Activities" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_state +msgid "Activity State" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_ata +msgid "Actual Time Of Arrival" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_atd +msgid "Actual Time Of Departure" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Arrival" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__arrival_location_id +msgid "Arrival Location" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__arrived +msgid "Arrived" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__bill_of_lading_ref +msgid "Bill Of Lading No." +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__weight +msgid "Bruto Weight" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_stock_picking__container_id +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Container" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__code +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Container Reference" +msgstr "" + +#. module: purchase_container +#: model:ir.actions.act_window,name:purchase_container.container_type_action +#: model:ir.ui.menu,name:purchase_container.menu_container_type +msgid "Container Types" +msgstr "" + +#. module: purchase_container +#: model:ir.actions.act_window,name:purchase_container.purchase_container_action +#: model:ir.model.fields,field_description:purchase_container.field_purchase_order__container_ids +#: model:ir.ui.menu,name:purchase_container.menu_procurement_management_container +msgid "Containers" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__cost +msgid "Cost" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__cost_currency_id +msgid "Cost Currency" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__create_uid +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__create_uid +msgid "Created by" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__create_date +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__create_date +msgid "Created on" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Departure" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__departure_location_id +msgid "Departure Location" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__description +msgid "Description" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__display_name +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__display_name +msgid "Display Name" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__displayed_incoterm_id +msgid "Displayed Incoterm" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_eta +msgid "ETA Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_etd +msgid "ETD Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__date_ett +msgid "ETT Date" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_eta +msgid "Estimated Time Of Arrival" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_etd +msgid "Estimated Time Of Departure" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__date_ett +msgid "Estimated Time Of Travel" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_search +msgid "Group By" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__has_message +msgid "Has Message" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__id +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__id +msgid "ID" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_has_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__incoterm_id +msgid "Incoterm" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__is_locked +msgid "Is Locked" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__write_uid +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__write_date +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__write_date +msgid "Last Updated on" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Lock" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__locked +msgid "Locked" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__manual_incoterm_id +msgid "Manual Incoterm" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_ids +msgid "Messages" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_container_type__name +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__name +msgid "Name" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_needaction_counter +msgid "Number of messages requiring action" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_order_view_form_container +msgid "Open" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__package_qty +msgid "Package Qty" +msgstr "" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_purchase_order +msgid "Purchase Order" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Purchase Orders" +msgstr "" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_purchase_container +msgid "Purchase order related container" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Purchases" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_rfq_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "RFQ" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__picking_count +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Receipts" +msgstr "" + +#. module: purchase_container +#: model:ir.actions.server,name:purchase_container.ir_cron_recompute_purchase_container_state_ir_actions_server +msgid "Recompute purchase container state" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__picking_ids +msgid "Related Pickings" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__purchase_order_ids +msgid "Related Purchases" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__shipping_agent_id +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_search +msgid "Shipping Agent" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__state +msgid "State" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Transfers" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__transit +msgid "Transit" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__type_id +msgid "Type" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: purchase_container +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Unlock" +msgstr "" + +#. module: purchase_container +#: model:ir.model,name:purchase_container.model_container_type +msgid "Usual containers" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__volume +#: model_terms:ir.ui.view,arch_db:purchase_container.purchase_container_view_form +msgid "Volume" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__volume_uom_id +msgid "Volume Units of Measure" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields.selection,name:purchase_container.selection__purchase_container__state__waiting +msgid "Waiting" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,help:purchase_container.field_purchase_container__weight_uom_id +msgid "Weight Unit of Measure" +msgstr "" + +#. module: purchase_container +#: model:ir.model.fields,field_description:purchase_container.field_purchase_container__weight_uom_id +msgid "Weight Units of Measure" +msgstr "" diff --git a/purchase_container/models/__init__.py b/purchase_container/models/__init__.py new file mode 100644 index 00000000000..484bbd40c3a --- /dev/null +++ b/purchase_container/models/__init__.py @@ -0,0 +1,9 @@ +from . import ( + container_cost_line, + container_document, + container_line, + container_type, + purchase_container, + purchase_order, + stock_picking, +) diff --git a/purchase_container/models/container_cost_line.py b/purchase_container/models/container_cost_line.py new file mode 100644 index 00000000000..c16febe1190 --- /dev/null +++ b/purchase_container/models/container_cost_line.py @@ -0,0 +1,111 @@ +from odoo import api, fields, models + + +class ContainerCostLine(models.Model): + """Track individual cost components for a container. + + Allows detailed breakdown of all costs associated with importing a container: + - Ocean freight (from freight forwarder like RL Swearer) + - Drayage/local trucking + - Customs holds and fees + - Tariffs and duties + - Per diem charges + - Other miscellaneous fees + """ + + _name = "container.cost.line" + _description = "Container Cost Line" + _order = "container_id, sequence, id" + + container_id = fields.Many2one( + "purchase.container", + string="Container", + required=True, + ondelete="cascade", + index=True, + ) + sequence = fields.Integer(default=10) + + cost_type = fields.Selection( + [ + ("ocean_freight", "Ocean Freight"), + ("drayage", "Drayage / Local Freight"), + ("customs_hold", "Customs Hold"), + ("tariff", "Tariffs / Duties"), + ("per_diem", "Per Diem"), + ("storage", "Storage Fees"), + ("documentation", "Documentation Fees"), + ("inspection", "Inspection Fees"), + ("other", "Other"), + ], + required=True, + default="other", + ) + name = fields.Char( + string="Description", + required=True, + help="Description of the cost", + ) + amount = fields.Monetary( + currency_field="currency_id", + required=True, + help="Cost amount", + ) + currency_id = fields.Many2one( + "res.currency", + string="Currency", + default=lambda self: self.env.company.currency_id, + required=True, + ) + + # Link to vendor/invoice + partner_id = fields.Many2one( + "res.partner", + string="Vendor", + help="Vendor who charged this cost", + ) + invoice_id = fields.Many2one( + "account.move", + string="Invoice", + domain=[("move_type", "=", "in_invoice")], + help="Related vendor bill", + ) + invoice_line_id = fields.Many2one( + "account.move.line", + string="Invoice Line", + domain="[('move_id', '=', invoice_id)]", + help="Specific invoice line for this cost", + ) + + # Additional info + date = fields.Date( + default=fields.Date.context_today, + help="Date of the cost/charge", + ) + notes = fields.Text(help="Additional notes about this cost") + + @api.onchange("cost_type") + def _onchange_cost_type(self): + """Set default name based on cost type.""" + if self.cost_type and not self.name: + type_labels = dict(self._fields["cost_type"].selection) + self.name = type_labels.get(self.cost_type, "") + + @api.onchange("invoice_line_id") + def _onchange_invoice_line_id(self): + """Auto-fill from invoice line.""" + if self.invoice_line_id: + self.amount = self.invoice_line_id.price_subtotal + self.partner_id = self.invoice_line_id.partner_id + if not self.name: + self.name = self.invoice_line_id.name + + def name_get(self): + result = [] + for line in self: + name = f"{line.container_id.code}: {line.name}" + if line.amount: + symbol = line.currency_id.symbol or "" + name += f" ({symbol}{line.amount:,.2f})" # noqa: E231 + result.append((line.id, name)) + return result diff --git a/purchase_container/models/container_document.py b/purchase_container/models/container_document.py new file mode 100644 index 00000000000..f99e37ace80 --- /dev/null +++ b/purchase_container/models/container_document.py @@ -0,0 +1,92 @@ +from odoo import api, fields, models + + +class ContainerDocumentType(models.Model): + """Types of documents required for container shipments.""" + + _name = "container.document.type" + _description = "Container Document Type" + _order = "sequence, name" + + name = fields.Char(required=True, translate=True) + sequence = fields.Integer(default=10) + required = fields.Boolean( + default=False, + help="If checked, this document type is required for all containers", + ) + active = fields.Boolean(default=True) + description = fields.Text(help="Description or instructions for this document type") + + +class ContainerDocument(models.Model): + """Track shipping documents attached to containers.""" + + _name = "container.document" + _description = "Container Document" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "container_id, document_type_id" + + container_id = fields.Many2one( + "purchase.container", + string="Container", + required=True, + ondelete="cascade", + index=True, + ) + document_type_id = fields.Many2one( + "container.document.type", + string="Document Type", + required=True, + ondelete="restrict", + ) + name = fields.Char(compute="_compute_name", store=True) + attachment_id = fields.Many2one( + "ir.attachment", + string="Attachment", + help="The uploaded document file", + ) + filename = fields.Char(related="attachment_id.name", string="Filename") + state = fields.Selection( + [ + ("missing", "Missing"), + ("pending", "Pending Review"), + ("approved", "Approved"), + ("rejected", "Rejected"), + ], + default="missing", + required=True, + tracking=True, + ) + reference = fields.Char( + help="External reference number for this document (e.g., invoice number)" + ) + date_received = fields.Date() + date_approved = fields.Date() + notes = fields.Text() + required = fields.Boolean( + related="document_type_id.required", string="Required", store=True + ) + + @api.depends("container_id.code", "document_type_id.name") + def _compute_name(self): + for record in self: + record.name = f"{record.container_id.code} - {record.document_type_id.name}" + + @api.onchange("attachment_id") + def _onchange_attachment_id(self): + """Auto-update state when attachment is added.""" + if self.attachment_id and self.state == "missing": + self.state = "pending" + self.date_received = fields.Date.today() + + def action_approve(self): + """Approve the document.""" + self.write({"state": "approved", "date_approved": fields.Date.today()}) + + def action_reject(self): + """Reject the document.""" + self.write({"state": "rejected"}) + + def action_reset(self): + """Reset to pending for re-review.""" + self.write({"state": "pending", "date_approved": False}) diff --git a/purchase_container/models/container_line.py b/purchase_container/models/container_line.py new file mode 100644 index 00000000000..a71a2859567 --- /dev/null +++ b/purchase_container/models/container_line.py @@ -0,0 +1,110 @@ +from odoo import api, fields, models + + +class ContainerLine(models.Model): + """Track individual products/quantities within a container.""" + + _name = "container.line" + _description = "Container Line" + _order = "container_id, sequence, id" + + container_id = fields.Many2one( + "purchase.container", + string="Container", + required=True, + ondelete="cascade", + index=True, + ) + sequence = fields.Integer(default=10) + product_id = fields.Many2one( + "product.product", + string="Product", + required=True, + domain=[("purchase_ok", "=", True)], + ) + product_tmpl_id = fields.Many2one( + related="product_id.product_tmpl_id", string="Product Template", store=True + ) + hs_code = fields.Char( + related="product_tmpl_id.hs_code", + string="HTS Code", + store=True, + help="Harmonized Tariff Schedule code for customs classification", + ) + description = fields.Text() + quantity = fields.Float( + digits="Product Unit of Measure", + default=1.0, + required=True, + ) + product_uom_id = fields.Many2one( + "uom.uom", + string="Unit of Measure", + domain="[('category_id', '=', product_uom_category_id)]", + ) + product_uom_category_id = fields.Many2one( + related="product_id.uom_id.category_id", string="UoM Category" + ) + + # Carton/package information + carton_qty = fields.Integer( + string="Cartons", + help="Number of cartons/packages for this line", + ) + units_per_carton = fields.Float( + string="Units/Carton", + help="Number of units per carton", + ) + + # Weight and volume + weight = fields.Float( + digits="Stock Weight", + help="Total weight for this line", + ) + volume = fields.Float( + digits="Volume", + help="Total volume for this line (CBM)", + ) + + # Purchase order reference + purchase_line_id = fields.Many2one( + "purchase.order.line", + string="Purchase Order Line", + help="Link to the original purchase order line", + ) + purchase_order_id = fields.Many2one( + related="purchase_line_id.order_id", string="Purchase Order", store=True + ) + + # Landed cost allocation + landed_cost = fields.Monetary( + currency_field="currency_id", + help="Allocated landed cost for this line", + ) + currency_id = fields.Many2one( + "res.currency", + string="Currency", + default=lambda self: self.env.company.currency_id, + ) + + @api.onchange("product_id") + def _onchange_product_id(self): + """Set default values from product.""" + if self.product_id: + self.product_uom_id = self.product_id.uom_po_id or self.product_id.uom_id + self.description = self.product_id.display_name + + @api.onchange("carton_qty", "units_per_carton") + def _onchange_carton_info(self): + """Auto-calculate quantity from carton information.""" + if self.carton_qty and self.units_per_carton: + self.quantity = self.carton_qty * self.units_per_carton + + def name_get(self): + result = [] + for line in self: + name = f"{line.container_id.code}: {line.product_id.display_name}" + if line.quantity: + name += f" ({line.quantity} {line.product_uom_id.name or ''})" + result.append((line.id, name)) + return result diff --git a/purchase_container/models/container_type.py b/purchase_container/models/container_type.py new file mode 100644 index 00000000000..a366b72a2cb --- /dev/null +++ b/purchase_container/models/container_type.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class ContainerType(models.Model): + _name = "container.type" + _description = "Usual containers" + + name = fields.Char() + description = fields.Char() diff --git a/purchase_container/models/purchase_container.py b/purchase_container/models/purchase_container.py new file mode 100644 index 00000000000..5078e598f66 --- /dev/null +++ b/purchase_container/models/purchase_container.py @@ -0,0 +1,595 @@ +from datetime import date + +from odoo import api, fields, models + + +class PurchaseContainer(models.Model): + _name = "purchase.container" + _description = "Purchase order related container" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "id desc" + + name = fields.Char(compute="_compute_name", store=True, readonly=True, index=True) + code = fields.Char( + string="Container Reference", + compute="_compute_code", + inverse="_inverse_code", + store=True, + required=True, + copy=False, + ) + bill_of_lading_ref = fields.Char("Bill Of Lading No.", copy=False) + seal_number = fields.Char("Seal #", copy=False, help="Container seal number") + shipping_agent_id = fields.Many2one( + comodel_name="res.partner", + string="Freight Forwarder", + help="Freight forwarding company (e.g., RL Swearer)", + ) + freight_forwarder_ref = fields.Char( + help="Freight forwarder booking/reference number (e.g., RL Swearer ID: B00064516)", + tracking=True, + ) + drayage_company_id = fields.Many2one( + comodel_name="res.partner", + string="Drayage Company", + help="Local drayage/trucking company for final delivery", + ) + type_id = fields.Many2one(comodel_name="container.type") + package_qty = fields.Integer(copy=False) + cost = fields.Float(digits="Product Price", copy=False, string="Ocean Freight Cost") + cost_currency_id = fields.Many2one("res.currency", "Cost Currency", copy=False) + has_additional_fees = fields.Boolean( + string="Has Add'l Fees", + help="Check if there are per diem or additional fees to note", + ) + per_diem_fees = fields.Float( + digits="Product Price", + copy=False, + string="Per Diem / Add'l Fees", + help="Per diem and additional fees amount", + ) + per_diem_reason = fields.Text( + "Fee Reason", help="Reason for per diem or additional fees" + ) + notes = fields.Text(help="Internal notes about this container") + volume = fields.Float(digits="Volume", copy=False) + volume_uom_id = fields.Many2one( + "uom.uom", + string="Volume Units of Measure", + domain=lambda self: [ + ("category_id", "=", self.env.ref("uom.product_uom_categ_vol").id) + ], + default=lambda self: self.env[ + "product.template" + ]._get_volume_uom_id_from_ir_config_parameter(), + ) + weight = fields.Float(string="Bruto Weight", digits="Stock Weight", copy=False) + weight_uom_id = fields.Many2one( + "uom.uom", + string="Weight Units of Measure", + domain=lambda self: [ + ("category_id", "=", self.env.ref("uom.product_uom_categ_kgm").id) + ], + help="Weight Unit of Measure", + default=lambda self: self.env[ + "product.template" + ]._get_weight_uom_id_from_ir_config_parameter(), + ) + purchase_order_ids = fields.Many2many( + "purchase.order", string="Related Purchases", copy=False + ) + purchase_order_count = fields.Integer( + string="Purchases", compute="_compute_purchase_order_count" + ) + purchase_order_rfq_count = fields.Integer( + string="RFQ", compute="_compute_purchase_order_rfq_count" + ) + picking_ids = fields.One2many( + comodel_name="stock.picking", + inverse_name="container_id", + string="Related Pickings", + ) + picking_count = fields.Integer(string="Receipts", compute="_compute_picking_count") + + incoterm_id = fields.Many2one( + "account.incoterms", compute="_compute_incoterm_id", store=False, readonly=True + ) + manual_incoterm_id = fields.Many2one("account.incoterms") + displayed_incoterm_id = fields.Many2one( + "account.incoterms", + compute="_compute_displayed_incoterm_id", + inverse="_inverse_displayed_incoterm_id", + store=True, + tracking=True, + ) + + departure_location_id = fields.Many2one( + "res.partner", string="Port of Lading", help="Origin port" + ) + arrival_location_id = fields.Many2one( + "res.partner", string="Port of Discharge", help="Destination port" + ) + warehouse_id = fields.Many2one( + "stock.warehouse", + string="Destination Warehouse", + help="Final destination warehouse (PA, ID, GA)", + ) + date_eta = fields.Date( + string="Port ETA", help="Estimated Time Of Arrival at Port", tracking=True + ) + date_etd = fields.Date( + string="ETD Date", help="Estimated Time Of Departure", tracking=True + ) + date_warehouse_eta = fields.Date( + string="Warehouse ETA", + help="Estimated Time Of Arrival at final warehouse", + tracking=True, + ) + date_ata = fields.Date( + string="ATA Date", help="Actual Time Of Arrival at Port", tracking=True + ) + date_atd = fields.Date( + string="ATD Date", help="Actual Time Of Departure", tracking=True + ) + date_delivered = fields.Date( + string="Delivered Date", + help="Date container was delivered to warehouse", + tracking=True, + ) + date_received = fields.Date( + string="Received Date", + help="Date inventory was received into system", + tracking=True, + ) + date_ett = fields.Char( + string="ETT Date", + help="Estimated Time Of Travel", + compute="_compute_date_ett", + store=False, + tracking=True, + ) + + state = fields.Selection( + [ + ("in_progress", "In Progress"), + ("pos_confirmed", "POs Confirmed"), + ("freight_notified", "Freight Forwarder Notified"), + ("on_water", "On the Water"), + ("arrival_notice", "Arrival Notice"), + ("drayage_confirmed", "Drayage Confirmed"), + ("awaiting_gate_out", "Awaiting Gate Out"), + ("customs_hold", "Customs Hold"), + ("released", "Released for Delivery"), + ("delivered", "Delivered"), + ("received", "Received"), + ("on_hold", "On Hold"), + ("ready_to_process", "Ready to Process"), + ("processed", "Processed"), + ], + default="in_progress", + tracking=True, + copy=False, + ) + is_locked = fields.Boolean() + + # Document tracking + document_ids = fields.One2many( + "container.document", + "container_id", + string="Documents", + ) + document_count = fields.Integer(compute="_compute_document_count") + documents_complete = fields.Boolean( + string="Docs Complete", + compute="_compute_documents_complete", + store=True, + help="All required documents have been approved", + ) + + # Container line items + line_ids = fields.One2many( + "container.line", + "container_id", + string="Container Lines", + ) + line_count = fields.Integer(string="Lines", compute="_compute_line_count") + + # Shipment tracking + carrier_id = fields.Many2one( + "res.partner", + string="Shipping Carrier", + help="Ocean carrier / shipping line (e.g., Maersk, MSC, COSCO)", + ) + vessel_name = fields.Char(help="Name of the vessel/ship") + voyage_number = fields.Char(help="Voyage or trip number") + tracking_number = fields.Char( + help="Carrier tracking number or booking reference", + tracking=True, + ) + tracking_url = fields.Char( + compute="_compute_tracking_url", + help="URL to track shipment on carrier website", + ) + last_tracking_update = fields.Datetime( + help="When tracking information was last updated", + ) + tracking_status = fields.Char(help="Latest status from carrier tracking") + + # Invoice tracking for landed cost readiness + freight_invoice_id = fields.Many2one( + "account.move", + string="Freight Invoice", + domain=[("move_type", "=", "in_invoice")], + help="Vendor bill from freight forwarder (e.g., RL Swearer)", + tracking=True, + ) + freight_invoice_state = fields.Selection( + related="freight_invoice_id.state", + string="Freight Invoice Status", + ) + drayage_invoice_id = fields.Many2one( + "account.move", + string="Drayage Invoice", + domain=[("move_type", "=", "in_invoice")], + help="Vendor bill from drayage/trucking company", + tracking=True, + ) + drayage_invoice_state = fields.Selection( + related="drayage_invoice_id.state", + string="Drayage Invoice Status", + ) + ready_for_landed_cost = fields.Boolean( + compute="_compute_ready_for_landed_cost", + store=True, + help="True when both freight and drayage invoices are received/posted", + ) + + # Cost breakdown lines + cost_line_ids = fields.One2many( + "container.cost.line", + "container_id", + string="Cost Breakdown", + help="Detailed breakdown of all costs", + ) + total_cost = fields.Monetary( + compute="_compute_total_cost", + currency_field="cost_currency_id", + store=True, + help="Total of all cost lines", + ) + + # Landed cost integration + landed_cost_ids = fields.Many2many( + "stock.landed.cost", + string="Landed Costs", + help="Landed cost records associated with this container", + ) + landed_cost_count = fields.Integer(compute="_compute_landed_cost_count") + total_landed_cost = fields.Monetary( + compute="_compute_total_landed_cost", + currency_field="cost_currency_id", + help="Total landed costs applied to this container", + ) + + @api.depends("freight_invoice_id.state", "drayage_invoice_id.state") + def _compute_ready_for_landed_cost(self): + """Container is ready for landed cost when both invoices are posted.""" + for record in self: + freight_ok = record.freight_invoice_id.state == "posted" + drayage_ok = record.drayage_invoice_id.state == "posted" + record.ready_for_landed_cost = freight_ok and drayage_ok + + @api.depends("cost_line_ids.amount") + def _compute_total_cost(self): + """Sum all cost breakdown lines.""" + for record in self: + record.total_cost = sum(record.cost_line_ids.mapped("amount")) + + def _compute_incoterm_id(self): + for record in self: + record.incoterm_id = record.purchase_order_ids.filtered( + lambda po: po.incoterm_id + )[:1].incoterm_id + + @api.depends( + "manual_incoterm_id", "purchase_order_ids", "purchase_order_ids.incoterm_id" + ) + def _compute_displayed_incoterm_id(self): + for record in self: + record.displayed_incoterm_id = ( + record.manual_incoterm_id + if record.manual_incoterm_id + else record.incoterm_id + ) + + def _inverse_displayed_incoterm_id(self): + for record in self: + record.manual_incoterm_id = record.displayed_incoterm_id + + @api.depends("date_eta", "date_etd") + def _compute_date_ett(self): + for record in self: + record.date_ett = 0 + if record.date_eta and record.date_etd: + record.date_ett = record.date_eta - record.date_etd + + def button_lock(self): + """Lock the container to prevent further changes.""" + self.is_locked = True + + def button_unlock(self): + """Unlock the container to allow changes.""" + self.is_locked = False + + def action_set_on_water(self): + """Mark container as on the water (departed).""" + self.write({"state": "on_water", "date_atd": date.today()}) + + def action_set_arrival_notice(self): + """Mark container as having arrival notice.""" + self.write({"state": "arrival_notice"}) + + def action_set_delivered(self): + """Mark container as delivered to warehouse.""" + self.write({"state": "delivered", "date_delivered": date.today()}) + + def action_set_received(self): + """Mark container as received into inventory.""" + self.write({"state": "received", "date_received": date.today()}) + + @api.depends("code", "purchase_order_ids") + def _compute_name(self): + for record in self: + record.name = record.code + po = record.purchase_order_ids + if po: + record.name += " ({})".format(",".join(po.mapped("name"))) + + @api.model + def _code_transform(self, code): + return code.upper() if code else code + + @api.model + def _code_from_name(self, name): + words = name.split() if name else None + code = words[0] if words else False + return self._code_transform(code) + + @api.depends("name") + def _compute_code(self): + for record in self: + if not record.code: + record.code = record._code_from_name(record.name) + + def _inverse_code(self): + for record in self: + code = self._code_transform(record.code) + if record.code != code: + record.code = code + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + vals.setdefault("code", self._code_from_name(vals.get("name"))) + return super().create(vals_list=vals_list) + + def _compute_purchase_order_count(self): + for record in self: + record.purchase_order_count = self.env["purchase.order"].search_count( + [ + ("state", "in", ("purchase", "done")), + ("container_ids", "=", self.id), + ], + ) + + def _compute_purchase_order_rfq_count(self): + for record in self: + record.purchase_order_rfq_count = self.env["purchase.order"].search_count( + [ + ("state", "in", ("draft", "sent", "to approve")), + ("container_ids", "=", self.id), + ], + ) + + def _compute_picking_count(self): + for record in self: + record.picking_count = len(record.picking_ids) + + def action_view_rfq(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq") + action["domain"] = [ + ( + "id", + "in", + [ + po.id + for po in self.purchase_order_ids + if po.state in ("draft", "sent", "to approve") + ], + ) + ] + action["context"] = {"create": False} + return action + + def action_view_order(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "purchase.purchase_form_action" + ) + action["domain"] = [ + ( + "id", + "in", + [ + po.id + for po in self.purchase_order_ids + if po.state in ("purchase", "done") + ], + ) + ] + action["context"] = {"create": False} + return action + + def action_view_picking(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "stock.action_picking_tree_all" + ) + action["domain"] = [("id", "in", self.picking_ids.ids)] + action["context"] = {"create": False} + return action + + # Document tracking methods + def _compute_document_count(self): + for record in self: + record.document_count = len(record.document_ids) + + @api.depends("document_ids.state", "document_ids.required") + def _compute_documents_complete(self): + for record in self: + required_docs = record.document_ids.filtered(lambda d: d.required) + record.documents_complete = ( + all(doc.state == "approved" for doc in required_docs) + if required_docs + else True + ) + + def action_view_documents(self): + self.ensure_one() + return { + "name": "Container Documents", + "type": "ir.actions.act_window", + "res_model": "container.document", + "view_mode": "list,form", + "domain": [("container_id", "=", self.id)], + "context": {"default_container_id": self.id}, + } + + def action_create_required_documents(self): + """Create document records for all required document types.""" + self.ensure_one() + required_types = self.env["container.document.type"].search( + [("required", "=", True)] + ) + existing_types = self.document_ids.mapped("document_type_id") + for doc_type in required_types - existing_types: + self.env["container.document"].create( + { + "container_id": self.id, + "document_type_id": doc_type.id, + } + ) + return True + + # Container line methods + def _compute_line_count(self): + for record in self: + record.line_count = len(record.line_ids) + + def action_view_lines(self): + self.ensure_one() + return { + "name": "Container Lines", + "type": "ir.actions.act_window", + "res_model": "container.line", + "view_mode": "list,form", + "domain": [("container_id", "=", self.id)], + "context": {"default_container_id": self.id}, + } + + # Shipment tracking methods + def _compute_tracking_url(self): + """Generate tracking URL based on carrier. + + This can be extended to support different carrier tracking portals. + """ + for record in self: + url = False + if record.tracking_number and record.carrier_id: + carrier_name = (record.carrier_id.name or "").lower() + tracking = record.tracking_number + url = record._get_tracking_url_for_carrier(carrier_name, tracking) + record.tracking_url = url + + def _get_tracking_url_for_carrier(self, carrier_name, tracking): + """Get tracking URL for a specific carrier. + + Can be extended to add more carriers. + """ + base_urls = { + "maersk": "https://www.maersk.com/tracking/", + "msc": "https://www.msc.com/track-a-shipment?agencyPath=msc&trackingNumber=", + "cosco": "https://elines.coscoshipping.com/ebusiness/cargoTracking?trackNo=", + "hapag": ( + "https://www.hapag-lloyd.com/en/online-business/track/" + "track-by-container-solution.html?container=" + ), + "one": ( + "https://ecomm.one-line.com/one-ecom/manage-shipment/" + "cargo-tracking?trakNoParam=" + ), + "evergreen": ( + "https://www.shipmentlink.com/tvs2/jsp/" + "TVS2_ContainerTracking.jsp?cntr=" + ), + } + for key, url in base_urls.items(): + if key in carrier_name: + return url + tracking + # Generic tracking via searates + return "https://www.searates.com/container/tracking/?number=" + tracking + + def action_open_tracking(self): + """Open tracking URL in browser.""" + self.ensure_one() + if self.tracking_url: + return { + "type": "ir.actions.act_url", + "url": self.tracking_url, + "target": "new", + } + return False + + # Landed cost methods + def _compute_landed_cost_count(self): + for record in self: + record.landed_cost_count = len(record.landed_cost_ids) + + def _compute_total_landed_cost(self): + for record in self: + record.total_landed_cost = sum( + lc.amount_total + for lc in record.landed_cost_ids.filtered(lambda l: l.state == "done") + ) + + def action_view_landed_costs(self): + self.ensure_one() + action = self.env["ir.actions.actions"]._for_xml_id( + "stock_landed_costs.action_stock_landed_cost" + ) + action["domain"] = [("id", "in", self.landed_cost_ids.ids)] + action["context"] = {"default_container_id": self.id} + return action + + def action_create_landed_cost(self): + """Create a new landed cost record for this container.""" + self.ensure_one() + if not self.picking_ids: + return False + # Get done pickings for landed cost + done_pickings = self.picking_ids.filtered(lambda p: p.state == "done") + if not done_pickings: + return False + landed_cost = self.env["stock.landed.cost"].create( + { + "picking_ids": [(6, 0, done_pickings.ids)], + } + ) + self.landed_cost_ids = [(4, landed_cost.id)] + return { + "type": "ir.actions.act_window", + "res_model": "stock.landed.cost", + "view_mode": "form", + "res_id": landed_cost.id, + } diff --git a/purchase_container/models/purchase_order.py b/purchase_container/models/purchase_order.py new file mode 100644 index 00000000000..15324f844c3 --- /dev/null +++ b/purchase_container/models/purchase_order.py @@ -0,0 +1,36 @@ +from odoo import Command, api, fields, models + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + container_ids = fields.Many2many( + "purchase.container", + string="Containers", + compute="_compute_container_ids", + inverse="_inverse_container_ids", + copy=False, + store=True, + index=True, + ) + + @api.depends("picking_ids.container_id") + def _compute_container_ids(self): + for purchase in self: + if purchase.picking_ids: + purchase.container_ids = [ + Command.set(purchase.picking_ids.container_id.ids) + ] + + def _inverse_container_ids(self): + pass + + def _prepare_picking(self): + # This function initialize the picking vals from the current order + vals = super()._prepare_picking() + # So if we have at least a container set on the order, we should + # initialize the picking container with one. + if self.container_ids: + # Arbitrarily, we take the first one by default + vals["container_id"] = self.container_ids[0].id + return vals diff --git a/purchase_container/models/stock_picking.py b/purchase_container/models/stock_picking.py new file mode 100644 index 00000000000..77b2cf41c52 --- /dev/null +++ b/purchase_container/models/stock_picking.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + container_id = fields.Many2one( + "purchase.container", string="Container", copy=False, index=True + ) diff --git a/purchase_container/readme/CONTRIBUTORS.md b/purchase_container/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..629a218f062 --- /dev/null +++ b/purchase_container/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +* Akretion + * Olivier Nibart + * Mathieu Delva diff --git a/purchase_container/readme/DESCRIPTION.md b/purchase_container/readme/DESCRIPTION.md new file mode 100644 index 00000000000..e82102c0ed5 --- /dev/null +++ b/purchase_container/readme/DESCRIPTION.md @@ -0,0 +1,3 @@ +Add containers to purchase orders and stock pickings. + +![Purchase container image](../static/description/img.png) diff --git a/purchase_container/security/ir.model.access.csv b/purchase_container/security/ir.model.access.csv new file mode 100644 index 00000000000..4cff0be7940 --- /dev/null +++ b/purchase_container/security/ir.model.access.csv @@ -0,0 +1,14 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_purchase_container,purchase.container,model_purchase_container,purchase.group_purchase_user,1,1,1,1 +access_container_type,container.type,model_container_type,purchase.group_purchase_user,1,1,1,1 +access_purchase_container_manager,purchase.container,model_purchase_container,purchase.group_purchase_manager,1,1,1,1 +access_purchase_container_readonly,purchase.container,model_purchase_container,account.group_account_readonly,1,0,0,0 +access_purchase_container_invoice,purchase.container,model_purchase_container,account.group_account_invoice,1,1,0,0 +access_container_document_type,container.document.type,model_container_document_type,purchase.group_purchase_user,1,1,1,1 +access_container_document,container.document,model_container_document,purchase.group_purchase_user,1,1,1,1 +access_container_document_readonly,container.document,model_container_document,account.group_account_readonly,1,0,0,0 +access_container_line,container.line,model_container_line,purchase.group_purchase_user,1,1,1,1 +access_container_line_readonly,container.line,model_container_line,account.group_account_readonly,1,0,0,0 +access_container_cost_line,container.cost.line,model_container_cost_line,purchase.group_purchase_user,1,1,1,1 +access_container_cost_line_readonly,container.cost.line,model_container_cost_line,account.group_account_readonly,1,0,0,0 +access_container_cost_line_invoice,container.cost.line,model_container_cost_line,account.group_account_invoice,1,1,1,0 diff --git a/purchase_container/static/description/icon.png b/purchase_container/static/description/icon.png new file mode 100644 index 00000000000..1dcc49c24f3 Binary files /dev/null and b/purchase_container/static/description/icon.png differ diff --git a/purchase_container/static/description/img.png b/purchase_container/static/description/img.png new file mode 100644 index 00000000000..b9970bef1b6 Binary files /dev/null and b/purchase_container/static/description/img.png differ diff --git a/purchase_container/static/description/index.html b/purchase_container/static/description/index.html new file mode 100644 index 00000000000..76e5f762c6d --- /dev/null +++ b/purchase_container/static/description/index.html @@ -0,0 +1,431 @@ + + + + + +Purchase Container + + + +
+

Purchase Container

+ + +

Beta License: AGPL-3 OCA/purchase-workflow Translate me on Weblate Try me on Runboat

+

Add containers to purchase orders and stock pickings.

+

Purchase container image

+

Table of contents

+ +
+

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 +feedback.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
  • Kencove
  • +
+
+
+

Contributors

+
    +
  • Akretion
      +
    • Olivier Nibart
    • +
    • Mathieu Delva
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

Current maintainer:

+

nayatec

+

This module is part of the OCA/purchase-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/purchase_container/tests/__init__.py b/purchase_container/tests/__init__.py new file mode 100644 index 00000000000..d9b96c4fa5a --- /dev/null +++ b/purchase_container/tests/__init__.py @@ -0,0 +1 @@ +from . import test_module diff --git a/purchase_container/tests/test_module.py b/purchase_container/tests/test_module.py new file mode 100644 index 00000000000..74e6d63bce6 --- /dev/null +++ b/purchase_container/tests/test_module.py @@ -0,0 +1,537 @@ +from datetime import date, timedelta + +from odoo import Command, fields +from odoo.tests.common import TransactionCase + +from odoo.addons.base.tests.common import BaseCommon + + +class TestPurchaseContainerBase(BaseCommon): + """Base test class with common setup for purchase container tests.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Test Supplier"}) + # Handle base_unit_count: website_sale adds NOT NULL constraint but may not be + # loaded in test registry. Set DB default to handle schema mismatch. + if "base_unit_count" not in cls.env["product.template"]._fields: + cls.env.cr.execute( + """ + ALTER TABLE product_template + ALTER COLUMN base_unit_count SET DEFAULT 0; + ALTER TABLE product_product + ALTER COLUMN base_unit_count SET DEFAULT 0; + """ + ) + product_vals = {"name": "Test Product"} + if "base_unit_count" in cls.env["product.template"]._fields: + product_vals["base_unit_count"] = 1 + product_tmpl = cls.env["product.template"].create(product_vals) + cls.product = product_tmpl.product_variant_id + cls.cont_a = cls.env["purchase.container"].create({"code": "AA"}) + cls.cont_b = cls.env["purchase.container"].create({"code": "BB"}) + cls.incoterm_id = cls.env.ref("account.incoterm_FCA") + + def _validate_picking(self, picking): + """Helper to validate picking by setting quantities and validating.""" + for move in picking.move_ids: + move.quantity_done = move.product_uom_qty + picking.button_validate() + + def get_po(self): + return self.env["purchase.order"].create( + { + "partner_id": self.partner.id, + "date_planned": fields.Datetime.now(), + "incoterm_id": self.incoterm_id.id, + "order_line": [ + Command.create( + { + "name": "Test Line", + "product_id": self.product.id, + "product_qty": 4.0, + "product_uom": self.product.uom_po_id.id, + "price_unit": 1, + "date_planned": fields.Datetime.now(), + }, + ) + ], + } + ) + + +class TestPurchaseContainer(TestPurchaseContainerBase): + """Tests for core purchase container functionality.""" + + def test_container_by_purchase(self): + """Test container-purchase order relationships.""" + # first PO + po = self.get_po() + po.button_confirm() + pick01 = po.picking_ids[0] + pick01.container_id = self.cont_a.id + self.assertIn(self.cont_a, po.container_ids) + self.assertEqual(self.cont_a.purchase_order_count, 1) + self._validate_picking(pick01) + # this update triggers a new picking + po.order_line[0].product_qty = 7 + pick02 = po.picking_ids.filtered(lambda x: x.state != "done") + pick02.container_id = self.cont_b.id + self._validate_picking(pick02) + self.assertEqual(pick02.state, "done") + self.assertIn(self.cont_b, po.container_ids) + self.assertEqual(self.cont_b.purchase_order_count, 1) + # second PO + po2 = po.copy() + po2.button_confirm() + pick11 = po2.picking_ids + pick11.container_id = self.cont_b.id + self._validate_picking(pick11) + self.assertIn(self.cont_b, po2.container_ids) + self.assertEqual(pick11.state, "done") + self.assertEqual(len(self.cont_b.purchase_order_ids), 2) + self.assertEqual(self.cont_b.purchase_order_count, 1) + # this method is not computed because not a stored field + self.cont_b._compute_purchase_order_count() + self.assertEqual(self.cont_b.purchase_order_count, 2) + + self.cont_b._compute_incoterm_id() + self.assertEqual(self.cont_b.displayed_incoterm_id, self.incoterm_id) + + def test_action_views(self): + """Test action view methods return proper actions.""" + po = self.get_po() + po.button_confirm() + pick01 = po.picking_ids[0] + pick01.container_id = self.cont_a.id + self.cont_a.action_view_rfq() + self.cont_a.action_view_order() + self.cont_a.action_view_picking() + + def test_container_code_uppercase(self): + """Test that container code is converted to uppercase.""" + container = self.env["purchase.container"].create({"code": "test123"}) + self.assertEqual(container.code, "TEST123") + + def test_container_name_computation(self): + """Test container name includes code and PO references.""" + po = self.get_po() + po.button_confirm() + pick = po.picking_ids[0] + pick.container_id = self.cont_a.id + self.cont_a.purchase_order_ids = [(4, po.id)] + self.cont_a._compute_name() + self.assertIn("AA", self.cont_a.name) + self.assertIn(po.name, self.cont_a.name) + + +class TestContainerStateTransitions(TestPurchaseContainerBase): + """Tests for container state transitions and actions.""" + + def test_state_transitions(self): + """Test state transition actions.""" + container = self.env["purchase.container"].create( + { + "code": "STATE-TEST", + "state": "in_progress", + } + ) + self.assertEqual(container.state, "in_progress") + + # Test on_water transition + container.action_set_on_water() + self.assertEqual(container.state, "on_water") + self.assertEqual(container.date_atd, date.today()) + + # Test arrival notice transition + container.action_set_arrival_notice() + self.assertEqual(container.state, "arrival_notice") + + # Test delivered transition + container.action_set_delivered() + self.assertEqual(container.state, "delivered") + self.assertEqual(container.date_delivered, date.today()) + + # Test received transition + container.action_set_received() + self.assertEqual(container.state, "received") + self.assertEqual(container.date_received, date.today()) + + def test_container_lock_unlock(self): + """Test container lock and unlock functionality.""" + container = self.env["purchase.container"].create({"code": "LOCK-TEST"}) + self.assertFalse(container.is_locked) + + container.button_lock() + self.assertTrue(container.is_locked) + + container.button_unlock() + self.assertFalse(container.is_locked) + + def test_ett_computation(self): + """Test estimated transit time computation.""" + container = self.env["purchase.container"].create( + { + "code": "ETT-TEST", + "date_etd": date.today(), + "date_eta": date.today() + timedelta(days=30), + } + ) + container._compute_date_ett() + # date_ett is a Char field, so it stores the timedelta as a string + self.assertIn("30 days", container.date_ett) + + +class TestContainerFields(TestPurchaseContainerBase): + """Tests for container field functionality.""" + + def test_freight_forwarder_ref(self): + """Test freight forwarder reference field.""" + container = self.env["purchase.container"].create( + { + "code": "FF-TEST", + "freight_forwarder_ref": "B00064516", + } + ) + self.assertEqual(container.freight_forwarder_ref, "B00064516") + + def test_additional_fees_fields(self): + """Test additional fees boolean and amount fields.""" + container = self.env["purchase.container"].create( + { + "code": "FEES-TEST", + "has_additional_fees": True, + "per_diem_fees": 150.00, + "per_diem_reason": "Customs delay", + } + ) + self.assertTrue(container.has_additional_fees) + self.assertEqual(container.per_diem_fees, 150.00) + self.assertEqual(container.per_diem_reason, "Customs delay") + + def test_shipping_tracking_fields(self): + """Test shipping and tracking fields.""" + carrier = self.env["res.partner"].create({"name": "Maersk Line"}) + container = self.env["purchase.container"].create( + { + "code": "TRACK-TEST", + "carrier_id": carrier.id, + "vessel_name": "MSC Oscar", + "voyage_number": "VY2025001", + "tracking_number": "MSKU1234567", + } + ) + self.assertEqual(container.carrier_id, carrier) + self.assertEqual(container.vessel_name, "MSC Oscar") + self.assertEqual(container.voyage_number, "VY2025001") + self.assertEqual(container.tracking_number, "MSKU1234567") + + +class TestContainerTrackingUrl(TestPurchaseContainerBase): + """Tests for tracking URL computation.""" + + def test_tracking_url_maersk(self): + """Test tracking URL generation for Maersk.""" + carrier = self.env["res.partner"].create({"name": "Maersk Line"}) + container = self.env["purchase.container"].create( + { + "code": "URL-MAERSK", + "carrier_id": carrier.id, + "tracking_number": "MSKU1234567", + } + ) + container._compute_tracking_url() + self.assertIn("maersk.com", container.tracking_url) + self.assertIn("MSKU1234567", container.tracking_url) + + def test_tracking_url_msc(self): + """Test tracking URL generation for MSC.""" + carrier = self.env["res.partner"].create({"name": "MSC Mediterranean"}) + container = self.env["purchase.container"].create( + { + "code": "URL-MSC", + "carrier_id": carrier.id, + "tracking_number": "MSCU7654321", + } + ) + container._compute_tracking_url() + self.assertIn("msc.com", container.tracking_url) + + def test_tracking_url_generic(self): + """Test tracking URL generation for unknown carriers.""" + carrier = self.env["res.partner"].create({"name": "Unknown Carrier"}) + container = self.env["purchase.container"].create( + { + "code": "URL-GENERIC", + "carrier_id": carrier.id, + "tracking_number": "UNKN1234567", + } + ) + container._compute_tracking_url() + self.assertIn("searates.com", container.tracking_url) + + def test_tracking_url_no_carrier(self): + """Test tracking URL is empty without carrier.""" + container = self.env["purchase.container"].create( + { + "code": "URL-NONE", + "tracking_number": "TEST1234567", + } + ) + container._compute_tracking_url() + self.assertFalse(container.tracking_url) + + def test_action_open_tracking(self): + """Test open tracking action returns URL action.""" + carrier = self.env["res.partner"].create({"name": "Maersk"}) + container = self.env["purchase.container"].create( + { + "code": "ACTION-TRACK", + "carrier_id": carrier.id, + "tracking_number": "MSKU1234567", + } + ) + container._compute_tracking_url() + action = container.action_open_tracking() + self.assertEqual(action["type"], "ir.actions.act_url") + self.assertIn("maersk.com", action["url"]) + + +class TestContainerDocuments(TestPurchaseContainerBase): + """Tests for container document tracking.""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + # Create document types + cls.doc_type_invoice = cls.env["container.document.type"].create( + { + "name": "Commercial Invoice", + "required": True, + "sequence": 10, + } + ) + cls.doc_type_packing = cls.env["container.document.type"].create( + { + "name": "Packing List", + "required": True, + "sequence": 20, + } + ) + cls.doc_type_cert = cls.env["container.document.type"].create( + { + "name": "Certificate of Origin", + "required": False, + "sequence": 30, + } + ) + + def test_document_creation(self): + """Test creating documents for a container.""" + container = self.env["purchase.container"].create({"code": "DOC-TEST"}) + doc = self.env["container.document"].create( + { + "container_id": container.id, + "document_type_id": self.doc_type_invoice.id, + } + ) + self.assertEqual(doc.state, "missing") + self.assertTrue(doc.required) + self.assertEqual(doc.container_id, container) + + def test_document_state_workflow(self): + """Test document state transitions.""" + container = self.env["purchase.container"].create({"code": "DOC-STATE"}) + doc = self.env["container.document"].create( + { + "container_id": container.id, + "document_type_id": self.doc_type_invoice.id, + "state": "missing", + } + ) + + # Move to pending + doc.state = "pending" + doc.date_received = date.today() + self.assertEqual(doc.state, "pending") + + # Approve + doc.action_approve() + self.assertEqual(doc.state, "approved") + self.assertEqual(doc.date_approved, date.today()) + + # Reset + doc.action_reset() + self.assertEqual(doc.state, "pending") + self.assertFalse(doc.date_approved) + + # Reject + doc.action_reject() + self.assertEqual(doc.state, "rejected") + + def test_documents_complete_computation(self): + """Test documents_complete field computation.""" + container = self.env["purchase.container"].create({"code": "DOC-COMPLETE"}) + + # No documents - should be complete (no required docs to check) + container._compute_documents_complete() + self.assertTrue(container.documents_complete) + + # Add required document in missing state + doc1 = self.env["container.document"].create( + { + "container_id": container.id, + "document_type_id": self.doc_type_invoice.id, + "state": "missing", + } + ) + container._compute_documents_complete() + self.assertFalse(container.documents_complete) + + # Approve the document + doc1.action_approve() + container._compute_documents_complete() + + # Add another required document + doc2 = self.env["container.document"].create( + { + "container_id": container.id, + "document_type_id": self.doc_type_packing.id, + "state": "missing", + } + ) + container._compute_documents_complete() + self.assertFalse(container.documents_complete) + + # Approve second document + doc2.action_approve() + container._compute_documents_complete() + self.assertTrue(container.documents_complete) + + def test_create_required_documents(self): + """Test action to create required document records.""" + container = self.env["purchase.container"].create({"code": "DOC-CREATE"}) + self.assertEqual(len(container.document_ids), 0) + + container.action_create_required_documents() + + # Should have created records for required document types + required_types = self.env["container.document.type"].search( + [("required", "=", True)] + ) + self.assertGreaterEqual(len(container.document_ids), len(required_types)) + + def test_document_count(self): + """Test document count computation.""" + container = self.env["purchase.container"].create({"code": "DOC-COUNT"}) + self.assertEqual(container.document_count, 0) + + self.env["container.document"].create( + { + "container_id": container.id, + "document_type_id": self.doc_type_invoice.id, + } + ) + container._compute_document_count() + self.assertEqual(container.document_count, 1) + + self.env["container.document"].create( + { + "container_id": container.id, + "document_type_id": self.doc_type_packing.id, + } + ) + container._compute_document_count() + self.assertEqual(container.document_count, 2) + + def test_action_view_documents(self): + """Test action to view documents.""" + container = self.env["purchase.container"].create({"code": "DOC-VIEW"}) + action = container.action_view_documents() + self.assertEqual(action["res_model"], "container.document") + # Domain is [('container_id', '=', container.id)] + self.assertEqual(action["domain"][0][2], container.id) + + +class TestContainerLines(TestPurchaseContainerBase): + """Tests for container line functionality.""" + + def test_line_creation(self): + """Test creating container lines.""" + container = self.env["purchase.container"].create({"code": "LINE-TEST"}) + line = self.env["container.line"].create( + { + "container_id": container.id, + "product_id": self.product.id, + "quantity": 100, + } + ) + self.assertEqual(line.container_id, container) + self.assertEqual(line.product_id, self.product) + self.assertEqual(line.quantity, 100) + + def test_line_carton_calculation(self): + """Test quantity calculation from carton info.""" + container = self.env["purchase.container"].create({"code": "LINE-CARTON"}) + line = self.env["container.line"].create( + { + "container_id": container.id, + "product_id": self.product.id, + "quantity": 0, + "carton_qty": 10, + "units_per_carton": 50, + } + ) + line._onchange_carton_info() + self.assertEqual(line.quantity, 500) + + def test_line_count(self): + """Test line count computation.""" + container = self.env["purchase.container"].create({"code": "LINE-COUNT"}) + self.assertEqual(container.line_count, 0) + + self.env["container.line"].create( + { + "container_id": container.id, + "product_id": self.product.id, + "quantity": 100, + } + ) + container._compute_line_count() + self.assertEqual(container.line_count, 1) + + def test_action_view_lines(self): + """Test action to view lines.""" + container = self.env["purchase.container"].create({"code": "LINE-VIEW"}) + action = container.action_view_lines() + self.assertEqual(action["res_model"], "container.line") + # Domain is [('container_id', '=', container.id)] + self.assertEqual(action["domain"][0][2], container.id) + + def test_line_product_onchange(self): + """Test product onchange sets defaults.""" + container = self.env["purchase.container"].create({"code": "LINE-ONCHANGE"}) + line = self.env["container.line"].create( + { + "container_id": container.id, + "product_id": self.product.id, + "quantity": 1, + } + ) + line._onchange_product_id() + self.assertEqual( + line.product_uom_id, self.product.uom_po_id or self.product.uom_id + ) + + +class TestContainerType(TransactionCase): + """Tests for container type model.""" + + def test_container_types_exist(self): + """Test that default container types are loaded.""" + types = self.env["container.type"].search([]) + type_names = types.mapped("name") + self.assertIn("20'", type_names) + self.assertIn("40'", type_names) + self.assertIn("40' HC", type_names) diff --git a/purchase_container/views/container_cost_line.xml b/purchase_container/views/container_cost_line.xml new file mode 100644 index 00000000000..3ca1ca10e1d --- /dev/null +++ b/purchase_container/views/container_cost_line.xml @@ -0,0 +1,122 @@ + + + + container.cost.line + +
+ + + + + + + + + + + + + + + +
+
+
+ + + container.cost.line + + + + + + + + + + + + + + + + + container.cost.line + + + + + + + + + + + + + + + + + + + + + + + + Container Costs + container.cost.line + list,form + {'search_default_group_container': 1} + +
diff --git a/purchase_container/views/container_document.xml b/purchase_container/views/container_document.xml new file mode 100644 index 00000000000..ae0a5043322 --- /dev/null +++ b/purchase_container/views/container_document.xml @@ -0,0 +1,201 @@ + + + + + container.document.type + +
+ + + + + + + + + + + + + +
+
+
+ + + container.document.type + + + + + + + + + + + + Document Types + container.document.type + list,form + + + + + container.document + +
+
+
+ + + + + + + + + + + + + + + + + + + +
+ + + +
+
+
+
+ + + container.document + + + + + + + + + + + + + + + container.document + + + + + + + + + + + + + + + + + + + + + + + + Container Documents + container.document + list,form + {'search_default_group_container': 1} + + + + +
diff --git a/purchase_container/views/container_line.xml b/purchase_container/views/container_line.xml new file mode 100644 index 00000000000..f861c576b80 --- /dev/null +++ b/purchase_container/views/container_line.xml @@ -0,0 +1,105 @@ + + + + + container.line + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + container.line + + + + + + + + + + + + + + + + + + + + + container.line + + + + + + + + + + + + + + + + + Container Lines + container.line + list,form + {'search_default_group_container': 1} + +
diff --git a/purchase_container/views/container_type.xml b/purchase_container/views/container_type.xml new file mode 100644 index 00000000000..2c9fe41f734 --- /dev/null +++ b/purchase_container/views/container_type.xml @@ -0,0 +1,50 @@ + + + container.type + +
+ + + + + + + + + + +
+
+
+ + + container.type + + + + + + + + + + container.type + + + + + + + + + Container Types + container.type + list,form + + + +
diff --git a/purchase_container/views/purchase.xml b/purchase_container/views/purchase.xml new file mode 100644 index 00000000000..a4af217b79a --- /dev/null +++ b/purchase_container/views/purchase.xml @@ -0,0 +1,44 @@ + + + + purchase.order + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +