Compare commits
75 Commits
v13.54.7
...
version-13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37e00a66da | ||
|
|
4394c0113e | ||
|
|
ab82e30fac | ||
|
|
32f3365ac7 | ||
|
|
80012b7339 | ||
|
|
b049b52294 | ||
|
|
82c28ac89f | ||
|
|
692d98dd4d | ||
|
|
5d1c35634c | ||
|
|
9900274b27 | ||
|
|
14955c70d4 | ||
|
|
8c55e35d20 | ||
|
|
e6e9f1dc26 | ||
|
|
4f8b13ac57 | ||
|
|
f0877ffa47 | ||
|
|
e291b5db3d | ||
|
|
b0f7de1a0f | ||
|
|
8dbb200fe3 | ||
|
|
7df8425756 | ||
|
|
3863c4e7fb | ||
|
|
10f02e60ce | ||
|
|
48eaa51c4a | ||
|
|
fee4eae96c | ||
|
|
ee7c9add39 | ||
|
|
b6229515ef | ||
|
|
6e25a5189c | ||
|
|
1b78dd17c9 | ||
|
|
77e01ebacf | ||
|
|
bd371e697c | ||
|
|
2c4cee025b | ||
|
|
303becf1e3 | ||
|
|
983c2133c2 | ||
|
|
a76c6ab042 | ||
|
|
6f44a1630f | ||
|
|
b7944a7c07 | ||
|
|
3dfc1450a1 | ||
|
|
835c85a087 | ||
|
|
190f77abff | ||
|
|
a37bdb54cd | ||
|
|
b28e29cab1 | ||
|
|
095d99dbd2 | ||
|
|
01fc5f6865 | ||
|
|
911e2c5b36 | ||
|
|
a9429e160d | ||
|
|
5342cd0dfa | ||
|
|
3bf84e1464 | ||
|
|
65ae8d9c05 | ||
|
|
35717124cd | ||
|
|
89c107ea8b | ||
|
|
958db77cda | ||
|
|
bc1da4678a | ||
|
|
6cb8a40339 | ||
|
|
9139c14639 | ||
|
|
461eb7a50d | ||
|
|
635c3d54f5 | ||
|
|
1bd3f4eeef | ||
|
|
4b8ed0f6ae | ||
|
|
eea7bbcea7 | ||
|
|
1e436052e2 | ||
|
|
91a132c011 | ||
|
|
2263542a6b | ||
|
|
5be5fde276 | ||
|
|
6a53e823f0 | ||
|
|
c808607975 | ||
|
|
8cb0f690d5 | ||
|
|
4789ecacea | ||
|
|
d00257ffd7 | ||
|
|
37b1a0e778 | ||
|
|
6952f0f082 | ||
|
|
c84384aa1b | ||
|
|
428f7a61ef | ||
|
|
8f4ded6ad1 | ||
|
|
9607d69ada | ||
|
|
aaba335273 | ||
|
|
13d5eec194 |
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
@@ -22,10 +22,8 @@ jobs:
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
./frappe-semgrep-rules/rules
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.54.7"
|
||||
__version__ = "13.55.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -358,6 +358,7 @@ def update_outstanding_amt(
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
|
||||
|
||||
bal = flt(bal, frappe.get_precision(against_voucher_type, "outstanding_amount"))
|
||||
# Didn't use db_set for optimization purpose
|
||||
ref_doc.outstanding_amount = bal
|
||||
frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)
|
||||
|
||||
@@ -175,52 +175,53 @@ class PaymentEntry(AccountsController):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
}
|
||||
)
|
||||
if self.references:
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
|
||||
|
||||
for d in self.get("references"):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
|
||||
for d in self.get("references"):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
|
||||
@@ -295,6 +295,7 @@ class PaymentReconciliation(Document):
|
||||
"amount": pay.get("amount"),
|
||||
"allocated_amount": allocated_amount,
|
||||
"difference_amount": pay.get("difference_amount"),
|
||||
"currency": inv.get("currency"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"column_break_7",
|
||||
"difference_account"
|
||||
"difference_account",
|
||||
"currency"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -37,7 +38,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -112,7 +113,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Unreconciled Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -120,7 +121,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -129,11 +130,18 @@
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-06 11:48:59.616562",
|
||||
"modified": "2023-11-28 16:30:43.344612",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
||||
@@ -226,7 +226,9 @@ def set_address_details(
|
||||
party_details.update(
|
||||
{
|
||||
"shipping_address": shipping_address,
|
||||
"shipping_address_display": render_address(shipping_address),
|
||||
"shipping_address_display": render_address(
|
||||
shipping_address, check_permissions=not ignore_permissions
|
||||
),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, msgprint
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.contacts.doctype.address.address import render_address
|
||||
from frappe.utils import cint, cstr, flt, getdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
@@ -187,7 +187,9 @@ class BuyingController(StockController, Subcontracting):
|
||||
|
||||
for address_field, address_display_field in address_dict.items():
|
||||
if self.get(address_field):
|
||||
self.set(address_display_field, get_address_display(self.get(address_field)))
|
||||
self.set(
|
||||
address_display_field, render_address(self.get(address_field), check_permissions=False)
|
||||
)
|
||||
|
||||
def set_total_in_words(self):
|
||||
from frappe.utils import money_in_words
|
||||
|
||||
@@ -914,8 +914,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
|
||||
|
||||
repost_entry = frappe.new_doc("Repost Item Valuation")
|
||||
repost_entry.based_on = "Item and Warehouse"
|
||||
repost_entry.voucher_type = voucher_type
|
||||
repost_entry.voucher_no = voucher_no
|
||||
|
||||
repost_entry.item_code = sle.item_code
|
||||
repost_entry.warehouse = sle.warehouse
|
||||
|
||||
@@ -296,18 +296,27 @@ class LeaveAllocation(Document):
|
||||
|
||||
def get_previous_allocation(from_date, leave_type, employee):
|
||||
"""Returns document properties of previous allocation"""
|
||||
return frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
filters={
|
||||
"to_date": ("<", from_date),
|
||||
"leave_type": leave_type,
|
||||
"employee": employee,
|
||||
"docstatus": 1,
|
||||
},
|
||||
order_by="to_date DESC",
|
||||
fieldname=["name", "from_date", "to_date", "employee", "leave_type"],
|
||||
as_dict=1,
|
||||
)
|
||||
Allocation = frappe.qb.DocType("Leave Allocation")
|
||||
allocations = (
|
||||
frappe.qb.from_(Allocation)
|
||||
.select(
|
||||
Allocation.name,
|
||||
Allocation.from_date,
|
||||
Allocation.to_date,
|
||||
Allocation.employee,
|
||||
Allocation.leave_type,
|
||||
)
|
||||
.where(
|
||||
(Allocation.employee == employee)
|
||||
& (Allocation.leave_type == leave_type)
|
||||
& (Allocation.to_date < from_date)
|
||||
& (Allocation.docstatus == 1)
|
||||
)
|
||||
.orderby(Allocation.to_date, order=frappe.qb.desc)
|
||||
.limit(1)
|
||||
).run(as_dict=True)
|
||||
|
||||
return allocations[0] if allocations else None
|
||||
|
||||
|
||||
def get_leave_allocation_for_period(
|
||||
|
||||
@@ -706,19 +706,22 @@ def get_allocation_expiry_for_cf_leaves(
|
||||
employee: str, leave_type: str, to_date: str, from_date: str
|
||||
) -> str:
|
||||
"""Returns expiry of carry forward allocation in leave ledger entry"""
|
||||
expiry = frappe.get_all(
|
||||
"Leave Ledger Entry",
|
||||
filters={
|
||||
"employee": employee,
|
||||
"leave_type": leave_type,
|
||||
"is_carry_forward": 1,
|
||||
"transaction_type": "Leave Allocation",
|
||||
"to_date": ["between", (from_date, to_date)],
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["to_date"],
|
||||
)
|
||||
return expiry[0]["to_date"] if expiry else ""
|
||||
Ledger = frappe.qb.DocType("Leave Ledger Entry")
|
||||
expiry = (
|
||||
frappe.qb.from_(Ledger)
|
||||
.select(Ledger.to_date)
|
||||
.where(
|
||||
(Ledger.employee == employee)
|
||||
& (Ledger.leave_type == leave_type)
|
||||
& (Ledger.is_carry_forward == 1)
|
||||
& (Ledger.transaction_type == "Leave Allocation")
|
||||
& (Ledger.to_date.between(from_date, to_date))
|
||||
& (Ledger.docstatus == 1)
|
||||
)
|
||||
.limit(1)
|
||||
).run()
|
||||
|
||||
return expiry[0][0] if expiry else ""
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1017,7 +1020,7 @@ def get_leaves_for_period(
|
||||
if leave_entry.leaves % 1:
|
||||
half_day = 1
|
||||
half_day_date = frappe.db.get_value(
|
||||
"Leave Application", {"name": leave_entry.transaction_name}, ["half_day_date"]
|
||||
"Leave Application", leave_entry.transaction_name, "half_day_date"
|
||||
)
|
||||
|
||||
leave_days += (
|
||||
|
||||
@@ -713,25 +713,31 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
self.assertEqual(details.leave_balance, 30)
|
||||
|
||||
def test_earned_leaves_creation(self):
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
from erpnext.hr.doctype.leave_policy_assignment.test_leave_policy_assignment import (
|
||||
allocate_earned_leaves_for_months,
|
||||
)
|
||||
|
||||
leave_period = get_leave_period()
|
||||
year_start = get_year_start(getdate())
|
||||
year_end = get_year_ending(getdate())
|
||||
frappe.flags.current_date = year_start
|
||||
|
||||
leave_period = get_leave_period(year_start, year_end)
|
||||
employee = get_employee()
|
||||
leave_type = "Test Earned Leave Type"
|
||||
|
||||
make_policy_assignment(employee, leave_type, leave_period)
|
||||
|
||||
for i in range(0, 14):
|
||||
allocate_earned_leaves()
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||
# leaves for 6 months = 3, but max leaves restricts allocation to 2
|
||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 2)
|
||||
allocate_earned_leaves_for_months(6)
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, frappe.flags.current_date), 2)
|
||||
|
||||
# validate earned leaves creation without maximum leaves
|
||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||
allocate_earned_leaves_for_months(5)
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, frappe.flags.current_date), 4.5)
|
||||
|
||||
for i in range(0, 6):
|
||||
allocate_earned_leaves()
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||
frappe.flags.current_date = None
|
||||
|
||||
# test to not consider current leave in leave balance while submitting
|
||||
def test_current_leave_on_submit(self):
|
||||
@@ -1254,7 +1260,7 @@ def set_leave_approver():
|
||||
dept_doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def get_leave_period():
|
||||
def get_leave_period(from_date=None, to_date=None):
|
||||
leave_period_name = frappe.db.exists({"doctype": "Leave Period", "company": "_Test Company"})
|
||||
if leave_period_name:
|
||||
return frappe.get_doc("Leave Period", leave_period_name[0][0])
|
||||
@@ -1263,8 +1269,8 @@ def get_leave_period():
|
||||
dict(
|
||||
name="Test Leave Period",
|
||||
doctype="Leave Period",
|
||||
from_date=add_months(nowdate(), -6),
|
||||
to_date=add_months(nowdate(), 6),
|
||||
from_date=from_date or add_months(nowdate(), -6),
|
||||
to_date=to_date or add_months(nowdate(), 6),
|
||||
company="_Test Company",
|
||||
is_active=1,
|
||||
)
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Employee",
|
||||
"options": "Employee"
|
||||
"options": "Employee",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.employee_name",
|
||||
@@ -57,13 +58,15 @@
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Transaction Type",
|
||||
"options": "DocType"
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Transaction Name",
|
||||
"options": "transaction_type"
|
||||
"options": "transaction_type",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "leaves",
|
||||
@@ -123,7 +126,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-04 18:47:45.146652",
|
||||
"modified": "2023-11-17 12:36:36.963697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Ledger Entry",
|
||||
|
||||
@@ -225,3 +225,7 @@ def expire_carried_forward_allocation(allocation):
|
||||
to_date=allocation.to_date,
|
||||
)
|
||||
create_leave_ledger_entry(allocation, args)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Leave Ledger Entry", ["transaction_type", "transaction_name"])
|
||||
|
||||
@@ -100,7 +100,7 @@ class LeavePolicyAssignment(Document):
|
||||
return leave_allocations
|
||||
|
||||
def create_leave_allocation(
|
||||
self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining
|
||||
self, leave_type, annual_allocation, leave_type_details, date_of_joining
|
||||
):
|
||||
# Creates leave allocation for the given employee in the provided leave period
|
||||
carry_forward = self.carry_forward
|
||||
@@ -108,7 +108,7 @@ class LeavePolicyAssignment(Document):
|
||||
carry_forward = 0
|
||||
|
||||
new_leaves_allocated = self.get_new_leaves(
|
||||
leave_type, new_leaves_allocated, leave_type_details, date_of_joining
|
||||
leave_type, annual_allocation, leave_type_details, date_of_joining
|
||||
)
|
||||
|
||||
allocation = frappe.get_doc(
|
||||
@@ -129,7 +129,7 @@ class LeavePolicyAssignment(Document):
|
||||
allocation.submit()
|
||||
return allocation.name, new_leaves_allocated
|
||||
|
||||
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
|
||||
def get_new_leaves(self, leave_type, annual_allocation, leave_type_details, date_of_joining):
|
||||
from frappe.model.meta import get_field_precision
|
||||
|
||||
precision = get_field_precision(
|
||||
@@ -146,20 +146,27 @@ class LeavePolicyAssignment(Document):
|
||||
else:
|
||||
# get leaves for past months if assignment is based on Leave Period / Joining Date
|
||||
new_leaves_allocated = self.get_leaves_for_passed_months(
|
||||
leave_type, new_leaves_allocated, leave_type_details, date_of_joining
|
||||
leave_type, annual_allocation, leave_type_details, date_of_joining
|
||||
)
|
||||
|
||||
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
|
||||
elif getdate(date_of_joining) > getdate(self.effective_from):
|
||||
remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / (
|
||||
date_diff(self.effective_to, self.effective_from) + 1
|
||||
)
|
||||
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
|
||||
else:
|
||||
if getdate(date_of_joining) > getdate(self.effective_from):
|
||||
remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / (
|
||||
date_diff(self.effective_to, self.effective_from) + 1
|
||||
)
|
||||
new_leaves_allocated = ceil(annual_allocation * remaining_period)
|
||||
else:
|
||||
new_leaves_allocated = annual_allocation
|
||||
|
||||
# leave allocation should not exceed annual allocation as per policy assignment
|
||||
if new_leaves_allocated > annual_allocation:
|
||||
new_leaves_allocated = annual_allocation
|
||||
|
||||
return flt(new_leaves_allocated, precision)
|
||||
|
||||
def get_leaves_for_passed_months(
|
||||
self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining
|
||||
self, leave_type, annual_allocation, leave_type_details, date_of_joining
|
||||
):
|
||||
from erpnext.hr.utils import get_monthly_earned_leave
|
||||
|
||||
@@ -184,7 +191,7 @@ class LeavePolicyAssignment(Document):
|
||||
|
||||
if months_passed > 0:
|
||||
monthly_earned_leave = get_monthly_earned_leave(
|
||||
new_leaves_allocated,
|
||||
annual_allocation,
|
||||
leave_type_details.get(leave_type).earned_leave_frequency,
|
||||
leave_type_details.get(leave_type).rounding,
|
||||
)
|
||||
|
||||
@@ -5,8 +5,10 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
|
||||
from frappe.utils import add_days, add_months, get_first_day, get_last_day, get_year_start, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_employee,
|
||||
get_leave_period,
|
||||
@@ -15,6 +17,7 @@ from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_polic
|
||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
create_assignment_for_multiple_employees,
|
||||
)
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
test_dependencies = ["Employee"]
|
||||
|
||||
@@ -34,6 +37,8 @@ class TestLeavePolicyAssignment(FrappeTestCase):
|
||||
self.original_doj = employee.date_of_joining
|
||||
self.employee = employee
|
||||
|
||||
self.leave_type = "Test Earned Leave"
|
||||
|
||||
def test_grant_leaves(self):
|
||||
leave_period = get_leave_period()
|
||||
# allocation = 10
|
||||
@@ -326,6 +331,90 @@ class TestLeavePolicyAssignment(FrappeTestCase):
|
||||
self.assertEqual(effective_from, self.employee.date_of_joining)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_overallocation(self):
|
||||
"""Tests if earned leave allocation does not exceed annual allocation"""
|
||||
frappe.flags.current_date = get_year_start(getdate())
|
||||
make_policy_assignment(
|
||||
self.employee,
|
||||
annual_allocation=22,
|
||||
allocate_on_day="First Day",
|
||||
start_date=frappe.flags.current_date,
|
||||
)
|
||||
|
||||
# leaves for 12 months = 22
|
||||
# With rounding, 22 leaves would be allocated in 11 months only
|
||||
frappe.db.set_value("Leave Type", self.leave_type, "rounding", 1.0)
|
||||
allocate_earned_leaves_for_months(11)
|
||||
self.assertEqual(
|
||||
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 22
|
||||
)
|
||||
|
||||
# should not allocate more leaves than annual allocation
|
||||
allocate_earned_leaves_for_months(1)
|
||||
self.assertEqual(
|
||||
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 22
|
||||
)
|
||||
|
||||
def test_over_allocation_during_assignment_creation(self):
|
||||
"""Tests backdated earned leave allocation does not exceed annual allocation"""
|
||||
start_date = get_first_day(add_months(getdate(), -12))
|
||||
|
||||
# joining date set to 1Y ago
|
||||
self.employee.date_of_joining = start_date
|
||||
self.employee.save()
|
||||
|
||||
# create backdated assignment for last year
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
|
||||
leave_policy_assignments = make_policy_assignment(
|
||||
self.employee, start_date=start_date, allocate_on_day="Date of Joining"
|
||||
)
|
||||
|
||||
# 13 months have passed but annual allocation = 12
|
||||
# check annual allocation is not exceeded
|
||||
leaves_allocated = get_allocated_leaves(leave_policy_assignments[0])
|
||||
self.assertEqual(leaves_allocated, 12)
|
||||
|
||||
def test_overallocation_with_carry_forwarding(self):
|
||||
"""Tests earned leave allocation with cf leaves does not exceed annual allocation"""
|
||||
year_start = get_year_start(getdate())
|
||||
|
||||
# initial leave allocation = 5
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name,
|
||||
leave_type=self.leave_type,
|
||||
from_date=get_first_day(add_months(year_start, -1)),
|
||||
to_date=get_last_day(add_months(year_start, -1)),
|
||||
new_leaves_allocated=5,
|
||||
carry_forward=0,
|
||||
)
|
||||
leave_allocation.submit()
|
||||
|
||||
frappe.flags.current_date = year_start
|
||||
# carry forwarded leaves = 5
|
||||
make_policy_assignment(
|
||||
self.employee,
|
||||
annual_allocation=22,
|
||||
allocate_on_day="First Day",
|
||||
start_date=year_start,
|
||||
carry_forward=True,
|
||||
)
|
||||
|
||||
frappe.db.set_value("Leave Type", self.leave_type, "rounding", 1.0)
|
||||
allocate_earned_leaves_for_months(11)
|
||||
|
||||
# 5 carry forwarded leaves + 22 EL allocated = 27 leaves
|
||||
self.assertEqual(
|
||||
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 27
|
||||
)
|
||||
|
||||
# should not allocate more leaves than annual allocation (22 excluding 5 cf leaves)
|
||||
allocate_earned_leaves_for_months(1)
|
||||
self.assertEqual(
|
||||
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 27
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
|
||||
frappe.flags.current_date = None
|
||||
@@ -376,3 +465,51 @@ def setup_leave_period_and_policy(start_date, based_on_doj=False):
|
||||
).insert()
|
||||
|
||||
return leave_period, leave_policy
|
||||
|
||||
|
||||
def make_policy_assignment(
|
||||
employee,
|
||||
allocate_on_day="Last Day",
|
||||
earned_leave_frequency="Monthly",
|
||||
start_date=None,
|
||||
annual_allocation=12,
|
||||
carry_forward=0,
|
||||
assignment_based_on="Leave Period",
|
||||
):
|
||||
leave_type = create_earned_leave_type("Test Earned Leave", allocate_on_day)
|
||||
leave_period = create_leave_period("Test Earned Leave Period", start_date=start_date)
|
||||
leave_policy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Earned Leave Policy",
|
||||
"leave_policy_details": [
|
||||
{"leave_type": leave_type.name, "annual_allocation": annual_allocation}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": assignment_based_on,
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name,
|
||||
"carry_forward": carry_forward,
|
||||
}
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees(
|
||||
[employee.name], frappe._dict(data)
|
||||
)
|
||||
return leave_policy_assignments
|
||||
|
||||
|
||||
def get_allocated_leaves(assignment):
|
||||
return frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{"leave_policy_assignment": assignment},
|
||||
"total_leaves_allocated",
|
||||
)
|
||||
|
||||
|
||||
def allocate_earned_leaves_for_months(months):
|
||||
for i in range(0, months):
|
||||
frappe.flags.current_date = add_months(frappe.flags.current_date, 1)
|
||||
allocate_earned_leaves()
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-02-22 15:29:34",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:18:04.317397",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Leave Balance",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Employee",
|
||||
"report_name": "Employee Leave Balance",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-02-22 15:29:34",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2023-11-17 13:28:40.669200",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Leave Balance",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Employee",
|
||||
"report_name": "Employee Leave Balance",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "HR User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "HR Manager"
|
||||
},
|
||||
{
|
||||
"role": "Employee"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -85,19 +85,10 @@ def get_columns() -> List[Dict]:
|
||||
|
||||
|
||||
def get_data(filters: Filters) -> List:
|
||||
leave_types = frappe.db.get_list("Leave Type", pluck="name", order_by="name")
|
||||
conditions = get_conditions(filters)
|
||||
leave_types = get_leave_types()
|
||||
active_employees = get_employees(filters)
|
||||
|
||||
user = frappe.session.user
|
||||
department_approver_map = get_department_leave_approver_map(filters.department)
|
||||
|
||||
active_employees = frappe.get_list(
|
||||
"Employee",
|
||||
filters=conditions,
|
||||
fields=["name", "employee_name", "department", "user_id", "leave_approver"],
|
||||
)
|
||||
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||
consolidate_leave_types = len(active_employees) > 1 and filters.consolidate_leave_types
|
||||
row = None
|
||||
|
||||
@@ -110,10 +101,6 @@ def get_data(filters: Filters) -> List:
|
||||
row = frappe._dict({"leave_type": leave_type})
|
||||
|
||||
for employee in active_employees:
|
||||
leave_approvers = department_approver_map.get(employee.department_name, []).append(
|
||||
employee.leave_approver
|
||||
)
|
||||
|
||||
if consolidate_leave_types:
|
||||
row = frappe._dict()
|
||||
else:
|
||||
@@ -144,6 +131,35 @@ def get_data(filters: Filters) -> List:
|
||||
return data
|
||||
|
||||
|
||||
def get_leave_types() -> List[str]:
|
||||
LeaveType = frappe.qb.DocType("Leave Type")
|
||||
leave_types = (frappe.qb.from_(LeaveType).select(LeaveType.name).orderby(LeaveType.name)).run(
|
||||
as_dict=True
|
||||
)
|
||||
return [leave_type.name for leave_type in leave_types]
|
||||
|
||||
|
||||
def get_employees(filters: Filters) -> List[Dict]:
|
||||
Employee = frappe.qb.DocType("Employee")
|
||||
query = frappe.qb.from_(Employee).select(
|
||||
Employee.name,
|
||||
Employee.employee_name,
|
||||
Employee.department,
|
||||
)
|
||||
|
||||
for field in ["company", "department"]:
|
||||
if filters.get(field):
|
||||
query = query.where((getattr(Employee, field) == filters.get(field)))
|
||||
|
||||
if filters.get("employee"):
|
||||
query = query.where(Employee.name == filters.get("employee"))
|
||||
|
||||
if filters.get("employee_status"):
|
||||
query = query.where(Employee.status == filters.get("employee_status"))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_opening_balance(
|
||||
employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float
|
||||
) -> float:
|
||||
@@ -168,48 +184,6 @@ def get_opening_balance(
|
||||
return opening_balance
|
||||
|
||||
|
||||
def get_conditions(filters: Filters) -> Dict:
|
||||
conditions = {}
|
||||
|
||||
if filters.employee:
|
||||
conditions["name"] = filters.employee
|
||||
|
||||
if filters.company:
|
||||
conditions["company"] = filters.company
|
||||
|
||||
if filters.department:
|
||||
conditions["department"] = filters.department
|
||||
|
||||
if filters.employee_status:
|
||||
conditions["status"] = filters.employee_status
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_department_leave_approver_map(department: Optional[str] = None):
|
||||
# get current department and all its child
|
||||
department_list = frappe.get_list(
|
||||
"Department",
|
||||
filters={"disabled": 0},
|
||||
or_filters={"name": department, "parent_department": department},
|
||||
pluck="name",
|
||||
)
|
||||
# retrieve approvers list from current department and from its subsequent child departments
|
||||
approver_list = frappe.get_all(
|
||||
"Department Approver",
|
||||
filters={"parentfield": "leave_approvers", "parent": ("in", department_list)},
|
||||
fields=["parent", "approver"],
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
approvers = {}
|
||||
|
||||
for k, v in approver_list:
|
||||
approvers.setdefault(k, []).append(v)
|
||||
|
||||
return approvers
|
||||
|
||||
|
||||
def get_allocated_and_expired_leaves(
|
||||
from_date: str, to_date: str, employee: str, leave_type: str
|
||||
) -> Tuple[float, float, float]:
|
||||
@@ -244,7 +218,7 @@ def get_leave_ledger_entries(
|
||||
from_date: str, to_date: str, employee: str, leave_type: str
|
||||
) -> List[Dict]:
|
||||
ledger = frappe.qb.DocType("Leave Ledger Entry")
|
||||
records = (
|
||||
return (
|
||||
frappe.qb.from_(ledger)
|
||||
.select(
|
||||
ledger.employee,
|
||||
@@ -270,8 +244,6 @@ def get_leave_ledger_entries(
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def get_chart_data(data: List, filters: Filters) -> Dict:
|
||||
labels = []
|
||||
|
||||
@@ -6,9 +6,6 @@ import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_leave_details
|
||||
from erpnext.hr.report.employee_leave_balance.employee_leave_balance import (
|
||||
get_department_leave_approver_map,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -54,17 +51,11 @@ def get_data(filters, leave_types):
|
||||
active_employees = frappe.get_all(
|
||||
"Employee",
|
||||
filters=conditions,
|
||||
fields=["name", "employee_name", "department", "user_id", "leave_approver"],
|
||||
fields=["name", "employee_name", "department", "user_id"],
|
||||
)
|
||||
|
||||
department_approver_map = get_department_leave_approver_map(filters.get("department"))
|
||||
|
||||
data = []
|
||||
for employee in active_employees:
|
||||
leave_approvers = department_approver_map.get(employee.department_name, [])
|
||||
if employee.leave_approver:
|
||||
leave_approvers.append(employee.leave_approver)
|
||||
|
||||
row = [employee.name, employee.employee_name, employee.department]
|
||||
available_leave = get_leave_details(employee.name, filters.date)
|
||||
for leave_type in leave_types:
|
||||
|
||||
@@ -459,7 +459,7 @@ def generate_leave_encashment():
|
||||
def allocate_earned_leaves():
|
||||
"""Allocate earned leaves to Employees"""
|
||||
e_leave_types = get_earned_leaves()
|
||||
today = getdate()
|
||||
today = frappe.flags.current_date or getdate()
|
||||
|
||||
for e_leave_type in e_leave_types:
|
||||
|
||||
@@ -496,18 +496,28 @@ def allocate_earned_leaves():
|
||||
|
||||
|
||||
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
|
||||
allocation = frappe.get_doc("Leave Allocation", allocation.name)
|
||||
annual_allocation = flt(annual_allocation, allocation.precision("total_leaves_allocated"))
|
||||
|
||||
earned_leaves = get_monthly_earned_leave(
|
||||
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
|
||||
)
|
||||
|
||||
allocation = frappe.get_doc("Leave Allocation", allocation.name)
|
||||
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
|
||||
new_allocation_without_cf = flt(
|
||||
flt(allocation.get_existing_leave_count()) + flt(earned_leaves),
|
||||
allocation.precision("total_leaves_allocated"),
|
||||
)
|
||||
|
||||
if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
|
||||
new_allocation = e_leave_type.max_leaves_allowed
|
||||
|
||||
if new_allocation != allocation.total_leaves_allocated:
|
||||
today_date = today()
|
||||
if (
|
||||
new_allocation != allocation.total_leaves_allocated
|
||||
# annual allocation as per policy should not be exceeded
|
||||
and new_allocation_without_cf <= annual_allocation
|
||||
):
|
||||
today_date = frappe.flags.current_date or getdate()
|
||||
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
|
||||
@@ -306,7 +306,7 @@ def get_last_accrual_date(loan, posting_date):
|
||||
def get_last_disbursement_date(loan, posting_date):
|
||||
last_disbursement_date = frappe.db.get_value(
|
||||
"Loan Disbursement",
|
||||
{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
|
||||
{"docstatus": 1, "against_loan": loan, "posting_date": ("<=", posting_date)},
|
||||
"MAX(posting_date)",
|
||||
)
|
||||
|
||||
|
||||
@@ -3,23 +3,27 @@ import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("stock", "doctype", "quality_inspection_parameter")
|
||||
params = set()
|
||||
|
||||
# get all distinct parameters from QI readigs table
|
||||
reading_params = frappe.db.get_all(
|
||||
"Quality Inspection Reading", fields=["distinct specification"]
|
||||
)
|
||||
reading_params = [d.specification for d in reading_params]
|
||||
# get all parameters from QI readings table
|
||||
for (p,) in frappe.db.get_all(
|
||||
"Quality Inspection Reading", fields=["specification"], as_list=True
|
||||
):
|
||||
params.add(p.strip())
|
||||
|
||||
# get all distinct parameters from QI Template as some may be unused in QI
|
||||
template_params = frappe.db.get_all(
|
||||
"Item Quality Inspection Parameter", fields=["distinct specification"]
|
||||
)
|
||||
template_params = [d.specification for d in template_params]
|
||||
# get all parameters from QI Template as some may be unused in QI
|
||||
for (p,) in frappe.db.get_all(
|
||||
"Item Quality Inspection Parameter", fields=["specification"], as_list=True
|
||||
):
|
||||
params.add(p.strip())
|
||||
|
||||
params = list(set(reading_params + template_params))
|
||||
# because db primary keys are case insensitive, so duplicates will cause an exception
|
||||
params = set({x.casefold(): x for x in params}.values())
|
||||
|
||||
for parameter in params:
|
||||
if not frappe.db.exists("Quality Inspection Parameter", parameter):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
|
||||
).insert(ignore_permissions=True)
|
||||
if frappe.db.exists("Quality Inspection Parameter", parameter):
|
||||
continue
|
||||
|
||||
frappe.get_doc(
|
||||
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
@@ -125,6 +125,7 @@ def execute():
|
||||
loan_type_doc.company = loan.company
|
||||
loan_type_doc.mode_of_payment = loan.mode_of_payment
|
||||
loan_type_doc.payment_account = loan.payment_account
|
||||
loan_type_doc.disbursement_account = loan.payment_account
|
||||
loan_type_doc.loan_account = loan.loan_account
|
||||
loan_type_doc.interest_income_account = loan.interest_income_account
|
||||
loan_type_doc.penalty_income_account = penalty_account
|
||||
|
||||
@@ -71,6 +71,12 @@ class Timesheet(Document):
|
||||
if args.is_billable:
|
||||
if flt(args.billing_hours) == 0.0:
|
||||
args.billing_hours = args.hours
|
||||
elif flt(args.billing_hours) > flt(args.hours):
|
||||
frappe.msgprint(
|
||||
_("Warning - Row {0}: Billing Hours are more than Actual Hours").format(args.idx),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
args.billing_hours = 0
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", {
|
||||
frappe.confirm(__(confirm_msg, [__("Issue")]), () => {
|
||||
frm.trigger('make_issue_from_communication');
|
||||
})
|
||||
}, "Create");
|
||||
}, __("Create"));
|
||||
}
|
||||
|
||||
if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) {
|
||||
|
||||
@@ -1378,7 +1378,7 @@ class GSPConnector:
|
||||
|
||||
def set_einvoice_data(self, res):
|
||||
enc_signed_invoice = res.get("SignedInvoice")
|
||||
dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)["data"]
|
||||
dec_signed_invoice = jwt.decode(enc_signed_invoice, options={"verify_signature": False})["data"]
|
||||
|
||||
self.invoice.irn = res.get("Irn")
|
||||
self.invoice.ewaybill = res.get("EwbNo")
|
||||
|
||||
@@ -532,6 +532,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
|
||||
primary_action={
|
||||
"label": "Send Email",
|
||||
"server_action": "erpnext.selling.doctype.customer.customer.send_emails",
|
||||
"hide_on_success": True,
|
||||
"args": {
|
||||
"customer": customer,
|
||||
"customer_outstanding": customer_outstanding,
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_price_list_rate_for
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -50,6 +50,42 @@ def get_columns(filters=None):
|
||||
]
|
||||
|
||||
|
||||
def fetch_item_prices(
|
||||
customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None
|
||||
):
|
||||
price_list_map = frappe._dict()
|
||||
ip = qb.DocType("Item Price")
|
||||
and_conditions = []
|
||||
or_conditions = []
|
||||
if items:
|
||||
and_conditions.append(ip.item_code.isin([x.item_code for x in items]))
|
||||
and_conditions.append(ip.selling == True)
|
||||
|
||||
or_conditions.append(ip.customer == None)
|
||||
or_conditions.append(ip.price_list == None)
|
||||
|
||||
if customer:
|
||||
or_conditions.append(ip.customer == customer)
|
||||
|
||||
if price_list:
|
||||
or_conditions.append(ip.price_list == price_list)
|
||||
|
||||
if selling_price_list:
|
||||
or_conditions.append(ip.price_list == selling_price_list)
|
||||
|
||||
res = (
|
||||
qb.from_(ip)
|
||||
.select(ip.item_code, ip.price_list, ip.price_list_rate)
|
||||
.where(Criterion.all(and_conditions))
|
||||
.where(Criterion.any(or_conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for x in res:
|
||||
price_list_map.update({(x.item_code, x.price_list): x.price_list_rate})
|
||||
|
||||
return price_list_map
|
||||
|
||||
|
||||
def get_data(filters=None):
|
||||
data = []
|
||||
customer_details = get_customer_details(filters)
|
||||
@@ -59,9 +95,17 @@ def get_data(filters=None):
|
||||
"Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code"
|
||||
)
|
||||
item_stock_map = {item.item_code: item.available for item in item_stock_map}
|
||||
price_list_map = fetch_item_prices(
|
||||
customer_details.customer,
|
||||
customer_details.price_list,
|
||||
customer_details.selling_price_list,
|
||||
items,
|
||||
)
|
||||
|
||||
for item in items:
|
||||
price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0
|
||||
price_list_rate = price_list_map.get(
|
||||
(item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0
|
||||
)
|
||||
available_stock = item_stock_map.get(item.item_code)
|
||||
|
||||
data.append(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
def get_leaderboards():
|
||||
@@ -54,12 +53,13 @@ def get_leaderboards():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_customers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Invoice",
|
||||
fields=["customer as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@@ -69,26 +69,20 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(so_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(so_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "so.transaction_date")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select so.customer as name, {0} as value
|
||||
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item
|
||||
ON so.name = so_item.parent
|
||||
where so.docstatus = 1 {1} and so.company = %s
|
||||
group by so.customer
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=["customer as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="customer",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,55 +90,58 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
def get_all_items(date_range, company, field, limit=None):
|
||||
if field in ("available_stock_qty", "available_stock_value"):
|
||||
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
|
||||
return frappe.db.get_all(
|
||||
results = frappe.db.get_all(
|
||||
"Bin",
|
||||
fields=["item_code as name", "{0} as value".format(select_field)],
|
||||
group_by="item_code",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
|
||||
return [item for item in results if item["name"] in readable_active_items]
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_purchase_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Purchase Order"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Purchase Order"
|
||||
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select order_item.item_code as name, {0} as value
|
||||
from `tab{1}` sales_order join `tab{1} Item` as order_item
|
||||
on sales_order.name = order_item.parent
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s {2}
|
||||
group by order_item.item_code
|
||||
order by value desc
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, select_doctype, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
child_doctype = f"{select_doctype} Item"
|
||||
return frappe.get_list(
|
||||
select_doctype,
|
||||
fields=[
|
||||
f"`tab{child_doctype}`.item_code as name",
|
||||
f"sum(`tab{child_doctype}`.{select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
order_by="value desc",
|
||||
group_by=f"`tab{child_doctype}`.item_code",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_suppliers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Purchase Invoice",
|
||||
fields=["supplier as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@@ -154,48 +151,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_purchase_amount":
|
||||
select_field = "sum(purchase_order_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(purchase_order_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "purchase_order.modified")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select purchase_order.supplier as name, {0} as value
|
||||
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item`
|
||||
as purchase_order_item ON purchase_order.name = purchase_order_item.parent
|
||||
where
|
||||
purchase_order.docstatus = 1
|
||||
{1}
|
||||
and purchase_order.company = %s
|
||||
group by purchase_order.supplier
|
||||
order by value DESC
|
||||
limit %s""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
return frappe.get_list(
|
||||
"Purchase Order",
|
||||
fields=["supplier as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="supplier",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(`base_net_total`)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_commission":
|
||||
select_field = "sum(`total_commission`)"
|
||||
select_field = "total_commission"
|
||||
|
||||
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company}
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]]
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`sales_partner` as name",
|
||||
"{} as value".format(select_field),
|
||||
"sales_partner as name",
|
||||
f"sum({select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="sales_partner",
|
||||
@@ -206,24 +195,25 @@ def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_person(date_range, company, field=None, limit=0):
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [
|
||||
["docstatus", "=", "1"],
|
||||
["company", "=", company],
|
||||
["Sales Team", "sales_person", "is", "set"],
|
||||
]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value
|
||||
from `tabSales Order` as sales_order join `tabSales Team` as sales_team
|
||||
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order'
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s
|
||||
{date_condition}
|
||||
group by sales_team.sales_person
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
date_condition=date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`tabSales Team`.sales_person as name",
|
||||
"sum(`tabSales Team`.allocated_amount) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="`tabSales Team`.sales_person",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -236,3 +226,11 @@ def get_date_condition(date_range, field):
|
||||
field, frappe.db.escape(from_date), frappe.db.escape(to_date)
|
||||
)
|
||||
return date_condition
|
||||
|
||||
|
||||
def parse_date_range(date_range):
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
return date_range[0], date_range[1]
|
||||
|
||||
return None, None
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import nowdate
|
||||
from frappe.utils.data import add_to_date, today
|
||||
|
||||
@@ -173,6 +173,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
riv.set_status("Skipped")
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_prevention_of_cancelled_transaction_riv(self):
|
||||
frappe.flags.dont_execute_stock_reposts = True
|
||||
|
||||
@@ -295,6 +296,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
||||
accounts_settings.acc_frozen_upto = ""
|
||||
accounts_settings.save()
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_create_repost_entry_for_cancelled_document(self):
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company with perpetual inventory",
|
||||
|
||||
@@ -1414,6 +1414,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertEqual(se.items[0].item_name, item.item_name)
|
||||
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_reposting_for_depedent_warehouse(self):
|
||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"label": "Limit timeslot for Stock Reposting"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "1",
|
||||
"fieldname": "item_based_reposting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Item based reposting"
|
||||
@@ -57,7 +57,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-02 01:22:45.155841",
|
||||
"modified": "2023-11-01 16:14:29.080697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
@@ -77,4 +77,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,18 @@ const DIFFERENCE_FIELD_NAMES = [
|
||||
|
||||
frappe.query_reports["Stock Ledger Variance"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"default": frappe.defaults.get_user_default("Company")
|
||||
},
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"label": __("Item"),
|
||||
"options": "Item",
|
||||
get_query: function() {
|
||||
return {
|
||||
@@ -27,7 +35,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"label": __("Warehouse"),
|
||||
"options": "Warehouse",
|
||||
get_query: function() {
|
||||
return {
|
||||
@@ -38,7 +46,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
|
||||
{
|
||||
"fieldname": "difference_in",
|
||||
"fieldtype": "Select",
|
||||
"label": "Difference In",
|
||||
"label": __("Difference In"),
|
||||
"options": [
|
||||
"",
|
||||
"Qty",
|
||||
@@ -49,7 +57,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
|
||||
{
|
||||
"fieldname": "include_disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include Disabled",
|
||||
"label": __("Include Disabled"),
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@ def get_columns():
|
||||
"label": _("Warehouse"),
|
||||
"options": "Warehouse",
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_method",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Valuation Method"),
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
@@ -194,6 +199,7 @@ def get_columns():
|
||||
def get_data(filters=None):
|
||||
filters = frappe._dict(filters or {})
|
||||
item_warehouse_map = get_item_warehouse_combinations(filters)
|
||||
valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
|
||||
|
||||
data = []
|
||||
if item_warehouse_map:
|
||||
@@ -206,8 +212,17 @@ def get_data(filters=None):
|
||||
continue
|
||||
|
||||
for row in report_data:
|
||||
if has_difference(row, precision, filters.difference_in):
|
||||
data.append(add_item_warehouse_details(row, item_warehouse))
|
||||
if has_difference(
|
||||
row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method
|
||||
):
|
||||
row.update(
|
||||
{
|
||||
"item_code": item_warehouse.item_code,
|
||||
"warehouse": item_warehouse.warehouse,
|
||||
"valuation_method": item_warehouse.valuation_method or valuation_method,
|
||||
}
|
||||
)
|
||||
data.append(row)
|
||||
break
|
||||
|
||||
return data
|
||||
@@ -229,8 +244,14 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict:
|
||||
.select(
|
||||
bin.item_code,
|
||||
bin.warehouse,
|
||||
item.valuation_method,
|
||||
)
|
||||
.where(
|
||||
(item.is_stock_item == 1)
|
||||
& (item.has_serial_no == 0)
|
||||
& (warehouse.is_group == 0)
|
||||
& (warehouse.company == filters.company)
|
||||
)
|
||||
.where((item.is_stock_item == 1) & (item.has_serial_no == 0) & (warehouse.is_group == 0))
|
||||
)
|
||||
|
||||
if filters.item_code:
|
||||
@@ -243,37 +264,27 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict:
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def has_difference(row, precision, difference_in):
|
||||
has_qty_difference = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
|
||||
has_value_difference = (
|
||||
flt(row.diff_value_diff, precision)
|
||||
or flt(row.fifo_value_diff, precision)
|
||||
or flt(row.fifo_difference_diff, precision)
|
||||
)
|
||||
has_valuation_difference = flt(row.valuation_diff, precision) or flt(
|
||||
row.fifo_valuation_diff, precision
|
||||
)
|
||||
def has_difference(row, precision, difference_in, valuation_method):
|
||||
if valuation_method == "Moving Average":
|
||||
qty_diff = flt(row.difference_in_qty, precision)
|
||||
value_diff = flt(row.diff_value_diff, precision)
|
||||
valuation_diff = flt(row.valuation_diff, precision)
|
||||
else:
|
||||
qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
|
||||
value_diff = (
|
||||
flt(row.diff_value_diff, precision)
|
||||
or flt(row.fifo_value_diff, precision)
|
||||
or flt(row.fifo_difference_diff, precision)
|
||||
)
|
||||
valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision)
|
||||
|
||||
if difference_in == "Qty" and has_qty_difference:
|
||||
if difference_in == "Qty" and qty_diff:
|
||||
return True
|
||||
elif difference_in == "Value" and has_value_difference:
|
||||
elif difference_in == "Value" and value_diff:
|
||||
return True
|
||||
elif difference_in == "Valuation" and has_valuation_difference:
|
||||
elif difference_in == "Valuation" and valuation_diff:
|
||||
return True
|
||||
elif difference_in not in ["Qty", "Value", "Valuation"] and (
|
||||
has_qty_difference or has_value_difference or has_valuation_difference
|
||||
qty_diff or value_diff or valuation_diff
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def add_item_warehouse_details(row, item_warehouse):
|
||||
row.update(
|
||||
{
|
||||
"item_code": item_warehouse.item_code,
|
||||
"warehouse": item_warehouse.warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
return row
|
||||
|
||||
@@ -4,7 +4,7 @@ googlemaps # used in ERPNext, but dependency is defined in Frappe
|
||||
pandas>=1.1.5,<2.0.0
|
||||
plaid-python~=7.2.1
|
||||
pycountry~=20.7.3
|
||||
PyGithub~=1.54.1
|
||||
PyGithub~=2.1.1
|
||||
python-stdnum~=1.16
|
||||
python-youtube~=0.8.0
|
||||
taxjar~=1.9.2
|
||||
|
||||
Reference in New Issue
Block a user