From 6010a10ad1f61ff29a67d6f63edc3b2f299ac78a Mon Sep 17 00:00:00 2001 From: Pawan Mehta Date: Wed, 5 Sep 2018 17:16:57 +0530 Subject: [PATCH] Amazon MWS Connector (#15061) * [fix] #15054 * [fix] #15058 * codacy issues * Codacy fixes-1 * Travis Fix * fix codacy * fix codacy * review changes 1 * Custom fields and fixes * remove amazon id * remove item.json and sales_order.json * added back sales_order and item files --- erpnext/config/integrations.py | 5 + .../doctype/amazon_mws_settings/__init__.py | 0 .../amazon_mws_settings/amazon_methods.py | 505 +++++++++ .../amazon_mws_settings/amazon_mws_api.py | 642 ++++++++++++ .../amazon_mws_settings.js | 3 + .../amazon_mws_settings.json | 974 ++++++++++++++++++ .../amazon_mws_settings.py | 40 + .../test_amazon_mws_settings.js | 23 + .../test_amazon_mws_settings.py | 9 + .../doctype/amazon_mws_settings/xml_utils.py | 102 ++ erpnext/hooks.py | 3 +- 11 files changed, 2305 insertions(+), 1 deletion(-) create mode 100644 erpnext/erpnext_integrations/doctype/amazon_mws_settings/__init__.py create mode 100644 erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py create mode 100755 erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py create mode 100644 erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js create mode 100644 erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.json create mode 100644 erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py create mode 100644 erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.js create mode 100644 erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.py create mode 100644 erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py diff --git a/erpnext/config/integrations.py b/erpnext/config/integrations.py index e27b7cd04f..01e077fba3 100644 --- a/erpnext/config/integrations.py +++ b/erpnext/config/integrations.py @@ -30,6 +30,11 @@ def get_data(): "type": "doctype", "name": "Shopify Settings", "description": _("Connect Shopify with ERPNext"), + }, + { + "type": "doctype", + "name": "Amazon MWS Settings", + "description": _("Connect Amazon with ERPNext"), } ] } diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/__init__.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py new file mode 100644 index 0000000000..cb11ece175 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py @@ -0,0 +1,505 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, time, dateutil, math, csv, StringIO +import amazon_mws_api as mws +from frappe import _ + +#Get and Create Products +def get_products_details(): + products = get_products_instance() + reports = get_reports_instance() + + mws_settings = frappe.get_doc("Amazon MWS Settings") + market_place_list = return_as_list(mws_settings.market_place_id) + + for marketplace in market_place_list: + report_id = request_and_fetch_report_id("_GET_FLAT_FILE_OPEN_LISTINGS_DATA_", None, None, market_place_list) + + if report_id: + listings_response = reports.get_report(report_id=report_id) + + #Get ASIN Codes + string_io = StringIO.StringIO(listings_response.original) + csv_rows = list(csv.reader(string_io, delimiter=str('\t'))) + asin_list = list(set([row[1] for row in csv_rows[1:]])) + #break into chunks of 10 + asin_chunked_list = list(chunks(asin_list, 10)) + + #Map ASIN Codes to SKUs + sku_asin = [{"asin":row[1],"sku":row[0]} for row in csv_rows[1:]] + + #Fetch Products List from ASIN + for asin_list in asin_chunked_list: + products_response = call_mws_method(products.get_matching_product,marketplaceid=marketplace, + asins=asin_list) + + matching_products_list = products_response.parsed + for product in matching_products_list: + skus = [row["sku"] for row in sku_asin if row["asin"]==product.ASIN] + for sku in skus: + create_item_code(product, sku) + +def get_products_instance(): + mws_settings = frappe.get_doc("Amazon MWS Settings") + products = mws.Products( + account_id = mws_settings.seller_id, + access_key = mws_settings.aws_access_key_id, + secret_key = mws_settings.secret_key, + region = mws_settings.region, + domain = mws_settings.domain + ) + + return products + +def get_reports_instance(): + mws_settings = frappe.get_doc("Amazon MWS Settings") + reports = mws.Reports( + account_id = mws_settings.seller_id, + access_key = mws_settings.aws_access_key_id, + secret_key = mws_settings.secret_key, + region = mws_settings.region, + domain = mws_settings.domain + ) + + return reports + +#returns list as expected by amazon API +def return_as_list(input_value): + if isinstance(input_value, list): + return input_value + else: + return [input_value] + +#function to chunk product data +def chunks(l, n): + for i in range(0, len(l), n): + yield l[i:i+n] + +def request_and_fetch_report_id(report_type, start_date=None, end_date=None, marketplaceids=None): + reports = get_reports_instance() + report_response = reports.request_report(report_type=report_type, + start_date=start_date, + end_date=end_date, + marketplaceids=marketplaceids) + + #add time delay to wait for amazon to generate report + time.sleep(20) + report_request_id = report_response.parsed["ReportRequestInfo"]["ReportRequestId"]["value"] + generated_report_id = None + #poll to get generated report + for x in range(1,10): + report_request_list_response = reports.get_report_request_list(requestids=[report_request_id]) + report_status = report_request_list_response.parsed["ReportRequestInfo"]["ReportProcessingStatus"]["value"] + + if report_status == "_SUBMITTED_" or report_status == "_IN_PROGRESS_": + #add time delay to wait for amazon to generate report + time.sleep(15) + continue + elif report_status == "_CANCELLED_": + break + elif report_status == "_DONE_NO_DATA_": + break + elif report_status == "_DONE_": + generated_report_id = report_request_list_response.parsed["ReportRequestInfo"]["GeneratedReportId"]["value"] + break + return generated_report_id + +def call_mws_method(mws_method, *args, **kwargs): + + mws_settings = frappe.get_doc("Amazon MWS Settings") + max_retries = mws_settings.max_retry_limit + + for x in xrange(0, max_retries): + try: + response = mws_method(*args, **kwargs) + return response + except Exception as e: + delay = math.pow(4, x) * 125 + frappe.log_error(message=e, title=str(mws_method)) + time.sleep(delay) + continue + + mws_settings.enable_synch = 0 + mws_settings.save() + + frappe.throw(_("Sync has been temporarily disabled because maximum retries have been exceeded")) + +def create_item_code(amazon_item_json, sku): + if frappe.db.get_value("Item", sku): + return + + item = frappe.new_doc("Item") + + new_manufacturer = create_manufacturer(amazon_item_json) + new_brand = create_brand(amazon_item_json) + + mws_settings = frappe.get_doc("Amazon MWS Settings") + + item.item_code = sku + item.amazon_item_code = amazon_item_json.ASIN + item.item_group = mws_settings.item_group + item.description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title + item.brand = new_brand + item.manufacturer = new_manufacturer + item.web_long_description = amazon_item_json.Product.AttributeSets.ItemAttributes.Title + + item.image = amazon_item_json.Product.AttributeSets.ItemAttributes.SmallImage.URL + + temp_item_group = amazon_item_json.Product.AttributeSets.ItemAttributes.ProductGroup + + item_group = frappe.db.get_value("Item Group",filters={"item_group_name": temp_item_group}) + + if not item_group: + igroup = frappe.new_doc("Item Group") + igroup.item_group_name = temp_item_group + igroup.parent_item_group = mws_settings.item_group + igroup.insert() + + item.insert(ignore_permissions=True) + create_item_price(amazon_item_json, item.item_code) + + return item.name + +def create_manufacturer(amazon_item_json): + existing_manufacturer = frappe.db.get_value("Manufacturer", + filters={"short_name":amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer}) + + if not existing_manufacturer: + manufacturer = frappe.new_doc("Manufacturer") + manufacturer.short_name = amazon_item_json.Product.AttributeSets.ItemAttributes.Manufacturer + manufacturer.insert() + return manufacturer.short_name + else: + return existing_manufacturer + +def create_brand(amazon_item_json): + existing_brand = frappe.db.get_value("Brand", + filters={"brand":amazon_item_json.Product.AttributeSets.ItemAttributes.Brand}) + if not existing_brand: + brand = frappe.new_doc("Brand") + brand.brand = amazon_item_json.Product.AttributeSets.ItemAttributes.Brand + brand.insert() + return brand.brand + else: + return existing_brand + +def create_item_price(amazon_item_json, item_code): + item_price = frappe.new_doc("Item Price") + item_price.price_list = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "price_list") + if not("ListPrice" in amazon_item_json.Product.AttributeSets.ItemAttributes): + item_price.price_list_rate = 0 + else: + item_price.price_list_rate = amazon_item_json.Product.AttributeSets.ItemAttributes.ListPrice.Amount + + item_price.item_code = item_code + item_price.insert() + +#Get and create Orders +def get_orders(after_date): + try: + orders = get_orders_instance() + statuses = ["PartiallyShipped", "Unshipped", "Shipped", "Canceled"] + mws_settings = frappe.get_doc("Amazon MWS Settings") + market_place_list = return_as_list(mws_settings.market_place_id) + + orders_response = call_mws_method(orders.list_orders, marketplaceids=market_place_list, + fulfillment_channels=["MFN", "AFN"], + lastupdatedafter=after_date, + orderstatus=statuses, + max_results='20') + + while True: + orders_list = [] + + if "Order" in orders_response.parsed.Orders: + orders_list = return_as_list(orders_response.parsed.Orders.Order) + + if len(orders_list) == 0: + break + + for order in orders_list: + create_sales_order(order, after_date) + + if not "NextToken" in orders_response.parsed: + break + + next_token = orders_response.parsed.NextToken + orders_response = call_mws_method(orders.list_orders_by_next_token, next_token) + + except Exception as e: + frappe.log_error(title="get_orders", message=e) + +def get_orders_instance(): + mws_settings = frappe.get_doc("Amazon MWS Settings") + orders = mws.Orders( + account_id = mws_settings.seller_id, + access_key = mws_settings.aws_access_key_id, + secret_key = mws_settings.secret_key, + region= mws_settings.region, + domain= mws_settings.domain, + version="2013-09-01" + ) + + return orders + +def create_sales_order(order_json,after_date): + customer_name = create_customer(order_json) + create_address(order_json, customer_name) + + market_place_order_id = order_json.AmazonOrderId + + so = frappe.db.get_value("Sales Order", + filters={"amazon_order_id": market_place_order_id}, + fieldname="name") + + taxes_and_charges = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "taxes_charges") + + if so: + return + + if not so: + items = get_order_items(market_place_order_id) + delivery_date = dateutil.parser.parse(order_json.LatestShipDate).strftime("%Y-%m-%d") + transaction_date = dateutil.parser.parse(order_json.PurchaseDate).strftime("%Y-%m-%d") + + so = frappe.get_doc({ + "doctype": "Sales Order", + "naming_series": "SO-", + "amazon_order_id": market_place_order_id, + "marketplace_id": order_json.MarketplaceId, + "customer": customer_name, + "delivery_date": delivery_date, + "transaction_date": transaction_date, + "items": items, + "company": frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "company") + }) + + try: + if taxes_and_charges: + charges_and_fees = get_charges_and_fees(market_place_order_id) + for charge in charges_and_fees.get("charges"): + so.append('taxes', charge) + + for fee in charges_and_fees.get("fees"): + so.append('taxes', fee) + + so.insert(ignore_permissions=True) + so.submit() + + except Exception as e: + frappe.log_error(message=e, title="Create Sales Order") + +def create_customer(order_json): + order_customer_name = "" + + if not("BuyerName" in order_json): + order_customer_name = "Buyer - " + order_json.AmazonOrderId + else: + order_customer_name = order_json.BuyerName + + existing_customer_name = frappe.db.get_value("Customer", + filters={"name": order_customer_name}, fieldname="name") + + if existing_customer_name: + filters = [ + ["Dynamic Link", "link_doctype", "=", "Customer"], + ["Dynamic Link", "link_name", "=", existing_customer_name], + ["Dynamic Link", "parenttype", "=", "Contact"] + ] + + existing_contacts = frappe.get_list("Contact", filters) + + if existing_contacts: + pass + else: + new_contact = frappe.new_doc("Contact") + new_contact.first_name = order_customer_name + new_contact.append('links', { + "link_doctype": "Customer", + "link_name": existing_customer_name + }) + new_contact.insert() + + return existing_customer_name + else: + mws_customer_settings = frappe.get_doc("Amazon MWS Settings") + new_customer = frappe.new_doc("Customer") + new_customer.customer_name = order_customer_name + new_customer.customer_group = mws_customer_settings.customer_group + new_customer.territory = mws_customer_settings.territory + new_customer.customer_type = mws_customer_settings.customer_type + new_customer.save() + + new_contact = frappe.new_doc("Contact") + new_contact.first_name = order_customer_name + new_contact.append('links', { + "link_doctype": "Customer", + "link_name": new_customer.name + }) + + new_contact.insert() + + return new_customer.name + +def create_address(amazon_order_item_json, customer_name): + + filters = [ + ["Dynamic Link", "link_doctype", "=", "Customer"], + ["Dynamic Link", "link_name", "=", customer_name], + ["Dynamic Link", "parenttype", "=", "Address"] + ] + + existing_address = frappe.get_list("Address", filters) + + if not("ShippingAddress" in amazon_order_item_json): + return None + else: + make_address = frappe.new_doc("Address") + + if "AddressLine1" in amazon_order_item_json.ShippingAddress: + make_address.address_line1 = amazon_order_item_json.ShippingAddress.AddressLine1 + else: + make_address.address_line1 = "Not Provided" + + if "City" in amazon_order_item_json.ShippingAddress: + make_address.city = amazon_order_item_json.ShippingAddress.City + else: + make_address.city = "Not Provided" + + if "StateOrRegion" in amazon_order_item_json.ShippingAddress: + make_address.state = amazon_order_item_json.ShippingAddress.StateOrRegion + + if "PostalCode" in amazon_order_item_json.ShippingAddress: + make_address.pincode = amazon_order_item_json.ShippingAddress.PostalCode + + for address in existing_address: + address_doc = frappe.get_doc("Address", address["name"]) + if (address_doc.address_line1 == make_address.address_line1 and + address_doc.pincode == make_address.pincode): + return address + + make_address.append("links", { + "link_doctype": "Customer", + "link_name": customer_name + }) + make_address.address_type = "Shipping" + make_address.insert() + +def get_order_items(market_place_order_id): + mws_orders = get_orders_instance() + + order_items_response = call_mws_method(mws_orders.list_order_items, amazon_order_id=market_place_order_id) + final_order_items = [] + + order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem) + + warehouse = frappe.db.get_value("Amazon MWS Settings", "Amazon MWS Settings", "warehouse") + + while True: + for order_item in order_items_list: + + if not "ItemPrice" in order_item: + price = 0 + else: + price = order_item.ItemPrice.Amount + + final_order_items.append({ + "item_code": get_item_code(order_item), + "item_name": order_item.SellerSKU, + "description": order_item.Title, + "rate": price, + "qty": order_item.QuantityOrdered, + "stock_uom": "Nos", + "warehouse": warehouse, + "conversion_factor": "1.0" + }) + + if not "NextToken" in order_items_response.parsed: + break + + next_token = order_items_response.parsed.NextToken + + order_items_response = call_mws_method(mws_orders.list_order_items_by_next_token, next_token) + order_items_list = return_as_list(order_items_response.parsed.OrderItems.OrderItem) + + return final_order_items + +def get_item_code(order_item): + asin = order_item.ASIN + item_code = frappe.db.get_value("Item", {"amazon_item_code": asin}, "item_code") + if item_code: + return item_code + +def get_charges_and_fees(market_place_order_id): + finances = get_finances_instance() + + charges_fees = {"charges":[], "fees":[]} + + response = call_mws_method(finances.list_financial_events, amazon_order_id=market_place_order_id) + + shipment_event_list = return_as_list(response.parsed.FinancialEvents.ShipmentEventList) + + for shipment_event in shipment_event_list: + if shipment_event: + shipment_item_list = return_as_list(shipment_event.ShipmentEvent.ShipmentItemList.ShipmentItem) + + for shipment_item in shipment_item_list: + charges = return_as_list(shipment_item.ItemChargeList.ChargeComponent) + fees = return_as_list(shipment_item.ItemFeeList.FeeComponent) + + for charge in charges: + if(charge.ChargeType != "Principal"): + charge_account = get_account(charge.ChargeType) + charges_fees.get("charges").append({ + "charge_type":"Actual", + "account_head": charge_account, + "tax_amount": charge.ChargeAmount.CurrencyAmount, + "description": charge.ChargeType + " for " + shipment_item.SellerSKU + }) + + for fee in fees: + fee_account = get_account(fee.FeeType) + charges_fees.get("fees").append({ + "charge_type":"Actual", + "account_head": fee_account, + "tax_amount": fee.FeeAmount.CurrencyAmount, + "description": fee.FeeType + " for " + shipment_item.SellerSKU + }) + + return charges_fees + +def get_finances_instance(): + + mws_settings = frappe.get_doc("Amazon MWS Settings") + + finances = mws.Finances( + account_id = mws_settings.seller_id, + access_key = mws_settings.aws_access_key_id, + secret_key = mws_settings.secret_key, + region= mws_settings.region, + domain= mws_settings.domain, + version="2015-05-01" + ) + + return finances + +def get_account(name): + existing_account = frappe.db.get_value("Account", {"account_name": "Amazon {0}".format(name)}) + account_name = existing_account + mws_settings = frappe.get_doc("Amazon MWS Settings") + + if not existing_account: + try: + new_account = frappe.new_doc("Account") + new_account.account_name = "Amazon {0}".format(name) + new_account.company = mws_settings.company + new_account.parent_account = mws_settings.market_place_account_group + new_account.insert(ignore_permissions=True) + account_name = new_account.name + except Exception as e: + frappe.log_error(message=e, title="Create Account") + + return account_name diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py new file mode 100755 index 0000000000..798e637f29 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py @@ -0,0 +1,642 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Basic interface to Amazon MWS +# Based on http://code.google.com/p/amazon-mws-python +# Extended to include finances object + +import urllib +import hashlib +import hmac +import base64 +import xml_utils +import re +try: + from xml.etree.ElementTree import ParseError as XMLError +except ImportError: + from xml.parsers.expat import ExpatError as XMLError +from time import strftime, gmtime + +from requests import request +from requests.exceptions import HTTPError + + +__all__ = [ + 'Feeds', + 'Inventory', + 'MWSError', + 'Reports', + 'Orders', + 'Products', + 'Recommendations', + 'Sellers', + 'Finances' +] + +# See https://images-na.ssl-images-amazon.com/images/G/01/mwsportal/doc/en_US/bde/MWSDeveloperGuide._V357736853_.pdf page 8 +# for a list of the end points and marketplace IDs + +MARKETPLACES = { + "CA" : "https://mws.amazonservices.ca", #A2EUQ1WTGCTBG2 + "US" : "https://mws.amazonservices.com", #ATVPDKIKX0DER", + "DE" : "https://mws-eu.amazonservices.com", #A1PA6795UKMFR9 + "ES" : "https://mws-eu.amazonservices.com", #A1RKKUPIHCS9HS + "FR" : "https://mws-eu.amazonservices.com", #A13V1IB3VIYZZH + "IN" : "https://mws.amazonservices.in", #A21TJRUUN4KGV + "IT" : "https://mws-eu.amazonservices.com", #APJ6JRA9NG5V4 + "UK" : "https://mws-eu.amazonservices.com", #A1F83G8C2ARO7P + "JP" : "https://mws.amazonservices.jp", #A1VC38T7YXB528 + "CN" : "https://mws.amazonservices.com.cn", #AAHKV2X7AFYLW +} + + +class MWSError(Exception): + """ + Main MWS Exception class + """ + # Allows quick access to the response object. + # Do not rely on this attribute, always check if its not None. + response = None + +def calc_md5(string): + """Calculates the MD5 encryption for the given string + """ + md = hashlib.md5() + md.update(string) + return base64.encodestring(md.digest()).strip('\n') + +def remove_empty(d): + """ + Helper function that removes all keys from a dictionary (d), + that have an empty value. + """ + for key in d.keys(): + if not d[key]: + del d[key] + return d + +def remove_namespace(xml): + regex = re.compile(' xmlns(:ns2)?="[^"]+"|(ns2:)|(xml:)') + return regex.sub('', xml) + +class DictWrapper(object): + def __init__(self, xml, rootkey=None): + self.original = xml + self._rootkey = rootkey + self._mydict = xml_utils.xml2dict().fromstring(remove_namespace(xml)) + self._response_dict = self._mydict.get(self._mydict.keys()[0], + self._mydict) + + @property + def parsed(self): + if self._rootkey: + return self._response_dict.get(self._rootkey) + else: + return self._response_dict + +class DataWrapper(object): + """ + Text wrapper in charge of validating the hash sent by Amazon. + """ + def __init__(self, data, header): + self.original = data + if 'content-md5' in header: + hash_ = calc_md5(self.original) + if header['content-md5'] != hash_: + raise MWSError("Wrong Contentlength, maybe amazon error...") + + @property + def parsed(self): + return self.original + +class MWS(object): + """ Base Amazon API class """ + + # This is used to post/get to the different uris used by amazon per api + # ie. /Orders/2011-01-01 + # All subclasses must define their own URI only if needed + URI = "/" + + # The API version varies in most amazon APIs + VERSION = "2009-01-01" + + # There seem to be some xml namespace issues. therefore every api subclass + # is recommended to define its namespace, so that it can be referenced + # like so AmazonAPISubclass.NS. + # For more information see http://stackoverflow.com/a/8719461/389453 + NS = '' + + # Some APIs are available only to either a "Merchant" or "Seller" + # the type of account needs to be sent in every call to the amazon MWS. + # This constant defines the exact name of the parameter Amazon expects + # for the specific API being used. + # All subclasses need to define this if they require another account type + # like "Merchant" in which case you define it like so. + # ACCOUNT_TYPE = "Merchant" + # Which is the name of the parameter for that specific account type. + ACCOUNT_TYPE = "SellerId" + + def __init__(self, access_key, secret_key, account_id, region='US', domain='', uri="", version=""): + self.access_key = access_key + self.secret_key = secret_key + self.account_id = account_id + self.version = version or self.VERSION + self.uri = uri or self.URI + + if domain: + self.domain = domain + elif region in MARKETPLACES: + self.domain = MARKETPLACES[region] + else: + error_msg = "Incorrect region supplied ('%(region)s'). Must be one of the following: %(marketplaces)s" % { + "marketplaces" : ', '.join(MARKETPLACES.keys()), + "region" : region, + } + raise MWSError(error_msg) + + def make_request(self, extra_data, method="GET", **kwargs): + """Make request to Amazon MWS API with these parameters + """ + + # Remove all keys with an empty value because + # Amazon's MWS does not allow such a thing. + extra_data = remove_empty(extra_data) + + params = { + 'AWSAccessKeyId': self.access_key, + self.ACCOUNT_TYPE: self.account_id, + 'SignatureVersion': '2', + 'Timestamp': self.get_timestamp(), + 'Version': self.version, + 'SignatureMethod': 'HmacSHA256', + } + params.update(extra_data) + request_description = '&'.join(['%s=%s' % (k, urllib.quote(params[k], safe='-_.~').encode('utf-8')) for k in sorted(params)]) + signature = self.calc_signature(method, request_description) + url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, urllib.quote(signature)) + headers = {'User-Agent': 'python-amazon-mws/0.0.1 (Language=Python)'} + headers.update(kwargs.get('extra_headers', {})) + + try: + # Some might wonder as to why i don't pass the params dict as the params argument to request. + # My answer is, here i have to get the url parsed string of params in order to sign it, so + # if i pass the params dict as params to request, request will repeat that step because it will need + # to convert the dict to a url parsed string, so why do it twice if i can just pass the full url :). + response = request(method, url, data=kwargs.get('body', ''), headers=headers) + response.raise_for_status() + # When retrieving data from the response object, + # be aware that response.content returns the content in bytes while response.text calls + # response.content and converts it to unicode. + data = response.content + + # I do not check the headers to decide which content structure to server simply because sometimes + # Amazon's MWS API returns XML error responses with "text/plain" as the Content-Type. + try: + parsed_response = DictWrapper(data, extra_data.get("Action") + "Result") + except XMLError: + parsed_response = DataWrapper(data, response.headers) + + except HTTPError, e: + error = MWSError(str(e)) + error.response = e.response + raise error + + # Store the response object in the parsed_response for quick access + parsed_response.response = response + return parsed_response + + def get_service_status(self): + """ + Returns a GREEN, GREEN_I, YELLOW or RED status. + Depending on the status/availability of the API its being called from. + """ + + return self.make_request(extra_data=dict(Action='GetServiceStatus')) + + def calc_signature(self, method, request_description): + """Calculate MWS signature to interface with Amazon + """ + sig_data = method + '\n' + self.domain.replace('https://', '').lower() + '\n' + self.uri + '\n' + request_description + return base64.b64encode(hmac.new(str(self.secret_key), sig_data, hashlib.sha256).digest()) + + def get_timestamp(self): + """ + Returns the current timestamp in proper format. + """ + return strftime("%Y-%m-%dT%H:%M:%SZ", gmtime()) + + def enumerate_param(self, param, values): + """ + Builds a dictionary of an enumerated parameter. + Takes any iterable and returns a dictionary. + ie. + enumerate_param('MarketplaceIdList.Id', (123, 345, 4343)) + returns + { + MarketplaceIdList.Id.1: 123, + MarketplaceIdList.Id.2: 345, + MarketplaceIdList.Id.3: 4343 + } + """ + params = {} + if values is not None: + if not param.endswith('.'): + param = "%s." % param + for num, value in enumerate(values): + params['%s%d' % (param, (num + 1))] = value + return params + + +class Feeds(MWS): + """ Amazon MWS Feeds API """ + + ACCOUNT_TYPE = "Merchant" + + def submit_feed(self, feed, feed_type, marketplaceids=None, + content_type="text/xml", purge='false'): + """ + Uploads a feed ( xml or .tsv ) to the seller's inventory. + Can be used for creating/updating products on Amazon. + """ + data = dict(Action='SubmitFeed', + FeedType=feed_type, + PurgeAndReplace=purge) + data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) + md = calc_md5(feed) + return self.make_request(data, method="POST", body=feed, + extra_headers={'Content-MD5': md, 'Content-Type': content_type}) + + def get_feed_submission_list(self, feedids=None, max_count=None, feedtypes=None, + processingstatuses=None, fromdate=None, todate=None): + """ + Returns a list of all feed submissions submitted in the previous 90 days. + That match the query parameters. + """ + + data = dict(Action='GetFeedSubmissionList', + MaxCount=max_count, + SubmittedFromDate=fromdate, + SubmittedToDate=todate,) + data.update(self.enumerate_param('FeedSubmissionIdList.Id', feedids)) + data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes)) + data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses)) + return self.make_request(data) + + def get_submission_list_by_next_token(self, token): + data = dict(Action='GetFeedSubmissionListByNextToken', NextToken=token) + return self.make_request(data) + + def get_feed_submission_count(self, feedtypes=None, processingstatuses=None, fromdate=None, todate=None): + data = dict(Action='GetFeedSubmissionCount', + SubmittedFromDate=fromdate, + SubmittedToDate=todate) + data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes)) + data.update(self.enumerate_param('FeedProcessingStatusList.Status.', processingstatuses)) + return self.make_request(data) + + def cancel_feed_submissions(self, feedids=None, feedtypes=None, fromdate=None, todate=None): + data = dict(Action='CancelFeedSubmissions', + SubmittedFromDate=fromdate, + SubmittedToDate=todate) + data.update(self.enumerate_param('FeedSubmissionIdList.Id.', feedids)) + data.update(self.enumerate_param('FeedTypeList.Type.', feedtypes)) + return self.make_request(data) + + def get_feed_submission_result(self, feedid): + data = dict(Action='GetFeedSubmissionResult', FeedSubmissionId=feedid) + return self.make_request(data) + +class Reports(MWS): + """ Amazon MWS Reports API """ + + ACCOUNT_TYPE = "Merchant" + + ## REPORTS ### + + def get_report(self, report_id): + data = dict(Action='GetReport', ReportId=report_id) + return self.make_request(data) + + def get_report_count(self, report_types=(), acknowledged=None, fromdate=None, todate=None): + data = dict(Action='GetReportCount', + Acknowledged=acknowledged, + AvailableFromDate=fromdate, + AvailableToDate=todate) + data.update(self.enumerate_param('ReportTypeList.Type.', report_types)) + return self.make_request(data) + + def get_report_list(self, requestids=(), max_count=None, types=(), acknowledged=None, + fromdate=None, todate=None): + data = dict(Action='GetReportList', + Acknowledged=acknowledged, + AvailableFromDate=fromdate, + AvailableToDate=todate, + MaxCount=max_count) + data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids)) + data.update(self.enumerate_param('ReportTypeList.Type.', types)) + return self.make_request(data) + + def get_report_list_by_next_token(self, token): + data = dict(Action='GetReportListByNextToken', NextToken=token) + return self.make_request(data) + + def get_report_request_count(self, report_types=(), processingstatuses=(), fromdate=None, todate=None): + data = dict(Action='GetReportRequestCount', + RequestedFromDate=fromdate, + RequestedToDate=todate) + data.update(self.enumerate_param('ReportTypeList.Type.', report_types)) + data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses)) + return self.make_request(data) + + def get_report_request_list(self, requestids=(), types=(), processingstatuses=(), + max_count=None, fromdate=None, todate=None): + data = dict(Action='GetReportRequestList', + MaxCount=max_count, + RequestedFromDate=fromdate, + RequestedToDate=todate) + data.update(self.enumerate_param('ReportRequestIdList.Id.', requestids)) + data.update(self.enumerate_param('ReportTypeList.Type.', types)) + data.update(self.enumerate_param('ReportProcessingStatusList.Status.', processingstatuses)) + return self.make_request(data) + + def get_report_request_list_by_next_token(self, token): + data = dict(Action='GetReportRequestListByNextToken', NextToken=token) + return self.make_request(data) + + def request_report(self, report_type, start_date=None, end_date=None, marketplaceids=()): + data = dict(Action='RequestReport', + ReportType=report_type, + StartDate=start_date, + EndDate=end_date) + data.update(self.enumerate_param('MarketplaceIdList.Id.', marketplaceids)) + return self.make_request(data) + + ### ReportSchedule ### + + def get_report_schedule_list(self, types=()): + data = dict(Action='GetReportScheduleList') + data.update(self.enumerate_param('ReportTypeList.Type.', types)) + return self.make_request(data) + + def get_report_schedule_count(self, types=()): + data = dict(Action='GetReportScheduleCount') + data.update(self.enumerate_param('ReportTypeList.Type.', types)) + return self.make_request(data) + + +class Orders(MWS): + """ Amazon Orders API """ + + URI = "/Orders/2013-09-01" + VERSION = "2013-09-01" + NS = '{https://mws.amazonservices.com/Orders/2011-01-01}' + + def list_orders(self, marketplaceids, created_after=None, created_before=None, lastupdatedafter=None, + lastupdatedbefore=None, orderstatus=(), fulfillment_channels=(), + payment_methods=(), buyer_email=None, seller_orderid=None, max_results='100'): + + data = dict(Action='ListOrders', + CreatedAfter=created_after, + CreatedBefore=created_before, + LastUpdatedAfter=lastupdatedafter, + LastUpdatedBefore=lastupdatedbefore, + BuyerEmail=buyer_email, + SellerOrderId=seller_orderid, + MaxResultsPerPage=max_results, + ) + data.update(self.enumerate_param('OrderStatus.Status.', orderstatus)) + data.update(self.enumerate_param('MarketplaceId.Id.', marketplaceids)) + data.update(self.enumerate_param('FulfillmentChannel.Channel.', fulfillment_channels)) + data.update(self.enumerate_param('PaymentMethod.Method.', payment_methods)) + return self.make_request(data) + + def list_orders_by_next_token(self, token): + data = dict(Action='ListOrdersByNextToken', NextToken=token) + return self.make_request(data) + + def get_order(self, amazon_order_ids): + data = dict(Action='GetOrder') + data.update(self.enumerate_param('AmazonOrderId.Id.', amazon_order_ids)) + return self.make_request(data) + + def list_order_items(self, amazon_order_id): + data = dict(Action='ListOrderItems', AmazonOrderId=amazon_order_id) + return self.make_request(data) + + def list_order_items_by_next_token(self, token): + data = dict(Action='ListOrderItemsByNextToken', NextToken=token) + return self.make_request(data) + + +class Products(MWS): + """ Amazon MWS Products API """ + + URI = '/Products/2011-10-01' + VERSION = '2011-10-01' + NS = '{http://mws.amazonservices.com/schema/Products/2011-10-01}' + + def list_matching_products(self, marketplaceid, query, contextid=None): + """ Returns a list of products and their attributes, ordered by + relevancy, based on a search query that you specify. + Your search query can be a phrase that describes the product + or it can be a product identifier such as a UPC, EAN, ISBN, or JAN. + """ + data = dict(Action='ListMatchingProducts', + MarketplaceId=marketplaceid, + Query=query, + QueryContextId=contextid) + return self.make_request(data) + + def get_matching_product(self, marketplaceid, asins): + """ Returns a list of products and their attributes, based on a list of + ASIN values that you specify. + """ + data = dict(Action='GetMatchingProduct', MarketplaceId=marketplaceid) + data.update(self.enumerate_param('ASINList.ASIN.', asins)) + return self.make_request(data) + + def get_matching_product_for_id(self, marketplaceid, type, id): + """ Returns a list of products and their attributes, based on a list of + product identifier values (asin, sellersku, upc, ean, isbn and JAN) + Added in Fourth Release, API version 2011-10-01 + """ + data = dict(Action='GetMatchingProductForId', + MarketplaceId=marketplaceid, + IdType=type) + data.update(self.enumerate_param('IdList.Id', id)) + return self.make_request(data) + + def get_competitive_pricing_for_sku(self, marketplaceid, skus): + """ Returns the current competitive pricing of a product, + based on the SellerSKU and MarketplaceId that you specify. + """ + data = dict(Action='GetCompetitivePricingForSKU', MarketplaceId=marketplaceid) + data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus)) + return self.make_request(data) + + def get_competitive_pricing_for_asin(self, marketplaceid, asins): + """ Returns the current competitive pricing of a product, + based on the ASIN and MarketplaceId that you specify. + """ + data = dict(Action='GetCompetitivePricingForASIN', MarketplaceId=marketplaceid) + data.update(self.enumerate_param('ASINList.ASIN.', asins)) + return self.make_request(data) + + def get_lowest_offer_listings_for_sku(self, marketplaceid, skus, condition="Any", excludeme="False"): + data = dict(Action='GetLowestOfferListingsForSKU', + MarketplaceId=marketplaceid, + ItemCondition=condition, + ExcludeMe=excludeme) + data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus)) + return self.make_request(data) + + def get_lowest_offer_listings_for_asin(self, marketplaceid, asins, condition="Any", excludeme="False"): + data = dict(Action='GetLowestOfferListingsForASIN', + MarketplaceId=marketplaceid, + ItemCondition=condition, + ExcludeMe=excludeme) + data.update(self.enumerate_param('ASINList.ASIN.', asins)) + return self.make_request(data) + + def get_product_categories_for_sku(self, marketplaceid, sku): + data = dict(Action='GetProductCategoriesForSKU', + MarketplaceId=marketplaceid, + SellerSKU=sku) + return self.make_request(data) + + def get_product_categories_for_asin(self, marketplaceid, asin): + data = dict(Action='GetProductCategoriesForASIN', + MarketplaceId=marketplaceid, + ASIN=asin) + return self.make_request(data) + + def get_my_price_for_sku(self, marketplaceid, skus, condition=None): + data = dict(Action='GetMyPriceForSKU', + MarketplaceId=marketplaceid, + ItemCondition=condition) + data.update(self.enumerate_param('SellerSKUList.SellerSKU.', skus)) + return self.make_request(data) + + def get_my_price_for_asin(self, marketplaceid, asins, condition=None): + data = dict(Action='GetMyPriceForASIN', + MarketplaceId=marketplaceid, + ItemCondition=condition) + data.update(self.enumerate_param('ASINList.ASIN.', asins)) + return self.make_request(data) + + +class Sellers(MWS): + """ Amazon MWS Sellers API """ + + URI = '/Sellers/2011-07-01' + VERSION = '2011-07-01' + NS = '{http://mws.amazonservices.com/schema/Sellers/2011-07-01}' + + def list_marketplace_participations(self): + """ + Returns a list of marketplaces a seller can participate in and + a list of participations that include seller-specific information in that marketplace. + The operation returns only those marketplaces where the seller's account is in an active state. + """ + + data = dict(Action='ListMarketplaceParticipations') + return self.make_request(data) + + def list_marketplace_participations_by_next_token(self, token): + """ + Takes a "NextToken" and returns the same information as "list_marketplace_participations". + Based on the "NextToken". + """ + data = dict(Action='ListMarketplaceParticipations', NextToken=token) + return self.make_request(data) + +#### Fulfillment APIs #### + +class InboundShipments(MWS): + URI = "/FulfillmentInboundShipment/2010-10-01" + VERSION = '2010-10-01' + + # To be completed + + +class Inventory(MWS): + """ Amazon MWS Inventory Fulfillment API """ + + URI = '/FulfillmentInventory/2010-10-01' + VERSION = '2010-10-01' + NS = "{http://mws.amazonaws.com/FulfillmentInventory/2010-10-01}" + + def list_inventory_supply(self, skus=(), datetime=None, response_group='Basic'): + """ Returns information on available inventory """ + + data = dict(Action='ListInventorySupply', + QueryStartDateTime=datetime, + ResponseGroup=response_group, + ) + data.update(self.enumerate_param('SellerSkus.member.', skus)) + return self.make_request(data, "POST") + + def list_inventory_supply_by_next_token(self, token): + data = dict(Action='ListInventorySupplyByNextToken', NextToken=token) + return self.make_request(data, "POST") + + +class OutboundShipments(MWS): + URI = "/FulfillmentOutboundShipment/2010-10-01" + VERSION = "2010-10-01" + # To be completed + + +class Recommendations(MWS): + + """ Amazon MWS Recommendations API """ + + URI = '/Recommendations/2013-04-01' + VERSION = '2013-04-01' + NS = "{https://mws.amazonservices.com/Recommendations/2013-04-01}" + + def get_last_updated_time_for_recommendations(self, marketplaceid): + """ + Checks whether there are active recommendations for each category for the given marketplace, and if there are, + returns the time when recommendations were last updated for each category. + """ + + data = dict(Action='GetLastUpdatedTimeForRecommendations', + MarketplaceId=marketplaceid) + return self.make_request(data, "POST") + + def list_recommendations(self, marketplaceid, recommendationcategory=None): + """ + Returns your active recommendations for a specific category or for all categories for a specific marketplace. + """ + + data = dict(Action="ListRecommendations", + MarketplaceId=marketplaceid, + RecommendationCategory=recommendationcategory) + return self.make_request(data, "POST") + + def list_recommendations_by_next_token(self, token): + """ + Returns the next page of recommendations using the NextToken parameter. + """ + + data = dict(Action="ListRecommendationsByNextToken", + NextToken=token) + return self.make_request(data, "POST") + +class Finances(MWS): + """ Amazon Finances API""" + URI = '/Finances/2015-05-01' + VERSION = '2015-05-01' + NS = "{https://mws.amazonservices.com/Finances/2015-05-01}" + + def list_financial_events(self , posted_after=None, posted_before=None, + amazon_order_id=None, max_results='100'): + + data = dict(Action='ListFinancialEvents', + PostedAfter=posted_after, + PostedBefore=posted_before, + AmazonOrderId=amazon_order_id, + MaxResultsPerPage=max_results, + ) + return self.make_request(data) diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js new file mode 100644 index 0000000000..a9925adee7 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.js @@ -0,0 +1,3 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.json b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.json new file mode 100644 index 0000000000..771d1f26e2 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.json @@ -0,0 +1,974 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "autoname": "", + "beta": 0, + "creation": "2018-07-31 05:51:41.357047", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "enable_amazon", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Enable Amazon", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": "", + "columns": 0, + "fieldname": "mws_credentials", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "MWS Credentials", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "seller_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Seller ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "aws_access_key_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "AWS Access Key ID", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "mws_auth_token", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "MWS Auth Token", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "secret_key", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Secret Key", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_4", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "market_place_id", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Market Place ID", + "length": 0, + "no_copy": 0, + "options": "", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "region", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Region", + "length": 0, + "no_copy": 0, + "options": "\nIN\nCN\nJP\nBR\nAU\nES\nUK\nFR\nDE\nIT\nCA\nUS\nMX", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "domain", + "fieldtype": "Data", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Domain", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_13", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Company", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "warehouse", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Warehouse", + "length": 0, + "no_copy": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "item_group", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Item Group", + "length": 0, + "no_copy": 0, + "options": "Item Group", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "price_list", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Price List", + "length": 0, + "no_copy": 0, + "options": "Price List", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_17", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer_group", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Customer Group", + "length": 0, + "no_copy": 0, + "options": "Customer Group", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "territory", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Territory", + "length": 0, + "no_copy": 0, + "options": "Territory", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "customer_type", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Customer Type", + "length": 0, + "no_copy": 0, + "options": "Individual\nCompany", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "market_place_account_group", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Market Place Account Group", + "length": 0, + "no_copy": 0, + "options": "Account", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "section_break_12", + "fieldtype": "Section Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Amazon will synch data updated after this date", + "fieldname": "after_date", + "fieldtype": "Datetime", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "After Date", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Get financial breakup of Taxes and charges data by Amazon ", + "fieldname": "taxes_charges", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Synch Taxes and Charges", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Always synch your products from Amazon MWS before synching the Orders details", + "fieldname": "synch_products", + "fieldtype": "Button", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Synch Products", + "length": 0, + "no_copy": 0, + "options": "get_products_details", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "description": "Click this button to pull your Sales Order data from Amazon MWS.", + "fieldname": "synch_orders", + "fieldtype": "Button", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Synch Orders", + "length": 0, + "no_copy": 0, + "options": "get_order_details", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "column_break_10", + "fieldtype": "Column Break", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "1", + "description": "Check this to enable a scheduled Daily synchronization routine via scheduler", + "fieldname": "enable_synch", + "fieldtype": "Check", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Enable Scheduled Synch", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "3", + "fieldname": "max_retry_limit", + "fieldtype": "Int", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Max Retry Limit", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + } + ], + "has_web_view": 0, + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2018-08-23 20:52:58.471424", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "Amazon MWS Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "System Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "show_name_in_global_search": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py new file mode 100644 index 0000000000..7e64915c3f --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_settings.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +import dateutil +from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from amazon_methods import get_products_details, get_orders + +class AmazonMWSSettings(Document): + def validate(self): + if self.enable_amazon == 1: + setup_custom_fields() + + def get_products_details(self): + if self.enable_amazon == 1: + get_products_details() + + def get_order_details(self): + if self.enable_amazon == 1: + after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d") + get_orders(after_date = after_date) + +def schedule_get_order_details(): + mws_settings = frappe.get_doc("Amazon MWS Settings") + if mws_settings.enable_synch: + after_date = dateutil.parser.parse(mws_settings.after_date).strftime("%Y-%m-%d") + get_orders(after_date = after_date) + +def setup_custom_fields(): + custom_fields = { + "Item": [dict(fieldname='amazon_item_code', label='Amazon Item Code', + fieldtype='Data', insert_after='series', read_only=1, print_hide=1)], + "Sales Order": [dict(fieldname='amazon_order_id', label='Amazon Order ID', + fieldtype='Data', insert_after='title', read_only=1, print_hide=1)] + } + + create_custom_fields(custom_fields) \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.js b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.js new file mode 100644 index 0000000000..9c8990986e --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +// rename this file from _test_[name] to test_[name] to activate +// and remove above this line + +QUnit.test("test: Amazon MWS Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Amazon MWS Settings + () => frappe.tests.make('Amazon MWS Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.py new file mode 100644 index 0000000000..7b40014c52 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/test_amazon_mws_settings.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import unittest + +class TestAmazonMWSSettings(unittest.TestCase): + pass diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py new file mode 100644 index 0000000000..985ac083e5 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/xml_utils.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +""" +Created on Tue Jun 26 15:42:07 2012 + +Borrowed from https://github.com/timotheus/ebaysdk-python + +@author: pierre +""" + +import xml.etree.ElementTree as ET +import re + + +class object_dict(dict): + """object view of dict, you can + >>> a = object_dict() + >>> a.fish = 'fish' + >>> a['fish'] + 'fish' + >>> a['water'] = 'water' + >>> a.water + 'water' + >>> a.test = {'value': 1} + >>> a.test2 = object_dict({'name': 'test2', 'value': 2}) + >>> a.test, a.test2.name, a.test2.value + (1, 'test2', 2) + """ + def __init__(self, initd=None): + if initd is None: + initd = {} + dict.__init__(self, initd) + + def __getattr__(self, item): + + d = self.__getitem__(item) + + if isinstance(d, dict) and 'value' in d and len(d) == 1: + return d['value'] + else: + return d + + # if value is the only key in object, you can omit it + def __setstate__(self, item): + return False + + def __setattr__(self, item, value): + self.__setitem__(item, value) + + def getvalue(self, item, value=None): + return self.get(item, {}).get('value', value) + + +class xml2dict(object): + + def __init__(self): + pass + + def _parse_node(self, node): + node_tree = object_dict() + # Save attrs and text, hope there will not be a child with same name + if node.text: + node_tree.value = node.text + for (k, v) in node.attrib.items(): + k, v = self._namespace_split(k, object_dict({'value':v})) + node_tree[k] = v + #Save childrens + for child in node.getchildren(): + tag, tree = self._namespace_split(child.tag, + self._parse_node(child)) + if tag not in node_tree: # the first time, so store it in dict + node_tree[tag] = tree + continue + old = node_tree[tag] + if not isinstance(old, list): + node_tree.pop(tag) + node_tree[tag] = [old] # multi times, so change old dict to a list + node_tree[tag].append(tree) # add the new one + + return node_tree + + def _namespace_split(self, tag, value): + """ + Split the tag '{http://cs.sfsu.edu/csc867/myscheduler}patients' + ns = http://cs.sfsu.edu/csc867/myscheduler + name = patients + """ + result = re.compile("\{(.*)\}(.*)").search(tag) + if result: + value.namespace, tag = result.groups() + + return (tag, value) + + def parse(self, file): + """parse a xml file to a dict""" + f = open(file, 'r') + return self.fromstring(f.read()) + + def fromstring(self, s): + """parse a string""" + t = ET.fromstring(s) + root_tag, root_tree = self._namespace_split(t.tag, self._parse_node(t)) + return object_dict({root_tag: root_tree}) \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 300808aa95..20552beaa4 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -224,7 +224,8 @@ doc_events = { scheduler_events = { "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', - "erpnext.accounts.doctype.subscription.subscription.process_all" + "erpnext.accounts.doctype.subscription.subscription.process_all", + "erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_mws_settings.schedule_get_order_details" ], "daily": [ "erpnext.stock.reorder_item.reorder_item",