Merge branch 'mig/15.0/website_sale_signifyd' into '15.0'

mig/15.0/website_sale_signifyd into 15.0

See merge request hibou-io/hibou-odoo/suite!1104
This commit is contained in:
Jared Kipe
2021-10-06 20:11:16 +00:00
20 changed files with 884 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import controllers
from . import models

View File

@@ -0,0 +1,28 @@
{
'name': 'Signifyd Connector',
'author': 'Hibou Corp. <hello@hibou.io>',
'version': '15.0.1.0.0',
'category': 'Sale',
'description': """
Automate Order Fraud Detection with the Signifyd API.
""",
'website': 'https://hibou.io/',
'depends': [
'delivery',
'hibou_professional',
'stock',
'website_sale',
],
'data': [
'security/ir.model.access.csv',
'views/partner_views.xml',
'views/sale_views.xml',
'views/signifyd_views.xml',
'views/stock_views.xml',
'views/web_assets.xml',
'views/website_views.xml',
],
'installable': True,
'application': False,
'license': 'OPL-1',
}

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import signifyd

View File

@@ -0,0 +1,28 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
import json
from odoo.http import Controller, request, route
from werkzeug.exceptions import NotFound
class SignifydWebhooks(Controller):
@route(['/signifyd/cases/update'], type='json', auth='public', methods=['POST'], csrf=False, website=True)
def case_update(self, *args, **post):
return self._case_update()
def _case_update(self):
data = json.loads(request.httprequest.data)
vals = request.env['signifyd.connector'].process_post_values(data)
case_id = vals.get('case_id')
case = self._get_case(case_id)
if case:
case.update_case_info(vals)
return {'response': 'success'}
if case_id == 1:
# Special case when verifying webhook.
return {'response': 'success'}
raise NotFound('CaseId: %s Cannot be found.' % (case_id,))
def _get_case(self, case_id):
return request.env['signifyd.case'].sudo().search([('case_id', '=', case_id)], limit=1)

View File

@@ -0,0 +1,8 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import partner
from . import sale_order
from . import signifyd
from . import signifyd_connector
from . import stock
from . import website

View File

@@ -0,0 +1,22 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
signifyd_case_ids = fields.One2many('signifyd.case', 'partner_id', string='Signifyd Cases')
signifyd_case_count = fields.Integer(compute='_compute_signifyd_stats', string='Signifyd Cases')
signifyd_average_score = fields.Float(compute='_compute_signifyd_stats', string='Signifyd Score')
@api.depends('signifyd_case_ids')
def _compute_signifyd_stats(self):
for record in self:
cases = record.signifyd_case_ids
if cases:
record.signifyd_case_count = len(cases)
record.signifyd_average_score = sum(cases.mapped('score')) / record.signifyd_case_count
else:
record.signifyd_case_count = 0
record.signifyd_average_score = 0

View File

@@ -0,0 +1,156 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.http import request
class SaleOrder(models.Model):
_inherit = 'sale.order'
signifyd_case_id = fields.Many2one('signifyd.case', readonly=1, copy=False)
singifyd_score = fields.Float(related='signifyd_case_id.score')
signifyd_disposition_status = fields.Selection(related='signifyd_case_id.guarantee_disposition')
def action_view_signifyd_case(self):
self.ensure_one()
if not self.signifyd_case_id:
raise UserError('This order has no Signifyd Case')
form_id = self.env.ref('website_sale_signifyd.signifyd_case_form_view').id
context = {'create': False, 'delete': False}
return {
'type': 'ir.actions.act_window',
'name': 'Signifyd Case',
'view_mode': 'form',
'views': [(form_id, 'form')],
'res_model': 'signifyd.case',
'res_id': self.signifyd_case_id.id,
'context': context,
}
def action_confirm(self):
res = super().action_confirm()
for sale in self.filtered(lambda so: so._should_post_signifyd()):
_case = sale.post_signifyd_case()
return res
def _should_post_signifyd(self):
return self.state in ('sale', 'done') and not self.signifyd_case_id
def post_signifyd_case(self):
if not self.website_id.signifyd_connector_id:
return
browser_ip_address = request.httprequest.environ['REMOTE_ADDR']
if request.session:
checkout_token = request.session.session_token
order_session_id = checkout_token
else:
checkout_token = ''
# Session values for Signifyd post
sig_vals = self._prepare_signifyd_case_values(order_session_id, checkout_token, browser_ip_address)
case = self.env['signifyd.case'].post_case(self.website_id.signifyd_connector_id, sig_vals)
success_response = case.get('investigationId')
if success_response:
new_case = self.env['signifyd.case'].create({
'order_id': self.id,
'case_id': success_response,
'name': success_response,
'partner_id': self.partner_id.id,
})
self.write({'signifyd_case_id': new_case.id})
return new_case
# TODO do we need to raise an exception?
return None
@api.model
def _prepare_signifyd_case_values(self, order_session_id, checkout_token, browser_ip_address):
tx_status_type = {
'draft': 'FAILURE',
'pending': 'PENDING',
'authorized': 'SUCCESS',
'done': 'SUCCESS',
'cancel': 'FAILURE',
'error': 'ERROR',
}
recipients = self.partner_invoice_id + self.partner_shipping_id
new_case_vals = {
'decisionRequest': {
'paymentFraud': 'GUARANTEE',
},
'purchase': {
"orderSessionId": order_session_id,
"orderId": self.id,
"checkoutToken": checkout_token,
"browserIpAddress": browser_ip_address,
"currency": self.partner_id.currency_id.name,
"orderChannel": "WEB",
"totalPrice": self.amount_total,
'products': [
{
"itemId": line.product_id.id,
"itemName": line.product_id.name,
"itemIsDigital": False,
"itemCategory": line.product_id.categ_id.name,
"itemUrl": line.product_id.website_url or '',
"itemQuantity": line.product_uom_qty,
"itemPrice": line.price_unit,
"itemWeight": line.product_id.weight or 0.1,
}
for line in self.order_line if line.product_id
],
'shipments': [{
"shipper": carrier.name,
"shippingMethod": "ground",
"shippingPrice": self.amount_delivery,
}
for carrier in self.carrier_id
],
},
'recipients': [
{
"fullName": partner.name,
"confirmationEmail": partner.email,
"confirmationPhone": partner.phone,
"organization": partner.company_id.name,
"deliveryAddress": {
"streetAddress": partner.street,
"unit": partner.street2,
"city": partner.city,
"provinceCode": partner.state_id.code,
"postalCode": partner.zip,
"countryCode": partner.country_id.code,
}
}
for partner in recipients
],
'transactions': [
{
"parentTransactionId": None,
"transactionId": tx.id,
"gateway": tx.acquirer_id.name,
"paymentMethod": "CREDIT_CARD",
"gatewayStatusCode": tx_status_type.get(tx.state, 'PENDING'),
"type": "AUTHORIZATION",
"currency": self.partner_id.currency_id.name,
"amount": tx.amount,
# "avsResponseCode": "Y",
# "cvvResponseCode": "N",
"checkoutPaymentDetails": {
"holderName": tx.partner_id.name,
"billingAddress": {
"streetAddress": tx.partner_id.street,
"unit": tx.partner_id.street2,
"city": tx.partner_id.city,
"provinceCode": tx.partner_id.state_id.code,
"postalCode": tx.partner_id.zip,
"countryCode": tx.partner_id.country_id.code,
}
}
}
for tx in self.transaction_ids
],
}
return new_case_vals

View File

@@ -0,0 +1,150 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
import requests
import json
from datetime import datetime as dt
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class SignifydCase(models.Model):
_name = 'signifyd.case'
_description = 'Stores Signifyd case information on orders.'
order_id = fields.Many2one('sale.order', required=True)
partner_id = fields.Many2one('res.partner')
case_id = fields.Char(string='Case ID')
uuid = fields.Char(string='Unique ID')
status = fields.Selection([
('OPEN', 'Open'),
('DISMISSED', 'Dismissed'),
], string='Case Status')
name = fields.Char(string='Headline')
team_name = fields.Char(string='Team Name')
team_id = fields.Char(string='Team ID')
last_update = fields.Date('Last Update')
review_disposition = fields.Selection([
('UNSET', 'Pending'),
('FRAUD', 'Fraudulent'),
('GOOD', 'Good'),
], string='Review Status')
order_outcome = fields.Selection([
('PENDING', 'pending'),
('SUCCESSFUL', 'Successful'),
])
guarantee_disposition = fields.Selection([
('IN_REVIEW', 'Reviewing'),
('PENDING', 'Pending'),
('APPROVED', 'Approved'),
('DECLINED', 'Declined'),
('CANCELED', 'Canceled'),
], string='Guarantee Status')
disposition_reason = fields.Char('Disposition Reason')
score = fields.Float(string='Transaction Score')
adjusted_score = fields.Float(string='Adjusted Score')
signifyd_url = fields.Char('Signifyd.com', compute='_compute_signifyd_url')
def _get_connector(self):
return self.order_id.website_id.signifyd_connector_id
@api.model
def _compute_signifyd_url(self):
for record in self:
if record.case_id:
self.signifyd_url = 'https://app.signifyd.com/cases/%s' % (record.case_id, )
else:
self.signifyd_url = ''
def write(self, vals):
original_disposition = {c: c.guarantee_disposition for c in self}
res = super(SignifydCase, self).write(vals)
disposition = vals.get('guarantee_disposition', False)
for case in self.filtered(lambda c: c.order_id and original_disposition[c] != disposition):
case.order_id.message_post(body=_('Signifyd Updated Record to %s' % disposition),
subtype_xmlid='website_sale_signifyd.disposition_change')
return res
@api.model
def post_case(self, connector, values):
headers = connector.get_headers()
data = json.dumps(values, indent=4, sort_keys=True, default=str)
# TODO this should be in `signifyd.connector`
r = requests.post(
connector.API_URL + '/cases',
headers=headers,
data=data,
)
return r.json()
def get_case(self):
self.ensure_one()
if not self.case_id:
raise UserError('Case not represented in Signifyd.')
connector = self._get_connector()
headers = connector.get_headers()
r = requests.get(
connector.API_URL + '/cases/' + str(self.case_id),
headers=headers
)
return r.json()
def action_force_update_case(self):
for record in self:
record.update_case_info()
def update_case_info(self, vals=None):
self.ensure_one()
if not self.case_id:
raise UserError('Case not represented in Signifyd.')
if not vals:
case = self.get_case()
case_id = case.get('caseId')
if not case_id:
raise ValueError('Signifyd Case has no ID?')
team_id = case.get('teamId', self.team_id)
team_name = case.get('teamName', self.team_name)
uuid = case.get('uuid', self.uuid)
status = case.get('status', self.status)
review_disposition = case.get('reviewDisposition', self.review_disposition)
order_outcome = case.get('orderOutcome', self.order_outcome)
guarantee_disposition = case.get('guaranteeDisposition', self.guarantee_disposition)
adjusted_score = case.get('adjustedScore', self.adjusted_score)
score = case.get('score', self.score)
vals = {
'case_id': case_id,
'team_id': team_id,
'team_name': team_name,
'uuid': uuid,
'status': status,
'review_disposition': review_disposition,
'order_outcome': order_outcome,
'adjusted_score': adjusted_score,
'guarantee_disposition': guarantee_disposition,
'score': score,
'last_update': dt.now(), # why not just use
}
outcome = vals.get('guarantee_disposition')
if outcome == 'DECLINED':
connector = self._get_connector()
for user in connector.notify_user_ids:
self.create_notification(user, outcome)
self.write(vals)
def create_notification(self, user, outcome):
self.ensure_one()
vals = {
'summary': 'Signifyd Case %s %s' % (self.case_id, outcome),
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'user_id': user.id,
'res_id': self.order_id.id,
'res_model_id': self.env['ir.model']._get(self.order_id._name).id,
}
self.env['mail.activity'].create(vals)

View File

@@ -0,0 +1,148 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
import requests
from datetime import datetime as dt
from base64 import b64encode
import json
from odoo import api, fields, models
class SignifydConnector(models.Model):
_name = 'signifyd.connector'
_description = 'Interact with Signifyd API'
name = fields.Char(string='Connector Name', required=True)
test_mode = fields.Boolean(string='Test Mode')
secret_key = fields.Char(string='API Key', required=True)
secret_key_test = fields.Char(string='TEST API Key')
webhooks_registered = fields.Boolean(string='Successfully Registered Webhooks')
notify_user_ids = fields.Many2many('res.users', string='Receive decline notifications')
website_ids = fields.One2many('website', 'signifyd_connector_id', string='Used on Websites')
# TODO ideally this would be a regular constant
# however other entities currently use this by reference
API_URL = 'https://api.signifyd.com/v2'
def get_headers(self):
self.ensure_one()
# Check for prod or test mode
if self.test_mode:
api_key = self.secret_key_test
else:
api_key = self.secret_key
b64_auth_key = b64encode(api_key.encode()).decode().replace('=', '')
headers = {
'Authorization': 'Basic ' + b64_auth_key,
'Content-Type': 'application/json',
}
return headers
def register_webhooks(self):
self.ensure_one()
headers = self.get_headers()
# This should come from the website...
# we may need a better way to link the connector to the website.
base_url = None
website = self.env['website'].search([('signifyd_connector_id', '=', self.id)], limit=1)
if website and website.domain:
base_url = website.domain
if base_url.find('://') <= 0: # documentation says if no protocol use http
base_url = 'http://' + base_url
if not base_url:
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
values = {
"webhooks": [
# Given we are creating the cases, we do not need to know about it
# {
# "event": "CASE_CREATION",
# "url": base_url + "/signifyd/cases/update"
# },
{
"event": "CASE_RESCORE",
"url": base_url + "/signifyd/cases/update"
},
{
"event": "CASE_REVIEW",
"url": base_url + "/signifyd/cases/update"
},
{
"event": "GUARANTEE_COMPLETION",
"url": base_url + "/signifyd/cases/update"
},
]
}
data = json.dumps(values, indent=4)
r = requests.post(
self.API_URL + '/teams/webhooks',
headers=headers,
data=data,
)
# r.raise_for_status()
return r
def action_register_webhooks(self):
notification = {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': ('Signifyd Connector'),
'sticky': True,
},
}
res = self.register_webhooks()
if 200 <= res.status_code < 300:
notification['params']['type'] = 'success'
notification['params']['message'] = 'Successfully registered webhooks with Signifyd.'
self.webhooks_registered = True
return notification
else:
notification['params']['type'] = 'danger'
notification['params']['message'] = res.content.decode('utf-8')
try:
# trying to make a better error, not be exhaustive with error handling.
object = json.loads(notification['params']['message'])
notification['params']['message'] = '\n'.join([e[0] for e in (object.get('errors') or {}).values()])
except:
pass
self.webhooks_registered = False
return notification
def process_post_values(self, post):
# Construct dict from request data for endpoints
uuid = post.get('uuid')
case_id = post.get('caseId')
team_name = post.get('teamName')
team_id = post.get('teamId')
review_disposition = post.get('reviewDisposition')
guarantee_disposition = post.get('guaranteeDisposition')
order_outcome = post.get('orderOutcome')
status = post.get('status')
score = post.get('score')
disposition_reason = post.get('dispositionReason')
disposition = post.get('disposition')
last_update = str(dt.now())
values = {}
# Validate that the order and case match the request
values.update({'uuid': uuid}) if uuid else ''
values.update({'team_name': team_name}) if team_name else ''
values.update({'team_id': team_id}) if team_id else ''
values.update({'review_disposition': review_disposition}) if review_disposition else ''
values.update({'guarantee_disposition': guarantee_disposition}) if guarantee_disposition else ''
values.update({'order_outcome': order_outcome}) if order_outcome else ''
values.update({'status': status}) if status else ''
values.update({'score': score}) if score else ''
values.update({'case_id': case_id}) if case_id else ''
values.update({'disposition_reason': disposition_reason}) if disposition_reason else ''
values.update({'disposition': disposition}) if disposition else ''
values.update({'last_update': last_update})
return values

View File

@@ -0,0 +1,26 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import fields, models
from odoo.exceptions import UserError
class StockPicking(models.Model):
_inherit = 'stock.picking'
singifyd_case_id = fields.Many2one(related='sale_id.signifyd_case_id')
signifyd_hold = fields.Selection(related='sale_id.signifyd_disposition_status')
def action_view_signifyd_case(self):
self.ensure_one()
if not self.singifyd_case_id:
raise UserError('No Signifyd Case')
form_id = self.env.ref('website_sale_signifyd.signifyd_case_form_view').id
context = {'create': False, 'delete': False, 'id': self.sale_id.signifyd_case_id.id}
return {
'type': 'ir.actions.act_window',
'name': 'Signifyd Case',
'view_mode': 'form',
'views': [(form_id, 'form')],
'res_model': 'signifyd.case',
'res_id': self.singifyd_case_id.id,
'context': context,
}

View File

@@ -0,0 +1,9 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
class Website(models.Model):
_inherit = 'website'
signifyd_connector_id = fields.Many2one('signifyd.connector', ondelete='set null')

View File

@@ -0,0 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
manage_signifyd_connector,manage_signifyd_connector,model_signifyd_connector,base.group_erp_manager,1,1,1,1
access_signifyd_connector,access_signifyd_connector,model_signifyd_connector,base.group_user,1,0,0,0
public_signifyd_connector,public_signifyd_connector,model_signifyd_connector,base.group_public,1,0,0,0
portal_signifyd_connector,portal_signifyd_connector,model_signifyd_connector,base.group_portal,1,0,0,0
manage_signifyd_case,manage_signifyd_case,model_signifyd_case,base.group_erp_manager,1,1,1,1
access_signifyd_case,access_signifyd_case,model_signifyd_case,base.group_user,1,0,0,0
public_signifyd_case,public_signifyd_case,model_signifyd_case,base.group_public,1,0,0,0
portal_signifyd_case,portal_signifyd_case,model_signifyd_case,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 manage_signifyd_connector manage_signifyd_connector model_signifyd_connector base.group_erp_manager 1 1 1 1
3 access_signifyd_connector access_signifyd_connector model_signifyd_connector base.group_user 1 0 0 0
4 public_signifyd_connector public_signifyd_connector model_signifyd_connector base.group_public 1 0 0 0
5 portal_signifyd_connector portal_signifyd_connector model_signifyd_connector base.group_portal 1 0 0 0
6 manage_signifyd_case manage_signifyd_case model_signifyd_case base.group_erp_manager 1 1 1 1
7 access_signifyd_case access_signifyd_case model_signifyd_case base.group_user 1 0 0 0
8 public_signifyd_case public_signifyd_case model_signifyd_case base.group_public 1 0 0 0
9 portal_signifyd_case portal_signifyd_case model_signifyd_case base.group_portal 1 0 0 0

View File

@@ -0,0 +1 @@
from . import test_order_flow

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="partner_view_form_view_inherit" model="ir.ui.view">
<field name="name">res.partner.form.inherit</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<field name="signifyd_case_count" invisible="1"/>
<field name="signifyd_average_score" invisible="1"/>
<button class="oe_stat_button text-success" icon="fa-flag-checkered"
attrs="{'invisible': [('signifyd_average_score', '&lt;=', 600)]}">
<field string="Signifyd Score" name="signifyd_average_score" widget="statinfo"/>
</button>
<button class="oe_stat_button text-warning" icon="fa-flag"
attrs="{'invisible': [ '|', ('signifyd_average_score', '&gt;', 600), ('signifyd_average_score', '&lt;', 300)]}">
<field string="Signifyd Score" name="signifyd_average_score" widget="statinfo"/>
</button>
<button class="oe_stat_button text-danger" icon="fa-flag"
attrs="{'invisible': [('signifyd_average_score', '&gt;', 300)]}">
<field string="Signifyd Score" name="signifyd_average_score" widget="statinfo"/>
</button>
</xpath>
<!-- Page for Signifyd info -->
<xpath expr="//notebook//page[last()]" position="after">
<page string="Signifyd Cases">
<field name="signifyd_case_ids" widget="section_and_note_one2many" mode="tree">
<tree create="false" delete="false">
<field name="create_date" readonly="1"/>
<field name="name" string="ID" readonly="1"/>
<field name="order_id" readonly="1"/>
<field name="score" readonly="1"/>
<field name="guarantee_disposition" readonly="1"/>
<field name="last_update" string="Last Update" readonly="1"/>
</tree>
</field>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="sale_order_form_view_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='state']" position="after">
<field name="singifyd_score" invisible="1"/>
</xpath>
<xpath expr="//div[@name='button_box']" position="inside">
<field name="signifyd_case_id" invisible="1"/>
<button name="action_view_signifyd_case" type="object" class="oe_stat_button text-success"
icon="fa-flag-checkered"
attrs="{'invisible': [ '|', ('singifyd_score', '&lt;=', 600), ('signifyd_case_id', '=', False)]}">
<field string="Signifyd Score" name="singifyd_score" widget="statinfo"/>
</button>
<button name="action_view_signifyd_case" type="object" class="oe_stat_button text-warning"
icon="fa-flag"
attrs="{'invisible': [ '|', '|', ('singifyd_score', '&gt;', 600), ('singifyd_score', '&lt;=', 300), ('signifyd_case_id', '=', False)]}">
<field string="Signifyd Score" name="singifyd_score" widget="statinfo"/>
</button>
<button name="action_view_signifyd_case" type="object" class="oe_stat_button text-danger" icon="fa-flag"
attrs="{'invisible': [ '|', ('singifyd_score', '&gt;', 300), ('signifyd_case_id', '=', False)]}">
<field string="Signifyd Score" name="singifyd_score" widget="statinfo"/>
</button>
</xpath>
<xpath expr="//field[@name='payment_term_id']" position="after">
<field name="signifyd_case_id" attrs="{'invisible': [('signifyd_case_id', '=', False)]}"/>
<field name="signifyd_disposition_status" string="Signifyd Status"
attrs="{'invisible': [('signifyd_case_id', '=', False)]}"/>
</xpath>
</field>
</record>
<record id="sale_view_order_tree_inherit" model="ir.ui.view">
<field name="name">sale.order.tree.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_status']" position="after">
<field name="signifyd_disposition_status"/>
</xpath>
</field>
</record>
<record id="disposition_change" model="mail.message.subtype">
<field name="name">Signifyd Updated</field>
<field name="res_model">sale.order</field>
<field name="default" eval="True"/>
<field name="description">Signifyd Status Updated</field>
<field name="relation_field">signifyd_case_id.guarantee_disposition</field>
</record>
</odoo>

View File

@@ -0,0 +1,145 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="signifyd_case_form_view" model="ir.ui.view">
<field name="name">signifyd.form.view</field>
<field name="model">signifyd.case</field>
<field name="arch" type="xml">
<form string="Signifyd Case" class="o_signifyd_case">
<header>
<button name="action_force_update_case" type="object"
string="Force Update" class="oe_highlight"/>
</header>
<sheet>
<field name="score" invisible="1"/>
<div class="oe_button_box" name="button_box">
<field name="score" invisible="1"/>
<button class="oe_stat_button text-success" icon="fa-flag-checkered"
attrs="{'invisible': [('score', '&lt;=', 600)]}">
<field string="Score" name="score" widget="statinfo"/>
</button>
<button class="oe_stat_button text-warning" icon="fa-flag"
attrs="{'invisible': [ '|', ('score', '&gt;', 600), ('score', '&lt;', 300)]}">
<field string="Score" name="score" widget="statinfo"/>
</button>
<button class="oe_stat_button text-danger" icon="fa-flag"
attrs="{'invisible': [('score', '&gt;', 300)]}">
<field string="Score" name="score" widget="statinfo"/>
</button>
</div>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
<field name="signifyd_url" widget="url"/>
</div>
<group>
<group>
<field name="last_update"/>
<field name="uuid"/>
<field name="case_id"/>
<field name="status"/>
<field name="order_outcome"/>
<field name="review_disposition"/>
<field name="guarantee_disposition"/>
<field name="disposition_reason"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="action_signifyd_case_form" model="ir.actions.act_window">
<field name="name">Signifyd Case Information</field>
<field name="res_model">signifyd.case</field>
<field name="view_mode">form</field>
<field name="view_id" ref="website_sale_signifyd.signifyd_case_form_view"/>
</record>
<record id="signifyd_connector_form_view" model="ir.ui.view">
<field name="name">signifyd.form.view</field>
<field name="model">signifyd.connector</field>
<field name="arch" type="xml">
<form string="Signifyd Connector" class="o_signifyd_connector">
<header>
<button name="action_register_webhooks" type="object"
string="Register Webhooks" class="oe_highlight"
attrs="{'invisible': [('webhooks_registered', '=', True)]}"/>
<button name="action_register_webhooks" type="object"
string="Re-register Webhooks"
attrs="{'invisible': [('webhooks_registered', '=', False)]}"/>
</header>
<sheet>
<div name="status_box">
<field name="webhooks_registered" invisible="1"/>
<div class="text-success float-right"
attrs="{'invisible': [('webhooks_registered', '=', False)]}">
<i class="fa fa-check-square" title="Webhooks Active"/>
<strong>
Webhooks Active
</strong>
</div>
<div class="text-danger float-right"
attrs="{'invisible': [('webhooks_registered', '=', True)]}">
<i class="fa fa-square" title="Webhooks Inactive"/>
<strong>
Webhooks Inactive
</strong>
</div>
</div>
<div class="oe_title">
<label for="test_mode"/>
<field name="test_mode"/>
<h1>
<field name="name"/>
</h1>
</div>
<group>
<group>
<field name="secret_key" attrs="{'invisible': [('test_mode', '=', True)]}"/>
<field name="secret_key_test" attrs="{'invisible': [('test_mode', '!=', True)]}"/>
<p class="text-muted">
Optional: Add users to be notified if a sale order is declined by Signifyd.
</p>
<field name="notify_user_ids" widget="many2many_tags"/>
<field name="website_ids" widget="many2many_tags"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="signifyd_connector_tree_view" model="ir.ui.view">
<field name="name">signifyd.tree.view</field>
<field name="model">signifyd.connector</field>
<field name="arch" type="xml">
<tree string="Signifyd Connectors">
<field name="name"/>
<field name="test_mode"/>
<field name="webhooks_registered"/>
</tree>
</field>
</record>
<record id="action_signifyd_connector" model="ir.actions.act_window">
<field name="name">Signifyd Connectors</field>
<field name="res_model">signifyd.connector</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem
name="Signifyd"
parent="website.menu_website_global_configuration"
action="action_signifyd_connector"
id="menu_action_signifyd_connector"
sequence="10" />
</odoo>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="stock_picking_form_inherit" model="ir.ui.view">
<field name="name">stock.picking.form.view.inherit</field>
<field name="model">stock.picking</field>
<field name="inherit_id" ref="stock.view_picking_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<field name="signifyd_hold" invisible="1"/>
<field name="singifyd_case_id" invisible="1"/>
<button name="action_view_signifyd_case" string="On Hold" type="object" class="oe_stat_button text-danger"
icon="fa-hand-stop-o" attrs="{'invisible': [ '|', ('signifyd_hold', '=', 'APPROVED'), ('singifyd_case_id', '=', False)]}"/>
<button name="action_view_signifyd_case" string="Approved" type="object" class="oe_stat_button text-success"
icon="fa-thumbs-o-up" attrs="{'invisible': [ '|', ('signifyd_hold', '!=', 'APPROVED'), ('singifyd_case_id', '=', False)]}"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<template id="checkout" inherit_id="payment.checkout">
<xpath expr="." position="inside">
<script defer="1" type="text/javascript" id="sig-api" t-att-data-order-session-id="request.session.session_token"
src="https://cdn-scripts.signifyd.com/api/script-tag.js"></script>
</xpath>
</template>
</odoo>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_website_form_inherit" model="ir.ui.view">
<field name="name">website.form.inherit</field>
<field name="model">website</field>
<field name="inherit_id" ref="website.view_website_form"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='other']" position="inside">
<field name="signifyd_connector_id"/>
</xpath>
</field>
</record>
</odoo>