Compare commits
1 commit
18.0
...
delivery-d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08065d7c99 |
17 changed files with 968 additions and 0 deletions
1
bemade_delivery_gls_canada/__init__.py
Normal file
1
bemade_delivery_gls_canada/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
33
bemade_delivery_gls_canada/__manifest__.py
Normal file
33
bemade_delivery_gls_canada/__manifest__.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
#
|
||||||
|
# Bemade Inc.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||||
|
# Author: Marc Durepos (Contact : mdurepos@durpro.com)
|
||||||
|
#
|
||||||
|
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||||
|
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||||
|
# or modified copies of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
# DEALINGS IN THE SOFTWARE.
|
||||||
|
#
|
||||||
|
{
|
||||||
|
'name': '',
|
||||||
|
'version': '15.0.1.0.0',
|
||||||
|
'summary': '',
|
||||||
|
'description': '',
|
||||||
|
'category': '',
|
||||||
|
'author': 'Bemade Inc.',
|
||||||
|
'website': 'http://www.bemade.org',
|
||||||
|
'license': 'OPL-1',
|
||||||
|
'depends': ['delivery', 'bemade_delivery_partner_accounts'],
|
||||||
|
'data': [],
|
||||||
|
'assets': {},
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False
|
||||||
|
}
|
||||||
6
bemade_delivery_gls_canada/models/__init__.py
Normal file
6
bemade_delivery_gls_canada/models/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from . import delivery_carrier
|
||||||
|
from . import gls_canada_shipping_charge
|
||||||
|
from . import gls_canada_credentials
|
||||||
|
from . import gls_canada_shipment
|
||||||
|
from . import res_partner
|
||||||
|
from . import sale_order
|
||||||
605
bemade_delivery_gls_canada/models/delivery_carrier.py
Normal file
605
bemade_delivery_gls_canada/models/delivery_carrier.py
Normal file
|
|
@ -0,0 +1,605 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
import binascii
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
from odoo import fields, models, api, _, exceptions
|
||||||
|
from datetime import date, timedelta, datetime
|
||||||
|
import holidays
|
||||||
|
import pytz
|
||||||
|
from timezonefinder import TimezoneFinder
|
||||||
|
import phonenumbers
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
_logger = logging.getLogger('Gls Canada')
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveryCarrier(models.Model):
|
||||||
|
_inherit = 'delivery.carrier'
|
||||||
|
|
||||||
|
delivery_type = fields.Selection(selection_add=[("gls_canada", "GLS Canada")],
|
||||||
|
ondelete={'gls_canada': 'set default'})
|
||||||
|
|
||||||
|
gls_canada_packaging_id = fields.Many2one('stock.package.type',
|
||||||
|
string="Default Package Type")
|
||||||
|
|
||||||
|
gls_canada_payment_type = fields.Selection([('Prepaid', 'Prepaid'),
|
||||||
|
('ThirdParty', 'ThirdParty'),
|
||||||
|
('Collect', 'Collect')],
|
||||||
|
string='Gls canada payment type',
|
||||||
|
help="Please select GLS Canada payment type")
|
||||||
|
gls_canada_delivery_type = fields.Selection([('AIR', 'AIR'),
|
||||||
|
('GRD', 'GRD')],
|
||||||
|
string='Gls canada delivery type',
|
||||||
|
help="Please select GLS Canada delivery type")
|
||||||
|
gls_canada_unit_measurement = fields.Selection([('K', 'kg (implies cm)'),
|
||||||
|
('L', 'lb (implies inch)'),
|
||||||
|
('KC', 'kg-cm'), ('KM', 'kg-meter'),
|
||||||
|
('LI', 'lb-inch'),
|
||||||
|
('LF', 'lb-feet'), ],
|
||||||
|
string='Unit Of Measurement')
|
||||||
|
parcel_type = fields.Selection([('Barrel', 'Barrel - freight'),
|
||||||
|
('Bundle', 'Bundle - freight'),
|
||||||
|
('Box', 'Box - freight'),
|
||||||
|
('Crate', 'Crate - freight'),
|
||||||
|
('FullLoad', 'FullLoad - freight'),
|
||||||
|
('Piece', 'Piece - freight'),
|
||||||
|
('Skid', 'Skid - freight'),
|
||||||
|
('Tube', 'Tube - freight'),
|
||||||
|
('Other', 'Other - freight'),
|
||||||
|
('Box', 'Box - Parcel Canada'),
|
||||||
|
('Envelope', 'Envelope - Parcel Canada'),
|
||||||
|
('Other', 'Other - Parcel Canada'),
|
||||||
|
('Mixed', 'Mixed - Parcel Canada')],
|
||||||
|
string='Parcel Type')
|
||||||
|
return_label = fields.Boolean("Required Return Label ?")
|
||||||
|
category = fields.Selection([('Parcel', 'Parcel'),
|
||||||
|
('Freight', 'Freight'),
|
||||||
|
('Distribution', 'Distribution'),
|
||||||
|
('Logistics', 'Logistics'), ],
|
||||||
|
string='Gls canada category type')
|
||||||
|
appointment_type = fields.Selection([('Scheduled', 'Scheduled'),
|
||||||
|
('Required', 'Required')])
|
||||||
|
|
||||||
|
def gls_canada_rate_shipment(self, order):
|
||||||
|
"""
|
||||||
|
Estimate the cost of a shipment from a sales order. Called by rate_shipment in
|
||||||
|
delivery/models/delivery_carrier.py.
|
||||||
|
|
||||||
|
:param order: ResultSet of sale.order records, all for the same recipient and from the same warehouse
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
Estimate = self.env['gls.canada.shipping.estimate']
|
||||||
|
|
||||||
|
sender = order.warehouse_id.partner_id
|
||||||
|
consignee = order.partner_shipping_id
|
||||||
|
billing_account = order.delivery_billing_account
|
||||||
|
weight = sum([
|
||||||
|
(line.product_id.weight * line.product_uom_qty) for line in
|
||||||
|
order.order_line if not line.is_delivery
|
||||||
|
])
|
||||||
|
data = {
|
||||||
|
"sender": {
|
||||||
|
"postalCode": sender.zip or '',
|
||||||
|
"provinceCode": sender.state_id and sender.state_id.code or '',
|
||||||
|
"countryCode": sender.country_id and sender.country_id.code or '',
|
||||||
|
},
|
||||||
|
"consignee": {
|
||||||
|
"postalCode": consignee.zip or '',
|
||||||
|
"provinceCode": consignee.state_id and consignee.state_id.code or '',
|
||||||
|
"countryCode": consignee.country_id and consignee.country_id.code or '',
|
||||||
|
},
|
||||||
|
"billing": self.company_id.gls_canada_billing_number or '',
|
||||||
|
"paymentType": self.gls_canada_payment_type,
|
||||||
|
"deliveryType": self.gls_canada_delivery_type,
|
||||||
|
|
||||||
|
"unitOfMeasurement": self.gls_canada_unit_measurement,
|
||||||
|
|
||||||
|
"parcels": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"parcelType": self.parcel_type,
|
||||||
|
"quantity": "1",
|
||||||
|
"weight": weight,
|
||||||
|
"length": self.gls_canada_packaging_id.packaging_length or 0,
|
||||||
|
"depth": self.gls_canada_packaging_id.height or 0,
|
||||||
|
"width": self.gls_canada_packaging_id.width or 0,
|
||||||
|
# "FCA_Class": "100.00",
|
||||||
|
|
||||||
|
# "requestReturnLabel": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"category": self.category,
|
||||||
|
|
||||||
|
}
|
||||||
|
if self.category == "Freight":
|
||||||
|
if self.appointment_type == 'Scheduled':
|
||||||
|
data.update({"appointment": {
|
||||||
|
"type": self.appointment_type,
|
||||||
|
"date": order.commitment_date.strftime(
|
||||||
|
"%Y-%m-%d") if self.appointment_type == "Scheduled" else '',
|
||||||
|
"time": order.commitment_date.strftime(
|
||||||
|
"%H:%M") if self.appointment_type == "Scheduled" else '',
|
||||||
|
}})
|
||||||
|
# if self.appointment_type != 'Scheduled':
|
||||||
|
else:
|
||||||
|
data.update({"appointment": {
|
||||||
|
"type": self.appointment_type,
|
||||||
|
"phone": consignee.phone or ''
|
||||||
|
}})
|
||||||
|
|
||||||
|
data = json.dumps(data)
|
||||||
|
_logger.info("Rate Request Data %s" % data)
|
||||||
|
username = self.company_id.gls_canada_username
|
||||||
|
password = self.company_id.gls_canada_password
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
api_url = "%s/rate" % (self.company_id.gls_canada_api_url)
|
||||||
|
response_data = requests.post(url=api_url, headers=headers,
|
||||||
|
auth=HTTPBasicAuth(username, password),
|
||||||
|
data=data)
|
||||||
|
_logger.info("Rate response %s" % response_data.content)
|
||||||
|
try:
|
||||||
|
if response_data.status_code in [200, 201]:
|
||||||
|
response_body = response_data.json()
|
||||||
|
_logger.info(response_body)
|
||||||
|
existing_records = Estimate.search(
|
||||||
|
[('sale_order_id', '=', order and order.id)])
|
||||||
|
existing_records.sudo().unlink()
|
||||||
|
if isinstance(response_body.get('rates'), dict):
|
||||||
|
response_body = [response_body.get('rates')]
|
||||||
|
for response_dict in response_body.get('rates'):
|
||||||
|
Estimate.sudo().create(
|
||||||
|
{
|
||||||
|
'account_type': response_dict.get('accountType'),
|
||||||
|
'rate_type': response_dict.get('rateType'),
|
||||||
|
'total_charge': response_dict.get('total'),
|
||||||
|
'sale_order_id': order and order.id
|
||||||
|
}
|
||||||
|
)
|
||||||
|
gls_canada_charge_id = Estimate.search(
|
||||||
|
[('sale_order_id', '=', order and order.id)], order='total_charge',
|
||||||
|
limit=1)
|
||||||
|
order.gls_canada_shipping_charge_id = gls_canada_charge_id and gls_canada_charge_id.id
|
||||||
|
return {'success': True,
|
||||||
|
'price': gls_canada_charge_id and gls_canada_charge_id.total_charge or 0.0,
|
||||||
|
'error_message': False, 'warning_message': False}
|
||||||
|
else:
|
||||||
|
return {'success': False, 'price': 0.0,
|
||||||
|
'error_message': "%s %s" % (response_data, response_data.text),
|
||||||
|
'warning_message': False}
|
||||||
|
except Exception as e:
|
||||||
|
raise ValidationError(e)
|
||||||
|
|
||||||
|
def gls_canada_send_shipping(self, pickings):
|
||||||
|
""" Send the package to the service provider
|
||||||
|
|
||||||
|
:param pickings: A recordset of pickings
|
||||||
|
:return list: A list of dictionaries (one per picking) containing of the form::
|
||||||
|
{ 'exact_price': price,
|
||||||
|
'tracking_number': number }
|
||||||
|
"""
|
||||||
|
# See if there are available pickups
|
||||||
|
shipments_dict = self._group_pickings_by_warehouse_and_recipient(pickings)
|
||||||
|
pickups = self._gls_canada_get_pickups_for_senders(shipments_dict.keys())
|
||||||
|
picking_shipment_data_dict = {}
|
||||||
|
for sender, recipient_dict in shipments_dict.items():
|
||||||
|
for recipient, picks in recipient_dict.items():
|
||||||
|
shipping_data, shipment_id = self._gls_canada_send_shipment(picks, pickups.get(sender))
|
||||||
|
picking_shipment_data_dict.update(
|
||||||
|
{pick: shipping_data for pick in picks})
|
||||||
|
pickup_id = pickups.get(sender)
|
||||||
|
if pickup_id:
|
||||||
|
self._add_shipment_to_pickup(shipment_id, pickup_id)
|
||||||
|
if not pickups.get(sender):
|
||||||
|
pickup_time = self._get_pickup_time(sender)
|
||||||
|
pickup_id = self._gls_canada_schedule_pickup(picks[0].shipper,
|
||||||
|
picks.mapped('shipment_id'),
|
||||||
|
pickup_time)
|
||||||
|
picks.pickup_id = pickup_id
|
||||||
|
return [picking_shipment_data_dict[picking] for picking in pickings]
|
||||||
|
|
||||||
|
def _add_shipment_to_pickup(self, shipment_id, pickup_id):
|
||||||
|
endpoint = f'/pickup/{pickup_id}/add-shipment/{shipment_id}'
|
||||||
|
response = self._gls_canada_api_call(endpoint, "POST", {})
|
||||||
|
if response.status_code != 200:
|
||||||
|
self._raise_gls_error(endpoint, response)
|
||||||
|
|
||||||
|
def gls_canada_cancel_shipment(self, pickings):
|
||||||
|
"""This Method is used for cancel shipment from odoo to matkahuolto"""
|
||||||
|
api_url = "%s/shipment/%s" % (
|
||||||
|
self.company_id.gls_canada_api_url, pickings.shipment_id)
|
||||||
|
username = self.company_id.gls_canada_username
|
||||||
|
password = self.company_id.gls_canada_password
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
response_body = requests.request(method="DELETE", url=api_url, headers=headers,
|
||||||
|
auth=HTTPBasicAuth(username, password))
|
||||||
|
_logger.info("Cancel Shipment Response %s" % response_body.content)
|
||||||
|
if response_body.status_code in [200, 201]:
|
||||||
|
_logger.info("Shipment Cancel Successfully")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
raise ValidationError(response_body.content)
|
||||||
|
|
||||||
|
def _gls_canada_shipping_request_data(self, pickings):
|
||||||
|
parcel_data = []
|
||||||
|
counter = 0
|
||||||
|
for package in pickings.package_ids:
|
||||||
|
counter += 1
|
||||||
|
parcel_data.append({
|
||||||
|
"id": counter,
|
||||||
|
"parcelType": self.parcel_type,
|
||||||
|
"quantity": "1",
|
||||||
|
"weight": package.shipping_weight,
|
||||||
|
"length": package.package_type_id.packaging_length or '',
|
||||||
|
"depth": package.package_type_id.height or '',
|
||||||
|
"width": package.package_type_id.width or '',
|
||||||
|
# "requestReturnLabel": self.return_label,
|
||||||
|
})
|
||||||
|
if pickings.weight_bulk:
|
||||||
|
counter += 1
|
||||||
|
parcel_data.append({
|
||||||
|
"id": counter,
|
||||||
|
"parcelType": self.parcel_type,
|
||||||
|
"quantity": "1",
|
||||||
|
"weight": pickings.shipping_weight,
|
||||||
|
"length": self.gls_canada_packaging_id.packaging_length or '',
|
||||||
|
"depth": self.gls_canada_packaging_id.height or '',
|
||||||
|
"width": self.gls_canada_packaging_id.width or '',
|
||||||
|
# "requestReturnLabel": self.return_label,
|
||||||
|
})
|
||||||
|
|
||||||
|
receiver_address = pickings.partner_id
|
||||||
|
shipper_address = pickings.picking_type_id.warehouse_id.partner_id
|
||||||
|
sender_contact = pickings[0].shipper
|
||||||
|
request_data = {
|
||||||
|
"category": self.category,
|
||||||
|
"paymentType": self.gls_canada_payment_type,
|
||||||
|
"billingAccount": self.company_id.gls_canada_billing_number,
|
||||||
|
"sender": self._gls_canada_make_sender_data(shipper_address, sender_contact),
|
||||||
|
"consignee": self._gls_canada_make_consignee_data(receiver_address),
|
||||||
|
"unitOfMeasurement": self.gls_canada_unit_measurement,
|
||||||
|
"parcels": parcel_data,
|
||||||
|
"deliveryType": self.gls_canada_delivery_type,
|
||||||
|
}
|
||||||
|
# if self.return_label == True:
|
||||||
|
# request_data.update({"returnAddress": {"contact": {
|
||||||
|
# "fullName": shipper_address.name or '',
|
||||||
|
# "language": "en",
|
||||||
|
# "email": "example@dicom.com",
|
||||||
|
# "department": "",
|
||||||
|
# "telephone": shipper_address.phone or '',
|
||||||
|
# "extension": ""
|
||||||
|
# }
|
||||||
|
# }})
|
||||||
|
# if shipper_address.country_id and shipper_address.country_id.code != receiver_address.country_id.code:
|
||||||
|
# request_data.update(
|
||||||
|
# {"internationalDetails": {"descriptionOfGoods": 'Workspace123', "isDicomBroker": 'true', "products": [
|
||||||
|
# {
|
||||||
|
# "id": "17451",
|
||||||
|
# "Quantity": 1
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
# }})
|
||||||
|
_logger.info("Shipping Request Data %s" % request_data)
|
||||||
|
return request_data
|
||||||
|
|
||||||
|
def _gls_canada_get_pickups_for_senders(self, senders) -> dict:
|
||||||
|
"""
|
||||||
|
Get a dict matching each sender to any already existing pickups today (or next
|
||||||
|
business day). If no matching pickup exists, the sender is not added to the keys
|
||||||
|
of the dict and a pickup will need to be created for it elsewhere.
|
||||||
|
:param senders: The recordset of partner_ids matching the pickup locations
|
||||||
|
:return: dict of the form {sender: "pickup ID"}
|
||||||
|
"""
|
||||||
|
endpoint = "/pickup/list"
|
||||||
|
# First, group the senders by country and state so that holidays can be properly
|
||||||
|
# handled for each location.
|
||||||
|
locales = set()
|
||||||
|
for sender in senders:
|
||||||
|
locales.add((sender.country_id, sender.state_id))
|
||||||
|
sender_groups = [[sender for sender in senders if
|
||||||
|
(sender.country_id, sender.state_id) == locale] for locale in
|
||||||
|
locales]
|
||||||
|
senders_to_pickups_dict = {}
|
||||||
|
# For each group sharing a locale, get pickups for the next working day
|
||||||
|
for sender_group in sender_groups:
|
||||||
|
pickup_date = self._get_pickup_time(sender_group[0])
|
||||||
|
request_data = {
|
||||||
|
'category': self.category,
|
||||||
|
'pickupDate': pickup_date.strftime("%Y-%m-%d"),
|
||||||
|
}
|
||||||
|
response = self._gls_canada_api_call(endpoint, "GET", request_data)
|
||||||
|
_logger.info("GLS Response status code:%s" % response.status_code)
|
||||||
|
if response.status_code == 401:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Username or password is incorrect for GLS Canada."))
|
||||||
|
elif response.status_code == 204: # No pickups scheduled
|
||||||
|
return {}
|
||||||
|
pickups = response.json() if response else []
|
||||||
|
for pickup in pickups:
|
||||||
|
street = pickup['sender']['addressLine1']
|
||||||
|
zip = pickup['sender']['postalCode'].replace(' ', '')
|
||||||
|
matching_senders = [sender for sender in sender_group if
|
||||||
|
(sender.street == street
|
||||||
|
and sender.zip.replace(' ', '') == zip)]
|
||||||
|
senders_to_pickups_dict.update({
|
||||||
|
sender: pickup for sender in matching_senders
|
||||||
|
})
|
||||||
|
return senders_to_pickups_dict
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _group_pickings_by_warehouse_and_recipient(self, pickings) -> dict:
|
||||||
|
"""
|
||||||
|
Transform a list of pickings into a dict of the form::
|
||||||
|
|
||||||
|
{ sender_partner : { recipient_partner : recordset(pickings) } }
|
||||||
|
:param pickings: The recordset of pickings to be arranged
|
||||||
|
:return: dict as described above
|
||||||
|
"""
|
||||||
|
shipments = {pick.picking_type_id.warehouse_id.partner_id: {} for pick in
|
||||||
|
pickings}
|
||||||
|
for sender in shipments.keys():
|
||||||
|
matching_pickings = pickings.filtered(lambda p:
|
||||||
|
p.picking_type_id.warehouse_id.partner_id == sender)
|
||||||
|
for pick in matching_pickings:
|
||||||
|
recipient = pick.partner_id
|
||||||
|
if recipient in shipments[sender]:
|
||||||
|
shipments[sender][recipient] |= pick
|
||||||
|
else:
|
||||||
|
shipments[sender].update({recipient: pick})
|
||||||
|
return shipments
|
||||||
|
|
||||||
|
def _gls_canada_schedule_pickup(self, sender_contact, shipments: List[str],
|
||||||
|
pickup_time: datetime, office_close='16:00'):
|
||||||
|
shipment = self._load_shipment(shipments[0])
|
||||||
|
sender = shipment['sender']
|
||||||
|
contact = shipment['sender']['contact']
|
||||||
|
endpoint = '/pickup'
|
||||||
|
ready_time = pickup_time - timedelta(hours=1)
|
||||||
|
request_data = {
|
||||||
|
"shipments": [{'id': shipment} for shipment in shipments],
|
||||||
|
"contact": self._gls_canada_make_contact_data(sender_contact)['contact'],
|
||||||
|
"officeClose": office_close,
|
||||||
|
"date": pickup_time.strftime("%Y-%m-%d"),
|
||||||
|
"ready": pickup_time.strftime("%H:%M"),
|
||||||
|
"category": self.category,
|
||||||
|
"location": "SH",
|
||||||
|
}
|
||||||
|
""" Location is pickup location:
|
||||||
|
* SS: Basement
|
||||||
|
* RC: ground floor
|
||||||
|
* PH: home
|
||||||
|
* MR: mail room
|
||||||
|
* MB: mailbox
|
||||||
|
* BU: office
|
||||||
|
* OT: other
|
||||||
|
* SH: shipping
|
||||||
|
"""
|
||||||
|
response = self._gls_canada_api_call(endpoint, 'POST', request_data)
|
||||||
|
if response.status_code == 201: # pickup created
|
||||||
|
response_body = response.json()
|
||||||
|
return response_body.get('ID')
|
||||||
|
else:
|
||||||
|
self._raise_gls_error(endpoint, response)
|
||||||
|
|
||||||
|
def _load_shipment(self, shipment):
|
||||||
|
endpoint = f"/shipment/{shipment}"
|
||||||
|
response = self._gls_canada_api_call(endpoint, "GET", {})
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def _gls_canada_send_shipment(self, pickings, pickup_id):
|
||||||
|
"""
|
||||||
|
Create a new shipment for all the packages in the pickings and assign this
|
||||||
|
shipment to the pickup_id submitted. If successful, the shipping label is
|
||||||
|
retrieved from GLS and attached to each picking as a PDF.
|
||||||
|
|
||||||
|
:param pickings: The iterable of pickings to be shipped. These must all be from
|
||||||
|
the sender matching the pickup_id submitted and to the same
|
||||||
|
recipient.
|
||||||
|
:param pickups: The pickups that are already scheduled for the sender. The first will be used.
|
||||||
|
:param pickup: The pickup_id of the pickup that is already scheduled with GLS.
|
||||||
|
:return: Tuple containing The shipment data (see _gls_canada_send_shipping)
|
||||||
|
and the shipment ID returned from GLS
|
||||||
|
"""
|
||||||
|
endpoint = "/shipment"
|
||||||
|
request_data = self._gls_canada_shipping_request_data(pickings)
|
||||||
|
response = self._gls_canada_api_call(endpoint, "POST", request_data)
|
||||||
|
|
||||||
|
_logger.info("Shipping Response %s" % response.content)
|
||||||
|
# response_body = self.shipment_response()
|
||||||
|
if response.status_code in [200, 201]:
|
||||||
|
response_body = response.json()
|
||||||
|
_logger.info("Json Response %s " % response_body)
|
||||||
|
parcel_tracking_numbers = []
|
||||||
|
for parcel_tracking_number in response_body.get(
|
||||||
|
'parcelTrackingNumbers'):
|
||||||
|
parcel_tracking_numbers.append(parcel_tracking_number)
|
||||||
|
pickings.parcel_tracking_numbers = ', '.join(
|
||||||
|
parcel_tracking_numbers)
|
||||||
|
pickings.shipment_id = response_body.get('ID')
|
||||||
|
master_tracking_number = response_body.get('trackingNumber')
|
||||||
|
# print label code
|
||||||
|
self._gls_canada_get_label(master_tracking_number, pickings)
|
||||||
|
return {'exact_price': 0.0,
|
||||||
|
'tracking_number': master_tracking_number}, response_body.get("ID")
|
||||||
|
else:
|
||||||
|
self._raise_gls_error(endpoint, response)
|
||||||
|
|
||||||
|
def _gls_canada_get_label(self, master_tracking_number, pickings):
|
||||||
|
endpoint = f"/shipment/label/{pickings.shipment_id}"
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/pdf', # TODO: implement application/zpl here
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
response = self._gls_canada_api_call(endpoint, "GET", {},
|
||||||
|
headers=headers)
|
||||||
|
_logger.info("Label Response %s" % response)
|
||||||
|
log_message = f"<b>Tracking Numbers:</b> {master_tracking_number}"
|
||||||
|
pickings.message_post(body=log_message,
|
||||||
|
attachments=[(f"Label GLS Canada {pickings.name}.pdf",
|
||||||
|
response.content)])
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _gls_canada_make_sender_data(cls, sender, sender_contact):
|
||||||
|
phone_parsed = phonenumbers.parse(
|
||||||
|
sender_contact.phone) if sender_contact.phone else ''
|
||||||
|
contact_data = cls._gls_canada_make_contact_data(sender_contact)
|
||||||
|
return {
|
||||||
|
"addressLine1": sender.street or '',
|
||||||
|
"addressLine2": sender.street2 or '',
|
||||||
|
"city": sender.city or '',
|
||||||
|
"provinceCode": sender.state_id and sender.state_id.code or '',
|
||||||
|
"postalCode": sender.zip or '',
|
||||||
|
"countryCode": sender.country_id and sender.country_id.code or '',
|
||||||
|
"customerName": sender.name or '',
|
||||||
|
"contact": contact_data['contact']
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _gls_canada_make_contact_data(self, partner):
|
||||||
|
"""
|
||||||
|
Build a dict of the form { "contact": { ... GLS contact params ... } } from
|
||||||
|
the given partner.
|
||||||
|
:param partner: A single res_partner record.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
phone_parsed = phonenumbers.parse(partner.phone) if partner.phone else ''
|
||||||
|
return {
|
||||||
|
"contact": {
|
||||||
|
"fullName": partner.name or '',
|
||||||
|
"language": partner.lang and partner.lang.split('_')[0] or '',
|
||||||
|
"email": partner.email or '',
|
||||||
|
"telephone": phone_parsed and phone_parsed.national_number or '',
|
||||||
|
"extension": phone_parsed and phone_parsed.extension or '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _gls_canada_make_consignee_data(cls, partner_id):
|
||||||
|
if partner_id.parent_id and partner_id.parent_id.name:
|
||||||
|
parent = partner_id.parent_id
|
||||||
|
receiver_company_name = parent.name
|
||||||
|
receiver_contact_name = partner_id.name or ''
|
||||||
|
else:
|
||||||
|
receiver_company_name = partner_id.name or ''
|
||||||
|
receiver_contact_name = ''
|
||||||
|
return {
|
||||||
|
"addressLine1": partner_id.street or '',
|
||||||
|
"addressLine2": partner_id.street2 or '',
|
||||||
|
# "streetType": "AVE",
|
||||||
|
# "streetDirection": "N",
|
||||||
|
# "suite": "10",
|
||||||
|
"city": partner_id.city or '',
|
||||||
|
"provinceCode": partner_id.state_id and partner_id.state_id.code or '',
|
||||||
|
"postalCode": partner_id.zip or '',
|
||||||
|
"countryCode": partner_id.country_id and partner_id.country_id.code or '',
|
||||||
|
"customerName": receiver_company_name,
|
||||||
|
"contact": {
|
||||||
|
"fullName": receiver_contact_name,
|
||||||
|
"language": "en",
|
||||||
|
"email": partner_id.email or '',
|
||||||
|
"department": "",
|
||||||
|
"telephone": partner_id.phone or '',
|
||||||
|
"extension": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def _gls_canada_api_call(self, endpoint: str, method: str, request_data: dict,
|
||||||
|
headers: dict = {"Content-Type": "application/json"},
|
||||||
|
) -> requests.Response:
|
||||||
|
"""
|
||||||
|
Make a call to the GLS Canada API using the credentials set on the current
|
||||||
|
company_id
|
||||||
|
|
||||||
|
|
||||||
|
:param endpoint: The endpoint to use, such as "/pickup/list"
|
||||||
|
:param method: The request method to use, typically "GET" or "POST"
|
||||||
|
:param headers: Request headers
|
||||||
|
:param request_data:
|
||||||
|
:return: A list of dicts matching the GLS Canada API response specifications
|
||||||
|
"""
|
||||||
|
request_data = json.dumps(request_data)
|
||||||
|
try:
|
||||||
|
api_url = "%s%s" % (
|
||||||
|
self.company_id and self.company_id.gls_canada_api_url, endpoint)
|
||||||
|
_logger.info("Sending GLS Canada request %s" % request_data)
|
||||||
|
username = self.company_id.gls_canada_username
|
||||||
|
password = self.company_id.gls_canada_password
|
||||||
|
if method == "GET":
|
||||||
|
response_body = requests.get(url=api_url, params=request_data,
|
||||||
|
headers=headers,
|
||||||
|
auth=HTTPBasicAuth(username, password))
|
||||||
|
elif method == "DELETE":
|
||||||
|
response_body = requests.delete(url=api_url, params=request_data,
|
||||||
|
headers=headers,
|
||||||
|
auth=HTTPBasicAuth(username, password))
|
||||||
|
else:
|
||||||
|
response_body = requests.request(method=method, url=api_url,
|
||||||
|
data=request_data, headers=headers,
|
||||||
|
auth=HTTPBasicAuth(username, password))
|
||||||
|
_logger.info(f"GLS Canada Response Status Code: {response_body.status_code}")
|
||||||
|
_logger.info("GLS Canada API Response: %s" % response_body.content)
|
||||||
|
return response_body
|
||||||
|
except Exception as e:
|
||||||
|
raise ValidationError(e)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def gls_canada_get_tracking_link(self, picking):
|
||||||
|
"""This method is used for track you parcel"""
|
||||||
|
return "https://www.gls-canada.com/en/express/tracking"
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_pickup_time(cls, sender, closing_time='16:00',
|
||||||
|
earliest_pickup='12:00') -> datetime:
|
||||||
|
"""
|
||||||
|
Generate a pickup time within working hours and not on holidays, based on the
|
||||||
|
sender's localization (time zone) and closing and starting times.
|
||||||
|
|
||||||
|
:param sender: res_partner representing the sender location, used for holidays
|
||||||
|
:param closing_time: closing time to determine if datetime.now() is after the
|
||||||
|
workday's closing time, as a string formatted '%H:%M'.
|
||||||
|
:param earliest_pickup: earliest time of day for pickups at the sender location
|
||||||
|
:return: Today if today is a business day, the next business day otherwise.
|
||||||
|
"""
|
||||||
|
# In theory all sender addresses are from Canada, but this is a more reusable
|
||||||
|
# tool and doesn't introduce any extra dependencies for generalizing to all
|
||||||
|
# locales.
|
||||||
|
if not sender.partner_latitude or not sender.partner_longitude:
|
||||||
|
if not sender.geo_localize():
|
||||||
|
_logger.warning(f'Sender address for {sender.name} could not be '
|
||||||
|
f'geolocated for time zone information. Server time zone'
|
||||||
|
f' used instead.')
|
||||||
|
localized = sender.partner_latitude and sender.partner_longitude
|
||||||
|
now = datetime.now() + timedelta(minutes=30)
|
||||||
|
if localized:
|
||||||
|
tz_name = TimezoneFinder().timezone_at(lng=sender.partner_longitude,
|
||||||
|
lat=sender.partner_latitude)
|
||||||
|
tz = pytz.timezone(tz_name)
|
||||||
|
now = now.astimezone(tz=tz)
|
||||||
|
close_h, close_m = closing_time.split(':')
|
||||||
|
start_h, start_m = earliest_pickup.split(':')
|
||||||
|
closing = datetime(year=now.year, month=now.month, day=now.day,
|
||||||
|
hour=int(close_h), minute=int(close_m)).astimezone(tz)
|
||||||
|
starting = datetime(year=now.year, month=now.month, day=now.day,
|
||||||
|
hour=int(start_h), minute=int(start_m)).astimezone(tz)
|
||||||
|
# If past closing time, move to next day at starting time
|
||||||
|
if now > closing:
|
||||||
|
now = starting + timedelta(days=1)
|
||||||
|
# If before starting time, move to current day starting time
|
||||||
|
if now < starting:
|
||||||
|
now = starting
|
||||||
|
local_holidays = holidays.country_holidays(sender.country_id.code,
|
||||||
|
sender.state_id.code)
|
||||||
|
while now.date() in local_holidays or now.weekday() in (5, 6):
|
||||||
|
now += timedelta(days=1)
|
||||||
|
return now
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _raise_gls_error(cls, endpoint, response):
|
||||||
|
raise ValidationError(_(f"Got an error from GLS endpoint {endpoint}\n"
|
||||||
|
f"Response: {response.content}"))
|
||||||
25
bemade_delivery_gls_canada/models/gls_canada_credentials.py
Normal file
25
bemade_delivery_gls_canada/models/gls_canada_credentials.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class GLSCanadaAccount(models.Model):
|
||||||
|
_name = 'gls.canada.account'
|
||||||
|
_inherit = 'delivery.account'
|
||||||
|
_description = 'GLS Canada Account'
|
||||||
|
|
||||||
|
account_credentials = fields.Many2one('gls.canada.credentials', string='Login Credentials')
|
||||||
|
account_type = fields.Selection([
|
||||||
|
('Parcel', 'Parcel'),
|
||||||
|
('Freight', 'Freight'),
|
||||||
|
('Distribution', 'Distribution'),
|
||||||
|
('Logistics', 'Logistics'),
|
||||||
|
],
|
||||||
|
default='Parcel')
|
||||||
|
|
||||||
|
|
||||||
|
class GLSCanadaCredentials(models.Model):
|
||||||
|
_name = 'gls.canada.credentials'
|
||||||
|
_inherit = 'delivery.account.credentials'
|
||||||
|
_description = 'Login credentials for GLS Canada'
|
||||||
|
|
||||||
|
production_url = fields.Char('Production URL', default='https://smart4i.dicom.com/v1')
|
||||||
|
test_url = fields.Char('Test URL', default='https://sandbox-smart4i.dicom.com/v1')
|
||||||
47
bemade_delivery_gls_canada/models/gls_canada_shipment.py
Normal file
47
bemade_delivery_gls_canada/models/gls_canada_shipment.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class GLSCanadaShipment(models.Model):
|
||||||
|
_name = "gls.canada.shipment"
|
||||||
|
_description = "A shipment booked with GLS Canada"
|
||||||
|
|
||||||
|
shipment_id = fields.Integer(help="The shipment ID on the GLS side")
|
||||||
|
|
||||||
|
sender_address_line_1 = fields.Char()
|
||||||
|
sender_address_line_2 = fields.Char()
|
||||||
|
sender_street_number = fields.Char()
|
||||||
|
sender_street_name = fields.Char()
|
||||||
|
sender_street_direction = fields.Char()
|
||||||
|
sender_city = fields.Char()
|
||||||
|
sender_state = fields.Char()
|
||||||
|
sender_country = fields.Char()
|
||||||
|
sender_postal_code = fields.Char()
|
||||||
|
|
||||||
|
consignee_address_line_1 = fields.Char()
|
||||||
|
consignee_address_line_2 = fields.Char()
|
||||||
|
consignee_street_number = fields.Char()
|
||||||
|
consignee_street_name = fields.Char()
|
||||||
|
consignee_street_direction = fields.Char()
|
||||||
|
consignee_city = fields.Char()
|
||||||
|
consignee_state = fields.Char()
|
||||||
|
consignee_country = fields.Char()
|
||||||
|
consignee_postal_code = fields.Char()
|
||||||
|
|
||||||
|
parcel_ids = fields.One2many('gls.canada.parcel', string='Parcels')
|
||||||
|
|
||||||
|
class GLSCanadaParcel(models.Model):
|
||||||
|
_name = 'gls.canada.parcel'
|
||||||
|
_description = 'A parcel in a shipment booked with GLS Canada'
|
||||||
|
|
||||||
|
shipment_id = fields.Many2one('gls.canada.shipment', string='Shipment')
|
||||||
|
parcel_id = fields.Integer(help="The parcel ID on the GLS side")
|
||||||
|
|
||||||
|
weight = fields.Float()
|
||||||
|
length = fields.Float()
|
||||||
|
width = fields.Float()
|
||||||
|
depth = fields.Float()
|
||||||
|
note = fields.Char()
|
||||||
|
status = fields.Integer()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class GLSCanadaShippingEstimate(models.Model):
|
||||||
|
_name = 'gls.canada.shipping.estimate'
|
||||||
|
_description = 'GLS Canada Shipping Estimate'
|
||||||
|
|
||||||
|
account_id = fields.Many2one('gls.canada.account', string='GLS Canada Account')
|
||||||
|
total_charge = fields.Monetary()
|
||||||
|
sale_order_id = fields.Many2one('sale.order', string='Sales Order')
|
||||||
|
|
||||||
|
def set_service(self):
|
||||||
|
self.ensure_one()
|
||||||
|
carrier = self.sale_order_id.carrier_id
|
||||||
|
self.sale_order_id._remove_delivery_line()
|
||||||
|
self.sale_order_id.gls_canada_shipping_charge_id = self.id
|
||||||
|
self.sale_order_id.set_delivery_line(carrier, self.total_charge)
|
||||||
|
|
||||||
|
class GLSCanadaRate(models.Model):
|
||||||
|
_name = 'gls.canada.rate'
|
||||||
|
_rec_name = 'rate_type'
|
||||||
6
bemade_delivery_gls_canada/models/res_partner.py
Normal file
6
bemade_delivery_gls_canada/models/res_partner.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class Partner(models.Model):
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
10
bemade_delivery_gls_canada/models/sale_order.py
Normal file
10
bemade_delivery_gls_canada/models/sale_order.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class SaleOrder(models.Model):
|
||||||
|
_inherit = 'sale.order'
|
||||||
|
|
||||||
|
gls_canada_shipping_estimate_ids = fields.One2many('gls.canada.shipping.estimate', 'sale_order_id',
|
||||||
|
string='GLS Canada Rates')
|
||||||
|
gls_canada_shipping_estimate_id = fields.Many2one('gls.canada.shipping.estimate', string='Shipping Estimate')
|
||||||
|
delivery_billing_account = fields.Many2one('gls.canada.account', string='Delivery Billing Account')
|
||||||
48
bemade_delivery_gls_canada/tools.py
Normal file
48
bemade_delivery_gls_canada/tools.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import requests
|
||||||
|
import logging
|
||||||
|
import json
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
from models.gls_canada_credentials import GLSCanadaAccount
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def api_call(account: GLSCanadaAccount, endpoint: str, method: str, request_data: dict,
|
||||||
|
headers: dict = {"Content-Type": "application/json"},
|
||||||
|
) -> requests.Response:
|
||||||
|
"""
|
||||||
|
Make a call to the GLS Canada API using the credentials set on the current
|
||||||
|
company_id
|
||||||
|
|
||||||
|
|
||||||
|
:param endpoint: The endpoint to use, such as "/pickup/list"
|
||||||
|
:param method: The request method to use, typically "GET" or "POST"
|
||||||
|
:param headers: Request headers
|
||||||
|
:param request_data:
|
||||||
|
:return: A list of dicts matching the GLS Canada API response specifications
|
||||||
|
"""
|
||||||
|
request_data = json.dumps(request_data)
|
||||||
|
try:
|
||||||
|
api_url = "%s%s" % (
|
||||||
|
account.account_credentials.url, endpoint)
|
||||||
|
_logger.info("Sending GLS Canada request %s" % request_data)
|
||||||
|
username = account.account_credentials.username
|
||||||
|
password = account.account_credentials.password
|
||||||
|
if method == "GET":
|
||||||
|
response_body = requests.get(url=api_url, params=request_data,
|
||||||
|
headers=headers,
|
||||||
|
auth=HTTPBasicAuth(username, password))
|
||||||
|
elif method == "DELETE":
|
||||||
|
response_body = requests.delete(url=api_url, params=request_data,
|
||||||
|
headers=headers,
|
||||||
|
auth=HTTPBasicAuth(username, password))
|
||||||
|
else:
|
||||||
|
response_body = requests.request(method=method, url=api_url,
|
||||||
|
data=request_data, headers=headers,
|
||||||
|
auth=HTTPBasicAuth(username, password))
|
||||||
|
_logger.info(f"GLS Canada Response Status Code: {response_body.status_code}")
|
||||||
|
_logger.info("GLS Canada API Response: %s" % response_body.content)
|
||||||
|
return response_body
|
||||||
|
except Exception as e:
|
||||||
|
raise ValidationError(e)
|
||||||
1
bemade_delivery_partner_accounts/__init__.py
Normal file
1
bemade_delivery_partner_accounts/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models
|
||||||
38
bemade_delivery_partner_accounts/__manifest__.py
Normal file
38
bemade_delivery_partner_accounts/__manifest__.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#
|
||||||
|
# Bemade Inc.
|
||||||
|
#
|
||||||
|
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||||
|
# Author: Marc Durepos (Contact : mdurepos@durpro.com)
|
||||||
|
#
|
||||||
|
# This program is under the terms of the Odoo Proprietary License v1.0 (OPL-1)
|
||||||
|
# It is forbidden to publish, distribute, sublicense, or sell copies of the Software
|
||||||
|
# or modified copies of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||||
|
# DEALINGS IN THE SOFTWARE.
|
||||||
|
#
|
||||||
|
{
|
||||||
|
'name': 'Delivery Partner Accounts',
|
||||||
|
'version': '15.0.1.0.0',
|
||||||
|
'summary': 'Adds delivery accounts to partners.',
|
||||||
|
'description': '''Technical module allowing the use of multiple delivery accounts per partner. Each account can
|
||||||
|
include login credentials for the delivery service provider. This module is intended to be used with specific
|
||||||
|
carrier integrations to allow for easier use of collect payments, different carrier accounts per warehouse location,
|
||||||
|
etc.''',
|
||||||
|
'category': 'Inventory/Delivery',
|
||||||
|
'author': 'Bemade Inc.',
|
||||||
|
'website': 'http://www.bemade.org',
|
||||||
|
'license': 'OPL-1',
|
||||||
|
'depends': [
|
||||||
|
'delivery',
|
||||||
|
],
|
||||||
|
'data': [],
|
||||||
|
'assets': {},
|
||||||
|
'installable': True,
|
||||||
|
'auto_install': False
|
||||||
|
}
|
||||||
3
bemade_delivery_partner_accounts/models/__init__.py
Normal file
3
bemade_delivery_partner_accounts/models/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import delivery_account
|
||||||
|
from . import res_partner
|
||||||
|
from . import delivery_carrier
|
||||||
66
bemade_delivery_partner_accounts/models/delivery_account.py
Normal file
66
bemade_delivery_partner_accounts/models/delivery_account.py
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveryAccount(models.Model):
|
||||||
|
"""
|
||||||
|
A delivery account is a set of credentials that can be used to access a delivery service. It can be as simple as
|
||||||
|
an account number to use for a collect shipment and as complex as the full set of credentials required to make API
|
||||||
|
calls to book shipments with a given carrier. This class is meant to be extended by other modules via classical
|
||||||
|
inheritance to provide credentials that can be used for various partners, specially calibrated to different
|
||||||
|
delivery carrier types.
|
||||||
|
|
||||||
|
If the account should use credentials information specific to a given delivery method (carrier), then the
|
||||||
|
delivery.account.credentials model should be extended and the extended model should be used in the
|
||||||
|
account_credentials field of the extended delivery.account model. Models that extend delivery.account should be
|
||||||
|
set to _auto = False
|
||||||
|
"""
|
||||||
|
_name = 'delivery.account'
|
||||||
|
_description = 'Delivery Account'
|
||||||
|
_rec_name = 'account_number'
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _get_carrier_selection(self):
|
||||||
|
return self.env['delivery.carrier'].fields_get(['delivery_type'])['delivery_type']['selection']
|
||||||
|
|
||||||
|
def _get_account_type_domain(self):
|
||||||
|
if self.delivery_type:
|
||||||
|
return [('delivery_type', '=', self.delivery_type)]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
delivery_type = fields.Selection(selection='_get_carrier_selection', string='Applicable Carrier')
|
||||||
|
account_number = fields.Char()
|
||||||
|
account_credentials_id = fields.Many2one('delivery.account.credentials', string='Login Credentials')
|
||||||
|
partner_ids = fields.Many2many(comodel_name='res.partner', string='Partners',
|
||||||
|
relation='delivery_account_partner_rel',
|
||||||
|
column1='account_id', column2='partner_id',
|
||||||
|
help="Partners who can use this account.")
|
||||||
|
account_type_id = fields.Many2one('delivery.account.type', string='Account Type', domain=_get_account_type_domain)
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveryAccountCredentials(models.Model):
|
||||||
|
"""
|
||||||
|
Login credentials for a given account. This model is meant to be extended using classic inheritance. Suggested usage
|
||||||
|
is to extend the model and set the default production_url and test_url values to simplify the UI for the user. The
|
||||||
|
extended model should be used in the account_credentials field of the DeliveryAccount model.
|
||||||
|
"""
|
||||||
|
_name = 'delivery.account.credentials'
|
||||||
|
_description = 'Delivery Account Login Credentials'
|
||||||
|
_rec_name = 'username'
|
||||||
|
|
||||||
|
username = fields.Char()
|
||||||
|
password = fields.Char()
|
||||||
|
production_url = fields.Char(string='Production URL')
|
||||||
|
test_url = fields.Char(string='Test URL')
|
||||||
|
delivery_account_id = fields.One2many(comodel_name='delivery.account', inverse_name='account_credentials_id', )
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveryAccountType(models.Model):
|
||||||
|
_name = 'delivery.account.type'
|
||||||
|
_description = 'Delivery Account Type'
|
||||||
|
|
||||||
|
name = fields.Char(required=True)
|
||||||
|
technical_name = fields.Char(help="The technical name used by the carrier to identify the account type.",
|
||||||
|
required=True)
|
||||||
|
carrier_id = fields.Many2one('delivery.carrier', string='Carrier', required=True)
|
||||||
|
delivery_type = fields.Selection(related='carrier_id.delivery_type', string='Delivery Type', readonly=True)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveryCarrier(models.Model):
|
||||||
|
_inherit = 'delivery.carrier'
|
||||||
|
|
||||||
|
account_type_ids = fields.One2many('delivery.account.type', 'carrier_id',
|
||||||
|
string='Account Types')
|
||||||
28
bemade_delivery_partner_accounts/models/res_partner.py
Normal file
28
bemade_delivery_partner_accounts/models/res_partner.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from odoo import models, fields, api, _
|
||||||
|
|
||||||
|
|
||||||
|
class Partner(models.Model):
|
||||||
|
_inherit = 'res.partner'
|
||||||
|
|
||||||
|
delivery_account_ids = fields.Many2many('delivery.account', string='Delivery Accounts',
|
||||||
|
relation='delivery_account_partner_rel',
|
||||||
|
column1='partner_id', column2='account_id')
|
||||||
|
default_delivery_account_id = fields.Many2one('delivery.account', string='Default Delivery Account')
|
||||||
|
|
||||||
|
def write(self, values):
|
||||||
|
super().write(values)
|
||||||
|
if 'delivery_account_ids' in values:
|
||||||
|
for rec in self.filtered(lambda r: not r.default_delivery_account_id and rec.delivery_account_ids):
|
||||||
|
rec.default_delivery_account_id = rec.delivery_account_ids[0]
|
||||||
|
if 'default_delivery_account_id' in values:
|
||||||
|
for rec in self.filtered(lambda r: not r.delivery_account_ids and r.default_delivery_account_id):
|
||||||
|
rec.delivery_account_ids = [(4, rec.default_delivery_account_id.id)]
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
res = super().create(vals_list)
|
||||||
|
for rec in res.filtered(lambda r: not r.default_delivery_account_id and rec.delivery_account_ids):
|
||||||
|
rec.default_delivery_account_id = rec.delivery_account_ids[0]
|
||||||
|
for rec in res.filtered(lambda r: not r.delivery_account_ids and r.default_delivery_account_id):
|
||||||
|
rec.delivery_account_ids = [(4, rec.default_delivery_account_id.id)]
|
||||||
|
return res
|
||||||
22
bemade_delivery_partner_accounts/views/res_partner_views.xml
Normal file
22
bemade_delivery_partner_accounts/views/res_partner_views.xml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<odoo>
|
||||||
|
<record id="res_partner_view_form_delivery_accounts" model="ir.ui.view">
|
||||||
|
<field name="name">res.partner.view.form.delivery.accounts</field>
|
||||||
|
<field name="model">res.partner</field>
|
||||||
|
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<notebook position="inside">
|
||||||
|
<page string="Delivery Accounts">
|
||||||
|
<field name="default_delivery_account_id"/>
|
||||||
|
<field name="delivery_account_ids" widget="one2many">
|
||||||
|
<tree editable="bottom">
|
||||||
|
<field name="delivery_type"/>
|
||||||
|
<field name="account_number"/>
|
||||||
|
<field name="account_id"/>
|
||||||
|
</tree>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</odoo>
|
||||||
Loading…
Reference in a new issue