def update_shipping_rate(address, awc_session): awc = awc_session.get("cart") shipping_rate_api = frappe.get_hooks("shipping_rate_api")[0] address_link = frappe.get_value("AWC Settings", "AWC Settings", "shipping_address") from_address = frappe.get_doc("Address", address_link) package_items=[] for item in awc["items"]: if not item.get("options", {}).get("subgroup"): package_item = { "item_code": item.get("sku"), "qty": item.get("qty") } package_items.append(package_item) try: rates = frappe.call(shipping_rate_api["module"], from_address=from_address, to_address=address, items=package_items) if rates: # cache quoted rates to reference later on checkout awc_session["shipping_address"] = address awc_session["shipping_rates"] = { rate.get("name"): rate for rate in rates } awc_session["shipping_rates_list"] = rates else: rates = [] except Exception as ex: log(traceback.format_exc()) return [] return rates
def create_request(self, data): # simulates a transaction by just submitting a quotation # if there is enough credit available there should be no issues self.process_data = frappe._dict(data) self.billing_info = self.process_data.get("billing_info") self.shipping_info = self.process_data.get("shipping_info") redirect_url = "" redirect_to = data.get("notes", {}).get("redirect_to") or None redirect_message = data.get("notes", {}).get("redirect_message") or None status = "Completed" if not self.process_data.get("unittest"): self.integration_request = create_request_log(self.process_data, "Host", self.service_name) self.integration_request.status = status self.integration_request.save() custom_redirect_to = None try: if not self.process_data.get("unittest"): ref_doc = frappe.get_doc( self.process_data.reference_doctype, self.process_data.reference_docname) ref_doc.flags["skip_payment_request"] = 1 custom_redirect_to = ref_doc.run_method("on_payment_authorized", status) except Exception as ex: log(frappe.get_traceback()) raise ex if custom_redirect_to: redirect_to = custom_redirect_to redirect_url = "/integrations/credit_success" redirect_message = "Continue Shopping" success = True params = [] if redirect_to: params.append(urllib.urlencode({"redirect_to": redirect_to})) if redirect_message: params.append(urllib.urlencode({"redirect_message": redirect_message})) if len(params) > 0: redirect_url += "?" + "&".join(params) self.process_data = {} self.billing_info = {} self.shipping_info = {} return { "redirect_to": redirect_url, "error": redirect_message if status == "Failed" else None, "status": status }
def get_shipping_rate(address): try: address = json.loads(address) except Exception as ex: log(ex) return [] awc_session = get_awc_session() result = update_shipping_rate(address, awc_session) set_awc_session(awc_session) return result
def update_shipping_rate(address, awc_session, is_pickup=False): validation_hooks = frappe.get_hooks("awc_address_validation") or [] quotation = get_user_quotation(awc_session).get('doc') if quotation: for fn in validation_hooks: frappe.call(fn, doc=quotation, address=address) if address: address_link = frappe.get_value("AWC Settings", "AWC Settings", "shipping_address") from_address = frappe.get_doc("Address", address_link) package_items = [] awc = awc_session.get("cart") if not is_pickup: for item in awc["items"]: if not item.get("options", {}).get("subgroup"): package_item = { "item_code": item.get("sku"), "qty": item.get("qty") } package_items.append(package_item) shipping_rate_api = frappe.get_hooks("shipping_rate_api")[0] try: if address and not is_pickup: rates = frappe.call(shipping_rate_api["module"], from_address=from_address, to_address=address, items=package_items) else: rates = [] if rates or is_pickup: rates.append({u'fee': 0, u'name': u'PICK UP', u'label': u'FLORIDA HQ PICK UP'}) else: rates = [] except Exception as ex: log(traceback.format_exc()) rates = [] if address: awc_session["last_shipping_address"] = address awc_session["shipping_address"] = address elif "shipping_address" in awc_session: del awc_session["shipping_address"] awc_session["shipping_rates"] = { rate.get("name"): rate for rate in rates } awc_session["shipping_rates_list"] = rates return rates
def call_hook(hook_name, **kwargs): hooks = frappe.get_hooks(hook_name) or [] for hook in hooks: # don't allow hooks to break processing try: frappe.call(hook, **kwargs) except Exception: # Hook inception, pass exception to hook listening for exception reporting(sentry) error_hooks = frappe.get_hooks("error_capture_log") or [] if len(error_hooks) > 0: for error_hook in error_hooks: frappe.call(error_hook, async=True) else: log("Error calling hook method: {}->{}".format(hook_name, hook)) log(frappe.get_traceback())
def save_and_commit_quotation(quotation, is_dirty, awc_session, commit=False, save_session=True): result=(False, None) if quotation and is_dirty: try: update_cart_settings(quotation, awc_session) quotation.flags.ignore_permissions = True quotation.save() collect_totals(quotation, None, awc_session) result = (True, None) except Exception as ex: log(traceback.format_exc()) result = (False, ex) else: collect_totals(None, None, awc_session) if save_session: set_awc_session(awc_session) if commit: frappe.db.commit() return result
def on_payment_authorized(self, payment_status): try: quotation = frappe.get_doc("Quotation", self.order_id) # check if we have a billing address linked if self.get('billing_address'): quotation.customer_address = self.billing_address else: # else create one from transaction data quotation.customer_address = create_address( parent_dt="Customer", parent=quotation.customer, address_1=self.get("billing_address_1"), address_2=self.get("billing_address_2"), city=self.get("billing_city"), state=self.get("billing_state"), pincode=self.get("billing_pincode"), country=self.get("billing_country"), email=self.get("payer_email"), address_type="Billing", phone=self.get("billing_phone"), title=self.get("billing_title"), return_name=1, flags={"ignore_permissions": 1}) # check if we have a shipping address linked quotation.shipping_address_name = self.shipping_address # assign formatted address text if not quotation.shipping_address_name: quotation.shipping_address_name = frappe.get_value( "AWC Settings", "AWC Settings", "shipping_address") quotation.address_display = get_address_display( frappe.get_doc("Address", quotation.customer_address).as_dict()) quotation.shipping_address = get_address_display( frappe.get_doc("Address", quotation.shipping_address_name).as_dict()) quotation.flags.ignore_permissions = 1 quotation.save() # create sales order so = convert_quotation_to_sales_order(quotation) if self.flags.get("skip_payment_request", False): so.submit() # NOTE: TEST THIS!!!! WHY IS THERE A SAVE AFTER SUBMIT??????? #so.save() self.reference_doctype = "Sales Order" self.reference_docname = so.name else: # then immediately create payment request # no emails should be sent as this is intended for immediate fullfilment req_type = frappe.local.response.get("type", None) req_location = frappe.local.response.get("location", None) preq = payment_request.make_payment_request(dt="Sales Order", dn=so.name, submit_doc=1, return_doc=1, mute_email=1) ############################################################# # DIRTY FIX: payment request codebase redirects # shopping cart payment requests. Here we are undoing that. if req_type: frappe.local.response["type"] = req_type elif frappe.local.response.get("type"): frappe.local.response.pop("type") if req_location: frappe.local.response["location"] = req_location elif frappe.local.response.get("location"): frappe.local.response.pop("location") ############################################################# preq.flags.ignore_permissions = 1 # preq.insert() log(preq) if preq.docstatus != 1: preq.submit() # update transaction record to track payment request record self.reference_doctype = "Payment Request" self.reference_docname = preq.name self.order_id = so.name self.flags.ignore_permissions = 1 self.save() if not self.flags.get("skip_payment_request", False): # finally let payment request run its own code to finalize transaction # invoice, payment entry docs should be created here result = preq.run_method("on_payment_authorized", payment_status) else: result = None # update shipping method in Sales Order #if self.get("shipping_method"): # frappe.db.set_value("Sales Order", self.order_id, "fedex_shipping_method", self.get("shipping_method")) if self.get("gateway_service"): has_universals = False for item in frappe.get_doc("Sales Order", self.order_id).items: if frappe.db.get_value("Item", item.item_code, "item_group") == "Universal": has_universals = True if self.get("gateway_service") == "credit_gateway": frappe.db.set_value("Sales Order", self.order_id, "payment_method", "Bill Me") elif self.get("gateway_service") == "paypal": frappe.db.set_value("Sales Order", self.order_id, "payment_method", "PayPal") frappe.db.set_value("Sales Order", self.order_id, "authorize_production", False) frappe.db.set_value("Sales Order", self.order_id, "authorize_delivery", False) else: if self.get("gateway_service") == "authorizenet": frappe.db.set_value("Sales Order", self.order_id, "payment_method", "Card") else: frappe.db.set_value("Sales Order", self.order_id, "payment_method", self.get("gateway_service")) if not has_universals: frappe.db.set_value("Sales Order", self.order_id, "authorize_production", True) frappe.db.set_value("Sales Order", self.order_id, "authorize_delivery", True) # override redirection to orders page if result: result = '/iems#filter=custom' # this is here to remove duplication warning messages. # TODO: Consider bring this to erpnext team to remove warning if frappe.local.message_log: for msg in frappe.local.message_log: self.log_action(msg, "Info") frappe.local.message_log = [] # don't kill processing if saving cleaning session address info breaks try: # clears awc session data awc.clear_awc_session() except Exception as awc_ex: log(frappe.get_traceback()) self.log_action(frappe.get_traceback(), "Error") pass return result except Exception as ex: log(frappe.get_traceback()) self.log_action(frappe.get_traceback(), "Error") raise ex
def on_payment_authorized(self, payment_status): try: # dumb loop to catch out of sync quotation. # loop at least 3 times for timestamp error tries = 3 while tries > 0: try: quotation = self.update_quotation() tries = 0 # success, exit loop except Exception as ex: tries = tries - 1 if tries <= 0: raise ex else: msg = """ -------------------------------------------------------------- Caught error while trying to save quotation in awc transaction Attempting to save again... {} - RESPONSE -------------- {}""".format(ex, frappe.local.respone) log(msg, trace=1) call_hook("awc_transaction_on_payment_authorized", transaction=self, payment_status=payment_status) # create sales order so = convert_quotation_to_sales_order(quotation) if self.flags.get("skip_payment_request", False): so.submit() self.reference_doctype = "Sales Order" self.reference_docname = so.name else: # then immediately create payment request # no emails should be sent as this is intended for immediate fullfilment req_type = frappe.local.response.get("type", None) req_location = frappe.local.response.get("location", None) preq = payment_request.make_payment_request(dt="Sales Order", dn=so.name, submit_doc=1, return_doc=1, mute_email=1) ############################################################# # DIRTY FIX: payment request codebase redirects # shopping cart payment requests. Here we are undoing that. if req_type: frappe.local.response["type"] = req_type elif frappe.local.response.get("type"): frappe.local.response.pop("type") if req_location: frappe.local.response["location"] = req_location elif frappe.local.response.get("location"): frappe.local.response.pop("location") ############################################################# preq.flags.ignore_permissions = 1 if preq.docstatus != 1: preq.submit() # update transaction record to track payment request record self.reference_doctype = "Payment Request" self.reference_docname = preq.name self.order_id = so.name self.flags.ignore_permissions = 1 self.save() if not self.flags.get("skip_payment_request", False): # finally let payment request run its own code to finalize transaction # invoice, payment entry docs should be created here result = preq.run_method("on_payment_authorized", payment_status) else: result = None # override redirection to orders page if result: result = '/iems#filter=custom' # let other apps do work after order is generated call_hook("awc_transaction_after_on_sales_order_created", transaction=self, order=so, pr_result=result) # remove redirect alert on cart generate so's if frappe.local.message_log: frappe.local.message_log = [] # don't kill processing if saving cleaning session address info breaks try: # clears awc session data awc.clear_awc_session() except Exception as awc_ex: log(frappe.get_traceback()) pass return result except Exception as ex: log(frappe.get_traceback()) raise ex
def process_payment(self): # used for feedback about which payment was used authorizenet_data = {} # the current logged in contact contact = get_contact() # get authorizenet user if available authnet_user = get_authorizenet_user() # the cc data available data = self.process_data # get auth keys settings = self.get_settings() # fetch redirect info redirect_to = data.get("notes", {}).get("redirect_to") or None redirect_message = data.get("notes", {}).get("redirect_message") or None # uses dummy request doc for unittests as we are only testing processing if not data.get("unittest"): if data.get("name"): request = frappe.get_doc("AuthorizeNet Request", data.get("name")) else: # Create request from scratch when embeding form on the fly # # This allows payment processing without having to pre-create # a request first. # # This path expects all the payment request information to be # available!! # # keys expected: ('amount', 'currency', 'order_id', 'title', \ # 'description', 'payer_email', 'payer_name', \ # 'reference_docname', 'reference_doctype') request = self.build_authorizenet_request(**{ \ key: data[key] for key in \ ('amount', 'currency', 'order_id', 'title', \ 'description', 'payer_email', 'payer_name', \ 'reference_docname', 'reference_doctype') }) data["name"] = request.get("name") else: request = frappe.get_doc({"doctype": "AuthorizeNet Request"}) request.flags.ignore_permissions = 1 # set the max log level as per settings request.max_log_level(self.log_level) try: if self.card_info: # ensure card fields exist required_card_fields = ['name_on_card', 'card_number', 'exp_month', 'exp_year', 'card_code'] for f in required_card_fields: if not self.card_info.get(f): request.status = "Error" return { request, None, "Missing field: %s" % f, {} } # prepare authorize api authorize.Configuration.configure( authorize.Environment.TEST if self.use_sandbox else authorize.Environment.PRODUCTION, settings.api_login_id, settings.api_transaction_key ) # cache billing fields as per authorize api requirements billing = authnet_address(self.billing_info) if self.shipping_info: shipping = authnet_address(self.shipping_info) else: shipping = None # attempt to find valid email address email = self.process_data.get("payer_email") if email: email = email.split(',')[0] if "@" not in email and contact: email = contact.get("email_id") if "@" not in email: if contact and contact.user: email = frappe.get_value("User", contact.user, "email_id") if "@" not in email: log("AUTHNET FAILURE! Bad email: {0}".format(email)) log(pretty_json(self.process_data)) raise ValueError("There are no valid emails associated with this customer") # build transaction data transaction_data = { "order": { "invoice_number": data["order_id"] }, "amount": flt(self.process_data.get("amount")), "email": email, "description": self.card_info.get("name_on_card"), "customer_type": "individual" } # track ip for tranasction records if frappe.local.request_ip: transaction_data.update({ "extra_options": { "customer_ip": frappe.local.request_ip } }) # get authorizenet profile informatio for stored payments authorizenet_profile = self.process_data.get("authorizenet_profile"); # use card # see: https://vcatalano.github.io/py-authorize/transaction.html if self.card_info != None: # exp formating for sale/auth api expiration_date = "{0}/{1}".format( self.card_info.get("exp_month"), self.card_info.get("exp_year")) transaction_data.update({ "credit_card": { "card_number": self.card_info.get("card_number"), "expiration_date": expiration_date, "card_code": self.card_info.get("card_code") } }) elif authorizenet_profile: # if the customer_id isn't provided, then fetch from authnetuser if not authorizenet_profile.get("customer_id"): authorizenet_profile["customer_id"] = authnet_user.get("authorizenet_id") # or stored payment transaction_data.update({ "customer_id": authorizenet_profile.get("customer_id"), "payment_id": authorizenet_profile.get("payment_id") }) # track transaction payment profile ids to return later authorizenet_data.update({ "customer_id": authorizenet_profile.get("customer_id"), "payment_id": authorizenet_profile.get("payment_id") }) else: raise "Missing Credit Card Information" name_parts = self.card_info["name_on_card"].split(' ') first_name = name_parts[0] last_name = " ".join(name_parts[1:]) # add billing information if available if len(billing.keys()): transaction_data["billing"] = billing transaction_data["billing"]["first_name"] = first_name transaction_data["billing"]["last_name"] = last_name if shipping and len(shipping.keys()): transaction_data["shipping"] = billing transaction_data["shipping"]["first_name"] = first_name transaction_data["shipping"]["last_name"] = last_name # include line items if available if self.process_data.get("line_items"): transaction_data["line_items"] = self.process_data.get("line_items") request.log_action("Requesting Transaction: %s" % \ json.dumps(transaction_data), "Debug") # performt transaction finally result = authorize.Transaction.sale(transaction_data) request.log_action(json.dumps(result), "Debug") # if all went well, record transaction id request.transaction_id = result.transaction_response.trans_id request.status = "Captured" request.flags.ignore_permissions = 1 except AuthorizeInvalidError as iex: # log validation errors request.log_action(frappe.get_traceback(), "Error") request.status = "Error" error_msg = "" errors = [] if iex.children and len(iex.children) > 0: for field_error in iex.children: print(field_error.asdict()) for field_name, error in field_error.asdict().iteritems(): errors.append(error) error_msg = "\n".join(errors) request.error_msg = error_msg except AuthorizeResponseError as ex: # log authorizenet server response errors result = ex.full_response request.log_action(json.dumps(result), "Debug") request.log_action(str(ex), "Error") request.status = "Error" request.error_msg = ex.text redirect_message = str(ex) if result and hasattr(result, 'transaction_response'): # if there is extra transaction data, log it errors = result.transaction_response.errors request.log_action("\n".join([err.error_text for err in errors]), "Error") request.log_action(frappe.get_traceback(), "Error") request.transaction_id = result.transaction_response.trans_id redirect_message = "Success" pass except Exception as ex: # any other errors request.log_action(frappe.get_traceback(), "Error") request.status = "Error" request.error_msg = "[UNEXPECTED ERROR]: {0}".format(ex) pass # now check if we should store payment information on success if request.status in ("Captured", "Authorized") and \ self.card_info and \ self.card_info.get("store_payment") and \ contact: try: # create customer if authnet_user doesn't exist if not authnet_user: request.log_action("Creating AUTHNET customer", "Info") customer_result = authorize.Customer.from_transaction(request.transaction_id) request.log_action("Success", "Debug") authnet_user = frappe.get_doc({ "doctype": "AuthorizeNet Users", "authorizenet_id": customer_result.customer_id, "contact": contact.name }) card_store_info = { "card_number": self.card_info.get("card_number"), "expiration_month": self.card_info.get("exp_month"), "expiration_year": self.card_info.get("exp_year"), "card_code": self.card_info.get("card_code"), "billing": self.billing_info } request.log_action("Storing Payment Information With AUTHNET", "Info") request.log_action(json.dumps(card_store_info), "Debug") try: card_result = authorize.CreditCard.create( authnet_user.get("authorizenet_id"), card_store_info) except AuthorizeResponseError as ex: card_result = ex.full_response request.log_action(json.dumps(card_result), "Debug") request.log_action(str(ex), "Error") try: # duplicate payment profile if card_result["messages"][0]["message"]["code"] == "E00039": request.log_action("Duplicate payment profile, ignore", "Error") else: raise ex except: raise ex request.log_action("Success: %s" % card_result.payment_id, "Debug") address_short = "{0}, {1} {2}".format( billing.get("city"), billing.get("state"), billing.get("pincode")) card_label = "{0}{1}".format( get_card_accronym(self.card_info.get("card_number")), self.card_info.get("card_number")[-4:]) authnet_user.flags.ignore_permissions = 1 authnet_user.append("stored_payments", { "doctype": "AuthorizeNet Stored Payment", "short_text": "%s %s" % (card_label, address_short), "long_text": "{0}\n{1}\n{2}, {3} {4}\n{5}".format( card_label, billing.get("address", ""), billing.get("city", ""), billing.get("state", ""), billing.get("pincode", ""), frappe.get_value("Country", filters={"name": self.billing_info.get("country")}, fieldname="country_name") ), "address_1": self.billing_info.get("address_1"), "address_2": self.billing_info.get("address_2"), "expires": "{0}-{1}-01".format( self.card_info.get("exp_year"), self.card_info.get("exp_month")), "city": self.billing_info.get("city"), "state": self.billing_info.get("state"), "postal_code": self.billing_info.get("pincode"), "country": frappe.get_value("Country", self.billing_info.get("country"), fieldname="code"), "payment_type": "Card", "authorizenet_payment_id": card_result.payment_id }) authorizenet_data.update({ "customer_id": authnet_user.get("authorizenet_id"), "payment_id": card_result.payment_id }) if not data.get("unittest"): authnet_user.save() request.log_action("Stored in DB", "Debug") except Exception as exx: # any other errors request.log_action(frappe.get_traceback(), "Error") raise exx return request, redirect_to, redirect_message, authorizenet_data
def sync_awc_and_quotation(awc_session, quotation, quotation_is_dirty=False, save_quotation=False): # convert quotation to awc object # and merge items in the case where items are added before logging in. # steps: # 1) loop over all awc items and update quotation items matching names/ids # 2) remove invalid awc items who's skus do not match any products(awc items) # 3) loop over remaining unmatched quotation items and create awc items # 4) remove invalid quotation items who's skus do not match any products(awc items) awc = awc_session.get("cart") # fixes issue where new quotation items require a parent to be inserted # and we require a quotation item name to reference in awc if quotation and quotation.name == None: save_and_commit_quotation(quotation, True, awc_session, commit=False, save_session=False) if not awc: # abnormal, there should be a cart instance on the session log(awc_session, trace=1) awc_is_dirty = False awc_items_to_remove = [] awc_items_matched = [] # step 1 # iterate over all awc items and update quotation to match values awc_items = awc.get("items", []) for awc_idx in range(0, len(awc_items)): awc_item = awc_items[awc_idx] product = get_product_by_sku(awc_item.get("sku"), quotation=quotation) if awc_item.get("id"): idx = find_index(quotation.get("items", []), lambda itm: itm.get("name") == awc_item.get("id")) if idx > -1: item = quotation.items[idx] # make sure product exists if product.get("success"): product = product.get("data") if item.qty != awc_item.get("qty"): item.qty = awc_item.get("qty") quotation_is_dirty = True if item.item_code != awc_item.get("sku"): item.item_code = awc_item.get("sku") quotation_is_dirty = True if item.awc_group != awc_item.get('options', {}).get('group'): item.awc_group = awc_item.get('options', {}).get('group') quotation_is_dirty = True if item.awc_subgroup != awc_item.get('options', {}).get('subgroup'): item.awc_subgroup = awc_item.get('options', {}).get('subgroup') quotation_is_dirty = True if item.awc_group_label != awc_item.get('options', {}).get('label'): item.awc_group_label = awc_item.get('options', {}).get('label') quotation_is_dirty = True if item.description != awc_item.get("options", {}).get("description", item.description): item.description = awc_item.get("options", {}).get("description") quotation_is_dirty = True if not item.image and awc_item.get("options", {}).get("image") and item.image != awc_item.get("options", {}).get("image"): item.image = awc_item["options"]["image"] quotation_is_dirty = True item.warehouse = product.get("warehouse") update_quotation_item_awc_fields(item, awc_item) if awc_item.get("options", {}).get("custom", {}).get("rate", None) != None: item.set("ignore_pricing_rule", 1) set_quotation_item_rate(item, awc_item["options"]["custom"]["rate"], product) else: set_quotation_item_rate(item, product.get("price"), product) item.set("ignore_pricing_rule", 0) awc_items_matched.append(awc_item.get("id")) else: # sku is invalid. Flag item to be removed from awc session awc_items_to_remove.append(awc_item) elif awc_item.get('id')[0:4] == "QUOD": # remove orphaned items awc_items_to_remove.append(awc_item) else: if product.get("success"): product = product.get("data") # no quotation item matched, so lets create one item_data = { "doctype": "Quotation Item", "item_code": awc_item.get("sku"), "item_name": product.get("name"), "description": awc_item.get("options", {}).get("description", product.get("name")), "qty": cint(awc_item.get("qty")), "warehouse": product.get("warehouse") } if awc_item.get("options", {}).get("image"): item_data["image"] = awc_item["options"]["image"] update_quotation_item_awc_fields(item_data, awc_item) new_quotation_item = quotation.append("items", item_data) if awc_item.get("options", {}).get("custom", {}).get("rate", None) != None: new_quotation_item.set("ignore_pricing_rule", 1) set_quotation_item_rate(new_quotation_item, awc_item["options"]["custom"]["rate"], product) else: new_quotation_item.set("ignore_pricing_rule", 0) set_quotation_item_rate(new_quotation_item, product.get("price"), product) awc_item["unit"] = new_quotation_item.rate awc_item["total"] = new_quotation_item.amount # BUGFIX: makes sure this item gets a name during login new_quotation_item.parent = quotation.name new_quotation_item.parenttype = "Quotation" new_quotation_item.parentfield = "items" new_quotation_item.save() awc_item["id"] = new_quotation_item.name # make sure we won't add this new item again on step 2 update_quotation_item_awc_fields(new_quotation_item, awc_item) awc_items_matched.append(awc_item.get("id")) quotation_is_dirty = True # update quotation records awc_is_dirty = True # flag awc session for storage else: awc_items_to_remove.append(awc_item) else: # drop awc items if they have invalid ids awc_items_to_remove.append(awc_item) # step 2 # remove invalid awc items for awc_item in awc_items_to_remove: idx = awc["items"].index(awc_item) del awc["items"][idx] awc_is_dirty = True quotation_item_to_remove = [] # step 3 # now create awc items for quotation items not matched with existing awc session for item in [qitem for qitem in quotation.get("items", []) \ if qitem.name not in awc_items_matched]: product = get_product_by_sku(item.get("item_code"), quotation=quotation) if product.get("success"): product = product.get("data") awc_item = { "id": item.name, "sku": item.item_code, "qty": cint(item.qty), "warehouse": product.get("warehouse"), "unit": item.rate, "total": item.amount, "image": item.image, "base_price": product.get("base_price"), "options": { "description": item.description } } if item.awc_group: awc_item["options"].update({ "group": item.awc_group, "subgroup": item.awc_subgroup, "label": item.awc_group_label, "image": item.image }) if awc_item["base_price"] != awc_item["unit"]: awc_item["options"]["custom"] = { "rate": item.rate } awc["items"].append(awc_item) awc_is_dirty = True else: quotation_item_to_remove.append(item) # step 4 # remove invalid quotation items for item in quotation_item_to_remove: idx = quotation.items.index(item) del quotation.items[idx] quotation_is_dirty = True call_awc_sync_hook(awc_session, quotation) if quotation_is_dirty: update_cart_settings(quotation, awc_session) quotation.flags.ignore_permissions = True if save_quotation: try: quotation.save() frappe.db.commit() except Exception as ex: log(traceback.format_exc()) collect_totals(quotation, awc, awc_session) if awc_is_dirty: set_awc_session(awc_session) return quotation_is_dirty
def fetch_products(tags="", terms="", order_by="order_weight", order_dir="asc", start=0, limit=None): """Fetches a list of products filtered by tags""" awc_session = get_awc_session() cache_key = "fetch_products-{}-{}-{}-{}-{}-{}-{}".format(tags, terms, order_by, order_dir, start, limit, is_logged_in()) cache_data = get_cache(cache_key, session=awc_session) if cache_data: return cache_data payload = { "success": False } try: price_list = None if is_logged_in(): quotation = _get_cart_quotation() price_list = quotation.get("selling_price_list") tags = tags.split(',') # split tag string into list # Convert order_by and order_dir values to acceptable values or defaults order_by_clean = dict(weight="order_weight").get(order_by if order_by else "", "order_weight") order_dir_clean = dict(asc="asc", desc="desc").get(order_dir if order_dir else "", "asc") # builds the WHERE part of the sql query to match tags by AND/OR binary matches # matches are grouped into groups of AND matches with extra groups being OR # example: # ( tagmatch AND tagmatch AND tagmatch ) OR ( tagmatch AND tagmatch AND tagmatch ) tags_match = [] tag_group = [] for tag in tags: if tag: # anything prepended with a pipe is an OR match if tag[0] == '|': if len(tag_group) > 0: tags_match.append(tag_group) tag_group = [] tag_group.append(' a.tags REGEXP "(^|,){}(,|$)" '.format(tag[1:])) else: # anything else is an AND match tag_group.append(' a.tags REGEXP "(^|,){}(,|$)" '.format(tag)) # add any dangly groups to match list if len(tag_group) > 0: tags_match.append(tag_group) # build actual WHERE query part from groups if len(tags_match) > 0: tags_match = " OR ".join( \ ["({})".format(" AND ".join(group)) \ for group in tags_match] ) if tags_match: tags_match = "({})".format(tags_match) else: tags_match = "" sql_count = "SELECT count(*) \ FROM `tabAWC Item` a\ {};\ ".format("WHERE %s" % tags_match if tags_match else "") result_count = cint(frappe.db.sql(sql_count, as_list=1)[0][0]) sql = """SELECT i.name, i.item_code, i.item_name, i.has_variants, i.standard_rate, i.net_weight, a.name as awc_item_name, a.product_route as awc_product_route, a.description_short as awc_description_short, a.description_long as awc_description_long, a.listing_widget as awc_listing_widget, a.product_widget as awc_product_widget, a.product_template as awc_product_template, a.product_thumbnail as awc_product_thumbnail, a.slider as awc_slider, a.tags as awc_tags FROM `tabAWC Item` a, `tabItem` i WHERE i.name = a.product_name AND a.catalog_visible = 1 and i.disabled != 1 {} ORDER BY {} {} {}""".format( "AND %s" % tags_match if tags_match else "", order_by_clean, order_dir_clean, "LIMIT {}, {}".format( int(start), int(limit)) if limit != None else "" ) result = frappe.db.sql(sql, as_dict=1) products = [] for item in result: price_info = get_price(item.get("item_code"), price_list) price = price_info.get("rate") variants = frappe.get_all("Item", fields=["name", "item_code"], filters={"variant_of": item.get("name"), "disabled": 0}) for vitem in variants: vprice = get_price(vitem.get("item_code"), price_list).get("rate") if vprice < price: price = vprice product = dict( sku=item.item_code, name=item.item_name, weight=item.get("net_weight", 0), custom=get_awc_item_custom_data(item.awc_item_name), productUrl="/p/%s" % item.awc_product_route, description=item.awc_description_short, imageUrl=item.awc_product_thumbnail, base_price=price_info.get("base_price_rate") , price=price, listing_widget=item.awc_listing_widget, product_widget=item.awc_product_widget, product_template=item.awc_product_template, options=build_awc_options_from_varients(item), tags=item.awc_tags.split(',') if item.awc_tags else [] ) products.append(product) payload["success"] = True payload["total_records"] = result_count payload["data"] = products except Exception as ex: log("ERROR") payload["success"] = False payload["message"] = traceback.format_exc(ex) log(payload["message"]) set_cache(cache_key, payload, session=awc_session) return payload
def cart(data=None, action=None): if data and isinstance(data, basestring): try: data = json.loads(data) except ex: log("REMOTE ADDR: {0}".format(frappe.request.get("remote_addr", "NO REMOTE ADDRESS?"))) log("URL: {0}".format(frappe.request.get("url", "NO URL DATA"))) log("Action: {0}".format(action)) log("Data: {0}".format(data)) log(traceback.format_exc()) data = None # make sure we can handle bulk actions if not isinstance(data, list): data = [data] customer = get_current_customer() quotation = None awc_session = get_awc_session() quotation_is_dirty = False if customer: cart_info = get_user_quotation(awc_session) quotation = cart_info.get('doc') if len(quotation.items) == 0: apply_cart_settings(quotation=quotation) awc = awc_session.get("cart") if not awc: awc = clear_awc_session() if quotation: quotation_is_dirty = sync_awc_and_quotation(awc_session, quotation) else: call_awc_sync_hook(awc_session, quotation) if not action: save_and_commit_quotation(quotation, quotation_is_dirty, awc_session, commit=True) return { "data": awc, "success": True} elif action == "calculate_shipping": rate_name = data[0].get("name") address = data[0].get("address") if address: address_name = address.get("shipping_address") if not address_name: new_address = frappe.new_doc("Address") new_address.update({ "address_title": data[0].get("address").get("title"), "address_type": data[0].get("address").get("address_type", "Shipping"), "address_contact": data[0].get("address").get("address_contact", ""), "address_line1": data[0].get("address").get("address_1"), "address_line2": data[0].get("address").get("address_2"), "city": data[0].get("address").get("city"), "state": data[0].get("address").get("state"), "country": data[0].get("address").get("country"), "phone": data[0].get("address").get("phone"), "email_id": frappe.session.user, "pincode": data[0].get("address").get("pincode"), "links": [{"link_doctype" : "Customer", "link_name" : quotation.customer}] }) new_address.flags.ignore_permissions= True new_address.save() address_name = new_address.name address["shipping_address"] = address_name if quotation: quotation.shipping_address_name = address_name awc_session["last_shipping_address"] = address_name # check and update use_customer_fedex_account field in quotation if quotation: quotation.use_customer_fedex_account = 1 if data[0].get( "address", {}).get("use_customer_fedex_account") else 0 quotation.flags.ignore_permissions = True quotation.save() frappe.db.commit() result = calculate_shipping(rate_name, address, awc_session, quotation, save=True) return result elif action == "updateItem": # for now only qty field is updatable # key in awc fields and values are erpnext fields for quotation item valid_update_fields = {"qty": "qty"} remove_items = [] removed_ids = [] for item in data: awc_item = next((i for i in awc["items"] if i.get("id") == item.get("id")), None) if awc_item: quotation_item = None if quotation: quotation_item = next((q for q in quotation.get("items", []) if q.name == awc_item.get("id")), None) for awc_key, erp_key in valid_update_fields.iteritems(): if awc_key in item: awc_item[awc_key] = item.get(awc_key) if awc_key == "qty": awc_item["total"] = awc_item["unit"] * awc_item["qty"] if quotation_item: quotation_item.set(erp_key, item.get(awc_key)) #if erp_key == "qty": #quotation_item.amount = flt(quotation_item.rate) * quotation_item.qty if awc_item.get('options', {}).get('group'): # find all subgroup items and update qty accordingly for sub_item in [i for i in awc["items"] if i.get('options', {}).get('group') == awc_item.get('options', {}).get('group')]: sub_item["qty"] = awc_item["qty"] if quotation: sub_quotation_item = next((q for q in quotation.get("items", []) if q.name == sub_item.get("id")), None) if sub_quotation_item: sub_quotation_item.set("qty", sub_item.get("qty")) #sub_quotation_item.amount = flt(sub_quotation_item.rate) * sub_quotation_item.qty sub_item["total"] = sub_item["unit"] * sub_item["qty"] if awc_item.get("qty") == 0: remove_items.append(awc_item) # remove all 0 qty items if len(remove_items) > 0: success, removed_ids, awc_items = remove_from_cart(remove_items, awc["items"]) if success: awc["items"] = awc_items if quotation: # remove item and related grouped items from quote quotation_items = [ itm for itm in quotation.get("items", []) \ if itm.name not in removed_ids ] quotation.set("items", quotation_items) shipping_info = calculate_shipping(None, None, awc_session, quotation, save=0, force=True) if quotation: quotation_is_dirty=True save_and_commit_quotation(quotation, quotation_is_dirty, awc_session, commit=True) return session_response({ "success": True, "removed": removed_ids, "shipping_rates": shipping_info.get("shipping_rates") }, awc_session, quotation) elif action == "addToCart": to_remove = [] for item in data: # need basic data validation here if not item.get("sku"): return { "success": False, "message": "Invalid Data" } if not item.get("qty"): return { "success": False, "message": "Invalid Data" } product = get_product_by_sku(item.get("sku"), quotation=quotation).get("data") if item.get("replaces"): to_remove.append(item.get("replaces")) del item["replaces"] if item.get("options", {}).get("custom", {}).get("rate", None) != None: item["total"] = flt(item["options"]["custom"]["rate"]) * item.get("qty") item["unit"] = flt(item["options"]["custom"]["rate"]) else: item["total"] = flt(product.get("price") * item.get("qty")) item["unit"] = flt(product.get("price")) if quotation: item_data = { "doctype": "Quotation Item", "item_code": item.get("sku"), "item_name": product.get("name"), "description": item.get("options", {}).get("description", product.get("name")), "qty": cint(item.get("qty")), "warehouse": product.get("warehouse") } update_quotation_item_awc_fields(item_data, item) quotation_item = quotation.append("items", item_data) # TODO: ( >_<) shitty way of setting rate due to rate reset # Please fix when not utterly pissed off if item.get("options", {}).get("custom", {}).get("rate", None) != None: quotation_item.set("ignore_pricing_rule", 1) set_quotation_item_rate(quotation_item, item["options"]["custom"]["rate"], product) item_data['total'] = item["options"]["custom"]["rate"] * cint(item.get("qty")) else: quotation_item.set("ignore_pricing_rule", 0) set_quotation_item_rate(quotation_item, product.get("price"), product) item_data['total'] = product.get("price") * cint(item.get("qty")) quotation_item.save() item["old_id"] = item["id"] item["id"] = quotation_item.name awc["items"].append(item) removed_ids = [] if len(to_remove) > 0: remove_success, removed_ids, awc_items = remove_from_cart([{ "id": x} for x in to_remove], awc["items"]) if remove_success: awc["items"] = awc_items if quotation: if len(removed_ids) > 0: # remove item and related grouped items from quote quotation_items = [ itm for itm in quotation.get("items", []) \ if itm.name not in removed_ids ] quotation.set("items", quotation_items) update_cart_settings(quotation, awc_session) quotation_is_dirty = True save_and_commit_quotation(quotation, quotation_is_dirty, awc_session, commit=True) return session_response({ "success": True, "removed": removed_ids }, awc_session, quotation) elif action == "removeFromCart": success = False removed_ids = [] success, removed_ids, awc_items = remove_from_cart(data, awc["items"]) if success: awc["items"] = awc_items if quotation: # remove item and related grouped items from quote quotation_items = [ itm for itm in quotation.get("items", []) \ if itm.name not in removed_ids ] quotation.set("items", quotation_items) quotation_is_dirty = True save_and_commit_quotation(quotation, quotation_is_dirty, awc_session, commit=True) return session_response({ "success": True, "removed": removed_ids }, awc_session, quotation) return session_response({ "success": False, "message": "Item not found." }, awc_session, quotation) elif action == "applyCoupon" and len(data) > 0: coupon = data[0] success = False msg = "Coupon not found" if quotation: # validate coupon msg = is_coupon_valid(coupon) is_valid = frappe.response.get("is_coupon_valid") if is_valid: discount, msg = calculate_coupon_discount(quotation.items, coupon)[0:2] if discount == 0: is_valid = False success = False msg = "Coupon is invalid for the current cart." else: quotation.coupon_code = coupon quotation_is_dirty = True success = True else: # must be logged in to use cupon success = False msg = "Please Login to Apply Coupon" if success: save_and_commit_quotation(quotation, quotation_is_dirty, awc_session, commit=True) return session_response({ "success": True }, awc_session, quotation) save_and_commit_quotation(quotation, quotation_is_dirty, awc_session, commit=True) return session_response({ "success": False, "message": _(msg) }, awc_session, quotation) elif action == "removeCoupon": if quotation: quotation.discount_amount = 0 quotation.coupon_code = None quotation_is_dirty = True save_and_commit_quotation(quotation, quotation_is_dirty, awc_session, commit=True) return session_response({ "success": True }, awc_session, quotation) else: return session_response({ "success": False, "message": "Unknown Command." }, awc_session, quotation)
def on_payment_authorized(self, payment_status): try: # dumb loop to catch out of sync quotation. # loop at least 3 times for timestamp error tries = 3 while tries > 0: try: quotation = self.update_quotation() tries = 0 # success, exit loop except Exception as ex: tries = tries - 1 if tries <= 0: raise ex else: msg = """ -------------------------------------------------------------- Caught error while trying to save quotation in awc transaction Attempting to save again... {} - RESPONSE -------------- {}""".format(ex, frappe.local.respone) log(msg, trace=1) # create sales order so = convert_quotation_to_sales_order(quotation) if self.flags.get("skip_payment_request", False): so.submit() self.reference_doctype = "Sales Order" self.reference_docname = so.name else: # then immediately create payment request # no emails should be sent as this is intended for immediate fullfilment req_type = frappe.local.response.get("type", None) req_location = frappe.local.response.get("location", None) preq = payment_request.make_payment_request(dt="Sales Order", dn=so.name, submit_doc=1, return_doc=1, mute_email=1) ############################################################# # DIRTY FIX: payment request codebase redirects # shopping cart payment requests. Here we are undoing that. if req_type: frappe.local.response["type"] = req_type elif frappe.local.response.get("type"): frappe.local.response.pop("type") if req_location: frappe.local.response["location"] = req_location elif frappe.local.response.get("location"): frappe.local.response.pop("location") ############################################################# preq.flags.ignore_permissions = 1 if preq.docstatus != 1: preq.submit() # update transaction record to track payment request record self.reference_doctype = "Payment Request" self.reference_docname = preq.name self.order_id = so.name self.flags.ignore_permissions = 1 self.save() if not self.flags.get("skip_payment_request", False): # finally let payment request run its own code to finalize transaction # invoice, payment entry docs should be created here result = preq.run_method("on_payment_authorized", payment_status) else: result = None if self.get("gateway_service"): has_universals = False for item in frappe.get_doc("Sales Order", self.order_id).items: if frappe.db.get_value("Item", item.item_code, "item_group") == "Universal": has_universals = True if self.get("gateway_service") == "credit_gateway": frappe.db.set_value("Sales Order", self.order_id, "payment_method", "Bill Me") elif self.get("gateway_service") == "paypal": frappe.db.set_value("Sales Order", self.order_id, "payment_method", "PayPal") frappe.db.set_value("Sales Order", self.order_id, "authorize_production", False) frappe.db.set_value("Sales Order", self.order_id, "authorize_delivery", False) else: if self.get("gateway_service") == "authorizenet": frappe.db.set_value("Sales Order", self.order_id, "payment_method", "Card") else: frappe.db.set_value("Sales Order", self.order_id, "payment_method", self.get("gateway_service")) if not has_universals: frappe.db.set_value("Sales Order", self.order_id, "authorize_production", True) frappe.db.set_value("Sales Order", self.order_id, "authorize_delivery", True) # override redirection to orders page if result: result = '/iems#filter=custom' # this is here to remove duplication warning messages. # TODO: Consider bring this to erpnext team to remove warning if frappe.local.message_log: for msg in frappe.local.message_log: log(msg) frappe.local.message_log = [] # don't kill processing if saving cleaning session address info breaks try: # clears awc session data awc.clear_awc_session() except Exception as awc_ex: log(frappe.get_traceback()) pass return result except Exception as ex: log(frappe.get_traceback()) raise ex