From 05574fc1fbdf888c46280008bf229f49f7ea4ecf Mon Sep 17 00:00:00 2001 From: Brett Spaulding Date: Wed, 18 Nov 2020 22:11:04 -0500 Subject: [PATCH 1/5] [ADD] connector_signifyd: Automate fraud detection on orders with the Signifyd API. --- connector_signifyd/__init__.py | 2 + connector_signifyd/__manifest__.py | 24 +++ connector_signifyd/controllers/__init__.py | 2 + connector_signifyd/controllers/main.py | 21 +++ connector_signifyd/controllers/signifyd.py | 36 ++++ connector_signifyd/models/__init__.py | 6 + connector_signifyd/models/company.py | 7 + connector_signifyd/models/partner.py | 19 ++ connector_signifyd/models/sale_order.py | 149 ++++++++++++++++ connector_signifyd/models/signifyd.py | 163 ++++++++++++++++++ .../models/signifyd_connector.py | 134 ++++++++++++++ connector_signifyd/models/stock.py | 22 +++ .../security/ir.model.access.csv | 9 + connector_signifyd/views/company_views.xml | 23 +++ connector_signifyd/views/partner_views.xml | 43 +++++ connector_signifyd/views/sale_views.xml | 56 ++++++ connector_signifyd/views/signifyd_views.xml | 128 ++++++++++++++ connector_signifyd/views/stock_views.xml | 22 +++ connector_signifyd/views/web_assets.xml | 11 ++ 19 files changed, 877 insertions(+) create mode 100644 connector_signifyd/__init__.py create mode 100644 connector_signifyd/__manifest__.py create mode 100644 connector_signifyd/controllers/__init__.py create mode 100644 connector_signifyd/controllers/main.py create mode 100644 connector_signifyd/controllers/signifyd.py create mode 100644 connector_signifyd/models/__init__.py create mode 100644 connector_signifyd/models/company.py create mode 100644 connector_signifyd/models/partner.py create mode 100644 connector_signifyd/models/sale_order.py create mode 100644 connector_signifyd/models/signifyd.py create mode 100644 connector_signifyd/models/signifyd_connector.py create mode 100644 connector_signifyd/models/stock.py create mode 100644 connector_signifyd/security/ir.model.access.csv create mode 100644 connector_signifyd/views/company_views.xml create mode 100644 connector_signifyd/views/partner_views.xml create mode 100644 connector_signifyd/views/sale_views.xml create mode 100644 connector_signifyd/views/signifyd_views.xml create mode 100644 connector_signifyd/views/stock_views.xml create mode 100644 connector_signifyd/views/web_assets.xml diff --git a/connector_signifyd/__init__.py b/connector_signifyd/__init__.py new file mode 100644 index 00000000..91c5580f --- /dev/null +++ b/connector_signifyd/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/connector_signifyd/__manifest__.py b/connector_signifyd/__manifest__.py new file mode 100644 index 00000000..5906d751 --- /dev/null +++ b/connector_signifyd/__manifest__.py @@ -0,0 +1,24 @@ +{ + 'name': 'Signifyd Connector', + 'author': 'Hibou Corp. ', + 'version': '13.0.1.0.0', + 'category': 'Sale', + 'description': """ +Automate Order Fraud Detection with the Signifyd API. + """, + 'website': 'https://hibou.io/', + 'depends': [ + 'website_sale', + ], + 'data': [ + 'security/ir.model.access.csv', + 'views/company_views.xml', + 'views/partner_views.xml', + 'views/sale_views.xml', + 'views/signifyd_views.xml', + 'views/stock_views.xml', + 'views/web_assets.xml', + ], + 'installable': True, + 'application': False, +} diff --git a/connector_signifyd/controllers/__init__.py b/connector_signifyd/controllers/__init__.py new file mode 100644 index 00000000..3b63458e --- /dev/null +++ b/connector_signifyd/controllers/__init__.py @@ -0,0 +1,2 @@ +from . import main +from . import signifyd diff --git a/connector_signifyd/controllers/main.py b/connector_signifyd/controllers/main.py new file mode 100644 index 00000000..2482f87f --- /dev/null +++ b/connector_signifyd/controllers/main.py @@ -0,0 +1,21 @@ +from odoo.http import request, route +from odoo.addons.website_sale.controllers.main import WebsiteSale + + +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 + 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: + order.sudo().post_signifyd_case(order_session_id, checkout_token, browser_ip_address) + + return res diff --git a/connector_signifyd/controllers/signifyd.py b/connector_signifyd/controllers/signifyd.py new file mode 100644 index 00000000..e8ff5bae --- /dev/null +++ b/connector_signifyd/controllers/signifyd.py @@ -0,0 +1,36 @@ +import json +from odoo.http import Controller, request, route +from odoo.http import Response + + +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') + 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') + + @route(['/cases/update'], type='json', auth='public', methods=['POST'], csrf=False) + def case_update(self, *args, **post): + data = json.loads(request.httprequest.data) + vals = request.env['signifyd.connector'].process_post_values(data) + case = request.env['signifyd.case'].sudo().search([('case_id', '=', vals['case_id'])]) + if case: + case.update_case_info(vals) + + outcome = vals.get('guarantee_disposition') + if case and outcome == 'DECLINED': + for user in request.env.company.signifyd_connector_id.notify_user_ids: + case.sudo().create_notification(user, outcome) diff --git a/connector_signifyd/models/__init__.py b/connector_signifyd/models/__init__.py new file mode 100644 index 00000000..7aacfb24 --- /dev/null +++ b/connector_signifyd/models/__init__.py @@ -0,0 +1,6 @@ +from . import company +from . import partner +from . import sale_order +from . import signifyd +from . import signifyd_connector +from . import stock diff --git a/connector_signifyd/models/company.py b/connector_signifyd/models/company.py new file mode 100644 index 00000000..3507b679 --- /dev/null +++ b/connector_signifyd/models/company.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = 'res.company' + + signifyd_connector_id = fields.Many2one('signifyd.connector') diff --git a/connector_signifyd/models/partner.py b/connector_signifyd/models/partner.py new file mode 100644 index 00000000..536ed814 --- /dev/null +++ b/connector_signifyd/models/partner.py @@ -0,0 +1,19 @@ +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') + + 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 diff --git a/connector_signifyd/models/sale_order.py b/connector_signifyd/models/sale_order.py new file mode 100644 index 00000000..02d29538 --- /dev/null +++ b/connector_signifyd/models/sale_order.py @@ -0,0 +1,149 @@ +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + signifyd_case_id = fields.Many2one('signifyd.case', readonly=1) + singifyd_score = fields.Float(related='signifyd_case_id.score', readonly=1) + signifyd_disposition_status = fields.Selection(related='signifyd_case_id.guarantee_disposition', tracking=True) + + def action_view_signifyd_case(self): + self.ensure_one() + form_id = self.env.ref('gcl_signifyd_connector.signifyd_case_form_view').id + context = {'create': False, 'delete': False, 'id': self.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.signifyd_case_id.id, + 'context': context, + } + + def post_signifyd_case(self, order_session_id, checkout_token, browser_ip_address): + # Session values for Signifyd post + data = { + '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) + + success_response = case_res.get('investigationId') + if success_response: + new_case = self.env['signifyd.case'].create({ + 'order_id': self.id, + 'case_id': success_response, + 'name': success_response, + }) + self.write({'signifyd_case_id': new_case.id}) + self.partner_id.write({ + 'signifyd_case_ids': [(4, new_case.id)], + }) + return new_case + + @api.model + def prepare_signifyd_case_values(self, data): + order_session_id = data.get('order_session_id') + checkout_token = data.get('checkout_token') + browser_ip_address = data.get('browser_ip_address') + + new_case_vals = {} + + new_case_vals['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, + } + + new_case_vals['purchase']['products'] = [] + for line in self.order_line: + product = line.product_id + vals = { + "itemId": product.id, + "itemName": product.name, + "itemIsDigital": False, + "itemCategory": product.categ_id.name, + "itemUrl": product.website_url, + "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, + "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, + } + } + } + new_case_vals['transactions'].append(vals) + + return new_case_vals diff --git a/connector_signifyd/models/signifyd.py b/connector_signifyd/models/signifyd.py new file mode 100644 index 00000000..16dce1fa --- /dev/null +++ b/connector_signifyd/models/signifyd.py @@ -0,0 +1,163 @@ +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') + 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') + guarantee_eligible = fields.Boolean('Eligible for Guarantee') + guarantee_requested = fields.Boolean('Requested Guarantee') + score = fields.Float(string='Transaction Score') + adjusted_score = fields.Float(string='Adjusted Score') + signifyd_url = fields.Char('Signifyd.com', compute='_compute_signifyd_url') + + @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): + res = super(SignifydCase, self).write(vals) + disposition = vals.get('guarantee_disposition') + if disposition: + self.order_id.message_post(body=_('Signifyd Updated Record to %s' % vals['guarantee_disposition']), + subtype='gcl_signifyd_connector.disposition_change') + return res + + @api.model + def post_case(self, values): + signifyd = self.env['signifyd.connector'] + headers = signifyd.get_headers() + data = json.dumps(values, indent=4, sort_keys=True, default=str) + + r = requests.post( + signifyd.API_URL + '/cases', + headers=headers, + data=data, + ) + return r.json() + + @api.model + def get_case(self): + signifyd = self.env['signifyd.connector'] + headers = signifyd.get_headers() + r = requests.get( + signifyd.API_URL + '/cases/' + str(self.case_id), + headers=headers + ) + return r.json() + + @api.model + def request_guarantee(self, *args): + 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): + for record in self: + record.update_case_info() + + @api.model + def update_case_info(self, vals=None): + if not vals: + case = self.get_case() + case_id = case.get('caseId') + team_id = case.get('teamId') + team_name = case.get('teamName') + uuid = case.get('uuid') + status = case.get('status') + review_disposition = case.get('reviewDisposition') + order_outcome = case.get('orderOutcome') + guarantee_disposition = case.get('guaranteeDisposition') + adjusted_score = case.get('adjustedScore') + score = case.get('score') + guarantee_eligible = case.get('guaranteeEligible') + # order_id = case.get('orderId') + + 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, + 'guarantee_eligible': guarantee_eligible, + 'last_update': dt.now(), + } + + outcome = vals.get('guarantee_disposition') + if outcome == 'DECLINED': + for user in self.env.company.signifyd_connector_id.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('sale.order').id, + } + self.env['mail.activity'].create(vals) diff --git a/connector_signifyd/models/signifyd_connector.py b/connector_signifyd/models/signifyd_connector.py new file mode 100644 index 00000000..24bb44b5 --- /dev/null +++ b/connector_signifyd/models/signifyd_connector.py @@ -0,0 +1,134 @@ +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') + test_mode = fields.Boolean(string='Test Mode') + user_key = fields.Char(string='Username') + secret_key = fields.Char(string='API Key') + user_key_test = fields.Char(string='TEST Username') + 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') + notify_user_ids = fields.Many2many('res.users', string='Receive event notifications') + + API_URL = 'https://api.signifyd.com/v2' + + def get_headers(self): + # Check for prod or test mode + signifyd = self.env.company.signifyd_connector_id + if not signifyd: + return False + + if signifyd.test_mode: + api_key = signifyd.secret_key_test + else: + api_key = signifyd.secret_key + + b64_auth_key = b64encode(api_key.encode('utf-8')) + + headers = { + 'Authorization': 'Basic ' + str(b64_auth_key, 'utf-8').replace('=', ''), + 'Content-Type': 'application/json', + } + + return headers + + def register_webhooks(self): + headers = self.get_headers() + base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url') + values = { + "webhooks": [ + { + "event": "CASE_CREATION", + "url": base_url + "/cases/creation" + }, + { + "event": "CASE_RESCORE", + "url": base_url + "/cases/update" + }, + { + "event": "CASE_REVIEW", + "url": base_url + "/cases/update" + }, + { + "event": "GUARANTEE_COMPLETION", + "url": base_url + "/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') + return notification + + def process_post_values(self, post): + # Construct dict from request data for endpoints + guarantee_eligible = post.get('guaranteeEligible') + 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({'guarantee_eligible': guarantee_eligible}) if guarantee_eligible else '' + 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 diff --git a/connector_signifyd/models/stock.py b/connector_signifyd/models/stock.py new file mode 100644 index 00000000..58aab0d6 --- /dev/null +++ b/connector_signifyd/models/stock.py @@ -0,0 +1,22 @@ +from odoo import fields, models + + +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() + form_id = self.env.ref('gcl_signifyd_connector.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, + } diff --git a/connector_signifyd/security/ir.model.access.csv b/connector_signifyd/security/ir.model.access.csv new file mode 100644 index 00000000..c537a258 --- /dev/null +++ b/connector_signifyd/security/ir.model.access.csv @@ -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,1,1,0 +public_signifyd_connector,public_signifyd_connector,model_signifyd_connector,base.group_public,1,1,1,0 +portal_signifyd_connector,portal_signifyd_connector,model_signifyd_connector,base.group_portal,1,1,1,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,1,1,0 +public_signifyd_case,public_signifyd_case,model_signifyd_case,base.group_public,1,1,1,0 +portal_signifyd_case,portal_signifyd_case,model_signifyd_case,base.group_portal,1,1,1,0 \ No newline at end of file diff --git a/connector_signifyd/views/company_views.xml b/connector_signifyd/views/company_views.xml new file mode 100644 index 00000000..1f217b4d --- /dev/null +++ b/connector_signifyd/views/company_views.xml @@ -0,0 +1,23 @@ + + + + + res.company.form.inherit + res.company + + + + +
+

+ Connector +

+
+
+
+
+
+ +
\ No newline at end of file diff --git a/connector_signifyd/views/partner_views.xml b/connector_signifyd/views/partner_views.xml new file mode 100644 index 00000000..3f76b8b3 --- /dev/null +++ b/connector_signifyd/views/partner_views.xml @@ -0,0 +1,43 @@ + + + + + res.partner.form.inherit + res.partner + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/connector_signifyd/views/sale_views.xml b/connector_signifyd/views/sale_views.xml new file mode 100644 index 00000000..555a8c64 --- /dev/null +++ b/connector_signifyd/views/sale_views.xml @@ -0,0 +1,56 @@ + + + + + sale.order.form.inherit + sale.order + + + + + + + + + + + + + + + + + + + + sale.order.tree.inherit + sale.order + + + + + + + + + + Signifyd Updated + sale.order + + Signifyd Status Updated + signifyd_case_id.guarantee_disposition + + + \ No newline at end of file diff --git a/connector_signifyd/views/signifyd_views.xml b/connector_signifyd/views/signifyd_views.xml new file mode 100644 index 00000000..442cac17 --- /dev/null +++ b/connector_signifyd/views/signifyd_views.xml @@ -0,0 +1,128 @@ + + + + + signifyd.form.view + signifyd.case + +
+
+
+ + +
+ + + + + +
+
+

+ +

+ +
+ + + + + + + + + + + + + + + + +
+
+
+
+ + + + Signifyd Case Information + signifyd.case + form + + + + + signifyd.form.view + signifyd.connector + +
+
+
+ + +
+ +
+ + + Webhooks Active + +
+
+ + + Webhooks Inactive + +
+
+
+
+ + + + + + + +

+ Optional: Add users to be notified if a sale order is declined by Signifyd. +

+ +
+ +
+
+
+
+
+ +
\ No newline at end of file diff --git a/connector_signifyd/views/stock_views.xml b/connector_signifyd/views/stock_views.xml new file mode 100644 index 00000000..822d2443 --- /dev/null +++ b/connector_signifyd/views/stock_views.xml @@ -0,0 +1,22 @@ + + + + + stock.picking.form.view.inherit + stock.picking + + + + + + -

@@ -50,8 +43,6 @@ - - @@ -79,6 +70,9 @@