Merge pull request #33801 from frappe/version-13-hotfix
chore: release v13
This commit is contained in:
99
.github/helper/documentation.py
vendored
99
.github/helper/documentation.py
vendored
@@ -3,52 +3,71 @@ from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
docs_repos = [
|
||||
"frappe_docs",
|
||||
"erpnext_documentation",
|
||||
WEBSITE_REPOS = [
|
||||
"erpnext_com",
|
||||
"frappe_io",
|
||||
]
|
||||
|
||||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"frappeframework.com",
|
||||
]
|
||||
|
||||
def uri_validator(x):
|
||||
result = urlparse(x)
|
||||
return all([result.scheme, result.netloc, result.path])
|
||||
|
||||
def docs_link_exists(body):
|
||||
for line in body.splitlines():
|
||||
for word in line.split():
|
||||
if word.startswith('http') and uri_validator(word):
|
||||
parsed_url = urlparse(word)
|
||||
if parsed_url.netloc == "github.com":
|
||||
parts = parsed_url.path.split('/')
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||
return True
|
||||
elif parsed_url.netloc == "docs.erpnext.com":
|
||||
return True
|
||||
def is_valid_url(url: str) -> bool:
|
||||
parts = urlparse(url)
|
||||
return all((parts.scheme, parts.netloc, parts.path))
|
||||
|
||||
|
||||
def is_documentation_link(word: str) -> bool:
|
||||
if not word.startswith("http") or not is_valid_url(word):
|
||||
return False
|
||||
|
||||
parsed_url = urlparse(word)
|
||||
if parsed_url.netloc in DOCUMENTATION_DOMAINS:
|
||||
return True
|
||||
|
||||
if parsed_url.netloc == "github.com":
|
||||
parts = parsed_url.path.split("/")
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def contains_documentation_link(body: str) -> bool:
|
||||
return any(
|
||||
is_documentation_link(word)
|
||||
for line in body.splitlines()
|
||||
for word in line.split()
|
||||
)
|
||||
|
||||
|
||||
def check_pull_request(number: str) -> "tuple[int, str]":
|
||||
response = requests.get(f"https://api.github.com/repos/frappe/erpnext/pulls/{number}")
|
||||
if not response.ok:
|
||||
return 1, "Pull Request Not Found! ⚠️"
|
||||
|
||||
payload = response.json()
|
||||
title = (payload.get("title") or "").lower().strip()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if (
|
||||
not title.startswith("feat")
|
||||
or not head_sha
|
||||
or "no-docs" in body
|
||||
or "backport" in body
|
||||
):
|
||||
return 0, "Skipping documentation checks... 🏃"
|
||||
|
||||
if contains_documentation_link(body):
|
||||
return 0, "Documentation Link Found. You're Awesome! 🎉"
|
||||
|
||||
return 1, "Documentation Link Not Found! ⚠️"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pr = sys.argv[1]
|
||||
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
|
||||
|
||||
if response.ok:
|
||||
payload = response.json()
|
||||
title = (payload.get("title") or "").lower().strip()
|
||||
head_sha = (payload.get("head") or {}).get("sha")
|
||||
body = (payload.get("body") or "").lower()
|
||||
|
||||
if (title.startswith("feat")
|
||||
and head_sha
|
||||
and "no-docs" not in body
|
||||
and "backport" not in body
|
||||
):
|
||||
if docs_link_exists(body):
|
||||
print("Documentation Link Found. You're Awesome! 🎉")
|
||||
|
||||
else:
|
||||
print("Documentation Link Not Found! ⚠️")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print("Skipping documentation checks... 🏃")
|
||||
exit_code, message = check_pull_request(sys.argv[1])
|
||||
print(message)
|
||||
sys.exit(exit_code)
|
||||
|
||||
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: '3.10'
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v2
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
<br>
|
||||
{% endif %}
|
||||
|
||||
{{ _("Against") }}: {{ row.against }}
|
||||
<br>{{ _("Remarks") }}: {{ row.remarks }}
|
||||
{% if row.bill_no %}
|
||||
<br>{{ _("Supplier Invoice No") }}: {{ row.bill_no }}
|
||||
|
||||
@@ -7,17 +7,7 @@ from frappe import _, msgprint, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||
from six import iteritems
|
||||
|
||||
import erpnext
|
||||
@@ -33,10 +23,12 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.utils import get_account_currency
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
depreciate_asset,
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
make_depreciation_entry,
|
||||
reset_depreciation_schedule,
|
||||
reverse_depreciation_entry_made_after_disposal,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
@@ -1114,18 +1106,20 @@ class SalesInvoice(SellingController):
|
||||
asset = self.get_asset(item)
|
||||
|
||||
if self.is_return:
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||
asset, item.base_net_amount, item.finance_book
|
||||
)
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
|
||||
reset_depreciation_schedule(asset, self.posting_date)
|
||||
|
||||
else:
|
||||
if asset.calculate_depreciation:
|
||||
self.depreciate_asset(asset)
|
||||
depreciate_asset(asset, self.posting_date)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset, item.base_net_amount, item.finance_book
|
||||
@@ -1193,95 +1187,6 @@ class SalesInvoice(SellingController):
|
||||
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
|
||||
)
|
||||
|
||||
def depreciate_asset(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(date_of_sale=self.posting_date)
|
||||
asset.save()
|
||||
|
||||
make_depreciation_entry(asset.name, self.posting_date)
|
||||
asset.load_from_db()
|
||||
|
||||
def reset_depreciation_schedule(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
asset.prepare_depreciation_data(date_of_return=self.posting_date)
|
||||
|
||||
self.modify_depreciation_schedule_for_asset_repairs(asset)
|
||||
asset.save()
|
||||
asset.load_from_db()
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
|
||||
)
|
||||
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
asset.prepare_depreciation_data()
|
||||
|
||||
def reverse_depreciation_entry_made_after_sale(self, asset):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
|
||||
|
||||
row = -1
|
||||
finance_book = asset.get("schedules")[0].get("finance_book")
|
||||
for schedule in asset.get("schedules"):
|
||||
if schedule.finance_book != finance_book:
|
||||
row = 0
|
||||
finance_book = schedule.finance_book
|
||||
else:
|
||||
row += 1
|
||||
|
||||
if schedule.schedule_date == posting_date_of_original_invoice:
|
||||
if not self.sale_was_made_on_original_schedule_date(
|
||||
asset, schedule, row, posting_date_of_original_invoice
|
||||
) or self.sale_happens_in_the_future(posting_date_of_original_invoice):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
|
||||
asset.finance_books[0].value_after_depreciation += depreciation_amount
|
||||
asset.save()
|
||||
|
||||
def get_posting_date_of_sales_invoice(self):
|
||||
return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
|
||||
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
||||
def sale_was_made_on_original_schedule_date(
|
||||
self, asset, schedule, row, posting_date_of_original_invoice
|
||||
):
|
||||
for finance_book in asset.get("finance_books"):
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
orginal_schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if orginal_schedule_date == posting_date_of_original_invoice:
|
||||
return True
|
||||
return False
|
||||
|
||||
def sale_happens_in_the_future(self, posting_date_of_original_invoice):
|
||||
if posting_date_of_original_invoice > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_depreciation_amount_in_je(self, journal_entry):
|
||||
if journal_entry.accounts[0].debit_in_account_currency:
|
||||
return journal_entry.accounts[0].debit_in_account_currency
|
||||
else:
|
||||
return journal_entry.accounts[0].credit_in_account_currency
|
||||
|
||||
@property
|
||||
def enable_discount_accounting(self):
|
||||
if not hasattr(self, "_enable_discount_accounting"):
|
||||
|
||||
@@ -1117,6 +1117,46 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
|
||||
bundle_item.append(
|
||||
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||
)
|
||||
bundle_item.save(ignore_permissions=True)
|
||||
|
||||
make_item("_Packed Item New 1", {"is_stock_item": 1})
|
||||
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
|
||||
|
||||
si = create_sales_invoice(
|
||||
item_code="_Test Product Bundle Item New",
|
||||
update_stock=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
transaction_date=add_days(nowdate(), -1),
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
|
||||
|
||||
bin_details = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
|
||||
["actual_qty", "projected_qty", "ordered_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
si.transaction_date = nowdate()
|
||||
si.save()
|
||||
|
||||
packed_item = si.packed_items[0]
|
||||
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
|
||||
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
|
||||
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
|
||||
|
||||
def test_pos_si_without_payment(self):
|
||||
make_pos_profile()
|
||||
|
||||
|
||||
@@ -378,15 +378,14 @@ class Deferred_Revenue_and_Expense_Report(object):
|
||||
ret += [{}]
|
||||
|
||||
# add total row
|
||||
if ret is not []:
|
||||
if self.filters.type == "Revenue":
|
||||
total_row = frappe._dict({"name": "Total Deferred Income"})
|
||||
elif self.filters.type == "Expense":
|
||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||
if self.filters.type == "Revenue":
|
||||
total_row = frappe._dict({"name": "Total Deferred Income"})
|
||||
elif self.filters.type == "Expense":
|
||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
total_row[period.key] = self.period_total[idx].total
|
||||
ret.append(total_row)
|
||||
for idx, period in enumerate(self.period_list, 0):
|
||||
total_row[period.key] = self.period_total[idx].total
|
||||
ret.append(total_row)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{%= __("Date") %}</th>
|
||||
<th style="width: 15%">{%= __("Ref") %}</th>
|
||||
<th style="width: 25%">{%= __("Party") %}</th>
|
||||
<th style="width: 15%">{%= __("Reference") %}</th>
|
||||
<th style="width: 25%">{%= __("Remarks") %}</th>
|
||||
<th style="width: 15%">{%= __("Debit") %}</th>
|
||||
<th style="width: 15%">{%= __("Credit") %}</th>
|
||||
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
|
||||
@@ -45,7 +45,6 @@
|
||||
<br>
|
||||
{% } %}
|
||||
|
||||
{{ __("Against") }}: {%= data[i].against %}
|
||||
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
||||
{% if(data[i].bill_no) { %}
|
||||
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
|
||||
@@ -202,6 +202,10 @@ frappe.ui.form.on('Asset', {
|
||||
},
|
||||
|
||||
setup_chart: function(frm) {
|
||||
if(frm.doc.finance_books.length > 1) {
|
||||
return
|
||||
}
|
||||
|
||||
var x_intervals = [frm.doc.purchase_date];
|
||||
var asset_values = [frm.doc.gross_purchase_amount];
|
||||
var last_depreciation_date = frm.doc.purchase_date;
|
||||
|
||||
@@ -79,12 +79,12 @@ class Asset(AccountsController):
|
||||
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
|
||||
)
|
||||
|
||||
def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None):
|
||||
def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None):
|
||||
if self.calculate_depreciation:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
self.make_depreciation_schedule(date_of_sale)
|
||||
self.set_accumulated_depreciation(date_of_sale, date_of_return)
|
||||
self.make_depreciation_schedule(date_of_disposal)
|
||||
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
|
||||
else:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
@@ -223,7 +223,7 @@ class Asset(AccountsController):
|
||||
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
|
||||
)
|
||||
|
||||
def make_depreciation_schedule(self, date_of_sale):
|
||||
def make_depreciation_schedule(self, date_of_disposal):
|
||||
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
|
||||
"schedules"
|
||||
):
|
||||
@@ -279,17 +279,17 @@ class Asset(AccountsController):
|
||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||
|
||||
# if asset is being sold
|
||||
if date_of_sale:
|
||||
if date_of_disposal:
|
||||
from_date = self.get_from_date(finance_book.finance_book)
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
||||
finance_book, depreciation_amount, from_date, date_of_sale
|
||||
finance_book, depreciation_amount, from_date, date_of_disposal
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self.append(
|
||||
"schedules",
|
||||
{
|
||||
"schedule_date": date_of_sale,
|
||||
"schedule_date": date_of_disposal,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": finance_book.depreciation_method,
|
||||
"finance_book": finance_book.finance_book,
|
||||
@@ -364,6 +364,9 @@ class Asset(AccountsController):
|
||||
},
|
||||
)
|
||||
|
||||
if len(self.get("finance_books")) > 1 and any(start):
|
||||
self.sort_depreciation_schedule()
|
||||
|
||||
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||
# JE: Journal Entry, FB: Finance Book
|
||||
def clear_depreciation_schedule(self):
|
||||
@@ -399,6 +402,14 @@ class Asset(AccountsController):
|
||||
|
||||
return start
|
||||
|
||||
def sort_depreciation_schedule(self):
|
||||
self.schedules = sorted(
|
||||
self.schedules, key=lambda s: (int(s.finance_book_id), getdate(s.schedule_date))
|
||||
)
|
||||
|
||||
for idx, s in enumerate(self.schedules, 1):
|
||||
s.idx = idx
|
||||
|
||||
def get_from_date(self, finance_book):
|
||||
if not self.get("schedules"):
|
||||
return self.available_for_use_date
|
||||
@@ -531,7 +542,7 @@ class Asset(AccountsController):
|
||||
return True
|
||||
|
||||
def set_accumulated_depreciation(
|
||||
self, date_of_sale=None, date_of_return=None, ignore_booked_entry=False
|
||||
self, date_of_disposal=None, date_of_return=None, ignore_booked_entry=False
|
||||
):
|
||||
straight_line_idx = [
|
||||
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
|
||||
@@ -554,7 +565,7 @@ class Asset(AccountsController):
|
||||
if (
|
||||
straight_line_idx
|
||||
and i == max(straight_line_idx) - 1
|
||||
and not date_of_sale
|
||||
and not date_of_disposal
|
||||
and not date_of_return
|
||||
):
|
||||
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, today
|
||||
from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today
|
||||
from frappe.utils.user import get_users_with_role
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -247,6 +247,11 @@ def scrap_asset(asset_name):
|
||||
_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)
|
||||
)
|
||||
|
||||
date = today()
|
||||
|
||||
depreciate_asset(asset, date)
|
||||
asset.reload()
|
||||
|
||||
depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, "series_for_depreciation_entry"
|
||||
)
|
||||
@@ -254,7 +259,7 @@ def scrap_asset(asset_name):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Journal Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = today()
|
||||
je.posting_date = date
|
||||
je.company = asset.company
|
||||
je.remark = "Scrap Entry for asset {0}".format(asset_name)
|
||||
|
||||
@@ -265,7 +270,7 @@ def scrap_asset(asset_name):
|
||||
je.flags.ignore_permissions = True
|
||||
je.submit()
|
||||
|
||||
frappe.db.set_value("Asset", asset_name, "disposal_date", today())
|
||||
frappe.db.set_value("Asset", asset_name, "disposal_date", date)
|
||||
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
|
||||
asset.set_status("Scrapped")
|
||||
|
||||
@@ -276,6 +281,9 @@ def scrap_asset(asset_name):
|
||||
def restore_asset(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
|
||||
reset_depreciation_schedule(asset, asset.disposal_date)
|
||||
|
||||
je = asset.journal_entry_for_scrap
|
||||
|
||||
asset.db_set("disposal_date", None)
|
||||
@@ -286,6 +294,96 @@ def restore_asset(asset_name):
|
||||
asset.set_status()
|
||||
|
||||
|
||||
def depreciate_asset(asset, date):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(date_of_disposal=date)
|
||||
asset.save()
|
||||
|
||||
make_depreciation_entry(asset.name, date)
|
||||
|
||||
|
||||
def reset_depreciation_schedule(asset, date):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
asset.prepare_depreciation_data(date_of_return=date)
|
||||
|
||||
modify_depreciation_schedule_for_asset_repairs(asset)
|
||||
asset.save()
|
||||
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
"Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"]
|
||||
)
|
||||
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc("Asset Repair", repair.name)
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
asset.prepare_depreciation_data()
|
||||
|
||||
|
||||
def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
row = -1
|
||||
finance_book = asset.get("schedules")[0].get("finance_book")
|
||||
for schedule in asset.get("schedules"):
|
||||
if schedule.finance_book != finance_book:
|
||||
row = 0
|
||||
finance_book = schedule.finance_book
|
||||
else:
|
||||
row += 1
|
||||
|
||||
if schedule.schedule_date == date:
|
||||
if not disposal_was_made_on_original_schedule_date(
|
||||
asset, schedule, row, date
|
||||
) or disposal_happens_in_the_future(date):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
|
||||
|
||||
idx = cint(schedule.finance_book_id)
|
||||
asset.finance_books[idx - 1].value_after_depreciation += depreciation_amount
|
||||
|
||||
asset.save()
|
||||
|
||||
|
||||
def get_depreciation_amount_in_je(journal_entry):
|
||||
if journal_entry.accounts[0].debit_in_account_currency:
|
||||
return journal_entry.accounts[0].debit_in_account_currency
|
||||
else:
|
||||
return journal_entry.accounts[0].credit_in_account_currency
|
||||
|
||||
|
||||
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
||||
def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal):
|
||||
for finance_book in asset.get("finance_books"):
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
orginal_schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if orginal_schedule_date == posting_date_of_disposal:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def disposal_happens_in_the_future(posting_date_of_disposal):
|
||||
if posting_date_of_disposal > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
|
||||
(
|
||||
fixed_asset_account,
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cstr,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status
|
||||
@@ -153,28 +162,59 @@ class TestAsset(AssetSetup):
|
||||
self.assertEqual(doc.items[0].is_fixed_asset, 1)
|
||||
|
||||
def test_scrap_asset(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-01-01",
|
||||
purchase_date="2020-01-01",
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
post_depreciation_entries(date=add_months("2020-01-01", 4))
|
||||
post_depreciation_entries(date=add_months(purchase_date, 2))
|
||||
asset.load_from_db()
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
self.assertEquals(accumulated_depr_amount, 18000.0)
|
||||
|
||||
scrap_asset(asset.name)
|
||||
|
||||
asset.load_from_db()
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
self.assertEquals(
|
||||
accumulated_depr_amount,
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Scrapped")
|
||||
self.assertTrue(asset.journal_entry_for_scrap)
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 36000.0, 0.0),
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
flt(82000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
@@ -183,7 +223,7 @@ class TestAsset(AssetSetup):
|
||||
order by account""",
|
||||
asset.journal_entry_for_scrap,
|
||||
)
|
||||
self.assertEqual(gle, expected_gle)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
restore_asset(asset.name)
|
||||
|
||||
@@ -191,34 +231,57 @@ class TestAsset(AssetSetup):
|
||||
self.assertFalse(asset.journal_entry_for_scrap)
|
||||
self.assertEqual(asset.status, "Partially Depreciated")
|
||||
|
||||
accumulated_depr_amount = flt(
|
||||
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
|
||||
asset.precision("gross_purchase_amount"),
|
||||
)
|
||||
this_month_depr_amount = 9000.0 if is_last_day_of_the_month(date) else 0
|
||||
|
||||
self.assertEquals(accumulated_depr_amount, 18000.0 + this_month_depr_amount)
|
||||
|
||||
def test_gle_made_by_asset_sale(self):
|
||||
date = nowdate()
|
||||
purchase_date = add_months(get_first_day(date), -2)
|
||||
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2020-06-06",
|
||||
purchase_date="2020-01-01",
|
||||
available_for_use_date=purchase_date,
|
||||
purchase_date=purchase_date,
|
||||
expected_value_after_useful_life=10000,
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=10,
|
||||
depreciation_start_date="2020-12-31",
|
||||
total_number_of_depreciations=10,
|
||||
frequency_of_depreciation=1,
|
||||
submit=1,
|
||||
)
|
||||
post_depreciation_entries(date="2021-01-01")
|
||||
|
||||
post_depreciation_entries(date=add_months(purchase_date, 2))
|
||||
|
||||
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||
si.customer = "_Test Customer"
|
||||
si.set_posting_time = 1
|
||||
si.posting_date = "2021-10-31"
|
||||
si.due_date = "2021-10-31"
|
||||
si.get("items")[0].rate = 75000
|
||||
si.due_date = nowdate()
|
||||
si.get("items")[0].rate = 25000
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
||||
|
||||
pro_rata_amount, _, _ = asset.get_pro_rata_amt(
|
||||
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
|
||||
)
|
||||
pro_rata_amount = flt(pro_rata_amount, asset.precision("gross_purchase_amount"))
|
||||
|
||||
expected_gle = (
|
||||
("_Test Accumulated Depreciations - _TC", 50490.2, 0.0),
|
||||
(
|
||||
"_Test Accumulated Depreciations - _TC",
|
||||
flt(18000.0 + pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||
("_Test Gain/Loss on Asset Disposal - _TC", 0.0, 25490.2),
|
||||
("Debtors - _TC", 75000.0, 0.0),
|
||||
(
|
||||
"_Test Gain/Loss on Asset Disposal - _TC",
|
||||
flt(57000.0 - pro_rata_amount, asset.precision("gross_purchase_amount")),
|
||||
0.0,
|
||||
),
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
@@ -228,14 +291,9 @@ class TestAsset(AssetSetup):
|
||||
si.name,
|
||||
)
|
||||
|
||||
for i, gle_entry in enumerate(gle):
|
||||
self.assertEqual(gle_entry[0], expected_gle[i][0])
|
||||
self.assertEqual(flt(gle_entry[1], 1), flt(expected_gle[i][1], 1))
|
||||
self.assertEqual(flt(gle_entry[2], 1), flt(expected_gle[i][2], 1))
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
si.load_from_db()
|
||||
si.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
@@ -1471,3 +1529,9 @@ def set_depreciation_settings_in_company():
|
||||
|
||||
def enable_cwip_accounting(asset_category, enable=1):
|
||||
frappe.db.set_value("Asset Category", asset_category, "enable_cwip_accounting", enable)
|
||||
|
||||
|
||||
def is_last_day_of_the_month(dt):
|
||||
last_day_of_the_month = get_last_day(dt)
|
||||
|
||||
return getdate(dt) == getdate(last_day_of_the_month)
|
||||
|
||||
@@ -1239,6 +1239,11 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
|
||||
def test_variant_item_po(self):
|
||||
po = create_purchase_order(item_code="_Test Variant Item", qty=1, rate=100, do_not_save=1)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
|
||||
|
||||
def make_pr_against_po(po, received_qty=0):
|
||||
pr = make_purchase_receipt(po)
|
||||
@@ -1342,8 +1347,8 @@ def create_purchase_order(**args):
|
||||
},
|
||||
)
|
||||
|
||||
po.set_missing_values()
|
||||
if not args.do_not_save:
|
||||
po.set_missing_values()
|
||||
po.insert()
|
||||
if not args.do_not_submit:
|
||||
if po.is_subcontracted == "Yes":
|
||||
|
||||
@@ -25,7 +25,7 @@ class SellingController(StockController):
|
||||
def onload(self):
|
||||
super(SellingController, self).onload()
|
||||
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
for item in self.get("items"):
|
||||
for item in self.get("items") + (self.get("packed_items") or []):
|
||||
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -58,7 +58,7 @@ status_map = {
|
||||
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1",
|
||||
],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed'"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
["On Hold", "eval:self.status=='On Hold'"],
|
||||
],
|
||||
"Purchase Order": [
|
||||
@@ -79,7 +79,7 @@ status_map = {
|
||||
["Delivered", "eval:self.status=='Delivered'"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["On Hold", "eval:self.status=='On Hold'"],
|
||||
["Closed", "eval:self.status=='Closed'"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
"Delivery Note": [
|
||||
["Draft", None],
|
||||
@@ -87,7 +87,7 @@ status_map = {
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed'"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
"Purchase Receipt": [
|
||||
["Draft", None],
|
||||
@@ -95,7 +95,7 @@ status_map = {
|
||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
["Closed", "eval:self.status=='Closed'"],
|
||||
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||
],
|
||||
"Material Request": [
|
||||
["Draft", None],
|
||||
|
||||
@@ -173,7 +173,10 @@ class TestWebsiteItem(unittest.TestCase):
|
||||
# Website Item Portal Tests Begin
|
||||
|
||||
def test_website_item_breadcrumbs(self):
|
||||
"Check if breadcrumbs include homepage, product listing navigation page, parent item group(s) and item group."
|
||||
"""
|
||||
Check if breadcrumbs include homepage, product listing navigation page,
|
||||
parent item group(s) and item group
|
||||
"""
|
||||
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
|
||||
|
||||
item_code = "Test Breadcrumb Item"
|
||||
@@ -196,7 +199,7 @@ class TestWebsiteItem(unittest.TestCase):
|
||||
breadcrumbs = get_parent_item_groups(item.item_group)
|
||||
|
||||
self.assertEqual(breadcrumbs[0]["name"], "Home")
|
||||
self.assertEqual(breadcrumbs[1]["name"], "Shop by Category")
|
||||
self.assertEqual(breadcrumbs[1]["name"], "All Products")
|
||||
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
|
||||
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
|
||||
|
||||
|
||||
@@ -13,38 +13,24 @@ frappe.query_reports["Work Order Summary"] = {
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: "fiscal_year",
|
||||
label: __("Fiscal Year"),
|
||||
fieldtype: "Link",
|
||||
options: "Fiscal Year",
|
||||
default: frappe.defaults.get_user_default("fiscal_year"),
|
||||
reqd: 1,
|
||||
on_change: function(query_report) {
|
||||
var fiscal_year = query_report.get_values().fiscal_year;
|
||||
if (!fiscal_year) {
|
||||
return;
|
||||
}
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
from_date: fy.year_start_date,
|
||||
to_date: fy.year_end_date
|
||||
});
|
||||
});
|
||||
}
|
||||
label: __("Based On"),
|
||||
fieldname:"based_on",
|
||||
fieldtype: "Select",
|
||||
options: "Creation Date\nPlanned Date\nActual Date",
|
||||
default: "Creation Date"
|
||||
},
|
||||
{
|
||||
label: __("From Posting Date"),
|
||||
fieldname:"from_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.defaults.get_user_default("year_start_date"),
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -3),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("To Posting Date"),
|
||||
fieldname:"to_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.defaults.get_user_default("year_end_date"),
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ def get_data(filters):
|
||||
"sales_order",
|
||||
"production_item",
|
||||
"qty",
|
||||
"creation",
|
||||
"produced_qty",
|
||||
"planned_start_date",
|
||||
"planned_end_date",
|
||||
@@ -47,8 +48,14 @@ def get_data(filters):
|
||||
if filters.get(field):
|
||||
query_filters[field] = filters.get(field)
|
||||
|
||||
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
|
||||
if filters.get("based_on") == "Planned Date":
|
||||
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["planned_end_date"] = ("<=", filters.get("to_date"))
|
||||
elif filters.get("based_on") == "Actual Date":
|
||||
query_filters["actual_start_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["actual_end_date"] = ("<=", filters.get("to_date"))
|
||||
else:
|
||||
query_filters["creation"] = ("between", [filters.get("from_date"), filters.get("to_date")])
|
||||
|
||||
data = frappe.get_all(
|
||||
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
|
||||
@@ -212,6 +219,12 @@ def get_columns(filters):
|
||||
"options": "Sales Order",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Created On"),
|
||||
"fieldname": "creation",
|
||||
"fieldtype": "Date",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Planned Start Date"),
|
||||
"fieldname": "planned_start_date",
|
||||
|
||||
@@ -374,4 +374,4 @@ erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
|
||||
execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_series": "", "set_options": "", "prefix": "", "current_value": 0, "user_must_always_select": 0})
|
||||
erpnext.patches.v13_0.update_schedule_type_in_loans
|
||||
erpnext.patches.v13_0.update_schedule_type_in_loans
|
||||
|
||||
@@ -7,6 +7,7 @@ from erpnext.stock.stock_ledger import update_entries_after
|
||||
|
||||
def execute():
|
||||
doctypes_to_reload = [
|
||||
("setup", "company"),
|
||||
("stock", "repost_item_valuation"),
|
||||
("stock", "stock_entry_detail"),
|
||||
("stock", "purchase_receipt_item"),
|
||||
|
||||
@@ -115,24 +115,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
calculate_item_values: function() {
|
||||
let me = this;
|
||||
if (!this.discount_amount_applied) {
|
||||
$.each(this.frm.doc["items"] || [], function(i, item) {
|
||||
for (const item of this.frm.doc.items || []) {
|
||||
frappe.model.round_floats_in(item);
|
||||
item.net_rate = item.rate;
|
||||
|
||||
if ((!item.qty) && me.frm.doc.is_return) {
|
||||
item.amount = flt(item.rate * -1, precision("amount", item));
|
||||
} else if ((!item.qty) && me.frm.doc.is_debit_note) {
|
||||
item.amount = flt(item.rate, precision("amount", item));
|
||||
} else {
|
||||
item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||
}
|
||||
|
||||
item.net_amount = item.amount;
|
||||
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
|
||||
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||
item.item_tax_amount = 0.0;
|
||||
item.total_weight = flt(item.weight_per_unit * item.stock_qty);
|
||||
|
||||
me.set_in_company_currency(item, ["price_list_rate", "rate", "amount", "net_rate", "net_amount"]);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -71,7 +71,11 @@ def validate_eligibility(doc):
|
||||
|
||||
# if export invoice, then taxes can be empty
|
||||
# invoice can only be ineligible if no taxes applied and is not an export invoice
|
||||
no_taxes_applied = not doc.get("taxes") and not doc.get("gst_category") == "Overseas"
|
||||
no_taxes_applied = (
|
||||
not doc.get("taxes")
|
||||
and not doc.get("gst_category") == "Overseas"
|
||||
and not doc.get("gst_category") == "SEZ"
|
||||
)
|
||||
has_non_gst_item = any(d for d in doc.get("items", []) if d.get("is_non_gst"))
|
||||
|
||||
if (
|
||||
|
||||
@@ -546,6 +546,42 @@ class TestSalesOrder(FrappeTestCase):
|
||||
workflow.is_active = 0
|
||||
workflow.save()
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
|
||||
bundle_item.append(
|
||||
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||
)
|
||||
bundle_item.save(ignore_permissions=True)
|
||||
|
||||
make_item("_Packed Item New 1", {"is_stock_item": 1})
|
||||
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
|
||||
|
||||
so = make_sales_order(
|
||||
item_code="_Test Product Bundle Item New",
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
transaction_date=add_days(nowdate(), -1),
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
|
||||
|
||||
bin_details = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
|
||||
["actual_qty", "projected_qty", "ordered_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
so.transaction_date = nowdate()
|
||||
so.save()
|
||||
|
||||
packed_item = so.packed_items[0]
|
||||
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
|
||||
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
|
||||
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
|
||||
|
||||
def test_update_child_product_bundle(self):
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Product Bundle Item"):
|
||||
|
||||
@@ -149,12 +149,12 @@ def get_item_for_list_in_html(context):
|
||||
|
||||
|
||||
def get_parent_item_groups(item_group_name, from_item=False):
|
||||
base_nav_page = {"name": _("Shop by Category"), "route": "/shop-by-category"}
|
||||
base_nav_page = {"name": _("All Products"), "route": "/all-products"}
|
||||
|
||||
if from_item and frappe.request.environ.get("HTTP_REFERER"):
|
||||
# base page after 'Home' will vary on Item page
|
||||
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
|
||||
if last_page and last_page in ("shop-by-category", "all-products"):
|
||||
if last_page and last_page == "shop-by-category":
|
||||
base_nav_page_title = " ".join(last_page.split("-")).title()
|
||||
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
|
||||
|
||||
|
||||
@@ -490,6 +490,46 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertEqual(gle_warehouse_amount, 1400)
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||
bundle_item = make_item("_Test Product Bundle Item New", {"is_stock_item": 0})
|
||||
bundle_item.append(
|
||||
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||
)
|
||||
bundle_item.save(ignore_permissions=True)
|
||||
|
||||
make_item("_Packed Item New 1", {"is_stock_item": 1})
|
||||
make_product_bundle("_Test Product Bundle Item New", ["_Packed Item New 1"], 2)
|
||||
|
||||
si = create_delivery_note(
|
||||
item_code="_Test Product Bundle Item New",
|
||||
update_stock=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
transaction_date=add_days(nowdate(), -1),
|
||||
do_not_submit=1,
|
||||
)
|
||||
|
||||
make_stock_entry(item="_Packed Item New 1", target="_Test Warehouse - _TC", qty=120, rate=100)
|
||||
|
||||
bin_details = frappe.db.get_value(
|
||||
"Bin",
|
||||
{"item_code": "_Packed Item New 1", "warehouse": "_Test Warehouse - _TC"},
|
||||
["actual_qty", "projected_qty", "ordered_qty"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
si.transaction_date = nowdate()
|
||||
si.save()
|
||||
|
||||
packed_item = si.packed_items[0]
|
||||
self.assertEqual(flt(bin_details.actual_qty), flt(packed_item.actual_qty))
|
||||
self.assertEqual(flt(bin_details.projected_qty), flt(packed_item.projected_qty))
|
||||
self.assertEqual(flt(bin_details.ordered_qty), flt(packed_item.ordered_qty))
|
||||
|
||||
def test_return_for_serialized_items(self):
|
||||
se = make_serialized_item()
|
||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||
@@ -650,6 +690,11 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
update_delivery_note_status(dn.name, "Closed")
|
||||
self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed")
|
||||
|
||||
# Check cancelling closed delivery note
|
||||
dn.load_from_db()
|
||||
dn.cancel()
|
||||
self.assertEqual(dn.status, "Cancelled")
|
||||
|
||||
def test_dn_billing_status_case1(self):
|
||||
# SO -> DN -> SI
|
||||
so = make_sales_order()
|
||||
|
||||
@@ -74,11 +74,10 @@ class ItemAttribute(Document):
|
||||
def validate_duplication(self):
|
||||
values, abbrs = [], []
|
||||
for d in self.item_attribute_values:
|
||||
d.abbr = d.abbr.upper()
|
||||
if d.attribute_value in values:
|
||||
frappe.throw(_("{0} must appear only once").format(d.attribute_value))
|
||||
if d.attribute_value.lower() in map(str.lower, values):
|
||||
frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title()))
|
||||
values.append(d.attribute_value)
|
||||
|
||||
if d.abbr in abbrs:
|
||||
frappe.throw(_("{0} must appear only once").format(d.abbr))
|
||||
if d.abbr.lower() in map(str.lower, abbrs):
|
||||
frappe.throw(_("Abbreviation: {0} must appear only once").format(d.abbr.title()))
|
||||
abbrs.append(d.abbr)
|
||||
|
||||
@@ -226,8 +226,10 @@ def validate_item_details(args, item):
|
||||
|
||||
validate_end_of_life(item.name, item.end_of_life, item.disabled)
|
||||
|
||||
if args.transaction_type == "selling" and cint(item.has_variants):
|
||||
throw(_("Item {0} is a template, please select one of its variants").format(item.name))
|
||||
if cint(item.has_variants):
|
||||
msg = f"Item {item.name} is a template, please select one of its variants"
|
||||
|
||||
throw(_(msg), title=_("Template Item Selected"))
|
||||
|
||||
elif args.transaction_type == "buying" and args.doctype != "Material Request":
|
||||
if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != 1:
|
||||
|
||||
Reference in New Issue
Block a user