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") }}
- {{ __('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") }}