diff --git a/edi_gs1_oca/README.rst b/edi_gs1_oca/README.rst new file mode 100644 index 000000000..deeb0aec8 --- /dev/null +++ b/edi_gs1_oca/README.rst @@ -0,0 +1,89 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============ +Base GS1 EDI +============ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:a5ab15a8b1dea2fd4da5955af650a8db9296c1a96cb53d4d7d98188d059a14da + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fedi--framework-lightgray.png?logo=github + :target: https://github.com/OCA/edi-framework/tree/19.0/edi_gs1_oca + :alt: OCA/edi-framework +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/edi-framework-19-0/edi-framework-19-0-edi_gs1_oca + :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/edi-framework&target_branch=19.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This is the base module for the implementation of the `GS1 -The Global +Language of Business `__ standard. + +This module contains methods to generate and parse XML files compliant +with this standard. + +Beside basic machinery it provides common components to generate common +GS1 schema elements. Namely: + + - StandardBusinessDocumentHeader + +**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 +------- + +* ACSONE + +Contributors +------------ + +- Simone Orsi + +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. + +This module is part of the `OCA/edi-framework `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/edi_gs1_oca/TODO b/edi_gs1_oca/TODO new file mode 100644 index 000000000..caf440dd3 --- /dev/null +++ b/edi_gs1_oca/TODO @@ -0,0 +1,13 @@ +Inbound Instruction + +- [x] allow skipping keys / elements by returning none +- [x] check UOM code handling +- [x] finish mapping of what we know +- [] draft RF-EFR module w/ tests +- [] allow export from PO +- [] handle state of export +- [] use storage backend to store it +- [] add lookup of acknowledge message +- [] add new field code to UOM +- [] draft send ack +- [] draft Outbound Notification parse diff --git a/edi_gs1_oca/__init__.py b/edi_gs1_oca/__init__.py new file mode 100644 index 000000000..cc6b6354a --- /dev/null +++ b/edi_gs1_oca/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import post_init_hook diff --git a/edi_gs1_oca/__manifest__.py b/edi_gs1_oca/__manifest__.py new file mode 100644 index 000000000..24ada98ef --- /dev/null +++ b/edi_gs1_oca/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Base GS1 EDI", + "summary": """ + Base module for GS1 standard EDI exchange""", + "version": "19.0.1.0.0", + "development_status": "Beta", + "license": "AGPL-3", + "website": "https://github.com/OCA/edi-framework", + "author": "ACSONE,Odoo Community Association (OCA)", + "depends": ["edi_core_oca", "uom"], + "external_dependencies": {"python": ["xmlschema", "xmlunittest"]}, + "data": [ + "security/gs1_backend_acl.xml", + "data/gs1_backend_data.xml", + "data/ack_in_exchange_type_data.xml", + "data/ack_out_exchange_type_data.xml", + "data/business_header_qweb_template.xml", + "data/contact_details_qweb_template.xml", + "views/res_partner.xml", + "views/edi_backend.xml", + "views/uom_uom.xml", + ], + "post_init_hook": "post_init_hook", +} diff --git a/edi_gs1_oca/data/ack_in_exchange_type_data.xml b/edi_gs1_oca/data/ack_in_exchange_type_data.xml new file mode 100644 index 000000000..e63b86fbe --- /dev/null +++ b/edi_gs1_oca/data/ack_in_exchange_type_data.xml @@ -0,0 +1,25 @@ + + + + + GS1 ApplicationReceiptAcknowledgement + applicationReceiptAcknowledgement_in + input + + + xml + + + +gs1: + schema_path: edi_gs1_oca:static/schemas/gs1/ecom/ApplicationReceiptAcknowledgement.xsd + + + diff --git a/edi_gs1_oca/data/ack_out_exchange_type_data.xml b/edi_gs1_oca/data/ack_out_exchange_type_data.xml new file mode 100644 index 000000000..edc47991c --- /dev/null +++ b/edi_gs1_oca/data/ack_out_exchange_type_data.xml @@ -0,0 +1,17 @@ + + + + + GS1 ApplicationReceiptAcknowledgement + applicationReceiptAcknowledgement_out + output + + {record_name}-{type.code}-{dt} + xml + + +gs1: + schema_path: edi_gs1_oca:static/schemas/gs1/ecom/ApplicationReceiptAcknowledgement.xsd + + + diff --git a/edi_gs1_oca/data/business_header_qweb_template.xml b/edi_gs1_oca/data/business_header_qweb_template.xml new file mode 100644 index 000000000..30f104fc0 --- /dev/null +++ b/edi_gs1_oca/data/business_header_qweb_template.xml @@ -0,0 +1,65 @@ + + + + + diff --git a/edi_gs1_oca/data/contact_details_qweb_template.xml b/edi_gs1_oca/data/contact_details_qweb_template.xml new file mode 100644 index 000000000..8cbe00ef1 --- /dev/null +++ b/edi_gs1_oca/data/contact_details_qweb_template.xml @@ -0,0 +1,47 @@ + + + + diff --git a/edi_gs1_oca/data/gs1_backend_data.xml b/edi_gs1_oca/data/gs1_backend_data.xml new file mode 100644 index 000000000..83f1a0b55 --- /dev/null +++ b/edi_gs1_oca/data/gs1_backend_data.xml @@ -0,0 +1,11 @@ + + + + GS1 + gs1 + + + GS1 default + + + diff --git a/edi_gs1_oca/hooks.py b/edi_gs1_oca/hooks.py new file mode 100644 index 000000000..c6dbb045f --- /dev/null +++ b/edi_gs1_oca/hooks.py @@ -0,0 +1,11 @@ +# Copyright 2021 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +def post_init_hook(env): + """ + Hook used to init automatically some GS1 code on existing UoM + :param env: Odoo Environment + :return: + """ + env["uom.uom"]._execute_gs1_map_code() diff --git a/edi_gs1_oca/models/__init__.py b/edi_gs1_oca/models/__init__.py new file mode 100644 index 000000000..a0074d0b1 --- /dev/null +++ b/edi_gs1_oca/models/__init__.py @@ -0,0 +1,7 @@ +from . import edi_gs1_xml +from . import edi_gs1_output +from . import edi_gs1_input +from . import edi_gs1_handler +from . import edi_backend +from . import res_partner +from . import uom_uom diff --git a/edi_gs1_oca/models/edi_backend.py b/edi_gs1_oca/models/edi_backend.py new file mode 100644 index 000000000..a642e07e7 --- /dev/null +++ b/edi_gs1_oca/models/edi_backend.py @@ -0,0 +1,21 @@ +# Copyright 2020 ACSONE SA +# @author Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class EDIBackend(models.Model): + _inherit = "edi.backend" + + # TODO: find another way to configure this w/out polluting edi backend + lsp_partner_id = fields.Many2one( + string="Logistic Services Provider (LSP)", + comodel_name="res.partner", + domain=[("is_lsp", "=", True)], + ) + lsc_partner_id = fields.Many2one( + string="Logistic Services Client (LSC)", + comodel_name="res.partner", + default=lambda self: self.env.ref("base.main_partner").id, + ) diff --git a/edi_gs1_oca/models/edi_gs1_handler.py b/edi_gs1_oca/models/edi_gs1_handler.py new file mode 100644 index 000000000..bff5164cc --- /dev/null +++ b/edi_gs1_oca/models/edi_gs1_handler.py @@ -0,0 +1,81 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import exceptions, models + +from odoo.addons.edi_core_oca.exceptions import EDIValidationError + + +class EdiGs1HandlerValidate(models.AbstractModel): + """Validate GS1 exchange files against their XSD schema. + + Wired on exchange types via ``input_validate_model_id`` / + ``output_validate_model_id``. The schema is read from the exchange type + advanced settings:: + + gs1: + schema_path: edi_gs1_oca:static/schemas/.../Foo.xsd + """ + + _name = "edi.gs1.handler.validate" + _inherit = ["edi.oca.handler.input.validate", "edi.oca.handler.output.validate"] + _description = "GS1 EDI XSD validation handler" + + def _gs1_validate(self, exchange_record, value=None): + settings = exchange_record.type_id.get_settings() or {} + schema_path = settings.get("gs1", {}).get("schema_path") + if not schema_path: + return None + content = value + if content is None: + content = exchange_record._get_file_content() + if isinstance(content, bytes): + content = content.decode() + error = self.env["edi.gs1.xml"].validate(schema_path, content) + if error: + raise EDIValidationError(error) + return None + + def input_validate(self, exchange_record, value=None, **kw): + return self._gs1_validate(exchange_record, value=value) + + def output_validate(self, exchange_record, value=None, **kw): + return self._gs1_validate(exchange_record, value=value) + + +class EdiGs1HandlerApplicationReceiptAck(models.AbstractModel): + """Process GS1 applicationReceiptAcknowledgement. + + Wired on the input ack exchange type via ``process_model_id``. + """ + + _name = "edi.gs1.handler.application.receipt.ack" + _inherit = ["edi.oca.handler.process", "edi.gs1.input.mixin"] + _description = "GS1 process applicationReceiptAcknowledgement" + + def process(self, exchange_record, **kw): + data = self._parse(exchange_record) + self._process_data(data, exchange_record) + return self.env._("applicationReceiptAcknowledgement processed") + + def _process_data(self, data, exchange_record): + if not self.is_ok(data): + # TODO: give a proper message based on the real state + # options: ERROR, RECEIVED, WARNING + raise exceptions.ValidationError(self.env._("Exchange not received")) + + def is_ok(self, data): + return self._get_status(data) == "RECEIVED" + + def _get_status(self, data): + try: + ack = data["applicationReceiptAcknowledgement"][0] + except IndexError as exc: + # Maybe raise an EDIValidationError? + raise exceptions.ValidationError( + self.env._("applicationReceiptAcknowledgement element not found!") + ) from exc + status_wrapper = ack["applicationResponseDocumentLevel"][0] + # options: ERROR, RECEIVED, WARNING + return status_wrapper["applicationResponseStatusCode"] diff --git a/edi_gs1_oca/models/edi_gs1_input.py b/edi_gs1_oca/models/edi_gs1_input.py new file mode 100644 index 000000000..710c3de5d --- /dev/null +++ b/edi_gs1_oca/models/edi_gs1_input.py @@ -0,0 +1,28 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class EdiGs1InputMixin(models.AbstractModel): + """Common GS1 input processing mixin. + + Replaces the old ``edi.gs1.input.mixin`` component. Concrete processors + inherit this mixin together with ``edi.oca.handler.process``. + """ + + _name = "edi.gs1.input.mixin" + _description = "GS1 EDI input mixin" + + def _get_schema_path(self, exchange_record): + settings = exchange_record.type_id.get_settings() or {} + return settings.get("gs1", {}).get("schema_path") + + def _parse(self, exchange_record): + schema_path = self._get_schema_path(exchange_record) + content = exchange_record._get_file_content() + return self.env["edi.gs1.xml"].parse_xml(schema_path, content) + + def _process_data(self, data, exchange_record): + raise NotImplementedError() diff --git a/edi_gs1_oca/models/edi_gs1_output.py b/edi_gs1_oca/models/edi_gs1_output.py new file mode 100644 index 000000000..61308416f --- /dev/null +++ b/edi_gs1_oca/models/edi_gs1_output.py @@ -0,0 +1,74 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import datetime + +import pytz + +from odoo import fields, models +from odoo.tools import DotDict + +from ..utils import xml_purge_nswrapper + + +class EdiGs1OutputMixin(models.AbstractModel): + """Common GS1 output generation mixin. + + Replaces the old ``edi.output.info.mixin`` (info provider) and the + ``edi.exchange.template.output`` QWeb rendering, both dropped in the + new EDI OCA architecture. Concrete generators inherit this mixin + together with ``edi.oca.handler.generate``. + """ + + _name = "edi.gs1.output.mixin" + _description = "GS1 EDI output mixin" + + # --- info provider (ported from edi.info.provider.mixin) --- + def generate_info(self, exchange_record, **kw): + """Generate and return data for output info. + + :return: odoo.tools.DotDict + """ + return DotDict(self._generate_info(exchange_record, **kw)) + + def _generate_info(self, exchange_record, **kw): + raise NotImplementedError("You must provide `_generate_info`") + + @staticmethod + def _utc_now(): + return datetime.datetime.utcnow().isoformat() + + @staticmethod + def date_to_string(dt, utc=True): + if utc: + dt = dt.astimezone(pytz.UTC) + return fields.Date.to_string(dt) + + def _document_action_code(self, replace_existing=False): + # Brand new file -> "ADD", otherwise replacing an existing one + return "CHANGE_BY_REFRESH" if replace_existing else "ADD" + + # --- template rendering (ported from edi.exchange.template.output) --- + def _get_render_values(self, exchange_record, **kw): + """Collect values to render a GS1 QWeb template.""" + values = { + "exchange_record": exchange_record, + "record": exchange_record.record, + "backend": exchange_record.backend_id, + # Default identifier to the exchange record one + "instance_identifier": exchange_record.identifier, + "doc_type": None, + "utc_now": self._utc_now, + "date_to_string": self.date_to_string, + "render_edi_template": self._render_edi_template, + "info": {}, + } + values.update(kw) + return values + + def _render_edi_template(self, exchange_record, xmlid, **kw): + """Render a GS1 QWeb template and purge the `nswrapper` helpers.""" + values = self._get_render_values(exchange_record, **kw) + output = self.env["ir.qweb"]._render(xmlid, values) + return xml_purge_nswrapper(output) diff --git a/edi_gs1_oca/models/edi_gs1_xml.py b/edi_gs1_oca/models/edi_gs1_xml.py new file mode 100644 index 000000000..df244e593 --- /dev/null +++ b/edi_gs1_oca/models/edi_gs1_xml.py @@ -0,0 +1,63 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import io +from contextlib import closing + +import xmlschema + +from odoo import models, tools +from odoo.tools import DotDict + + +class EdiGs1Xml(models.AbstractModel): + """Validate and parse XML against a bundled XSD schema. + + Replaces the ``edi.xml`` component from the old ``edi_xml`` module + (dropped in the new EDI OCA architecture). Schema paths use the + ``module:relative/path.xsd`` notation. + """ + + _name = "edi.gs1.xml" + _description = "GS1 EDI XML helper" + + @staticmethod + def _resolve_schema_path(schema_path): + try: + mod_name, path = schema_path.split(":") + except ValueError as exc: + raise ValueError("Path must be in the form `module:path`") from exc + return tools.file_path(f"{mod_name}/{path}") + + def _get_schema(self, schema_path): + return xmlschema.XMLSchema(self._resolve_schema_path(schema_path)) + + def parse_xml(self, schema_path, file_content, **kw): + """Read XML content and return a data dict. + + :param schema_path: schema path as ``module:path`` + :param file_content: str of XML file + :return: odoo.tools.DotDict with final data + """ + schema = self._get_schema(schema_path) + with closing(io.StringIO(file_content)) as fd: + return DotDict(schema.to_dict(fd, **kw)) + + def validate(self, schema_path, xml_content, raise_on_fail=False): + """Validate XML content against the XSD schema. + + :param schema_path: schema path as ``module:path`` + :param xml_content: str containing xml data to validate + :param raise_on_fail: turn on/off validation error exception on fail + :return: + * None if validation is ok + * error string if `raise_on_fail` is False + """ + schema = self._get_schema(schema_path) + try: + return schema.validate(xml_content) + except xmlschema.validators.exceptions.XMLSchemaValidationError as err: + if raise_on_fail: + raise + return str(err) diff --git a/edi_gs1_oca/models/res_partner.py b/edi_gs1_oca/models/res_partner.py new file mode 100644 index 000000000..6bf612db7 --- /dev/null +++ b/edi_gs1_oca/models/res_partner.py @@ -0,0 +1,12 @@ +# Copyright 2020 ACSONE SA/NV () +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + # TODO: move to another module? Or, is there another module providing this? + gln_code = fields.Char() + is_lsp = fields.Boolean(string="Is Logistic Services Provider (LSP)") diff --git a/edi_gs1_oca/models/uom_uom.py b/edi_gs1_oca/models/uom_uom.py new file mode 100644 index 000000000..acc375296 --- /dev/null +++ b/edi_gs1_oca/models/uom_uom.py @@ -0,0 +1,70 @@ +# Copyright 2020 ACSONE SA/NV () +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from odoo import api, fields, models + + +class UomUom(models.Model): + _inherit = "uom.uom" + + gs1_code = fields.Char( + string="GS1 code", help="Code used to export with GS1 format" + ) + + @api.model + def _gs1_mapping_code(self): + """ + Get a dict with the mapping between the UoM code (from Odoo) and the GS1 code. + This mapping table is used into the init_hook of the module to automatically + init existing UoM. + key should be always in lower case! + Codes come from this url: + https://resources.gs1us.org/GS1-US-Data-Hub-Help-Center/ArtMID/3451/ + ArticleID/116/Unit-of-Measure-Codes + :return: dict + """ + map_code = { + "kg": "KGM", + "day": "DAY", + "days": "DAY", + "box": "BX", + "boxes": "BX", + "dozen": "DZN", + "dozens": "DZN", + "hours": "HUR", + "hour": "HUR", + "liters": "LTR", + "liter": "LTR", + "cm": "CMT", + "fl oz": "OZI", + "foot(ft)": "FOT", + "foot": "FOT", + "g": "GRM", + "gal": "GLI", + "gals": "GLI", + "inches": "INH", + "km": "KMT", + "m": "MTR", + } + return map_code + + @api.model + def _execute_gs1_map_code(self, overwrite_existing=False): + """ + Execute (or update) the gs1 mapping based on UoM name (lower case). + By default, it doesn't update UoM with a value into gs1_code. + But it can be forced by setting the overwrite_existing parameter to True. + :param overwrite_existing: bool + :return: bool + """ + domain = [("gs1_code", "=", False)] + if overwrite_existing: + domain = [] + uoms = self.env["uom.uom"].search(domain) + map_code = uoms._gs1_mapping_code() + for uom in uoms: + # Key always in lower case so the name too! + name = uom.name.lower() + gs1_code = map_code.get(name) + if gs1_code and uom.gs1_code != gs1_code: + uom.write({"gs1_code": gs1_code}) + return True diff --git a/edi_gs1_oca/pyproject.toml b/edi_gs1_oca/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/edi_gs1_oca/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/edi_gs1_oca/readme/CONTRIBUTORS.md b/edi_gs1_oca/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..5792655b0 --- /dev/null +++ b/edi_gs1_oca/readme/CONTRIBUTORS.md @@ -0,0 +1 @@ +- Simone Orsi \<\> diff --git a/edi_gs1_oca/readme/DESCRIPTION.md b/edi_gs1_oca/readme/DESCRIPTION.md new file mode 100644 index 000000000..7f5b2ba48 --- /dev/null +++ b/edi_gs1_oca/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +This is the base module for the implementation of the [GS1 -The Global +Language of Business](http://gs1.org/) standard. + +This module contains methods to generate and parse XML files compliant +with this standard. + +Beside basic machinery it provides common components to generate common +GS1 schema elements. Namely: + +> - StandardBusinessDocumentHeader diff --git a/edi_gs1_oca/security/gs1_backend_acl.xml b/edi_gs1_oca/security/gs1_backend_acl.xml new file mode 100644 index 000000000..eda64df05 --- /dev/null +++ b/edi_gs1_oca/security/gs1_backend_acl.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/edi_gs1_oca/static/description/icon.png b/edi_gs1_oca/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/edi_gs1_oca/static/description/icon.png differ diff --git a/edi_gs1_oca/static/description/index.html b/edi_gs1_oca/static/description/index.html new file mode 100644 index 000000000..f3d540eee --- /dev/null +++ b/edi_gs1_oca/static/description/index.html @@ -0,0 +1,439 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Base GS1 EDI

+ +

Beta License: AGPL-3 OCA/edi-framework Translate me on Weblate Try me on Runboat

+

This is the base module for the implementation of the GS1 -The Global +Language of Business standard.

+

This module contains methods to generate and parse XML files compliant +with this standard.

+

Beside basic machinery it provides common components to generate common +GS1 schema elements. Namely:

+
+
    +
  • StandardBusinessDocumentHeader
  • +
+
+

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

+
    +
  • ACSONE
  • +
+
+
+

Contributors

+ +
+
+

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.

+

This module is part of the OCA/edi-framework project on GitHub.

+

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

+
+
+
+
+ + diff --git a/edi_gs1_oca/static/schemas/gs1/ecom/ApplicationReceiptAcknowledgement.xsd b/edi_gs1_oca/static/schemas/gs1/ecom/ApplicationReceiptAcknowledgement.xsd new file mode 100644 index 000000000..22c9524b7 --- /dev/null +++ b/edi_gs1_oca/static/schemas/gs1/ecom/ApplicationReceiptAcknowledgement.xsd @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingCommon.xsd b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingCommon.xsd new file mode 100644 index 000000000..3395efe4d --- /dev/null +++ b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingCommon.xsd @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingInboundInstruction.xsd b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingInboundInstruction.xsd new file mode 100644 index 000000000..942d4e657 --- /dev/null +++ b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingInboundInstruction.xsd @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingInboundNotification.xsd b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingInboundNotification.xsd new file mode 100644 index 000000000..c4231c980 --- /dev/null +++ b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingInboundNotification.xsd @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingOutboundInstruction.xsd b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingOutboundInstruction.xsd new file mode 100644 index 000000000..c43381530 --- /dev/null +++ b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingOutboundInstruction.xsd @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingOutboundNotification.xsd b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingOutboundNotification.xsd new file mode 100644 index 000000000..61ab3416b --- /dev/null +++ b/edi_gs1_oca/static/schemas/gs1/ecom/WarehousingOutboundNotification.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/gs1/ecom/eComCommon.xsd b/edi_gs1_oca/static/schemas/gs1/ecom/eComCommon.xsd new file mode 100644 index 000000000..ab3bfc7b4 --- /dev/null +++ b/edi_gs1_oca/static/schemas/gs1/ecom/eComCommon.xsd @@ -0,0 +1,1858 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/gs1/shared/SharedCommon.xsd b/edi_gs1_oca/static/schemas/gs1/shared/SharedCommon.xsd new file mode 100644 index 000000000..d51b853df --- /dev/null +++ b/edi_gs1_oca/static/schemas/gs1/shared/SharedCommon.xsd @@ -0,0 +1,1506 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/sbdh/BasicTypes.xsd b/edi_gs1_oca/static/schemas/sbdh/BasicTypes.xsd new file mode 100644 index 000000000..3718acfe5 --- /dev/null +++ b/edi_gs1_oca/static/schemas/sbdh/BasicTypes.xsd @@ -0,0 +1,20 @@ + + + + + + The MIME type as defined by IANA. Please refer to + http://www.iana.org/assignments/media-types/ for a list of types. + + + + + + + + ISO 639-2; 1998 representation of Language name. Refer to http://www.loc.gov/standards/iso639-2/iso639jac.html to get the latest version of the standard. + + + + + diff --git a/edi_gs1_oca/static/schemas/sbdh/BusinessScope.xsd b/edi_gs1_oca/static/schemas/sbdh/BusinessScope.xsd new file mode 100644 index 000000000..19ddd7ce0 --- /dev/null +++ b/edi_gs1_oca/static/schemas/sbdh/BusinessScope.xsd @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/sbdh/DocumentIdentification.xsd b/edi_gs1_oca/static/schemas/sbdh/DocumentIdentification.xsd new file mode 100644 index 000000000..5264a2e27 --- /dev/null +++ b/edi_gs1_oca/static/schemas/sbdh/DocumentIdentification.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/sbdh/Manifest.xsd b/edi_gs1_oca/static/schemas/sbdh/Manifest.xsd new file mode 100644 index 000000000..59bed11f1 --- /dev/null +++ b/edi_gs1_oca/static/schemas/sbdh/Manifest.xsd @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/sbdh/Partner.xsd b/edi_gs1_oca/static/schemas/sbdh/Partner.xsd new file mode 100644 index 000000000..ff4258da6 --- /dev/null +++ b/edi_gs1_oca/static/schemas/sbdh/Partner.xsd @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/static/schemas/sbdh/StandardBusinessDocumentHeader.xsd b/edi_gs1_oca/static/schemas/sbdh/StandardBusinessDocumentHeader.xsd new file mode 100644 index 000000000..b97fb45f4 --- /dev/null +++ b/edi_gs1_oca/static/schemas/sbdh/StandardBusinessDocumentHeader.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/tests/__init__.py b/edi_gs1_oca/tests/__init__.py new file mode 100644 index 000000000..321fab57d --- /dev/null +++ b/edi_gs1_oca/tests/__init__.py @@ -0,0 +1,3 @@ +from . import test_business_header + +# from . import test_application_receipt_ack diff --git a/edi_gs1_oca/tests/common.py b/edi_gs1_oca/tests/common.py new file mode 100644 index 000000000..7c0b2bae5 --- /dev/null +++ b/edi_gs1_oca/tests/common.py @@ -0,0 +1,57 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import os + +import xmlunittest + +from odoo.tests.common import TransactionCase, tagged + + +@tagged("-at_install", "post_install") +class BaseTestCase(TransactionCase, xmlunittest.XmlTestMixin): + _schema_path = "edi_gs1_oca:static/schemas/sbdh/StandardBusinessDocumentHeader.xsd" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.backend = cls._get_backend() + # Logistic Services Provider (LSP) + cls.lsp_partner = cls.env["res.partner"].create( + { + "name": "Test LSP", + "email": "lsp@example.com", + "phone": "+1-212-555-0001", + } + ) + # Logistic Services Client (LSC) + cls.lsc_partner = cls.env.ref("base.main_partner") + cls.lsc_partner.write({"email": "lsc@example.com", "phone": "+1-212-555-0002"}) + cls.backend.lsp_partner_id = cls.lsp_partner + cls.backend.lsc_partner_id = cls.lsc_partner + + # set fake GLN codes + cls.lsp_partner.gln_code = "1".zfill(13) + cls.lsc_partner.gln_code = "2".zfill(13) + # We have to trigger this gs1_code update manually in case of a submodule + # update them. + cls.env["uom.uom"]._execute_gs1_map_code() + + @classmethod + def _get_backend(cls): + return cls.env.ref("edi_gs1_oca.edi_backend_gs1_default") + + def flatten(self, txt): + return "".join([x.strip() for x in txt.splitlines()]) + + def read_test_file(self, filename): + path = os.path.join(os.path.dirname(__file__), "examples", filename) + with open(path) as thefile: + return thefile.read() + + def _validate_xml(self, content, schema_path=None): + return self.env["edi.gs1.xml"].validate( + schema_path or self._schema_path, content + ) diff --git a/edi_gs1_oca/tests/examples/ApplicationReceiptAcknowledgement.example1.xml b/edi_gs1_oca/tests/examples/ApplicationReceiptAcknowledgement.example1.xml new file mode 100644 index 000000000..8cd39ca3f --- /dev/null +++ b/edi_gs1_oca/tests/examples/ApplicationReceiptAcknowledgement.example1.xml @@ -0,0 +1,65 @@ + + + + 1.0 + + + + John Doe + John_Doe@purchasing.XYZretailer.com + +1-212-555-1213 + +1-212-555-2122 + Buyer + + + + + + Mary Smith + Mary_Smith@widgets.com + +1-312-555-1214 + +1-312-555-2125 + Seller + + + + GS1 + 3.4 + 100002 + + false + 2006-01-10T12:00:01.000-05:00 + + + + 2011-03-11T11:43:00.000-05:00 + ORIGINAL + + RA0009 + + 4098765000010 + + + + ERROR + 2011-03-11T11:00:00.000-05:00 + 35 + + PO3352 + + 5412345000013 + + + + INCOMPLETE_MESSAGE + + + + diff --git a/edi_gs1_oca/tests/test_business_header.py b/edi_gs1_oca/tests/test_business_header.py new file mode 100644 index 000000000..7bfcfc54b --- /dev/null +++ b/edi_gs1_oca/tests/test_business_header.py @@ -0,0 +1,101 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from freezegun import freeze_time + +from .common import BaseTestCase + +BH_NS = "http://www.unece.org/cefact/namespaces/StandardBusinessDocumentHeader" + + +class BusinessHeaderTestCase(BaseTestCase): + _schema_path = "edi_gs1_oca:static/schemas/sbdh/StandardBusinessDocumentHeader.xsd" + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._setup_records() + + @classmethod + def _setup_records(cls): + cls.output_handler = cls.env["edi.gs1.output.mixin"] + vals = { + "backend_id": cls.backend.id, + "backend_type_id": cls.backend.backend_type_id.id, + "name": "Template output 1", + "direction": "output", + "code": "test_type_out1", + "exchange_file_ext": "txt", + "exchange_filename_pattern": "{record.ref}-{type.code}-{dt}", + # Output types require a send handler (edi_core_oca constraint). + "send_model_id": cls.env.ref("edi_core_oca.model_edi_oca_handler_noop").id, + } + cls.exc_type = cls.env["edi.exchange.type"].create(vals) + # Does not really matter which record we bind the exchange to + cls.related_record = cls.env["res.partner"].create({"name": "Test Record"}) + vals = { + "model": cls.related_record._name, + "res_id": cls.related_record.id, + "type_id": cls.exc_type.id, + } + cls.exc_record = cls.backend.create_record("test_type_out1", vals) + + def test_template_render_values(self): + values = self.output_handler._get_render_values(self.exc_record) + expected = { + "backend": self.backend, + "exchange_record": self.exc_record, + "instance_identifier": self.exc_record.identifier, + "record": self.related_record, + } + for k, v in expected.items(): + self.assertEqual(values[k], v) + + @freeze_time("2020-07-08 07:30:00") + def test_xml(self): + output = self.output_handler._render_edi_template( + self.exc_record, + "edi_gs1_oca.edi_exchange_business_header", + sender=self.lsc_partner, + receiver=self.lsp_partner, + ) + expected = """ + + 1.0 + + + + {sender.name} + {sender.email} + {sender.phone} + Buyer + + + + + + {receiver.name} + {receiver.email} + {receiver.phone} + Seller + + + + GS1 + 3.4 + {identifier} + + false + {date} + + + """.format( + ns=BH_NS, + sender=self.lsc_partner, + receiver=self.lsp_partner, + identifier=self.exc_record.identifier, + date="2020-07-08T07:30:00", + ) + self.assertXmlEquivalentOutputs(self.flatten(output), self.flatten(expected)) + self.assertEqual(self._validate_xml(output), None) diff --git a/edi_gs1_oca/utils.py b/edi_gs1_oca/utils.py new file mode 100644 index 000000000..e22e20014 --- /dev/null +++ b/edi_gs1_oca/utils.py @@ -0,0 +1,39 @@ +# Copyright 2020 ACSONE +# @author: Simone Orsi +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from lxml import etree + + +def xml_purge_nswrapper(xml_content): + """Purge `nswrapper` elements. + + QWeb template does not allow parsing namespaced elements + without declaring namespaces on the root element. + + Hence, by default you cannot define smaller re-usable templates + if they have namespaced elements. + + The trick is to wrap your reusable template with `nswrapper` element + which holds the namespace for that particular sub template. + For instance: + + + + + + Then this method is going to purge these unwanted elements from the result. + """ + if not (xml_content and xml_content.strip()): + return xml_content + root = etree.XML(xml_content) + # deeper elements come after, keep the root element at the end (if any) + for nswrapper in reversed(root.xpath("//nswrapper")): + parent = nswrapper.getparent() + if parent is None: + return "".join( + etree.tostring(el, encoding="unicode") for el in nswrapper.getchildren() + ) + parent.extend(nswrapper.getchildren()) + parent.remove(nswrapper) + return etree.tostring(root, encoding="unicode") diff --git a/edi_gs1_oca/views/edi_backend.xml b/edi_gs1_oca/views/edi_backend.xml new file mode 100644 index 000000000..528ceb46a --- /dev/null +++ b/edi_gs1_oca/views/edi_backend.xml @@ -0,0 +1,23 @@ + + + + + edi.backend.form (in edi_gs1_oca) + edi.backend + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/views/res_partner.xml b/edi_gs1_oca/views/res_partner.xml new file mode 100644 index 000000000..a0656ba4a --- /dev/null +++ b/edi_gs1_oca/views/res_partner.xml @@ -0,0 +1,24 @@ + + + + + res.partner.form (in edi_gs1_oca) + res.partner + + + + + + + + + + + + + + + + + diff --git a/edi_gs1_oca/views/uom_uom.xml b/edi_gs1_oca/views/uom_uom.xml new file mode 100644 index 000000000..460b4fd1b --- /dev/null +++ b/edi_gs1_oca/views/uom_uom.xml @@ -0,0 +1,16 @@ + + + + + uom.uom.form (in edi_gs1_oca) + uom.uom + + + + + + + + + diff --git a/edi_gs1_oca/xmler.py b/edi_gs1_oca/xmler.py new file mode 100644 index 000000000..24888ec63 --- /dev/null +++ b/edi_gs1_oca/xmler.py @@ -0,0 +1,134 @@ +""" +Converts a Python dictionary to a valid XML string + +https://github.com/watzon/xmler Slightly modified and adapted for py3. + +TODO: project seems dead but it's very small and shall be easy to maintain. +Decide if we want to keep it here or contribute back. +""" + +from xml.dom import minidom +from xml.etree.ElementTree import Element, tostring + +__version__ = "0.2.0" +version = __version__ + + +def dict2xml(input_dict, encoding="utf-8", pretty=False): + """Converts a python dictionary into a valid XML string + + Args: + - encoding specifies the encoding to be included in the encoding + segment. If set to False no encoding segment will be displayed. + - customRoot defines the tag to wrap the returned output. Can be + a string, dictionary, or False if no custom root is to be used. + + Returns: + A XML formatted string representing the dictionary. + + Examples: + ``` + dic = { + "Envelope": { + "@ns": "soapenv", + "@attrs": { + "xmlns:soapenv": "http://schemas.xmlsoap.org/soap/envelope/", + "xmlns:urn": "urn:partner.soap.sforce.com" + }, + "Header": { + "@ns": "soapenv", + "SessionHeader": { + "@ns": "urn", + "sessionId": { + "@ns": "urn", + "@value": "00D36000000b28L!ARsAQMtHo4XD71VYRxoz" + } + } + }, + "Body": { + "@ns": "soapenv", + "query": { + "@ns": "urn", + "queryString": { + "@ns": "urn", + "@value": "SELECT Id, Name FROM Account LIMIT 2" + } + } + } + } + } + + xml = xml2dict(dict, customRoot=False) + print(xml) + ``` + + output: + ``` + + + + + {0} + + + + ``` + """ + + xml_string = tostring(parse(input_dict, pretty=pretty), encoding=encoding) + + if pretty: + xml_pretty_string = minidom.parseString(xml_string) + return xml_pretty_string.toprettyxml().decode(encoding) + else: + return xml_string.decode(encoding) + + +def parse(input_dict, parent=None, pretty=False): + parent = parent or {} + for key, value in input_dict.items(): + if isinstance(value, (float, int)): + # Enfoce strings here + value = str(value) + + parent["name"] = key + parent["value"] = value + + if isinstance(value, dict): + if "@ns" in value: + parent["namespace"] = value.pop("@ns", None) + + if "@attrs" in value: + parent["attributes"] = value.pop("@attrs", None) + + if "@name" in value: + parent["name"] = value.pop("@name", None) + + if "@value" in value: + val = value["@value"] + if isinstance(val, (float, int)): + # Enfoce strings here + val = str(val) + parent["value"] = value = val + + if "namespace" in parent: + parent["name"] = "{}:{}".format(parent["namespace"], parent["name"]) + + if "attributes" in parent: + element = Element(parent["name"], parent["attributes"]) + else: + element = Element(parent["name"]) + + if isinstance(parent["value"], dict): + for child_key, child_value in parent["value"].items(): + element.append(parse({child_key: child_value}, parent={})) + + elif isinstance(parent["value"], (list, set, tuple)): + for child in parent["value"]: + element.append(parse(child, parent={})) + + else: + element.text = parent["value"] + + return element diff --git a/requirements.txt b/requirements.txt index 4ec60a70e..d950e810d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ # generated from manifests external_dependencies PyYAML openupgradelib +xmlschema +xmlunittest