├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── connector_easypost ├── README.rst ├── __init__.py ├── __manifest__.py ├── components │ ├── __init__.py │ ├── backend_adapter.py │ ├── binder.py │ ├── core.py │ ├── exporter.py │ ├── importer.py │ ├── listener.py │ └── mapper.py ├── data │ ├── delivery_carrier_data.xml │ ├── product_packaging_data.xml │ └── res_partner_data.xml ├── demo │ ├── product_product_demo.xml │ ├── res_company_demo.xml │ └── res_partner_demo.xml ├── models │ ├── __init__.py │ ├── address │ │ ├── __init__.py │ │ ├── common.py │ │ ├── exporter.py │ │ └── importer.py │ ├── address_validate │ │ ├── __init__.py │ │ └── common.py │ ├── delivery_carrier │ │ ├── __init__.py │ │ └── common.py │ ├── easypost_backend │ │ ├── __init__.py │ │ └── common.py │ ├── easypost_binding │ │ ├── __init__.py │ │ └── common.py │ ├── parcel │ │ ├── __init__.py │ │ ├── common.py │ │ ├── exporter.py │ │ └── importer.py │ ├── product_packaging │ │ ├── __init__.py │ │ └── common.py │ ├── rate │ │ ├── __init__.py │ │ ├── common.py │ │ └── importer.py │ ├── res_company │ │ ├── __init__.py │ │ └── common.py │ ├── res_partner │ │ ├── __init__.py │ │ └── common.py │ ├── sale │ │ ├── __init__.py │ │ ├── common.py │ │ ├── exporter.py │ │ └── importer.py │ ├── sale_rate │ │ ├── __init__.py │ │ ├── common.py │ │ └── importer.py │ ├── shipment │ │ ├── __init__.py │ │ ├── common.py │ │ ├── exporter.py │ │ └── importer.py │ └── shipping_label │ │ ├── __init__.py │ │ ├── common.py │ │ └── importer.py ├── security │ └── ir.model.access.csv ├── static │ └── description │ │ ├── icon.png │ │ └── icon.svg ├── tests │ ├── __init__.py │ ├── common.py │ ├── fixtures │ │ └── cassettes │ │ │ ├── test_backend_easypost_get_address.yaml │ │ │ ├── test_common_carrier_cancel_shipment.yaml │ │ │ ├── test_common_carrier_get_shipping_label_for_rate.yaml │ │ │ ├── test_common_carrier_get_shipping_price_from_so.yaml │ │ │ ├── test_common_carrier_get_tracking_link.yaml │ │ │ ├── test_common_carrier_send_shipping.yaml │ │ │ ├── test_export_parcel_predefined_package.yaml │ │ │ ├── test_export_parcel_shipping_weight.yaml │ │ │ ├── test_export_parcel_total_weight.yaml │ │ │ ├── test_export_sale_imports_rates.yaml │ │ │ ├── test_export_shipment_basic.yaml │ │ │ ├── test_export_shipment_imports_rates.yaml │ │ │ ├── test_import_rate_partner.yaml │ │ │ ├── test_import_rate_service_existing.yaml │ │ │ ├── test_import_rate_service_new.yaml │ │ │ ├── test_rate_buy.yaml │ │ │ └── test_rate_cancel.yaml │ ├── test_common_delivery_carrier.py │ ├── test_common_easypost_backend.py │ ├── test_common_rate.py │ ├── test_common_res_company.py │ ├── test_export_parcel.py │ ├── test_export_sale.py │ ├── test_export_shipment.py │ ├── test_import_address.py │ └── test_import_rate.py └── views │ ├── connector_menu.xml │ ├── delivery_carrier_view.xml │ └── easypost_backend_view.xml ├── connector_easypost_tracker ├── README.rst ├── __init__.py ├── __manifest__.py ├── controllers │ ├── __init__.py │ └── main.py ├── models │ ├── __init__.py │ ├── stock_picking_tracking_event.py │ ├── stock_picking_tracking_group.py │ └── stock_picking_tracking_location.py ├── static │ └── description │ │ ├── screenshot_1.png │ │ └── screenshot_2.png ├── tests │ ├── __init__.py │ ├── common.py │ ├── test_stock_picking_tracking_event.py │ ├── test_stock_picking_tracking_group.py │ └── test_stock_picking_tracking_location.py └── unit │ ├── __init__.py │ └── binder.py ├── oca_dependencies.txt └── requirements.txt /.coveragerc: -------------------------------------------------------------------------------- 1 | # Config file .coveragerc 2 | # adapt the include for your project 3 | 4 | [report] 5 | include = 6 | */laslabs/odoo-connector-easypost/* 7 | 8 | omit = 9 | */tests/* 10 | *__init__.py 11 | 12 | # Regexes for lines to exclude from consideration 13 | exclude_lines = 14 | # Have to re-enable the standard pragma 15 | pragma: no cover 16 | 17 | # Don't complain about null context checking 18 | if context is None: 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Virtual Env 2 | _venv/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | bin/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | *.eggs 28 | 29 | # Installer logs 30 | pip-log.txt 31 | pip-delete-this-directory.txt 32 | 33 | # Unit test / coverage reports 34 | htmlcov/ 35 | .tox/ 36 | .coverage 37 | .cache 38 | nosetests.xml 39 | coverage.xml 40 | _results/ 41 | 42 | # Translations 43 | *.mo 44 | *.pot 45 | 46 | # Pycharm 47 | .idea 48 | 49 | # Django stuff: 50 | *.log 51 | 52 | # Mr Developer 53 | .mr.developer.cfg 54 | .project 55 | .pydevproject 56 | 57 | # Rope 58 | .ropeproject 59 | 60 | # Sphinx documentation 61 | docs/_build/ 62 | 63 | # Backup files 64 | *~ 65 | *.swp 66 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | cache: 4 | apt: true 5 | directories: 6 | - $HOME/.cache/pip 7 | 8 | python: 9 | - "2.7" 10 | 11 | addons: 12 | apt: 13 | packages: 14 | - expect-dev # provides unbuffer utility 15 | - python-lxml # because pip installation is slow 16 | - python-simplejson 17 | - python-serial 18 | - python-yaml 19 | 20 | env: 21 | global: 22 | - VERSION="10.0" TESTS="0" LINT_CHECK="0" TRANSIFEX="0" 23 | 24 | matrix: 25 | - LINT_CHECK="1" 26 | - TESTS="1" ODOO_REPO="odoo/odoo" 27 | - TESTS="1" ODOO_REPO="OCA/OCB" 28 | 29 | virtualenv: 30 | system_site_packages: true 31 | 32 | install: 33 | - git clone --depth=1 https://github.com/OCA/maintainer-quality-tools.git ${HOME}/maintainer-quality-tools 34 | - export PATH=${HOME}/maintainer-quality-tools/travis:${PATH} 35 | - travis_install_nightly 36 | 37 | script: 38 | - travis_run_tests 39 | 40 | after_success: 41 | - travis_after_tests_success 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/laslabs/odoo-connector-easypost.svg?branch=9.0)](https://travis-ci.org/laslabs/odoo-connector-easypost?branch=9.0) 2 | [![Coveralls Status](https://coveralls.io/repos/laslabs/odoo-connector-easypost/badge.png?branch=9.0)](https://coveralls.io/r/laslabs/odoo-connector-easypost?branch=9.0) 3 | [![Codecov Status](https://codecov.io/gh/laslabs/odoo-connector-easypost/branch/9.0/graph/badge.svg)](https://codecov.io/gh/laslabs/odoo-connector-easypost) 4 | 5 | Odoo EasyPost Connector 6 | ======================= 7 | 8 | This project provides an EasyPost connection in Odoo, allowing for features such as: 9 | 10 | - Rate Quotes 11 | - Purchase Shipping Labels 12 | - Address Verification 13 | - EasyPost WebHook Tracking Event handling 14 | 15 | 16 | [//]: # (addons) 17 | Available addons 18 | ---------------- 19 | addon | version | summary 20 | --- | --- | --- 21 | [connector_easypost](connector_easypost/) | 10.0.1.0.0 | EasyPost connector core 22 | [connector_easypost_tracker](connector_easypost/) | 10.0.1.0.0 | EasyPost connector tracking WebHooks module 23 | 24 | 25 | Unported addons 26 | --------------- 27 | addon | version | summary 28 | --- | --- | --- 29 | 30 | 31 | [//]: # (end addons) 32 | 33 | Credits 34 | ======= 35 | 36 | Contributors 37 | ------------ 38 | 39 | * Dave Lasley 40 | * Ted Salmon 41 | 42 | Maintainer 43 | ---------- 44 | 45 | This module is maintained by [LasLabs Inc.](https://laslabs.com) 46 | 47 | * https://github.com/laslabs/odoo-connector-easypost/ 48 | -------------------------------------------------------------------------------- /connector_easypost/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg 2 | :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html 3 | :alt: License: AGPL-3 4 | 5 | ================== 6 | EasyPost Connector 7 | ================== 8 | 9 | This module provides EasyPost connector functionality. 10 | 11 | 12 | Installation 13 | ============ 14 | 15 | To install this module, you need to: 16 | 17 | * Install Python dependencies - ``pip install easypost`` 18 | * Look at ``oca_dependencies.txt`` in the root of this repo. Modules from 19 | these repos and branches are required for this module. The specific modules 20 | are listed in ``__manifest__.py``. See `here `_ for 22 | more information regarding the syntax of the ``oca_dependencies.txt`` file. 23 | * Install Easypost Connector module 24 | 25 | Configuration 26 | ============= 27 | 28 | To configure this module, you need to: 29 | 30 | * Go to ``Connectors => [EasyPost] Backends`` 31 | * Configure one EasyPost backend per company you would like to use it with 32 | * Restart Odoo (requirement of any new connector to set proper DB triggers) 33 | 34 | If using multiple stock locations, you must make sure to assign the owner of 35 | all warehouse stock locations (``warehouse.lot_stock_id``) in order for the 36 | correct outbound address to be used. 37 | 38 | Note that the ``weight`` and ``volume`` fields in your products must be accurrate. 39 | If this is not the case, shipping estimations will be incorrect - particularly during 40 | the sale phase. 41 | 42 | Usage 43 | ===== 44 | 45 | Predefined Packages 46 | ------------------- 47 | 48 | * Predefined packages will automatically be added to Packaging options upon 49 | module install 50 | 51 | Address Verification 52 | -------------------- 53 | 54 | * Navigate to any partner 55 | * Click the ``More`` or ``Actions`` menu (depending on Odoo version) 56 | * Click ``Validate Address`` to trigger the address validation wizard 57 | 58 | Rate Purchases 59 | --------------- 60 | 61 | * Put products into a package 62 | * Assign a packaging template to that package 63 | * Click the ``Additional Info`` tab under a Stock Picking to view the Rates. 64 | * Click the green check button to purchase the rate. 65 | 66 | Note that you can only purchase a rate after you have moved the picking out of 67 | draft status. 68 | 69 | Known Issues / Roadmap 70 | ====================== 71 | 72 | * Handle validation errors from addresses 73 | * Some duplicate calls to EasyPost (Address, Shipment) - seems to be just in 74 | the tests though 75 | * Add a default EasyPost connection to span all companies 76 | * Mass address verification 77 | * Label import operates in Shipment context, due to needing selected rate info 78 | not within PostageLabel 79 | * Only USPS service types are included by default. Everything else is created 80 | the first time rates are gathered. 81 | 82 | Bug Tracker 83 | =========== 84 | 85 | Bugs are tracked on `GitHub Issues 86 | `_. In case of trouble, please 87 | check there if your issue has already been reported. If you spotted it first, 88 | please help us smash it by providing a detailed and welcomed feedback. 89 | 90 | 91 | Credits 92 | ======= 93 | 94 | Images 95 | ------ 96 | 97 | * LasLabs: `Icon `_. 98 | 99 | Contributors 100 | ------------ 101 | 102 | * Dave Lasley 103 | * Ted Salmon 104 | 105 | Maintainer 106 | ---------- 107 | 108 | .. image:: https://laslabs.com/logo.png 109 | :alt: LasLabs Inc. 110 | :target: https://laslabs.com 111 | 112 | This module is maintained by LasLabs Inc. 113 | -------------------------------------------------------------------------------- /connector_easypost/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import components 6 | from . import models 7 | -------------------------------------------------------------------------------- /connector_easypost/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | # pylint: disable=C8101 6 | { 7 | 'name': 'EasyPost Connector', 8 | 'version': '10.0.1.0.1', 9 | 'category': 'Connector, Delivery, Stock', 10 | 'author': "LasLabs", 11 | 'license': 'AGPL-3', 12 | 'website': 'https://laslabs.com', 13 | 'depends': [ 14 | 'base_delivery_carrier_label', 15 | 'base_partner_validate_address', 16 | 'connector', 17 | 'delivery_package_wizard', 18 | 'l10n_us_product', 19 | 'l10n_us_uom_profile', 20 | 'sale_delivery_rate', 21 | 'stock_package_info', 22 | ], 23 | "external_dependencies": { 24 | "python": [ 25 | 'easypost', 26 | ], 27 | }, 28 | 'data': [ 29 | 'data/product_packaging_data.xml', 30 | 'data/res_partner_data.xml', 31 | # Carrier depends on partner 32 | 'data/delivery_carrier_data.xml', 33 | 'views/delivery_carrier_view.xml', 34 | 'views/easypost_backend_view.xml', 35 | 'views/connector_menu.xml', 36 | 'security/ir.model.access.csv', 37 | ], 38 | 'demo': [ 39 | 'demo/product_product_demo.xml', 40 | 'demo/res_company_demo.xml', 41 | 'demo/res_partner_demo.xml', 42 | ], 43 | 'installable': True, 44 | 'application': False, 45 | } 46 | -------------------------------------------------------------------------------- /connector_easypost/components/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import core 6 | 7 | from . import backend_adapter 8 | from . import binder 9 | 10 | from . import listener 11 | 12 | from . import importer 13 | from . import exporter 14 | 15 | from . import mapper 16 | -------------------------------------------------------------------------------- /connector_easypost/components/backend_adapter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import logging 6 | 7 | from odoo.addons.component.core import AbstractComponent 8 | 9 | 10 | _logger = logging.getLogger(__name__) 11 | 12 | try: 13 | import easypost 14 | except ImportError: 15 | _logger.warning('Could not import module dependency `easypost`') 16 | 17 | 18 | class EasypostAdapter(AbstractComponent): 19 | """ External Records Adapter for Easypost """ 20 | 21 | _name = 'easypost.adapter' 22 | _inherit = ['base.backend.adapter', 'base.easypost.connector'] 23 | _usage = 'backend.adapter' 24 | 25 | def __init__(self, connector_env): 26 | """ Ready the DB adapter 27 | :param connector_env: current environment (backend, session, ...) 28 | :type connector_env: :class:`connector.connector.ConnectorEnvironment` 29 | """ 30 | super(EasypostAdapter, self).__init__(connector_env) 31 | backend = self.backend_record 32 | self.easypost = easypost 33 | self.easypost.api_key = backend.api_key 34 | 35 | def _get_ep_model(self): 36 | """ Get the correct model object by name from Easypost lib 37 | """ 38 | name = self.env[self.work.model_name]._easypost_model 39 | return getattr(self.easypost, name) 40 | 41 | # pylint: disable=W8106 42 | def read(self, _id): 43 | """ Gets record by id and returns the object 44 | :param _id: Id of record to get from Db 45 | :type _id: int 46 | :return: EasyPost record for model 47 | """ 48 | _logger.debug('Reading ID %s', _id) 49 | return self._get_ep_model().retrieve(_id) 50 | 51 | # pylint: disable=W8106 52 | def create(self, data): 53 | """ Wrapper to create a record on the external system 54 | :param data: Data to create record with 55 | :type data: dict 56 | """ 57 | _logger.debug('Creating w/ %s', data) 58 | return self._get_ep_model().create(**data) 59 | 60 | def update(self, _id, data): 61 | """ Wrapper to update a mutable record on external system 62 | :param _id: Id of record to get from Db 63 | :type _id: int 64 | :param data: Data to create record with 65 | :type data: dict 66 | """ 67 | _logger.debug('Updating w/ %s', data) 68 | record = self.read(_id) 69 | if not hasattr(record, 'save'): 70 | record = self._get_ep_model().create(**data) 71 | return record 72 | for key, val in data.iteritems(): 73 | setattr(record, key, val) 74 | record.save() 75 | return record 76 | -------------------------------------------------------------------------------- /connector_easypost/components/binder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | 6 | import odoo 7 | from odoo.addons.component.core import Component 8 | 9 | 10 | class EasypostModelBinder(Component): 11 | """ Bindings are done directly on the binding model. 12 | Binding models are models called ``easypost.{normal_model}``, 13 | like ``easypost.res.partner`` or ``easypost.product.packaging``. 14 | They are ``_inherits`` of the normal models and contains 15 | the Easypost ID, the ID of the Easypost Backend and the additional 16 | fields belonging to the Easypost instance. 17 | """ 18 | 19 | _name = 'easypost.binder' 20 | _inherit = ['base.binder', 'base.easypost.connector'] 21 | _apply_on = [ 22 | 'easypost.address', 23 | 'easypost.parcel', 24 | 'easypost.shipment', 25 | 'easypost.rate', 26 | 'easypost.sale', 27 | 'easypost.sale.rate', 28 | 'easypost.shipping.label', 29 | ] 30 | 31 | def to_odoo(self, external_id, unwrap=True, browse=False): 32 | """ Give the Odoo ID for an external ID 33 | :param external_id: external ID for which we want the Odoo ID 34 | :param unwrap: if True, returns the normal record (the one 35 | inherits'ed), else return the binding record 36 | :param browse: if True, returns a recordset 37 | :return: a recordset of one record, depending on the value of unwrap, 38 | or an empty recordset if no binding is found 39 | :rtype: recordset 40 | """ 41 | bindings = self.model.with_context(active_test=False).search([ 42 | ('external_id', '=', str(external_id)), 43 | ('backend_id', '=', self.backend_record.id) 44 | ]) 45 | if not bindings: 46 | return self.model.browse() if browse else None 47 | assert len(bindings) == 1, "Several records found: %s" % (bindings,) 48 | if unwrap: 49 | return bindings.odoo_id if browse else bindings.odoo_id.id 50 | else: 51 | return bindings if browse else bindings.id 52 | 53 | def to_backend(self, record_id, wrap=True): 54 | """ Give the external ID for an Odoo ID 55 | :param record_id: Odoo ID for which we want the external id 56 | or a recordset with one record 57 | :param wrap: if False, record_id is the ID of the binding, 58 | if True, record_id is the ID of the normal record, the 59 | method will search the corresponding binding and returns 60 | the backend id of the binding 61 | :return: backend identifier of the record 62 | """ 63 | record = self.model.browse() 64 | if isinstance(record_id, odoo.models.BaseModel): 65 | record_id.ensure_one() 66 | record = record_id 67 | record_id = record_id.id 68 | if wrap: 69 | binding = self.model.with_context(active_test=False).search([ 70 | ('odoo_id', '=', record_id), 71 | ('backend_id', '=', self.backend_record.id), 72 | ]) 73 | if binding: 74 | binding.ensure_one() 75 | return binding.external_id 76 | else: 77 | return None 78 | if not record: 79 | record = self.model.browse(record_id) 80 | assert record 81 | return record.external_id 82 | 83 | def bind(self, external_id, binding_id): 84 | """ Create the link between an external ID and an Odoo ID and 85 | update the last synchronization date. 86 | :param external_id: External ID to bind 87 | :param binding_id: Odoo ID to bind 88 | :type binding_id: int 89 | """ 90 | if hasattr(external_id, 'id'): 91 | external_id = external_id.id 92 | # the external ID can be 0 on Easypost! Prevent False values 93 | # like False, None, or "", but not 0. 94 | assert (external_id or external_id == 0) and binding_id, ( 95 | "external_id or binding_id missing, " 96 | "got: %s, %s" % (external_id, binding_id) 97 | ) 98 | # avoid to trigger the export when we modify the `external_id` 99 | if not isinstance(binding_id, odoo.models.BaseModel): 100 | binding_id = self.model.browse(binding_id) 101 | binding_id.with_context(connector_no_export=True).write({ 102 | 'external_id': str(external_id), 103 | 'sync_date': odoo.fields.Datetime.now(), 104 | }) 105 | return binding_id 106 | 107 | def unwrap_binding(self, binding_id, browse=False): 108 | """ For a binding record, gives the normal record. 109 | Example: when called with a ``easypost.address`` id, 110 | it will return the corresponding ``product.product`` id. 111 | :param browse: when True, returns a browse_record instance 112 | rather than an ID 113 | """ 114 | if isinstance(binding_id, odoo.models.BaseModel): 115 | binding = binding_id 116 | else: 117 | binding = self.model.browse(binding_id) 118 | odoo_record = binding.odoo_id 119 | if browse: 120 | return odoo_record 121 | return odoo_record.id 122 | 123 | def unwrap_model(self): 124 | """ For a binding model, gives the name of the normal model. 125 | Example: when called on a binder for ``easypost.address``, 126 | it will return ``product.product``. 127 | This binder assumes that the normal model lays in ``odoo_id`` since 128 | this is the field we use in the ``_inherits`` bindings. 129 | """ 130 | try: 131 | column = self.model._fields['odoo_id'] 132 | except KeyError: 133 | raise ValueError('Cannot unwrap model %s, because it has ' 134 | 'no odoo_id field' % self.model._name) 135 | return column.comodel_name 136 | -------------------------------------------------------------------------------- /connector_easypost/components/core.py: -------------------------------------------------------------------------------- 1 | 2 | # -*- coding: utf-8 -*- 3 | # Copyright 2017 LasLabs Inc. 4 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 5 | 6 | from odoo.addons.component.core import AbstractComponent 7 | 8 | 9 | class BaseEasyPostConnectorComponent(AbstractComponent): 10 | _name = 'base.easypost.connector' 11 | _inherit = 'base.connector' 12 | _collection = 'easypost.backend' 13 | -------------------------------------------------------------------------------- /connector_easypost/components/importer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | 6 | """ 7 | Importers for Easypost. 8 | An import can be skipped if the last sync date is more recent than 9 | the last update in Easypost. 10 | They should call the ``bind`` method if the binder even if the records 11 | are already bound, to update the last sync date. 12 | """ 13 | 14 | 15 | import logging 16 | import dateutil.parser 17 | import pytz 18 | from hashlib import md5 19 | from odoo import fields, _ 20 | from odoo.addons.component.core import AbstractComponent 21 | from odoo.addons.queue_job.exception import NothingToDoJob 22 | 23 | 24 | _logger = logging.getLogger(__name__) 25 | 26 | 27 | class EasypostImporter(AbstractComponent): 28 | """ Base importer for Easypost """ 29 | 30 | _name = 'easypost.importer' 31 | _inherit = ['base.importer', 'base.easypost.connector'] 32 | _usage = 'record.importer' 33 | 34 | def __init__(self, work_context): 35 | super(EasypostImporter, self).__init__(work_context) 36 | self.external_id = None 37 | self.easypost_record = None 38 | 39 | def _get_easypost_data(self): 40 | """ Return the raw Easypost data for ``self.external_id`` """ 41 | _logger.debug('Getting EasyPost data for %s', self.external_id) 42 | return self.backend_adapter.read(self.external_id) 43 | 44 | def _before_import(self): 45 | """ Hook called before the import, when we have the Easypost 46 | data""" 47 | 48 | def _is_uptodate(self, binding): 49 | """Return True if the import should be skipped because 50 | it is already up-to-date in Odoo""" 51 | assert self.easypost_record 52 | if not getattr(self.easypost_record, 'updated_at', False): 53 | return # no update date on Easypost, always import it. 54 | if not binding: 55 | return # it does not exist so it should not be skipped 56 | sync = binding.sync_date 57 | if not sync: 58 | return 59 | sync_date = fields.Datetime.from_string(sync) 60 | sync_date = pytz.utc.localize(sync_date) 61 | easypost_date = self.easypost_record.updated_at 62 | easypost_date = dateutil.parser.parse(easypost_date) 63 | # if the last synchronization date is greater than the last 64 | # update in easypost, we skip the import. 65 | # Important: at the beginning of the exporters flows, we have to 66 | # check if the easypost_date is more recent than the sync_date 67 | # and if so, schedule a new import. If we don't do that, we'll 68 | # miss changes done in Easypost 69 | return easypost_date < sync_date 70 | 71 | def _import_dependency(self, external_id, binding_model, 72 | importer=None, always=False): 73 | """Import a dependency. 74 | Args: 75 | external_id (int): ID of the external record to import. 76 | binding_model (basestring): Name of the model to bind to. 77 | importer (AbstractComponent, optional): Importer to use. 78 | always (bool, optional): Always update the record, regardless 79 | of if it exists in Odoo already. Note that if the record 80 | hasn't changed, it still may be skipped. 81 | """ 82 | if not external_id: 83 | return 84 | binder = self.binder_for(binding_model) 85 | if always or not binder.to_internal(external_id): 86 | if importer is None: 87 | importer = self.component(usage='record.importer', 88 | model_name=binding_model) 89 | try: 90 | importer.run(external_id) 91 | except NothingToDoJob: 92 | _logger.info( 93 | 'Dependency import of %s(%s) has been ignored.', 94 | binding_model._name, external_id 95 | ) 96 | 97 | def _import_dependencies(self): 98 | """ Import the dependencies for the record 99 | Import of dependencies can be done manually or by calling 100 | :meth:`_import_dependency` for each dependency. 101 | """ 102 | return 103 | 104 | def _map_data(self): 105 | """ Returns an instance of 106 | :py:class:`~odoo.addons.connector.unit.mapper.MapRecord` 107 | """ 108 | return self.mapper.map_record(self.easypost_record) 109 | 110 | def _validate_data(self, data): 111 | """ Check if the values to import are correct 112 | Pro-actively check before the ``_create`` or 113 | ``_update`` if some fields are missing or invalid. 114 | Raise `InvalidDataError` 115 | """ 116 | return 117 | 118 | def _must_skip(self): 119 | """ Hook called right after we read the data from the backend. 120 | If the method returns a message giving a reason for the 121 | skipping, the import will be interrupted and the message 122 | recorded in the job (if the import is called directly by the 123 | job, not by dependencies). 124 | If it returns None, the import will continue normally. 125 | :returns: None | str | unicode 126 | """ 127 | return 128 | 129 | def _get_binding(self): 130 | return self.binder.to_internal(self.external_id) 131 | 132 | def _generate_external_id(self): 133 | """ Some objects in EasyPost have no ID. Return one based on 134 | the unique data of the object 135 | :return str: The MD5 Hex Digest of the record 136 | """ 137 | obj_hash = md5() 138 | for attr in self._hashable_attrs: 139 | obj_hash.update(getattr(self.easypost_record, attr, '') or '') 140 | return '%s_%s' % (self._id_prefix, obj_hash.hexdigest()) 141 | 142 | def _create(self, data): 143 | """ Create the Odoo record """ 144 | # special check on data before import 145 | self._validate_data(data) 146 | model = self.model.with_context(connector_no_export=True) 147 | _logger.debug('Creating with %s', data) 148 | binding = model.create(data) 149 | _logger.debug( 150 | '%d created from easypost %s', 151 | binding, 152 | self.external_id) 153 | return binding 154 | 155 | def _update_data(self, map_record, **kwargs): 156 | return map_record.values(**kwargs) 157 | 158 | def _update(self, binding, data): 159 | """ Update an Odoo record """ 160 | # special check on data before import 161 | self._validate_data(data) 162 | binding.with_context(connector_no_export=True).write(data) 163 | _logger.debug( 164 | '%d updated from easypost %s', 165 | binding, 166 | self.external_id) 167 | return 168 | 169 | def _update_export(self, bind_record, easypost_record): 170 | """ Update record using data received from export """ 171 | self.easypost_record = easypost_record 172 | map_record = self._map_data() 173 | record = self._update_data(map_record) 174 | self._update(bind_record, record) 175 | 176 | def _after_import(self, binding): 177 | """ Hook called at the end of the import """ 178 | return 179 | 180 | def _create_data(self, map_record, **kwargs): 181 | return map_record.values(for_create=True, **kwargs) 182 | 183 | def run(self, external_id=None, force=False, external_record=None): 184 | """ Run the synchronization. 185 | 186 | Args: 187 | external_id (int | easypost.BaseModel): identifier of the 188 | record in HelpScout, or a HelpScout record. 189 | force (bool, optional): Set to ``True`` to force the sync. 190 | external_record (easypost.models.BaseModel): Record from 191 | HelpScout. Defining this will force the import of this 192 | record, instead of the search of the remote. 193 | 194 | Returns: 195 | str: Canonical status message. 196 | """ 197 | 198 | if external_record: 199 | self.easypost_record = external_record 200 | 201 | assert external_id or self.easypost_record 202 | 203 | self.external_id = external_id 204 | 205 | if not self.easypost_record: 206 | self.easypost_record = self._get_easypost_data() 207 | else: 208 | if not external_id: 209 | self.external_id = self._generate_external_id() 210 | if not getattr(self.easypost_record, 'id', None): 211 | self.easypost_record.id = self.external_id 212 | 213 | _logger.debug('self.easypost_record - %s', self.easypost_record) 214 | lock_name = 'import({}, {}, {}, {})'.format( 215 | self.backend_record._name, 216 | self.backend_record.id, 217 | self.model._name, 218 | external_id, 219 | ) 220 | # Keep a lock on this import until the transaction is committed 221 | self.advisory_lock_or_retry(lock_name) 222 | 223 | skip = self._must_skip() 224 | if skip: 225 | return skip 226 | 227 | binding = self._get_binding() 228 | 229 | if not force and self._is_uptodate(binding): 230 | return _('Already up-to-date.') 231 | self._before_import() 232 | 233 | # import the missing linked resources 234 | self._import_dependencies() 235 | 236 | map_record = self._map_data() 237 | _logger.debug('Mapped to %s', map_record) 238 | 239 | if binding: 240 | record = self._update_data(map_record) 241 | self._update(binding, record) 242 | else: 243 | record = self._create_data(map_record) 244 | binding = self._create(record) 245 | self.binder.bind(self.external_id, binding) 246 | 247 | self._after_import(binding) 248 | return binding 249 | 250 | 251 | class BatchImporter(AbstractComponent): 252 | """ The role of a BatchImporter is to search for a list of 253 | items to import, then it can either import them directly or delay 254 | the import of each item separately. 255 | """ 256 | 257 | _name = 'easypost.batch.importer' 258 | _inherit = ['base.importer', 'base.easypost.connector'] 259 | _usage = 'batch.importer' 260 | 261 | def run(self, filters=None): 262 | """ Run the synchronization """ 263 | record_ids = self.backend_adapter.search(filters) 264 | for record_id in record_ids: 265 | self._import_record(record_id) 266 | 267 | def _import_record(self, record_id): 268 | """ Import a record directly or delay the import of the record. 269 | Method to implement in sub-classes. 270 | """ 271 | raise NotImplementedError 272 | 273 | 274 | class DirectBatchImporter(AbstractComponent): 275 | """ Import the records directly, without delaying the jobs. """ 276 | _model_name = None 277 | 278 | _name = 'easypost.direct.batch.importer' 279 | _inherit = 'easypost.batch.importer' 280 | 281 | def _import_record(self, external_id, **kwargs): 282 | """Import the record directly.""" 283 | self.model.import_record(self.backend_record, external_id, **kwargs) 284 | 285 | 286 | class DelayedBatchImporter(AbstractComponent): 287 | """ Delay import of the records """ 288 | _model_name = None 289 | 290 | _name = 'easypost.delayed.batch.importer' 291 | _inherit = 'easypost.batch.importer' 292 | 293 | def _import_record(self, external_id, job_options=None, **kwargs): 294 | """Delay the record imports.""" 295 | delayed = self.model.with_delay(**job_options or {}) 296 | delayed.import_record(self.backend_record, external_id, **kwargs) 297 | -------------------------------------------------------------------------------- /connector_easypost/components/listener.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 4 | 5 | from odoo.addons.component.core import AbstractComponent, Component 6 | from odoo.addons.component_event import skip_if 7 | 8 | 9 | QUANT_DEPENDS = 'packaging_id' 10 | 11 | 12 | class EasyPostListener(AbstractComponent): 13 | """Generic event listener for EasyPost.""" 14 | _name = 'easypost.listener' 15 | _inherit = 'base.event.listener' 16 | 17 | def new_binding(self, record, force=False): 18 | record.easypost_bind_ids.ensure_bindings(record, force) 19 | 20 | def no_connector_export(self, record): 21 | return self.env.context.get('connector_no_export', False) 22 | 23 | def export_record(self, record, fields=None): 24 | record.with_delay().export_record(fields=fields) 25 | 26 | def delete_record(self, record): 27 | record.with_delay().export_delete_record() 28 | 29 | 30 | class EasyPostListenerOdooDelayed(AbstractComponent): 31 | """Generic event listener for Odoo models delayed export changes.""" 32 | _name = 'easypost.listener.odoo.delayed' 33 | _inherit = 'easypost.listener' 34 | 35 | @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) 36 | def on_record_create(self, record, fields=None): 37 | self.new_binding(record) 38 | self.export_record(record.easypost_bind_ids, fields) 39 | 40 | @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) 41 | def on_record_write(self, record, fields=None): 42 | if not record.easypost_bind_ids: 43 | return 44 | self.export_record(record.easypost_bind_ids, fields) 45 | 46 | 47 | class EasyPostListenerStockQuantPackage(Component): 48 | """Event listener for stock.quant.package""" 49 | _name = 'easypost.listener.stock.quant.package' 50 | _inherit = 'easypost.listener.odoo.delayed' 51 | _apply_on = 'stock.quant.package' 52 | 53 | @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) 54 | def on_record_create(self, record, fields=None): 55 | self.new_binding(record) 56 | self.on_record_write(record, fields=fields) 57 | 58 | @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) 59 | def on_record_write(self, record, fields=None): 60 | 61 | if not record[QUANT_DEPENDS]: 62 | # Not exporting because no package 63 | return 64 | 65 | super(EasyPostListenerStockQuantPackage, self).on_record_write(record) 66 | 67 | for bind in record.picking_ids.mapped('easypost_bind_ids'): 68 | self.export_record(bind) 69 | 70 | 71 | # class EasyPostListenerSaleOrder(Component): 72 | # """Event listener for sale.order""" 73 | # _name = 'easypost.listener.sale.order' 74 | # _inherit = 'easypost.listener' 75 | # _apply_on = 'sale.order' 76 | # 77 | # def is_easypost(self, record): 78 | # return record.carrier_id.delivery_type == 'easypost' 79 | # 80 | # def is_non_easypost(self, record): 81 | # return self.no_connector_export(record) or \ 82 | # not self.is_easypost(record) 83 | # 84 | # @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) 85 | # def on_record_create(self, record, fields=None): 86 | # self.new_binding(record) 87 | # if record.carrier_id.delivery_type == 'easypost': 88 | # self.on_record_write(record, fields=fields) 89 | # 90 | # @skip_if(lambda self, record, **kwargs: self.is_non_easypost(record)) 91 | # def on_record_write(self, record, fields=None): 92 | # record.easypost_bind_ids.export_record(fields=fields) 93 | 94 | 95 | # class EasyPostListenerStockPicking(Component): 96 | # """Event listener for stock.picking""" 97 | # _name = 'easypost.listener.stock.picking' 98 | # _inherit = 'easypost.listener' 99 | # _apply_on = 'stock.picking' 100 | # 101 | # @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) 102 | # def on_record_create(self, record, fields=None): 103 | # self.new_binding(record) 104 | # self.on_record_write(record, fields=fields) 105 | # 106 | # @skip_if(lambda self, record, **kwargs: self.no_connector_export(record)) 107 | # def on_record_write(self, record, fields=None): 108 | # self.export_record(record.easypost_bind_ids) 109 | -------------------------------------------------------------------------------- /connector_easypost/components/mapper.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import AbstractComponent 6 | from odoo.addons.connector.unit.mapper import ( 7 | mapping, 8 | only_create, 9 | ) 10 | 11 | 12 | def eval_false(field): 13 | """ A modifier intended to be used on the ``direct`` mappings. 14 | Convert "false" String to None 15 | Example:: 16 | direct = [(eval_false('source'), 'target')] 17 | :param field: name of the source field in the record 18 | """ 19 | def modifier(self, record, to_attr): 20 | value = getattr(record, field, None) 21 | if str(value).lower() == 'false': 22 | return None 23 | return value 24 | return modifier 25 | 26 | 27 | def inner_attr(attr, field): 28 | """ A modifier intended to be used on the ``direct`` mappings. 29 | Looks inside of an object for the field, using attr arg 30 | Example:: 31 | direct = [(inside_key('source', 'attr'), 'target')] 32 | :param attr: name of object attribute to look inside of 33 | :param field: name of the source field in the record 34 | """ 35 | def modifier(self, record, to_attr): 36 | value = None 37 | record_attr = getattr(record, attr, None) 38 | if record_attr is not None: 39 | value = getattr(record_attr, field, None) 40 | return value 41 | return modifier 42 | 43 | 44 | class EasypostImportMapper(AbstractComponent): 45 | 46 | _name = 'easypost.import.mapper' 47 | _inherit = ['base.easypost.connector', 'base.import.mapper'] 48 | _usage = 'import.mapper' 49 | 50 | @mapping 51 | def backend_id(self, record): 52 | return {'backend_id': self.backend_record.id} 53 | 54 | @mapping 55 | def external_id(self, record): 56 | return {'external_id': record.id} 57 | 58 | @mapping 59 | @only_create 60 | def odoo_id(self, record): 61 | """ Attempt to bind on an existing record 62 | EasyPost aggregates records upstream, this is to handle that 63 | """ 64 | binder = self.binder_for(record._name) 65 | odoo_id = binder.to_odoo(record.id) 66 | if odoo_id: 67 | return {'odoo_id': odoo_id} 68 | 69 | 70 | class EasypostExportMapper(AbstractComponent): 71 | 72 | _name = 'easypost.export.mapper' 73 | _inherit = ['base.easypost.connector', 'base.export.mapper'] 74 | _usage = 'export.mapper' 75 | 76 | @mapping 77 | def id(self, record): 78 | if record.external_id: 79 | return {'id': record.external_id} 80 | -------------------------------------------------------------------------------- /connector_easypost/data/delivery_carrier_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | USPS - First Class 11 | First 12 | 13 | easypost 14 | rate_and_ship 15 | 16 | 17 | service 18 | 19 | 20 | 21 | USPS - Priority Mail 22 | Priority 23 | 24 | easypost 25 | rate_and_ship 26 | 27 | 28 | service 29 | 30 | 31 | 32 | USPS - Express Mail 33 | Express 34 | 35 | easypost 36 | rate_and_ship 37 | 38 | 39 | service 40 | 41 | 42 | 43 | USPS - Parcel Select 44 | ParcelSelect 45 | 46 | easypost 47 | rate_and_ship 48 | 49 | 50 | service 51 | 52 | 53 | 54 | USPS - Library Mail 55 | LibraryMail 56 | 57 | easypost 58 | rate_and_ship 59 | 60 | 61 | service 62 | 63 | 64 | 65 | USPS - Media Mail 66 | MediaMail 67 | 68 | easypost 69 | rate_and_ship 70 | 71 | 72 | service 73 | 74 | 75 | 76 | USPS - First Class Mail (International) 77 | FirstClassMailInternational 78 | 79 | easypost 80 | rate_and_ship 81 | 82 | 83 | service 84 | 85 | 86 | 87 | USPS - First Class Package (International) 88 | FirstClassPackageInternationalService 89 | 90 | easypost 91 | rate_and_ship 92 | 93 | 94 | service 95 | 96 | 97 | 98 | USPS - Priority Mail (International) 99 | PriorityMailInternational 100 | 101 | easypost 102 | rate_and_ship 103 | 104 | 105 | service 106 | 107 | 108 | 109 | USPS - Express Mail International 110 | ExpressMailInternational 111 | 112 | easypost 113 | rate_and_ship 114 | 115 | 116 | service 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /connector_easypost/data/res_partner_data.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | USPS 10 | 11 | 12 | 13 | 14 | 15 | 16 | UPS 17 | 18 | 19 | 20 | 21 | 22 | 23 | FedEx 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /connector_easypost/demo/product_product_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 54 11 | 144 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /connector_easypost/demo/res_company_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 1725 SLOUGH AVE. 10 | SCRANTON 11 | 18540-0001 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /connector_easypost/demo/res_partner_demo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | The White House 10 | 1600 PENNSYLVANIA AVE NW 11 | WASHINGTON 12 | 20500-0003 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /connector_easypost/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import easypost_backend 6 | from . import easypost_binding 7 | 8 | from . import address 9 | from . import address_validate 10 | from . import delivery_carrier 11 | from . import parcel 12 | from . import product_packaging 13 | from . import rate 14 | from . import res_company 15 | from . import res_partner 16 | from . import shipping_label 17 | from . import shipment 18 | 19 | # Must be imported after shipment 20 | from . import sale 21 | from . import sale_rate 22 | -------------------------------------------------------------------------------- /connector_easypost/models/address/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | 7 | from . import exporter 8 | from . import importer 9 | -------------------------------------------------------------------------------- /connector_easypost/models/address/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import logging 6 | from odoo import models, fields, api 7 | 8 | from odoo.addons.component.core import Component 9 | 10 | 11 | _logger = logging.getLogger(__name__) 12 | 13 | 14 | class EasypostAddress(models.TransientModel): 15 | """ Binding Model for the Easypost Address 16 | 17 | TransientModel so that records are eventually deleted due to immutable 18 | EasyPost objects 19 | """ 20 | _name = 'easypost.address' 21 | _inherit = 'easypost.binding' 22 | _inherits = {'wizard.address.validate': 'odoo_id'} 23 | _description = 'Easypost Address' 24 | _easypost_model = 'Address' 25 | 26 | odoo_id = fields.Many2one( 27 | comodel_name='wizard.address.validate', 28 | string='Address Validator', 29 | required=True, 30 | ondelete='cascade', 31 | ) 32 | phone = fields.Char( 33 | related='odoo_id.partner_id.phone', 34 | readonly=True, 35 | ) 36 | email = fields.Char( 37 | related='odoo_id.partner_id.email', 38 | readonly=True, 39 | ) 40 | name = fields.Char( 41 | related='odoo_id.partner_id.name', 42 | readonly=True, 43 | ) 44 | company_name = fields.Char( 45 | related='odoo_id.partner_id.company_id.name', 46 | readonly=True, 47 | ) 48 | 49 | @api.model 50 | def _get_by_partner(self, partner_id): 51 | return self.search([ 52 | ('partner_id', '=', partner_id.id), 53 | ], 54 | limit=1, 55 | ) 56 | 57 | 58 | class WizardAddressValidate(models.TransientModel): 59 | """ Adds the ``one2many`` relation to the Easypost bindings 60 | (``easypost_bind_ids``) 61 | """ 62 | _inherit = 'wizard.address.validate' 63 | 64 | easypost_bind_ids = fields.One2many( 65 | comodel_name='easypost.address', 66 | inverse_name='odoo_id', 67 | string='Easypost Bindings', 68 | ) 69 | 70 | 71 | class EasypostAddressAdapter(Component): 72 | """ Backend Adapter for the Easypost EasypostAddress """ 73 | _name = 'easypost.address.adapter' 74 | _inherit = 'easypost.adapter' 75 | _apply_on = 'easypost.address' 76 | -------------------------------------------------------------------------------- /connector_easypost/models/address/exporter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import Component 6 | from odoo.addons.connector.components.mapper import changed_by, mapping 7 | 8 | 9 | class EasypostAddressExportMapper(Component): 10 | 11 | _name = 'easypost.export.mapper.address' 12 | _inherit = 'easypost.export.mapper' 13 | _apply_on = 'easypost.address' 14 | 15 | direct = [ 16 | ('name', 'name'), 17 | ('company_name', 'company'), 18 | ('street_original', 'street1'), 19 | ('street2_original', 'street2'), 20 | ('city_original', 'city'), 21 | ('zip_original', 'zip'), 22 | ('phone', 'phone'), 23 | ('email', 'email'), 24 | ] 25 | 26 | @mapping 27 | def verify(self, record): 28 | return {'verify': ['delivery']} 29 | 30 | @mapping 31 | @changed_by('state_id') 32 | def state(self, record): 33 | if record.state_id_original: 34 | return {'state': record.state_id_original.code} 35 | 36 | @mapping 37 | @changed_by('country_id') 38 | def country(self, record): 39 | if record.country_id_original: 40 | return {'country': record.country_id_original.code} 41 | 42 | 43 | class EasypostAddressExporter(Component): 44 | 45 | _name = 'easypost.address.record.exporter' 46 | _inherit = 'easypost.exporter' 47 | _apply_on = 'easypost.address' 48 | 49 | def _after_export(self): 50 | """ Immediate re-import """ 51 | self.binding_record.import_direct(self.binding_record.backend_id, 52 | self.easypost_record) 53 | -------------------------------------------------------------------------------- /connector_easypost/models/address/importer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import Component 6 | from odoo.addons.connector.components.mapper import mapping, only_create 7 | 8 | from ...components.mapper import eval_false 9 | 10 | 11 | class EasypostAddressImportMapper(Component): 12 | 13 | _name = 'easypost.import.mapper.address' 14 | _inherit = 'easypost.import.mapper' 15 | _apply_on = 'easypost.address' 16 | 17 | direct = [ 18 | (eval_false('street1'), 'street'), 19 | (eval_false('street2'), 'street2'), 20 | (eval_false('city'), 'city'), 21 | (eval_false('zip'), 'zip'), 22 | ] 23 | 24 | @mapping 25 | @only_create 26 | def odoo_id(self, record): 27 | """Handle multiple validations in a row.""" 28 | existing = self.env[self._apply_on].search([ 29 | ('backend_id', '=', self.backend_record.id), 30 | ('external_id', '=', record.id), 31 | ]) 32 | if existing: 33 | return {'odoo_id': existing.id} 34 | 35 | @mapping 36 | @only_create 37 | def partner_id(self, record): 38 | binder = self.binder_for(record._name) 39 | address_id = binder.to_odoo(record.id, browse=True) 40 | return {'partner_id': address_id.partner_id.id} 41 | 42 | @mapping 43 | def country_state_id(self, record): 44 | country_id = self.env['res.country'].search([ 45 | ('code', '=', record.country), 46 | ], 47 | limit=1, 48 | ) 49 | state_id = self.env['res.country.state'].search([ 50 | ('country_id', '=', country_id.id), 51 | ('code', '=', record.state), 52 | ], 53 | limit=1, 54 | ) 55 | return {'country_id': country_id.id, 56 | 'state_id': state_id.id, 57 | } 58 | 59 | @mapping 60 | def latitude_longitude(self, record): 61 | return { 62 | 'latitude': record.verifications.delivery.details.latitude, 63 | 'longitude': record.verifications.delivery.details.longitude, 64 | } 65 | 66 | @mapping 67 | def validation_messages(self, record): 68 | messages = [] 69 | for e in record.verifications.delivery.errors: 70 | messages.append(e.message) 71 | return {'validation_messages': ', '.join(messages)} 72 | 73 | @mapping 74 | def is_valid(self, record): 75 | return {'is_valid': record.verifications.delivery.success} 76 | 77 | 78 | class EasypostAddressImporter(Component): 79 | 80 | _name = 'easypost.importer.address' 81 | _inherit = 'easypost.importer' 82 | _apply_on = 'easypost.address' 83 | 84 | def _is_uptodate(self, binding): 85 | """Return False to always force import """ 86 | return False 87 | -------------------------------------------------------------------------------- /connector_easypost/models/address_validate/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | -------------------------------------------------------------------------------- /connector_easypost/models/address_validate/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import api 6 | from odoo import models 7 | 8 | 9 | class AddressValidate(models.Model): 10 | _inherit = 'address.validate' 11 | 12 | @api.model 13 | def _get_interface_types(self): 14 | res = super(AddressValidate, self)._get_interface_types() 15 | return res + [('easypost', 'EasyPost')] 16 | 17 | @api.multi 18 | def easypost_get_client(self): 19 | """Handled in the adapters.""" 20 | 21 | @api.multi 22 | def easypost_test_connection(self): 23 | """Handled in the adapters.""" 24 | 25 | @api.multi 26 | def easypost_get_address(self, api_client, partner): 27 | wizard = self.env['easypost.address'].create({ 28 | 'partner_id': partner.id, 29 | 'interface_id': self.id, 30 | }) 31 | wizard.export_record() 32 | return { 33 | 'street': wizard.street, 34 | 'street2': wizard.street2, 35 | 'city': wizard.city, 36 | 'zip': wizard.zip, 37 | 'state_id': wizard.state_id.id, 38 | 'country_id': wizard.country_id.id, 39 | 'validation_messages': wizard.validation_messages, 40 | 'latitude': wizard.latitude, 41 | 'longitude': wizard.longitude, 42 | 'is_valid': wizard.is_valid, 43 | } 44 | -------------------------------------------------------------------------------- /connector_easypost/models/delivery_carrier/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | -------------------------------------------------------------------------------- /connector_easypost/models/delivery_carrier/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import api, fields, models, _ 6 | 7 | 8 | class DeliveryCarrier(models.Model): 9 | _inherit = 'delivery.carrier' 10 | 11 | delivery_type = fields.Selection( 12 | selection_add=[ 13 | ('easypost', 'EasyPost'), 14 | ], 15 | ) 16 | easypost_service = fields.Selection( 17 | string="EasyPost Carrier & Service", 18 | selection=[ 19 | ('First', 'USPS - First Class'), 20 | ('Priority', 'USPS - Priority'), 21 | ('Express', 'USPS - Express'), 22 | ('ParcelSelect', 'USPS - Parcel Select'), 23 | ('LibraryMail', 'USPS - Library Mail'), 24 | ('MediaMail', 'USPS - Media Mail'), 25 | ('FirstClassMailInternational', 26 | 'USPS - First Class Mail International'), 27 | ('FirstClassPackageInternationalService', 28 | 'USPS - First Class Package International Service'), 29 | ('PriorityMailInternational', 30 | 'USPS - Priority Mail International'), 31 | ('ExpressMailInternational', 32 | 'USPS - Express Mail International'), 33 | ('Ground', 'UPS - Ground'), 34 | ('UPSStandard', 'UPS - UPS Standard'), 35 | ('UPSSaver', 'UPS - UPS Saver'), 36 | ('Express', 'UPS - Express'), 37 | ('ExpressPlus', 'UPS - Express Plus'), 38 | ('Expedited', 'UPS - Expedited'), 39 | ('NextDayAir', 'UPS - Next Day Air'), 40 | ('NextDayAirSaver', 'UPS - Next Day Air Saver'), 41 | ('NextDayAirEarlyAM', 'UPS - Next Day Air Early AM'), 42 | ('2ndDayAir', 'UPS - 2nd Day Air'), 43 | ('2ndDayAirAM', 'UPS - 2nd Day Air AM'), 44 | ('3DaySelect', 'UPS - 3-Day Select'), 45 | ('FEXEX_GROUND', 'FedEx - Ground'), 46 | ('FEDEX_2_DAY', 'FedEx - 2-Day'), 47 | ('FEDEX_2_DAY_AM', 'FedEx - 2-Day AM'), 48 | ('FEDEX_EXPRESS_SAVER', 'FedEx - Express Saver'), 49 | ('STANDARD_OVERNIGHT', 'FedEx - Standard Overnight'), 50 | ('FIRST_OVERNIGHT', 'FedEx - First Overnight'), 51 | ('PRIORITY_OVERNIGHT', 'FedEx - Priority Overnight'), 52 | ('INTERNATIONAL_ECONOMY', 'FedEx - International Economy'), 53 | ('INTERNATIONAL_FIRST', 'FedEx - International First'), 54 | ('INTERNATIONAL_PRIORITY', 'FedEx - International Priority'), 55 | ('GROUND_HOME_DELIVERY', 'FedEx - Ground Home Delivery'), 56 | ('SMART_POST', 'FedEx - Smart Post'), 57 | ], 58 | ) 59 | 60 | @api.model 61 | def _get_shipping_label_for_rate(self, rate, wrapped=False): 62 | """ Returns a shipping.label for the given rate """ 63 | easypost_label = self.env['easypost.shipping.label'].search([ 64 | ('rate_id', '=', rate.id), 65 | ]) 66 | return easypost_label if wrapped else easypost_label.odoo_id 67 | 68 | @api.multi 69 | def easypost_get_shipping_price_from_so(self, orders): 70 | ship_rates = [] 71 | orders.easypost_bind_ids.ensure_bindings(orders, export=True) 72 | for order in orders: 73 | rates = order.carrier_rate_ids.filtered( 74 | lambda r: r.service_id == self, 75 | ) 76 | if not rates: 77 | raise EnvironmentError( 78 | _('The selected shipping service (%s) is not available ' 79 | 'for this shipment. Please select another delivery ' 80 | 'method') % 81 | self.display_name, 82 | ) 83 | ship_rates.append(rates[0].rate) 84 | return ship_rates 85 | 86 | @api.multi 87 | def easypost_send_shipping(self, pickings, packages=None): 88 | shipping_data = [] 89 | for picking in pickings: 90 | rates = picking.dispatch_rate_ids.filtered( 91 | lambda r: self == r.service_id, 92 | ) 93 | if packages: 94 | rates = rates.filtered(lambda r: r.package_id in packages) 95 | rates[0].buy() 96 | shipment = self._get_shipping_label_for_rate(rates[0], True) 97 | shipping_data.append({ 98 | 'exact_price': rates[0].rate, 99 | 'tracking_number': shipment.tracking_number, 100 | 'name': '%s.pdf' % shipment.tracking_number, 101 | 'file': shipment.datas, 102 | }) 103 | return shipping_data 104 | 105 | @api.multi 106 | def easypost_get_tracking_link(self, pickings): 107 | tracking_urls = [] 108 | for picking in pickings: 109 | purchased = picking.dispatch_rate_ids.filtered( 110 | lambda r: r.state == 'purchase' 111 | ) 112 | if purchased: 113 | shipment = self._get_shipping_label_for_rate(purchased, True) 114 | tracking_urls.append(shipment.tracking_url) 115 | return tracking_urls 116 | 117 | @api.multi 118 | def easypost_cancel_shipment(self, pickings): 119 | for picking in pickings: 120 | purchased = picking.dispatch_rate_ids.filtered( 121 | lambda r: r.state == 'purchase' 122 | ) 123 | purchased.cancel() 124 | -------------------------------------------------------------------------------- /connector_easypost/models/easypost_backend/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | -------------------------------------------------------------------------------- /connector_easypost/models/easypost_backend/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import logging 6 | 7 | from odoo import models, fields, api, _ 8 | from odoo.exceptions import ValidationError 9 | 10 | _logger = logging.getLogger(__name__) 11 | 12 | 13 | class EasypostBackend(models.Model): 14 | _name = 'easypost.backend' 15 | _description = 'Easypost Backend' 16 | _inherit = 'connector.backend', 17 | _inherits = {'address.validate': 'validator_id'} 18 | _backend_type = 'easypost' 19 | 20 | validator_id = fields.Many2one( 21 | string='Validator', 22 | comodel_name='address.validate', 23 | required=True, 24 | ondelete='cascade', 25 | ) 26 | version = fields.Selection( 27 | selection='_get_versions', 28 | default='v2', 29 | required=True, 30 | ) 31 | api_key = fields.Char( 32 | string='API Key', 33 | required=True, 34 | related='password', 35 | ) 36 | is_default = fields.Boolean( 37 | default=True, 38 | help='Check this if this is the default connector for the company.' 39 | ' All newly created records for this company will be synced to the' 40 | ' default system. Only records that originated from non-default' 41 | ' systems will be synced with them.', 42 | ) 43 | active = fields.Boolean( 44 | default=True, 45 | ) 46 | company_id = fields.Many2one( 47 | string='Company', 48 | default=lambda s: s.env.user.company_id.id, 49 | comodel_name='res.company', 50 | compute='_compute_company_id', 51 | inverse='_inverse_company_id', 52 | search='_search_company_id', 53 | ) 54 | is_default_address_validator = fields.Boolean( 55 | string='Default Address Validator', 56 | compute='_compute_is_default_address_validator', 57 | inverse='_inverse_is_default_address_validator', 58 | ) 59 | 60 | @api.multi 61 | @api.constrains('is_default', 'company_id') 62 | def _check_default_for_company(self): 63 | for rec_id in self: 64 | domain = [ 65 | ('company_id', '=', rec_id.company_id.id), 66 | ('is_default', '=', True), 67 | ] 68 | if len(self.search(domain)) > 1: 69 | raise ValidationError(_( 70 | 'This company already has a default EasyPost connector.', 71 | )) 72 | 73 | @api.multi 74 | def _compute_company_id(self): 75 | for record in self: 76 | record.company_id = record.company_ids.id 77 | 78 | @api.multi 79 | def _inverse_company_id(self): 80 | for record in self: 81 | record.company_ids = [(6, 0, record.company_id.ids)] 82 | 83 | @api.model 84 | def _search_company_id(self, op, val): 85 | if isinstance(val, int): 86 | val = [val] 87 | return [('company_ids', op, val)] 88 | 89 | @api.multi 90 | def _compute_is_default_address_validator(self): 91 | for record in self: 92 | default = record.company_id.default_address_validate_id 93 | is_default = (default == record.validator_id) 94 | record.is_default_address_validator = is_default 95 | 96 | @api.multi 97 | def _inverse_is_default_address_validator(self): 98 | for record in self: 99 | validator_id = record.validator_id.id 100 | record.company_id.default_address_validate_id = validator_id 101 | 102 | @api.model 103 | def _get_versions(self): 104 | """ Available versions in the backend. 105 | Can be inherited to add custom versions. Using this method 106 | to add a version from an ``_inherit`` does not constrain 107 | to redefine the ``version`` field in the ``_inherit`` model. 108 | """ 109 | return [('v2', 'v2')] 110 | 111 | @api.model 112 | def _get_interface_types(self): 113 | res = super(EasypostBackend, self)._get_interface_types() 114 | return res + [('easypost', 'EasyPost')] 115 | 116 | @api.model 117 | def create(self, vals): 118 | if not vals.get('validator_id'): 119 | validator = self.env['address.validate'].create({ 120 | 'name': vals['name'], 121 | 'interface_type': 'easypost', 122 | 'system_type': 'address.validate', 123 | 'password': vals['api_key'], 124 | }) 125 | vals['validator_id'] = validator.id 126 | return super(EasypostBackend, self).create(vals) 127 | 128 | @api.multi 129 | def unlink(self): 130 | if not self.env.context.get('no_validator_unlink'): 131 | for record in self: 132 | system = record.validator_id.with_context( 133 | no_validator_unlink=True, 134 | ) 135 | return system.unlink() 136 | else: 137 | return super(EasypostBackend, self).unlink() 138 | 139 | @api.multi 140 | def _import_all(self, model_name): 141 | for backend in self: 142 | self.env[model_name].with_delay().import_batch(backend) 143 | 144 | # Address Validation Interface 145 | def easypost_get_address(self, partner): 146 | return self.validator_id.easypost_get_address(None, partner) 147 | -------------------------------------------------------------------------------- /connector_easypost/models/easypost_binding/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | -------------------------------------------------------------------------------- /connector_easypost/models/easypost_binding/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import logging 6 | 7 | from odoo import models, fields, api 8 | from odoo.addons.queue_job.job import job 9 | 10 | _logger = logging.getLogger(__name__) 11 | 12 | 13 | class EasypostBinding(models.AbstractModel): 14 | """Abstract model for all EasyPost binding models. 15 | 16 | All binding models should `_inherit` from this. They also need to declare 17 | the ``odoo_id`` Many2One field that relates to the Odoo record that the 18 | binding record represents. 19 | """ 20 | _name = 'easypost.binding' 21 | _inherit = 'external.binding' 22 | _description = 'Easypost Binding Abstract' 23 | 24 | # odoo_id = odoo-side id must be declared in concrete model 25 | backend_id = fields.Many2one( 26 | comodel_name='easypost.backend', 27 | string='Easypost Backend', 28 | required=True, 29 | readonly=True, 30 | ondelete='restrict', 31 | default=lambda s: s._default_backend_id(), 32 | ) 33 | external_id = fields.Char( 34 | string='ID on Easypost', 35 | ) 36 | 37 | mode = fields.Char( 38 | help='EasyPost Mode', 39 | ) 40 | created_at = fields.Date('Created At (on EasyPost)') 41 | updated_at = fields.Date('Updated At (on EasyPost)') 42 | 43 | _sql_constraints = [ 44 | ('easypost_uniq', 'unique(backend_id, external_id)', 45 | 'A binding already exists with the same Easypost ID.'), 46 | ] 47 | 48 | @api.model 49 | def _default_backend_id(self): 50 | return self.env['easypost.backend'].search([ 51 | ('is_default', '=', True), 52 | ('active', '=', True), 53 | ], 54 | limit=1, 55 | ) 56 | 57 | @api.model 58 | @job(default_channel='root.easypost') 59 | def import_batch(self, backend, filters=None): 60 | """Prepare the import of records modified in EasyPost.""" 61 | if filters is None: 62 | filters = {} 63 | with backend.work_on(self._name) as work: 64 | importer = work.component(usage='batch.importer') 65 | return importer.run(filters=filters) 66 | 67 | @api.model 68 | def import_direct(self, backend, external_record): 69 | """Directly import a data record.""" 70 | with backend.work_on(self._name) as work: 71 | importer = work.component(usage='record.importer') 72 | return importer.run( 73 | external_record.id, 74 | external_record=external_record, 75 | force=True, 76 | ) 77 | 78 | @api.model 79 | @job(default_channel='root.easypost') 80 | def import_record(self, backend, external_id, force=False): 81 | """Import an EasyPost record.""" 82 | with backend.work_on(self._name) as work: 83 | importer = work.component(usage='record.importer') 84 | return importer.run(external_id, force=force) 85 | 86 | @api.model_cr_context 87 | def ensure_bindings(self, odoo_records, force=False, export=False, 88 | external_id=None, company=None): 89 | bindings = odoo_records.easypost_bind_ids.with_context( 90 | connector_no_export=True, 91 | ) 92 | for record in odoo_records: 93 | if record.easypost_bind_ids and not force: 94 | continue 95 | try: 96 | company = (company or 97 | record.company_id or 98 | self.env.user.company_id 99 | ) 100 | except AttributeError: 101 | company = self.env.user.company_id 102 | if company.easypost_backend_id: 103 | vals = { 104 | 'odoo_id': record.id, 105 | 'backend_id': company.easypost_backend_id.id, 106 | } 107 | if external_id: 108 | vals['external_id'] = external_id 109 | new_binding = bindings.create(vals) 110 | bindings += new_binding 111 | if export: 112 | exporter = new_binding.with_context( 113 | connector_no_export=False, 114 | ) 115 | exporter.export_record() 116 | 117 | @api.multi 118 | @job(default_channel='root.easypost') 119 | def export_record(self, fields=None): 120 | self.ensure_one() 121 | with self.backend_id.work_on(self._name) as work: 122 | exporter = work.component(usage='record.exporter') 123 | _logger.debug( 124 | 'Exporting "%s" with fields "%s"', self, fields or 'all', 125 | ) 126 | return exporter.run(self, fields) 127 | 128 | @api.model 129 | @job(default_channel='root.easypost') 130 | def export_delete_record(self, backend, external_id): 131 | """Delete a record on EasyPost.""" 132 | with backend.work_on(self._name) as work: 133 | deleter = work.component(usage='record.exporter.deleter') 134 | return deleter.run(external_id) 135 | -------------------------------------------------------------------------------- /connector_easypost/models/parcel/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | 7 | from . import exporter 8 | from . import importer 9 | -------------------------------------------------------------------------------- /connector_easypost/models/parcel/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import logging 6 | 7 | from odoo import fields, models 8 | from odoo.addons.component.core import Component 9 | 10 | _logger = logging.getLogger(__name__) 11 | 12 | 13 | class EasypostParcel(models.Model): 14 | """ Binding Model for the Easypost StockQuantPackage. """ 15 | _name = 'easypost.parcel' 16 | _inherit = 'easypost.binding' 17 | _inherits = {'stock.quant.package': 'odoo_id'} 18 | _description = 'Easypost Parcel' 19 | _easypost_model = 'Parcel' 20 | 21 | odoo_id = fields.Many2one( 22 | comodel_name='stock.quant.package', 23 | string='StockQuantPackage', 24 | required=True, 25 | ondelete='cascade', 26 | ) 27 | 28 | 29 | class StockQuantPackage(models.Model): 30 | """ Adds the ``one2many`` relation to the Easypost bindings 31 | (``easypost_bind_ids``) 32 | """ 33 | _inherit = 'stock.quant.package' 34 | 35 | easypost_bind_ids = fields.One2many( 36 | comodel_name='easypost.parcel', 37 | inverse_name='odoo_id', 38 | string='Easypost Bindings', 39 | ) 40 | 41 | 42 | class ParcelAdapter(Component): 43 | """ Backend Adapter for the Easypost StockQuantPackage """ 44 | _name = 'easypost.parcel.adapter' 45 | _inherit = 'easypost.adapter' 46 | _apply_on = 'easypost.parcel' 47 | 48 | def update(self, _id, data): 49 | """Parcels are immutable; create a new one instead.""" 50 | _logger.info('Parcel update with %s and %s', _id, data) 51 | return self.create(data) 52 | -------------------------------------------------------------------------------- /connector_easypost/models/parcel/exporter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import Component 6 | from odoo.addons.connector.components.mapper import changed_by, mapping 7 | 8 | 9 | class ParcelExportMapper(Component): 10 | 11 | _name = 'easypost.export.mapper.parcel' 12 | _inherit = 'easypost.export.mapper' 13 | _apply_on = 'easypost.parcel' 14 | 15 | def _convert_to_inches(self, uom_qty, uom): 16 | inches = self.env.ref('product.product_uom_inch') 17 | if uom.id != inches.id: 18 | return uom._compute_quantity( 19 | uom_qty, inches, 20 | ) 21 | else: 22 | return uom_qty 23 | 24 | def _convert_to_ounces(self, uom_qty, uom): 25 | oz = self.env.ref('product.product_uom_oz') 26 | if uom.id != oz.id: 27 | return uom._compute_quantity( 28 | uom_qty, oz, 29 | ) 30 | else: 31 | return uom_qty 32 | 33 | @mapping 34 | @changed_by('length', 'length_uom_id') 35 | # pylint: disable=W8105 36 | def length(self, record): 37 | length = self._convert_to_inches( 38 | record.length, 39 | record.length_uom_id, 40 | ) 41 | return {'length': length} 42 | 43 | @mapping 44 | @changed_by('width', 'width_uom_id') 45 | def width(self, record): 46 | width = self._convert_to_inches( 47 | record.width, 48 | record.width_uom_id, 49 | ) 50 | return {'width': width} 51 | 52 | @mapping 53 | @changed_by('height', 'height_uom_id') 54 | def height(self, record): 55 | height = self._convert_to_inches( 56 | record.height, 57 | record.height_uom_id, 58 | ) 59 | return {'height': height} 60 | 61 | @mapping 62 | @changed_by('total_weight', 'weight', 'weight_uom_id') 63 | def weight(self, record): 64 | """ Lookup the actual picking weight as the record weight 65 | only accounts for the weight of the packaging """ 66 | weight = self._convert_to_ounces( 67 | record.shipping_weight or record.total_weight, 68 | record.weight_uom_id, 69 | ) 70 | return {'weight': weight} 71 | 72 | @mapping 73 | @changed_by('shipper_package_code') 74 | def predefined_package(self, record): 75 | package = record.packaging_id 76 | if package.shipper_package_code: 77 | return { 78 | 'predefined_package': package.shipper_package_code, 79 | } 80 | 81 | 82 | class StockQuantPackageExporter(Component): 83 | _name = 'easypost.parcel.record.exporter' 84 | _inherit = 'easypost.exporter' 85 | _apply_on = 'easypost.parcel' 86 | -------------------------------------------------------------------------------- /connector_easypost/models/parcel/importer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | 6 | from odoo.addons.component.core import Component 7 | 8 | 9 | class ParcelImportMapper(Component): 10 | 11 | _name = 'easypost.import.mapper.parcel' 12 | _inherit = 'easypost.import.mapper' 13 | _apply_on = 'easypost.parcel' 14 | 15 | _direct = [ 16 | ('length', 'length_float'), 17 | ('width', 'width_floath'), 18 | ('height', 'height_float'), 19 | ('weight', 'total_weight'), 20 | ] 21 | 22 | 23 | class StockQuantPackageImporter(Component): 24 | _name = 'easypost.parcel.record.importer' 25 | _inherit = 'easypost.importer' 26 | _apply_on = 'easypost.parcel' 27 | -------------------------------------------------------------------------------- /connector_easypost/models/product_packaging/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | -------------------------------------------------------------------------------- /connector_easypost/models/product_packaging/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import fields, models 6 | 7 | 8 | class ProductPackaging(models.Model): 9 | _inherit = 'product.packaging' 10 | 11 | package_carrier_type = fields.Selection( 12 | selection_add=[('easypost', 'EasyPost')], 13 | ) 14 | -------------------------------------------------------------------------------- /connector_easypost/models/rate/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | 7 | from . import importer 8 | -------------------------------------------------------------------------------- /connector_easypost/models/rate/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import api, fields, models 6 | from odoo.addons.component.core import Component 7 | 8 | 9 | class EasypostRate(models.Model): 10 | """ Binding Model for the Easypost StockPickingRate """ 11 | _name = 'easypost.rate' 12 | _inherit = 'easypost.binding' 13 | _inherits = {'stock.picking.rate': 'odoo_id'} 14 | _description = 'Easypost Rate' 15 | _easypost_model = 'Rate' 16 | 17 | odoo_id = fields.Many2one( 18 | comodel_name='stock.picking.rate', 19 | string='StockPickingRate', 20 | required=True, 21 | ondelete='cascade', 22 | ) 23 | 24 | @api.multi 25 | def buy(self): 26 | with self.backend_id.work_on('easypost.shipment') as work: 27 | adapter = work.component(usage='backend.adapter') 28 | for rate in self.filtered(lambda r: not r.date_purchased): 29 | shipment = adapter.buy(rate) 30 | self.env['easypost.shipment'].import_direct( 31 | self.backend_id, shipment, 32 | ) 33 | self.env['easypost.shipping.label'].import_direct( 34 | self.backend_id, shipment, 35 | ) 36 | 37 | @api.multi 38 | def cancel(self): 39 | with self.backend_id.work_on('easypost.shipment') as work: 40 | adapter = work.component(usage='backend.adapter') 41 | for rate in self.filtered(lambda r: r.date_purchased): 42 | shipment = adapter.cancel(rate) 43 | self.env['easypost.shipment'].import_direct( 44 | self.backend_id, shipment, 45 | ) 46 | 47 | 48 | class StockPickingRate(models.Model): 49 | """ Adds the ``one2many`` relation to the Easypost bindings 50 | (``easypost_bind_ids``) 51 | """ 52 | _inherit = 'stock.picking.rate' 53 | 54 | easypost_bind_ids = fields.One2many( 55 | comodel_name='easypost.rate', 56 | inverse_name='odoo_id', 57 | string='Easypost Bindings', 58 | ) 59 | 60 | @api.multi 61 | def buy(self): 62 | for binding in self.mapped('easypost_bind_ids'): 63 | binding.buy() 64 | return super(StockPickingRate, self).buy() 65 | 66 | @api.multi 67 | def cancel(self): 68 | for binding in self.mapped('easypost_bind_ids'): 69 | binding.cancel() 70 | return super(StockPickingRate, self).cancel() 71 | 72 | 73 | class EasypostRateAdapter(Component): 74 | """ Backend Adapter for the Easypost StockPickingRate """ 75 | _name = 'easypost.rate.adapter' 76 | _inherit = 'easypost.adapter' 77 | _apply_on = 'easypost.rate' 78 | -------------------------------------------------------------------------------- /connector_easypost/models/rate/importer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import re 6 | 7 | from odoo.addons.component.core import Component 8 | from odoo.addons.connector.components.mapper import mapping, only_create 9 | 10 | from ...components.mapper import eval_false 11 | 12 | 13 | class EastypostRateImportMapper(Component): 14 | 15 | _name = 'easypost.import.mapper.rate' 16 | _inherit = 'easypost.import.mapper' 17 | _apply_on = 'easypost.rate' 18 | 19 | direct = [ 20 | (eval_false('mode'), 'mode'), 21 | (eval_false('rate'), 'rate'), 22 | (eval_false('list_rate'), 'list_rate'), 23 | (eval_false('retail_rate'), 'retail_rate'), 24 | (eval_false('delivery_days'), 'delivery_days'), 25 | (eval_false('delivery_date_guaranteed'), 'is_guaranteed'), 26 | (eval_false('delivery_date'), 'date_delivery'), 27 | ] 28 | 29 | def _camel_to_title(self, camel_case): 30 | s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel_case) 31 | return re.sub('([a-z0-9])([A-Z])', r'\1 \2', s1) 32 | 33 | def _get_currency_id(self, name): 34 | return self.env['res.currency'].search([ 35 | ('name', '=', name), 36 | ], 37 | limit=1, 38 | ) 39 | 40 | @mapping 41 | @only_create 42 | def rate_currency_id(self, record): 43 | return {'rate_currency_id': self._get_currency_id(record.currency).id} 44 | 45 | @mapping 46 | @only_create 47 | def retail_rate_currency_id(self, record): 48 | return { 49 | 'retail_rate_currency_id': 50 | self._get_currency_id(record.retail_currency).id, 51 | } 52 | 53 | @mapping 54 | @only_create 55 | def list_rate_currency_id(self, record): 56 | return { 57 | 'list_rate_currency_id': 58 | self._get_currency_id(record.list_currency).id, 59 | } 60 | 61 | @mapping 62 | @only_create 63 | def service_id(self, record): 64 | Services = self.env['delivery.carrier'] 65 | Partners = self.env['res.partner'] 66 | partner = Partners.search([ 67 | ('name', '=', record.carrier), 68 | ('is_carrier', '=', True), 69 | ], 70 | limit=1, 71 | ) 72 | if not partner: 73 | partner = Partners.create({ 74 | 'name': record.carrier, 75 | 'is_carrier': True, 76 | 'customer': False, 77 | 'supplier': True, 78 | }) 79 | service = Services.search([ 80 | ('partner_id', '=', partner.id), 81 | ('easypost_service', '=', record.service), 82 | ('delivery_type', '=', 'easypost'), 83 | ], 84 | limit=1, 85 | ) 86 | if not service: 87 | name = '%s - %s' % ( 88 | partner.name, 89 | self._camel_to_title(record.service), 90 | ) 91 | service = Services.create({ 92 | 'name': name, 93 | 'delivery_type': 'easypost', 94 | 'partner_id': partner.id, 95 | 'easypost_service': record.service, 96 | 'integration_level': 'rate_and_ship', 97 | 'prod_environment': True, 98 | 'active': True, 99 | }) 100 | return {'service_id': service.id} 101 | 102 | @mapping 103 | @only_create 104 | def picking_id(self, record): 105 | picking_id = self.binder_for('easypost.shipment').to_odoo( 106 | record.shipment_id, unwrap=True, browse=False 107 | ) 108 | return {'picking_id': picking_id} 109 | 110 | @mapping 111 | @only_create 112 | def package_id(self, record): 113 | package_id = self.binder_for('easypost.parcel').to_odoo( 114 | record.parcel.id, unwrap=True, browse=False 115 | ) 116 | return {'package_id': package_id} 117 | 118 | 119 | class EasypostRateImporter(Component): 120 | _name = 'easypost.importer.rate' 121 | _inherit = 'easypost.importer' 122 | _apply_on = 'easypost.rate' 123 | -------------------------------------------------------------------------------- /connector_easypost/models/res_company/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | -------------------------------------------------------------------------------- /connector_easypost/models/res_company/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import api, fields, models 6 | 7 | 8 | class ResCompany(models.Model): 9 | _inherit = 'res.company' 10 | 11 | easypost_backend_id = fields.Many2one( 12 | string='Default EasyPost Backend', 13 | comodel_name='easypost.backend', 14 | compute='_compute_easypost_backend_id', 15 | ) 16 | 17 | @api.multi 18 | def _compute_easypost_backend_id(self): 19 | for record in self: 20 | backend = self.env['easypost.backend'].search([ 21 | ('company_id', '=', record.id), 22 | ('is_default', '=', True), 23 | ], 24 | limit=1 25 | ) 26 | record.easypost_backend_id = backend 27 | -------------------------------------------------------------------------------- /connector_easypost/models/res_partner/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | -------------------------------------------------------------------------------- /connector_easypost/models/res_partner/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import fields, models 6 | 7 | 8 | class ResPartner(models.Model): 9 | _inherit = 'res.partner' 10 | is_carrier = fields.Boolean() 11 | -------------------------------------------------------------------------------- /connector_easypost/models/sale/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | 7 | from . import importer 8 | from . import exporter 9 | -------------------------------------------------------------------------------- /connector_easypost/models/sale/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import fields 6 | from odoo import models 7 | 8 | from odoo.addons.component.core import Component 9 | 10 | 11 | class EasypostSale(models.Model): 12 | """ Binding Model for the Easypost Sale """ 13 | _name = 'easypost.sale' 14 | _inherit = 'easypost.binding' 15 | _inherits = {'sale.order': 'odoo_id'} 16 | _description = 'Easypost Sale' 17 | _easypost_model = 'Shipment' 18 | 19 | odoo_id = fields.Many2one( 20 | comodel_name='sale.order', 21 | string='Sale Order', 22 | required=True, 23 | ondelete='cascade', 24 | ) 25 | 26 | 27 | class SaleOrder(models.Model): 28 | _inherit = 'sale.order' 29 | 30 | easypost_bind_ids = fields.One2many( 31 | comodel_name='easypost.sale', 32 | inverse_name='odoo_id', 33 | string='Easypost Bindings', 34 | ) 35 | 36 | 37 | class EasypostSaleAdapter(Component): 38 | """ Backend Adapter for the Easypost Sale """ 39 | _name = 'easypost.sale.adapter' 40 | _inherit = 'easypost.adapter' 41 | _apply_on = 'easypost.sale' 42 | -------------------------------------------------------------------------------- /connector_easypost/models/sale/exporter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import Component 6 | from odoo.addons.connector.components.mapper import changed_by, mapping 7 | 8 | 9 | class EasypostSaleExportMapper(Component): 10 | 11 | _name = 'easypost.export.mapper.sale' 12 | _inherit = 'easypost.export.mapper.shipment' 13 | _apply_on = 'easypost.sale' 14 | 15 | @mapping 16 | @changed_by('order_line') 17 | def parcel(self, record): 18 | # Estimate based on a cube package 19 | weight = 0.0 20 | volume = 0.0 21 | for line in record.order_line: 22 | weight += line.product_id.weight_oz * line.product_uom_qty 23 | volume += line.product_id.volume_in * line.product_uom_qty 24 | cube_side = volume ** (1.0/2.0) 25 | return { 26 | 'parcel': { 27 | 'length': cube_side, 28 | 'width': cube_side, 29 | 'height': cube_side, 30 | 'weight': weight, 31 | }, 32 | } 33 | 34 | @mapping 35 | @changed_by('partner_id') 36 | def to_address(self, record): 37 | return {'to_address': self._map_partner(record.partner_id)} 38 | 39 | @mapping 40 | @changed_by('location_id') 41 | def from_address(self, record): 42 | return { 43 | 'from_address': self._map_partner(record.company_id.partner_id), 44 | } 45 | 46 | 47 | class EasypostSaleExporter(Component): 48 | 49 | _name = 'easypost.sale.record.exporter' 50 | _inherit = 'easypost.exporter' 51 | _apply_on = 'easypost.sale' 52 | 53 | def _after_export(self): 54 | """Immediate re-import & expire old rates. """ 55 | 56 | existing_rates = self.binding_record.carrier_rate_ids.filtered( 57 | lambda r: r.service_id.delivery_type == 'easypost' 58 | ) 59 | existing_rates.unlink() 60 | 61 | for rate in self.easypost_record.rates: 62 | rate.parcel = self.easypost_record.parcel 63 | self.env['easypost.sale.rate'].import_direct( 64 | self.backend_record, rate, 65 | ) 66 | -------------------------------------------------------------------------------- /connector_easypost/models/sale/importer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import Component 6 | 7 | 8 | class EasypostSaleImportMapper(Component): 9 | 10 | _name = 'easypost.import.mapper.sale' 11 | _inherit = 'easypost.import.mapper' 12 | _apply_on = 'easypost.sale' 13 | 14 | 15 | class EasypostSaleImporter(Component): 16 | 17 | _name = 'easypost.shipment.record.sale' 18 | _inherit = 'easypost.importer' 19 | _apply_on = 'easypost.sale' 20 | -------------------------------------------------------------------------------- /connector_easypost/models/sale_rate/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | 7 | from . import importer 8 | -------------------------------------------------------------------------------- /connector_easypost/models/sale_rate/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import api, fields, models 6 | from odoo.addons.component.core import Component 7 | 8 | 9 | class EasypostSaleRate(models.Model): 10 | """ Binding Model for the Easypost StockPickingRate """ 11 | _name = 'easypost.sale.rate' 12 | _inherit = 'easypost.binding' 13 | _inherits = {'delivery.carrier.rate': 'odoo_id'} 14 | _description = 'Easypost Sale Rate' 15 | _easypost_model = 'Rate' 16 | 17 | odoo_id = fields.Many2one( 18 | comodel_name='delivery.carrier.rate', 19 | string='Delivery Carrier Rate', 20 | required=True, 21 | ondelete='cascade', 22 | ) 23 | 24 | 25 | class DeliveryCarrierRate(models.Model): 26 | _inherit = 'delivery.carrier.rate' 27 | 28 | easypost_bind_ids = fields.One2many( 29 | comodel_name='easypost.sale.rate', 30 | inverse_name='odoo_id', 31 | string='Easypost Bindings', 32 | ) 33 | 34 | @api.multi 35 | def generate_equiv_picking_rates(self, stock_picking): 36 | rates = super(DeliveryCarrierRate, self).generate_equiv_picking_rates( 37 | stock_picking, 38 | ) 39 | stock_picking.easypost_bind_ids.ensure_bindings( 40 | stock_picking, 41 | ) 42 | return rates 43 | 44 | 45 | class EasypostSaleRateAdapter(Component): 46 | """ Backend Adapter for the Easypost StockPickingRate """ 47 | _name = 'easypost.sale.rate.adapter' 48 | _inherit = 'easypost.adapter' 49 | _apply_on = 'easypost.sale.rate' 50 | -------------------------------------------------------------------------------- /connector_easypost/models/sale_rate/importer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import Component 6 | from odoo.addons.connector.components.mapper import mapping, only_create 7 | 8 | 9 | class EastypostSaleRateImportMapper(Component): 10 | 11 | _name = 'easypost.import.mapper.sale.rate' 12 | _inherit = 'easypost.import.mapper.rate' 13 | _apply_on = 'easypost.sale.rate' 14 | 15 | @mapping 16 | @only_create 17 | def sale_order_id(self, record): 18 | sale_order_id = self.binder_for('easypost.sale').to_odoo( 19 | record.shipment_id, unwrap=True, browse=False 20 | ) 21 | return {'sale_order_id': sale_order_id} 22 | 23 | @mapping 24 | @only_create 25 | def picking_id(self, record): 26 | """This is required to stub off the picking logic from parent.""" 27 | return 28 | 29 | 30 | class EasypostSaleRateImporter(Component): 31 | _name = 'easypost.importer.sale.rate' 32 | _inherit = 'easypost.importer' 33 | _apply_on = 'easypost.sale.rate' 34 | -------------------------------------------------------------------------------- /connector_easypost/models/shipment/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | 7 | from . import importer 8 | from . import exporter 9 | -------------------------------------------------------------------------------- /connector_easypost/models/shipment/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import api, fields, models 6 | 7 | from odoo.addons.component.core import Component 8 | 9 | 10 | class EasypostShipment(models.Model): 11 | """ Binding Model for the Easypost EasypostShipment """ 12 | _name = 'easypost.shipment' 13 | _inherit = 'easypost.binding' 14 | _inherits = {'stock.picking': 'odoo_id'} 15 | _description = 'Easypost Shipment' 16 | _easypost_model = 'Shipment' 17 | 18 | odoo_id = fields.Many2one( 19 | comodel_name='stock.picking', 20 | string='StockPicking', 21 | required=True, 22 | ondelete='cascade', 23 | ) 24 | refund_status = fields.Char() 25 | 26 | 27 | class StockPicking(models.Model): 28 | """ Adds the ``one2many`` relation to the Easypost bindings 29 | (``easypost_bind_ids``) 30 | """ 31 | _inherit = 'stock.picking' 32 | 33 | easypost_bind_ids = fields.One2many( 34 | comodel_name='easypost.shipment', 35 | inverse_name='odoo_id', 36 | string='Easypost Bindings', 37 | ) 38 | 39 | @api.multi 40 | def generate_shipping_labels(self, packages=None): 41 | """ Add label generation for EasyPost """ 42 | self.ensure_one() 43 | if self.carrier_id.delivery_type == 'easypost': 44 | if not isinstance(packages, type(self.env['stock.quant.package'])): 45 | packages = self.env['stock.quant.package'].browse(packages) 46 | return self.carrier_id.easypost_send_shipping( 47 | self, packages, 48 | ) 49 | _super = super(StockPicking, self) 50 | return _super.generate_shipping_labels(package_ids=packages) 51 | 52 | 53 | class EasypostShipmentAdapter(Component): 54 | """ Backend Adapter for the Easypost EasypostShipment """ 55 | _name = 'easypost.shipment.adapter' 56 | _inherit = 'easypost.adapter' 57 | _apply_on = 'easypost.shipment' 58 | 59 | def buy(self, easypost_rate): 60 | """ Allows for purchasing of Rates through EasyPost 61 | :param rate: Unwrapped Odoo Rate record to purchase label for 62 | """ 63 | 64 | easypost_shipment = self._get_shipment(easypost_rate) 65 | easypost_shipment = self.read(easypost_shipment.external_id) 66 | 67 | easypost_shipment.buy(rate={'id': easypost_rate.external_id}) 68 | 69 | return easypost_shipment 70 | 71 | def cancel(self, easypost_rate): 72 | """ Allows for refunding of Rates through EasyPost 73 | :param rate: Unwrapped Odoo Rate record to cancel 74 | """ 75 | 76 | easypost_shipment = self._get_shipment(easypost_rate) 77 | easypost_shipment = self.read(easypost_shipment.external_id) 78 | 79 | easypost_shipment.refund(rate={'id': easypost_rate.external_id}) 80 | 81 | return easypost_shipment 82 | 83 | def _get_shipment(self, easypost_rate): 84 | """Return the binding picking and rate for an unwrapped rate.""" 85 | 86 | easypost_rate.ensure_one() 87 | 88 | ship_odoo_record = self.env['easypost.shipment'].search([ 89 | ('odoo_id', '=', easypost_rate.picking_id.id), 90 | ('backend_id', '=', self.backend_record.id), 91 | ], 92 | limit=1, 93 | ) 94 | 95 | return ship_odoo_record 96 | -------------------------------------------------------------------------------- /connector_easypost/models/shipment/exporter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import Component 6 | from odoo.addons.connector.components.mapper import changed_by, mapping 7 | 8 | 9 | class EasypostShipmentExportMapper(Component): 10 | 11 | _name = 'easypost.export.mapper.shipment' 12 | _inherit = 'easypost.export.mapper' 13 | _apply_on = 'easypost.shipment' 14 | 15 | def _map_partner(self, partner_id): 16 | vals = { 17 | 'name': partner_id.name, 18 | 'street1': partner_id.street, 19 | 'street2': partner_id.street2 or '', 20 | 'email': partner_id.email or '', 21 | 'phone': partner_id.phone or '', 22 | 'city': partner_id.city, 23 | 'zip': partner_id.zip, 24 | 'state': partner_id.state_id.code, 25 | 'country': partner_id.country_id.code, 26 | } 27 | if partner_id.company_id: 28 | vals['company'] = partner_id.company_id.name 29 | return vals 30 | 31 | @mapping 32 | @changed_by('package_ids') 33 | def parcel(self, record): 34 | if record.package_ids: 35 | binder = self.binder_for('easypost.parcel') 36 | parcel = binder.to_backend(record.package_ids[:1]) 37 | return {'parcel': {'id': parcel}} 38 | 39 | @mapping 40 | @changed_by('partner_id') 41 | def to_address(self, record): 42 | return {'to_address': self._map_partner(record.partner_id)} 43 | 44 | @mapping 45 | @changed_by('location_id') 46 | def from_address(self, record): 47 | warehouse = self.env['stock.warehouse'].search([ 48 | ('lot_stock_id', '=', record.location_id.id), 49 | ], 50 | limit=1, 51 | ) 52 | partner = warehouse.partner_id or record.company_id.partner_id 53 | return {'from_address': self._map_partner(partner)} 54 | 55 | 56 | class EasypostShipmentExporter(Component): 57 | 58 | _name = 'easypost.shipment.record.exporter' 59 | _inherit = 'easypost.exporter' 60 | _apply_on = 'easypost.shipment' 61 | 62 | def _after_export(self): 63 | """Immediate re-import & expire old rates. """ 64 | 65 | # Existing EasyPost rates are now invalid. 66 | existing_rates = self.binding_record.dispatch_rate_ids.filtered( 67 | lambda r: (r.service_id.delivery_type == 'easypost' and 68 | not r.date_purchased) 69 | ) 70 | existing_rates.unlink() 71 | 72 | for rate in self.easypost_record.rates: 73 | rate.parcel = self.easypost_record.parcel 74 | self.env['easypost.rate'].import_direct( 75 | self.backend_record, rate, 76 | ) 77 | -------------------------------------------------------------------------------- /connector_easypost/models/shipment/importer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.component.core import Component 6 | from odoo.addons.connector.components.mapper import none 7 | 8 | 9 | class EasypostShipmentImportMapper(Component): 10 | 11 | _name = 'easypost.import.mapper.shipment' 12 | _inherit = 'easypost.import.mapper' 13 | _apply_on = 'easypost.shipment' 14 | 15 | direct = [ 16 | (none('refund_status'), 'refund_status'), 17 | ] 18 | 19 | 20 | class EasypostShipmentImporter(Component): 21 | 22 | _name = 'easypost.shipment.record.importer' 23 | _inherit = 'easypost.importer' 24 | _apply_on = 'easypost.shipment' 25 | -------------------------------------------------------------------------------- /connector_easypost/models/shipping_label/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import common 6 | 7 | from . import importer 8 | -------------------------------------------------------------------------------- /connector_easypost/models/shipping_label/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo import models, fields 6 | 7 | from odoo.addons.component.core import Component 8 | 9 | 10 | class EasypostShippingLabel(models.Model): 11 | """ Binding Model for the Easypost ShippingLabel """ 12 | _name = 'easypost.shipping.label' 13 | _inherit = 'easypost.binding' 14 | _inherits = {'shipping.label': 'odoo_id'} 15 | _description = 'Easypost ShippingLabel' 16 | _easypost_model = 'Shipment' 17 | 18 | odoo_id = fields.Many2one( 19 | comodel_name='shipping.label', 20 | string='ShippingLabel', 21 | required=True, 22 | ondelete='cascade', 23 | ) 24 | rate_id = fields.Many2one( 25 | string='Rate', 26 | comodel_name='stock.picking.rate', 27 | ) 28 | tracking_url = fields.Char( 29 | related='package_id.parcel_tracking_uri', 30 | ) 31 | tracking_number = fields.Char( 32 | string='Tracking Number', 33 | related='package_id.parcel_tracking', 34 | ) 35 | 36 | 37 | class ShippingLabel(models.Model): 38 | """ Adds the ``one2many`` relation to the Easypost bindings 39 | (``easypost_bind_ids``) 40 | """ 41 | _inherit = 'shipping.label' 42 | 43 | easypost_bind_ids = fields.One2many( 44 | comodel_name='easypost.shipping.label', 45 | inverse_name='odoo_id', 46 | string='Easypost Bindings', 47 | ) 48 | 49 | 50 | class ShippingLabelAdapter(Component): 51 | """ Backend Adapter for the Easypost ShippingLabel """ 52 | _name = 'easypost.shipping.label.adapter' 53 | _inherit = 'easypost.adapter' 54 | _apply_on = 'easypost.shipping.label' 55 | -------------------------------------------------------------------------------- /connector_easypost/models/shipping_label/importer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import requests 6 | from odoo.addons.component.core import Component 7 | from odoo.addons.connector.components.mapper import mapping, only_create 8 | 9 | from ...components.mapper import inner_attr 10 | 11 | 12 | class ShippingLabelImportMapper(Component): 13 | _name = 'easypost.import.mapper.shipping.label' 14 | _inherit = 'easypost.import.mapper' 15 | _apply_on = 'easypost.shipping.label' 16 | 17 | direct = [ 18 | (inner_attr('postage_label', 'id'), 'external_id'), 19 | ] 20 | 21 | @mapping 22 | def package_id(self, record): 23 | binder = self.binder_for('easypost.parcel') 24 | package_id = binder.to_odoo(record.parcel.id) 25 | return {'package_id': package_id} 26 | 27 | @mapping 28 | @only_create 29 | def datas(self, record): 30 | label = requests.get(record.postage_label.label_url) 31 | return {'datas': label.content.encode('base64')} 32 | 33 | @mapping 34 | @only_create 35 | def picking_id(self, record): 36 | binder = self.binder_for('easypost.rate') 37 | rate = self.env['stock.picking.rate'].browse( 38 | binder.to_odoo(record.selected_rate.id) 39 | ) 40 | return {'picking_id': rate.picking_id.id} 41 | 42 | @mapping 43 | @only_create 44 | def rate_id(self, record): 45 | binder = self.binder_for('easypost.rate') 46 | rate_id = binder.to_odoo(record.selected_rate.id) 47 | return {'rate_id': rate_id} 48 | 49 | @mapping 50 | def backend_id(self, record): 51 | return {'backend_id': self.backend_record.id} 52 | 53 | @mapping 54 | def name(self, record): 55 | return {'name': record.postage_label.label_url.split('/')[-1]} 56 | 57 | @mapping 58 | def tracking_url(self, record): 59 | tracking_url = '' 60 | if record.tracker: 61 | tracking_url = record.tracker.public_url 62 | return {'tracking_url': tracking_url} 63 | 64 | @mapping 65 | def tracking_number(self, record): 66 | tracking_number = '' 67 | if record.tracker: 68 | tracking_number = record.tracker.tracking_code 69 | return {'tracking_number': tracking_number} 70 | 71 | @mapping 72 | def file_type(self, record): 73 | return {'file_type': 'pdf'} 74 | 75 | 76 | class ShippingLabelImporter(Component): 77 | _name = 'easypost.record.importer.shipping.label' 78 | _inherit = 'easypost.importer' 79 | _apply_on = 'easypost.shipping.label' 80 | -------------------------------------------------------------------------------- /connector_easypost/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | access_easypost_backend,access_easypost_backend,model_easypost_backend,stock.group_stock_manager,1,1,1,1 3 | access_easypost_shipment_user,access_easypost_shipment_user,model_easypost_shipment,stock.group_stock_user,1,1,1,1 4 | access_easypost_shipping_label,access_easypost_shipping_label,model_easypost_shipping_label,stock.group_stock_user,1,1,1,1 5 | access_easypost_parcel,access_easypost_parcel,model_easypost_parcel,stock.group_stock_user,1,1,1,1 6 | access_easypost_rate,access_easypost_rate,model_easypost_rate,stock.group_stock_user,1,1,1,1 7 | access_easypost_sale_user,access_easypost_sale_user,model_easypost_sale,stock.group_stock_user,1,1,1,1 8 | access_easypost_sale_rate,access_easypost_sale_rate,model_easypost_sale_rate,stock.group_stock_user,1,1,1,1 9 | -------------------------------------------------------------------------------- /connector_easypost/static/description/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasLabs/odoo-connector-easypost/7d2edbd0f82861f76cea58082c425a4b7c873ad6/connector_easypost/static/description/icon.png -------------------------------------------------------------------------------- /connector_easypost/static/description/icon.svg: -------------------------------------------------------------------------------- 1 | ModuleIcon_FullApp2 -------------------------------------------------------------------------------- /connector_easypost/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). 3 | 4 | # Address 5 | from . import test_import_address 6 | 7 | # Delivery Carrier 8 | from . import test_common_delivery_carrier 9 | 10 | # Easypost Backend 11 | from . import test_common_easypost_backend 12 | 13 | # Parcel 14 | from . import test_export_parcel 15 | 16 | # Rate 17 | from . import test_common_rate 18 | from . import test_import_rate 19 | 20 | # Res Company 21 | from . import test_common_res_company 22 | 23 | # Sale 24 | from . import test_export_sale 25 | 26 | # Shipment 27 | from . import test_export_shipment 28 | -------------------------------------------------------------------------------- /connector_easypost/tests/fixtures/cassettes/test_backend_easypost_get_address.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'address%5Bcity%5D=False&address%5Bcompany%5D=YourCompany&address%5Bemail%5D=False&address%5Bname%5D=The+White+House&address%5Bphone%5D=False&address%5Bstreet1%5D=1600+Pennsylvania&address%5Bstreet2%5D=False&address%5Bzip%5D=20500' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['229'] 9 | Content-type: [application/x-www-form-urlencoded] 10 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 11 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 12 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 13 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 14 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 15 | "client_version": "3.6.2"}'] 16 | method: POST 17 | uri: https://api.easypost.com/v2/addresses?verify[]=delivery 18 | response: 19 | body: {string: !!python/unicode '{"id":"adr_c35dfb1fd3704912a08567860f4c34ca","object":"Address","created_at":"2017-11-10T17:55:44Z","updated_at":"2017-11-10T17:55:44Z","name":"THE 20 | WHITE HOUSE","company":"YOURCOMPANY","street1":"1600 PENNSYLVANIA AVE NW","street2":"FALSE","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"False","mode":"test","carrier_facility":null,"residential":true,"federal_tax_id":null,"state_tax_id":null,"verifications":{"delivery":{"success":true,"errors":[{"code":"E.SECONDARY_INFORMATION.MISSING","field":"street2","message":"Missing 21 | secondary information(Apt/Suite#)","suggestion":null}],"details":{"latitude":38.8987,"longitude":-77.0352,"time_zone":"America/New_York"}}}}'} 22 | headers: 23 | cache-control: ['no-cache, no-store, must-revalidate, private'] 24 | content-length: ['706'] 25 | content-type: [application/json; charset=utf-8] 26 | expires: ['0'] 27 | location: [/api/v2/addresses/adr_c35dfb1fd3704912a08567860f4c34ca] 28 | pragma: [no-cache] 29 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 30 | transfer-encoding: [chunked] 31 | x-backend: [easypost] 32 | x-content-type-options: [nosniff] 33 | x-ep-request-uuid: [ad51a69d-8a83-4bd5-b75f-d52c7533840c] 34 | x-frame-options: [SAMEORIGIN] 35 | x-node: [web11sj, 42420b5b70, easypost] 36 | x-proxied: [intlb2sj 81b12b6948, intlb1wdc 81b12b6948, extlb2wdc 81b12b6948] 37 | x-runtime: ['0.108584'] 38 | x-xss-protection: [1; mode=block] 39 | status: {code: 201, message: Created} 40 | version: 1 41 | -------------------------------------------------------------------------------- /connector_easypost/tests/fixtures/cassettes/test_common_carrier_get_shipping_price_from_so.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'address%5Bcity%5D=False&address%5Bcompany%5D=YourCompany&address%5Bemail%5D=False&address%5Bname%5D=The+White+House&address%5Bphone%5D=False&address%5Bstreet1%5D=1600+Pennsylvania&address%5Bstreet2%5D=False&address%5Bzip%5D=20500' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['229'] 9 | Content-type: [application/x-www-form-urlencoded] 10 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 11 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 12 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 13 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 14 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 15 | "client_version": "3.6.2"}'] 16 | method: POST 17 | uri: https://api.easypost.com/v2/addresses?verify[]=delivery 18 | response: 19 | body: {string: !!python/unicode '{"id":"adr_d301d128d18e440abd88dfc82edbd58c","object":"Address","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","name":"THE 20 | WHITE HOUSE","company":"YOURCOMPANY","street1":"1600 PENNSYLVANIA AVE NW","street2":"FALSE","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"False","mode":"test","carrier_facility":null,"residential":true,"federal_tax_id":null,"state_tax_id":null,"verifications":{"delivery":{"success":true,"errors":[{"code":"E.SECONDARY_INFORMATION.MISSING","field":"street2","message":"Missing 21 | secondary information(Apt/Suite#)","suggestion":null}],"details":{"latitude":38.8987,"longitude":-77.0352,"time_zone":"America/New_York"}}}}'} 22 | headers: 23 | cache-control: ['no-cache, no-store, must-revalidate, private'] 24 | content-length: ['706'] 25 | content-type: [application/json; charset=utf-8] 26 | expires: ['0'] 27 | location: [/api/v2/addresses/adr_d301d128d18e440abd88dfc82edbd58c] 28 | pragma: [no-cache] 29 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 30 | transfer-encoding: [chunked] 31 | x-backend: [easypost] 32 | x-content-type-options: [nosniff] 33 | x-ep-request-uuid: [3a1e32fc-7665-4fc8-b4e4-109a288f0534] 34 | x-frame-options: [SAMEORIGIN] 35 | x-node: [bigweb1sj, 42420b5b70, easypost] 36 | x-proxied: [intlb1sj 81b12b6948, intlb2wdc 81b12b6948, extlb2wdc 81b12b6948] 37 | x-runtime: ['0.139689'] 38 | x-xss-protection: [1; mode=block] 39 | status: {code: 201, message: Created} 40 | - request: 41 | body: !!python/unicode 'shipment%5Bfrom_address%5D%5Bcity%5D=SCRANTON&shipment%5Bfrom_address%5D%5Bcompany%5D=YourCompany&shipment%5Bfrom_address%5D%5Bcountry%5D=US&shipment%5Bfrom_address%5D%5Bemail%5D=info%40yourcompany.example.com&shipment%5Bfrom_address%5D%5Bname%5D=YourCompany&shipment%5Bfrom_address%5D%5Bphone%5D=%2B1+555+123+8069&shipment%5Bfrom_address%5D%5Bstate%5D=PA&shipment%5Bfrom_address%5D%5Bstreet1%5D=1725+SLOUGH+AVE.&shipment%5Bfrom_address%5D%5Bstreet2%5D=&shipment%5Bfrom_address%5D%5Bzip%5D=18540-0001&shipment%5Bparcel%5D%5Bheight%5D=7.11378660898&shipment%5Bparcel%5D%5Blength%5D=7.11378660898&shipment%5Bparcel%5D%5Bweight%5D=2.0&shipment%5Bparcel%5D%5Bwidth%5D=7.11378660898&shipment%5Bto_address%5D%5Bcity%5D=WASHINGTON&shipment%5Bto_address%5D%5Bcompany%5D=YourCompany&shipment%5Bto_address%5D%5Bcountry%5D=US&shipment%5Bto_address%5D%5Bemail%5D=&shipment%5Bto_address%5D%5Bname%5D=The+White+House&shipment%5Bto_address%5D%5Bphone%5D=&shipment%5Bto_address%5D%5Bstate%5D=DC&shipment%5Bto_address%5D%5Bstreet1%5D=1600+PENNSYLVANIA+AVE+NW&shipment%5Bto_address%5D%5Bstreet2%5D=&shipment%5Bto_address%5D%5Bzip%5D=20500-0003' 42 | headers: 43 | Accept: ['*/*'] 44 | Accept-Encoding: ['gzip, deflate'] 45 | Connection: [keep-alive] 46 | Content-Length: ['1125'] 47 | Content-type: [application/x-www-form-urlencoded] 48 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 49 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 50 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 51 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 52 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 53 | "client_version": "3.6.2"}'] 54 | method: POST 55 | uri: https://api.easypost.com/v2/shipments 56 | response: 57 | body: {string: !!python/unicode '{"created_at":"2017-11-10T17:56:10Z","is_return":false,"messages":[],"mode":"test","options":{"currency":"USD","label_date":null,"date_advance":0},"reference":null,"status":"unknown","tracking_code":null,"updated_at":"2017-11-10T17:56:10Z","batch_id":null,"batch_status":null,"batch_message":null,"customs_info":null,"from_address":{"id":"adr_a7a19a25d1d845e7851a40994ae2491f","object":"Address","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","name":"YourCompany","company":"YourCompany","street1":"1725 58 | SLOUGH AVE.","street2":"","city":"SCRANTON","state":"PA","zip":"18540-0001","country":"US","phone":"15551238069","email":"info@yourcompany.example.com","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"insurance":null,"order_id":null,"parcel":{"id":"prcl_8027ea1178bf4ac2a56e34aa6e0caced","object":"Parcel","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","length":7.11378660898,"width":7.11378660898,"height":7.11378660898,"predefined_package":null,"weight":2.0,"mode":"test"},"postage_label":null,"rates":[{"id":"rate_548843248b4544caa754ffabe79b720a","object":"Rate","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","mode":"test","service":"Express","carrier":"USPS","rate":"21.18","currency":"USD","retail_rate":"23.75","retail_currency":"USD","list_rate":"21.18","list_currency":"USD","delivery_days":null,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":null,"shipment_id":"shp_afeaf329c5fb4c589ae84396b6551476","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"},{"id":"rate_6277d23119f443b8bb220e710dfd4e7f","object":"Rate","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","mode":"test","service":"First","carrier":"USPS","rate":"2.61","currency":"USD","retail_rate":"3.00","retail_currency":"USD","list_rate":"2.61","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_afeaf329c5fb4c589ae84396b6551476","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"},{"id":"rate_6dc8ea02d55f4ea2a719be7cbc60bd3f","object":"Rate","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","mode":"test","service":"Priority","carrier":"USPS","rate":"6.26","currency":"USD","retail_rate":"7.10","retail_currency":"USD","list_rate":"6.46","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_afeaf329c5fb4c589ae84396b6551476","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"},{"id":"rate_57bc5eb6e39e46a1934ac205665042f5","object":"Rate","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","mode":"test","service":"ParcelSelect","carrier":"USPS","rate":"6.46","currency":"USD","retail_rate":"6.46","retail_currency":"USD","list_rate":"6.46","list_currency":"USD","delivery_days":5,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":5,"shipment_id":"shp_afeaf329c5fb4c589ae84396b6551476","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"}],"refund_status":null,"scan_form":null,"selected_rate":null,"tracker":null,"to_address":{"id":"adr_596a46cde51f48dfa3606bf27d022eea","object":"Address","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","name":"The 59 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"usps_zone":3,"return_address":{"id":"adr_a7a19a25d1d845e7851a40994ae2491f","object":"Address","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","name":"YourCompany","company":"YourCompany","street1":"1725 60 | SLOUGH AVE.","street2":"","city":"SCRANTON","state":"PA","zip":"18540-0001","country":"US","phone":"15551238069","email":"info@yourcompany.example.com","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"buyer_address":{"id":"adr_596a46cde51f48dfa3606bf27d022eea","object":"Address","created_at":"2017-11-10T17:56:10Z","updated_at":"2017-11-10T17:56:10Z","name":"The 61 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"forms":[],"fees":[],"id":"shp_afeaf329c5fb4c589ae84396b6551476","object":"Shipment"}'} 62 | headers: 63 | cache-control: ['no-cache, no-store, must-revalidate, private'] 64 | content-length: ['4753'] 65 | content-type: [application/json; charset=utf-8] 66 | expires: ['0'] 67 | location: [/api/v2/shipments/shp_afeaf329c5fb4c589ae84396b6551476] 68 | pragma: [no-cache] 69 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 70 | transfer-encoding: [chunked] 71 | x-backend: [easypost] 72 | x-content-type-options: [nosniff] 73 | x-ep-request-uuid: [dc9932e4-c5f1-445a-834a-bfc1ea6b1079] 74 | x-frame-options: [SAMEORIGIN] 75 | x-node: [bigweb1sj, 42420b5b70, easypost] 76 | x-proxied: [intlb2sj 81b12b6948, intlb2wdc 81b12b6948, extlb2wdc 81b12b6948] 77 | x-runtime: ['0.160573'] 78 | x-xss-protection: [1; mode=block] 79 | status: {code: 201, message: Created} 80 | version: 1 81 | -------------------------------------------------------------------------------- /connector_easypost/tests/fixtures/cassettes/test_export_parcel_predefined_package.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'parcel%5Bheight%5D=1.7&parcel%5Blength%5D=1.7&parcel%5Bpredefined_package%5D=SmallFlatRateBox&parcel%5Bweight%5D=50.0&parcel%5Bwidth%5D=5.4' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['139'] 9 | Content-type: [application/x-www-form-urlencoded] 10 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 11 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 12 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 13 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 14 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 15 | "client_version": "3.6.2"}'] 16 | method: POST 17 | uri: https://api.easypost.com/v2/parcels 18 | response: 19 | body: {string: !!python/unicode '{"id":"prcl_4f98d9d88c0342efa5664e92a851a880","object":"Parcel","created_at":"2017-11-10T17:55:35Z","updated_at":"2017-11-10T17:55:35Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":50.0,"mode":"test"}'} 20 | headers: 21 | cache-control: ['no-cache, no-store, must-revalidate, private'] 22 | content-length: ['242'] 23 | content-type: [application/json; charset=utf-8] 24 | expires: ['0'] 25 | location: [/api/v2/parcels/prcl_4f98d9d88c0342efa5664e92a851a880] 26 | pragma: [no-cache] 27 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 28 | transfer-encoding: [chunked] 29 | x-backend: [easypost] 30 | x-content-type-options: [nosniff] 31 | x-ep-request-uuid: [7c11cd21-3936-4b4b-b135-23e4e05c6b2f] 32 | x-frame-options: [SAMEORIGIN] 33 | x-node: [web4sj, 42420b5b70, easypost] 34 | x-proxied: [intlb1sj 81b12b6948, lb5sj 81b12b6948] 35 | x-runtime: ['0.018612'] 36 | x-xss-protection: [1; mode=block] 37 | status: {code: 201, message: Created} 38 | - request: 39 | body: null 40 | headers: 41 | Accept: ['*/*'] 42 | Accept-Encoding: ['gzip, deflate'] 43 | Connection: [keep-alive] 44 | Content-type: [application/x-www-form-urlencoded] 45 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 46 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 47 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 48 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 49 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 50 | "client_version": "3.6.2"}'] 51 | method: GET 52 | uri: https://api.easypost.com/v2/parcels/prcl_4f98d9d88c0342efa5664e92a851a880 53 | response: 54 | body: {string: !!python/unicode '{"id":"prcl_4f98d9d88c0342efa5664e92a851a880","object":"Parcel","created_at":"2017-11-10T17:55:35Z","updated_at":"2017-11-10T17:55:35Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":50.0,"mode":"test"}'} 55 | headers: 56 | cache-control: ['no-cache, no-store, must-revalidate, private'] 57 | content-length: ['242'] 58 | content-type: [application/json; charset=utf-8] 59 | expires: ['0'] 60 | pragma: [no-cache] 61 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 62 | transfer-encoding: [chunked] 63 | x-backend: [easypost] 64 | x-content-type-options: [nosniff] 65 | x-ep-request-uuid: [66e6b8cc-5bc5-47d7-95ce-71d8c5e3d668] 66 | x-frame-options: [SAMEORIGIN] 67 | x-node: [web6sj, 42420b5b70, easypost] 68 | x-proxied: [intlb2sj 81b12b6948, lb5sj 81b12b6948] 69 | x-runtime: ['0.015014'] 70 | x-xss-protection: [1; mode=block] 71 | status: {code: 200, message: OK} 72 | version: 1 73 | -------------------------------------------------------------------------------- /connector_easypost/tests/fixtures/cassettes/test_export_parcel_shipping_weight.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'parcel%5Bheight%5D=1.7&parcel%5Blength%5D=1.7&parcel%5Bpredefined_package%5D=SmallFlatRateBox&parcel%5Bweight%5D=50.0&parcel%5Bwidth%5D=5.4' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['139'] 9 | Content-type: [application/x-www-form-urlencoded] 10 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 11 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 12 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 13 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 14 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 15 | "client_version": "3.6.2"}'] 16 | method: POST 17 | uri: https://api.easypost.com/v2/parcels 18 | response: 19 | body: {string: !!python/unicode '{"id":"prcl_dd99d27fe41f4a86a3d6b99def285348","object":"Parcel","created_at":"2017-11-10T17:55:36Z","updated_at":"2017-11-10T17:55:36Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":50.0,"mode":"test"}'} 20 | headers: 21 | cache-control: ['no-cache, no-store, must-revalidate, private'] 22 | content-length: ['242'] 23 | content-type: [application/json; charset=utf-8] 24 | expires: ['0'] 25 | location: [/api/v2/parcels/prcl_dd99d27fe41f4a86a3d6b99def285348] 26 | pragma: [no-cache] 27 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 28 | transfer-encoding: [chunked] 29 | x-backend: [easypost] 30 | x-content-type-options: [nosniff] 31 | x-ep-request-uuid: [1763ef75-e6da-4cd7-96cb-fc6436513d28] 32 | x-frame-options: [SAMEORIGIN] 33 | x-node: [web4sj, 42420b5b70, easypost] 34 | x-proxied: [intlb2sj 81b12b6948, intlb1wdc 81b12b6948, extlb1wdc 81b12b6948] 35 | x-runtime: ['0.026432'] 36 | x-xss-protection: [1; mode=block] 37 | status: {code: 201, message: Created} 38 | - request: 39 | body: null 40 | headers: 41 | Accept: ['*/*'] 42 | Accept-Encoding: ['gzip, deflate'] 43 | Connection: [keep-alive] 44 | Content-type: [application/x-www-form-urlencoded] 45 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 46 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 47 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 48 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 49 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 50 | "client_version": "3.6.2"}'] 51 | method: GET 52 | uri: https://api.easypost.com/v2/parcels/prcl_dd99d27fe41f4a86a3d6b99def285348 53 | response: 54 | body: {string: !!python/unicode '{"id":"prcl_dd99d27fe41f4a86a3d6b99def285348","object":"Parcel","created_at":"2017-11-10T17:55:36Z","updated_at":"2017-11-10T17:55:36Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":50.0,"mode":"test"}'} 55 | headers: 56 | cache-control: ['no-cache, no-store, must-revalidate, private'] 57 | content-length: ['242'] 58 | content-type: [application/json; charset=utf-8] 59 | expires: ['0'] 60 | pragma: [no-cache] 61 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 62 | transfer-encoding: [chunked] 63 | x-backend: [easypost] 64 | x-canary: [direct] 65 | x-content-type-options: [nosniff] 66 | x-ep-request-uuid: [da238ee6-1051-4e5a-9e70-5ab65997a79d] 67 | x-frame-options: [SAMEORIGIN] 68 | x-node: [web15sj, 42420b5b70, easypost] 69 | x-proxied: [intlb1sj 81b12b6948, intlb1wdc 81b12b6948, extlb1wdc 81b12b6948] 70 | x-runtime: ['0.011451'] 71 | x-xss-protection: [1; mode=block] 72 | status: {code: 200, message: OK} 73 | version: 1 74 | -------------------------------------------------------------------------------- /connector_easypost/tests/fixtures/cassettes/test_export_parcel_total_weight.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'parcel%5Bheight%5D=1.7&parcel%5Blength%5D=1.7&parcel%5Bpredefined_package%5D=SmallFlatRateBox&parcel%5Bweight%5D=3.0&parcel%5Bwidth%5D=5.4' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['138'] 9 | Content-type: [application/x-www-form-urlencoded] 10 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 11 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 12 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 13 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 14 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 15 | "client_version": "3.6.2"}'] 16 | method: POST 17 | uri: https://api.easypost.com/v2/parcels 18 | response: 19 | body: {string: !!python/unicode '{"id":"prcl_e54e17209a064554859b63590ef28c9e","object":"Parcel","created_at":"2017-11-10T17:55:37Z","updated_at":"2017-11-10T17:55:37Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":3.0,"mode":"test"}'} 20 | headers: 21 | cache-control: ['no-cache, no-store, must-revalidate, private'] 22 | content-length: ['241'] 23 | content-type: [application/json; charset=utf-8] 24 | expires: ['0'] 25 | location: [/api/v2/parcels/prcl_e54e17209a064554859b63590ef28c9e] 26 | pragma: [no-cache] 27 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 28 | transfer-encoding: [chunked] 29 | x-backend: [easypost] 30 | x-content-type-options: [nosniff] 31 | x-ep-request-uuid: [c4698cd3-275d-4b93-a439-0d17e3825fd3] 32 | x-frame-options: [SAMEORIGIN] 33 | x-node: [web4sj, 42420b5b70, easypost] 34 | x-proxied: [intlb2sj 81b12b6948, lb5sj 81b12b6948] 35 | x-runtime: ['0.015392'] 36 | x-xss-protection: [1; mode=block] 37 | status: {code: 201, message: Created} 38 | - request: 39 | body: null 40 | headers: 41 | Accept: ['*/*'] 42 | Accept-Encoding: ['gzip, deflate'] 43 | Connection: [keep-alive] 44 | Content-type: [application/x-www-form-urlencoded] 45 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 46 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 47 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 48 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 49 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 50 | "client_version": "3.6.2"}'] 51 | method: GET 52 | uri: https://api.easypost.com/v2/parcels/prcl_e54e17209a064554859b63590ef28c9e 53 | response: 54 | body: {string: !!python/unicode '{"id":"prcl_e54e17209a064554859b63590ef28c9e","object":"Parcel","created_at":"2017-11-10T17:55:37Z","updated_at":"2017-11-10T17:55:37Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":3.0,"mode":"test"}'} 55 | headers: 56 | cache-control: ['no-cache, no-store, must-revalidate, private'] 57 | content-length: ['241'] 58 | content-type: [application/json; charset=utf-8] 59 | expires: ['0'] 60 | pragma: [no-cache] 61 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 62 | transfer-encoding: [chunked] 63 | x-backend: [easypost] 64 | x-canary: [direct] 65 | x-content-type-options: [nosniff] 66 | x-ep-request-uuid: [aa43385c-f667-4725-b6ca-79a25df6be7a] 67 | x-frame-options: [SAMEORIGIN] 68 | x-node: [web15sj, 42420b5b70, easypost] 69 | x-proxied: [intlb1sj 81b12b6948, lb5sj 81b12b6948] 70 | x-runtime: ['0.010693'] 71 | x-xss-protection: [1; mode=block] 72 | status: {code: 200, message: OK} 73 | version: 1 74 | -------------------------------------------------------------------------------- /connector_easypost/tests/fixtures/cassettes/test_export_sale_imports_rates.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'address%5Bcity%5D=False&address%5Bcompany%5D=YourCompany&address%5Bemail%5D=False&address%5Bname%5D=The+White+House&address%5Bphone%5D=False&address%5Bstreet1%5D=1600+Pennsylvania&address%5Bstreet2%5D=False&address%5Bzip%5D=20500' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['229'] 9 | Content-type: [application/x-www-form-urlencoded] 10 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 11 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 12 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 13 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 14 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 15 | "client_version": "3.6.2"}'] 16 | method: POST 17 | uri: https://api.easypost.com/v2/addresses?verify[]=delivery 18 | response: 19 | body: {string: !!python/unicode '{"id":"adr_5ae4201c419a4ac6bf96ea135b82029b","object":"Address","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","name":"THE 20 | WHITE HOUSE","company":"YOURCOMPANY","street1":"1600 PENNSYLVANIA AVE NW","street2":"FALSE","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"False","mode":"test","carrier_facility":null,"residential":true,"federal_tax_id":null,"state_tax_id":null,"verifications":{"delivery":{"success":true,"errors":[{"code":"E.SECONDARY_INFORMATION.MISSING","field":"street2","message":"Missing 21 | secondary information(Apt/Suite#)","suggestion":null}],"details":{"latitude":38.8987,"longitude":-77.0352,"time_zone":"America/New_York"}}}}'} 22 | headers: 23 | cache-control: ['no-cache, no-store, must-revalidate, private'] 24 | content-length: ['706'] 25 | content-type: [application/json; charset=utf-8] 26 | expires: ['0'] 27 | location: [/api/v2/addresses/adr_5ae4201c419a4ac6bf96ea135b82029b] 28 | pragma: [no-cache] 29 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 30 | transfer-encoding: [chunked] 31 | x-backend: [easypost] 32 | x-content-type-options: [nosniff] 33 | x-ep-request-uuid: [b7f0501b-092e-4eed-b5d3-abcacc88b754] 34 | x-frame-options: [SAMEORIGIN] 35 | x-node: [web11sj, 42420b5b70, easypost] 36 | x-proxied: [intlb1sj 81b12b6948, lb5sj 81b12b6948] 37 | x-runtime: ['0.110560'] 38 | x-xss-protection: [1; mode=block] 39 | status: {code: 201, message: Created} 40 | - request: 41 | body: !!python/unicode 'shipment%5Bfrom_address%5D%5Bcity%5D=SCRANTON&shipment%5Bfrom_address%5D%5Bcompany%5D=YourCompany&shipment%5Bfrom_address%5D%5Bcountry%5D=US&shipment%5Bfrom_address%5D%5Bemail%5D=info%40yourcompany.example.com&shipment%5Bfrom_address%5D%5Bname%5D=YourCompany&shipment%5Bfrom_address%5D%5Bphone%5D=%2B1+555+123+8069&shipment%5Bfrom_address%5D%5Bstate%5D=PA&shipment%5Bfrom_address%5D%5Bstreet1%5D=1725+SLOUGH+AVE.&shipment%5Bfrom_address%5D%5Bstreet2%5D=&shipment%5Bfrom_address%5D%5Bzip%5D=18540-0001&shipment%5Bparcel%5D%5Bheight%5D=7.11378660898&shipment%5Bparcel%5D%5Blength%5D=7.11378660898&shipment%5Bparcel%5D%5Bweight%5D=2.0&shipment%5Bparcel%5D%5Bwidth%5D=7.11378660898&shipment%5Bto_address%5D%5Bcity%5D=WASHINGTON&shipment%5Bto_address%5D%5Bcompany%5D=YourCompany&shipment%5Bto_address%5D%5Bcountry%5D=US&shipment%5Bto_address%5D%5Bemail%5D=&shipment%5Bto_address%5D%5Bname%5D=The+White+House&shipment%5Bto_address%5D%5Bphone%5D=&shipment%5Bto_address%5D%5Bstate%5D=DC&shipment%5Bto_address%5D%5Bstreet1%5D=1600+PENNSYLVANIA+AVE+NW&shipment%5Bto_address%5D%5Bstreet2%5D=&shipment%5Bto_address%5D%5Bzip%5D=20500-0003' 42 | headers: 43 | Accept: ['*/*'] 44 | Accept-Encoding: ['gzip, deflate'] 45 | Connection: [keep-alive] 46 | Content-Length: ['1125'] 47 | Content-type: [application/x-www-form-urlencoded] 48 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 49 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 50 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 51 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 52 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 53 | "client_version": "3.6.2"}'] 54 | method: POST 55 | uri: https://api.easypost.com/v2/shipments 56 | response: 57 | body: {string: !!python/unicode '{"created_at":"2017-11-10T17:55:38Z","is_return":false,"messages":[],"mode":"test","options":{"currency":"USD","label_date":null,"date_advance":0},"reference":null,"status":"unknown","tracking_code":null,"updated_at":"2017-11-10T17:55:38Z","batch_id":null,"batch_status":null,"batch_message":null,"customs_info":null,"from_address":{"id":"adr_420048a65d78477998bdda85f24f5af5","object":"Address","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","name":"YourCompany","company":"YourCompany","street1":"1725 58 | SLOUGH AVE.","street2":"","city":"SCRANTON","state":"PA","zip":"18540-0001","country":"US","phone":"15551238069","email":"info@yourcompany.example.com","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"insurance":null,"order_id":null,"parcel":{"id":"prcl_b58c237579584bb994838714c3393988","object":"Parcel","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","length":7.11378660898,"width":7.11378660898,"height":7.11378660898,"predefined_package":null,"weight":2.0,"mode":"test"},"postage_label":null,"rates":[{"id":"rate_0882601c86164c09b7ef55945bb556e7","object":"Rate","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","mode":"test","service":"Express","carrier":"USPS","rate":"21.18","currency":"USD","retail_rate":"23.75","retail_currency":"USD","list_rate":"21.18","list_currency":"USD","delivery_days":null,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":null,"shipment_id":"shp_aa0a3136ced9465e8ff96399ffe7bd86","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"},{"id":"rate_89eb30e5bb8049c6ac14efa0f5c5614d","object":"Rate","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","mode":"test","service":"First","carrier":"USPS","rate":"2.61","currency":"USD","retail_rate":"3.00","retail_currency":"USD","list_rate":"2.61","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_aa0a3136ced9465e8ff96399ffe7bd86","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"},{"id":"rate_1124a3ed83f74b4e9870a75da070748d","object":"Rate","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","mode":"test","service":"Priority","carrier":"USPS","rate":"6.26","currency":"USD","retail_rate":"7.10","retail_currency":"USD","list_rate":"6.46","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_aa0a3136ced9465e8ff96399ffe7bd86","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"},{"id":"rate_158ea6114bb04b83b04b6b5ab7243127","object":"Rate","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","mode":"test","service":"ParcelSelect","carrier":"USPS","rate":"6.46","currency":"USD","retail_rate":"6.46","retail_currency":"USD","list_rate":"6.46","list_currency":"USD","delivery_days":5,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":5,"shipment_id":"shp_aa0a3136ced9465e8ff96399ffe7bd86","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"}],"refund_status":null,"scan_form":null,"selected_rate":null,"tracker":null,"to_address":{"id":"adr_30bfdd01a2ff4db3ae799fc414754717","object":"Address","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","name":"The 59 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"usps_zone":3,"return_address":{"id":"adr_420048a65d78477998bdda85f24f5af5","object":"Address","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","name":"YourCompany","company":"YourCompany","street1":"1725 60 | SLOUGH AVE.","street2":"","city":"SCRANTON","state":"PA","zip":"18540-0001","country":"US","phone":"15551238069","email":"info@yourcompany.example.com","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"buyer_address":{"id":"adr_30bfdd01a2ff4db3ae799fc414754717","object":"Address","created_at":"2017-11-10T17:55:38Z","updated_at":"2017-11-10T17:55:38Z","name":"The 61 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"forms":[],"fees":[],"id":"shp_aa0a3136ced9465e8ff96399ffe7bd86","object":"Shipment"}'} 62 | headers: 63 | cache-control: ['no-cache, no-store, must-revalidate, private'] 64 | content-length: ['4753'] 65 | content-type: [application/json; charset=utf-8] 66 | expires: ['0'] 67 | location: [/api/v2/shipments/shp_aa0a3136ced9465e8ff96399ffe7bd86] 68 | pragma: [no-cache] 69 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 70 | transfer-encoding: [chunked] 71 | x-backend: [easypost] 72 | x-content-type-options: [nosniff] 73 | x-ep-request-uuid: [872fcbb6-7123-4d66-a6e1-c9d987cfa784] 74 | x-frame-options: [SAMEORIGIN] 75 | x-node: [web11sj, 42420b5b70, easypost] 76 | x-proxied: [intlb2sj 81b12b6948, lb5sj 81b12b6948] 77 | x-runtime: ['0.255429'] 78 | x-xss-protection: [1; mode=block] 79 | status: {code: 201, message: Created} 80 | version: 1 81 | -------------------------------------------------------------------------------- /connector_easypost/tests/fixtures/cassettes/test_import_rate_partner.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: !!python/unicode 'address%5Bcity%5D=False&address%5Bcompany%5D=YourCompany&address%5Bemail%5D=False&address%5Bname%5D=The+White+House&address%5Bphone%5D=False&address%5Bstreet1%5D=1600+Pennsylvania&address%5Bstreet2%5D=False&address%5Bzip%5D=20500' 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-Length: ['229'] 9 | Content-type: [application/x-www-form-urlencoded] 10 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 11 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 12 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 13 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 14 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 15 | "client_version": "3.6.2"}'] 16 | method: POST 17 | uri: https://api.easypost.com/v2/addresses?verify[]=delivery 18 | response: 19 | body: {string: !!python/unicode '{"id":"adr_cf1a3f1e50484138a57523c3122f9c4c","object":"Address","created_at":"2017-11-10T17:59:16Z","updated_at":"2017-11-10T17:59:16Z","name":"THE 20 | WHITE HOUSE","company":"YOURCOMPANY","street1":"1600 PENNSYLVANIA AVE NW","street2":"FALSE","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"False","mode":"test","carrier_facility":null,"residential":true,"federal_tax_id":null,"state_tax_id":null,"verifications":{"delivery":{"success":true,"errors":[{"code":"E.SECONDARY_INFORMATION.MISSING","field":"street2","message":"Missing 21 | secondary information(Apt/Suite#)","suggestion":null}],"details":{"latitude":38.8987,"longitude":-77.0352,"time_zone":"America/New_York"}}}}'} 22 | headers: 23 | cache-control: ['no-cache, no-store, must-revalidate, private'] 24 | content-length: ['706'] 25 | content-type: [application/json; charset=utf-8] 26 | expires: ['0'] 27 | location: [/api/v2/addresses/adr_cf1a3f1e50484138a57523c3122f9c4c] 28 | pragma: [no-cache] 29 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 30 | transfer-encoding: [chunked] 31 | x-backend: [easypost] 32 | x-content-type-options: [nosniff] 33 | x-ep-request-uuid: [6991995b-2f19-4995-853d-d3456b45c242] 34 | x-frame-options: [SAMEORIGIN] 35 | x-node: [web5sj, 42420b5b70, easypost] 36 | x-proxied: [intlb2sj 81b12b6948, lb5sj 81b12b6948] 37 | x-runtime: ['0.151296'] 38 | x-xss-protection: [1; mode=block] 39 | status: {code: 201, message: Created} 40 | - request: 41 | body: !!python/unicode 'address%5Bcity%5D=False&address%5Bcompany%5D=YourCompany&address%5Bemail%5D=False&address%5Bname%5D=Twitter&address%5Bphone%5D=False&address%5Bstreet1%5D=1355+Market+Street+%23900&address%5Bstreet2%5D=False&address%5Bzip%5D=94103' 42 | headers: 43 | Accept: ['*/*'] 44 | Accept-Encoding: ['gzip, deflate'] 45 | Connection: [keep-alive] 46 | Content-Length: ['229'] 47 | Content-type: [application/x-www-form-urlencoded] 48 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 49 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 50 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 51 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 52 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 53 | "client_version": "3.6.2"}'] 54 | method: POST 55 | uri: https://api.easypost.com/v2/addresses?verify[]=delivery 56 | response: 57 | body: {string: !!python/unicode '{"id":"adr_92a9c24443cd405c866d8846c44f1ca1","object":"Address","created_at":"2017-11-10T17:59:16Z","updated_at":"2017-11-10T17:59:16Z","name":"TWITTER","company":"YOURCOMPANY","street1":"1355 58 | MARKET ST STE 900","street2":"FALSE","city":"SAN FRANCISCO","state":"CA","zip":"94103-1337","country":"US","phone":"","email":"False","mode":"test","carrier_facility":null,"residential":false,"federal_tax_id":null,"state_tax_id":null,"verifications":{"delivery":{"success":true,"errors":[],"details":{"latitude":37.77685,"longitude":-122.41687,"time_zone":"America/Los_Angeles"}}}}'} 59 | headers: 60 | cache-control: ['no-cache, no-store, must-revalidate, private'] 61 | content-length: ['574'] 62 | content-type: [application/json; charset=utf-8] 63 | expires: ['0'] 64 | location: [/api/v2/addresses/adr_92a9c24443cd405c866d8846c44f1ca1] 65 | pragma: [no-cache] 66 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 67 | transfer-encoding: [chunked] 68 | x-backend: [easypost] 69 | x-content-type-options: [nosniff] 70 | x-ep-request-uuid: [45b32ac4-346e-4e06-802b-1578d44971ee] 71 | x-frame-options: [SAMEORIGIN] 72 | x-node: [bigweb1sj, 42420b5b70, easypost] 73 | x-proxied: [intlb2sj 81b12b6948, lb5sj 81b12b6948] 74 | x-runtime: ['0.095088'] 75 | x-xss-protection: [1; mode=block] 76 | status: {code: 201, message: Created} 77 | - request: 78 | body: !!python/unicode 'parcel%5Bheight%5D=1.7&parcel%5Blength%5D=1.7&parcel%5Bpredefined_package%5D=SmallFlatRateBox&parcel%5Bweight%5D=50.0&parcel%5Bwidth%5D=5.4' 79 | headers: 80 | Accept: ['*/*'] 81 | Accept-Encoding: ['gzip, deflate'] 82 | Connection: [keep-alive] 83 | Content-Length: ['139'] 84 | Content-type: [application/x-www-form-urlencoded] 85 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 86 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 87 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 88 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 89 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 90 | "client_version": "3.6.2"}'] 91 | method: POST 92 | uri: https://api.easypost.com/v2/parcels 93 | response: 94 | body: {string: !!python/unicode '{"id":"prcl_0a265caa2c8b46a0a540a18fa1298bd1","object":"Parcel","created_at":"2017-11-10T17:59:16Z","updated_at":"2017-11-10T17:59:16Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":50.0,"mode":"test"}'} 95 | headers: 96 | cache-control: ['no-cache, no-store, must-revalidate, private'] 97 | content-length: ['242'] 98 | content-type: [application/json; charset=utf-8] 99 | expires: ['0'] 100 | location: [/api/v2/parcels/prcl_0a265caa2c8b46a0a540a18fa1298bd1] 101 | pragma: [no-cache] 102 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 103 | transfer-encoding: [chunked] 104 | x-backend: [easypost] 105 | x-content-type-options: [nosniff] 106 | x-ep-request-uuid: [91680554-1c6d-405a-94c8-305a63982bdb] 107 | x-frame-options: [SAMEORIGIN] 108 | x-node: [bigweb1sj, 42420b5b70, easypost] 109 | x-proxied: [intlb1sj 81b12b6948, lb5sj 81b12b6948] 110 | x-runtime: ['0.018379'] 111 | x-xss-protection: [1; mode=block] 112 | status: {code: 201, message: Created} 113 | - request: 114 | body: !!python/unicode 'shipment%5Bfrom_address%5D%5Bcity%5D=SAN+FRANCISCO&shipment%5Bfrom_address%5D%5Bcompany%5D=YourCompany&shipment%5Bfrom_address%5D%5Bcountry%5D=US&shipment%5Bfrom_address%5D%5Bemail%5D=&shipment%5Bfrom_address%5D%5Bname%5D=Twitter&shipment%5Bfrom_address%5D%5Bphone%5D=&shipment%5Bfrom_address%5D%5Bstate%5D=CA&shipment%5Bfrom_address%5D%5Bstreet1%5D=1355+MARKET+ST+STE+900&shipment%5Bfrom_address%5D%5Bstreet2%5D=&shipment%5Bfrom_address%5D%5Bzip%5D=94103-1337&shipment%5Bparcel%5D%5Bid%5D=prcl_0a265caa2c8b46a0a540a18fa1298bd1&shipment%5Bto_address%5D%5Bcity%5D=WASHINGTON&shipment%5Bto_address%5D%5Bcompany%5D=YourCompany&shipment%5Bto_address%5D%5Bcountry%5D=US&shipment%5Bto_address%5D%5Bemail%5D=&shipment%5Bto_address%5D%5Bname%5D=The+White+House&shipment%5Bto_address%5D%5Bphone%5D=&shipment%5Bto_address%5D%5Bstate%5D=DC&shipment%5Bto_address%5D%5Bstreet1%5D=1600+PENNSYLVANIA+AVE+NW&shipment%5Bto_address%5D%5Bstreet2%5D=&shipment%5Bto_address%5D%5Bzip%5D=20500-0003' 115 | headers: 116 | Accept: ['*/*'] 117 | Accept-Encoding: ['gzip, deflate'] 118 | Connection: [keep-alive] 119 | Content-Length: ['975'] 120 | Content-type: [application/x-www-form-urlencoded] 121 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 122 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 123 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 124 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 125 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 126 | "client_version": "3.6.2"}'] 127 | method: POST 128 | uri: https://api.easypost.com/v2/shipments 129 | response: 130 | body: {string: !!python/unicode '{"created_at":"2017-11-10T17:59:17Z","is_return":false,"messages":[],"mode":"test","options":{"currency":"USD","label_date":null,"date_advance":0},"reference":null,"status":"unknown","tracking_code":null,"updated_at":"2017-11-10T17:59:17Z","batch_id":null,"batch_status":null,"batch_message":null,"customs_info":null,"from_address":{"id":"adr_ca10b789501e469c9dec9ca5eee3f51b","object":"Address","created_at":"2017-11-10T17:59:17Z","updated_at":"2017-11-10T17:59:17Z","name":"Twitter","company":"YourCompany","street1":"1355 131 | MARKET ST STE 900","street2":"","city":"SAN FRANCISCO","state":"CA","zip":"94103-1337","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"insurance":null,"order_id":null,"parcel":{"id":"prcl_0a265caa2c8b46a0a540a18fa1298bd1","object":"Parcel","created_at":"2017-11-10T17:59:16Z","updated_at":"2017-11-10T17:59:16Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":50.0,"mode":"test"},"postage_label":null,"rates":[{"id":"rate_1e8d9d54bcfa49f19954e3fef62d873d","object":"Rate","created_at":"2017-11-10T17:59:17Z","updated_at":"2017-11-10T17:59:17Z","mode":"test","service":"Priority","carrier":"USPS","rate":"6.25","currency":"USD","retail_rate":"7.15","retail_currency":"USD","list_rate":"6.45","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_825f48139de74a249718e16b8f747293","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"}],"refund_status":null,"scan_form":null,"selected_rate":null,"tracker":null,"to_address":{"id":"adr_0efc7284420040d8a92ee2c0e1017c93","object":"Address","created_at":"2017-11-10T17:59:17Z","updated_at":"2017-11-10T17:59:17Z","name":"The 132 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"usps_zone":8,"return_address":{"id":"adr_ca10b789501e469c9dec9ca5eee3f51b","object":"Address","created_at":"2017-11-10T17:59:17Z","updated_at":"2017-11-10T17:59:17Z","name":"Twitter","company":"YourCompany","street1":"1355 133 | MARKET ST STE 900","street2":"","city":"SAN FRANCISCO","state":"CA","zip":"94103-1337","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"buyer_address":{"id":"adr_0efc7284420040d8a92ee2c0e1017c93","object":"Address","created_at":"2017-11-10T17:59:17Z","updated_at":"2017-11-10T17:59:17Z","name":"The 134 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"forms":[],"fees":[],"id":"shp_825f48139de74a249718e16b8f747293","object":"Shipment"}'} 135 | headers: 136 | cache-control: ['no-cache, no-store, must-revalidate, private'] 137 | content-length: ['3135'] 138 | content-type: [application/json; charset=utf-8] 139 | expires: ['0'] 140 | location: [/api/v2/shipments/shp_825f48139de74a249718e16b8f747293] 141 | pragma: [no-cache] 142 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 143 | transfer-encoding: [chunked] 144 | x-backend: [easypost] 145 | x-content-type-options: [nosniff] 146 | x-ep-request-uuid: [a17aa852-0b36-4e5d-a8d9-0a5c1574d45c] 147 | x-frame-options: [SAMEORIGIN] 148 | x-node: [web4sj, 42420b5b70, easypost] 149 | x-proxied: [intlb1sj 81b12b6948, lb5sj 81b12b6948] 150 | x-runtime: ['0.098091'] 151 | x-xss-protection: [1; mode=block] 152 | status: {code: 201, message: Created} 153 | version: 1 154 | -------------------------------------------------------------------------------- /connector_easypost/tests/fixtures/cassettes/test_rate_cancel.yaml: -------------------------------------------------------------------------------- 1 | interactions: 2 | - request: 3 | body: null 4 | headers: 5 | Accept: ['*/*'] 6 | Accept-Encoding: ['gzip, deflate'] 7 | Connection: [keep-alive] 8 | Content-type: [application/x-www-form-urlencoded] 9 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 10 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 11 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 12 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 13 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 14 | "client_version": "3.6.2"}'] 15 | method: GET 16 | uri: https://api.easypost.com/v2/shipments/shp_3889cfa2b4ba4c4d87633249ea9b9075 17 | response: 18 | body: {string: !!python/unicode '{"created_at":"2017-11-10T17:55:31Z","is_return":false,"messages":[],"mode":"test","options":{"currency":"USD","label_date":null,"date_advance":0},"reference":null,"status":"unknown","tracking_code":"9405536897846112508012","updated_at":"2017-11-10T17:55:32Z","batch_id":null,"batch_status":null,"batch_message":null,"customs_info":null,"from_address":{"id":"adr_d08fa7a02fd6415b8ef1ea8f4f96d0d2","object":"Address","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","name":"Twitter","company":"YourCompany","street1":"1355 19 | MARKET ST STE 900","street2":"","city":"SAN FRANCISCO","state":"CA","zip":"94103-1337","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"insurance":null,"order_id":null,"parcel":{"id":"prcl_b7795ac39c1f4e51ac4fe85c5641af76","object":"Parcel","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":50.0,"mode":"test"},"postage_label":{"object":"PostageLabel","id":"pl_3ab150b53cd34a1eb56196b79bab5716","created_at":"2017-11-10T17:55:32Z","updated_at":"2017-11-10T17:55:32Z","date_advance":0,"integrated_form":"none","label_date":"2017-11-10T17:55:32Z","label_resolution":300,"label_size":"4x6","label_type":"default","label_file_type":"image/png","label_url":"https://easypost-files.s3-us-west-2.amazonaws.com/files/postage_label/20171110/1f39da6eb816462fa4761c3966dc12b4.png","label_pdf_url":null,"label_zpl_url":null,"label_epl2_url":null,"label_file":null},"rates":[{"id":"rate_578988af7baa415488bc7d29eec8d729","object":"Rate","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","mode":"test","service":"Priority","carrier":"USPS","rate":"6.25","currency":"USD","retail_rate":"7.15","retail_currency":"USD","list_rate":"6.45","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_3889cfa2b4ba4c4d87633249ea9b9075","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"}],"refund_status":null,"scan_form":null,"selected_rate":{"id":"rate_578988af7baa415488bc7d29eec8d729","object":"Rate","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","mode":"test","service":"Priority","carrier":"USPS","rate":"6.25","currency":"USD","retail_rate":"7.15","retail_currency":"USD","list_rate":"6.45","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_3889cfa2b4ba4c4d87633249ea9b9075","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"},"tracker":{"id":"trk_c87b533e802840129e822e7119101b90","object":"Tracker","mode":"test","tracking_code":"9405536897846112508012","status":"unknown","status_detail":"unknown","created_at":"2017-11-10T17:55:32Z","updated_at":"2017-11-10T17:55:32Z","signed_by":null,"weight":null,"est_delivery_date":null,"shipment_id":"shp_3889cfa2b4ba4c4d87633249ea9b9075","carrier":"USPS","tracking_details":[],"fees":[],"carrier_detail":null,"public_url":"https://track.easypost.com/djE6dHJrX2M4N2I1MzNlODAyODQwMTI5ZTgyMmU3MTE5MTAxYjkw"},"to_address":{"id":"adr_2d766c8cd738474e97fb8a75a798a118","object":"Address","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","name":"The 20 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{"zip4":{"success":true,"errors":[],"details":null}}},"usps_zone":8,"return_address":{"id":"adr_d08fa7a02fd6415b8ef1ea8f4f96d0d2","object":"Address","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","name":"Twitter","company":"YourCompany","street1":"1355 21 | MARKET ST STE 900","street2":"","city":"SAN FRANCISCO","state":"CA","zip":"94103-1337","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"buyer_address":{"id":"adr_2d766c8cd738474e97fb8a75a798a118","object":"Address","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","name":"The 22 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{"zip4":{"success":true,"errors":[],"details":null}}},"forms":[],"fees":[{"object":"Fee","type":"LabelFee","amount":"0.03000","charged":true,"refunded":false},{"object":"Fee","type":"PostageFee","amount":"6.25000","charged":true,"refunded":false}],"id":"shp_3889cfa2b4ba4c4d87633249ea9b9075","object":"Shipment"}'} 23 | headers: 24 | cache-control: ['no-cache, no-store, must-revalidate, private'] 25 | content-length: ['4961'] 26 | content-type: [application/json; charset=utf-8] 27 | expires: ['0'] 28 | pragma: [no-cache] 29 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 30 | transfer-encoding: [chunked] 31 | x-backend: [easypost] 32 | x-content-type-options: [nosniff] 33 | x-ep-request-uuid: [39fc5fc9-5846-4388-9e1b-b4db3a596ab4] 34 | x-frame-options: [SAMEORIGIN] 35 | x-node: [web11sj, 42420b5b70, easypost] 36 | x-proxied: [intlb1sj 81b12b6948, lb5sj 81b12b6948] 37 | x-runtime: ['0.123538'] 38 | x-xss-protection: [1; mode=block] 39 | status: {code: 200, message: OK} 40 | - request: 41 | body: null 42 | headers: 43 | Accept: ['*/*'] 44 | Accept-Encoding: ['gzip, deflate'] 45 | Connection: [keep-alive] 46 | Content-type: [application/x-www-form-urlencoded] 47 | User-Agent: [EasyPost/v2 PythonClient/3.6.2] 48 | X-Client-User-Agent: ['{"lang": "python", "publisher": "easypost", "uname": 49 | "Linux odoo-dev-10 4.4.0-97-generic #120-Ubuntu SMP Tue Sep 19 17:28:18 50 | UTC 2017 x86_64 x86_64", "request_lib": "requests", "lang_version": "2.7.12", 51 | "openssl_version": "OpenSSL 1.0.2g 1 Mar 2016", "platform": "Linux-4.4.0-97-generic-x86_64-with-Ubuntu-16.04-xenial", 52 | "client_version": "3.6.2"}'] 53 | method: GET 54 | uri: https://api.easypost.com/v2/shipments/shp_3889cfa2b4ba4c4d87633249ea9b9075/refund?rate%5Bid%5D=rate_578988af7baa415488bc7d29eec8d729 55 | response: 56 | body: {string: !!python/unicode '{"created_at":"2017-11-10T17:55:31Z","is_return":false,"messages":[],"mode":"test","options":{"currency":"USD","label_date":null,"date_advance":0},"reference":null,"status":"unknown","tracking_code":"9405536897846112508012","updated_at":"2017-11-10T17:55:34Z","batch_id":null,"batch_status":null,"batch_message":null,"customs_info":null,"from_address":{"id":"adr_d08fa7a02fd6415b8ef1ea8f4f96d0d2","object":"Address","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","name":"Twitter","company":"YourCompany","street1":"1355 57 | MARKET ST STE 900","street2":"","city":"SAN FRANCISCO","state":"CA","zip":"94103-1337","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"insurance":null,"order_id":null,"parcel":{"id":"prcl_b7795ac39c1f4e51ac4fe85c5641af76","object":"Parcel","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","length":1.7,"width":5.4,"height":1.7,"predefined_package":"SmallFlatRateBox","weight":50.0,"mode":"test"},"postage_label":{"object":"PostageLabel","id":"pl_3ab150b53cd34a1eb56196b79bab5716","created_at":"2017-11-10T17:55:32Z","updated_at":"2017-11-10T17:55:32Z","date_advance":0,"integrated_form":"none","label_date":"2017-11-10T17:55:32Z","label_resolution":300,"label_size":"4x6","label_type":"default","label_file_type":"image/png","label_url":"https://easypost-files.s3-us-west-2.amazonaws.com/files/postage_label/20171110/1f39da6eb816462fa4761c3966dc12b4.png","label_pdf_url":null,"label_zpl_url":null,"label_epl2_url":null,"label_file":null},"rates":[{"id":"rate_578988af7baa415488bc7d29eec8d729","object":"Rate","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","mode":"test","service":"Priority","carrier":"USPS","rate":"6.25","currency":"USD","retail_rate":"7.15","retail_currency":"USD","list_rate":"6.45","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_3889cfa2b4ba4c4d87633249ea9b9075","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"}],"refund_status":"submitted","scan_form":null,"selected_rate":{"id":"rate_578988af7baa415488bc7d29eec8d729","object":"Rate","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","mode":"test","service":"Priority","carrier":"USPS","rate":"6.25","currency":"USD","retail_rate":"7.15","retail_currency":"USD","list_rate":"6.45","list_currency":"USD","delivery_days":2,"delivery_date":null,"delivery_date_guaranteed":false,"est_delivery_days":2,"shipment_id":"shp_3889cfa2b4ba4c4d87633249ea9b9075","carrier_account_id":"ca_af8c059654f5425295d1161dfa0f0290"},"tracker":{"id":"trk_c87b533e802840129e822e7119101b90","object":"Tracker","mode":"test","tracking_code":"9405536897846112508012","status":"unknown","status_detail":"unknown","created_at":"2017-11-10T17:55:32Z","updated_at":"2017-11-10T17:55:32Z","signed_by":null,"weight":null,"est_delivery_date":null,"shipment_id":"shp_3889cfa2b4ba4c4d87633249ea9b9075","carrier":"USPS","tracking_details":[],"fees":[],"carrier_detail":null,"public_url":"https://track.easypost.com/djE6dHJrX2M4N2I1MzNlODAyODQwMTI5ZTgyMmU3MTE5MTAxYjkw"},"to_address":{"id":"adr_2d766c8cd738474e97fb8a75a798a118","object":"Address","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","name":"The 58 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{"zip4":{"success":true,"errors":[],"details":null}}},"usps_zone":8,"return_address":{"id":"adr_d08fa7a02fd6415b8ef1ea8f4f96d0d2","object":"Address","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","name":"Twitter","company":"YourCompany","street1":"1355 59 | MARKET ST STE 900","street2":"","city":"SAN FRANCISCO","state":"CA","zip":"94103-1337","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{}},"buyer_address":{"id":"adr_2d766c8cd738474e97fb8a75a798a118","object":"Address","created_at":"2017-11-10T17:55:31Z","updated_at":"2017-11-10T17:55:31Z","name":"The 60 | White House","company":"YourCompany","street1":"1600 PENNSYLVANIA AVE NW","street2":"","city":"WASHINGTON","state":"DC","zip":"20500-0003","country":"US","phone":"","email":"","mode":"test","carrier_facility":null,"residential":null,"federal_tax_id":null,"state_tax_id":null,"verifications":{"zip4":{"success":true,"errors":[],"details":null}}},"forms":[],"fees":[{"object":"Fee","type":"LabelFee","amount":"0.03000","charged":true,"refunded":false},{"object":"Fee","type":"PostageFee","amount":"6.25000","charged":true,"refunded":false}],"id":"shp_3889cfa2b4ba4c4d87633249ea9b9075","object":"Shipment"}'} 61 | headers: 62 | cache-control: ['no-cache, no-store, must-revalidate, private'] 63 | content-length: ['4968'] 64 | content-type: [application/json; charset=utf-8] 65 | expires: ['0'] 66 | pragma: [no-cache] 67 | strict-transport-security: [max-age=15768000; includeSubDomains; preload] 68 | transfer-encoding: [chunked] 69 | x-backend: [easypost] 70 | x-content-type-options: [nosniff] 71 | x-ep-request-uuid: [05f80ae8-9039-4715-9732-df5a33c57984] 72 | x-frame-options: [SAMEORIGIN] 73 | x-node: [bigweb1sj, 42420b5b70, easypost] 74 | x-proxied: [intlb2sj 81b12b6948, lb5sj 81b12b6948] 75 | x-runtime: ['0.214019'] 76 | x-xss-protection: [1; mode=block] 77 | status: {code: 200, message: OK} 78 | version: 1 79 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_common_delivery_carrier.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from .common import EasyPostSyncTestCase, recorder 6 | 7 | 8 | class TestDeliveryCarrier(EasyPostSyncTestCase): 9 | 10 | def setUp(self): 11 | super(TestDeliveryCarrier, self).setUp() 12 | self.model = self.env['delivery.carrier'] 13 | 14 | @recorder.use_cassette 15 | def test_common_carrier_get_shipping_price_from_so(self): 16 | """It should return the proper shipping rate from the sale.""" 17 | 18 | sale = self._create_sale() 19 | sale.odoo_id.action_confirm() 20 | shipment = sale.picking_ids[0] 21 | 22 | rate = shipment.dispatch_rate_ids[0] 23 | 24 | self.assertEqual( 25 | rate.service_id.easypost_get_shipping_price_from_so(sale), 26 | [rate.rate], 27 | ) 28 | 29 | @recorder.use_cassette 30 | def test_common_carrier_get_shipping_label_for_rate(self): 31 | """It should return the shipping label for the rate.""" 32 | rate = self._create_shipment().dispatch_rate_ids[0] 33 | self.assertFalse(self.model._get_shipping_label_for_rate(rate)) 34 | rate.buy() 35 | self.assertTrue(self.model._get_shipping_label_for_rate(rate)) 36 | return rate 37 | 38 | @recorder.use_cassette 39 | def test_common_carrier_send_shipping(self): 40 | """It should purchase the proper rate.""" 41 | shipment = self._create_shipment() 42 | rate = shipment.dispatch_rate_ids[0] 43 | shipment_data = rate.service_id.easypost_send_shipping(shipment) 44 | self.assertEqual(shipment_data[0]['exact_price'], rate.rate) 45 | return rate 46 | 47 | @recorder.use_cassette 48 | def test_common_carrier_get_tracking_link(self): 49 | """It should return the tracking URLs for the rate.""" 50 | rate = self._create_shipment().dispatch_rate_ids[0] 51 | method = rate.service_id.easypost_get_tracking_link 52 | self.assertFalse(method(rate.picking_id)) 53 | rate.buy() 54 | self.assertTrue(method(rate.picking_id)) 55 | 56 | @recorder.use_cassette 57 | def test_common_carrier_cancel_shipment(self): 58 | """It should cancel the shipment on Easypost.""" 59 | rate = self._create_shipment().dispatch_rate_ids[0] 60 | rate.buy() 61 | self.assertFalse(rate.picking_id.easypost_bind_ids.refund_status) 62 | rate.cancel() 63 | self.assertTrue(rate.picking_id.easypost_bind_ids.refund_status) 64 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_common_easypost_backend.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.exceptions import ValidationError 6 | 7 | from .common import EasyPostSyncTestCase 8 | 9 | 10 | model = 'odoo.addons.connector_easypost.models.easypost_backend' 11 | 12 | 13 | class TestEasypostBackend(EasyPostSyncTestCase): 14 | 15 | def setUp(self): 16 | super(TestEasypostBackend, self).setUp() 17 | 18 | def test__check_default_for_company_raises_validation_error(self): 19 | """ Test _check_default_for_company raises `ValidationError` 20 | when creating a duplicate backend for a company """ 21 | with self.assertRaises(ValidationError): 22 | self.env['easypost.backend'].create({ 23 | 'name': 'Test', 24 | 'version': 'v2', 25 | 'api_key': 'DUMMY', 26 | }) 27 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_common_rate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) 4 | 5 | 6 | from .common import EasyPostSyncTestCase, recorder 7 | 8 | 9 | class TestCommonRate(EasyPostSyncTestCase): 10 | 11 | @recorder.use_cassette 12 | def test_rate_buy(self): 13 | """It should purchase the rate.""" 14 | shipment = self._create_shipment() 15 | self.assertFalse(shipment.shipping_label_ids) 16 | shipment.dispatch_rate_ids[0].buy() 17 | self.assertTrue(shipment.shipping_label_ids) 18 | return shipment 19 | 20 | @recorder.use_cassette 21 | def test_rate_cancel(self): 22 | """It should cancel the rate.""" 23 | shipment = self.test_rate_buy() 24 | self.assertFalse(shipment.refund_status) 25 | shipment.dispatch_rate_ids[0].cancel() 26 | self.assertTrue(shipment.refund_status) 27 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_common_res_company.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) 4 | 5 | from .common import EasyPostSyncTestCase 6 | 7 | 8 | class TestCommonResCompany(EasyPostSyncTestCase): 9 | 10 | def setUp(self): 11 | super(TestCommonResCompany, self).setUp() 12 | self.model = self.env['res.company'] 13 | self.record = self.env.user.company_id 14 | 15 | def test_compute_easypost_backend_id(self): 16 | """It should have the backend as the default for the company.""" 17 | self.assertEqual(self.record.easypost_backend_id, self.backend) 18 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_export_parcel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) 4 | 5 | 6 | from .common import EasyPostSyncTestCase, recorder 7 | 8 | 9 | class TestExportParcel(EasyPostSyncTestCase): 10 | 11 | def setUp(self): 12 | super(TestExportParcel, self).setUp() 13 | self.model = self.env['easypost.parcel'] 14 | 15 | @recorder.use_cassette 16 | def test_export_parcel_predefined_package(self): 17 | """It should pass the predefined package information when applicable. 18 | """ 19 | parcel = self._create_parcel() 20 | external = self._get_external(parcel.external_id) 21 | self.assertEqual(external.predefined_package, 22 | self.record.packaging_id.shipper_package_code) 23 | 24 | @recorder.use_cassette 25 | def test_export_parcel_shipping_weight(self): 26 | """It should export the parcel using shipping weight when avail.""" 27 | parcel = self._create_parcel() 28 | external = self._get_external(parcel.external_id) 29 | self.assertTrue(external) 30 | self.assertEqual(external.weight, self.record.shipping_weight) 31 | 32 | @recorder.use_cassette 33 | def test_export_parcel_total_weight(self): 34 | """It should export the parcel using calculated weight when no manual. 35 | """ 36 | vals = {'quant_ids': [(6, 0, self._create_quant().ids)], 37 | 'shipping_weight': False} 38 | parcel = self._create_parcel(vals=vals) 39 | external = self._get_external(parcel.external_id) 40 | self.assertTrue(external) 41 | self.assertEqual(external.weight, self.record.total_weight) 42 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_export_sale.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) 4 | 5 | 6 | from .common import EasyPostSyncTestCase, recorder 7 | 8 | 9 | class TestExportSale(EasyPostSyncTestCase): 10 | 11 | def setUp(self): 12 | super(TestExportSale, self).setUp() 13 | self.model = self.env['easypost.sale'] 14 | 15 | @recorder.use_cassette 16 | def test_export_sale_imports_rates(self): 17 | """It should import the rates provided when the sale was exported. 18 | """ 19 | sale = self._create_sale() 20 | self.assertTrue(sale.carrier_rate_ids) 21 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_export_shipment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) 4 | 5 | 6 | from .common import EasyPostSyncTestCase, recorder 7 | 8 | 9 | class TestExportShipment(EasyPostSyncTestCase): 10 | 11 | def setUp(self): 12 | super(TestExportShipment, self).setUp() 13 | self.model = self.env['easypost.shipment'] 14 | 15 | @recorder.use_cassette 16 | def test_export_shipment_basic(self): 17 | """It should perform a basic shipment export.""" 18 | shipment = self._create_shipment() 19 | self.assertTrue(self._get_external(shipment.external_id)) 20 | 21 | @recorder.use_cassette 22 | def test_export_shipment_imports_rates(self): 23 | """It should import the rates provided when the shipment was exported. 24 | """ 25 | shipment = self._create_shipment(export=False) 26 | self.assertFalse(shipment.dispatch_rate_ids) 27 | shipment.export_record() 28 | self.assertTrue(shipment.dispatch_rate_ids) 29 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_import_address.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) 4 | 5 | from .common import EasyPostSyncTestCase, recorder 6 | 7 | 8 | class TestImportAddress(EasyPostSyncTestCase): 9 | 10 | def setUp(self): 11 | super(TestImportAddress, self).setUp() 12 | self.model = self.env['easypost.address'] 13 | self.partner = self._create_partner() 14 | self.record = self.model.create({ 15 | 'partner_id': self.partner.id, 16 | }) 17 | self.validated = { 18 | 'street': u'1600 PENNSYLVANIA AVE NW', 19 | 'street2': False, 20 | 'city': u'WASHINGTON', 21 | 'state_id': self.env.ref('base.state_us_9').id, 22 | 'zip': u'20500-0003', 23 | 'country_id': self.env.ref('base.us').id, 24 | 'is_valid': True, 25 | 'latitude': 38.8987, 26 | 'longitude': -77.0352, 27 | 'validation_messages': 28 | u'Missing secondary information(Apt/Suite#)', 29 | } 30 | 31 | @recorder.use_cassette 32 | def test_backend_easypost_get_address(self): 33 | """It should return the validated address.""" 34 | res = self.backend.easypost_get_address(self.partner) 35 | self.assertDictEqual(res, self.validated) 36 | -------------------------------------------------------------------------------- /connector_easypost/tests/test_import_rate.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) 4 | 5 | from .common import EasyPostSyncTestCase, recorder 6 | 7 | 8 | class TestImportRate(EasyPostSyncTestCase): 9 | 10 | def setUp(self): 11 | super(TestImportRate, self).setUp() 12 | self.model = self.env['easypost.rate'] 13 | 14 | @recorder.use_cassette 15 | def test_import_rate_partner(self): 16 | """It should create a new delivery partner.""" 17 | domain = [('is_carrier', '=', True)] 18 | self.env['res.partner'].search(domain).unlink() 19 | self._create_shipment() 20 | self.assertTrue(self.env['res.partner'].search_count(domain)) 21 | 22 | @recorder.use_cassette 23 | def test_import_rate_service_new(self): 24 | """It should create a new delivery service.""" 25 | domain = [('delivery_type', '=', 'easypost')] 26 | self.env['delivery.carrier'].search(domain).unlink() 27 | self._create_shipment() 28 | carrier_count = self.env['delivery.carrier'].search_count(domain) 29 | self.assertTrue(carrier_count) 30 | return carrier_count 31 | 32 | @recorder.use_cassette 33 | def test_import_rate_service_existing(self): 34 | """It should use an existing service if it matches the current.""" 35 | existing_count = self.test_import_rate_service_new() 36 | self._create_shipment() 37 | self.assertEqual( 38 | existing_count, 39 | self.env['delivery.carrier'].search_count([ 40 | ('delivery_type', '=', 'easypost'), 41 | ]), 42 | ) 43 | -------------------------------------------------------------------------------- /connector_easypost/views/connector_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 15 | 16 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /connector_easypost/views/delivery_carrier_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | delivery.carrier.form 10 | delivery.carrier 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /connector_easypost/views/easypost_backend_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | easypost.backend.form 12 | easypost.backend 13 | 14 |
15 | 16 | 31 |
32 |
33 |
34 | 35 | 36 | easypost.backend.tree 37 | easypost.backend 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | EasyPost Backends 49 | easypost.backend 50 | form 51 | tree,form 52 | 53 | 54 | 55 |
56 | -------------------------------------------------------------------------------- /connector_easypost_tracker/README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://img.shields.io/badge/license-AGPL--3-blue.svg 2 | :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html 3 | :alt: License: AGPL-3 4 | 5 | ========================== 6 | EasyPost Connector Tracker 7 | ========================== 8 | 9 | This module provides web hooks to allow EasyPost to update package tracking 10 | status in Odoo. 11 | 12 | .. image:: static/description/screenshot_1.png?raw=true 13 | :alt: Tracking Data in Picking 14 | 15 | .. image:: static/description/screenshot_2.png?raw=true 16 | :alt: Tracking Events and Locations 17 | 18 | 19 | Installation 20 | ============ 21 | 22 | To install this module, you need to: 23 | 24 | * Install connector_easypost and make sure it's setup 25 | 26 | 27 | Configuration 28 | ============= 29 | 30 | None Necessary 31 | 32 | Usage 33 | ===== 34 | 35 | 36 | Known Issues / Roadmap 37 | ====================== 38 | 39 | None 40 | 41 | Bug Tracker 42 | =========== 43 | 44 | Bugs are tracked on `GitHub Issues 45 | `_. 46 | In case of trouble, please check there if your issue has already been reported. 47 | If you spotted it first, please help us smash it by providing a detailed and 48 | welcomed feedback. 49 | 50 | Credits 51 | ======= 52 | 53 | Images 54 | ------ 55 | 56 | * LasLabs: `Icon `_. 57 | 58 | Contributors 59 | ------------ 60 | 61 | * Ted Salmon 62 | 63 | Maintainer 64 | ---------- 65 | 66 | .. image:: https://laslabs.com/logo.png 67 | :alt: LasLabs Inc. 68 | :target: https://laslabs.com 69 | 70 | This module is maintained by LasLabs Inc. 71 | -------------------------------------------------------------------------------- /connector_easypost_tracker/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import controllers 6 | from . import models 7 | from . import unit 8 | -------------------------------------------------------------------------------- /connector_easypost_tracker/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | { 6 | 'name': 'EastPost Connector Tracker', 7 | 'description': 'Provides EasyPost Tracking Info', 8 | 'version': '10.0.1.0.0', 9 | 'category': 'Connector, Stock', 10 | 'author': "LasLabs", 11 | 'license': 'AGPL-3', 12 | 'website': 'https://laslabs.com', 13 | 'depends': [ 14 | 'connector_easypost', 15 | 'stock_picking_tracking', 16 | ], 17 | 'data': [], 18 | 'installable': False, 19 | 'application': False, 20 | } 21 | -------------------------------------------------------------------------------- /connector_easypost_tracker/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import main 6 | -------------------------------------------------------------------------------- /connector_easypost_tracker/controllers/main.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from json import loads, dumps 6 | from odoo import http, SUPERUSER_ID 7 | from odoo.http import request 8 | from odoo.addons.connector_easypost.unit.import_synchronizer import ( 9 | create_connector_session, 10 | import_data, 11 | ) 12 | 13 | 14 | class EasypostWebhookController(http.Controller): 15 | 16 | EVENTS = {'tracker.updated': 'easypost.shipment.tracking.group'} 17 | 18 | def _get_backend(self, env): 19 | return env['easypost.backend'].sudo().search([ 20 | ('is_default', '=', True), 21 | ]) 22 | 23 | @http.route([ 24 | '/connector_easypost_tracker/webhook', 25 | ], type='http', auth='none', csrf=False) 26 | def easypost_webhook(self): 27 | """ Handle requests from the EasyPost Webhook """ 28 | req = loads(request.httprequest.data) 29 | model = self.EVENTS.get(req.get('description')) 30 | if model: 31 | # We need to escalate to SUPERUSER in order to 32 | # access protected models. Open to suggestions here 33 | request.env.uid = SUPERUSER_ID 34 | backend = self._get_backend(request.env) 35 | session = create_connector_session(request.env, model, backend.id) 36 | import_data.delay(session, model, backend.id, 37 | dumps(req.get('result'))) 38 | return 'ok' 39 | -------------------------------------------------------------------------------- /connector_easypost_tracker/models/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import stock_picking_tracking_event 6 | from . import stock_picking_tracking_group 7 | from . import stock_picking_tracking_location 8 | -------------------------------------------------------------------------------- /connector_easypost_tracker/models/stock_picking_tracking_event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import logging 6 | from odoo import fields, models 7 | from odoo.addons.connector.unit.mapper import ( 8 | mapping, 9 | only_create, 10 | ) 11 | from odoo.addons.connector_easypost.backend import easypost 12 | from odoo.addons.connector_easypost.unit.backend_adapter import ( 13 | EasypostCRUDAdapter, 14 | ) 15 | from odoo.addons.connector_easypost.unit.import_synchronizer import ( 16 | EasypostImporter, 17 | ) 18 | from odoo.addons.connector_easypost.unit.mapper import ( 19 | EasypostImportMapper, 20 | eval_false, 21 | ) 22 | from .stock_picking_tracking_location import ( 23 | StockPickingTrackingLocationImporter, 24 | ) 25 | 26 | _logger = logging.getLogger(__name__) 27 | 28 | 29 | class EasypostShipmentTrackingEvent(models.Model): 30 | """ Binding Model for the Easypost StockPickingTrackingEvent""" 31 | _name = 'easypost.shipment.tracking.event' 32 | _inherit = 'easypost.binding' 33 | _inherits = {'stock.picking.tracking.event': 'odoo_id'} 34 | _description = 'Easypost StockPickingTrackingEvent' 35 | _easypost_model = 'Tracker' 36 | 37 | odoo_id = fields.Many2one( 38 | comodel_name='stock.picking.tracking.event', 39 | string='Stock Picking Tracking Event', 40 | required=True, 41 | ondelete='cascade', 42 | ) 43 | 44 | 45 | class StockPickingTrackingEvent(models.Model): 46 | """ Adds the ``one2many`` relation to the Easypost bindings 47 | (``easypost_bind_ids``) 48 | """ 49 | _inherit = 'stock.picking.tracking.event' 50 | 51 | easypost_bind_ids = fields.One2many( 52 | comodel_name='easypost.shipment.tracking.event', 53 | inverse_name='odoo_id', 54 | string='Easypost Bindings', 55 | ) 56 | 57 | 58 | @easypost 59 | class EasypostShipmentTrackingEventAdapter(EasypostCRUDAdapter): 60 | """ Backend Adapter for the Easypost EasypostShipment """ 61 | _model_name = 'easypost.shipment.tracking.event' 62 | 63 | 64 | @easypost 65 | class StockPickingTrackingEventImportMapper(EasypostImportMapper): 66 | _model_name = 'easypost.shipment.tracking.event' 67 | 68 | direct = [ 69 | (eval_false('message'), 'message'), 70 | (eval_false('status'), 'state'), 71 | (eval_false('source'), 'source'), 72 | (eval_false('datetime'), 'date_created'), 73 | ] 74 | 75 | @mapping 76 | @only_create 77 | def group_id(self, record): 78 | """ `group_id` is not present in the event object and should be 79 | injected by the caller """ 80 | return {'group_id': record.group.id} 81 | 82 | @mapping 83 | @only_create 84 | def location_id(self, record): 85 | return {'location_id': record.location_id} 86 | 87 | 88 | @easypost 89 | class StockPickingTrackingEventImporter(EasypostImporter): 90 | _model_name = ['easypost.shipment.tracking.event'] 91 | _base_mapper = StockPickingTrackingEventImportMapper 92 | _id_prefix = 'det' 93 | _hashable_attrs = ('message', 'status', 'source', 'datetime') 94 | 95 | def _before_import(self): 96 | """ Prior to import, export the TrackingLocation and add the resulting 97 | `location_id` to our easypost_record for mapping """ 98 | importer = self.unit_for( 99 | StockPickingTrackingLocationImporter, 100 | model='easypost.shipment.tracking.location' 101 | ) 102 | importer.easypost_record = self.easypost_record.tracking_location 103 | importer.default_easypost_values( 104 | self.easypost_record.group.picking_id.company_id 105 | ) 106 | ret = importer.run() 107 | self.easypost_record.location_id = ret.odoo_id.id 108 | -------------------------------------------------------------------------------- /connector_easypost_tracker/models/stock_picking_tracking_group.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import logging 6 | from odoo import fields, models 7 | from odoo.addons.connector.unit.mapper import ( 8 | mapping, 9 | only_create, 10 | ) 11 | from odoo.addons.connector_easypost.backend import easypost 12 | from odoo.addons.connector_easypost.unit.backend_adapter import ( 13 | EasypostCRUDAdapter, 14 | ) 15 | from odoo.addons.connector_easypost.unit.import_synchronizer import ( 16 | EasypostImporter, 17 | ) 18 | from odoo.addons.connector_easypost.unit.mapper import ( 19 | EasypostImportMapper, 20 | eval_false, 21 | ) 22 | from .stock_picking_tracking_event import StockPickingTrackingEventImporter 23 | 24 | 25 | _logger = logging.getLogger(__name__) 26 | 27 | 28 | class EasypostShipmentTrackingGroup(models.Model): 29 | """ Binding Model for the Easypost StockPickingTrackingGroup""" 30 | _name = 'easypost.shipment.tracking.group' 31 | _inherit = 'easypost.binding' 32 | _inherits = {'stock.picking.tracking.group': 'odoo_id'} 33 | _description = 'Easypost StockPickingTrackingGroup' 34 | _easypost_model = 'Tracker' 35 | 36 | odoo_id = fields.Many2one( 37 | comodel_name='stock.picking.tracking.group', 38 | string='Stock Picking Tracking Event', 39 | required=True, 40 | ondelete='cascade', 41 | ) 42 | 43 | 44 | class StockPickingTrackingGroup(models.Model): 45 | """ Adds the ``one2many`` relation to the Easypost bindings 46 | (``easypost_bind_ids``) 47 | """ 48 | _inherit = 'stock.picking.tracking.group' 49 | 50 | easypost_bind_ids = fields.One2many( 51 | comodel_name='easypost.shipment.tracking.group', 52 | inverse_name='odoo_id', 53 | string='Easypost Bindings', 54 | ) 55 | 56 | 57 | @easypost 58 | class EasypostShipmentAdapter(EasypostCRUDAdapter): 59 | """ Backend Adapter for the Easypost EasypostShipment """ 60 | _model_name = 'easypost.shipment.tracking.group' 61 | 62 | 63 | @easypost 64 | class StockPickingTrackingGroupImportMapper(EasypostImportMapper): 65 | _model_name = 'easypost.shipment.tracking.group' 66 | 67 | direct = [ 68 | (eval_false('tracking_code'), 'ref'), 69 | ] 70 | 71 | @mapping 72 | @only_create 73 | def picking_id(self, record): 74 | pickings = self.env['easypost.shipment'].search([ 75 | ('external_id', '=', record.shipment_id), 76 | ]) 77 | if pickings: 78 | return {'picking_id': pickings.odoo_id.id} 79 | 80 | 81 | @easypost 82 | class StockPickingTrackingGroupImporter(EasypostImporter): 83 | _model_name = ['easypost.shipment.tracking.group'] 84 | _base_mapper = StockPickingTrackingGroupImportMapper 85 | 86 | def _after_import(self, record): 87 | """ Immediately Import Events """ 88 | importer = self.unit_for(StockPickingTrackingEventImporter, 89 | model='easypost.shipment.tracking.event') 90 | for event in self.easypost_record.tracking_details: 91 | event.group = record.odoo_id 92 | importer.easypost_record = event 93 | importer.run() 94 | -------------------------------------------------------------------------------- /connector_easypost_tracker/models/stock_picking_tracking_location.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | import logging 6 | from odoo import fields, models 7 | from odoo.addons.connector.unit.mapper import ( 8 | mapping, 9 | only_create, 10 | ) 11 | from odoo.addons.connector_easypost.backend import easypost 12 | from odoo.addons.connector_easypost.unit.backend_adapter import ( 13 | EasypostCRUDAdapter, 14 | ) 15 | from odoo.addons.connector_easypost.unit.import_synchronizer import ( 16 | EasypostImporter, 17 | ) 18 | from odoo.addons.connector_easypost.unit.mapper import ( 19 | EasypostImportMapper, 20 | eval_false, 21 | ) 22 | 23 | 24 | _logger = logging.getLogger(__name__) 25 | 26 | 27 | class EasypostShipmentTrackingLocation(models.Model): 28 | """ Binding Model for the Easypost StockPickingTrackingLocation""" 29 | _name = 'easypost.shipment.tracking.location' 30 | _inherit = 'easypost.binding' 31 | _inherits = {'stock.picking.tracking.location': 'odoo_id'} 32 | _description = 'Easypost StockPickingTrackingLocation' 33 | _easypost_model = 'Tracker' 34 | 35 | odoo_id = fields.Many2one( 36 | comodel_name='stock.picking.tracking.location', 37 | string='Stock Picking Tracking Location', 38 | required=True, 39 | ondelete='cascade', 40 | ) 41 | 42 | 43 | class StockPickingTrackingLocation(models.Model): 44 | """ Adds the ``one2many`` relation to the Easypost bindings 45 | (``easypost_bind_ids``) 46 | """ 47 | _inherit = 'stock.picking.tracking.location' 48 | 49 | easypost_bind_ids = fields.One2many( 50 | comodel_name='easypost.shipment.tracking.location', 51 | inverse_name='odoo_id', 52 | string='Easypost Bindings', 53 | ) 54 | 55 | 56 | @easypost 57 | class EasypostShipmentTrackingLocationAdapter(EasypostCRUDAdapter): 58 | """ Backend Adapter for the Easypost 59 | EasypostShipmentTrackingLocation """ 60 | _model_name = 'easypost.shipment.tracking.location' 61 | 62 | 63 | @easypost 64 | class StockPickingTrackingLocationImportMapper(EasypostImportMapper): 65 | _model_name = 'easypost.shipment.tracking.location' 66 | 67 | direct = [ 68 | (eval_false('city'), 'city'), 69 | (eval_false('zip'), 'zip'), 70 | ] 71 | 72 | @mapping 73 | @only_create 74 | def odoo_id(self, record): 75 | rec = self.env['easypost.shipment.tracking.location'].search([ 76 | ('external_id', '=', record.id), 77 | ('backend_id', '=', self.backend_record.id), 78 | ]) 79 | if rec: 80 | return {'odoo_id': rec.odoo_id.id} 81 | 82 | @mapping 83 | @only_create 84 | def country_id(self, record): 85 | """ Load the country ID from the state code if it's not set. 86 | This is because the country code is not always included in 87 | `TrackingLocation` objects but the state is """ 88 | if record.country: 89 | country = self.env['res.country'].search([ 90 | ('code', '=', record.country), 91 | ]) 92 | return {'country_id': country.id} 93 | else: 94 | state = self.env['res.country.state'].search([ 95 | ('code', '=', record.state), 96 | ]) 97 | return {'country_id': state.country_id.id} 98 | 99 | @mapping 100 | @only_create 101 | def state_id(self, record): 102 | state = self.env['res.country.state'].search([ 103 | ('code', '=', record.state), 104 | ]) 105 | return {'state_id': state.id} 106 | 107 | 108 | @easypost 109 | class StockPickingTrackingLocationImporter(EasypostImporter): 110 | _model_name = ['easypost.shipment.tracking.location'] 111 | _base_mapper = StockPickingTrackingLocationImportMapper 112 | _id_prefix = 'loc' 113 | _hashable_attrs = ('city', 'zip', 'state') 114 | 115 | def default_easypost_values(self, defaults_obj): 116 | """ Get Default values from a given object to fill in any missing 117 | data in `self.easypost_record` 118 | :param defaults_obj: Record that we are filling in the gaps from 119 | """ 120 | for attr in self._hashable_attrs: 121 | rec_val = getattr(self.easypost_record, attr) 122 | if not rec_val: 123 | if attr == 'state': 124 | # Handle by getting the code from the ID 125 | val = getattr(defaults_obj, '%s_id' % attr) 126 | setattr(self.easypost_record, attr, val.code) 127 | else: 128 | setattr(self.easypost_record, attr, 129 | getattr(defaults_obj, attr)) 130 | -------------------------------------------------------------------------------- /connector_easypost_tracker/static/description/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasLabs/odoo-connector-easypost/7d2edbd0f82861f76cea58082c425a4b7c873ad6/connector_easypost_tracker/static/description/screenshot_1.png -------------------------------------------------------------------------------- /connector_easypost_tracker/static/description/screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasLabs/odoo-connector-easypost/7d2edbd0f82861f76cea58082c425a4b7c873ad6/connector_easypost_tracker/static/description/screenshot_2.png -------------------------------------------------------------------------------- /connector_easypost_tracker/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from . import test_stock_picking_tracking_event 6 | from . import test_stock_picking_tracking_group 7 | from . import test_stock_picking_tracking_location 8 | -------------------------------------------------------------------------------- /connector_easypost_tracker/tests/common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | """ 6 | Helpers usable in the tests 7 | """ 8 | from odoo.addons.connector_easypost.connector import get_environment 9 | from odoo.addons.connector_easypost.unit.import_synchronizer import ( 10 | EasypostImporter, 11 | ) 12 | from odoo.addons.connector_easypost.unit.object_dict import ObjectDict 13 | from odoo.addons.connector_easypost.tests.common import ( 14 | mock_api, 15 | SetUpEasypostBase, 16 | ) 17 | 18 | 19 | class EasypostTrackerHelper(SetUpEasypostBase): 20 | 21 | def setUp(self): 22 | super(EasypostTrackerHelper, self).setUp() 23 | self.model = 'easypost.shipment.tracking.group' 24 | self.record = self.new_record() 25 | rec = self.record 26 | self.picking = self.create_picking(rec.shipment_id) 27 | self.importer = self._get_importer(self.model) 28 | with mock_api() as mk: 29 | mk.Tracker.retrieve = lambda x: rec 30 | self.importer.run(rec.id) 31 | 32 | def _get_importer(self, model): 33 | """ Return an EasypostImporter instance """ 34 | env = get_environment(self.session, model, 35 | self.backend_id) 36 | return env.get_connector_unit(EasypostImporter) 37 | 38 | def new_record(self): 39 | return ObjectDict(**{ 40 | "id": "trk_c8e0edb5bb284caa934a0d3db23a148z", 41 | "object": "Tracker", 42 | "mode": "test", 43 | "tracking_code": "9400110898825022579493", 44 | "status": "in_transit", 45 | "created_at": "2016-01-13T21:52:28Z", 46 | "updated_at": "2016-01-13T21:52:32Z", 47 | "signed_by": None, 48 | "weight": None, 49 | "est_delivery_date": None, 50 | "shipment_id": "shp_b3740406f02c463fb29b06775e0b9c6c", 51 | "carrier": "USPS", 52 | "public_url": "https://track.easypost.com/test", 53 | "tracking_details": [ObjectDict(**{ 54 | "object": "TrackingDetail", 55 | "message": "Shipping Label Created", 56 | "status": "pre_transit", 57 | "datetime": "2015-12-31T15:58:00Z", 58 | "source": "USPS", 59 | "tracking_location": ObjectDict(**{ 60 | "object": "TrackingLocation", 61 | "city": "FOUNTAIN VALLEY", 62 | "state": "CA", 63 | "country": "US", 64 | "zip": "92708" 65 | }) 66 | })], 67 | "carrier_detail": None, 68 | }) 69 | 70 | def create_picking(self, ship_id): 71 | company = self.env.ref('base.main_company') 72 | picking_values = { 73 | 'partner_id': self.env.ref('base.res_partner_1').id, 74 | 'location_dest_id': self.env['stock.location'].search([])[0].id, 75 | 'location_id': self.env['stock.location'].search([ 76 | ('company_id', '=', company.id), 77 | ('name', '=', 'Stock') 78 | ])[0].id, 79 | 'picking_type_id': self.env['stock.picking.type'].search([])[0].id, 80 | } 81 | picking = self.env['stock.picking'].create(picking_values) 82 | 83 | ep_picking_obj = self.env['easypost.shipment'] 84 | ep_obj = ep_picking_obj.search([ 85 | ('odoo_id', '=', picking.id), 86 | ]) 87 | if not len(ep_obj): 88 | ep_picking_obj.create({ 89 | 'odoo_id': picking.id, 90 | 'external_id': ship_id, 91 | }) 92 | else: 93 | ep_obj.write({'external_id': ship_id}) 94 | return ep_obj 95 | -------------------------------------------------------------------------------- /connector_easypost_tracker/tests/test_stock_picking_tracking_event.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from .common import EasypostTrackerHelper 6 | 7 | 8 | class TestStockPickingTrackingEvent(EasypostTrackerHelper): 9 | 10 | def setUp(self): 11 | super(TestStockPickingTrackingEvent, self).setUp() 12 | group = self.env['easypost.shipment.tracking.group'].search([ 13 | ('external_id', '=', self.record.id) 14 | ]) 15 | self.event = group.last_event_id 16 | self.data = self.record.tracking_details[0] 17 | 18 | def test_message(self): 19 | """ It should match the event message """ 20 | self.assertEquals(self.event.message, self.data.message) 21 | 22 | def test_source(self): 23 | """ It should match the event status """ 24 | self.assertEquals(self.event.source, self.data.source) 25 | 26 | def test_state(self): 27 | """ It should match the event status """ 28 | self.assertEquals(self.event.state, self.data.status) 29 | -------------------------------------------------------------------------------- /connector_easypost_tracker/tests/test_stock_picking_tracking_group.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from .common import EasypostTrackerHelper 6 | 7 | 8 | class TestStockPickingTrackingGroup(EasypostTrackerHelper): 9 | 10 | def test_external_id(self): 11 | """ It should equal the objects record_id """ 12 | record = self.env['easypost.shipment.tracking.group'].search([ 13 | ('external_id', '=', self.record.id) 14 | ]) 15 | self.assertTrue(len(record) == 1) 16 | 17 | def test_ref(self): 18 | """ It should equal the objects tracking_code """ 19 | record = self.env['easypost.shipment.tracking.group'].search([ 20 | ('external_id', '=', self.record.id) 21 | ]) 22 | self.assertEquals(record.ref, self.record.tracking_code) 23 | -------------------------------------------------------------------------------- /connector_easypost_tracker/tests/test_stock_picking_tracking_location.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.connector_easypost.unit.object_dict import ObjectDict 6 | from .common import EasypostTrackerHelper 7 | 8 | 9 | class TestStockPickingTrackingLocation(EasypostTrackerHelper): 10 | 11 | _location_model = 'easypost.shipment.tracking.location' 12 | 13 | def setUp(self): 14 | super(TestStockPickingTrackingLocation, self).setUp() 15 | group = self.env['easypost.shipment.tracking.group'].search([ 16 | ('external_id', '=', self.record.id) 17 | ]) 18 | self.location = group.location_id 19 | self.data = self.record.tracking_details[0].tracking_location 20 | 21 | def test_city(self): 22 | """ It should match the event city """ 23 | self.assertEquals(self.location.city, self.data.city) 24 | 25 | def test_zip(self): 26 | """ It should match the location zip """ 27 | self.assertEquals(self.location.zip, self.data.zip) 28 | 29 | def test_state_id(self): 30 | """ It should match the location state """ 31 | self.assertEquals(self.location.state_id.code, self.data.state) 32 | 33 | def test_location_id(self): 34 | """ It should match the location state """ 35 | self.assertEquals(self.location.state_id.country_id.code, 36 | self.data.country) 37 | 38 | def test_default_easypost_values(self): 39 | """ It should use default values from the origin location to 40 | map missing but required values from the location object """ 41 | broken_data = ObjectDict(**{ 42 | "city": "", 43 | "state": "", 44 | "zip": "" 45 | }) 46 | importer = self._get_importer(self._location_model) 47 | importer.easypost_record = broken_data 48 | importer.default_easypost_values( 49 | self.picking.company_id 50 | ) 51 | expected_vals = { 52 | 'city': self.picking.company_id.city, 53 | 'state': self.picking.company_id.state_id.code, 54 | 'zip': self.picking.company_id.zip, 55 | } 56 | actual_vals = { 57 | 'city': importer.easypost_record.city, 58 | 'state': importer.easypost_record.state, 59 | 'zip': importer.easypost_record.zip, 60 | } 61 | self.assertEqual(expected_vals, actual_vals) 62 | 63 | def test_default_id(self): 64 | """ It should equal the hex digest of the MD5 hash of the city, 65 | state and zip of the location object """ 66 | expected = u"loc_c10e6da33abcab202dab527c3f1a7f7e" 67 | test_data = ObjectDict(**{ 68 | "city": "Las Vegas", 69 | "state": "NV", 70 | "zip": "89183", 71 | "country": None, 72 | }) 73 | importer = self._get_importer(self._location_model) 74 | importer.easypost_record = test_data 75 | actual = importer.run() 76 | self.assertEquals(actual.external_id, expected) 77 | -------------------------------------------------------------------------------- /connector_easypost_tracker/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import binder 4 | -------------------------------------------------------------------------------- /connector_easypost_tracker/unit/binder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2016-2017 LasLabs Inc. 3 | # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). 4 | 5 | from odoo.addons.connector_easypost.backend import easypost 6 | from odoo.addons.connector_easypost.unit.binder import EasypostModelBinder 7 | 8 | 9 | @easypost 10 | class EasypostTrackerModelBinder(EasypostModelBinder): 11 | _model_name = [ 12 | 'easypost.shipment.tracking.event', 13 | 'easypost.shipment.tracking.group', 14 | 'easypost.shipment.tracking.location', 15 | ] 16 | -------------------------------------------------------------------------------- /oca_dependencies.txt: -------------------------------------------------------------------------------- 1 | # https://github.com/OCA/delivery-carrier/pull/143 2 | # https://github.com/OCA/delivery-carrier/pull/146 3 | # https://github.com/OCA/delivery-carrier/pull/147 4 | delivery-carrier https://github.com/LasLabs/delivery-carrier.git long_term/10.0/easypost 5 | connector 6 | # https://github.com/OCA/l10n-usa/pull/18 7 | l10n-usa https://github.com/LasLabs/l10n-usa.git release/10.0/l10n_us_product 8 | # https://github.com/OCA/partner-contact/pull/500 9 | partner-contact https://github.com/LasLabs/partner-contact.git release/10.0/base_partner_address_validate 10 | queue 11 | # https://github.com/OCA/server-tools/pull/1033 12 | server-tools https://github.com/LasLabs/server-tools.git hotfix/10.0/base_external_system-context_stop_create 13 | # https://github.com/OCA/stock-logistics-workflow/pull/384 14 | stock-logistics-workflow https://github.com/LasLabs/stock-logistics-workflow.git release/10.0/10.0-mig-stock_package_info 15 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | easypost 2 | vcrpy 3 | --------------------------------------------------------------------------------