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)
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}
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
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)
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}
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}
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}
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)