Skip to content

Commit

Permalink
[ADD] sale_gelato: integrate with Gelato for Print-On-Demand
Browse files Browse the repository at this point in the history
This commit adds the possibility to fetch attributes of a template
configured on Gelato to automatically create the corresponding variants
in Odoo. When such variant is included in a sales order, the order is
forwarded to Gelato to trigger the printing and dropshipping of the
product.

task-3935688

Part-of: odoo#193457
Related: odoo/enterprise#77839
Related: odoo/documentation#11823
Signed-off-by: Antoine Vandevenne (anv) <[email protected]>
Co-authored-by: Antoine Vandevenne (anv) <[email protected]>
  • Loading branch information
anko-odoo and AntoineVDV committed Feb 6, 2025
1 parent dead43d commit d57b032
Show file tree
Hide file tree
Showing 43 changed files with 1,638 additions and 14 deletions.
18 changes: 18 additions & 0 deletions .tx/config
Original file line number Diff line number Diff line change
Expand Up @@ -2080,6 +2080,15 @@ resource_name = sale_expense_margin
replace_edited_strings = false
keep_translations = false

[o:odoo:p:odoo-18:r:sale_gelato]
file_filter = addons/sale_gelato/i18n/<lang>.po
source_file = addons/sale_gelato/i18n/sale_gelato.pot
type = PO
minimum_perc = 0
resource_name = sale_gelato
replace_edited_strings = false
keep_translations = false

[o:odoo:p:odoo-18:r:sale_loyalty]
file_filter = addons/sale_loyalty/i18n/<lang>.po
source_file = addons/sale_loyalty/i18n/sale_loyalty.pot
Expand Down Expand Up @@ -2953,6 +2962,15 @@ resource_name = website_sale_comparison_wishlist
replace_edited_strings = false
keep_translations = false

[o:odoo:p:odoo-18:r:website_sale_gelato]
file_filter = addons/website_sale_gelato/i18n/<lang>.po
source_file = addons/website_sale_gelato/i18n/website_sale_gelato.pot
type = PO
minimum_perc = 0
resource_name = website_sale_gelato
replace_edited_strings = false
keep_translations = false

[o:odoo:p:odoo-18:r:website_sale_loyalty]
file_filter = addons/website_sale_loyalty/i18n/<lang>.po
source_file = addons/website_sale_loyalty/i18n/website_sale_loyalty.pot
Expand Down
28 changes: 14 additions & 14 deletions addons/product/models/product_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,13 +203,9 @@ def _compute_item_count(self):

def _compute_product_document_count(self):
for template in self:
template.product_document_count = template.env['product.document'].search_count([
'|',
'&', ('res_model', '=', 'product.template'), ('res_id', '=', template.id),
'&',
('res_model', '=', 'product.product'),
('res_id', 'in', template.product_variant_ids.ids),
])
template.product_document_count = template.env['product.document'].search_count(
template._get_product_document_domain()
)

@api.depends('image_1920', 'image_1024')
def _compute_can_image_1024_be_zoomed(self):
Expand Down Expand Up @@ -630,13 +626,7 @@ def action_open_documents(self):
'default_res_id': self.id,
'default_company_id': self.company_id.id,
},
'domain': [
'|',
'&', ('res_model', '=', 'product.template'), ('res_id', '=', self.id),
'&',
('res_model', '=', 'product.product'),
('res_id', 'in', self.product_variant_ids.ids),
],
'domain': self._get_product_document_domain(),
'target': 'current',
'help': """
<p class="o_view_nocontent_smiling_face">
Expand Down Expand Up @@ -1463,6 +1453,16 @@ def _get_contextual_pricelist(self):
"""
return self.env['product.pricelist'].browse(self.env.context.get('pricelist'))

def _get_product_document_domain(self):
self.ensure_one()
return expression.OR([
expression.AND([[('res_model', '=', 'product.template')], [('res_id', '=', self.id)]]),
expression.AND([
[('res_model', '=', 'product.product')],
[('res_id', 'in', self.product_variant_ids.ids)],
])
])

###################
# DEMO DATA SETUP #
###################
Expand Down
5 changes: 5 additions & 0 deletions addons/sale_gelato/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import controlers
from . import models
from . import wizards
20 changes: 20 additions & 0 deletions addons/sale_gelato/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

{
'name': "Gelato",
'summary': "Place orders through Gelato's print-on-demand service",
'category': 'Sales/Sales',
'depends': ['sale_management', 'delivery'],
'data': [
'data/product_data.xml',
'data/delivery_carrier_data.xml', # Depends on product_data.xml
'data/mail_template_data.xml',

'views/delivery_carrier_views.xml',
'views/product_document_views.xml',
'views/product_product_views.xml',
'views/product_template_views.xml',
'wizards/res_config_settings_views.xml',
],
'license': 'LGPL-3',
}
10 changes: 10 additions & 0 deletions addons/sale_gelato/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

COUNTRIES_WITHOUT_ZIPCODE = [
'AO', 'AG', 'AW', 'BS', 'BZ', 'BJ', 'BW', 'BF', 'BI', 'CM', 'CF', 'KM',
'CG', 'CD', 'CK', 'CI', 'DJ', 'DM', 'GQ', 'ER', 'FJ', 'TF', 'GM', 'GH',
'GD', 'GN', 'GY', 'HK', 'IE', 'JM', 'KE', 'KI', 'MO', 'MW', 'ML', 'MR',
'MU', 'MS', 'NR', 'AN', 'NU', 'KP', 'PA', 'QA', 'RW', 'KN', 'LC', 'ST',
'SC', 'SL', 'SB', 'SO', 'ZA', 'SR', 'SY', 'TZ', 'TL', 'TK', 'TO', 'TT',
'TV', 'UG', 'AE', 'VU', 'YE', 'ZW'
]
3 changes: 3 additions & 0 deletions addons/sale_gelato/controlers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

from . import main
121 changes: 121 additions & 0 deletions addons/sale_gelato/controlers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.

import hmac
import logging
import pprint

from werkzeug.exceptions import Forbidden

from odoo import SUPERUSER_ID, _
from odoo.http import Controller, request, route


_logger = logging.getLogger(__name__)


class GelatoController(Controller):
_webhook_url = '/gelato/webhook'

@route(_webhook_url, type='http', methods=['POST'], auth='public', csrf=False)
def gelato_webhook(self):
""" Process the notification data sent by Gelato to the webhook.
See https://dashboard.gelato.com/docs/orders/order_details/#order-statuses for the event
codes.
:return: An empty response to acknowledge the notification.
:rtype: odoo.http.Response
"""
event_data = request.get_json_data()
_logger.info("Webhook notification received from Gelato:\n%s", pprint.pformat(event_data))

if event_data['event'] == 'order_status_updated':
# Check the signature of the webhook notification.
order_id = int(event_data['orderReferenceId'])
order_sudo = request.env['sale.order'].sudo().browse(order_id).exists()
received_signature = request.httprequest.headers.get('signature', '')
self._verify_notification_signature(received_signature, order_sudo)

# Process the event.
fulfillment_status = event_data.get('fulfillmentStatus')
if fulfillment_status == 'failed':
# Log a message on the order.
log_message = _(
"Gelato could not proceed with the fulfillment of order %(order_reference)s:"
" %(gelato_message)s",
order_reference=order_sudo.display_name,
gelato_message=event_data['comment'],
)
order_sudo.message_post(
body=log_message, author_id=request.env.ref('base.partner_root').id
)
elif fulfillment_status == 'canceled':
# Cancel the order.
order_sudo.with_user(SUPERUSER_ID)._action_cancel()

# Manually cache the currency while in a sudoed environment to prevent an
# AccessError. The state of the sales order is a dependency of
# `untaxed_amount_to_invoice`, which is a monetary field. They require the currency
# to ensure the values are saved in the correct format. However, the currency cannot
# be read directly during the flush due to access rights, necessitating manual
# caching.
order_sudo.order_line.currency_id

# Send the generic order cancellation email.
order_sudo.message_post_with_source(
source_ref=request.env.ref('sale.mail_template_sale_cancellation'),
author_id=request.env.ref('base.partner_root').id,
)
elif fulfillment_status == 'in_transit':
# Send the Gelato order status update email.
tracking_data = self._extract_tracking_data(item_data=event_data['items'])
order_sudo.with_context({'tracking_data': tracking_data}).message_post_with_source(
source_ref=request.env.ref('sale_gelato.order_status_update'),
author_id=request.env.ref('base.partner_root').id,
)
elif fulfillment_status == 'delivered':
# Send the Gelato order status update email.
order_sudo.with_context({'order_delivered': True}).message_post_with_source(
source_ref=request.env.ref('sale_gelato.order_status_update'),
author_id=request.env.ref('base.partner_root').id,
)
elif fulfillment_status == 'returned':
# Log a message on the order.
log_message = _(
"Gelato has returned order %(reference)s.", reference=order_sudo.display_name
)
order_sudo.message_post(
body=log_message, author_id=request.env.ref('base.partner_root').id
)
return request.make_json_response('')

@staticmethod
def _verify_notification_signature(received_signature, order_sudo):
""" Check if the received signature matches the expected one.
:param str received_signature: The received signature.
:param sale.order order_sudo: The sales order for which the webhook notification was sent.
:return: None
:raise Forbidden: If the signatures don't match.
"""
company_sudo = order_sudo.company_id.sudo() # In sudo mode to read on the company.
expected_signature = company_sudo.gelato_webhook_secret
if not hmac.compare_digest(received_signature, expected_signature):
_logger.warning("Received notification with invalid signature.")
raise Forbidden()

@staticmethod
def _extract_tracking_data(item_data):
""" Extract the tracking URL and code from the item data.
:param dict item_data: The item data.
:return: The extracted tracking data.
:rtype: dict
"""
tracking_data = {}
for i in item_data:
for fulfilment_data in i['fulfillments']:
tracking_data.setdefault(
fulfilment_data['trackingUrl'], fulfilment_data['trackingCode']
) # Different items can have the same tracking URL.
return tracking_data
22 changes: 22 additions & 0 deletions addons/sale_gelato/data/delivery_carrier_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">

<record id="standard_delivery" model="delivery.carrier">
<field name="name">Standard Delivery</field>
<field name="delivery_type">gelato</field>
<field name="integration_level">rate</field>
<field name="gelato_shipping_service_type">normal</field>
<field name="product_id" ref="sale_gelato.standard_delivery_product"/>
<field name="prod_environment" eval="True"/>
</record>

<record id="express_delivery" model="delivery.carrier">
<field name="name">Express Delivery</field>
<field name="delivery_type">gelato</field>
<field name="integration_level">rate</field>
<field name="gelato_shipping_service_type">express</field>
<field name="product_id" ref="sale_gelato.express_delivery_product"/>
<field name="prod_environment" eval="True"/>
</record>

</odoo>
48 changes: 48 additions & 0 deletions addons/sale_gelato/data/mail_template_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?xml version='1.0' encoding='utf-8'?>
<odoo noupdate="1">

<record id="order_status_update" model="mail.template">
<field name="name">Gelato: Order status update</field>
<field name="model_id" ref="model_sale_order"/>
<field name="subject">{{ object.reference }}</field>
<field name="partner_to">{{ object.partner_id.email and object.partner_id.id or object.partner_id.parent_id.id }}</field>
<field name="description">Sent to the customer when Gelato updates the status of an order</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px; font-size: 13px;">
Hello <t t-out="object.partner_id.name or ''">Brandon Freeman</t>,<br/><br/>
<!-- Order in transit body -->
<t t-if="ctx.get('tracking_data')">
We are glad to inform you that your order is in transit.
<t t-if="len(ctx['tracking_data']) == 1">
<t t-set="tracking_url" t-value="list(ctx['tracking_data'].keys())[0]"/>
Your tracking number is <a t-attf-href="tracking_url" t-out="ctx['tracking_data'][tracking_url]"/>.
<br/><br/>
</t>
<t t-else="">
Your tracking numbers are:
<ul>
<li t-foreach="ctx['tracking_data']" t-as="tracking_url">
<a t-attf-href="{{tracking_url}}" t-out="ctx['tracking_data'][tracking_url]"/>
</li>
</ul>
</t>
</t>
<!-- Order delivered body -->
<t t-if="ctx.get('order_delivered')">
We are glad to inform you that your order has been delivered.
<br/><br/>
</t>
Thank you,
<t t-if="object.user_id.name">
<br />
<t t-out="object.user_id.name or ''">--<br/>Mitchell Admin</t>
</t>
</p>
</div>
</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>

</odoo>
4 changes: 4 additions & 0 deletions addons/sale_gelato/data/neutralize.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- disable Gelato
UPDATE res_company
SET gelato_api_key = NULL,
gelato_webhook_secret = NULL;
24 changes: 24 additions & 0 deletions addons/sale_gelato/data/product_data.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">

<record id="standard_delivery_product" model="product.product">
<field name="name">Standard Delivery (Gelato)</field>
<field name="default_code">normal</field>
<field name="type">service</field>
<field name="categ_id" ref="delivery.product_category_deliveries"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="list_price">0.0</field>
</record>

<record id="express_delivery_product" model="product.product">
<field name="name">Express Delivery (Gelato)</field>
<field name="default_code">express</field>
<field name="type">service</field>
<field name="categ_id" ref="delivery.product_category_deliveries"/>
<field name="sale_ok" eval="False"/>
<field name="purchase_ok" eval="False"/>
<field name="list_price">0.0</field>
</record>

</odoo>
Loading

0 comments on commit d57b032

Please sign in to comment.