Exemple #1
0
def products(request, company):
    c = get_object_or_404(Company, url_name=company)
    
    # needs to be at least guest to view products
    if not has_permission(request.user, c, 'product', 'view'):
        return no_permission_view(request, c, _("You have no permission to view products."))

    # if there are no taxes defined, don't show anything
    if Tax.objects.filter(company=c).count() == 0:
        return error(request, c, _("There are no taxes defined. Please go to tax management and define them."))

    # if there are no categories defined, throw an error
    if Category.objects.filter(company=c).count() == 0:
        return error(request, c, _("There are no categories defined, please go to category management to define them."))

    # fields that need to be limited in length:
    lengths = {
        'code': max_field_length(Product, 'code'),
        'price': g.DECIMAL['currency_digits'] + 1,
        'purchase_price': g.DECIMAL['currency_digits'] + 1,
        'shortcut': max_field_length(Product, 'shortcut'),
        'stock': g.DECIMAL['quantity_digits'],
        'name': max_field_length(Product, 'name'),
        'tax': g.DECIMAL['percentage_decimal_places'] + 4,  # up to '100.' + 'decimal_digits'
    }
    
    context = {
        'company': c,
        'title': _("Products"),
        'site_title': g.MISC['site_title'],
        # lists
        'taxes': JsonStringify(get_all_taxes(request.user, c)),
        'categories': JsonStringify(get_all_categories(c, json=True)),
        'units': JsonStringify(g.UNITS),
        'discounts': JsonStringify(get_all_discounts(request.user, c)),
        # urls for ajax calls
        'add_url': reverse('pos:create_product', args=[c.url_name]),
        # config variables
        'can_edit': has_permission(request.user, c, 'product', 'edit'),
        'currency': get_company_value(request.user, c, 'pos_currency'),
        # images
        'image_dimensions': g.IMAGE_DIMENSIONS['product'],
        'image_upload_formats': g.MISC['image_upload_formats'], # what can be uploaded
        'max_upload_size': round(g.MISC['max_upload_image_size']/2**20, 2), # show in megabytes
        'max_upload_size_bytes': g.MISC['max_upload_image_size'], # bytes for javascript
        # html fields
        'field_lengths': lengths,
        'separator': get_company_value(request.user, c, 'pos_decimal_separator'),
        # numbers etc
        'default_tax_id': get_default_tax(request.user, c)['id'],
        'decimal_places': get_company_value(request.user, c, 'pos_decimal_places')*2,  # ACHTUNG: rounding comes at the end
    }
    return render(request, 'pos/manage/products.html', context)
Exemple #2
0
def validate_company(user, company, data):

    def r(status, msg):
        return {'status': status,
                'data': data,
                'message': msg}

    if not has_permission(user, company, 'company', 'edit'):
        return r(False, _("You have no permission to edit this company"))

    if data.get('url_name'):
        url_name = data['url_name']
        if url_name != company.url_name:
            if not check_url_name(url_name):
                return r(False, _("Url of the company is invalid or exists already."))
    else:
        url_name = company.url_name

    if not data.get('name'):
        return r(False, _("No name entered"))
    elif len(data['name']) > max_field_length(Company, 'name'):
        return r(False, _("Name too long"))

    if not data.get('email'):
        return r(False, _("No email entered"))

    return {'status':  True, 'data': data}
Exemple #3
0
    def value(ir, ic, product_name=None, check_col_len=None):
        # returns cell value at specified row/column, formatted as text
        #if book.cell_type(ir, ic) != xlrd.XL_CELL_TEXT:
        # this is obviously not working since nothing is formatted as text at all
        #    info(_("Cell value is not formatted as text"), product_name, ir, ic)

        v = unicode(book.cell_value(ir, ic)).strip()

        if check_col_len: # if there's a maximum length of field
            l = max_field_length(Product, check_col_len) - 1
            if l:
                return v[-l:]

        return v  # or don't truncate it at all
Exemple #4
0
def list_taxes(request, company):
    c = get_object_or_404(Company, url_name=company)
    
    # permissions
    list_permission = has_permission(request.user, c, 'tax', 'view')
    edit_permission = has_permission(request.user, c, 'tax', 'edit')
    
    if not list_permission:
        return no_permission_view(request, c, _("You have no permission to view taxes."))
    
    context = {
        'company': c,
        'taxes': JsonStringify(get_all_taxes(request.user, c)),
        'edit_permission': edit_permission,
        'max_name_length': max_field_length(Tax, 'name'),
        'title': _("Manage Tax Rates"),
        'site_title': g.MISC['site_title'],
        
        'separator': get_company_value(request.user, c, 'pos_decimal_separator'),
    }
    
    return render(request, 'pos/manage/tax.html', context)
Exemple #5
0
def validate_tax(user, company, tax):
    # validate_product, validate_discount, validate_contact for more info
    def err(msg):
        return {'success': False, 'data': None, 'message': msg}

    # name: check length
    if len(tax['name']) > 0:
        if len(tax['name']) > max_field_length(Tax, 'name'):
            return err(_("Name too long"))
    
    # amount: try to parse number
    if tax['amount']:
        r = parse_decimal(user, company, tax['amount'])
        if r['success']:
            tax['amount'] = r['number']  # this could
        else:
            return err(_("Wrong number format for amount"))
    else:
        # amount is mandatory
        return err(_("No amount entered"))

    # default: does not matter
        
    return {'success': True, 'data': tax, 'message': None}
Exemple #6
0
def validate_product(user, company, data):
    # data format (*-validation needed):
    # id
    # name*
    # price - numeric value*
    # unit type
    # discounts - list of discount ids (checked in create/edit_product)
    # image
    # category - id (checked in create/edit_product)
    # code*
    # shortcut*
    # description
    # private notes
    # tax*
    # stock*

    def r(status, msg):
        return {'status':status,
            'data':data,
            'message':msg}

    # return:
    # {status:true/false - if cleaning succeeded
    #  data:cleaned_data - empty dict if status = false
    #  message:error_message - empty if status = true
    
    try:
        data['id'] = int(data['id'])
    except (ValueError, TypeError):
        # this shouldn't happen
        return r(False, _("Wrong product id"))
    
    
    if data['id'] != -1:
        # check if product belongs to this company and if he has the required permissions
        try:
            p = Product.objects.get(id=data['id'], company=company)
        except Product.DoesNotExist:
            return r(False, _("Cannot edit product: it does not exist"))
        
        if p.company != company or not has_permission(user, company, 'product', 'edit'):
            return r(False, _("You have no permission to edit this product"))
    
    # name
    if not data['name']:
        return r(False, _("No name entered"))
    elif len(data['name']) > max_field_length(Product, 'name'):
        return r(False, _("Name too long"))
    else:
        if data['id'] == -1: # when adding new products:
            # check if a product with that name exists
            p = Product.objects.filter(company=company,name=data['name'])
            if p.count() > 0:
                return r(False,
                    _("There is already a product with that name") +  \
                    " (" + _("code") + ": " + p[0].code + ")")
    data['name'] = data['name'].strip()
    
    # category: must be present and must exist
    if not data.get('category_id'):
        return r(False, _("No category assigned"))
    else:
        try:
            data['category_id'] = int(data['category_id'])
            data['category'] = Category.objects.get(id=data['category_id'], company=company)
        except Category.DoesNotExist:
            return r(False, _("Selected category does not exist"))
    
    
    # price
    if len(data['price']) > g.DECIMAL['currency_digits']+1:
        return r(False, _("Price too long"))
    
    ret = parse_decimal(user, company, data['price'], g.DECIMAL['currency_digits'])
    if not ret['success']:
        return r(False, _("Check price notation"))
    data['price'] = ret['number']
    
    # purchase price
    if 'purchase_price' in data:
        if len(data['purchase_price']) > g.DECIMAL['currency_digits']+1:
            return r(False, _("Purchase price too long"))
    
        if len(data['purchase_price']) > 1:  # purchase price is not mandatory, so don't whine if it's not entered
            ret = parse_decimal(user, company, data['purchase_price'], g.DECIMAL['currency_digits'])
            if not ret['success']:
                return r(False, _("Check purchase price notation"))
            data['purchase_price'] = ret['number']
    
    
    # unit type (probably doesn't need checking
    if not data['unit_type'] in dict(g.UNITS):
        return r(False, _("Invalid unit type"))
        
    # image:
    if data['change_image'] == True:
        if 'image' in data and data['image']:  # a new image has been uploaded
            data['image'] = import_color_image(data['image'], g.IMAGE_DIMENSIONS['product'], 'fill')
            if not data['image']:
                # something has gone wrong during conversion
                return r(False, _("Image upload failed"))
        else:
            # image will be deleted in view
            pass
    else:
        # no change regarding images
        data['image'] = None
            
    # code: must exist and must be unique
    data['code'] = data['code'].strip()
    if not data['code']:
        return r(False, _("No code entered"))
    else:
        if len(data['code']) > max_field_length(Product, 'code'):
            return r(False, _("Code too long"))
        
        try:
            p = Product.objects.get(company=company, code=data['code'])
            if p.id != data['id']:
                # same ids = product is being edited, so codes can be the same
                # if ids are not the same, it's either new product or another product's code 
                return r(False,
                    _("A product with this code already exists: ") + p.name)
        except Product.DoesNotExist:
            pass # there is no product with this code, everything is ok
    
    # shortcut: if exists, must be unique
    data['shortcut'] = data['shortcut'].strip()
    if data['shortcut']:
        if len(data['shortcut']) > max_field_length(Product, 'shortcut'):
            return r(False, _("Shortcut too long"))
        
        try:
            p = Product.objects.get(company=company, shortcut=data['shortcut'])
            if p.id != data['id']:
                return r(False,
                    _("A product with this shortcut already exists: ") + p.name)
        except Product.DoesNotExist:
            pass  # ok
    
    # description, notes - anything can be entered
    data['description'] = data['description'].strip()
    data['private_notes'] = data['private_notes'].strip()
    
    # tax: id should be among tax rates for this company
    try:
        tax = Tax.objects.get(id=int(data['tax_id']))
    except:
        return r(False, _("Invalid tax rate"))
    
    
    
    del data['tax_id']
    data['tax'] = tax
    
    # stock
    if len(data['stock']) > g.DECIMAL['quantity_digits']:
        return r(False, _("Stock number too big"))
    
    ret = parse_decimal(user, company, data['stock'],
        g.DECIMAL['quantity_digits']-g.DECIMAL['quantity_decimal_places']-1)
    if not ret['success']:
        return r(False, _("Check stock notation"))
    else:
        data['stock'] = ret['number']
        # it cannot be negative
        if data['stock'] < decimal.Decimal('0'):
            return r(False, _("Stock cannot be negative"))
        
    return {'status': True, 'data': data}
Exemple #7
0
def validate_contact(user, company, data):
    # data format (*-required data):
    # type*
    # company_name* (only if Company)
    # first_name* (only if Individual)
    # last_name* (only if Individual)
    # date_of_birth - list of discount ids (checked in create/edit_product)
    # street_address
    # postcode
    # city
    # country
    # state
    # email
    # phone
    # vat

    def err(msg):  # the value that is returned if anything goes wrong
        return {'status': False, 'data': None, 'message': msg}

    # return:
    # status: True if validation passed, else False
    #  data: 'cleaned' data if validation succeeded, else False
    #  message: error message or empty if validation succeeded

    if 'type' not in data:
        return err(_("No type specified"))
    if data['type'] not in [x[0] for x in g.CONTACT_TYPES]:
        return err(_("Wrong contact type"))

    # check credentials for Individual
    if data['type'] == 'Individual':
        # first name: check length (mandatory)
        if not data.get('first_name'):
            return err(_("No first name"))
        elif len(data['first_name']) > max_field_length(Contact, 'first_name'):
            return err(_("First name too long"))
        
        # last name: check length (mandatory)
        if not data.get('last_name'):
            return err(_("No last name"))
        elif len(data['last_name']) > max_field_length(Contact, 'last_name'):
            return err(_("Last name too long"))
        
        # sex: must be in g.SEXES
        if 'sex' not in data and data['sex'] not in [x[0] for x in g.SEXES]:
            return err(_("Wrong sex"))
        
        # date of birth: parse date
        if 'date_of_birth' in data and len(data['date_of_birth']) > 0:
            r = parse_date(user, company, data['date_of_birth'])
            if not r['success']:
                return err(_("Wrong format of date of birth"))
            else:
                data['date_of_birth'] = r['date']
        else:
            data['date_of_birth'] = None
        
    # check credentials for company
    else:
        if not data.get('company_name'):
            return err(_("No company name"))
            
        if len(data['company_name']) > max_field_length(Contact, 'company_name'):
            return err(_("Company name too long"))
        
        # one shalt not save u'' into date field.
        data['date_of_birth'] = None
            
    # common fields:
    # email is not required
    if 'email' in data:
        if len(data['email']) > max_field_length(Contact, 'email'):
            return err(_("Email address too long"))
        if 0 < len(data['email']) < 6:  # there's something entered, but nothing useful
            return err(_("Email address too short"))

        # validate email with regex:
        # ^[\w\d._+%]+  a string with any number of characters, numbers, and ._+% signs
        # @[\w\d.-]{2,} an @ sign followed by domain name of at least 2 characters long (can contain . and -)
        # \.\w{2,4}$'   domain (2-4 alphabetic characters)
        m = re.search('([\w.-]+)@([\w.-]+)', data['email'])
        if not m or not m.group() or data['email'] not in m.group():
            return err(_("Invalid email address"))
    else:
        data['email'] = None

    # country: check for correct code
    if 'country' in data:
        if data['country'] not in country_by_code:
            return err(_("Country does not exist"))
    
    # other, non-mandatory fields: check length only
    def check_length(d, l):
        if d:
            if len(d) > l:
                return False
            else:
                return True
        else:
            return True
        
    # street_address
    if not check_length(data.get('street_address'), max_field_length(Contact, 'street_address')):
        return err(_("Street address too long"))
    
    # postcode
    if not check_length(data.get('postcode'), max_field_length(Contact, 'postcode')):
        return err(_("Post code too long"))
        
    # city
    if not check_length(data.get('city'), max_field_length(Contact, 'city')):
        return err(_("City name too long"))
    
    # state 
    if not check_length(data.get('state'), max_field_length(Contact, 'state')):
        return err(_("State name too long"))
    
    # phone
    if not check_length(data.get('phone'), max_field_length(Contact, 'phone')):
        return err(_("Phone number too long"))

    # vat
    if not check_length(data.get('vat'), max_field_length(Contact, 'vat')):
        return err(_("VAT number too long"))
    
    # everything OK
    return {'status': True, 'data': data, 'message': None}
Exemple #8
0
def create_bill_(request, c):
    def item_error(message, product):
        return JsonError(message + " " + _("(Item" + ": ") + product.name + ")")

    # check permissions
    if not has_permission(request.user, c, "bill", "edit"):
        return JsonError(_("You have no permission to create bills"))

    # get data
    data = JsonParse(request.POST.get("data"))
    if not data:
        return JsonError(_("No data received"))

    # see if we're updating an existing bill
    existing_bill = None
    try:
        existing_bill = Bill.objects.get(company=c, id=int(data.get("id")))
        if existing_bill.status == g.PAID:
            return JsonError(_("This bill has already been paid, editing is not possible"))
    except (ValueError, TypeError):
        pass
    except Bill.DoesNotExist:
        pass

    # current company (that will be fixed on this bill forever):
    # save a FK to BillCompany; the last company is the current one
    bill_company = BillCompany.objects.filter(company=c).order_by("-datetime_created")[0]

    # current register: get BillRegister with the same id as current one
    try:
        bill_registers = BillRegister.objects.filter(register__id=int(data.get("register_id")))
        if len(bill_registers) > 0:
            bill_register = bill_registers[0]
        else:
            raise Register.DoesNotExist
    except (TypeError, ValueError, Register.DoesNotExist):
        return JsonError(_("Invalid register specified."))

    # current contact: get BillContact with the same id as the requested contact
    if data.get("contact"):
        try:
            bill_contacts = BillContact.objects.get(contact__id=int(data.get("contact").get("id"))).order_by(
                "datetime_created"
            )
            if len(bill_contacts) > 0:
                bill_contact = bill_contacts[0]
            else:
                raise Contact.DoesNotExist
        except (Contact.DoesNotExist, ValueError, TypeError):
            return JsonError(_("Invalid contact"))
    else:
        bill_contact = None

    # save all validated stuff in bill to a dictionary and insert into database at the end
    # prepare data for insert
    bill = {
        "company": c,
        "issuer": bill_company,
        "register": bill_register,
        "contact": bill_contact,
        "user_id": request.user.id,
        "user_name": str(request.user),
        "notes": data.get("notes", "")[: max_field_length(Bill, "notes")],
        "type": g.CASH,
        "status": g.WAITING,
        "items": [],
        "currency": get_company_value(request.user, c, "pos_currency"),
        # numbers...
        "base": Decimal(0),
        "discount": Decimal(0),
        "tax": Decimal(0),
        "total": Decimal(0),
        "created_by": request.user,
    }

    # timestamp
    try:
        # timestamp: send in an array of number:
        # [year, month, day, hour, minute, second]
        tn = [int(n) for n in data.get("timestamp")]
        bill["timestamp"] = dtm(year=tn[0], month=tn[1], day=tn[2], hour=tn[3], minute=tn[4], second=tn[5])

    except (ValueError, TypeError):
        return JsonError(_("Invalid timestamp"))

    r = parse_decimal(request.user, c, data.get("total"))
    if not r["success"] or r["number"] <= Decimal("0"):
        return JsonError(_("Invalid grand total value"))
    else:
        bill["total"] = r["number"]

    # validate items
    for i in data.get("items"):
        # get product
        try:
            product = Product.objects.get(company=c, id=int(i.get("product_id")))
        except Product.DoesNotExist:
            return JsonError(_("Product with this id does not exist") + " (id=" + i.get("product_id") + ")")

        # parse quantity
        r = parse_decimal(request.user, c, i.get("quantity"), g.DECIMAL["quantity_digits"])
        if not r["success"]:
            return item_error(_("Invalid quantity value"), product)
        else:
            if r["number"] <= Decimal("0"):
                return item_error(_("Cannot add an item with zero or negative quantity"), product)
        quantity = r["number"]

        # remove from stock; TODO: check negative quantities (?)
        # actually we leave negative quantities as they are or
        # when stock is empty, we leave it at 0

        product.destockify(quantity)
        product.save()

        item = {
            "created_by": request.user,
            "code": product.code,
            "shortcut": product.shortcut,
            "name": product.name,
            "description": product.description,
            "private_notes": product.private_notes,
            "unit_type": product.get_unit_type_display(),  # ! display, not the 'code'
            "stock": product.stock,
            # 'bill':  not now, after bill is saved
            "product_id": product.id,
            "bill_notes": i.get("bill_notes"),
            "discounts": [],  # validated discounts (FK in database)
            # prices: will be calculated when discounts are ready
            "base": None,
            "quantity": None,
            "tax_rate": None,
            "batch": None,
            "discount": None,
            "net": None,
            "tax": None,
            "total": None,
        }

        bill["items"].append(item)

        for d in i["discounts"]:
            # check:
            # discount id: if it's -1, it's a unique discount on this item;
            #              if it's anything else, the discount must belong to this company
            #              and must be active and enabled
            d_id = int(d.get("id"))
            if d_id != -1:
                try:
                    dbd = Discount.objects.get(id=d_id, company=c)

                    if not dbd.is_active:
                        return item_error(_("The discount is not active"), product)
                except Discount.DoesNotExist:
                    return item_error(_("Chosen discount does not exist or is not valid"), product)

            # amount: parse number and check that percentage does not exceed 100%
            r = parse_decimal(request.user, c, d.get("amount"))
            if not r["success"]:
                return item_error(_("Invalid discount amount"), product)
            else:
                d_amount = r["number"]
                if d_amount < Decimal(0) or (d.get("type") == "Relative" and d_amount > Decimal(100)):
                    return item_error(_("Invalid discount amount"), product)

            # save data to bill
            discount = {
                "id": d_id,
                "code": d.get("code"),
                "description": d.get("description"),
                "type": d.get("type"),
                "amount": d_amount,
            }
            item["discounts"].append(discount)

        # save this item's prices to item's dictionary (will go into database later)
        try:
            item["base"] = parse_decimal_exc(request.user, c, i.get("base"), message=_("Invalid base price"))
            item["quantity"] = parse_decimal_exc(request.user, c, i.get("quantity"), message=_("Invalid quantity"))
            item["tax_rate"] = parse_decimal_exc(request.user, c, i.get("tax_rate"), message=_("Invalid tax rate"))
            item["batch"] = parse_decimal_exc(request.user, c, i.get("batch"), message=_("Invalid batch price"))
            item["discount"] = parse_decimal_exc(
                request.user, c, i.get("discount"), message=_("Invalid discount amount")
            )
            item["net"] = parse_decimal_exc(request.user, c, i.get("net"), message=_("Invalid net price"))
            item["tax"] = parse_decimal_exc(request.user, c, i.get("tax"), message=_("Invalid tax amount"))
            item["total"] = parse_decimal_exc(request.user, c, i.get("total"), message=_("Invalid total"))

            bill["base"] += item["batch"]
            bill["discount"] += item["discount"]
            bill["tax"] += item["tax"]
        except ValueError as e:
            return item_error(e.message, product)

    # at this point, everything is fine, insert into database
    if existing_bill:
        existing_bill.delete()

    bill_payment = Payment(
        type=g.CASH,
        total=bill["total"],
        currency=get_company_value(request.user, c, "pos_currency"),
        transaction_datetime=datetime.utcnow(),
        status=g.WAITING,
        created_by=request.user,
    )
    bill_payment.save()

    # create a new bill
    db_bill = Bill(
        created_by=request.user,
        company=c,  # current company, FK to Company object
        issuer=bill["issuer"],  # fixed company details at this moment, FK to BillCompany object
        user_id=bill["user_id"],  # id of user that created this bill, just an integer, not a FK
        user_name=bill["user_name"],  # copied user name in case that user gets 'fired'
        register=bill["register"],  # current settings of the register this bill was created on
        contact=bill["contact"],  # FK on BillContact, copy of the Contact object
        notes=bill["notes"],
        # timestamp=dtm.utcnow().replace(tzinfo=timezone(get_company_value(request.user, c, 'pos_timezone'))),
        timestamp=bill["timestamp"],
        payment=bill_payment,
        base=bill["base"],
        discount=bill["discount"],
        tax=bill["tax"],
    )
    db_bill.save()

    # create new items
    for item in bill["items"]:
        db_item = BillItem(
            created_by=item["created_by"],
            code=item["code"],
            shortcut=item["shortcut"],
            name=item["name"],
            description=item["description"],
            private_notes=item["private_notes"],
            unit_type=item["unit_type"],
            bill=db_bill,
            bill_notes=item["bill_notes"],
            product_id=item["product_id"],
            quantity=item["quantity"],
            base=item["base"],
            tax_rate=item["tax_rate"],
            batch=item["batch"],
            discount=item["discount"],
            net=item["net"],
            tax=item["tax"],
            total=item["total"],
        )
        db_item.save()

        # save discounts for this item
        for discount in item["discounts"]:
            db_discount = BillItemDiscount(
                created_by=request.user,
                bill_item=db_item,
                description=discount["description"],
                code=discount["code"],
                type=discount["type"],
                amount=discount["amount"],
            )
            db_discount.save()

    db_bill.save()

    d = {"bill": bill_to_dict(request.user, c, db_bill)}

    return JsonOk(extra=d)