details:   https://code.tryton.org/tryton/commit/61c6cde208f1
branch:    default
user:      Cédric Krier <[email protected]>
date:      Thu Mar 19 16:41:35 2026 +0100
description:
        Define Typless fields per document type

        Closes #14648
diffstat:

 modules/document_incoming_ocr_typless/CHANGELOG                                
        |    1 +
 modules/document_incoming_ocr_typless/doc/setup.rst                            
        |    5 +-
 modules/document_incoming_ocr_typless/document.py                              
        |  292 +++++----
 
modules/document_incoming_ocr_typless/tests/scenario_document_incoming_ocr_typless.rst
 |   21 +-
 modules/document_incoming_ocr_typless/tryton.cfg                               
        |    4 +
 
modules/document_incoming_ocr_typless/view/document_incoming_ocr_service_form.xml
      |    9 +
 6 files changed, 207 insertions(+), 125 deletions(-)

diffs (442 lines):

diff -r cd597ed62d56 -r 61c6cde208f1 
modules/document_incoming_ocr_typless/CHANGELOG
--- a/modules/document_incoming_ocr_typless/CHANGELOG   Thu Mar 05 10:49:11 
2026 +0100
+++ b/modules/document_incoming_ocr_typless/CHANGELOG   Thu Mar 19 16:41:35 
2026 +0100
@@ -1,3 +1,4 @@
+* Define fields per document type
 * Add support for payment reference of invoice
 
 Version 7.8.0 - 2025-12-15
diff -r cd597ed62d56 -r 61c6cde208f1 
modules/document_incoming_ocr_typless/doc/setup.rst
--- a/modules/document_incoming_ocr_typless/doc/setup.rst       Thu Mar 05 
10:49:11 2026 +0100
+++ b/modules/document_incoming_ocr_typless/doc/setup.rst       Thu Mar 19 
16:41:35 2026 +0100
@@ -16,8 +16,9 @@
 When setting the `OCR Service
 <document_incoming_ocr:model-document.incoming.ocr.service>`'s type to
 :guilabel:`Typless`, you must fill in the :guilabel:`API Key` with the key and
-:guilabel:`Document Type` with the name of the document type created. You can
-fill if needed the selection criteria.
+:guilabel:`Document Type` with the name of the document type created.
+You must select the fields setup for the document type on Typless model.
+You can fill if needed the selection criteria.
 
 
 .. _Setup fields for Unknown type:
diff -r cd597ed62d56 -r 61c6cde208f1 
modules/document_incoming_ocr_typless/document.py
--- a/modules/document_incoming_ocr_typless/document.py Thu Mar 05 10:49:11 
2026 +0100
+++ b/modules/document_incoming_ocr_typless/document.py Thu Mar 19 16:41:35 
2026 +0100
@@ -24,6 +24,35 @@
 ADD_DOCUMENT_FEEDBACK = (
     'https://developers.typless.com/api/add-document-feedback')
 
+SUPPLIER_INVOICE_FIELDS = [
+    'company_name',
+    'company_tax_identifier',
+    'supplier_name',
+    'tax_identifier',
+    'currency',
+    'number',
+    'description',
+    'invoice_date',
+    'payment_term_date',
+    'payment_reference',
+    'total_amount',
+    'untaxed_amount',
+    'tax_amount',
+    'purchase_orders',
+    ]
+SUPPLIER_INVOICE_FIELDS = [(x, x) for x in SUPPLIER_INVOICE_FIELDS]
+SUPPLIER_INVOICE_LINE_ITEM_FIELDS = [
+    'product_name',
+    'description',
+    'unit',
+    'quantity',
+    'unit_price',
+    'amount',
+    'purchase_order',
+    ]
+SUPPLIER_INVOICE_LINE_ITEM_FIELDS = [
+    (x, x) for x in SUPPLIER_INVOICE_LINE_ITEM_FIELDS]
+
 
 def typless_api(func):
     @wraps(func)
@@ -66,12 +95,39 @@
     typless_document_type = fields.Char(
         "Document Type", states=_states,
         help="The name of the document type on Typless.")
+    typless_fields = fields.MultiSelection(
+        'get_typless_fields', "Fields",
+        translate=False,
+        states={
+            'invisible': _states['invisible'],
+            },
+        help="The metadata fields setup for this document type.")
+    typless_line_item_fields = fields.MultiSelection(
+        'get_typless_line_item_fields', "Line Item Fields",
+        translate=False,
+        states={
+            'invisible': _states['invisible'],
+            },
+        help="The line item fields setup for this document type.")
+    typless_vat_rates = fields.Boolean(
+        "VAT Rates",
+        states={
+            'invisible': _states['invisible'],
+            },
+        help="Check if the vat rate net plugin is activated "
+        "for this document type.")
 
     @classmethod
     def __setup__(cls):
         super().__setup__()
         cls.type.selection.append(('typless', "Typless"))
 
+    def get_typless_fields(self):
+        return [('document_type', 'document_type')]
+
+    def get_typless_line_item_fields(self):
+        return []
+
     def match_mime_type(self, mime_type):
         match = super().match_mime_type(mime_type)
         if self.type == 'typless':
@@ -107,16 +163,119 @@
             document_data['document_type'] = document_type
         return document_data
 
+    @typless_api
+    def _send_feedback_typless(self, document):
+        payload = self._typless_feedback_payload(document)
+        headers = {
+            'Accept': 'application/json',
+            'Content-Type': 'application/json',
+            'Authorization': self.typless_api_key,
+            }
+        response = requests.post(
+            ADD_DOCUMENT_FEEDBACK, json=payload, headers=headers)
+        response.raise_for_status()
+
+    def _typless_feedback_payload(self, document):
+        source = document.result
+        fields = document.ocr_service.typless_fields
+        learning_fields = []
+        getter = getattr(self, f'_typless_feedback_payload_{document.type}')
+        for name in fields:
+            value = getter(source, name)
+            learning_fields.append({
+                    'name': name,
+                    'value': value,
+                    })
+        payload = {
+            'document_type_name': self.typless_document_type,
+            'document_object_id': document.parsed_data['object_id'],
+            'learning_fields': learning_fields,
+            }
+        line_items = document.ocr_service.typless_line_item_fields
+        if line_items:
+            lines = []
+            getter = getattr(
+                self, f'_typless_feedback_payload_line_items_{document.type}')
+            for line in source.line_lines:
+                line_item = []
+                for name in line_items:
+                    value = getter(line, name)
+                    line_item.append({
+                            'name': name,
+                            'value': value,
+                            })
+                lines.append(line_item)
+            payload['line_items'] = lines
+        if document.ocr_service.typless_vat_rates:
+            taxes = []
+            getter = getattr(
+                self, f'_typless_feedback_payload_vat_rates_{document.type}')
+            for tax_line in source.taxes:
+                percentage = getter(tax_line, 'vat_rate_percentage')
+                net = getter(tax_line, 'vat_rate_net')
+                if percentage and net:
+                    taxes.append([{
+                                'name': 'vat_rate_percentage',
+                                'value': percentage,
+                                }, {
+                                'name': 'vat_rate_net',
+                                'value': net,
+                                }])
+            payload['vat_rates'] = taxes
+        return payload
+
+    def _typless_feedback_payload_document_incoming(self, document, name):
+        if name == 'document_type':
+            return document.type
+
+    def _typless_feedback_payload_line_items_document_incoming(
+            self, line, name):
+        pass
+
+    def _typless_feedback_payload_vat_rates_document_incoming(
+            self, tax_line, name):
+        pass
+
+    @classmethod
+    def check_modification(cls, mode, services, values=None, external=False):
+        pool = Pool()
+        Warning = pool.get('res.user.warning')
+
+        super().check_modification(
+            mode, services, values=values, external=external)
+
+        if mode == 'write' and external and 'typless_api_key' in values:
+            warning_name = Warning.format('typless_credential', services)
+            if Warning.check(warning_name):
+                raise TyplessCredentialWarning(
+                    warning_name,
+                    gettext('document_incoming_ocr_typless'
+                        '.msg_typless_credential_modified'))
+
+
+class IncomingOCRService_IncomingInvoice(metaclass=PoolMeta):
+    __name__ = 'document.incoming.ocr.service'
+
+    @fields.depends('document_type')
+    def get_typless_fields(self):
+        selection = super().get_typless_fields()
+        if self.document_type == 'supplier_invoice':
+            selection += SUPPLIER_INVOICE_FIELDS
+        return selection
+
+    @fields.depends('document_type')
+    def get_typless_line_item_fields(self):
+        selection = super().get_typless_line_item_fields()
+        if self.document_type == 'supplier_invoice':
+            selection += SUPPLIER_INVOICE_LINE_ITEM_FIELDS
+        return selection
+
     def _get_supplier_invoice_typless(self, document):
         invoice_data = {}
         if not document.parsed_data:
             return invoice_data
         fields = document.parsed_data.get('extracted_fields', [])
-        for name in [
-                'company_name', 'company_tax_identifier', 'supplier_name',
-                'tax_identifier', 'currency', 'number', 'description',
-                'invoice_date', 'payment_term_date', 'payment_reference',
-                'total_amount', 'purchase_orders']:
+        for name in self.typless_fields:
             value = get_best_value(fields, name)
             if value is not None:
                 if name == 'total_amount':
@@ -136,9 +295,7 @@
         line = {}
         if 'purchase_orders' in invoice_data:
             line['purchase_orders'] = invoice_data['purchase_orders']
-        for name in [
-                'product_name', 'description', 'unit', 'quantity',
-                'unit_price', 'amount', 'purchase_order']:
+        for name in self.typless_line_item_fields:
             value = get_best_value(parsed_line, name)
             if value is not None:
                 if name in {'unit_price', 'amount'}:
@@ -158,90 +315,6 @@
             tax['base'] = Decimal(net)
         return tax
 
-    @typless_api
-    def _send_feedback_typless(self, document):
-        payload = self._typless_feedback_payload(document)
-        headers = {
-            'Accept': 'application/json',
-            'Content-Type': 'application/json',
-            'Authorization': self.typless_api_key,
-            }
-        response = requests.post(
-            ADD_DOCUMENT_FEEDBACK, json=payload, headers=headers)
-        response.raise_for_status()
-
-    def _typless_feedback_payload(self, document):
-        source = document.result
-        fields = document.parsed_data.get('extracted_fields', [])
-        learning_fields = []
-        getter = getattr(self, f'_typless_feedback_payload_{document.type}')
-        for field in fields:
-            name = field['name']
-            value = getter(source, name)
-            if value is None:
-                value = get_best_value(fields, name)
-            learning_fields.append({
-                    'name': name,
-                    'value': value,
-                    })
-        payload = {
-            'document_type_name': self.typless_document_type,
-            'document_object_id': document.parsed_data['object_id'],
-            'learning_fields': learning_fields,
-            }
-        line_items = document.parsed_data.get('line_items')
-        if line_items is not None:
-            lines = []
-            getter = getattr(
-                self, f'_typless_feedback_payload_line_items_{document.type}')
-            for i, line_item in enumerate(line_items):
-                line = []
-                for field in line_item:
-                    name = field['name']
-                    value = getter(i, name, source)
-                    if value is None:
-                        value = get_best_value(line_item, name)
-                    line.append({
-                            'name': name,
-                            'value': value,
-                            })
-                lines.append(line)
-            payload['line_items'] = lines
-        vat_rates = document.parsed_data.get('vat_rates')
-        if vat_rates is not None:
-            taxes = []
-            getter = getattr(
-                self, f'_typless_feedback_payload_vat_rates_{document.type}')
-            for i, vat_rate in enumerate(vat_rates):
-                percentage = getter(i, 'vat_rate_percentage', source)
-                if percentage is None:
-                    percentage = get_best_value(
-                        vat_rate, 'vat_rate_percentage')
-                net = getter(i, 'vat_rate_net', source)
-                if net is None:
-                    net = get_best_value(vat_rate, 'vat_rate_net')
-                taxes.append([{
-                            'name': 'vat_rate_percentage',
-                            'value': percentage,
-                            }, {
-                            'name': 'vat_rate_net',
-                            'value': net,
-                            }])
-            payload['vat_rates'] = taxes
-        return payload
-
-    def _typless_feedback_payload_document_incoming(self, document, name):
-        if name == 'document_type':
-            return document.type
-
-    def _typless_feedback_payload_line_items_document_incoming(
-            self, index, name, document):
-        pass
-
-    def _typless_feedback_payload_vat_rates_document_incoming(
-            self, index, name, document):
-        pass
-
     def _typless_feedback_payload_supplier_invoice(self, invoice, name):
         if name == 'company_name':
             return invoice.company.party.name
@@ -282,12 +355,7 @@
             return invoice.origins
 
     def _typless_feedback_payload_line_items_supplier_invoice(
-            self, index, name, invoice):
-        lines = [l for l in invoice.lines if l.type == 'line']
-        try:
-            line = lines[index]
-        except IndexError:
-            return ''
+            self, line, name):
         if name == 'product_name':
             if line.product:
                 return line.product.name
@@ -310,31 +378,11 @@
             return line.origin_name
 
     def _typless_feedback_payload_vat_rates_supplier_invoice(
-            self, index, name, invoice):
-        try:
-            line = invoice.taxes[index]
-        except IndexError:
-            return ''
+            self, tax_line, name):
         if name == 'vat_rate_percentage':
-            if line.tax and line.tax.type == 'percentage':
-                return str(line.tax.rate * 100)
+            if tax_line.tax and tax_line.tax.type == 'percentage':
+                return str(tax_line.tax.rate * 100)
             else:
                 return ''
         elif name == 'vat_rate_net':
-            return str(line.base)
-
-    @classmethod
-    def check_modification(cls, mode, services, values=None, external=False):
-        pool = Pool()
-        Warning = pool.get('res.user.warning')
-
-        super().check_modification(
-            mode, services, values=values, external=external)
-
-        if mode == 'write' and external and 'typless_api_key' in values:
-            warning_name = Warning.format('typless_credential', services)
-            if Warning.check(warning_name):
-                raise TyplessCredentialWarning(
-                    warning_name,
-                    gettext('document_incoming_ocr_typless'
-                        '.msg_typless_credential_modified'))
+            return str(tax_line.base)
diff -r cd597ed62d56 -r 61c6cde208f1 
modules/document_incoming_ocr_typless/tests/scenario_document_incoming_ocr_typless.rst
--- 
a/modules/document_incoming_ocr_typless/tests/scenario_document_incoming_ocr_typless.rst
    Thu Mar 05 10:49:11 2026 +0100
+++ 
b/modules/document_incoming_ocr_typless/tests/scenario_document_incoming_ocr_typless.rst
    Thu Mar 19 16:41:35 2026 +0100
@@ -70,9 +70,28 @@
 
 Setup Typless service::
 
-    >>> ocr_service = OCRService(type='typless')
+    >>> ocr_service = OCRService(type='typless', 
document_type='supplier_invoice')
     >>> ocr_service.typless_api_key = os.getenv('TYPLESS_API_KEY')
     >>> ocr_service.typless_document_type = os.getenv('TYPLESS_DOCUMENT_TYPE')
+    >>> ocr_service.typless_fields = [
+    ...     'supplier_name',
+    ...     'company_name',
+    ...     'number',
+    ...     'purchase_orders',
+    ...     'invoice_date',
+    ...     'payment_term_date',
+    ...     'total_amount',
+    ...     'untaxed_amount',
+    ...     ]
+    >>> ocr_service.typless_line_item_fields = [
+    ...     'description',
+    ...     'product_name',
+    ...     'unit',
+    ...     'amount',
+    ...     'quantity',
+    ...     'unit_price',
+    ...     ]
+    >>> ocr_service.typless_vat_rates = True
     >>> ocr_service.save()
 
 Create incoming document::
diff -r cd597ed62d56 -r 61c6cde208f1 
modules/document_incoming_ocr_typless/tryton.cfg
--- a/modules/document_incoming_ocr_typless/tryton.cfg  Thu Mar 05 10:49:11 
2026 +0100
+++ b/modules/document_incoming_ocr_typless/tryton.cfg  Thu Mar 19 16:41:35 
2026 +0100
@@ -15,3 +15,7 @@
 [register]
 model:
     document.IncomingOCRService
+
+[register document_incoming_invoice]
+model:
+    document.IncomingOCRService_IncomingInvoice
diff -r cd597ed62d56 -r 61c6cde208f1 
modules/document_incoming_ocr_typless/view/document_incoming_ocr_service_form.xml
--- 
a/modules/document_incoming_ocr_typless/view/document_incoming_ocr_service_form.xml
 Thu Mar 05 10:49:11 2026 +0100
+++ 
b/modules/document_incoming_ocr_typless/view/document_incoming_ocr_service_form.xml
 Thu Mar 19 16:41:35 2026 +0100
@@ -8,6 +8,15 @@
         <field name="typless_api_key" widget="password"/>
         <label name="typless_document_type"/>
         <field name="typless_document_type"/>
+
+        <separator name="typless_fields" colspan="4"/>
+        <field name="typless_fields" colspan="4"/>
+
+        <separator name="typless_line_item_fields" colspan="4"/>
+        <field name="typless_line_item_fields" colspan="4"/>
+
+        <label name="typless_vat_rates"/>
+        <field name="typless_vat_rates"/>
         <newline/>
     </xpath>
 </data>

Reply via email to