[REF] website_sale_signifyd: no longer hook on payment controller, improve everything

This commit is contained in:
Jared Kipe
2021-06-14 14:39:06 -07:00
parent 2062edcb8c
commit d9178ee8a2
16 changed files with 254 additions and 289 deletions

View File

@@ -9,15 +9,17 @@ Automate Order Fraud Detection with the Signifyd API.
'website': 'https://hibou.io/', 'website': 'https://hibou.io/',
'depends': [ 'depends': [
'website_sale', 'website_sale',
'stock',
'delivery',
], ],
'data': [ 'data': [
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/company_views.xml',
'views/partner_views.xml', 'views/partner_views.xml',
'views/sale_views.xml', 'views/sale_views.xml',
'views/signifyd_views.xml', 'views/signifyd_views.xml',
'views/stock_views.xml', 'views/stock_views.xml',
'views/web_assets.xml', 'views/web_assets.xml',
'views/website_views.xml',
], ],
'installable': True, 'installable': True,
'application': False, 'application': False,

View File

@@ -1,2 +1 @@
from . import main
from . import signifyd from . import signifyd

View File

@@ -1,27 +0,0 @@
from odoo.http import request, route
from odoo.addons.website_sale.controllers.main import WebsiteSale
import logging
_logger = logging.getLogger(__name__)
class WebsiteSale(WebsiteSale):
@route(['/shop/confirmation'], type='http', auth="public", website=True, sitemap=False)
def payment_confirmation(self, **post):
res = super(WebsiteSale, self).payment_confirmation()
# order_session_id = request.session.session_token
checkout_token = request.session.session_token
order_session_id = checkout_token # TODO what is the appropriate variable?
_logger.warn(str(request.session))
browser_ip_address = request.httprequest.environ['REMOTE_ADDR']
sale_order_id = request.session.get('sale_last_order_id')
if sale_order_id:
order = request.env['sale.order'].sudo().browse(sale_order_id)
# Post completed order to Signifyd
signifyd = request.env.company.signifyd_connector_id
if signifyd:
# TODO should the signifyd variable be used?
order.post_signifyd_case(order_session_id, checkout_token, browser_ip_address)
return res

View File

@@ -4,36 +4,19 @@ from odoo.http import Response
class SignifydWebhooks(Controller): class SignifydWebhooks(Controller):
@route(['/cases/creation'], type='json', auth='public', methods=['POST'], csrf=False)
def case_creation(self, *args, **post):
data = json.loads(request.httprequest.data)
vals = request.env['signifyd.connector'].process_post_values(data)
# Update case with info
case = request.env['signifyd.case'].sudo().search([('case_id', '=', vals['case_id'])])
if case:
case.sudo().update_case_info(vals)
# Request guarantee for case if eligible
try:
case.request_guarantee()
if case.guarantee_requested and not case.guarantee_eligible:
# Only alert Signifyd to stop trying if we have at least tried once already
return Response({'response': 'success'}, status=200, mimetype='application/json')
# TODO what would the return case be here?
except:
# Signifyd API will try again up to 15 times if a non-2** code is returned
return Response({'response': 'failed'}, status=500, mimetype='application/json')
# TODO what would the return case be here?
@route(['/cases/update'], type='json', auth='public', methods=['POST'], csrf=False) @route(['/signifyd/cases/update'], type='json', auth='public', methods=['POST'], csrf=False, website=True)
def case_update(self, *args, **post): def case_update(self, *args, **post):
return self._case_update()
def _case_update(self):
data = json.loads(request.httprequest.data) data = json.loads(request.httprequest.data)
vals = request.env['signifyd.connector'].process_post_values(data) vals = request.env['signifyd.connector'].process_post_values(data)
case = request.env['signifyd.case'].sudo().search([('case_id', '=', vals['case_id'])]) case = self._get_case(vals.get('case_id'))
if case: if case:
case.update_case_info(vals) case.update_case_info(vals)
return Response({'response': 'success'}, status=200, mimetype='application/json')
return Response({'response': 'failed'}, status=500, mimetype='application/json')
outcome = vals.get('guarantee_disposition') def _get_case(self, case_id):
if case and outcome == 'DECLINED': return request.env['signifyd.case'].sudo().search([('case_id', '=', case_id)], limit=1)
for user in request.env.company.signifyd_connector_id.notify_user_ids:
case.sudo().create_notification(user, outcome)
# TODO any return result?

View File

@@ -1,6 +1,6 @@
from . import company
from . import partner from . import partner
from . import sale_order from . import sale_order
from . import signifyd from . import signifyd
from . import signifyd_connector from . import signifyd_connector
from . import stock from . import stock
from . import website

View File

@@ -1,8 +0,0 @@
from odoo import fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
# TODO move to website
signifyd_connector_id = fields.Many2one('signifyd.connector')

View File

@@ -8,6 +8,7 @@ class ResPartner(models.Model):
signifyd_case_count = fields.Integer(compute='_compute_signifyd_stats', 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') signifyd_average_score = fields.Float(compute='_compute_signifyd_stats', string='Signifyd Score')
@api.depends('signifyd_case_ids')
def _compute_signifyd_stats(self): def _compute_signifyd_stats(self):
for record in self: for record in self:
cases = record.signifyd_case_ids cases = record.signifyd_case_ids

View File

@@ -1,15 +1,19 @@
from odoo import api, fields, models from odoo import api, fields, models
from odoo.exceptions import UserError
from odoo.http import request
class SaleOrder(models.Model): class SaleOrder(models.Model):
_inherit = 'sale.order' _inherit = 'sale.order'
signifyd_case_id = fields.Many2one('signifyd.case', readonly=1) signifyd_case_id = fields.Many2one('signifyd.case', readonly=1, copy=False)
singifyd_score = fields.Float(related='signifyd_case_id.score') singifyd_score = fields.Float(related='signifyd_case_id.score')
signifyd_disposition_status = fields.Selection(related='signifyd_case_id.guarantee_disposition') signifyd_disposition_status = fields.Selection(related='signifyd_case_id.guarantee_disposition')
def action_view_signifyd_case(self): def action_view_signifyd_case(self):
self.ensure_one() 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 form_id = self.env.ref('website_sale_signifyd.signifyd_case_form_view').id
context = {'create': False, 'delete': False} context = {'create': False, 'delete': False}
return { return {
@@ -22,128 +26,126 @@ class SaleOrder(models.Model):
'context': context, 'context': context,
} }
def post_signifyd_case(self, order_session_id, checkout_token, browser_ip_address): def action_confirm(self):
res = super().action_confirm()
for sale in self.filtered(lambda so: so.state in ('sale', 'done') and not so.signifyd_case_id):
_case = sale.post_signifyd_case()
return res
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 # Session values for Signifyd post
data = { sig_vals = self._prepare_signifyd_case_values(order_session_id, checkout_token, browser_ip_address)
'order_session_id': order_session_id,
'checkout_token': checkout_token,
'browser_ip_address': browser_ip_address,
}
sig_vals = self.prepare_signifyd_case_values(data)
case_res = self.env['signifyd.case'].post_case(sig_vals) case = self.env['signifyd.case'].post_case(self.website_id.signifyd_connector_id, sig_vals)
success_response = case_res.get('investigationId') success_response = case.get('investigationId')
if success_response: if success_response:
new_case = self.env['signifyd.case'].create({ new_case = self.env['signifyd.case'].create({
'order_id': self.id, 'order_id': self.id,
'case_id': success_response, 'case_id': success_response,
'name': success_response, 'name': success_response,
'partner_id': self.partner_id.id,
}) })
self.write({'signifyd_case_id': new_case.id}) self.write({'signifyd_case_id': new_case.id})
self.partner_id.write({
'signifyd_case_ids': [(4, new_case.id)],
})
return new_case return new_case
# TODO do we need to raise an exception?
return None
@api.model @api.model
def prepare_signifyd_case_values(self, data): def _prepare_signifyd_case_values(self, order_session_id, checkout_token, browser_ip_address):
order_session_id = data.get('order_session_id') tx_status_type = {
checkout_token = data.get('checkout_token') 'draft': 'FAILURE',
browser_ip_address = data.get('browser_ip_address') 'pending': 'PENDING',
'authorized': 'SUCCESS',
new_case_vals = {} 'done': 'SUCCESS',
'cancel': 'FAILURE',
new_case_vals['purchase'] = { 'error': 'ERROR',
"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,
} }
recipients = self.partner_invoice_id + self.partner_shipping_id
new_case_vals['purchase']['products'] = [] new_case_vals = {
for line in self.order_line: 'decisionRequest': {
product = line.product_id 'paymentFraud': 'GUARANTEE',
vals = { },
"itemId": product.id, 'purchase': {
"itemName": product.name, "orderSessionId": order_session_id,
"itemIsDigital": False, "orderId": self.id,
"itemCategory": product.categ_id.name, "checkoutToken": checkout_token,
"itemUrl": product.website_url, "browserIpAddress": browser_ip_address,
"itemQuantity": line.product_uom_qty,
"itemPrice": line.price_unit,
"itemWeight": product.weight,
}
new_case_vals['purchase']['products'].append(vals)
new_case_vals['purchase']['shipments'] = []
if self.carrier_id:
vals = {
"shipper": self.carrier_id.name,
"shippingMethod": "ground",
"shippingPrice": self.amount_delivery,
}
new_case_vals['purchase']['shipments'].append(vals)
new_case_vals['recipients'] = []
recipients = [self.partner_invoice_id, self.partner_shipping_id]
for partner in recipients:
vals = {
"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,
}
}
new_case_vals['recipients'].append(vals)
new_case_vals['transactions'] = []
# payment.transaction
for tx in self.transaction_ids:
tx_status_type = {
'draft': 'FAILURE',
'pending': 'PENDING',
'authorized': 'SUCCESS',
'done': 'SUCCESS',
'cancel': 'FAILURE',
'error': 'ERROR',
}
tx_status = tx_status_type[tx.state]
vals = {
"parentTransactionId": None,
"transactionId": tx.id,
"gateway": tx.acquirer_id.name,
"paymentMethod": "CREDIT_CARD",
"gatewayStatusCode": tx_status,
"type": "AUTHORIZATION",
"currency": self.partner_id.currency_id.name, "currency": self.partner_id.currency_id.name,
"amount": tx.amount, "orderChannel": "WEB",
"avsResponseCode": "Y", "totalPrice": self.amount_total,
"cvvResponseCode": "N", 'products': [
"checkoutPaymentDetails": { {
"holderName": tx.partner_id.name, "itemId": line.product_id.id,
"billingAddress": { "itemName": line.product_id.name,
"streetAddress": tx.partner_id.street, "itemIsDigital": False,
"unit": tx.partner_id.street2, "itemCategory": line.product_id.categ_id.name,
"city": tx.partner_id.city, "itemUrl": line.product_id.website_url or '',
"provinceCode": tx.partner_id.state_id.code, "itemQuantity": line.product_uom_qty,
"postalCode": tx.partner_id.zip, "itemPrice": line.price_unit,
"countryCode": tx.partner_id.country_id.code, "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
new_case_vals['transactions'].append(vals) ],
'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 return new_case_vals

View File

@@ -42,97 +42,77 @@ class SignifydCase(models.Model):
('CANCELED', 'Canceled'), ('CANCELED', 'Canceled'),
], string='Guarantee Status') ], string='Guarantee Status')
disposition_reason = fields.Char('Disposition Reason') disposition_reason = fields.Char('Disposition Reason')
guarantee_eligible = fields.Boolean('Eligible for Guarantee')
guarantee_requested = fields.Boolean('Requested Guarantee')
score = fields.Float(string='Transaction Score') score = fields.Float(string='Transaction Score')
adjusted_score = fields.Float(string='Adjusted Score') adjusted_score = fields.Float(string='Adjusted Score')
signifyd_url = fields.Char('Signifyd.com', compute='_compute_signifyd_url') 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 @api.model
def _compute_signifyd_url(self): def _compute_signifyd_url(self):
for record in self: for record in self:
if record.case_id: if record.case_id:
self.signifyd_url = 'https://app.signifyd.com/cases/%s' % record.case_id self.signifyd_url = 'https://app.signifyd.com/cases/%s' % (record.case_id, )
else: else:
self.signifyd_url = '' self.signifyd_url = ''
def write(self, vals): def write(self, vals):
original_disposition = {c: c.guarantee_disposition for c in self} original_disposition = {c: c.guarantee_disposition for c in self}
res = super(SignifydCase, self).write(vals) res = super(SignifydCase, self).write(vals)
disposition = vals.get('guarantee_disposition') disposition = vals.get('guarantee_disposition', False)
for case in self: for case in self.filtered(lambda c: c.order_id and original_disposition[c] != disposition):
if case.order_id and original_disposition[case] != disposition: case.order_id.message_post(body=_('Signifyd Updated Record to %s' % disposition),
self.order_id.message_post(body=_('Signifyd Updated Record to %s' % vals['guarantee_disposition']), subtype='website_sale_signifyd.disposition_change')
subtype='website_sale_signifyd.disposition_change')
return res return res
@api.model @api.model
def post_case(self, values): def post_case(self, connector, values):
signifyd = self.env['signifyd.connector'] # TODO HOW, this shouldn't be a singleton headers = connector.get_headers()
headers = signifyd.get_headers()
data = json.dumps(values, indent=4, sort_keys=True, default=str) data = json.dumps(values, indent=4, sort_keys=True, default=str)
# TODO this should be in `signifyd.connector` # TODO this should be in `signifyd.connector`
r = requests.post( r = requests.post(
signifyd.API_URL + '/cases', connector.API_URL + '/cases',
headers=headers, headers=headers,
data=data, data=data,
) )
return r.json() return r.json()
@api.model
def get_case(self): def get_case(self):
# TODO See above.... self.ensure_one()
signifyd = self.env['signifyd.connector'] if not self.case_id:
headers = signifyd.get_headers() raise UserError('Case not represented in Signifyd.')
connector = self._get_connector()
headers = connector.get_headers()
r = requests.get( r = requests.get(
signifyd.API_URL + '/cases/' + str(self.case_id), connector.API_URL + '/cases/' + str(self.case_id),
headers=headers headers=headers
) )
return r.json() return r.json()
@api.model
def request_guarantee(self, *args):
# TODO See above....
signifyd = self.env['signifyd.connector']
headers = signifyd.get_headers()
values = json.dumps({"caseId": self.case_id})
r = requests.post(
signifyd.API_URL + '/async/guarantees',
headers=headers,
data=values,
)
if 200 <= r.status_code < 300:
self.write({'guarantee_requested': True})
else:
msg = r.content.decode("utf-8")
raise UserError(_(msg))
def action_request_guarantee(self):
for record in self:
record.request_guarantee()
def action_force_update_case(self): def action_force_update_case(self):
for record in self: for record in self:
record.update_case_info() record.update_case_info()
@api.model
def update_case_info(self, vals=None): 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: if not vals:
case = self.get_case() case = self.get_case()
case_id = case.get('caseId') case_id = case.get('caseId')
team_id = case.get('teamId') if not case_id:
team_name = case.get('teamName') raise ValueError('Signifyd Case has no ID?')
uuid = case.get('uuid') team_id = case.get('teamId', self.team_id)
status = case.get('status') team_name = case.get('teamName', self.team_name)
review_disposition = case.get('reviewDisposition') uuid = case.get('uuid', self.uuid)
order_outcome = case.get('orderOutcome') status = case.get('status', self.status)
guarantee_disposition = case.get('guaranteeDisposition') review_disposition = case.get('reviewDisposition', self.review_disposition)
adjusted_score = case.get('adjustedScore') order_outcome = case.get('orderOutcome', self.order_outcome)
score = case.get('score') guarantee_disposition = case.get('guaranteeDisposition', self.guarantee_disposition)
guarantee_eligible = case.get('guaranteeEligible') adjusted_score = case.get('adjustedScore', self.adjusted_score)
# order_id = case.get('orderId') score = case.get('score', self.score)
vals = { vals = {
'case_id': case_id, 'case_id': case_id,
@@ -145,13 +125,13 @@ class SignifydCase(models.Model):
'adjusted_score': adjusted_score, 'adjusted_score': adjusted_score,
'guarantee_disposition': guarantee_disposition, 'guarantee_disposition': guarantee_disposition,
'score': score, 'score': score,
'guarantee_eligible': guarantee_eligible, 'last_update': dt.now(), # why not just use
'last_update': dt.now(),
} }
outcome = vals.get('guarantee_disposition') outcome = vals.get('guarantee_disposition')
if outcome == 'DECLINED': if outcome == 'DECLINED':
for user in self.env.company.signifyd_connector_id.notify_user_ids: connector = self._get_connector()
for user in connector.notify_user_ids:
self.create_notification(user, outcome) self.create_notification(user, outcome)
self.write(vals) self.write(vals)
@@ -163,6 +143,6 @@ class SignifydCase(models.Model):
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id, 'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'user_id': user.id, 'user_id': user.id,
'res_id': self.order_id.id, 'res_id': self.order_id.id,
'res_model_id': self.env['ir.model']._get('sale.order').id, 'res_model_id': self.env['ir.model']._get(self.order_id._name).id,
} }
self.env['mail.activity'].create(vals) self.env['mail.activity'].create(vals)

View File

@@ -12,56 +12,65 @@ class SignifydConnector(models.Model):
name = fields.Char(string='Connector Name') name = fields.Char(string='Connector Name')
test_mode = fields.Boolean(string='Test Mode') test_mode = fields.Boolean(string='Test Mode')
user_key = fields.Char(string='Username') user_key = fields.Char(string='Team/Username')
secret_key = fields.Char(string='API Key') secret_key = fields.Char(string='API Key')
user_key_test = fields.Char(string='TEST Username') user_key_test = fields.Char(string='TEST Team/Username')
secret_key_test = fields.Char(string='TEST API Key') secret_key_test = fields.Char(string='TEST API Key')
open_so_cap = fields.Integer(string='Cap requests at:')
webhooks_registered = fields.Boolean(string='Successfully Registered Webhooks') webhooks_registered = fields.Boolean(string='Successfully Registered Webhooks')
notify_user_ids = fields.Many2many('res.users', string='Receive event notifications') notify_user_ids = fields.Many2many('res.users', string='Receive decline notifications')
# TODO ideally this would be a regular constant
# however other entities currently use this by reference
API_URL = 'https://api.signifyd.com/v2' API_URL = 'https://api.signifyd.com/v2'
def get_headers(self): def get_headers(self):
self.ensure_one()
# Check for prod or test mode # Check for prod or test mode
signifyd = self.env.company.signifyd_connector_id if self.test_mode:
if not signifyd: api_key = self.secret_key_test
return False
if signifyd.test_mode:
api_key = signifyd.secret_key_test
else: else:
api_key = signifyd.secret_key api_key = self.secret_key
b64_auth_key = b64encode(api_key.encode('utf-8')) b64_auth_key = b64encode(api_key.encode()).decode().replace('=', '')
headers = { headers = {
'Authorization': 'Basic ' + str(b64_auth_key, 'utf-8').replace('=', ''), 'Authorization': 'Basic ' + b64_auth_key,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} }
return headers return headers
def register_webhooks(self): def register_webhooks(self):
self.ensure_one()
headers = self.get_headers() headers = self.get_headers()
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') # 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 = { values = {
"webhooks": [ "webhooks": [
{ # Given we are creating the cases, we do not need to know about it
"event": "CASE_CREATION", # {
"url": base_url + "/cases/creation" # "event": "CASE_CREATION",
}, # "url": base_url + "/signifyd/cases/update"
# },
{ {
"event": "CASE_RESCORE", "event": "CASE_RESCORE",
"url": base_url + "/cases/update" "url": base_url + "/signifyd/cases/update"
}, },
{ {
"event": "CASE_REVIEW", "event": "CASE_REVIEW",
"url": base_url + "/cases/update" "url": base_url + "/signifyd/cases/update"
}, },
{ {
"event": "GUARANTEE_COMPLETION", "event": "GUARANTEE_COMPLETION",
"url": base_url + "/cases/update" "url": base_url + "/signifyd/cases/update"
}, },
] ]
} }
@@ -75,7 +84,6 @@ class SignifydConnector(models.Model):
return r return r
def action_register_webhooks(self): def action_register_webhooks(self):
notification = { notification = {
'type': 'ir.actions.client', 'type': 'ir.actions.client',
'tag': 'display_notification', 'tag': 'display_notification',
@@ -96,11 +104,11 @@ class SignifydConnector(models.Model):
else: else:
notification['params']['type'] = 'danger' notification['params']['type'] = 'danger'
notification['params']['message'] = res.content.decode('utf-8') notification['params']['message'] = res.content.decode('utf-8')
self.webhooks_registered = False
return notification return notification
def process_post_values(self, post): def process_post_values(self, post):
# Construct dict from request data for endpoints # Construct dict from request data for endpoints
guarantee_eligible = post.get('guaranteeEligible')
uuid = post.get('uuid') uuid = post.get('uuid')
case_id = post.get('caseId') case_id = post.get('caseId')
team_name = post.get('teamName') team_name = post.get('teamName')
@@ -117,7 +125,6 @@ class SignifydConnector(models.Model):
values = {} values = {}
# Validate that the order and case match the request # Validate that the order and case match the request
values.update({'guarantee_eligible': guarantee_eligible}) if guarantee_eligible else ''
values.update({'uuid': uuid}) if uuid else '' values.update({'uuid': uuid}) if uuid else ''
values.update({'team_name': team_name}) if team_name else '' values.update({'team_name': team_name}) if team_name else ''
values.update({'team_id': team_id}) if team_id else '' values.update({'team_id': team_id}) if team_id else ''

View File

@@ -1,5 +1,5 @@
from odoo import fields, models from odoo import fields, models
from odoo.exceptions import UserError
class StockPicking(models.Model): class StockPicking(models.Model):
_inherit = 'stock.picking' _inherit = 'stock.picking'
@@ -9,6 +9,8 @@ class StockPicking(models.Model):
def action_view_signifyd_case(self): def action_view_signifyd_case(self):
self.ensure_one() 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 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} context = {'create': False, 'delete': False, 'id': self.sale_id.signifyd_case_id.id}
return { return {

View File

@@ -0,0 +1,7 @@
from odoo import api, fields, models
class Website(models.Model):
_inherit = 'website'
signifyd_connector_id = fields.Many2one('signifyd.connector')

View File

@@ -1,9 +1,9 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 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 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,1,1,0 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,1,1,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,1,1,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 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,1,1,0 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,1,1,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,1,1,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 1 0 1 0 0
4 public_signifyd_connector public_signifyd_connector model_signifyd_connector base.group_public 1 1 0 1 0 0
5 portal_signifyd_connector portal_signifyd_connector model_signifyd_connector base.group_portal 1 1 0 1 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 1 0 1 0 0
8 public_signifyd_case public_signifyd_case model_signifyd_case base.group_public 1 1 0 1 0 0
9 portal_signifyd_case portal_signifyd_case model_signifyd_case base.group_portal 1 1 0 1 0 0

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="res_company_form_view_inherit" model="ir.ui.view">
<field name="name">res.company.form.inherit</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook//page[last()]" position="after">
<page name="signifyd" string="Signifyd">
<div class="oe_title">
<h3>
Connector
</h3>
</div>
<label for="signifyd_connector_id" string="Settings"/>
<field name="signifyd_connector_id"/>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@@ -26,14 +26,7 @@
attrs="{'invisible': [('score', '&gt;', 300)]}"> attrs="{'invisible': [('score', '&gt;', 300)]}">
<field string="Score" name="score" widget="statinfo"/> <field string="Score" name="score" widget="statinfo"/>
</button> </button>
<button class="oe_stat_button text-success"
icon="fa-share-square-o"
type="object"
name="action_request_guarantee"
string="Request Guarantee"
attrs="{'invisible': [ '|', ('guarantee_eligible', '=', False), ('guarantee_requested', '=', True)]}">
</button>
</div> </div>
<div class="oe_title"> <div class="oe_title">
<h1> <h1>
@@ -50,8 +43,6 @@
<field name="status"/> <field name="status"/>
<field name="order_outcome"/> <field name="order_outcome"/>
<field name="review_disposition"/> <field name="review_disposition"/>
<field name="guarantee_eligible"/>
<field name="guarantee_requested"/>
<field name="guarantee_disposition"/> <field name="guarantee_disposition"/>
<field name="disposition_reason"/> <field name="disposition_reason"/>
</group> </group>
@@ -79,6 +70,9 @@
<button name="action_register_webhooks" type="object" <button name="action_register_webhooks" type="object"
string="Register Webhooks" class="oe_highlight" string="Register Webhooks" class="oe_highlight"
attrs="{'invisible': [('webhooks_registered', '=', True)]}"/> attrs="{'invisible': [('webhooks_registered', '=', True)]}"/>
<button name="action_register_webhooks" type="object"
string="Re-register Webhooks"
attrs="{'invisible': [('webhooks_registered', '=', False)]}"/>
</header> </header>
<sheet> <sheet>
@@ -125,4 +119,29 @@
</field> </field>
</record> </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> </odoo>

View File

@@ -0,0 +1,21 @@
<?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">
<div class="oe_title">
<h3>
Signifyd Configuration
</h3>
</div>
<label for="signifyd_connector_id" string="Settings"/>
<field name="signifyd_connector_id"/>
</xpath>
</field>
</record>
</odoo>