Compare commits

..

24 Commits

Author SHA1 Message Date
Frappe PR Bot
9a659254e3 chore(release): Bumped to Version 13.51.2
## [13.51.2](https://github.com/frappe/erpnext/compare/v13.51.1...v13.51.2) (2023-06-14)

### Bug Fixes

* **accounts:** validate payment entry references with latest data. (backport [#31166](https://github.com/frappe/erpnext/issues/31166)) ([#35674](https://github.com/frappe/erpnext/issues/35674)) ([4d4f218](4d4f218175))
* Asset Depreciation Ledger Report - Add Total Row Checkbox Enabled ([3831c79](3831c7920d))
* calculate wdv depr schedule properly for existing assets [v13] ([#35615](https://github.com/frappe/erpnext/issues/35615)) ([97f4af8](97f4af8d97))
* CSS not applied to product title (backport [#35582](https://github.com/frappe/erpnext/issues/35582)) ([#35635](https://github.com/frappe/erpnext/issues/35635)) ([1b69b37](1b69b37229))
* don't set default payment amount in case of invoice return (backport [#35645](https://github.com/frappe/erpnext/issues/35645)) ([#35648](https://github.com/frappe/erpnext/issues/35648)) ([8e3636f](8e3636ff53))
* Lower deduction certificate not getting applied ([#35667](https://github.com/frappe/erpnext/issues/35667)) ([c2bf8e3](c2bf8e3502))
* make showing taxes as table in print configurable ([#35672](https://github.com/frappe/erpnext/issues/35672)) ([4c2c037](4c2c037a86))
* Project in item-wise sales register ([#35596](https://github.com/frappe/erpnext/issues/35596)) ([9d5b500](9d5b500060))
* savepoint policy assignment submission, log errors & inform the user about failures ([#35507](https://github.com/frappe/erpnext/issues/35507)) ([4a35ff0](4a35ff0e57))

### Performance Improvements

* refactor `get_all_nodes` in Org Chart ([986a90e](986a90efe0))
2023-06-14 06:09:31 +00:00
Deepesh Garg
e75ca14a88 Merge pull request #35665 from frappe/version-13-hotfix
chore: release v13
2023-06-14 11:37:59 +05:30
mergify[bot]
c2bf8e3502 fix: Lower deduction certificate not getting applied (#35667)
* fix: Lower deduction certificate not getting applied (#35667)

(cherry picked from commit 937c0feefe)

# Conflicts:
#	erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py

* chore: Resolve conflicts

---------

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-06-14 09:01:54 +05:30
mergify[bot]
4d4f218175 fix(accounts): validate payment entry references with latest data. (backport #31166) (#35674)
* fix(accounts): validate payment entry references with latest data. (#31166)

* test: payment entry over allocation.

* fix: validate allocated_amount against latest outstanding amount.

* fix: payment entry get outstanding documents for advance payments

* fix: only fetch latest outstanding_amount.

* fix: throw if reference is allocated

* test: throw error if a reference has been partially allocated after inital creation.

* chore: test name

* fix: remove unused part of test

* chore: linter

* chore: more user friendly error messages

* fix: only validate outstanding amount if partly paid and don't filter by cost center

* chore: minor refactor for doc.cost_center

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
(cherry picked from commit 20de27d480)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/test_payment_entry.py

* chore: resolve conflicts

* chore: resolve more conflicts

* chore: don't validate allocated amount in case of donation

---------

Co-authored-by: Devin Slauenwhite <devin.slauenwhite@gmail.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-06-13 23:01:51 +05:30
mergify[bot]
4c2c037a86 fix: make showing taxes as table in print configurable (#35672)
* fix: make showing taxes as table in print configurable (#35672)

(cherry picked from commit 491a50a027)

# Conflicts:
#	erpnext/accounts/doctype/accounts_settings/accounts_settings.json

* chore: fixing conflicts

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-06-13 21:38:16 +05:30
mergify[bot]
4f79214ae6 Stock aging report fix when called in dashboard chart (backport #35671) (#35676)
fix: get_range_age conditions fixed (#35671)

see https://github.com/frappe/erpnext/issues/35669

(cherry picked from commit 9f669d4c2f)

Co-authored-by: Hossein Yousefian <86075967+ihosseinu@users.noreply.github.com>
2023-06-13 19:24:21 +05:30
Rucha Mahabal
5b37abd2d6 Merge pull request #35663 from ruchamahabal/perf-org-charts-v13 2023-06-13 13:20:09 +05:30
Rucha Mahabal
a24d488817 test: get children for org chart 2023-06-13 12:29:53 +05:30
Rucha Mahabal
986a90efe0 perf: refactor get_all_nodes in Org Chart 2023-06-13 12:07:15 +05:30
Rucha Mahabal
4a35ff0e57 fix: savepoint policy assignment submission, log errors & inform the user about failures (#35507) 2023-06-13 12:03:55 +05:30
mergify[bot]
8e3636ff53 fix: don't set default payment amount in case of invoice return (backport #35645) (#35648)
* fix: don't set default payment amount in case of invoice return (#35645)

(cherry picked from commit 79483cc90e)

# Conflicts:
#	erpnext/public/js/controllers/taxes_and_totals.js

* chore: fixing conflict

---------

Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
2023-06-12 18:44:56 +05:30
Trusted Computer
1b69b37229 fix: CSS not applied to product title (backport #35582) (#35635)
In an erpnext website, the /all-products route shows website items that have been published to the web site.

In the list view (erpnext/e_commerce/product_ui/list.js), the css class is null for the product title. Instead, inline style statements have been added in that can not be modified by overriding CSS.

This fix uses a similar approach to that which is taken in the grid view (erpnext/e_commerce/product_ui/grid.js). It removes the null CSS parameter in the product title link as well as the inline style statement. Then, as in the grid view, the product title is wrapped in a div tag with the product_title CSS class.

This makes it possible to style the product title as desired with a CSS override.
2023-06-12 14:39:41 +05:30
Anand Baburajan
97f4af8d97 fix: calculate wdv depr schedule properly for existing assets [v13] (#35615)
* fix: calculate wdv depr schedule properly for existing assets

* fix: calculate wdv depr schedule properly for existing assets properly
2023-06-08 23:16:35 +05:30
mergify[bot]
9d5b500060 fix: Project in item-wise sales register (#35596)
fix: Project in item-wise sales register (#35596)

(cherry picked from commit f732cac678)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-06-07 22:36:49 +05:30
AJAY NATARAJAN
3831c7920d fix: Asset Depreciation Ledger Report - Add Total Row Checkbox Enabled 2023-06-07 21:48:27 +05:30
Frappe PR Bot
f182fc1f8e chore(release): Bumped to Version 13.51.1
## [13.51.1](https://github.com/frappe/erpnext/compare/v13.51.0...v13.51.1) (2023-06-07)

### Bug Fixes

* Interest Accrual on Loan Topup ([#35555](https://github.com/frappe/erpnext/issues/35555)) ([1415f40](1415f40dfb))
* Task gantt popup style ([5544801](55448017d7))
2023-06-07 06:18:08 +00:00
Deepesh Garg
1897d6f214 Merge pull request #35570 from frappe/version-13-hotfix
chore: release v13
2023-06-07 11:46:43 +05:30
mergify[bot]
1415f40dfb fix: Interest Accrual on Loan Topup (#35555)
fix: Interest Accrual on Loan Topup (#35555)

* fix: Interest Accrual on Loan Topup

* chore: CI

* chore: Ignore test

(cherry picked from commit 2ffcca6f10)

Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
2023-06-07 10:11:54 +05:30
Suraj Shetty
09cf050b0d Merge pull request #35532 from frappe/mergify/bp/version-13-hotfix/pr-35530
fix: Task gantt popup style and info (backport #35530)
2023-06-02 11:11:35 +05:30
Suraj Shetty
55448017d7 fix: Task gantt popup style
(cherry picked from commit f7b2d103e7)
2023-06-02 05:39:53 +00:00
ruthra kumar
202513ae6a Merge pull request #35520 from frappe/mergify/bp/version-13-hotfix/pr-35112
refactor(Gross Profit): simplify group by invoice logic (backport #35112)
2023-06-01 15:13:02 +05:30
ruthra kumar
e3a8a8d195 refactor: simplify group by invoice logic
(cherry picked from commit 092c4b4c58)
2023-06-01 09:05:35 +00:00
Sagar Vora
169af8f9f8 Merge pull request #35502 from frappe/mergify/bp/version-13-hotfix/pr-35500 2023-05-31 14:46:24 +05:30
Sagar Vora
f0580b0e4d chore: remove whitelisting for method not accessed from UI
(cherry picked from commit 517d8a03ec)
2023-05-31 09:15:46 +00:00
26 changed files with 386 additions and 145 deletions

View File

@@ -4,7 +4,7 @@ import frappe
from erpnext.hooks import regional_overrides
__version__ = "13.51.0"
__version__ = "13.51.2"
def get_default_company(user=None):

View File

@@ -40,6 +40,7 @@
"submit_journal_entries",
"print_settings",
"show_inclusive_tax_in_print",
"show_taxes_as_table_in_print",
"column_break_12",
"show_payment_schedule_in_print",
"currency_exchange_section",
@@ -293,6 +294,12 @@
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
},
{
"default": "0",
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
}
],
"icon": "icon-cog",
@@ -300,7 +307,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2023-04-14 17:22:03.680886",
"modified": "2023-06-13 18:47:46.430291",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -150,19 +150,57 @@ class PaymentEntry(AccountsController):
)
def validate_allocated_amount(self):
for d in self.get("references"):
if self.payment_type == "Internal Transfer" or self.party_type in ("Donor"):
return
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,
}
)
# 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").copy():
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 d.outstanding_amount != latest.outstanding_amount
):
frappe.throw(
_(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' button to get the latest outstanding amount."
).format(d.reference_doctype, d.reference_name)
)
d.outstanding_amount = latest.outstanding_amount
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
if (flt(d.allocated_amount)) > 0:
if flt(d.allocated_amount) > flt(d.outstanding_amount):
frappe.throw(
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
)
frappe.throw(fail_message.format(d.idx))
# Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0:
if flt(d.allocated_amount) < flt(d.outstanding_amount):
frappe.throw(
_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
)
frappe.throw(fail_message.format(d.idx))
def delink_advance_entry_references(self):
for reference in self.references:
@@ -391,7 +429,7 @@ class PaymentEntry(AccountsController):
for k, v in no_oustanding_refs.items():
frappe.msgprint(
_(
"{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry."
"{} - {} now has {} as it had no outstanding amount left before submitting the Payment Entry."
).format(
_(k),
frappe.bold(", ".join(d.reference_name for d in v)),
@@ -1457,7 +1495,7 @@ def get_orders_to_be_billed(
if voucher_type:
doc = frappe.get_doc({"doctype": voucher_type})
condition = ""
if doc and hasattr(doc, "cost_center"):
if doc and hasattr(doc, "cost_center") and doc.cost_center:
condition = " and cost_center='%s'" % cost_center
orders = []
@@ -1503,9 +1541,15 @@ def get_orders_to_be_billed(
order_list = []
for d in orders:
if not (
flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))
if (
filters
and filters.get("outstanding_amt_greater_than")
and filters.get("outstanding_amt_less_than")
and not (
flt(filters.get("outstanding_amt_greater_than"))
<= flt(d.outstanding_amount)
<= flt(filters.get("outstanding_amt_less_than"))
)
):
continue

View File

@@ -999,6 +999,30 @@ class TestPaymentEntry(unittest.TestCase):
self.assertTrue("is on hold" in str(err.exception).lower())
def test_duplicate_payment_entry_allocate_amount(self):
si = create_sales_invoice()
pe_draft = get_payment_entry("Sales Invoice", si.name)
pe_draft.insert()
pe = get_payment_entry("Sales Invoice", si.name)
pe.submit()
self.assertRaises(frappe.ValidationError, pe_draft.submit)
def test_duplicate_payment_entry_partial_allocate_amount(self):
si = create_sales_invoice()
pe_draft = get_payment_entry("Sales Invoice", si.name)
pe_draft.insert()
pe = get_payment_entry("Sales Invoice", si.name)
pe.received_amount = si.total / 2
pe.references[0].allocated_amount = si.total / 2
pe.submit()
self.assertRaises(frappe.ValidationError, pe_draft.submit)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, getdate
from frappe.utils import cint, flt, getdate
class TaxWithholdingCategory(Document):
@@ -520,8 +520,13 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
tds_amount = 0
limit_consumed = frappe.db.get_value(
"Purchase Invoice",
{"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1},
"sum(net_total)",
{
"supplier": ("in", parties),
"apply_tds": 1,
"docstatus": 1,
"posting_date": ("between", (ldc.valid_from, ldc.valid_upto)),
},
"sum(tax_withholding_net_total)",
)
if is_valid_certificate(
@@ -535,10 +540,10 @@ def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount):
if certificate_limit - flt(deducted_amount) - flt(current_amount) >= 0:
return current_amount * rate / 100
else:
ltds_amount = certificate_limit - deducted_amount
ltds_amount = certificate_limit - flt(deducted_amount)
tds_amount = current_amount - ltds_amount
return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100
@@ -549,9 +554,9 @@ def is_valid_certificate(
):
valid = False
if (
getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)
) and certificate_limit > deducted_amount:
available_amount = flt(certificate_limit) - flt(deducted_amount) - flt(current_amount)
if (getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and available_amount > 0:
valid = True
return valid

View File

@@ -1,20 +1,23 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-04-08 14:49:58.133098",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:08:26.084484",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciation Ledger",
"owner": "Administrator",
"ref_doctype": "Asset",
"report_name": "Asset Depreciation Ledger",
"report_type": "Script Report",
"add_total_row": 1,
"columns": [],
"creation": "2016-04-08 14:49:58.133098",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"modified": "2023-06-06 09:00:07.435151",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciation Ledger",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Asset",
"report_name": "Asset Depreciation Ledger",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"

View File

@@ -1,20 +1,23 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2016-04-08 14:56:37.235981",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 2,
"is_standard": "Yes",
"modified": "2017-02-24 20:08:18.660476",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciations and Balances",
"owner": "Administrator",
"ref_doctype": "Asset",
"report_name": "Asset Depreciations and Balances",
"report_type": "Script Report",
"add_total_row": 1,
"columns": [],
"creation": "2016-04-08 14:56:37.235981",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 2,
"is_standard": "Yes",
"modified": "2023-06-06 11:33:29.611277",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Asset Depreciations and Balances",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Asset",
"report_name": "Asset Depreciations and Balances",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from collections import OrderedDict
import frappe
from frappe import _, qb, scrub
@@ -760,30 +761,30 @@ class GrossProfitGenerator(object):
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
"""
parents = []
grouped = OrderedDict()
for row in self.si_list:
if row.parent not in parents:
parents.append(row.parent)
# initialize list with a header row for each new parent
grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append(
row.update(
{"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code}
) # descendant rows will have indent: 1.0 or greater
)
parents_index = 0
for index, row in enumerate(self.si_list):
if parents_index < len(parents) and row.parent == parents[parents_index]:
invoice = self.get_invoice_row(row)
self.si_list.insert(index, invoice)
parents_index += 1
# if item is a bundle, add it's components as seperate rows
if frappe.db.exists("Product Bundle", row.item_code):
bundled_items = self.get_bundle_items(row)
for x in bundled_items:
bundle_item = self.get_bundle_item_row(row, x)
grouped.get(row.parent).append(bundle_item)
else:
# skipping the bundle items rows
if not row.indent:
row.indent = 1.0
row.parent_invoice = row.parent
row.invoice_or_item = row.item_code
self.si_list.clear()
if frappe.db.exists("Product Bundle", row.item_code):
self.add_bundle_items(row, index)
for items in grouped.values():
self.si_list.extend(items)
def get_invoice_row(self, row):
# header row format
return frappe._dict(
{
"parent_invoice": "",
@@ -812,13 +813,6 @@ class GrossProfitGenerator(object):
}
)
def add_bundle_items(self, product_bundle, index):
bundle_items = self.get_bundle_items(product_bundle)
for i, item in enumerate(bundle_items):
bundle_item = self.get_bundle_item_row(product_bundle, item)
self.si_list.insert((index + i + 1), bundle_item)
def get_bundle_items(self, product_bundle):
return frappe.get_all(
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]

View File

@@ -399,8 +399,9 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.is_internal_customer,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.project,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,

View File

@@ -27,6 +27,7 @@ from erpnext.accounts.general_ledger import make_reverse_gl_entries
from erpnext.assets.doctype.asset.depreciation import (
get_depreciation_accounts,
get_disposal_account_and_cost_center,
is_first_day_of_the_month,
is_last_day_of_the_month,
)
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
@@ -364,8 +365,14 @@ class Asset(AccountsController):
break
# For first row
if n == 0 and has_pro_rata and not self.opening_accumulated_depreciation:
from_date = add_days(self.available_for_use_date, -1)
if (
n == 0
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation
):
from_date = add_days(
self.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book,
depreciation_amount,
@@ -374,10 +381,18 @@ class Asset(AccountsController):
has_wdv_or_dd_non_yearly_pro_rata,
)
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
from_date = add_months(
getdate(self.available_for_use_date),
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
)
if not is_first_day_of_the_month(getdate(self.available_for_use_date)):
from_date = get_last_day(
add_months(
getdate(self.available_for_use_date),
((self.number_of_depreciations_booked - 1) * finance_book.frequency_of_depreciation),
)
)
else:
from_date = add_months(
getdate(add_days(self.available_for_use_date, -1)),
(self.number_of_depreciations_booked * finance_book.frequency_of_depreciation),
)
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book,
depreciation_amount,

View File

@@ -8,6 +8,7 @@ from frappe.utils import (
add_months,
cint,
flt,
get_first_day,
get_last_day,
get_link_to_form,
getdate,
@@ -543,3 +544,9 @@ def is_last_day_of_the_month(date):
last_day_of_the_month = get_last_day(date)
return getdate(last_day_of_the_month) == getdate(date)
def is_first_day_of_the_month(date):
first_day_of_the_month = get_first_day(date)
return getdate(first_day_of_the_month) == getdate(date)

View File

@@ -686,14 +686,14 @@ class TestDepreciationMethods(AssetSetup):
number_of_depreciations_booked=1,
opening_accumulated_depreciation=50000,
expected_value_after_useful_life=10000,
depreciation_start_date="2030-12-31",
depreciation_start_date="2031-12-31",
total_number_of_depreciations=3,
frequency_of_depreciation=12,
)
self.assertEqual(asset.status, "Draft")
expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]]
expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]

View File

@@ -96,7 +96,6 @@ class AssetCategory(Document):
frappe.throw(msg, title=_("Missing Account"))
@frappe.whitelist()
def get_asset_category_account(
fieldname, item=None, asset=None, account=None, asset_category=None, company=None
):

View File

@@ -891,6 +891,9 @@ class AccountsController(TransactionBase):
return is_inclusive
def should_show_taxes_as_table_in_print(self):
return cint(frappe.db.get_single_value("Accounts Settings", "show_taxes_as_table_in_print"))
def validate_advance_entries(self):
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))

View File

@@ -30,10 +30,16 @@ def set_print_templates_for_taxes(doc, settings):
doc.print_templates.update(
{
"total": "templates/print_formats/includes/total.html",
"taxes": "templates/print_formats/includes/taxes.html",
}
)
if not doc.should_show_taxes_as_table_in_print():
doc.print_templates.update(
{
"taxes": "templates/print_formats/includes/taxes.html",
}
)
def format_columns(display_columns, compact_fields):
compact_fields = compact_fields + ["image", "item_code", "item_name"]

View File

@@ -78,9 +78,10 @@ erpnext.ProductList = class {
let title_html = `<div style="display: flex; margin-left: -15px;">`;
title_html += `
<div class="col-8" style="margin-right: -15px;">
<a class="" href="/${ item.route || '#' }"
style="color: var(--gray-800); font-weight: 500;">
<a href="/${ item.route || '#' }">
<div class="product-title">
${ title }
</div>
</a>
</div>
`;
@@ -201,4 +202,4 @@ erpnext.ProductList = class {
}
}
};
};

View File

@@ -8,7 +8,15 @@ from math import ceil
import frappe
from frappe import _, bold
from frappe.model.document import Document
from frappe.utils import date_diff, flt, formatdate, get_last_day, get_link_to_form, getdate
from frappe.utils import (
comma_and,
date_diff,
flt,
formatdate,
get_last_day,
get_link_to_form,
getdate,
)
from six import string_types
@@ -207,7 +215,6 @@ def add_current_month_if_applicable(months_passed, date_of_joining, based_on_doj
@frappe.whitelist()
def create_assignment_for_multiple_employees(employees, data):
if isinstance(employees, string_types):
employees = json.loads(employees)
@@ -215,6 +222,8 @@ def create_assignment_for_multiple_employees(employees, data):
data = frappe._dict(json.loads(data))
docs_name = []
failed = []
for employee in employees:
assignment = frappe.new_doc("Leave Policy Assignment")
assignment.employee = employee
@@ -225,18 +234,45 @@ def create_assignment_for_multiple_employees(employees, data):
assignment.leave_period = data.leave_period or None
assignment.carry_forward = data.carry_forward
assignment.save()
try:
assignment.submit()
except frappe.exceptions.ValidationError:
continue
frappe.db.commit()
savepoint = "before_assignment_submission"
try:
frappe.db.savepoint(savepoint)
assignment.submit()
except Exception as e:
frappe.db.rollback(save_point=savepoint)
frappe.log_error(title=f"Leave Policy Assignment submission failed for {assignment.name}")
failed.append(assignment.name)
docs_name.append(assignment.name)
if failed:
show_assignment_submission_status(failed)
return docs_name
def show_assignment_submission_status(failed):
frappe.clear_messages()
assignment_list = [get_link_to_form("Leave Policy Assignment", entry) for entry in failed]
msg = _("Failed to submit some leave policy assignments:")
msg += " " + comma_and(assignment_list, False) + "<hr>"
msg += (
_("Check {0} for more details")
.format("<a href='/app/List/Error Log?reference_doctype=Leave Policy Assignment'>{0}</a>")
.format(_("Error Log"))
)
frappe.msgprint(
msg,
indicator="red",
title=_("Submission Failed"),
is_minimizable=True,
)
def get_leave_type_details():
leave_type_details = frappe._dict()
leave_types = frappe.get_all(

View File

@@ -52,7 +52,7 @@ frappe.listview_settings['Leave Policy Assignment'] = {
get_query() {
let filters = {"is_active": 1};
if (cur_dialog.fields_dict.company.value)
filters["company"] = cur_dialog.fields_dict.company.value;
filters["company"] = cur_dialog?.fields_dict?.company?.value;
return {
filters: filters

View File

@@ -1,4 +1,5 @@
import frappe
from frappe.query_builder.functions import Count
@frappe.whitelist()
@@ -15,31 +16,34 @@ def get_children(parent=None, company=None, exclude_node=None):
if exclude_node:
filters.append(["name", "!=", exclude_node])
employees = frappe.get_list(
employees = frappe.get_all(
"Employee",
fields=["employee_name as name", "name as id", "reports_to", "image", "designation as title"],
fields=[
"employee_name as name",
"name as id",
"lft",
"rgt",
"reports_to",
"image",
"designation as title",
],
filters=filters,
order_by="name",
)
for employee in employees:
is_expandable = frappe.db.count("Employee", filters={"reports_to": employee.get("id")})
employee.connections = get_connections(employee.id)
employee.expandable = 1 if is_expandable else 0
employee.connections = get_connections(employee.id, employee.lft, employee.rgt)
employee.expandable = bool(employee.connections)
return employees
def get_connections(employee):
num_connections = 0
def get_connections(employee: str, lft: int, rgt: int) -> int:
Employee = frappe.qb.DocType("Employee")
query = (
frappe.qb.from_(Employee)
.select(Count(Employee.name))
.where((Employee.lft > lft) & (Employee.rgt < rgt))
).run()
nodes_to_expand = frappe.get_list("Employee", filters=[["reports_to", "=", employee]])
num_connections += len(nodes_to_expand)
while nodes_to_expand:
parent = nodes_to_expand.pop(0)
descendants = frappe.get_list("Employee", filters=[["reports_to", "=", parent.name]])
num_connections += len(descendants)
nodes_to_expand.extend(descendants)
return num_connections
return query[0][0]

View File

@@ -0,0 +1,52 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.page.organizational_chart.organizational_chart import get_children
class TestOrganizationalChart(FrappeTestCase):
def setUp(self):
self.company = create_company("Test Org Chart").name
frappe.db.delete("Employee", {"company": self.company})
def test_get_children(self):
company = create_company("Test Org Chart").name
emp1 = make_employee("testemp1@mail.com", company=self.company)
emp2 = make_employee("testemp2@mail.com", company=self.company, reports_to=emp1)
emp3 = make_employee("testemp3@mail.com", company=self.company, reports_to=emp1)
make_employee("testemp4@mail.com", company=self.company, reports_to=emp2)
# root node
children = get_children(company=self.company)
self.assertEqual(len(children), 1)
self.assertEqual(children[0].id, emp1)
self.assertEqual(children[0].connections, 3)
# root's children
children = get_children(parent=emp1, company=self.company)
self.assertEqual(len(children), 2)
self.assertEqual(children[0].id, emp2)
self.assertEqual(children[0].connections, 1)
self.assertEqual(children[1].id, emp3)
self.assertEqual(children[1].connections, 0)
def create_company(name):
if frappe.db.exists("Company", name):
return frappe.get_doc("Company", name)
company = frappe.new_doc("Company")
company.update(
{
"company_name": name,
"default_currency": "USD",
"country": "United States",
}
)
return company.insert()

View File

@@ -160,4 +160,3 @@ class TestLoanDisbursement(unittest.TestCase):
interest = per_day_interest * 15
self.assertEqual(amounts["pending_principal_amount"], 1500000)
self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2))

View File

@@ -22,7 +22,7 @@ class LoanInterestAccrual(AccountsController):
frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
if not self.last_accrual_date:
self.last_accrual_date = get_last_accrual_date(self.loan)
self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date)
def on_submit(self):
self.make_gl_entries()
@@ -274,14 +274,14 @@ def make_loan_interest_accrual_entry(args):
def get_no_of_days_for_interest_accural(loan, posting_date):
last_interest_accrual_date = get_last_accrual_date(loan.name)
last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date)
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
return no_of_days
def get_last_accrual_date(loan):
def get_last_accrual_date(loan, posting_date):
last_posting_date = frappe.db.sql(
""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
WHERE loan = %s and docstatus = 1""",
@@ -289,12 +289,30 @@ def get_last_accrual_date(loan):
)
if last_posting_date[0][0]:
last_interest_accrual_date = last_posting_date[0][0]
# interest for last interest accrual date is already booked, so add 1 day
return add_days(last_posting_date[0][0], 1)
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
if last_disbursement_date and getdate(last_disbursement_date) > getdate(
last_interest_accrual_date
):
last_interest_accrual_date = last_disbursement_date
return add_days(last_interest_accrual_date, 1)
else:
return frappe.db.get_value("Loan", loan, "disbursement_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)},
"MAX(posting_date)",
)
return last_disbursement_date
def days_in_year(year):
days = 365

View File

@@ -102,7 +102,7 @@ class LoanRepayment(AccountsController):
if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
if not self.is_term_loan:
# get last loan interest accrual date
last_accrual_date = get_last_accrual_date(self.against_loan)
last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date)
# get posting date upto which interest has to be accrued
per_day_interest = get_per_day_interest(
@@ -724,7 +724,7 @@ def get_amounts(amounts, against_loan, posting_date):
if due_date:
pending_days = date_diff(posting_date, due_date) + 1
else:
last_accrual_date = get_last_accrual_date(against_loan_doc.name)
last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date)
pending_days = date_diff(posting_date, last_accrual_date) + 1
if pending_days > 0:

View File

@@ -25,20 +25,38 @@ frappe.listview_settings['Task'] = {
}
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
},
gantt_custom_popup_html: function(ganttobj, task) {
var html = `<h5><a style="text-decoration:underline"\
href="/app/task/${ganttobj.id}""> ${ganttobj.name} </a></h5>`;
gantt_custom_popup_html: function (ganttobj, task) {
let html = `
<a class="text-white mb-2 inline-block cursor-pointer"
href="/app/task/${ganttobj.id}"">
${ganttobj.name}
</a>
`;
if(task.project) html += `<p>Project: ${task.project}</p>`;
html += `<p>Progress: ${ganttobj.progress}</p>`;
if (task.project) {
html += `<p class="mb-1">${__("Project")}:
<a class="text-white inline-block"
href="/app/project/${task.project}"">
${task.project}
</a>
</p>`;
}
html += `<p class="mb-1">
${__("Progress")}:
<span class="text-white">${ganttobj.progress}%</span>
</p>`;
if(task._assign_list) {
html += task._assign_list.reduce(
(html, user) => html + frappe.avatar(user)
, '');
if (task._assign) {
const assign_list = JSON.parse(task._assign);
const assignment_wrapper = `
<span>Assigned to:</span>
<span class="text-white">
${assign_list.map((user) => frappe.user_info(user).fullname).join(", ")}
</span>
`;
html += assignment_wrapper;
}
return html;
}
return `<div class="p-3" style="min-width: 220px">${html}</div>`;
},
};

View File

@@ -764,11 +764,13 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
precision("base_grand_total")
);
}
this.frm.doc.payments.find(pay => {
if (pay.default) {
pay.amount = total_amount_to_pay;
}
});
if(!this.frm.doc.is_return){
this.frm.doc.payments.find(payment => {
if (payment.default) {
payment.amount = total_amount_to_pay;
}
});
}
this.frm.refresh_fields();
},

View File

@@ -96,14 +96,14 @@ def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: D
range1 = range2 = range3 = above_range3 = 0.0
for item in fifo_queue:
age = date_diff(to_date, item[1])
age = flt(date_diff(to_date, item[1]))
qty = flt(item[0]) if not item_dict["has_serial_no"] else 1.0
if age <= filters.range1:
if age <= flt(filters.range1):
range1 = flt(range1 + qty, precision)
elif age <= filters.range2:
elif age <= flt(filters.range2):
range2 = flt(range2 + qty, precision)
elif age <= filters.range3:
elif age <= flt(filters.range3):
range3 = flt(range3 + qty, precision)
else:
above_range3 = flt(above_range3 + qty, precision)