weltlink / django-quickbooks

Django Integration with Quickbooks
Mozilla Public License 2.0
37 stars 13 forks source link

Is there an easy way to create an customer/invoice via this project? #38

Open Ryang20718 opened 7 months ago

Ryang20718 commented 7 months ago

I see there's classes such as Customer, Invoice which I presume are created for common use cases.

Say my goal is to programmatically create a customer/invoice, is there a quickstart guide for doing so?

I.e once I'm authed with QB desktop, is there a way to create a customer and send that request to the webconnector to process? either via raw xml or a model?

If yall could post a short snippet on either this README example https://github.com/weltlink/django-quickbooks#implementation

or provide guidance on how I would add this customer to the QBD Task queue, that would be greatly appreciated! Trying to understand what the django_quickbooks.services is used for....

from django_quickbooks.objects import BillAddress, ShipAddress, Customer
from django_quickbooks.services.customer import CustomerService
from lxml import etree

customer_xml = """<?xml version="1.0"?><?qbxml version="13.0"?>
<QBXML>
    <QBXMLMsgsRq onError="stopOnError">
        <CustomerAddRq>
            <CustomerAdd>
                <Name>Amazon</Name>
                <FullName>Amazon</FullName>
                <IsActive>true</IsActive>
                <CompanyName>Amazon</CompanyName>
                <BillAddress>
                    <Addr1>2305 Litton Ln</Addr1>
                    <City>Hebron</City>
                    <State>Kentucky</State>
                    <PostalCode>41048</PostalCode>
                    <Country>United States</Country>
                    <Note>Nice address</Note>
                </BillAddress>
                <ShipAddress>
                    <Addr1>2305 Litton Ln</Addr1>
                    <City>Hebron</City>
                    <State>Kentucky</State>
                    <PostalCode>41048</PostalCode>
                    <Country>United States</Country>
                    <Note>Nice address</Note>
                </ShipAddress>
                <Phone>998909090909</Phone>
                <AltPhone>998909090910</AltPhone>
                <Fax>998909090911</Fax>
                <Email>info@amazon.com</Email>
                <Contact>Someone from Amazon</Contact>
                <AltContact>Some other one from Amazon</AltContact>
            </CustomerAdd>
        </CustomerAddRq>
    </QBXMLMsgsRq>
</QBXML>"""

root_lxml = etree.fromstring(customer_xml)
customer = Customer.from_lxml(root_lxml)
cs = CustomerService()
cs.add(customer)
hassaanalansary commented 7 months ago

Hello @Ryang20718

you need to create a QBDTask to sync your Model object to

This is a snippet pulled from my production code

# Add something like this to your post_save signal.
            QBDTask.objects.update_or_create(
                qb_operation=QUICKBOOKS_ENUMS.OPP_MOD,
                qb_resource=QUICKBOOKS_ENUMS.RESOURCE_CUSTOMER_POS,
                object_id=instance.id,
                content_type=ContentType.objects.get_for_model(instance),
                realm_id=YOUR_REALM_ID,
            )

it should call you ModelClass.to_qbd_obj()

Something Extra you may also want to add something like this to handle the signal coming back from QB in your settings.py if you want to get qbd_object_id propagated into your DB

QBWC_SETTINGS = {
"LOCAL_MODEL_CLASSES": {
        "CustomerPOS": "Inventory.models.Customer",
        "ItemInventoryPOS": "Inventory.models.Sku",
        "VoucherPOS": "Inventory.models.OrderVoucher",
    },
    "RESPONSE_PROCESSORS": (),
    "POS_RESPONSE_PROCESSORS": (
        "Inventory.qbd.ItemInventoryResponseProcessor",
        "Inventory.qbd.SkuAddResponseProcessor", # Custom one I made
        "django_quickbooks.processors.VoucherQueryResponseProcessor", # Default one
    ),
}

notice that `POS_RESPONSE_PROCESSORS` are only in my fork because it supports QBPOS
you can use `RESPONSE_PROCESSORS` for QB Finance

let me know if you find this helpful or if you need more help

Ryang20718 commented 7 months ago

@hassaanalansary Really appreciate the help here! If you could provide some pointers on where to place the QBDTask in relationship to a class, I think that would clear up all my confusion!

so I would need to create a class in order to leverage the customer/invoice classes

i.e if I have this in my models.py

class Customer(QBDModelMixin):
    first_name = models.CharField(max_length=255, null=True)
    last_name = models.CharField(max_length=255, null=True)
    email = models.CharField(max_length=255, blank=True, null=True)
    phone = models.CharField(max_length=10)
    street = models.CharField(max_length=255, blank=True, null=True)
    zip = models.CharField(max_length=255, blank=True, null=True)
    city = models.CharField(max_length=255, blank=True, null=True)
    state = models.CharField(max_length=255, blank=True, null=True)

    def __str__(self):
        return f'{self.first_name} {self.last_name}'

    def to_qbd_obj(self, **fields):
        from django_quickbooks.objects import Customer as QBCustomer
        # map your fields to the qbd_obj fields
        return QBCustomer(Name=self.__str__(),
                          IsActive=True,
                          Phone=self.phone,
                          )

    @classmethod              
    def from_qbd_obj(cls, qbd_obj):
        # map qbd_obj fields to your model fields
        return cls(
            first_name=qbd_obj.Name,
            phone=qbd_obj.Phone,
            qbd_object_id=qbd_obj.ListID,
            qbd_object_version=qbd_obj.EditSequence
        )

in my views.py,

I can instantiate the object

    from .models import Customer
    sample_customer = Customer(
        first_name="John",
        last_name="Doe",
        email="john.doe@example.com",
        phone="1234567890",
        street="123 Main St",
        zip="12345",
        city="Anytown",
        state="CA"
    )

However, based on your snippet above, where am I supposed to place the following? I understand the realm ID is based on the QWC, but what object is instance supposed to be? is that supposed to be the instance of the object?

            QBDTask.objects.update_or_create(
                qb_operation=QUICKBOOKS_ENUMS.OPP_MOD,
                qb_resource=QUICKBOOKS_ENUMS.RESOURCE_CUSTOMER_POS,
                object_id=instance.id,
                content_type=ContentType.objects.get_for_model(instance),
                realm_id=YOUR_REALM_ID,
            )
hassaanalansary commented 7 months ago

Instance is supposed to be Customer in your case

Ryang20718 commented 7 months ago

so If I have the following Customer class defined above in models.py

and the following in views.py, navigating to the URL to invoke hello should add a task to the QBWC?

from django.shortcuts import render
from django.http import HttpResponse

from django_quickbooks.models import QBDTask
from django_quickbooks import QUICKBOOKS_ENUMS
from django.contrib.contenttypes.models import ContentType
from django_quickbooks.services.invoice import InvoiceService
from django_quickbooks.services.customer import CustomerService
from qbPy.models import Customer

def create_customer():
    sample_customer = Customer(
        first_name="John",
        last_name="Doe",
        email="john.doe@example.com",
        phone="1234567890",
        street="123 Main St",
        zip="12345",
        city="Anytown",
        state="CA"
    )

    QBDTask.objects.update_or_create(
        qb_operation=QUICKBOOKS_ENUMS.OPP_MOD,
        qb_resource=QUICKBOOKS_ENUMS.RESOURCE_CUSTOMER,
        object_id=sample_customer.id,
        content_type=ContentType.objects.get_for_model(sample_customer),
        realm_id="073924d0-30a1-4e53-b8ce-121f184ae6d3",
    )

def hello(request):
    create_customer()
    return HttpResponse("Tried creating a customer")

Assuming QuickBooks Desktop is Auth'd and connected to QBWC, if I were to invoke the app via update selected in QBWC, I should be seeing the customer appear in QB desktop right? Currently I'm not quite sure if this is correct or if I'm missing something? 😅 png-qb

hassaanalansary commented 7 months ago

Yes, however your code doesn't really create the customer in the database You should .save() And make sure you have redis working

Ryang20718 commented 7 months ago

ah, I see

I changed django_quickbooks to use redismanager rather than rabbit mq in the settings and have a redis server running.

I think I'm getting closer to reaching the final step of creating a customer programatically ❗ However, parsing the XML response results in an exception when trying to dynamically import... is this expected?

full code ``` from django.shortcuts import render from django.http import HttpResponse from django_quickbooks.models import QBDTask from django_quickbooks import QUICKBOOKS_ENUMS from django.contrib.contenttypes.models import ContentType from django_quickbooks.services.invoice import InvoiceService from django_quickbooks.services.customer import CustomerService from qbPy.models import Customer def create_customer(): sample_customer = Customer( first_name="Joh2n", last_name="Doe", email="john.doe@example.com", phone="1234567890", street="123 Main St", zip="12345", city="Anytown", state="CA" ) sample_customer.save() QBDTask.objects.update_or_create( qb_operation=QUICKBOOKS_ENUMS.OPP_MOD, qb_resource=QUICKBOOKS_ENUMS.RESOURCE_CUSTOMER, object_id=sample_customer.id, content_type=ContentType.objects.get_for_model(sample_customer), realm_id="795c2f85-00a3-4f1d-9968-8e6bbf971da5", ) def hello(request): create_customer() return HttpResponse("Tried creating a customer") ```
stack trace ``` sendRequestXML() has been called ticket: d96ddcf4-f574-11ee-ba0d-e7cd672c55f7 strHCPResponse Intuit QuickBooks Enterprise Solutions 23.0 33 0 US 1.0 1.1 2.0 2.1 3.0 4.0 4.1 5.0 6.0 7.0 8.0 9.0 10.0 11.0 12.0 13.0 14.0 15.0 16.0 false SingleUser false testing123 testing123
LA US
LA LA US waorkerqoijqoiwjeoiqwjeoij@gmail.com January January Form1040 QuickBooks Online Banking banking.qb Never QuickBooks Online Billing billing.qb Never QuickBooks Online Billing Level 1 Service qbob1.qbn Never QuickBooks Online Billing Level 2 Service qbob2.qbn Never QuickBooks Online Billing Payment Service qbobpay.qbn Never QuickBooks Bill Payment billpay.qb Never QuickBooks Online Billing Paper Mailing Service qbobpaper.qbn Never QuickBooks Payroll Service payroll.qb Never QuickBooks Basic Payroll Service payrollbsc.qb Never QuickBooks Basic Disk Payroll Service payrollbscdisk.qb Never QuickBooks Deluxe Payroll Service payrolldlx.qb Never QuickBooks Premier Payroll Service payrollprm.qb Never Basic Plus Federal basic_plus_fed.qb Never Basic Plus Federal and State basic_plus_fed_state.qb Never Basic Plus Direct Deposit basic_plus_dd.qb Never Merchant Account Service mas.qbn Never false {55a9fd50-79e9-44e4-8fef-98411c2e8785} AppLock STR255TYPE LOCKED:BP-W-03:638481560711509765 {55a9fd50-79e9-44e4-8fef-98411c2e8785} FileID STR255TYPE {30830c9c-c1b7-4773-a12d-b193437a8fef}
false true false true true 0.00 0.00 0 false DueDate false true false false false false false false 10 false AgeFromDueDate Accrual false true true true Monday true Admin false false None false false false false
strCompanyFileName C:\Users\Public\Documents\Intuit\QuickBooks\Company Files\testing123.qbw qbXMLCountry US [08/Apr/2024 06:54:32] "POST /qwc/ HTTP/1.1" 200 659 receiveResponseXML() ticket=d96ddcf4-f574-11ee-ba0d-e7cd672c55f7 response= hresult=0x80040400 message=QuickBooks found an error when parsing the provided XML text stream. FK not enough values to unpack (expected 2, got 1) Traceback (most recent call last): File "/root/quickbooks-py/.venv/lib/python3.10/site-packages/spyne/application.py", line 173, in process_request ctx.out_object = self.call_wrapper(ctx) File "/root/quickbooks-py/.venv/lib/python3.10/site-packages/spyne/application.py", line 242, in call_wrapper return ctx.descriptor.service_class.call_wrapper(ctx) File "/root/quickbooks-py/.venv/lib/python3.10/site-packages/spyne/service.py", line 194, in call_wrapper return ctx.function(*args) File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/views/service.py", line 174, in receiveResponseXML return session_manager.process_response(ticket, response, hresult, message) File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/session_manager.py", line 64, in process_response processors = get_processors() File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/__init__.py", line 70, in get_processors for processor_class in qbwc_settings.RESPONSE_PROCESSORS: File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/settings.py", line 126, in __getattr__ val = perform_import(val, attr) File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/settings.py", line 75, in perform_import return [import_from_string(item, setting_name) for item in val] File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/settings.py", line 75, in return [import_from_string(item, setting_name) for item in val] File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/settings.py", line 88, in import_from_string module = import_module(module_path) File "/usr/lib/python3.10/importlib/__init__.py", line 126, in import_module return _bootstrap._gcd_import(name[level:], package, level) File "", line 1050, in _gcd_import File "", line 1027, in _find_and_load File "", line 1006, in _find_and_load_unlocked File "", line 688, in _load_unlocked File "", line 883, in exec_module File "", line 241, in _call_with_frames_removed File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/processors/__init__.py", line 4, in from django_quickbooks.processors.customer import \ File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/processors/customer.py", line 5, in LocalCustomer = qbwc_settings.LOCAL_MODEL_CLASSES['Customer'] File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/settings.py", line 130, in __getattr__ val[key] = import_from_string(value, value) File "/root/quickbooks-py/.venv/src/django-quickbooks/django_quickbooks/settings.py", line 87, in import_from_string module_path, class_name = val.rsplit('.', 1) ValueError: not enough values to unpack (expected 2, got 1) ```
hassaanalansary commented 7 months ago

I noticed 2 things You are trying to perform an action on the Customer resource in QB but you didn't provide django_quiclbooks with the mapping between your Customer model and qb Cusotmer model.

You need to add 'Customer' to your LOCAL_MODEL_CLASSES in settings.py And you are issuing qb_operation=QUICKBOOKS_ENUMS.OPP_MOD on a non existent customer

Ryang20718 commented 7 months ago

Added a check when importing

        if "." not in val:
            return

and switching the enums to OP_ADD worked :). Appreciate all the help!

Sorry to abuse this issue for an unrelated question; what steps would I need to take to query existing customers already in quickbooks that I haven't added from this django app? (I assume sending an XML to query should be sufficient, but is there an example on how to do so?)

hassaanalansary commented 7 months ago

sorry missed your message: I imagine you have figured it out by now

you can do so by add QBDTask for qb_operation=QUICKBOOKS_ENUMS.OPP_QR to the Resource that you want to query

hassaanalansary commented 7 months ago

If you want, can you please open a PR with documentation about the issues that you have faced and how you have fixed them? This will others trying to use this project.

Ryang20718 commented 7 months ago

If you want, can you please open a PR with documentation about the issues that you have faced and how you have fixed them? This will others trying to use this project.

Definitely, I can do that. Hoping to do that once I confirm I have a solid understanding of querying as well 😓

Ryang20718 commented 7 months ago

you can do so by add QBDTask for qb_operation=QUICKBOOKS_ENUMS.OPP_QR to the Resource that you want to query

here's what I did.

I created a customer from quickbooks UI and then wanted to see if I could fetch that object via a query. Query is below. I received an XML response. However, is this right? I assume after reading the xml, I can just convert the xml to the actual object?

def query_customer():
    print("QUERYING CUSTOMER")
    sample_customer = CustomerModel(
        first_name="john",
        last_name="doe33",
        email="john.doe@example.com",
        phone="1234567890",
        street="123 Main St",
        zip="12345",
        city="Anytown",
        state="CA"
    )
    sample_customer.save()
    res = QBDTask.objects.update_or_create(
        qb_operation=QUICKBOOKS_ENUMS.OPP_QR,
        qb_resource=QUICKBOOKS_ENUMS.RESOURCE_CUSTOMER,
        object_id=sample_customer.id,
        content_type=ContentType.objects.get_for_model(sample_customer),
        realm_id="af1a9c3c-fdbc-45a2-9b2d-57646df3dc19",
    )
    print("FINISHED QUERYING", sample_customer)

What's not clear to me is:

hassaanalansary commented 7 months ago

when you issue the query task. QB desktop will send you the customers in a response

all responses are handled ResponseProcessor refer to my first comment you need to define RESPONSE_PROCESSORS in your settings.py either use the builtin processors or you can define one and use it "RESPONSE_PROCESSORS": (), this is the builtin customers response processors

class CustomerQueryResponseProcessor(ResponseProcessor, ResponseProcessorMixin):
    resource = QUICKBOOKS_ENUMS.RESOURCE_CUSTOMER
    op_type = QUICKBOOKS_ENUMS.OPP_QR
    local_model_class = LocalCustomer
    obj_class = Customer

    def process(self, realm):
        cont = super().process(realm)
        if not cont:
            return False
        for customer_ret in list(self._response_body):
            customer = self.obj_class.from_lxml(customer_ret)
            local_customer = None
            if customer.ListID:
                local_customer = self.find_by_list_id(customer.ListID)
            if not local_customer and customer.Name:
                local_customer = self.find_by_name(customer.Name)

            if local_customer:
                self.update(local_customer, customer)
            else:
                self.create(customer)
        return True

you can inherit from it or define your own

if you have more questions you can send me an email, I will be more than happy to get in a meeting with you.

Ryon-NG commented 3 months ago

Hi there! XML/QB noob here, this thread has already been super helpful in programmatic customer creation. Would you mind also posting your solution to creating an invoice?