diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 91cd0e95..021a3548 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,7 +11,7 @@ assignees: '' **IMPORTANT** Before reporting a bug: -A properly detailed bug report can save a LOT of time and help fixing issues as soon as possible. +A properly detailed bug report can save a LOT of time and help fix issues as soon as possible. - -> ### Versions @@ -31,7 +31,7 @@ A properly detailed bug report can save a LOT of time and help fixing issues as 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' -4. See error +4. See the error ### What is Expected? @@ -44,4 +44,4 @@ Add any other context about the problem here. ### Screenshots -If applicable, add screenshots to help explain your problem. +If applicable, could you add screenshots to help explain your problem? diff --git a/posawesome/__init__.py b/posawesome/__init__.py index 7a5f57c6..21e6bdff 100644 --- a/posawesome/__init__.py +++ b/posawesome/__init__.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals import frappe -__version__ = "6.0.4" +__version__ = "6.1.0" def console(*data): diff --git a/posawesome/posawesome/api/payment_entry.py b/posawesome/posawesome/api/payment_entry.py index 6eb5e3f7..c7472615 100644 --- a/posawesome/posawesome/api/payment_entry.py +++ b/posawesome/posawesome/api/payment_entry.py @@ -12,6 +12,7 @@ from erpnext.setup.utils import get_exchange_rate from erpnext.accounts.doctype.bank_account.bank_account import get_party_bank_account from posawesome.posawesome.api.m_pesa import submit_mpesa_payment +from erpnext.accounts.utils import QueryPaymentLedger, get_outstanding_invoices as _get_outstanding_invoices def create_payment_entry( @@ -23,6 +24,7 @@ def create_payment_entry( reference_date=None, reference_no=None, posting_date=None, + cost_center=None, submit=0, ): # TODO : need to have a better way to handle currency @@ -48,7 +50,7 @@ def create_payment_entry( pe = frappe.new_doc("Payment Entry") pe.payment_type = payment_type pe.company = company - pe.cost_center = erpnext.get_default_cost_center(company) + pe.cost_center = cost_center or erpnext.get_default_cost_center(company) pe.posting_date = date pe.mode_of_payment = mode_of_payment pe.party_type = party_type @@ -127,34 +129,66 @@ def set_paid_amount_and_received_amount( @frappe.whitelist() def get_outstanding_invoices(company, currency, customer=None, pos_profile_name=None): - filters = { - "company": company, - "outstanding_amount": (">", 0), - "docstatus": 1, - "is_return": 0, - "currency": currency, - } if customer: - filters.update({"customer": customer}) - if pos_profile_name: - filters.update({"pos_profile": pos_profile_name}) - invoices = frappe.get_all( - "Sales Invoice", - filters=filters, - fields=[ - "name", - "customer", - "customer_name", - "outstanding_amount", - "grand_total", - "due_date", - "posting_date", - "currency", - "pos_profile", - ], - order_by="due_date asc", - ) - return invoices + precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 + outstanding_invoices = _get_outstanding_invoices( + party_type="Customer", + party=customer, + account=get_party_account("Customer", customer, company), + ) + invoices_list = [] + customer_name = frappe.get_cached_value("Customer", customer, "customer_name") + for invoice in outstanding_invoices: + if invoice.get("currency") == currency: + if pos_profile_name and frappe.get_cached_value( + "Sales Invoice", invoice.get("voucher_no"), "pos_profile" + ) != pos_profile_name: + continue + outstanding_amount = invoice.outstanding_amount + if outstanding_amount > 0.5 / (10**precision): + invoice_dict = { + "name": invoice.get("voucher_no"), + "customer": customer, + "customer_name": customer_name, + "outstanding_amount": invoice.get("outstanding_amount"), + "grand_total": invoice.get("invoice_amount"), + "due_date": invoice.get("due_date"), + "posting_date": invoice.get("posting_date"), + "currency": invoice.get("currency"), + "pos_profile": pos_profile_name, + + } + invoices_list.append(invoice_dict) + return invoices_list + else: + filters = { + "company": company, + "outstanding_amount": (">", 0), + "docstatus": 1, + "is_return": 0, + "currency": currency, + } + if customer: + filters.update({"customer": customer}) + if pos_profile_name: + filters.update({"pos_profile": pos_profile_name}) + invoices = frappe.get_all( + "Sales Invoice", + filters=filters, + fields=[ + "name", + "customer", + "customer_name", + "outstanding_amount", + "grand_total", + "due_date", + "posting_date", + "currency", + "pos_profile", + ], + order_by="due_date asc", + ) + return invoices @frappe.whitelist() @@ -258,6 +292,7 @@ def process_pos_payment(payload): posting_date=today, reference_no=pos_opening_shift_name, reference_date=today, + cost_center=data.pos_profile.get("cost_center"), submit=1, ) new_payments_entry.append(new_payment_entry) diff --git a/posawesome/posawesome/api/posapp.py b/posawesome/posawesome/api/posapp.py index 4aca5faf..d737a14c 100644 --- a/posawesome/posawesome/api/posapp.py +++ b/posawesome/posawesome/api/posapp.py @@ -30,12 +30,13 @@ get_applicable_delivery_charges as _get_applicable_delivery_charges, ) from frappe.utils.caching import redis_cache +from posawesome.posawesome.api.taxes import calculate_taxes @frappe.whitelist() def get_opening_dialog_data(): data = {} - data["companys"] = frappe.get_list("Company", limit_page_length=0, order_by="name") + data["companies"] = frappe.get_list("Company", limit_page_length=0, order_by="name") data["pos_profiles_data"] = frappe.get_list( "POS Profile", filters={"disabled": 0}, @@ -266,6 +267,7 @@ def _get_items(pos_profile, price_list, item_group, search_value): "batch_qty": batch.qty, "expiry_date": batch_doc.expiry_date, "batch_price": batch_doc.posa_batch_price, + "manufacturing_date": batch_doc.manufacturing_date, } ) serial_no_data = [] @@ -491,6 +493,7 @@ def update_invoice(data): for tax in invoice_doc.taxes: tax.included_in_print_rate = 1 + calculate_taxes(invoice_doc) invoice_doc.save() return invoice_doc @@ -884,6 +887,7 @@ def _get_items_details(pos_profile, items_data): "batch_qty": batch.qty, "expiry_date": batch_doc.expiry_date, "batch_price": batch_doc.posa_batch_price, + "manufacturing_date": batch_doc.manufacturing_date, } ) @@ -913,12 +917,31 @@ def _get_items_details(pos_profile, items_data): @frappe.whitelist() def get_item_detail(item, doc=None, warehouse=None, price_list=None): item = json.loads(item) + today = nowdate() item_code = item.get("item_code") - if warehouse and item.get("has_batch_no") and not item.get("batch_no"): - item["batch_no"] = get_batch_no( - item_code, warehouse, item.get("qty"), False, item.get("d") - ) + batch_no_data = [] + if warehouse and item.get("has_batch_no"): + batch_list = get_batch_qty(warehouse=warehouse, item_code=item_code) + if batch_list: + for batch in batch_list: + if batch.qty > 0 and batch.batch_no: + batch_doc = frappe.get_cached_doc("Batch", batch.batch_no) + if ( + str(batch_doc.expiry_date) > str(today) + or batch_doc.expiry_date in ["", None] + ) and batch_doc.disabled == 0: + batch_no_data.append( + { + "batch_no": batch.batch_no, + "batch_qty": batch.qty, + "expiry_date": batch_doc.expiry_date, + "batch_price": batch_doc.posa_batch_price, + "manufacturing_date": batch_doc.manufacturing_date, + } + ) + item["selling_price_list"] = price_list + max_discount = frappe.get_value("Item", item_code, "max_discount") res = get_item_details( item, @@ -928,6 +951,7 @@ def get_item_detail(item, doc=None, warehouse=None, price_list=None): if item.get("is_stock_item") and warehouse: res["actual_qty"] = get_stock_availability(item_code, warehouse) res["max_discount"] = max_discount + res["batch_no_data"] = batch_no_data return res diff --git a/posawesome/posawesome/api/taxes.py b/posawesome/posawesome/api/taxes.py new file mode 100644 index 00000000..58af9b6b --- /dev/null +++ b/posawesome/posawesome/api/taxes.py @@ -0,0 +1,165 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2023, Youssef Restom and contributors +# For license information, please see license.txt + + +from __future__ import unicode_literals + +# import frappe +import json +from frappe import _ +from frappe.utils import flt, cint, nowdate + + +def calculate_taxes(invoice_doc): + invoice_doc.transaction_date = nowdate() + rounding_adjustment_computed = invoice_doc.get( + "is_consolidated" + ) and invoice_doc.get("rounding_adjustment") + if not rounding_adjustment_computed: + invoice_doc.rounding_adjustment = 0 + + # maintain actual tax rate based on idx + actual_tax_dict = dict( + [ + [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))] + for tax in invoice_doc.get("taxes") + if tax.charge_type == "Actual" + ] + ) + + for tax in invoice_doc.get("taxes"): + if not (invoice_doc.get("is_consolidated") or tax.get("dont_recompute_tax")): + tax.item_wise_tax_detail = {} + tax.tax_amount = 0.0 + tax.tax_amount_after_discount_amount = 0.0 + + discount_amount_applied = False + if invoice_doc.apply_discount_on == "Grand Total" and invoice_doc.get( + "is_cash_or_non_trade_discount" + ): + discount_amount_applied = True + + for n, item in enumerate(invoice_doc.items): + item.net_amount = item.amount + item_tax_map = _load_item_tax_rate(item.item_tax_rate) + + for i, tax in enumerate(invoice_doc.get("taxes")): + # tax_amount represents the amount of tax for the current step + current_tax_amount = get_current_tax_amount( + invoice_doc, item, tax, item_tax_map + ) + + # Adjust divisional loss to the last item + if tax.charge_type == "Actual": + actual_tax_dict[tax.idx] -= current_tax_amount + if n == len(invoice_doc.items) - 1: + current_tax_amount += actual_tax_dict[tax.idx] + + # accumulate tax amount into tax.tax_amount + if tax.charge_type != "Actual" and not ( + discount_amount_applied + and invoice_doc.apply_discount_on == "Grand Total" + ): + tax.tax_amount += current_tax_amount + + # store tax_amount for current item as it will be used for + # charge type = 'On Previous Row Amount' + tax.tax_amount_for_current_item = current_tax_amount + + # set tax after discount + tax.tax_amount_after_discount_amount += current_tax_amount + + current_tax_amount = get_tax_amount_if_for_valuation_or_deduction( + invoice_doc, current_tax_amount, tax + ) + + # note: grand_total_for_current_item contains the contribution of + # item's amount, previously applied tax and the current tax on that item + if i == 0: + tax.grand_total_for_current_item = flt( + item.net_amount + current_tax_amount + ) + else: + tax.grand_total_for_current_item = flt( + invoice_doc.get("taxes")[i - 1].grand_total_for_current_item + + current_tax_amount + ) + + for tax in invoice_doc.get("taxes"): + tax.dont_recompute_tax = 1 + tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail) + + +def _load_item_tax_rate(item_tax_rate): + return json.loads(item_tax_rate) if item_tax_rate else {} + + +def _get_tax_rate(invoice_doc, tax, item_tax_map): + if tax.account_head in item_tax_map: + return flt( + item_tax_map.get(tax.account_head), invoice_doc.precision("rate", tax) + ) + else: + return 0.0 + + +def set_item_wise_tax(invoice_doc, item, tax, tax_rate, current_tax_amount): + # store tax breakup for each item + key = item.item_code or item.item_name + item_wise_tax_amount = current_tax_amount * invoice_doc.conversion_rate + if ( + tax.item_wise_tax_detail.get(key) + and tax.item_wise_tax_detail.get(key)[0] == tax_rate + ): + item_wise_tax_amount += tax.item_wise_tax_detail[key][1] + + elif not tax.item_wise_tax_detail.get(key): + tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)] + + +def get_current_tax_amount(invoice_doc, item, tax, item_tax_map): + tax_rate = _get_tax_rate(invoice_doc, tax, item_tax_map) + current_tax_amount = 0.0 + + if tax.charge_type == "Actual": + # distribute the tax amount proportionally to each item row + actual = flt(tax.tax_amount, tax.precision("tax_amount")) + current_tax_amount = ( + item.net_amount * actual / invoice_doc.doc.net_total + if invoice_doc.net_total + else 0.0 + ) + + elif tax.charge_type == "On Net Total": + current_tax_amount = (tax_rate / 100.0) * item.net_amount + elif tax.charge_type == "On Previous Row Amount": + current_tax_amount = (tax_rate / 100.0) * invoice_doc.get("taxes")[ + cint(tax.row_id) - 1 + ].tax_amount_for_current_item + elif tax.charge_type == "On Previous Row Total": + current_tax_amount = (tax_rate / 100.0) * invoice_doc.get("taxes")[ + cint(tax.row_id) - 1 + ].grand_total_for_current_item + elif tax.charge_type == "On Item Quantity": + current_tax_amount = tax_rate * item.qty + + if not (invoice_doc.get("is_consolidated") or tax.get("dont_recompute_tax")): + set_item_wise_tax(invoice_doc, item, tax, tax_rate, current_tax_amount) + + return current_tax_amount + + +def get_tax_amount_if_for_valuation_or_deduction(invoice_doc, tax_amount, tax): + # if just for valuation, do not add the tax amount in total + # if tax/charges is for deduction, multiply by -1 + if getattr(tax, "category", None): + tax_amount = 0.0 if (tax.category == "Valuation") else tax_amount + if invoice_doc.doctype in [ + "Purchase Order", + "Purchase Invoice", + "Purchase Receipt", + "Supplier Quotation", + ]: + tax_amount *= -1.0 if (tax.add_deduct_tax == "Deduct") else 1.0 + return tax_amount diff --git a/posawesome/public/js/posapp/components/payments/Pay.vue b/posawesome/public/js/posapp/components/payments/Pay.vue index a24ab90d..c10f8b0b 100644 --- a/posawesome/public/js/posapp/components/payments/Pay.vue +++ b/posawesome/public/js/posapp/components/payments/Pay.vue @@ -12,9 +12,9 @@

- {{ __('Invoices') }} + {{ __("Invoices") }} {{ __('- Total Outstanding') }} : + >{{ __("- Total Outstanding") }} : {{ currencySymbol(pos_profile.currency) }} {{ formtCurrency(total_outstanding_amount) }} @@ -22,7 +22,7 @@

- {{ __('Total Selected :') }} + {{ __("Total Selected :") }} {{ currencySymbol(pos_profile.currency) }} {{ formtCurrency(total_selected_invoices) }} @@ -51,7 +51,7 @@ color="warning" dark @click="get_outstanding_invoices" - >{{ __('Search') }}{{ __("Search") }} @@ -88,9 +88,9 @@

- {{ __('Payments') }} + {{ __("Payments") }} - {{ __('- Total Unallocated') }} : + {{ __("- Total Unallocated") }} : {{ currencySymbol(pos_profile.currency) }} {{ formtCurrency(total_unallocated_amount) }} @@ -98,7 +98,7 @@

- {{ __('Total Selected :') }} + {{ __("Total Selected :") }} {{ currencySymbol(pos_profile.currency) }} {{ formtCurrency(total_selected_payments) }} @@ -135,13 +135,13 @@

{{ __('Search Mpesa Payments') }}{{ __("Search Mpesa Payments") }}

- {{ __('Total Selected :') }} + {{ __("Total Selected :") }} {{ currencySymbol(pos_profile.currency) }} {{ formtCurrency(total_selected_mpesa_payments) }} @@ -181,7 +181,7 @@ color="warning" dark @click="get_draft_mpesa_payments_register" - >{{ __('Search') }}{{ __("Search") }} @@ -215,7 +215,7 @@

Totals

- {{ __('Total Invoices:') }} + {{ __("Total Invoices:") }} {{ __('Total Payments:') }}{{ __("Total Payments:") }} {{ __('Total Mpesa:') }}{{ __("Total Mpesa:") }} -

{{ __('Difference:') }}

+

{{ __("Difference:") }}

- {{ __('Submit') }} + {{ __("Submit") }} @@ -339,21 +339,21 @@ diff --git a/posawesome/public/js/posapp/components/pos/Invoice.vue b/posawesome/public/js/posapp/components/pos/Invoice.vue index 0805b1bf..83dc35ab 100644 --- a/posawesome/public/js/posapp/components/pos/Invoice.vue +++ b/posawesome/public/js/posapp/components/pos/Invoice.vue @@ -4,16 +4,16 @@ {{ - __('Cancel Current Invoice ?') + __("Cancel Current Invoice ?") }} - {{ __('Cancel') }} + {{ __("Cancel") }} - {{ __('Back') }} + {{ __("Back") }} @@ -750,7 +750,7 @@ color="warning" dark @click="get_draft_invoices" - >{{ __('Held') }}{{ __("Held") }} @@ -761,7 +761,7 @@ color="secondary" dark @click="open_returns" - >{{ __('Return') }}{{ __("Return") }} @@ -771,7 +771,7 @@ color="error" dark @click="cancel_dialog = true" - >{{ __('Cancel') }}{{ __("Cancel") }} @@ -781,7 +781,7 @@ color="accent" dark @click="new_invoice" - >{{ __('Save/New') }}{{ __("Save/New") }} @@ -791,7 +791,7 @@ color="success" @click="show_payment" dark - >{{ __('PAY') }}{{ __("PAY") }} {{ __('Print Draft') }}{{ __("Print Draft") }}
@@ -816,21 +816,21 @@