details:   https://code.tryton.org/tryton/commit/cedac0e38ccc
branch:    default
user:      Cédric Krier <[email protected]>
date:      Sat Feb 01 01:52:03 2025 +0100
description:
        Add import/export agent for customs
diffstat:

 modules/customs/CHANGELOG                                      |    1 +
 modules/customs/customs.py                                     |   81 +++++++-
 modules/customs/customs.xml                                    |  114 
++++++++++
 modules/customs/doc/design.rst                                 |   34 ++
 modules/customs/tests/test_module.py                           |   53 ++++-
 modules/customs/tryton.cfg                                     |    4 +
 modules/customs/view/customs_agent_form.xml                    |   14 +
 modules/customs/view/customs_agent_list.xml                    |    8 +
 modules/customs/view/customs_agent_selection_form.xml          |   21 +
 modules/customs/view/customs_agent_selection_list.xml          |    9 +
 modules/customs/view/customs_agent_selection_list_sequence.xml |    9 +
 11 files changed, 346 insertions(+), 2 deletions(-)

diffs (448 lines):

diff -r 7e5d3e3bef70 -r cedac0e38ccc modules/customs/CHANGELOG
--- a/modules/customs/CHANGELOG Thu Feb 13 23:47:46 2025 +0100
+++ b/modules/customs/CHANGELOG Sat Feb 01 01:52:03 2025 +0100
@@ -1,3 +1,4 @@
+* Add import/export agent
 
 Version 7.6.0 - 2025-04-28
 --------------------------
diff -r 7e5d3e3bef70 -r cedac0e38ccc modules/customs/customs.py
--- a/modules/customs/customs.py        Thu Feb 13 23:47:46 2025 +0100
+++ b/modules/customs/customs.py        Sat Feb 01 01:52:03 2025 +0100
@@ -7,7 +7,8 @@
 
 from trytond import backend
 from trytond.model import (
-    DeactivableMixin, MatchMixin, ModelSQL, ModelView, fields)
+    DeactivableMixin, MatchMixin, ModelSQL, ModelView, fields,
+    sequence_ordered)
 from trytond.modules.product import price_digits
 from trytond.pool import Pool
 from trytond.pyson import Bool, Eval, If
@@ -229,3 +230,81 @@
         amount = Uom.compute_price(self.uom, self.amount, uom)
         amount *= Decimal(str(quantity))
         return Currency.compute(self.currency, amount, currency)
+
+
+class Agent(DeactivableMixin, ModelSQL, ModelView):
+    __name__ = 'customs.agent'
+
+    party = fields.Many2One(
+        'party.party', "Party", required=True, ondelete='CASCADE',
+        help="The party which represents the import/export agent.")
+    address = fields.Many2One(
+        'party.address', "Address", required=True, ondelete='RESTRICT',
+        domain=[
+            ('party', '=', Eval('party', -1)),
+            ],
+        help="The address of the agent.")
+    tax_identifier = fields.Many2One(
+        'party.identifier', "Tax Identifier",
+        required=True, ondelete='RESTRICT',
+        help="The identifier of the agent for tax report.")
+
+    @classmethod
+    def __setup__(cls):
+        pool = Pool()
+        Party = pool.get('party.party')
+        super().__setup__()
+        tax_identifier_types = Party.tax_identifier_types()
+        cls.tax_identifier.domain = [
+            ('party', '=', Eval('party', -1)),
+            ('type', 'in', tax_identifier_types),
+            ]
+
+    @fields.depends('party', 'address', 'tax_identifier')
+    def on_change_party(self):
+        if self.party:
+            if not self.address or self.address.party != self.party:
+                self.address = self.party.address_get()
+            if (not self.tax_identifier
+                    or self.tax_identifier.party != self.party):
+                self.tax_identifier = self.party.tax_identifier
+        else:
+            self.address = self.tax_identifier = None
+
+    def get_rec_name(self, name):
+        return self.party.rec_name
+
+    @classmethod
+    def search_rec_name(cls, name, clause):
+        return [('party.rec_name', *clause[1:])]
+
+
+class AgentSelection(
+        DeactivableMixin, sequence_ordered(), MatchMixin, ModelSQL, ModelView):
+    __name__ = 'customs.agent.selection'
+
+    company = fields.Many2One(
+        'company.company', "Company", required=True)
+    from_country = fields.Many2One(
+        'country.country', "From Country", ondelete='RESTRICT',
+        help="Apply only when shipping from this country.\n"
+        "Leave empty for any countries.")
+    to_country = fields.Many2One(
+        'country.country', "To Country", ondelete='RESTRICT',
+        help="Apply only when shipping to this country.\n"
+        "Leave empty for any countries.")
+    agent = fields.Many2One(
+        'customs.agent', "Agent", required=True, ondelete='CASCADE',
+        help="The selected agent.")
+
+    @classmethod
+    def default_company(cls):
+        return Transaction().context.get('company')
+
+    @classmethod
+    def get_agent(cls, company, pattern):
+        for selection in cls.search([
+                    ('company', '=', company),
+                    ]):
+            if selection.match(pattern):
+                return selection.agent
diff -r 7e5d3e3bef70 -r cedac0e38ccc modules/customs/customs.xml
--- a/modules/customs/customs.xml       Thu Feb 13 23:47:46 2025 +0100
+++ b/modules/customs/customs.xml       Sat Feb 01 01:52:03 2025 +0100
@@ -135,5 +135,119 @@
             <field name="perm_create" eval="True"/>
             <field name="perm_delete" eval="True"/>
         </record>
+
+        <record model="ir.ui.view" id="customs_agent_view_form">
+            <field name="model">customs.agent</field>
+            <field name="type">form</field>
+            <field name="name">customs_agent_form</field>
+        </record>
+
+        <record model="ir.ui.view" id="customs_agent_view_list">
+            <field name="model">customs.agent</field>
+            <field name="type">tree</field>
+            <field name="name">customs_agent_list</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_customs_agent_form">
+            <field name="name">Agents</field>
+            <field name="res_model">customs.agent</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_customs_agent_form_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="customs_agent_view_list"/>
+            <field name="act_window" ref="act_customs_agent_form"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_customs_agent_form_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="customs_agent_view_form"/>
+            <field name="act_window" ref="act_customs_agent_form"/>
+        </record>
+        <menuitem
+            parent="menu_customs"
+            action="act_customs_agent_form"
+            sequence="20"
+            id="menu_customs_agent_form"/>
+
+        <record model="ir.model.access" id="access_customs_agent">
+            <field name="model">customs.agent</field>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" id="access_customs_agent_admin">
+            <field name="model">customs.agent</field>
+            <field name="group" ref="group_customs_admin"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="True"/>
+            <field name="perm_create" eval="True"/>
+            <field name="perm_delete" eval="True"/>
+        </record>
+
+        <record model="ir.ui.view" id="customs_agent_selection_view_form">
+            <field name="model">customs.agent.selection</field>
+            <field name="type">form</field>
+            <field name="name">customs_agent_selection_form</field>
+        </record>
+
+        <record model="ir.ui.view" id="customs_agent_selection_view_list">
+            <field name="model">customs.agent.selection</field>
+            <field name="type">tree</field>
+            <field name="priority" eval="10"/>
+            <field name="name">customs_agent_selection_list</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="customs_agent_selection_view_list_sequence">
+            <field name="model">customs.agent.selection</field>
+            <field name="type">tree</field>
+            <field name="priority" eval="20"/>
+            <field name="name">customs_agent_selection_list_sequence</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_customs_agent_selection_form">
+            <field name="name">Selection</field>
+            <field name="res_model">customs.agent.selection</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_customs_agent_selection_form_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" 
ref="customs_agent_selection_view_list_sequence"/>
+            <field name="act_window" ref="act_customs_agent_selection_form"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_customs_agent_selection_form_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="customs_agent_selection_view_form"/>
+            <field name="act_window" ref="act_customs_agent_selection_form"/>
+        </record>
+        <menuitem
+            parent="menu_customs_agent_form"
+            action="act_customs_agent_selection_form"
+            sequence="10"
+            id="menu_customs_agent_selection_form"/>
+
+        <record model="ir.model.access" id="access_customs_agent_selection">
+            <field name="model">customs.agent.selection</field>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_customs_agent_selection_admin">
+            <field name="model">customs.agent.selection</field>
+            <field name="group" ref="group_customs_admin"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="True"/>
+            <field name="perm_create" eval="True"/>
+            <field name="perm_delete" eval="True"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_customs_agent_selection_companies">
+            <field name="name">User in companies</field>
+            <field name="model">customs.agent.selection</field>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_customs_agent_selection_companies">
+            <field name="domain" eval="[('company', 'in', Eval('companies', 
[]))]" pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_customs_agent_selection_companies"/>
+        </record>
     </data>
 </tryton>
diff -r 7e5d3e3bef70 -r cedac0e38ccc modules/customs/doc/design.rst
--- a/modules/customs/doc/design.rst    Thu Feb 13 23:47:46 2025 +0100
+++ b/modules/customs/doc/design.rst    Sat Feb 01 01:52:03 2025 +0100
@@ -44,6 +44,40 @@
    .. |Customs --> Duty Rates| replace:: :menuselection:`Customs --> Duty 
Rates`
    __ https://demo.tryton.org/model/customs.duty.rate
 
+.. _model-customs.agent:
+
+Agent
+=====
+
+The *Agent* defines a `Party <party:model-party.party>` as a import or export
+agent.
+
+.. seealso::
+
+   The *Agents* can be found by opening the main menu item:
+
+   |Customs --> Agents|__
+
+   .. |Customs --> Agents| replace:: :menuselection:`Customs --> Agents`
+   __ https://demo.tryton.org/model/customs.agent
+
+.. _model-customs.agent.selection:
+
+Agent Selection
+===============
+
+The *Agent Selection* defines which agent to use based on criteria such as the
+origin and destination `Countries <country:model-country.country>`.
+
+.. seealso::
+
+   The *Agent Selections* can be found by opening the main menu item:
+
+   |Customs --> Agents --> Selection|__
+
+   .. |Customs --> Agents --> Selection| replace:: :menuselection:`Customs --> 
Agents --> Selection`
+   __ https://demo.tryton.org/model/customs.agent.selection
+
 .. _concept-product:
 
 Product
diff -r 7e5d3e3bef70 -r cedac0e38ccc modules/customs/tests/test_module.py
--- a/modules/customs/tests/test_module.py      Thu Feb 13 23:47:46 2025 +0100
+++ b/modules/customs/tests/test_module.py      Sat Feb 01 01:52:03 2025 +0100
@@ -4,11 +4,13 @@
 from datetime import date
 from decimal import Decimal
 
+from trytond.modules.company.tests import CompanyTestMixin, create_company
 from trytond.pool import Pool
 from trytond.tests.test_tryton import ModuleTestCase, with_transaction
+from trytond.transaction import Transaction
 
 
-class CustomsTestCase(ModuleTestCase):
+class CustomsTestCase(CompanyTestMixin, ModuleTestCase):
     'Test Customs module'
     module = 'customs'
 
@@ -260,5 +262,54 @@
                     ('id', '=', product_tariff_code.id),
                     ]), [])
 
+    @with_transaction()
+    def test_agent_selection(self):
+        "Test agent selection"
+        pool = Pool()
+        Party = pool.get('party.party')
+        Identifier = pool.get('party.identifier')
+        Agent = pool.get('customs.agent')
+        Country = pool.get('country.country')
+        Selection = pool.get('customs.agent.selection')
+
+        party = Party(name="Agent", addresses=[{}])
+        party.save()
+        address, = party.addresses
+        tax_identifier = Identifier(
+            party=party, type='be_vat', code="BE403019261")
+        tax_identifier.save()
+        agent1 = Agent(
+            party=party,
+            address=address,
+            tax_identifier=tax_identifier)
+        agent1.save()
+        agent2 = Agent(
+            party=party,
+            address=address,
+            tax_identifier=tax_identifier)
+        agent2.save()
+
+        country1 = Country(name="Country 1")
+        country1.save()
+        country2 = Country(name="Country 2")
+        country2.save()
+
+        company = create_company()
+
+        with Transaction().set_context(company=company.id):
+            Selection.create([{
+                        'to_country': country1,
+                        'agent': agent1,
+                        }, {
+                        'agent': agent2,
+                        }])
+
+            self.assertEqual(Selection.get_agent(company, {
+                        'to_country': country1.id,
+                        }), agent1)
+            self.assertEqual(Selection.get_agent(company, {
+                        'to_country': country2.id,
+                        }), agent2)
+
 
 del ModuleTestCase
diff -r 7e5d3e3bef70 -r cedac0e38ccc modules/customs/tryton.cfg
--- a/modules/customs/tryton.cfg        Thu Feb 13 23:47:46 2025 +0100
+++ b/modules/customs/tryton.cfg        Sat Feb 01 01:52:03 2025 +0100
@@ -1,9 +1,11 @@
 [tryton]
 version=7.7.0
 depends:
+    company
     country
     currency
     ir
+    party
     product
     res
 xml:
@@ -14,6 +16,8 @@
 model:
     customs.TariffCode
     customs.DutyRate
+    customs.Agent
+    customs.AgentSelection
     product.Category
     product.Template
     product.Product_TariffCode
diff -r 7e5d3e3bef70 -r cedac0e38ccc modules/customs/view/customs_agent_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/customs/view/customs_agent_form.xml       Sat Feb 01 01:52:03 
2025 +0100
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="party"/>
+    <field name="party"/>
+    <label name="active"/>
+    <field name="active"/>
+
+    <label name="address"/>
+    <field name="address"/>
+    <label name="tax_identifier"/>
+    <field name="tax_identifier"/>
+</form>
diff -r 7e5d3e3bef70 -r cedac0e38ccc modules/customs/view/customs_agent_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/customs/view/customs_agent_list.xml       Sat Feb 01 01:52:03 
2025 +0100
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree>
+    <field name="party" expand="2"/>
+    <field name="address" expand="1" optional="0"/>
+    <field name="tax_identifier" expand="1" optional="1"/>
+</tree>
diff -r 7e5d3e3bef70 -r cedac0e38ccc 
modules/customs/view/customs_agent_selection_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/customs/view/customs_agent_selection_form.xml     Sat Feb 01 
01:52:03 2025 +0100
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form cursor="agent">
+    <label name="company"/>
+    <field name="company"/>
+    <label name="active"/>
+    <field name="active"/>
+
+    <label name="agent"/>
+    <field name="agent"/>
+    <label name="sequence"/>
+    <field name="sequence"/>
+
+    <separator string="Criteria" id="criteria" colspan="4"/>
+
+    <label name="from_country"/>
+    <field name="from_country"/>
+    <label name="to_country"/>
+    <field name="to_country"/>
+</form>
diff -r 7e5d3e3bef70 -r cedac0e38ccc 
modules/customs/view/customs_agent_selection_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/customs/view/customs_agent_selection_list.xml     Sat Feb 01 
01:52:03 2025 +0100
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree>
+    <field name="company" expand="1" optional="1"/>
+    <field name="from_country" expand="1" optional="0"/>
+    <field name="to_country" expand="1"/>
+    <field name="agent" expand="2"/>
+</tree>
diff -r 7e5d3e3bef70 -r cedac0e38ccc 
modules/customs/view/customs_agent_selection_list_sequence.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/customs/view/customs_agent_selection_list_sequence.xml    Sat Feb 
01 01:52:03 2025 +0100
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree editable="1" sequence="sequence">
+    <field name="company" expand="1" optional="1"/>
+    <field name="from_country" expand="1" optional="0"/>
+    <field name="to_country" expand="1"/>
+    <field name="agent" expand="2"/>
+</tree>

Reply via email to