forked from Mapan/odoo17e
299 lines
14 KiB
Python
299 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import logging
|
|
from datetime import datetime
|
|
from odoo import models, fields, api, _
|
|
from odoo.exceptions import UserError
|
|
from . import product
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class SaleOrder(models.Model):
|
|
_inherit = "sale.order"
|
|
|
|
@api.model
|
|
def _process_order(self, order):
|
|
for transaction in order['TransactionArray']['Transaction']:
|
|
so = self.env['sale.order'].search(
|
|
[('client_order_ref', '=', transaction['OrderLineItemID'])], limit=1)
|
|
try:
|
|
if not so:
|
|
so = self._process_order_new(order, transaction)
|
|
so._process_order_update(order)
|
|
except Exception as e:
|
|
message = _("Ebay could not synchronize order:\n%s", e)
|
|
path = str(order)
|
|
product._log_logging(self.env, message, "_process_order", path)
|
|
_logger.exception(message)
|
|
|
|
@api.model
|
|
def _process_order_new(self, order, transaction):
|
|
(partner, shipping_partner) = self._process_order_new_find_partners(order)
|
|
fp = self.env['account.fiscal.position']._get_fiscal_position(partner, delivery=shipping_partner)
|
|
if fp:
|
|
partner.property_account_position_id = fp
|
|
create_values = {
|
|
'partner_id': partner.id,
|
|
'partner_shipping_id': shipping_partner.id,
|
|
'state': 'draft',
|
|
'client_order_ref': transaction['OrderLineItemID'],
|
|
'origin': 'eBay' + transaction['OrderLineItemID'],
|
|
'fiscal_position_id': fp.id,
|
|
'date_order': product._ebay_parse_date(order['PaidTime']),
|
|
}
|
|
if self.env['ir.config_parameter'].sudo().get_param('ebay_sales_team'):
|
|
create_values['team_id'] = int(
|
|
self.env['ir.config_parameter'].sudo().get_param('ebay_sales_team'))
|
|
|
|
sale_order = self.env['sale.order'].create(create_values)
|
|
|
|
sale_order._process_order_new_transaction(transaction)
|
|
|
|
sale_order._process_order_shipping(order)
|
|
|
|
return sale_order
|
|
|
|
def _process_order_new_find_partners(self, order):
|
|
def _find_state():
|
|
state = self.env['res.country.state'].search([
|
|
('code', '=', infos.get('StateOrProvince')),
|
|
('country_id', '=', shipping_data['country_id'])
|
|
], limit=1)
|
|
if not state:
|
|
state = self.env['res.country.state'].search([
|
|
('name', '=', infos.get('StateOrProvince')),
|
|
('country_id', '=', shipping_data['country_id'])
|
|
], limit=1)
|
|
return state
|
|
|
|
buyer_ebay_id = order['BuyerUserID']
|
|
infos = order['ShippingAddress']
|
|
|
|
partner = self.env['res.partner'].search([('ebay_id', '=', buyer_ebay_id)], limit=1)
|
|
if not partner:
|
|
partner = self.env['res.partner'].create({'name': buyer_ebay_id, 'ebay_id': buyer_ebay_id})
|
|
partner_data = {
|
|
'name': infos.get('Name'),
|
|
'ebay_id': buyer_ebay_id,
|
|
'ref': 'eBay',
|
|
}
|
|
email = order['TransactionArray']['Transaction'][0]['Buyer']['Email']
|
|
# After 15 days eBay doesn't send the email anymore but 'Invalid Request'.
|
|
if email != 'Invalid Request':
|
|
partner_data['email'] = email
|
|
# if we reuse an existing partner, addresses might already been set on it
|
|
# so we hold the address data in a temporary dictionary to see if we need to create it or not
|
|
shipping_data = {}
|
|
info_to_extract = [('name', 'Name'), ('street', 'Street1'),
|
|
('street2', 'Street2'), ('city', 'CityName'),
|
|
('zip', 'PostalCode'), ('phone', 'Phone')]
|
|
for (odoo_name, ebay_name) in info_to_extract:
|
|
shipping_data[odoo_name] = infos.get(ebay_name, '')
|
|
shipping_data['country_id'] = self.env['res.country'].search(
|
|
[('code', '=', infos['Country'])], limit=1).id
|
|
shipping_data['state_id'] = _find_state().id
|
|
shipping_partner = partner._find_existing_address(shipping_data)
|
|
if not shipping_partner:
|
|
# if the partner already has an address we create a new child contact to hold it
|
|
# otherwise we can directly set the new address on the partner
|
|
if partner.street:
|
|
contact_data = {'parent_id': partner.id, 'type': 'delivery'}
|
|
shipping_partner = self.env['res.partner'].create({**shipping_data, **contact_data})
|
|
else:
|
|
partner.write(shipping_data)
|
|
shipping_partner = partner
|
|
partner.write(partner_data)
|
|
|
|
return (partner, shipping_partner)
|
|
|
|
@api.model
|
|
def _process_all_taxes(self, tax_dict, price_unit):
|
|
"""If there is more than one product sold, price_unit should actually be the sum of all products;
|
|
price_unit is given per product, whereas the tax amount is computed over the sum of all products.
|
|
"""
|
|
tax_commands = []
|
|
tax_list_or_dict = tax_dict.get('TaxDetails', []) # returns a list if it contains more than one tax, directly returns the tax dict
|
|
for tax in [tax_list_or_dict] if isinstance(tax_list_or_dict, dict) else tax_list_or_dict:
|
|
tax_amount = float(tax['TaxAmount']['value'])
|
|
tax_rate = 100 * tax_amount / (price_unit - tax_amount) if price_unit > tax_amount > 0 else 0
|
|
tax_description = tax.get('TaxDescription', '')
|
|
tax_id = self._handle_taxes(tax_amount, tax_rate, description=tax_description)
|
|
if tax_id:
|
|
tax_commands.append((4, tax_id.id))
|
|
return tax_commands or False
|
|
|
|
@api.model
|
|
def _handle_taxes(self, amount, rate, description=''):
|
|
"""eBay use price-included taxes.
|
|
We ignore 0% taxes to avoid useless clutter,
|
|
but that could be changed if their presence is required.
|
|
"""
|
|
company = self.env.company
|
|
tax = False
|
|
if amount > 0 and rate > 0:
|
|
tax = self.env['account.tax'].with_context(active_test=False).sudo().search([
|
|
*self.env['account.tax']._check_company_domain(company),
|
|
('amount', '=', rate),
|
|
('amount_type', '=', 'percent'),
|
|
('price_include', '=', True),
|
|
('type_tax_use', '=', 'sale')], limit=1)
|
|
if not tax:
|
|
tax = self.env['account.tax'].sudo().create({
|
|
'name': 'Tax %.4f %%' % rate,
|
|
'amount': rate,
|
|
'amount_type': 'percent',
|
|
'type_tax_use': 'sale',
|
|
'description': '%s (eBay)' % description,
|
|
'company_id': company.id,
|
|
'price_include': True,
|
|
'active': False,
|
|
})
|
|
return tax
|
|
|
|
def _process_order_shipping(self, order):
|
|
self.ensure_one()
|
|
|
|
if 'ShippingServiceSelected' in order:
|
|
shipping_cost_dict = order['ShippingServiceSelected']['ShippingServiceCost']
|
|
shipping_amount = float(shipping_cost_dict['value'])
|
|
shipping_currency = self.env['res.currency'].with_context(active_test=False).search(
|
|
[('name', '=', shipping_cost_dict['_currencyID'])], limit=1)
|
|
shipping_name = order['ShippingServiceSelected']['ShippingService']
|
|
shipping_product = self.env['product.template'].search(
|
|
[('name', '=', shipping_name)], limit=1)
|
|
if not shipping_product:
|
|
shipping_product = self.env['product.template'].create({
|
|
'name': shipping_name,
|
|
'type': 'service',
|
|
'categ_id': self.env.ref('sale_ebay.product_category_ebay').id,
|
|
})
|
|
tax_dict = order['ShippingDetails']['SalesTax']
|
|
tax_amount = float(tax_dict.get('SalesTaxAmount', {}).get('value', 0))
|
|
# the rate on the tax amount is actually on the product unit price, not on the shipping
|
|
# and it's a tax not included in the price, contrarily to the product tax
|
|
tax_rate = tax_dict.get('SalesTaxPercent', '0')
|
|
tax_id = False
|
|
if tax_amount:
|
|
tax_id = self.env['account.tax'].sudo().create({
|
|
'name': tax_rate + '% Sales tax (eBay)',
|
|
'amount': tax_amount,
|
|
'amount_type': 'fixed',
|
|
'type_tax_use': 'sale',
|
|
'company_id': self.env.company.id,
|
|
'active': False,
|
|
})
|
|
|
|
price_unit = shipping_currency._convert(shipping_amount,
|
|
self.currency_id, self.company_id, self.date_order or datetime.now())
|
|
|
|
so_line = self.env['sale.order.line'].create({
|
|
'order_id': self.id,
|
|
'name': shipping_name,
|
|
'product_id': shipping_product.product_variant_ids[0].id,
|
|
'product_uom_qty': 1,
|
|
'price_unit': price_unit,
|
|
'tax_id': [(4, tax_id.id)] if tax_id else False,
|
|
'is_delivery': True,
|
|
})
|
|
|
|
def _process_transaction_product(self, transaction):
|
|
Template = self.env['product.template']
|
|
ebay_id = transaction['Item']['ItemID']
|
|
product = Template.search([('ebay_id', '=', ebay_id)], order='ebay_use desc', limit=1)
|
|
if not product:
|
|
product = Template.create({
|
|
'name': transaction['Item']['Title'],
|
|
'ebay_id': ebay_id,
|
|
'ebay_use': True,
|
|
'ebay_sync_stock': False,
|
|
})
|
|
product.message_post(body=
|
|
_('Product created from eBay transaction %s', transaction['TransactionID']))
|
|
|
|
if product.product_variant_count > 1:
|
|
if 'Variation' in transaction:
|
|
splitted_url = transaction['Variation']['VariationViewItemURL'].split("vti", 1)[1]
|
|
variant = product.product_variant_ids.filtered(
|
|
lambda l:
|
|
l.ebay_use and
|
|
l.ebay_variant_url.split("vti", 1)[1] == splitted_url
|
|
or l.name == transaction['Variation']['VariationTitle']
|
|
)
|
|
|
|
# If multiple variants but only one listed on eBay as Item Specific
|
|
else:
|
|
call_data = {'ItemID': product.ebay_id, 'IncludeItemSpecifics': True}
|
|
resp = product._ebay_execute('GetItem', call_data)
|
|
name_value_list = resp.dict()['Item']['ItemSpecifics']['NameValueList']
|
|
if not isinstance(name_value_list, list):
|
|
name_value_list = [name_value_list]
|
|
# get only the item specific in the value list
|
|
variant = product._get_variant_from_ebay_specs([n for n in name_value_list if n['Source'] == 'ItemSpecific'])
|
|
else:
|
|
variant = product.product_variant_ids[0]
|
|
variant.ebay_quantity_sold = variant.ebay_quantity_sold + int(transaction['QuantityPurchased'])
|
|
if not product.ebay_sync_stock:
|
|
variant.ebay_quantity = variant.ebay_quantity - int(transaction['QuantityPurchased'])
|
|
variant_qty = 0
|
|
if len(product.product_variant_ids.filtered('ebay_use')) > 1:
|
|
variant_qty = sum(product.product_variant_ids.mapped('ebay_quantity'))
|
|
else:
|
|
variant_qty = variant.ebay_quantity
|
|
if variant_qty <= 0:
|
|
if self.env['ir.config_parameter'].sudo().get_param('ebay_out_of_stock'):
|
|
product.ebay_listing_status = 'Out Of Stock'
|
|
else:
|
|
product.ebay_listing_status = 'Ended'
|
|
return product, variant
|
|
|
|
def _process_order_new_transaction(self, transaction):
|
|
self.ensure_one()
|
|
|
|
product, variant = self._process_transaction_product(transaction)
|
|
|
|
transaction_currency = self.env['res.currency'].with_context(active_test=False).search(
|
|
[('name', '=', transaction['TransactionPrice']['_currencyID'])], limit=1)
|
|
price_unit = float(transaction['TransactionPrice']['value'])
|
|
price_unit = transaction_currency._convert(price_unit,
|
|
self.currency_id, self.company_id, self.date_order or datetime.now())
|
|
qty = float(transaction['QuantityPurchased'])
|
|
tax_commands = self._process_all_taxes(transaction['Taxes'], price_unit * qty)
|
|
|
|
sol = self.env['sale.order.line'].create({
|
|
'product_id': variant.id,
|
|
'order_id': self.id,
|
|
'product_uom_qty': qty,
|
|
'price_unit': price_unit,
|
|
'tax_id': tax_commands,
|
|
})
|
|
|
|
if 'BuyerCheckoutMessage' in transaction:
|
|
self.message_post(body=_('The Buyer Posted :\n') + transaction['BuyerCheckoutMessage'])
|
|
|
|
self.env['product.template']._put_in_queue(product.id)
|
|
|
|
def _process_order_update(self, order):
|
|
self.ensure_one()
|
|
|
|
product_lines = self.order_line.filtered(lambda l: not l._is_delivery())
|
|
are_all_products_listed = all(product_lines.mapped('product_id.ebay_url'))
|
|
can_be_invoiced = ('order' in product_lines.mapped('product_id.invoice_policy') and
|
|
'to invoice' in product_lines.mapped('invoice_status'))
|
|
|
|
no_confirm = (self.env.context.get('ebay_no_confirm', False) or
|
|
not are_all_products_listed)
|
|
try:
|
|
if (not no_confirm and self.state in ['draft', 'sent']):
|
|
self.action_confirm()
|
|
if not no_confirm and can_be_invoiced:
|
|
self._create_invoices()
|
|
shipping_name = order['ShippingServiceSelected']['ShippingService']
|
|
if self.picking_ids and shipping_name:
|
|
self.picking_ids[-1].message_post(
|
|
body=_('The Buyer Chose The Following Delivery Method :\n') + shipping_name)
|
|
except UserError as e:
|
|
self.message_post(body=
|
|
_('Ebay Synchronisation could not confirm because of the following error:\n%s', str)(e))
|