diff --git a/erpnext/config/erpnext_integrations.py b/erpnext/config/erpnext_integrations.py index 22a01a6ada..88c4779187 100644 --- a/erpnext/config/erpnext_integrations.py +++ b/erpnext/config/erpnext_integrations.py @@ -18,5 +18,14 @@ def get_data(): "description": _("GoCardless SEPA Mandate"), } ] + }, + { + "label": _("Settings"), + "items": [ + { + "type": "doctype", + "name": "Woocommerce Settings" + } + ] } ] diff --git a/erpnext/docs/assets/img/erpnext_integrations/woocommerce_demo.gif b/erpnext/docs/assets/img/erpnext_integrations/woocommerce_demo.gif new file mode 100644 index 0000000000..6bd8050c7a Binary files /dev/null and b/erpnext/docs/assets/img/erpnext_integrations/woocommerce_demo.gif differ diff --git a/erpnext/docs/assets/img/erpnext_integrations/woocommerce_setting_config.gif b/erpnext/docs/assets/img/erpnext_integrations/woocommerce_setting_config.gif new file mode 100644 index 0000000000..949b5ea839 Binary files /dev/null and b/erpnext/docs/assets/img/erpnext_integrations/woocommerce_setting_config.gif differ diff --git a/erpnext/docs/user/manual/en/erpnext_integration/woocommerce_integration.md b/erpnext/docs/user/manual/en/erpnext_integration/woocommerce_integration.md new file mode 100644 index 0000000000..9b7ea886b3 --- /dev/null +++ b/erpnext/docs/user/manual/en/erpnext_integration/woocommerce_integration.md @@ -0,0 +1,57 @@ + +# WooCommerce Integration + +#### Setting Up WooCommerce on ERPNEXT:- + +Steps:- + +1. From Awesome-bar, go to "Woocommerce Settings" doctype. + +2. From your woocommerce site, generate "API consumer key" and "API consumer secret" using Keys/Apps tab. + +3. Paste those generated "API consumer key" and "API consumer secret" into "Woocommerce Settings" doctype. + +4. In "Woocommerce Server URL" paste the url of your site where ERPNEXT is installed. + +5. Make sure "Enable Sync" is checked. + +6. Select Account type from Account Details Section. + +7. Click Save. + +8. After saving, "Secret" and "Endpoint" are generated automatically and can be seen on "Woocommerce Settings" doctype. + +9. Now from your woocommerce site, click on webhooks option and click on "Add Webhook". + +10. Give name to the webhook of your choice. Click on Status dropdown and select "Active". Select Topic as "Order Created". Copy the "Endpoint" from "Woocommerce Settings" doctype and paste it in "Delivery URL" field. Copy "Secret" from "Woocommerce Settings" doctype and paste it in "Secret" field. Keep API VERSION as it is and click on Save Webhook. + +11. Now the WooCommerce is successful setup on your system. + +Woocommerce Integration + + +### Note:- In above gif, inplace of delivery url on woocommerce website, you need to paste the url you will obtain after saving the "Woocommerce Settings" page (i.e. Endpoint from "Woocommerce Settings"). I pasted other url because I was using localhost. Please paste your endpoint in place of Delivery URL. + + + +#### WooCommerce Integration Working:- + +Steps:- + +1. From your Woocommerce website, register yourself as a user. + +2. Now Click on Address Details and provide the required details. + +3. For start shopping, click on Shop option and now available products can be seen. + +4. Add the desired products into cart and click on View Cart. + +5. From Cart, once you have added the desired products, you can click on proceed to checkout. + +6. All billing details and Order details can be seen now. Once you are ok with it, click on Place Order button. + +7. "Order Received" message can been seen indicating that the order is placed successfully. + +8. Now on system where ERPNEXT is installed check the following doctypes: Customer, Address, Item, Sales Order. + +Woocommerce Integration \ No newline at end of file diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py new file mode 100644 index 0000000000..62c25b3746 --- /dev/null +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -0,0 +1,206 @@ + +from __future__ import unicode_literals +import frappe, base64, hashlib, hmac, json +import datetime +from frappe import _ + + +def verify_request(): + woocommerce_settings = frappe.get_doc("Woocommerce Settings") + sig = base64.b64encode( + hmac.new( + woocommerce_settings.secret.encode('utf8'), + frappe.request.data, + hashlib.sha256 + ).digest() + ) + + if frappe.request.data and \ + frappe.get_request_header("X-Wc-Webhook-Signature") and \ + not sig == bytes(frappe.get_request_header("X-Wc-Webhook-Signature").encode()): + frappe.throw(_("Unverified Webhook Data")) + frappe.set_user(woocommerce_settings.modified_by) + +@frappe.whitelist(allow_guest=True) +def order(): + + verify_request() + + if frappe.request.data: + fd = json.loads(frappe.request.data) + else: + return "success" + + event = frappe.get_request_header("X-Wc-Webhook-Event") + + if event == "created": + + raw_billing_data = fd.get("billing") + customer_woo_com_email = raw_billing_data.get("email") + + if frappe.get_value("Customer",{"woocommerce_email": customer_woo_com_email}): + # Edit + link_customer_and_address(raw_billing_data,1) + else: + # Create + link_customer_and_address(raw_billing_data,0) + + + items_list = fd.get("line_items") + for item in items_list: + + item_woo_com_id = item.get("product_id") + + if frappe.get_value("Item",{"woocommerce_id": item_woo_com_id}): + #Edit + link_item(item,1) + else: + link_item(item,0) + + + customer_name = raw_billing_data.get("first_name") + " " + raw_billing_data.get("last_name") + + new_sales_order = frappe.new_doc("Sales Order") + new_sales_order.customer = customer_name + + created_date = fd.get("date_created").split("T") + new_sales_order.transaction_date = created_date[0] + + new_sales_order.po_no = fd.get("id") + new_sales_order.woocommerce_id = fd.get("id") + new_sales_order.naming_series = "SO-" + + placed_order_date = created_date[0] + raw_date = datetime.datetime.strptime(placed_order_date, "%Y-%m-%d") + raw_delivery_date = frappe.utils.add_to_date(raw_date,days = 7) + order_delivery_date_str = raw_delivery_date.strftime('%Y-%m-%d') + order_delivery_date = str(order_delivery_date_str) + + new_sales_order.delivery_date = order_delivery_date + + for item in items_list: + woocomm_item_id = item.get("product_id") + found_item = frappe.get_doc("Item",{"woocommerce_id": woocomm_item_id}) + + ordered_items_tax = item.get("total_tax") + + default_set_company = frappe.get_doc("Global Defaults") + company = default_set_company.default_company + found_company = frappe.get_doc("Company",{"name":company}) + company_abbr = found_company.abbr + + new_sales_order.append("items",{ + "item_code": found_item.item_code, + "item_name": found_item.item_name, + "description": found_item.item_name, + "delivery_date":order_delivery_date, + "uom": "Nos", + "qty": item.get("quantity"), + "rate": item.get("price"), + "warehouse": "Stores" + " - " + company_abbr + }) + + add_tax_details(new_sales_order,ordered_items_tax,"Ordered Item tax",0) + + # shipping_details = fd.get("shipping_lines") # used for detailed order + shipping_total = fd.get("shipping_total") + shipping_tax = fd.get("shipping_tax") + + add_tax_details(new_sales_order,shipping_tax,"Shipping Tax",1) + add_tax_details(new_sales_order,shipping_total,"Shipping Total",1) + + new_sales_order.submit() + + frappe.db.commit() + +def link_customer_and_address(raw_billing_data,customer_status): + + if customer_status == 0: + # create + customer = frappe.new_doc("Customer") + address = frappe.new_doc("Address") + + if customer_status == 1: + # Edit + customer_woo_com_email = raw_billing_data.get("email") + customer = frappe.get_doc("Customer",{"woocommerce_email": customer_woo_com_email}) + old_name = customer.customer_name + + full_name = str(raw_billing_data.get("first_name"))+ " "+str(raw_billing_data.get("last_name")) + customer.customer_name = full_name + customer.woocommerce_email = str(raw_billing_data.get("email")) + customer.save() + frappe.db.commit() + + if customer_status == 1: + frappe.rename_doc("Customer", old_name, full_name) + address = frappe.get_doc("Address",{"woocommerce_email":customer_woo_com_email}) + customer = frappe.get_doc("Customer",{"woocommerce_email": customer_woo_com_email}) + + address.address_line1 = raw_billing_data.get("address_1", "Not Provided") + address.address_line2 = raw_billing_data.get("address_2", "Not Provided") + address.city = raw_billing_data.get("city", "Not Provided") + address.woocommerce_email = str(raw_billing_data.get("email")) + address.address_type = "Shipping" + address.country = frappe.get_value("Country", filters={"code":raw_billing_data.get("country", "IN").lower()}) + address.state = raw_billing_data.get("state") + address.pincode = str(raw_billing_data.get("postcode")) + address.phone = str(raw_billing_data.get("phone")) + address.email_id = str(raw_billing_data.get("email")) + + address.append("links", { + "link_doctype": "Customer", + "link_name": customer.customer_name + }) + + address.save() + frappe.db.commit() + + if customer_status == 1: + + address = frappe.get_doc("Address",{"woocommerce_email":customer_woo_com_email}) + old_address_title = address.name + new_address_title = customer.customer_name+"-billing" + address.address_title = customer.customer_name + address.save() + + frappe.rename_doc("Address",old_address_title,new_address_title) + + frappe.db.commit() + +def link_item(item_data,item_status): + + if item_status == 0: + #Create Item + item = frappe.new_doc("Item") + + if item_status == 1: + #Edit Item + item_woo_com_id = item_data.get("product_id") + item = frappe.get_doc("Item",{"woocommerce_id": item_woo_com_id}) + + item.item_name = str(item_data.get("name")) + item.item_code = "woocommerce - " + str(item_data.get("product_id")) + item.woocommerce_id = str(item_data.get("product_id")) + item.item_group = "WooCommerce Products" + item.save() + frappe.db.commit() + +def add_tax_details(sales_order,price,desc,status): + + woocommerce_settings = frappe.get_doc("Woocommerce Settings") + + if status == 0: + # Product taxes + account_head_type = woocommerce_settings.tax_account + + if status == 1: + # Shipping taxes + account_head_type = woocommerce_settings.f_n_f_account + + sales_order.append("taxes",{ + "charge_type":"Actual", + "account_head": account_head_type, + "tax_amount": price, + "description": desc + }) \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.js b/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.js new file mode 100644 index 0000000000..ea06ab2dc4 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_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: Woocommerce Settings", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Woocommerce Settings + () => frappe.tests.make('Woocommerce Settings', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_settings.py new file mode 100644 index 0000000000..458a23fae2 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/test_woocommerce_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 TestWoocommerceSettings(unittest.TestCase): + pass diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js new file mode 100644 index 0000000000..7056931f6c --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.js @@ -0,0 +1,45 @@ +// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Woocommerce Settings', { + refresh (frm) { + frm.trigger("add_button_generate_secret"); + frm.trigger("check_enabled"); + frm.set_query("tax_account", ()=>{ + return { + "filters": { + "company": frappe.defaults.get_default("company"), + "is_group": 0 + } + }; + }); + }, + + enable_sync (frm) { + frm.trigger("check_enabled"); + }, + + add_button_generate_secret(frm) { + frm.add_custom_button(__('Generate Secret'), () => { + frappe.confirm( + __("Apps using current key won't be able to access, are you sure?"), + () => { + frappe.call({ + type:"POST", + method:"erpnext.erpnext_integrations.doctype.woocommerce_settings.woocommerce_settings.generate_secret", + }).done(() => { + frm.reload_doc(); + }).fail(() => { + frappe.msgprint(__("Could not generate Secret")); + }); + } + ); + }); + }, + + check_enabled (frm) { + frm.set_df_property("woocommerce_server_url", "reqd", frm.doc.enable_sync); + frm.set_df_property("api_consumer_key", "reqd", frm.doc.enable_sync); + frm.set_df_property("api_consumer_secret", "reqd", frm.doc.enable_sync); + } +}); \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json new file mode 100644 index 0000000000..2fa7960036 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.json @@ -0,0 +1,465 @@ +{ + "allow_copy": 0, + "allow_guest_to_view": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2018-02-12 15:10:05.495713", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "enable_sync", + "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 Sync", + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "sb_00", + "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": "", + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "woocommerce_server_url", + "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": "Woocommerce Server URL", + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "secret", + "fieldtype": "Code", + "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": "Secret", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "cb_00", + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "api_consumer_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": "API consumer 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": 0, + "search_index": 0, + "set_only_once": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_bulk_edit": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "api_consumer_secret", + "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": "API consumer secret", + "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_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "sb_accounting_details", + "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": "Accounting Details", + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "tax_account", + "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": "Tax Account", + "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_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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "f_n_f_account", + "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": "Freight and Forwarding Account", + "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_on_submit": 0, + "bold": 0, + "collapsible": 1, + "columns": 0, + "fieldname": "endpoints", + "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": "Endpoints", + "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_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "endpoint", + "fieldtype": "Code", + "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": "Endpoint", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "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-03-23 16:57:20.880513", + "modified_by": "Administrator", + "module": "ERPNext Integrations", + "name": "Woocommerce Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 0, + "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 +} \ No newline at end of file diff --git a/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py new file mode 100644 index 0000000000..bb4f62a099 --- /dev/null +++ b/erpnext/erpnext_integrations/doctype/woocommerce_settings/woocommerce_settings.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe + +from frappe import _ +from frappe.model.document import Document +from six.moves.urllib.parse import urlparse + +class WoocommerceSettings(Document): + def validate(self): + self.validate_settings() + self.create_delete_custom_fields() + self.create_webhook_url() + + def create_delete_custom_fields(self): + if self.enable_sync: + # create + create_custom_field_id_and_check_status = False + create_custom_field_email_check = False + names = ["Customer-woocommerce_id","Sales Order-woocommerce_id","Item-woocommerce_id","Address-woocommerce_id"] + names_check_box = ["Customer-woocommerce_check","Sales Order-woocommerce_check","Item-woocommerce_check","Address-woocommerce_check"] + email_names = ["Customer-woocommerce_email","Address-woocommerce_email"] + + for i in zip(names,names_check_box): + + if not frappe.get_value("Custom Field",{"name":i[0]}) or not frappe.get_value("Custom Field",{"name":i[1]}): + create_custom_field_id_and_check_status = True + break; + + + if create_custom_field_id_and_check_status: + names = ["Customer","Sales Order","Item","Address"] + for name in names: + custom = frappe.new_doc("Custom Field") + custom.dt = name + custom.label = "woocommerce_id" + custom.read_only = 1 + custom.save() + + custom = frappe.new_doc("Custom Field") + custom.dt = name + custom.label = "woocommerce_check" + custom.fieldtype = "Check" + custom.read_only = 1 + custom.save() + + for i in email_names: + + if not frappe.get_value("Custom Field",{"name":i}): + create_custom_field_email_check = True + break; + + if create_custom_field_email_check: + names = ["Customer","Address"] + for name in names: + custom = frappe.new_doc("Custom Field") + custom.dt = name + custom.label = "woocommerce_email" + custom.read_only = 1 + custom.save() + + if not frappe.get_value("Item Group",{"name": "WooCommerce Products"}): + item_group = frappe.new_doc("Item Group") + item_group.item_group_name = "WooCommerce Products" + item_group.parent_item_group = "All Item Groups" + item_group.save() + + + elif not self.enable_sync: + # delete + names = ["Customer-woocommerce_id","Sales Order-woocommerce_id","Item-woocommerce_id","Address-woocommerce_id"] + names_check_box = ["Customer-woocommerce_check","Sales Order-woocommerce_check","Item-woocommerce_check","Address-woocommerce_check"] + email_names = ["Customer-woocommerce_email","Address-woocommerce_email"] + for name in names: + frappe.delete_doc("Custom Field",name) + + for name in names_check_box: + frappe.delete_doc("Custom Field",name) + + for name in email_names: + frappe.delete_doc("Custom Field",name) + + frappe.delete_doc("Item Group","WooCommerce Products") + + frappe.db.commit() + + def validate_settings(self): + if self.enable_sync: + if not self.secret: + self.set("secret", frappe.generate_hash()) + + if not self.woocommerce_server_url: + frappe.throw(_("Please enter Woocommerce Server URL")) + + if not self.api_consumer_key: + frappe.throw(_("Please enter API Consumer Key")) + + if not self.api_consumer_secret: + frappe.throw(_("Please enter API Consumer Secret")) + + def create_webhook_url(self): + endpoint = "/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order" + + try: + url = frappe.request.url + except RuntimeError: + # for CI Test to work + url = "http://localhost:8000" + + server_url = '{uri.scheme}://{uri.netloc}'.format( + uri=urlparse(url) + ) + + delivery_url = server_url + endpoint + self.endpoint = delivery_url + +@frappe.whitelist() +def generate_secret(): + woocommerce_settings = frappe.get_doc("Woocommerce Settings") + woocommerce_settings.secret = frappe.generate_hash() + woocommerce_settings.save() diff --git a/erpnext/tests/test_woocommerce.py b/erpnext/tests/test_woocommerce.py new file mode 100644 index 0000000000..67e62f2e30 --- /dev/null +++ b/erpnext/tests/test_woocommerce.py @@ -0,0 +1,91 @@ +import unittest, frappe, requests, os, time, erpnext + +class TestWoocommerce(unittest.TestCase): + + def setUp(self): + # Set Secret in Woocommerce Settings + company = frappe.new_doc("Company") + company.company_name = "Woocommerce" + company.abbr = "W" + company.default_currency = "INR" + company.save() + frappe.db.commit() + + default = frappe.get_doc("Global Defaults") + self.old_default_company = default.default_company + default.default_company = "Woocommerce" + default.save() + + frappe.db.commit() + + time.sleep(5) + + woo_settings = frappe.get_doc("Woocommerce Settings") + woo_settings.secret = "ec434676aa1de0e502389f515c38f89f653119ab35e9117c7a79e576" + woo_settings.woocommerce_server_url = "https://woocommerce.mntechnique.com/" + woo_settings.api_consumer_key = "ck_fd43ff5756a6abafd95fadb6677100ce95a758a1" + woo_settings.api_consumer_secret = "cs_94360a1ad7bef7fa420a40cf284f7b3e0788454e" + woo_settings.enable_sync = 1 + woo_settings.tax_account = "Sales Expenses - W" + woo_settings.f_n_f_account = "Expenses - W" + woo_settings.save(ignore_permissions=True) + + frappe.db.commit() + + def test_woocommerce_request(self): + r = emulate_request() + self.assertTrue(r.status_code == 200) + self.assertTrue(frappe.get_value("Customer",{"woocommerce_email":"tony@gmail.com"})) + self.assertTrue(frappe.get_value("Item",{"woocommerce_id": 56})) + self.assertTrue(frappe.get_value("Sales Order",{"woocommerce_id":74})) + + # cancel & delete order + cancel_and_delete_order() + + # Emulate Request when Customer, Address, Item data exists + r = emulate_request() + self.assertTrue(r.status_code == 200) + self.assertTrue(frappe.get_value("Sales Order",{"woocommerce_id":74})) + + def tearDown(self): + default = frappe.get_doc("Global Defaults") + default.default_company = self.old_default_company + default.save() + frappe.db.commit() + + + +def emulate_request(): + # Emulate Woocommerce Request + headers = { + "X-Wc-Webhook-Event":"created", + "X-Wc-Webhook-Signature":"ckV+JSfmloltGpl/+YllrPXhe8KypukMhdZEMp0ChJM=" + } + # Emulate Request Data + data = """{"id":74,"parent_id":0,"number":"74","order_key":"wc_order_5aa1281c2dacb","created_via":"checkout","version":"3.3.3","status":"processing","currency":"INR","date_created":"2018-03-08T12:10:04","date_created_gmt":"2018-03-08T12:10:04","date_modified":"2018-03-08T12:10:04","date_modified_gmt":"2018-03-08T12:10:04","discount_total":"0.00","discount_tax":"0.00","shipping_total":"150.00","shipping_tax":"0.00","cart_tax":"0.00","total":"649.00","total_tax":"0.00","prices_include_tax":false,"customer_id":12,"customer_ip_address":"103.54.99.5","customer_user_agent":"mozilla\\/5.0 (x11; linux x86_64) applewebkit\\/537.36 (khtml, like gecko) chrome\\/64.0.3282.186 safari\\/537.36","customer_note":"","billing":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN","email":"tony@gmail.com","phone":"123457890"},"shipping":{"first_name":"Tony","last_name":"Stark","company":"","address_1":"Mumbai","address_2":"","city":"Dadar","state":"MH","postcode":"123","country":"IN"},"payment_method":"cod","payment_method_title":"Cash on delivery","transaction_id":"","date_paid":null,"date_paid_gmt":null,"date_completed":null,"date_completed_gmt":null,"cart_hash":"8e76b020d5790066496f244860c4703f","meta_data":[],"line_items":[{"id":80,"name":"Marvel","product_id":56,"variation_id":0,"quantity":1,"tax_class":"","subtotal":"499.00","subtotal_tax":"0.00","total":"499.00","total_tax":"0.00","taxes":[],"meta_data":[],"sku":"","price":499}],"tax_lines":[],"shipping_lines":[{"id":81,"method_title":"Flat rate","method_id":"flat_rate:1","total":"150.00","total_tax":"0.00","taxes":[],"meta_data":[{"id":623,"key":"Items","value":"Marvel × 1"}]}],"fee_lines":[],"coupon_lines":[],"refunds":[]}""" + + # Build URL + port = frappe.get_site_config().webserver_port or '8000' + + if os.environ.get('CI'): + host = 'localhost' + else: + host = frappe.local.site + + url = "http://{site}:{port}/api/method/erpnext.erpnext_integrations.connectors.woocommerce_connection.order".format(site=host, port=port) + + r = requests.post(url=url, headers=headers, data=data) + + time.sleep(2) + return r + +def cancel_and_delete_order(): + # cancel & delete order + try: + so = frappe.get_doc("Sales Order",{"woocommerce_id":74}) + if isinstance(so, erpnext.selling.doctype.sales_order.sales_order.SalesOrder): + so.cancel() + so.delete() + frappe.db.commit() + except frappe.DoesNotExistError: + pass \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index c5641f564d..13055ac2ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ googlemaps python-stdnum braintree gocardless_pro +woocommerce