mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-26 16:34:46 +00:00
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
|
import requests
|
||||||
|
|
||||||
docs_repos = [
|
WEBSITE_REPOS = [
|
||||||
"frappe_docs",
|
|
||||||
"erpnext_documentation",
|
|
||||||
"erpnext_com",
|
"erpnext_com",
|
||||||
"frappe_io",
|
"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):
|
def is_valid_url(url: str) -> bool:
|
||||||
for line in body.splitlines():
|
parts = urlparse(url)
|
||||||
for word in line.split():
|
return all((parts.scheme, parts.netloc, parts.path))
|
||||||
if word.startswith('http') and uri_validator(word):
|
|
||||||
parsed_url = urlparse(word)
|
|
||||||
if parsed_url.netloc == "github.com":
|
def is_documentation_link(word: str) -> bool:
|
||||||
parts = parsed_url.path.split('/')
|
if not word.startswith("http") or not is_valid_url(word):
|
||||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
return False
|
||||||
return True
|
|
||||||
elif parsed_url.netloc == "docs.erpnext.com":
|
parsed_url = urlparse(word)
|
||||||
return True
|
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__":
|
if __name__ == "__main__":
|
||||||
pr = sys.argv[1]
|
exit_code, message = check_pull_request(sys.argv[1])
|
||||||
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
|
print(message)
|
||||||
|
sys.exit(exit_code)
|
||||||
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... 🏃")
|
|
||||||
|
|||||||
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: 'Setup Environment'
|
- name: 'Setup Environment'
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.6
|
python-version: '3.10'
|
||||||
|
|
||||||
- name: 'Clone repo'
|
- name: 'Clone repo'
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|||||||
@@ -49,7 +49,6 @@
|
|||||||
<br>
|
<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ _("Against") }}: {{ row.against }}
|
|
||||||
<br>{{ _("Remarks") }}: {{ row.remarks }}
|
<br>{{ _("Remarks") }}: {{ row.remarks }}
|
||||||
{% if row.bill_no %}
|
{% if row.bill_no %}
|
||||||
<br>{{ _("Supplier Invoice No") }}: {{ 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.contacts.doctype.address.address import get_address_display
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
from frappe.utils import (
|
from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
|
||||||
add_days,
|
|
||||||
add_months,
|
|
||||||
cint,
|
|
||||||
cstr,
|
|
||||||
flt,
|
|
||||||
formatdate,
|
|
||||||
get_link_to_form,
|
|
||||||
getdate,
|
|
||||||
nowdate,
|
|
||||||
)
|
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
|
|
||||||
import erpnext
|
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.party import get_due_date, get_party_account, get_party_details
|
||||||
from erpnext.accounts.utils import get_account_currency
|
from erpnext.accounts.utils import get_account_currency
|
||||||
from erpnext.assets.doctype.asset.depreciation import (
|
from erpnext.assets.doctype.asset.depreciation import (
|
||||||
|
depreciate_asset,
|
||||||
get_disposal_account_and_cost_center,
|
get_disposal_account_and_cost_center,
|
||||||
get_gl_entries_on_asset_disposal,
|
get_gl_entries_on_asset_disposal,
|
||||||
get_gl_entries_on_asset_regain,
|
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.accounts_controller import validate_account_head
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
@@ -1114,18 +1106,20 @@ class SalesInvoice(SellingController):
|
|||||||
asset = self.get_asset(item)
|
asset = self.get_asset(item)
|
||||||
|
|
||||||
if self.is_return:
|
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(
|
fixed_asset_gl_entries = get_gl_entries_on_asset_regain(
|
||||||
asset, item.base_net_amount, item.finance_book
|
asset, item.base_net_amount, item.finance_book
|
||||||
)
|
)
|
||||||
asset.db_set("disposal_date", None)
|
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:
|
else:
|
||||||
if asset.calculate_depreciation:
|
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(
|
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||||
asset, item.base_net_amount, item.finance_book
|
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)
|
_("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
|
@property
|
||||||
def enable_discount_accounting(self):
|
def enable_discount_accounting(self):
|
||||||
if not hasattr(self, "_enable_discount_accounting"):
|
if not hasattr(self, "_enable_discount_accounting"):
|
||||||
|
|||||||
@@ -1117,6 +1117,46 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.sql("delete from `tabPOS Profile`")
|
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):
|
def test_pos_si_without_payment(self):
|
||||||
make_pos_profile()
|
make_pos_profile()
|
||||||
|
|
||||||
|
|||||||
@@ -378,15 +378,14 @@ class Deferred_Revenue_and_Expense_Report(object):
|
|||||||
ret += [{}]
|
ret += [{}]
|
||||||
|
|
||||||
# add total row
|
# add total row
|
||||||
if ret is not []:
|
if self.filters.type == "Revenue":
|
||||||
if self.filters.type == "Revenue":
|
total_row = frappe._dict({"name": "Total Deferred Income"})
|
||||||
total_row = frappe._dict({"name": "Total Deferred Income"})
|
elif self.filters.type == "Expense":
|
||||||
elif self.filters.type == "Expense":
|
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||||
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
|
||||||
|
|
||||||
for idx, period in enumerate(self.period_list, 0):
|
for idx, period in enumerate(self.period_list, 0):
|
||||||
total_row[period.key] = self.period_total[idx].total
|
total_row[period.key] = self.period_total[idx].total
|
||||||
ret.append(total_row)
|
ret.append(total_row)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 12%">{%= __("Date") %}</th>
|
<th style="width: 12%">{%= __("Date") %}</th>
|
||||||
<th style="width: 15%">{%= __("Ref") %}</th>
|
<th style="width: 15%">{%= __("Reference") %}</th>
|
||||||
<th style="width: 25%">{%= __("Party") %}</th>
|
<th style="width: 25%">{%= __("Remarks") %}</th>
|
||||||
<th style="width: 15%">{%= __("Debit") %}</th>
|
<th style="width: 15%">{%= __("Debit") %}</th>
|
||||||
<th style="width: 15%">{%= __("Credit") %}</th>
|
<th style="width: 15%">{%= __("Credit") %}</th>
|
||||||
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
|
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
|
||||||
@@ -45,7 +45,6 @@
|
|||||||
<br>
|
<br>
|
||||||
{% } %}
|
{% } %}
|
||||||
|
|
||||||
{{ __("Against") }}: {%= data[i].against %}
|
|
||||||
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
||||||
{% if(data[i].bill_no) { %}
|
{% if(data[i].bill_no) { %}
|
||||||
<br>{%= __("Supplier Invoice No") %}: {%= 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) {
|
setup_chart: function(frm) {
|
||||||
|
if(frm.doc.finance_books.length > 1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var x_intervals = [frm.doc.purchase_date];
|
var x_intervals = [frm.doc.purchase_date];
|
||||||
var asset_values = [frm.doc.gross_purchase_amount];
|
var asset_values = [frm.doc.gross_purchase_amount];
|
||||||
var last_depreciation_date = frm.doc.purchase_date;
|
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)
|
_("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:
|
if self.calculate_depreciation:
|
||||||
self.value_after_depreciation = 0
|
self.value_after_depreciation = 0
|
||||||
self.set_depreciation_rate()
|
self.set_depreciation_rate()
|
||||||
self.make_depreciation_schedule(date_of_sale)
|
self.make_depreciation_schedule(date_of_disposal)
|
||||||
self.set_accumulated_depreciation(date_of_sale, date_of_return)
|
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
|
||||||
else:
|
else:
|
||||||
self.finance_books = []
|
self.finance_books = []
|
||||||
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
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")
|
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(
|
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
|
||||||
"schedules"
|
"schedules"
|
||||||
):
|
):
|
||||||
@@ -279,17 +279,17 @@ class Asset(AccountsController):
|
|||||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||||
|
|
||||||
# if asset is being sold
|
# if asset is being sold
|
||||||
if date_of_sale:
|
if date_of_disposal:
|
||||||
from_date = self.get_from_date(finance_book.finance_book)
|
from_date = self.get_from_date(finance_book.finance_book)
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(
|
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:
|
if depreciation_amount > 0:
|
||||||
self.append(
|
self.append(
|
||||||
"schedules",
|
"schedules",
|
||||||
{
|
{
|
||||||
"schedule_date": date_of_sale,
|
"schedule_date": date_of_disposal,
|
||||||
"depreciation_amount": depreciation_amount,
|
"depreciation_amount": depreciation_amount,
|
||||||
"depreciation_method": finance_book.depreciation_method,
|
"depreciation_method": finance_book.depreciation_method,
|
||||||
"finance_book": finance_book.finance_book,
|
"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
|
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
|
||||||
# JE: Journal Entry, FB: Finance Book
|
# JE: Journal Entry, FB: Finance Book
|
||||||
def clear_depreciation_schedule(self):
|
def clear_depreciation_schedule(self):
|
||||||
@@ -399,6 +402,14 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
return start
|
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):
|
def get_from_date(self, finance_book):
|
||||||
if not self.get("schedules"):
|
if not self.get("schedules"):
|
||||||
return self.available_for_use_date
|
return self.available_for_use_date
|
||||||
@@ -531,7 +542,7 @@ class Asset(AccountsController):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def set_accumulated_depreciation(
|
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 = [
|
straight_line_idx = [
|
||||||
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
|
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
|
||||||
@@ -554,7 +565,7 @@ class Asset(AccountsController):
|
|||||||
if (
|
if (
|
||||||
straight_line_idx
|
straight_line_idx
|
||||||
and i == max(straight_line_idx) - 1
|
and i == max(straight_line_idx) - 1
|
||||||
and not date_of_sale
|
and not date_of_disposal
|
||||||
and not date_of_return
|
and not date_of_return
|
||||||
):
|
):
|
||||||
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
|
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
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 frappe.utils.user import get_users_with_role
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
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)
|
_("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(
|
depreciation_series = frappe.get_cached_value(
|
||||||
"Company", asset.company, "series_for_depreciation_entry"
|
"Company", asset.company, "series_for_depreciation_entry"
|
||||||
)
|
)
|
||||||
@@ -254,7 +259,7 @@ def scrap_asset(asset_name):
|
|||||||
je = frappe.new_doc("Journal Entry")
|
je = frappe.new_doc("Journal Entry")
|
||||||
je.voucher_type = "Journal Entry"
|
je.voucher_type = "Journal Entry"
|
||||||
je.naming_series = depreciation_series
|
je.naming_series = depreciation_series
|
||||||
je.posting_date = today()
|
je.posting_date = date
|
||||||
je.company = asset.company
|
je.company = asset.company
|
||||||
je.remark = "Scrap Entry for asset {0}".format(asset_name)
|
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.flags.ignore_permissions = True
|
||||||
je.submit()
|
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)
|
frappe.db.set_value("Asset", asset_name, "journal_entry_for_scrap", je.name)
|
||||||
asset.set_status("Scrapped")
|
asset.set_status("Scrapped")
|
||||||
|
|
||||||
@@ -276,6 +281,9 @@ def scrap_asset(asset_name):
|
|||||||
def restore_asset(asset_name):
|
def restore_asset(asset_name):
|
||||||
asset = frappe.get_doc("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
|
je = asset.journal_entry_for_scrap
|
||||||
|
|
||||||
asset.db_set("disposal_date", None)
|
asset.db_set("disposal_date", None)
|
||||||
@@ -286,6 +294,96 @@ def restore_asset(asset_name):
|
|||||||
asset.set_status()
|
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):
|
def get_gl_entries_on_asset_regain(asset, selling_amount=0, finance_book=None):
|
||||||
(
|
(
|
||||||
fixed_asset_account,
|
fixed_asset_account,
|
||||||
|
|||||||
@@ -4,7 +4,16 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
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.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice, update_maintenance_status
|
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)
|
self.assertEqual(doc.items[0].is_fixed_asset, 1)
|
||||||
|
|
||||||
def test_scrap_asset(self):
|
def test_scrap_asset(self):
|
||||||
|
date = nowdate()
|
||||||
|
purchase_date = add_months(get_first_day(date), -2)
|
||||||
|
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
available_for_use_date="2020-01-01",
|
available_for_use_date=purchase_date,
|
||||||
purchase_date="2020-01-01",
|
purchase_date=purchase_date,
|
||||||
expected_value_after_useful_life=10000,
|
expected_value_after_useful_life=10000,
|
||||||
total_number_of_depreciations=10,
|
total_number_of_depreciations=10,
|
||||||
frequency_of_depreciation=1,
|
frequency_of_depreciation=1,
|
||||||
submit=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)
|
scrap_asset(asset.name)
|
||||||
|
|
||||||
asset.load_from_db()
|
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.assertEqual(asset.status, "Scrapped")
|
||||||
self.assertTrue(asset.journal_entry_for_scrap)
|
self.assertTrue(asset.journal_entry_for_scrap)
|
||||||
|
|
||||||
expected_gle = (
|
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 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(
|
gle = frappe.db.sql(
|
||||||
@@ -183,7 +223,7 @@ class TestAsset(AssetSetup):
|
|||||||
order by account""",
|
order by account""",
|
||||||
asset.journal_entry_for_scrap,
|
asset.journal_entry_for_scrap,
|
||||||
)
|
)
|
||||||
self.assertEqual(gle, expected_gle)
|
self.assertSequenceEqual(gle, expected_gle)
|
||||||
|
|
||||||
restore_asset(asset.name)
|
restore_asset(asset.name)
|
||||||
|
|
||||||
@@ -191,34 +231,57 @@ class TestAsset(AssetSetup):
|
|||||||
self.assertFalse(asset.journal_entry_for_scrap)
|
self.assertFalse(asset.journal_entry_for_scrap)
|
||||||
self.assertEqual(asset.status, "Partially Depreciated")
|
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):
|
def test_gle_made_by_asset_sale(self):
|
||||||
|
date = nowdate()
|
||||||
|
purchase_date = add_months(get_first_day(date), -2)
|
||||||
|
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
calculate_depreciation=1,
|
calculate_depreciation=1,
|
||||||
available_for_use_date="2020-06-06",
|
available_for_use_date=purchase_date,
|
||||||
purchase_date="2020-01-01",
|
purchase_date=purchase_date,
|
||||||
expected_value_after_useful_life=10000,
|
expected_value_after_useful_life=10000,
|
||||||
total_number_of_depreciations=3,
|
total_number_of_depreciations=10,
|
||||||
frequency_of_depreciation=10,
|
frequency_of_depreciation=1,
|
||||||
depreciation_start_date="2020-12-31",
|
|
||||||
submit=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 = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
|
||||||
si.customer = "_Test Customer"
|
si.customer = "_Test Customer"
|
||||||
si.set_posting_time = 1
|
si.due_date = nowdate()
|
||||||
si.posting_date = "2021-10-31"
|
si.get("items")[0].rate = 25000
|
||||||
si.due_date = "2021-10-31"
|
si.insert()
|
||||||
si.get("items")[0].rate = 75000
|
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
|
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 = (
|
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 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(
|
gle = frappe.db.sql(
|
||||||
@@ -228,14 +291,9 @@ class TestAsset(AssetSetup):
|
|||||||
si.name,
|
si.name,
|
||||||
)
|
)
|
||||||
|
|
||||||
for i, gle_entry in enumerate(gle):
|
self.assertSequenceEqual(gle, expected_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))
|
|
||||||
|
|
||||||
si.load_from_db()
|
|
||||||
si.cancel()
|
si.cancel()
|
||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
|
||||||
|
|
||||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
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):
|
def enable_cwip_accounting(asset_category, enable=1):
|
||||||
frappe.db.set_value("Asset Category", asset_category, "enable_cwip_accounting", enable)
|
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)
|
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):
|
def make_pr_against_po(po, received_qty=0):
|
||||||
pr = make_purchase_receipt(po)
|
pr = make_purchase_receipt(po)
|
||||||
@@ -1342,8 +1347,8 @@ def create_purchase_order(**args):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
po.set_missing_values()
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
|
po.set_missing_values()
|
||||||
po.insert()
|
po.insert()
|
||||||
if not args.do_not_submit:
|
if not args.do_not_submit:
|
||||||
if po.is_subcontracted == "Yes":
|
if po.is_subcontracted == "Yes":
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class SellingController(StockController):
|
|||||||
def onload(self):
|
def onload(self):
|
||||||
super(SellingController, self).onload()
|
super(SellingController, self).onload()
|
||||||
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
|
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))
|
item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
|
||||||
|
|
||||||
def validate(self):
|
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",
|
"eval:(self.per_delivered == 100 or self.skip_delivery_note) and self.per_billed == 100 and self.docstatus == 1",
|
||||||
],
|
],
|
||||||
["Cancelled", "eval:self.docstatus==2"],
|
["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'"],
|
["On Hold", "eval:self.status=='On Hold'"],
|
||||||
],
|
],
|
||||||
"Purchase Order": [
|
"Purchase Order": [
|
||||||
@@ -79,7 +79,7 @@ status_map = {
|
|||||||
["Delivered", "eval:self.status=='Delivered'"],
|
["Delivered", "eval:self.status=='Delivered'"],
|
||||||
["Cancelled", "eval:self.docstatus==2"],
|
["Cancelled", "eval:self.docstatus==2"],
|
||||||
["On Hold", "eval:self.status=='On Hold'"],
|
["On Hold", "eval:self.status=='On Hold'"],
|
||||||
["Closed", "eval:self.status=='Closed'"],
|
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||||
],
|
],
|
||||||
"Delivery Note": [
|
"Delivery Note": [
|
||||||
["Draft", None],
|
["Draft", None],
|
||||||
@@ -87,7 +87,7 @@ status_map = {
|
|||||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||||
["Cancelled", "eval:self.docstatus==2"],
|
["Cancelled", "eval:self.docstatus==2"],
|
||||||
["Closed", "eval:self.status=='Closed'"],
|
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||||
],
|
],
|
||||||
"Purchase Receipt": [
|
"Purchase Receipt": [
|
||||||
["Draft", None],
|
["Draft", None],
|
||||||
@@ -95,7 +95,7 @@ status_map = {
|
|||||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||||
["Cancelled", "eval:self.docstatus==2"],
|
["Cancelled", "eval:self.docstatus==2"],
|
||||||
["Closed", "eval:self.status=='Closed'"],
|
["Closed", "eval:self.status=='Closed' and self.docstatus != 2"],
|
||||||
],
|
],
|
||||||
"Material Request": [
|
"Material Request": [
|
||||||
["Draft", None],
|
["Draft", None],
|
||||||
|
|||||||
@@ -173,7 +173,10 @@ class TestWebsiteItem(unittest.TestCase):
|
|||||||
# Website Item Portal Tests Begin
|
# Website Item Portal Tests Begin
|
||||||
|
|
||||||
def test_website_item_breadcrumbs(self):
|
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
|
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
|
||||||
|
|
||||||
item_code = "Test Breadcrumb Item"
|
item_code = "Test Breadcrumb Item"
|
||||||
@@ -196,7 +199,7 @@ class TestWebsiteItem(unittest.TestCase):
|
|||||||
breadcrumbs = get_parent_item_groups(item.item_group)
|
breadcrumbs = get_parent_item_groups(item.item_group)
|
||||||
|
|
||||||
self.assertEqual(breadcrumbs[0]["name"], "Home")
|
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[2]["name"], "_Test Item Group B") # parent item group
|
||||||
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
|
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
|
||||||
|
|
||||||
|
|||||||
@@ -13,38 +13,24 @@ frappe.query_reports["Work Order Summary"] = {
|
|||||||
reqd: 1
|
reqd: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "fiscal_year",
|
label: __("Based On"),
|
||||||
label: __("Fiscal Year"),
|
fieldname:"based_on",
|
||||||
fieldtype: "Link",
|
fieldtype: "Select",
|
||||||
options: "Fiscal Year",
|
options: "Creation Date\nPlanned Date\nActual Date",
|
||||||
default: frappe.defaults.get_user_default("fiscal_year"),
|
default: "Creation Date"
|
||||||
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: __("From Posting Date"),
|
label: __("From Posting Date"),
|
||||||
fieldname:"from_date",
|
fieldname:"from_date",
|
||||||
fieldtype: "Date",
|
fieldtype: "Date",
|
||||||
default: frappe.defaults.get_user_default("year_start_date"),
|
default: frappe.datetime.add_months(frappe.datetime.get_today(), -3),
|
||||||
reqd: 1
|
reqd: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("To Posting Date"),
|
label: __("To Posting Date"),
|
||||||
fieldname:"to_date",
|
fieldname:"to_date",
|
||||||
fieldtype: "Date",
|
fieldtype: "Date",
|
||||||
default: frappe.defaults.get_user_default("year_end_date"),
|
default: frappe.datetime.get_today(),
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ def get_data(filters):
|
|||||||
"sales_order",
|
"sales_order",
|
||||||
"production_item",
|
"production_item",
|
||||||
"qty",
|
"qty",
|
||||||
|
"creation",
|
||||||
"produced_qty",
|
"produced_qty",
|
||||||
"planned_start_date",
|
"planned_start_date",
|
||||||
"planned_end_date",
|
"planned_end_date",
|
||||||
@@ -47,8 +48,14 @@ def get_data(filters):
|
|||||||
if filters.get(field):
|
if filters.get(field):
|
||||||
query_filters[field] = filters.get(field)
|
query_filters[field] = filters.get(field)
|
||||||
|
|
||||||
query_filters["planned_start_date"] = (">=", filters.get("from_date"))
|
if filters.get("based_on") == "Planned Date":
|
||||||
query_filters["planned_end_date"] = ("<=", filters.get("to_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(
|
data = frappe.get_all(
|
||||||
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
|
"Work Order", fields=fields, filters=query_filters, order_by="planned_start_date asc"
|
||||||
@@ -212,6 +219,12 @@ def get_columns(filters):
|
|||||||
"options": "Sales Order",
|
"options": "Sales Order",
|
||||||
"width": 90,
|
"width": 90,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": _("Created On"),
|
||||||
|
"fieldname": "creation",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"width": 150,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": _("Planned Start Date"),
|
"label": _("Planned Start Date"),
|
||||||
"fieldname": "planned_start_date",
|
"fieldname": "planned_start_date",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from erpnext.stock.stock_ledger import update_entries_after
|
|||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
doctypes_to_reload = [
|
doctypes_to_reload = [
|
||||||
|
("setup", "company"),
|
||||||
("stock", "repost_item_valuation"),
|
("stock", "repost_item_valuation"),
|
||||||
("stock", "stock_entry_detail"),
|
("stock", "stock_entry_detail"),
|
||||||
("stock", "purchase_receipt_item"),
|
("stock", "purchase_receipt_item"),
|
||||||
|
|||||||
@@ -115,24 +115,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
|||||||
calculate_item_values: function() {
|
calculate_item_values: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
if (!this.discount_amount_applied) {
|
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);
|
frappe.model.round_floats_in(item);
|
||||||
item.net_rate = item.rate;
|
item.net_rate = item.rate;
|
||||||
|
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
|
||||||
if ((!item.qty) && me.frm.doc.is_return) {
|
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
|
||||||
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.item_tax_amount = 0.0;
|
item.item_tax_amount = 0.0;
|
||||||
item.total_weight = flt(item.weight_per_unit * item.stock_qty);
|
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"]);
|
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
|
# if export invoice, then taxes can be empty
|
||||||
# invoice can only be ineligible if no taxes applied and is not an export invoice
|
# 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"))
|
has_non_gst_item = any(d for d in doc.get("items", []) if d.get("is_non_gst"))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -546,6 +546,42 @@ class TestSalesOrder(FrappeTestCase):
|
|||||||
workflow.is_active = 0
|
workflow.is_active = 0
|
||||||
workflow.save()
|
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):
|
def test_update_child_product_bundle(self):
|
||||||
# test Update Items with product bundle
|
# test Update Items with product bundle
|
||||||
if not frappe.db.exists("Item", "_Product Bundle Item"):
|
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):
|
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"):
|
if from_item and frappe.request.environ.get("HTTP_REFERER"):
|
||||||
# base page after 'Home' will vary on Item page
|
# base page after 'Home' will vary on Item page
|
||||||
last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
|
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_title = " ".join(last_page.split("-")).title()
|
||||||
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
|
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
|
||||||
|
|
||||||
|
|||||||
@@ -490,6 +490,46 @@ class TestDeliveryNote(FrappeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(gle_warehouse_amount, 1400)
|
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):
|
def test_return_for_serialized_items(self):
|
||||||
se = make_serialized_item()
|
se = make_serialized_item()
|
||||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
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")
|
update_delivery_note_status(dn.name, "Closed")
|
||||||
self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "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):
|
def test_dn_billing_status_case1(self):
|
||||||
# SO -> DN -> SI
|
# SO -> DN -> SI
|
||||||
so = make_sales_order()
|
so = make_sales_order()
|
||||||
|
|||||||
@@ -74,11 +74,10 @@ class ItemAttribute(Document):
|
|||||||
def validate_duplication(self):
|
def validate_duplication(self):
|
||||||
values, abbrs = [], []
|
values, abbrs = [], []
|
||||||
for d in self.item_attribute_values:
|
for d in self.item_attribute_values:
|
||||||
d.abbr = d.abbr.upper()
|
if d.attribute_value.lower() in map(str.lower, values):
|
||||||
if d.attribute_value in values:
|
frappe.throw(_("Attribute value: {0} must appear only once").format(d.attribute_value.title()))
|
||||||
frappe.throw(_("{0} must appear only once").format(d.attribute_value))
|
|
||||||
values.append(d.attribute_value)
|
values.append(d.attribute_value)
|
||||||
|
|
||||||
if d.abbr in abbrs:
|
if d.abbr.lower() in map(str.lower, abbrs):
|
||||||
frappe.throw(_("{0} must appear only once").format(d.abbr))
|
frappe.throw(_("Abbreviation: {0} must appear only once").format(d.abbr.title()))
|
||||||
abbrs.append(d.abbr)
|
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)
|
validate_end_of_life(item.name, item.end_of_life, item.disabled)
|
||||||
|
|
||||||
if args.transaction_type == "selling" and cint(item.has_variants):
|
if cint(item.has_variants):
|
||||||
throw(_("Item {0} is a template, please select one of its variants").format(item.name))
|
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":
|
elif args.transaction_type == "buying" and args.doctype != "Material Request":
|
||||||
if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != 1:
|
if args.get("is_subcontracted") == "Yes" and item.is_sub_contracted_item != 1:
|
||||||
|
|||||||
Reference in New Issue
Block a user