Merge branch 'develop' into consider-posting-time-for-internal-po
This commit is contained in:
commit
0d732609f0
@ -9,6 +9,7 @@ pull_request_rules:
|
||||
- author!=nabinhait
|
||||
- author!=ankush
|
||||
- author!=deepeshgarg007
|
||||
- author!=frappe-pr-bot
|
||||
- author!=mergify[bot]
|
||||
|
||||
- or:
|
||||
|
@ -82,6 +82,8 @@ GNU/General Public License (see [license.txt](license.txt))
|
||||
|
||||
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
|
||||
|
||||
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
|
||||
|
||||
## Logo and Trademark Policy
|
||||
|
||||
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).
|
||||
|
@ -53,15 +53,13 @@ class BankStatementImport(DataImport):
|
||||
if "Bank Account" not in json.dumps(preview["columns"]):
|
||||
frappe.throw(_("Please add the Bank Account column"))
|
||||
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.utils.background_jobs import is_job_queued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
|
||||
if self.name not in enqueued_jobs:
|
||||
if not is_job_queued(self.name):
|
||||
enqueue(
|
||||
start_import,
|
||||
queue="default",
|
||||
|
@ -4,22 +4,20 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.background_jobs import is_job_queued
|
||||
|
||||
from erpnext.accounts.doctype.account.account import merge_account
|
||||
|
||||
|
||||
class LedgerMerge(Document):
|
||||
def start_merge(self):
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
|
||||
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
|
||||
if self.name not in enqueued_jobs:
|
||||
if not is_job_queued(self.name):
|
||||
enqueue(
|
||||
start_merge,
|
||||
queue="default",
|
||||
|
@ -6,7 +6,7 @@ import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.background_jobs import enqueue, is_job_queued
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@ -207,14 +207,12 @@ class OpeningInvoiceCreationTool(Document):
|
||||
if len(invoices) < 50:
|
||||
return start_import(invoices)
|
||||
else:
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
if is_scheduler_inactive() and not frappe.flags.in_test:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
if self.name not in enqueued_jobs:
|
||||
if not is_job_queued(self.name):
|
||||
enqueue(
|
||||
start_import,
|
||||
queue="default",
|
||||
|
@ -6,11 +6,10 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.background_jobs import enqueue, is_job_queued
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
|
||||
@ -467,7 +466,7 @@ def enqueue_job(job, **kwargs):
|
||||
closing_entry = kwargs.get("closing_entry") or {}
|
||||
|
||||
job_name = closing_entry.get("name")
|
||||
if not job_already_enqueued(job_name):
|
||||
if not is_job_queued(job_name):
|
||||
enqueue(
|
||||
job,
|
||||
**kwargs,
|
||||
@ -491,12 +490,6 @@ def check_scheduler_status():
|
||||
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
|
||||
|
||||
|
||||
def job_already_enqueued(job_name):
|
||||
enqueued_jobs = [d.get("job_name") for d in get_info()]
|
||||
if job_name in enqueued_jobs:
|
||||
return True
|
||||
|
||||
|
||||
def safe_load_json(message):
|
||||
try:
|
||||
json_message = json.loads(message).get("message")
|
||||
|
@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<table class="table table-bordered" style="font-size: 10px">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
|
@ -34,8 +34,8 @@ pricing_rule_fields = [
|
||||
other_fields = [
|
||||
"min_qty",
|
||||
"max_qty",
|
||||
"min_amt",
|
||||
"max_amt",
|
||||
"min_amount",
|
||||
"max_amount",
|
||||
"priority",
|
||||
"warehouse",
|
||||
"threshold_percentage",
|
||||
@ -246,7 +246,11 @@ def prepare_pricing_rule(
|
||||
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
|
||||
pr.update(args)
|
||||
for field in other_fields + discount_fields:
|
||||
pr.set(field, child_doc_fields.get(field))
|
||||
target_field = field
|
||||
if target_field in ["min_amount", "max_amount"]:
|
||||
target_field = "min_amt" if field == "min_amount" else "max_amt"
|
||||
|
||||
pr.set(target_field, child_doc_fields.get(field))
|
||||
|
||||
pr.promotional_scheme_id = child_doc_fields.name
|
||||
pr.promotional_scheme = doc.name
|
||||
|
@ -90,6 +90,23 @@ class TestPromotionalScheme(unittest.TestCase):
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
def test_min_max_amount_configuration(self):
|
||||
ps = make_promotional_scheme()
|
||||
ps.price_discount_slabs[0].min_amount = 10
|
||||
ps.price_discount_slabs[0].max_amount = 1000
|
||||
ps.save()
|
||||
|
||||
price_rules_data = frappe.db.get_value(
|
||||
"Pricing Rule", {"promotional_scheme": ps.name}, ["min_amt", "max_amt"], as_dict=1
|
||||
)
|
||||
|
||||
self.assertEqual(price_rules_data.min_amt, 10)
|
||||
self.assertEqual(price_rules_data.max_amt, 1000)
|
||||
|
||||
frappe.delete_doc("Promotional Scheme", ps.name)
|
||||
price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name})
|
||||
self.assertEqual(price_rules, [])
|
||||
|
||||
|
||||
def make_promotional_scheme(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -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
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
@ -35,7 +25,6 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
make_depreciation_entry,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
@ -186,7 +175,7 @@ class SalesInvoice(SellingController):
|
||||
if self.update_stock:
|
||||
frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale"))
|
||||
|
||||
elif asset.status in ("Scrapped", "Cancelled") or (
|
||||
elif asset.status in ("Scrapped", "Cancelled", "Capitalized", "Decapitalized") or (
|
||||
asset.status == "Sold" and not self.is_return
|
||||
):
|
||||
frappe.throw(
|
||||
@ -1097,7 +1086,7 @@ class SalesInvoice(SellingController):
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||
self.reverse_depreciation_entry_made_after_disposal(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
|
||||
else:
|
||||
@ -1162,101 +1151,6 @@ class SalesInvoice(SellingController):
|
||||
self.check_finance_books(item, asset)
|
||||
return asset
|
||||
|
||||
def check_finance_books(self, item, asset):
|
||||
if (
|
||||
len(asset.finance_books) > 1 and not item.finance_book and asset.finance_books[0].finance_book
|
||||
):
|
||||
frappe.throw(
|
||||
_("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)
|
||||
|
||||
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()
|
||||
|
||||
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"):
|
||||
|
@ -489,7 +489,6 @@ def make_reverse_gl_entries(
|
||||
).run(as_dict=1)
|
||||
|
||||
if gl_entries:
|
||||
create_payment_ledger_entry(gl_entries, cancel=1)
|
||||
create_payment_ledger_entry(
|
||||
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
|
||||
)
|
||||
|
@ -784,7 +784,7 @@ class ReceivablePayableReport(object):
|
||||
def add_customer_filters(
|
||||
self,
|
||||
):
|
||||
self.customter = qb.DocType("Customer")
|
||||
self.customer = qb.DocType("Customer")
|
||||
|
||||
if self.filters.get("customer_group"):
|
||||
self.get_hierarchical_filters("Customer Group", "customer_group")
|
||||
@ -838,7 +838,7 @@ class ReceivablePayableReport(object):
|
||||
customer = self.customer
|
||||
groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt))
|
||||
customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups))
|
||||
self.qb_selection_filter.append(ple.isin(ple.party.isin(customers)))
|
||||
self.qb_selection_filter.append(ple.party.isin(customers))
|
||||
|
||||
def add_accounting_dimensions_filters(self):
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
@ -97,6 +97,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
row.update({"rate": d.base_net_rate, "amount": d.base_net_amount})
|
||||
|
||||
total_tax = 0
|
||||
total_other_charges = 0
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
|
||||
row.update(
|
||||
@ -105,10 +106,18 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0),
|
||||
}
|
||||
)
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
if item_tax.get("is_other_charges"):
|
||||
total_other_charges += flt(item_tax.get("tax_amount"))
|
||||
else:
|
||||
total_tax += flt(item_tax.get("tax_amount"))
|
||||
|
||||
row.update(
|
||||
{"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency}
|
||||
{
|
||||
"total_tax": total_tax,
|
||||
"total_other_charges": total_other_charges,
|
||||
"total": d.base_net_amount + total_tax,
|
||||
"currency": company_currency,
|
||||
}
|
||||
)
|
||||
|
||||
if filters.get("group_by"):
|
||||
@ -477,7 +486,7 @@ def get_tax_accounts(
|
||||
tax_details = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, parent, description, item_wise_tax_detail,
|
||||
name, parent, description, item_wise_tax_detail, account_head,
|
||||
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
|
||||
from `tab%s`
|
||||
where
|
||||
@ -493,11 +502,22 @@ def get_tax_accounts(
|
||||
tuple([doctype] + list(invoice_item_row)),
|
||||
)
|
||||
|
||||
account_doctype = frappe.qb.DocType("Account")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(account_doctype)
|
||||
.select(account_doctype.name)
|
||||
.where((account_doctype.account_type == "Tax"))
|
||||
)
|
||||
|
||||
tax_accounts = query.run()
|
||||
|
||||
for (
|
||||
name,
|
||||
parent,
|
||||
description,
|
||||
item_wise_tax_detail,
|
||||
account_head,
|
||||
charge_type,
|
||||
add_deduct_tax,
|
||||
tax_amount,
|
||||
@ -540,7 +560,11 @@ def get_tax_accounts(
|
||||
)
|
||||
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
{"tax_rate": tax_rate, "tax_amount": tax_value}
|
||||
{
|
||||
"tax_rate": tax_rate,
|
||||
"tax_amount": tax_value,
|
||||
"is_other_charges": 0 if tuple([account_head]) in tax_accounts else 1,
|
||||
}
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
@ -583,6 +607,13 @@ def get_tax_accounts(
|
||||
"options": "currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Total Other Charges"),
|
||||
"fieldname": "total_other_charges",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Total"),
|
||||
"fieldname": "total",
|
||||
|
@ -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"
|
||||
):
|
||||
@ -235,9 +235,9 @@ class Asset(AccountsController):
|
||||
start = self.clear_depreciation_schedule()
|
||||
|
||||
for finance_book in self.get("finance_books"):
|
||||
self._make_depreciation_schedule(finance_book, start, date_of_sale)
|
||||
self._make_depreciation_schedule(finance_book, start, date_of_disposal)
|
||||
|
||||
def _make_depreciation_schedule(self, finance_book, start, date_of_sale):
|
||||
def _make_depreciation_schedule(self, finance_book, start, date_of_disposal):
|
||||
self.validate_asset_finance_books(finance_book)
|
||||
|
||||
value_after_depreciation = self._get_value_after_depreciation(finance_book)
|
||||
@ -274,15 +274,15 @@ 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._add_depreciation_row(
|
||||
date_of_sale,
|
||||
date_of_disposal,
|
||||
depreciation_amount,
|
||||
finance_book.depreciation_method,
|
||||
finance_book.finance_book,
|
||||
|
@ -10,6 +10,9 @@ frappe.listview_settings['Asset'] = {
|
||||
} else if (doc.status === "Sold") {
|
||||
return [__("Sold"), "green", "status,=,Sold"];
|
||||
|
||||
} else if (["Capitalized", "Decapitalized"].includes(doc.status)) {
|
||||
return [__(doc.status), "grey", "status,=," + doc.status];
|
||||
|
||||
} else if (doc.status === "Scrapped") {
|
||||
return [__("Scrapped"), "grey", "status,=,Scrapped"];
|
||||
|
||||
|
@ -11,7 +11,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
)
|
||||
|
||||
|
||||
def post_depreciation_entries(date=None):
|
||||
def post_depreciation_entries(date=None, commit=True):
|
||||
# Return if automatic booking of asset depreciation is disabled
|
||||
if not cint(
|
||||
frappe.db.get_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically")
|
||||
@ -22,7 +22,8 @@ def post_depreciation_entries(date=None):
|
||||
date = today()
|
||||
for asset in get_depreciable_assets(date):
|
||||
make_depreciation_entry(asset, date)
|
||||
frappe.db.commit()
|
||||
if commit:
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_depreciable_assets(date):
|
||||
@ -190,7 +191,7 @@ def scrap_asset(asset_name):
|
||||
|
||||
if asset.docstatus != 1:
|
||||
frappe.throw(_("Asset {0} must be submitted").format(asset.name))
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped"):
|
||||
elif asset.status in ("Cancelled", "Sold", "Scrapped", "Capitalized", "Decapitalized"):
|
||||
frappe.throw(
|
||||
_("Asset {0} cannot be scrapped, as it is already {1}").format(asset.name, asset.status)
|
||||
)
|
||||
@ -358,3 +359,30 @@ def get_disposal_account_and_cost_center(company):
|
||||
frappe.throw(_("Please set 'Asset Depreciation Cost Center' in Company {0}").format(company))
|
||||
|
||||
return disposal_account, depreciation_cost_center
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
|
||||
asset_doc = frappe.get_doc("Asset", asset)
|
||||
|
||||
if asset_doc.calculate_depreciation:
|
||||
asset_doc.prepare_depreciation_data(getdate(disposal_date))
|
||||
|
||||
finance_book_id = 1
|
||||
if finance_book:
|
||||
for fb in asset_doc.finance_books:
|
||||
if fb.finance_book == finance_book:
|
||||
finance_book_id = fb.idx
|
||||
break
|
||||
|
||||
asset_schedules = [
|
||||
sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id
|
||||
]
|
||||
accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount
|
||||
|
||||
return flt(
|
||||
flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
|
||||
asset_doc.precision("gross_purchase_amount"),
|
||||
)
|
||||
else:
|
||||
return flt(asset_doc.value_after_depreciation)
|
||||
|
@ -1425,6 +1425,16 @@ def create_asset_category():
|
||||
"depreciation_expense_account": "_Test Depreciations - _TC",
|
||||
},
|
||||
)
|
||||
asset_category.append(
|
||||
"accounts",
|
||||
{
|
||||
"company_name": "_Test Company with perpetual inventory",
|
||||
"fixed_asset_account": "_Test Fixed Asset - TCP1",
|
||||
"accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1",
|
||||
"depreciation_expense_account": "_Test Depreciations - TCP1",
|
||||
},
|
||||
)
|
||||
|
||||
asset_category.insert()
|
||||
|
||||
|
||||
|
@ -0,0 +1,417 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.provide("erpnext.assets");
|
||||
|
||||
|
||||
erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.stock.StockController {
|
||||
setup() {
|
||||
this.setup_posting_date_time_check();
|
||||
}
|
||||
|
||||
onload() {
|
||||
this.setup_queries();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
erpnext.hide_company();
|
||||
this.show_general_ledger();
|
||||
if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) {
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
var me = this;
|
||||
|
||||
me.setup_warehouse_query();
|
||||
|
||||
me.frm.set_query("target_item_code", function() {
|
||||
if (me.frm.doc.entry_type == "Capitalization") {
|
||||
return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 1});
|
||||
} else {
|
||||
return erpnext.queries.item({"is_stock_item": 1, "is_fixed_asset": 0});
|
||||
}
|
||||
});
|
||||
|
||||
me.frm.set_query("target_asset", function() {
|
||||
var filters = {};
|
||||
|
||||
if (me.frm.doc.target_item_code) {
|
||||
filters['item_code'] = me.frm.doc.target_item_code;
|
||||
}
|
||||
|
||||
filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]];
|
||||
filters['docstatus'] = 1;
|
||||
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
|
||||
me.frm.set_query("asset", "asset_items", function() {
|
||||
var filters = {
|
||||
'status': ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]],
|
||||
'docstatus': 1
|
||||
};
|
||||
|
||||
if (me.frm.doc.target_asset) {
|
||||
filters['name'] = ['!=', me.frm.doc.target_asset];
|
||||
}
|
||||
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
|
||||
me.frm.set_query("item_code", "stock_items", function() {
|
||||
return erpnext.queries.item({"is_stock_item": 1});
|
||||
});
|
||||
|
||||
me.frm.set_query("item_code", "service_items", function() {
|
||||
return erpnext.queries.item({"is_stock_item": 0, "is_fixed_asset": 0});
|
||||
});
|
||||
|
||||
me.frm.set_query('batch_no', 'stock_items', function(doc, cdt, cdn) {
|
||||
var item = locals[cdt][cdn];
|
||||
if (!item.item_code) {
|
||||
frappe.throw(__("Please enter Item Code to get Batch Number"));
|
||||
} else {
|
||||
var filters = {
|
||||
'item_code': item.item_code,
|
||||
'posting_date': me.frm.doc.posting_date || frappe.datetime.nowdate(),
|
||||
'warehouse': item.warehouse
|
||||
};
|
||||
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_batch_no",
|
||||
filters: filters
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
me.frm.set_query('expense_account', 'service_items', function() {
|
||||
return {
|
||||
filters: {
|
||||
"account_type": ['in', ["Tax", "Expense Account", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]],
|
||||
"is_group": 0,
|
||||
"company": me.frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
target_item_code() {
|
||||
return this.get_target_item_details();
|
||||
}
|
||||
|
||||
target_asset() {
|
||||
return this.get_target_asset_details();
|
||||
}
|
||||
|
||||
item_code(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (cdt === "Asset Capitalization Stock Item") {
|
||||
this.get_consumed_stock_item_details(row);
|
||||
} else if (cdt == "Asset Capitalization Service Item") {
|
||||
this.get_service_item_details(row);
|
||||
}
|
||||
}
|
||||
|
||||
warehouse(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (cdt === "Asset Capitalization Stock Item") {
|
||||
this.get_warehouse_details(row);
|
||||
}
|
||||
}
|
||||
|
||||
asset(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (cdt === "Asset Capitalization Asset Item") {
|
||||
this.get_consumed_asset_details(row);
|
||||
}
|
||||
}
|
||||
|
||||
posting_date() {
|
||||
if (this.frm.doc.posting_date) {
|
||||
frappe.run_serially([
|
||||
() => this.get_all_item_warehouse_details(),
|
||||
() => this.get_all_asset_values()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
posting_time() {
|
||||
if (this.frm.doc.posting_time) {
|
||||
this.get_all_item_warehouse_details();
|
||||
}
|
||||
}
|
||||
|
||||
finance_book(doc, cdt, cdn) {
|
||||
if (cdt === "Asset Capitalization Asset Item") {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
this.get_consumed_asset_details(row);
|
||||
} else {
|
||||
this.get_all_asset_values();
|
||||
}
|
||||
}
|
||||
|
||||
stock_qty() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
qty() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
target_qty() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
rate() {
|
||||
this.calculate_totals();
|
||||
}
|
||||
|
||||
company() {
|
||||
var me = this;
|
||||
|
||||
if (me.frm.doc.company) {
|
||||
frappe.model.set_value(me.frm.doc.doctype, me.frm.doc.name, "cost_center", null);
|
||||
$.each(me.frm.doc.stock_items || [], function (i, d) {
|
||||
frappe.model.set_value(d.doctype, d.name, "cost_center", null);
|
||||
});
|
||||
$.each(me.frm.doc.asset_items || [], function (i, d) {
|
||||
frappe.model.set_value(d.doctype, d.name, "cost_center", null);
|
||||
});
|
||||
$.each(me.frm.doc.service_items || [], function (i, d) {
|
||||
frappe.model.set_value(d.doctype, d.name, "cost_center", null);
|
||||
});
|
||||
}
|
||||
|
||||
erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype);
|
||||
}
|
||||
|
||||
stock_items_add(doc, cdt, cdn) {
|
||||
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'stock_items');
|
||||
}
|
||||
|
||||
asset_items_add(doc, cdt, cdn) {
|
||||
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'asset_items');
|
||||
}
|
||||
|
||||
serivce_items_add(doc, cdt, cdn) {
|
||||
erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'service_items');
|
||||
}
|
||||
|
||||
get_target_item_details() {
|
||||
var me = this;
|
||||
|
||||
if (me.frm.doc.target_item_code) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_item_details",
|
||||
child: me.frm.doc,
|
||||
args: {
|
||||
item_code: me.frm.doc.target_item_code,
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_target_asset_details() {
|
||||
var me = this;
|
||||
|
||||
if (me.frm.doc.target_asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
|
||||
child: me.frm.doc,
|
||||
args: {
|
||||
asset: me.frm.doc.target_asset,
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_consumed_stock_item_details(row) {
|
||||
var me = this;
|
||||
|
||||
if (row && row.item_code) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_stock_item_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
item_code: row.item_code,
|
||||
warehouse: row.warehouse,
|
||||
stock_qty: flt(row.stock_qty),
|
||||
doctype: me.frm.doc.doctype,
|
||||
name: me.frm.doc.name,
|
||||
company: me.frm.doc.company,
|
||||
posting_date: me.frm.doc.posting_date,
|
||||
posting_time: me.frm.doc.posting_time,
|
||||
}
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_consumed_asset_details(row) {
|
||||
var me = this;
|
||||
|
||||
if (row && row.asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_asset_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
asset: row.asset,
|
||||
doctype: me.frm.doc.doctype,
|
||||
name: me.frm.doc.name,
|
||||
company: me.frm.doc.company,
|
||||
finance_book: row.finance_book || me.frm.doc.finance_book,
|
||||
posting_date: me.frm.doc.posting_date,
|
||||
posting_time: me.frm.doc.posting_time,
|
||||
}
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_service_item_details(row) {
|
||||
var me = this;
|
||||
|
||||
if (row && row.item_code) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_service_item_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
item_code: row.item_code,
|
||||
qty: flt(row.qty),
|
||||
expense_account: row.expense_account,
|
||||
company: me.frm.doc.company,
|
||||
}
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_warehouse_details(item) {
|
||||
var me = this;
|
||||
if (item.item_code && item.warehouse) {
|
||||
me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_warehouse_details",
|
||||
child: item,
|
||||
args: {
|
||||
args: {
|
||||
'item_code': item.item_code,
|
||||
'warehouse': cstr(item.warehouse),
|
||||
'qty': flt(item.stock_qty),
|
||||
'serial_no': item.serial_no,
|
||||
'posting_date': me.frm.doc.posting_date,
|
||||
'posting_time': me.frm.doc.posting_time,
|
||||
'company': me.frm.doc.company,
|
||||
'voucher_type': me.frm.doc.doctype,
|
||||
'voucher_no': me.frm.doc.name,
|
||||
'allow_zero_valuation': 1
|
||||
}
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_all_item_warehouse_details() {
|
||||
var me = this;
|
||||
return me.frm.call({
|
||||
method: "set_warehouse_details",
|
||||
doc: me.frm.doc,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get_all_asset_values() {
|
||||
var me = this;
|
||||
return me.frm.call({
|
||||
method: "set_asset_values",
|
||||
doc: me.frm.doc,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
calculate_totals() {
|
||||
var me = this;
|
||||
|
||||
me.frm.doc.stock_items_total = 0;
|
||||
me.frm.doc.asset_items_total = 0;
|
||||
me.frm.doc.service_items_total = 0;
|
||||
|
||||
$.each(me.frm.doc.stock_items || [], function (i, d) {
|
||||
d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), precision('amount', d));
|
||||
me.frm.doc.stock_items_total += d.amount;
|
||||
});
|
||||
|
||||
$.each(me.frm.doc.asset_items || [], function (i, d) {
|
||||
d.asset_value = flt(flt(d.asset_value), precision('asset_value', d));
|
||||
me.frm.doc.asset_items_total += d.asset_value;
|
||||
});
|
||||
|
||||
$.each(me.frm.doc.service_items || [], function (i, d) {
|
||||
d.amount = flt(flt(d.qty) * flt(d.rate), precision('amount', d));
|
||||
me.frm.doc.service_items_total += d.amount;
|
||||
});
|
||||
|
||||
me.frm.doc.stock_items_total = flt(me.frm.doc.stock_items_total, precision('stock_items_total'));
|
||||
me.frm.doc.asset_items_total = flt(me.frm.doc.asset_items_total, precision('asset_items_total'));
|
||||
me.frm.doc.service_items_total = flt(me.frm.doc.service_items_total, precision('service_items_total'));
|
||||
|
||||
me.frm.doc.total_value = me.frm.doc.stock_items_total + me.frm.doc.asset_items_total + me.frm.doc.service_items_total;
|
||||
me.frm.doc.total_value = flt(me.frm.doc.total_value, precision('total_value'));
|
||||
|
||||
me.frm.doc.target_qty = flt(me.frm.doc.target_qty, precision('target_qty'));
|
||||
me.frm.doc.target_incoming_rate = me.frm.doc.target_qty ? me.frm.doc.total_value / flt(me.frm.doc.target_qty)
|
||||
: me.frm.doc.total_value;
|
||||
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
};
|
||||
|
||||
cur_frm.cscript = new erpnext.assets.AssetCapitalization({frm: cur_frm});
|
@ -0,0 +1,381 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2021-09-04 13:38:04.217187",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"naming_series",
|
||||
"entry_type",
|
||||
"target_item_code",
|
||||
"target_item_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"column_break_9",
|
||||
"target_asset",
|
||||
"target_asset_name",
|
||||
"target_warehouse",
|
||||
"target_qty",
|
||||
"target_stock_uom",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"column_break_5",
|
||||
"company",
|
||||
"finance_book",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
"set_posting_time",
|
||||
"amended_from",
|
||||
"section_break_16",
|
||||
"stock_items",
|
||||
"stock_items_total",
|
||||
"section_break_26",
|
||||
"asset_items",
|
||||
"asset_items_total",
|
||||
"service_expenses_section",
|
||||
"service_items",
|
||||
"service_items_total",
|
||||
"totals_section",
|
||||
"total_value",
|
||||
"column_break_36",
|
||||
"target_incoming_rate",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"target_fixed_asset_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code",
|
||||
"fetch_from": "target_item_code.item_name",
|
||||
"fieldname": "target_item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Target Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "target_item_code.is_fixed_asset",
|
||||
"fieldname": "target_is_fixed_asset",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Target Is Fixed Asset",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fieldname": "target_asset",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Asset",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"no_copy": 1,
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fetch_from": "target_asset.asset_name",
|
||||
"fieldname": "target_asset_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Asset Name",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "asset.company",
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Posting Date",
|
||||
"no_copy": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "Now",
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Time",
|
||||
"label": "Posting Time",
|
||||
"no_copy": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.docstatus==0",
|
||||
"fieldname": "set_posting_time",
|
||||
"fieldtype": "Check",
|
||||
"label": "Edit Posting Date and Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Series",
|
||||
"options": "ACC-ASC-.YYYY.-",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Asset Capitalization",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.stock_items && doc.stock_items.length)",
|
||||
"fieldname": "section_break_16",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consumed Stock Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Stock Items",
|
||||
"options": "Asset Capitalization Stock Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fieldname": "target_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Warehouse",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"depends_on": "target_has_batch_no",
|
||||
"fieldname": "target_batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Batch No",
|
||||
"options": "Batch"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "target_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Target Qty",
|
||||
"read_only_depends_on": "target_is_fixed_asset"
|
||||
},
|
||||
{
|
||||
"fetch_from": "target_item_code.stock_uom",
|
||||
"fieldname": "target_stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "target_item_code.has_batch_no",
|
||||
"fieldname": "target_has_batch_no",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Target Has Batch No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "target_item_code.has_serial_no",
|
||||
"fieldname": "target_has_serial_no",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Target Has Serial No",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "target_has_serial_no",
|
||||
"fieldname": "target_serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Target Serial No"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.asset_items && doc.asset_items.length)",
|
||||
"fieldname": "section_break_26",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Consumed Asset Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "asset_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Assets",
|
||||
"options": "Asset Capitalization Asset Item"
|
||||
},
|
||||
{
|
||||
"default": "Capitalization",
|
||||
"fieldname": "entry_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Entry Type",
|
||||
"options": "Capitalization\nDecapitalization",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_items_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Consumed Stock Total Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "asset_items_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Consumed Asset Total Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.docstatus == 0 || (doc.service_items && doc.service_items.length)",
|
||||
"fieldname": "service_expenses_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Service Expenses"
|
||||
},
|
||||
{
|
||||
"fieldname": "service_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Services",
|
||||
"options": "Asset Capitalization Service Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "service_items_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Service Expense Total Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "totals_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"fieldname": "total_value",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_incoming_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Target Incoming Rate",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "target_fixed_asset_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Fixed Asset Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-12 15:09:40.771332",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Manufacturing Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Quality Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
@ -0,0 +1,749 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
|
||||
# import erpnext
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
from six import string_types
|
||||
|
||||
import erpnext
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_value_after_depreciation_on_disposal_date,
|
||||
)
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
|
||||
get_current_asset_value,
|
||||
)
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.setup.doctype.brand.brand import get_brand_defaults
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.stock.get_item_details import (
|
||||
get_default_cost_center,
|
||||
get_default_expense_account,
|
||||
get_item_warehouse,
|
||||
)
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
force_fields = [
|
||||
"target_item_name",
|
||||
"target_asset_name",
|
||||
"item_name",
|
||||
"asset_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_serial_no",
|
||||
"target_has_batch_no",
|
||||
"target_stock_uom",
|
||||
"stock_uom",
|
||||
"target_fixed_asset_account",
|
||||
"fixed_asset_account",
|
||||
"valuation_rate",
|
||||
]
|
||||
|
||||
|
||||
class AssetCapitalization(StockController):
|
||||
def validate(self):
|
||||
self.validate_posting_time()
|
||||
self.set_missing_values(for_validate=True)
|
||||
self.validate_target_item()
|
||||
self.validate_target_asset()
|
||||
self.validate_consumed_stock_item()
|
||||
self.validate_consumed_asset_item()
|
||||
self.validate_service_item()
|
||||
self.set_warehouse_details()
|
||||
self.set_asset_values()
|
||||
self.calculate_totals()
|
||||
self.set_title()
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_source_mandatory()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.update_target_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.update_target_asset()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.target_asset_name or self.target_item_name or self.target_item_code
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
target_item_details = get_target_item_details(self.target_item_code, self.company)
|
||||
for k, v in target_item_details.items():
|
||||
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
|
||||
self.set(k, v)
|
||||
|
||||
# Remove asset if item not a fixed asset
|
||||
if not self.target_is_fixed_asset:
|
||||
self.target_asset = None
|
||||
|
||||
target_asset_details = get_target_asset_details(self.target_asset, self.company)
|
||||
for k, v in target_asset_details.items():
|
||||
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
|
||||
self.set(k, v)
|
||||
|
||||
for d in self.stock_items:
|
||||
args = self.as_dict()
|
||||
args.update(d.as_dict())
|
||||
args.doctype = self.doctype
|
||||
args.name = self.name
|
||||
consumed_stock_item_details = get_consumed_stock_item_details(args)
|
||||
for k, v in consumed_stock_item_details.items():
|
||||
if d.meta.has_field(k) and (not d.get(k) or k in force_fields):
|
||||
d.set(k, v)
|
||||
|
||||
for d in self.asset_items:
|
||||
args = self.as_dict()
|
||||
args.update(d.as_dict())
|
||||
args.doctype = self.doctype
|
||||
args.name = self.name
|
||||
args.finance_book = d.get("finance_book") or self.get("finance_book")
|
||||
consumed_asset_details = get_consumed_asset_details(args)
|
||||
for k, v in consumed_asset_details.items():
|
||||
if d.meta.has_field(k) and (not d.get(k) or k in force_fields):
|
||||
d.set(k, v)
|
||||
|
||||
for d in self.service_items:
|
||||
args = self.as_dict()
|
||||
args.update(d.as_dict())
|
||||
args.doctype = self.doctype
|
||||
args.name = self.name
|
||||
service_item_details = get_service_item_details(args)
|
||||
for k, v in service_item_details.items():
|
||||
if d.meta.has_field(k) and (not d.get(k) or k in force_fields):
|
||||
d.set(k, v)
|
||||
|
||||
def validate_target_item(self):
|
||||
target_item = frappe.get_cached_doc("Item", self.target_item_code)
|
||||
|
||||
if not target_item.is_fixed_asset and not target_item.is_stock_item:
|
||||
frappe.throw(
|
||||
_("Target Item {0} is neither a Fixed Asset nor a Stock Item").format(target_item.name)
|
||||
)
|
||||
|
||||
if self.entry_type == "Capitalization" and not target_item.is_fixed_asset:
|
||||
frappe.throw(_("Target Item {0} must be a Fixed Asset item").format(target_item.name))
|
||||
elif self.entry_type == "Decapitalization" and not target_item.is_stock_item:
|
||||
frappe.throw(_("Target Item {0} must be a Stock Item").format(target_item.name))
|
||||
|
||||
if target_item.is_fixed_asset:
|
||||
self.target_qty = 1
|
||||
if flt(self.target_qty) <= 0:
|
||||
frappe.throw(_("Target Qty must be a positive number"))
|
||||
|
||||
if not target_item.is_stock_item:
|
||||
self.target_warehouse = None
|
||||
if not target_item.is_fixed_asset:
|
||||
self.target_asset = None
|
||||
self.target_fixed_asset_account = None
|
||||
if not target_item.has_batch_no:
|
||||
self.target_batch_no = None
|
||||
if not target_item.has_serial_no:
|
||||
self.target_serial_no = ""
|
||||
|
||||
if target_item.is_stock_item and not self.target_warehouse:
|
||||
frappe.throw(_("Target Warehouse is mandatory for Decapitalization"))
|
||||
|
||||
self.validate_item(target_item)
|
||||
|
||||
def validate_target_asset(self):
|
||||
if self.target_asset:
|
||||
target_asset = self.get_asset_for_validation(self.target_asset)
|
||||
|
||||
if target_asset.item_code != self.target_item_code:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)
|
||||
)
|
||||
|
||||
self.validate_asset(target_asset)
|
||||
|
||||
def validate_consumed_stock_item(self):
|
||||
for d in self.stock_items:
|
||||
if d.item_code:
|
||||
item = frappe.get_cached_doc("Item", d.item_code)
|
||||
|
||||
if not item.is_stock_item:
|
||||
frappe.throw(_("Row #{0}: Item {1} is not a stock item").format(d.idx, d.item_code))
|
||||
|
||||
if flt(d.stock_qty) <= 0:
|
||||
frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx))
|
||||
|
||||
self.validate_item(item)
|
||||
|
||||
def validate_consumed_asset_item(self):
|
||||
for d in self.asset_items:
|
||||
if d.asset:
|
||||
if d.asset == self.target_asset:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Consumed Asset {1} cannot be the same as the Target Asset").format(
|
||||
d.idx, d.asset
|
||||
)
|
||||
)
|
||||
|
||||
asset = self.get_asset_for_validation(d.asset)
|
||||
self.validate_asset(asset)
|
||||
|
||||
def validate_service_item(self):
|
||||
for d in self.service_items:
|
||||
if d.item_code:
|
||||
item = frappe.get_cached_doc("Item", d.item_code)
|
||||
|
||||
if item.is_stock_item or item.is_fixed_asset:
|
||||
frappe.throw(_("Row #{0}: Item {1} is not a service item").format(d.idx, d.item_code))
|
||||
|
||||
if flt(d.qty) <= 0:
|
||||
frappe.throw(_("Row #{0}: Qty must be a positive number").format(d.idx))
|
||||
|
||||
if flt(d.rate) <= 0:
|
||||
frappe.throw(_("Row #{0}: Amount must be a positive number").format(d.idx))
|
||||
|
||||
self.validate_item(item)
|
||||
|
||||
if not d.cost_center:
|
||||
d.cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
|
||||
|
||||
def validate_source_mandatory(self):
|
||||
if not self.target_is_fixed_asset and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Asset Items is mandatory for Decapitalization"))
|
||||
|
||||
if not self.get("stock_items") and not self.get("asset_items"):
|
||||
frappe.throw(_("Consumed Stock Items or Consumed Asset Items is mandatory for Capitalization"))
|
||||
|
||||
def validate_item(self, item):
|
||||
from erpnext.stock.doctype.item.item import validate_end_of_life
|
||||
|
||||
validate_end_of_life(item.name, item.end_of_life, item.disabled)
|
||||
|
||||
def get_asset_for_validation(self, asset):
|
||||
return frappe.db.get_value(
|
||||
"Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1
|
||||
)
|
||||
|
||||
def validate_asset(self, asset):
|
||||
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
|
||||
frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status))
|
||||
|
||||
if asset.docstatus == 0:
|
||||
frappe.throw(_("Asset {0} is Draft").format(asset.name))
|
||||
if asset.docstatus == 2:
|
||||
frappe.throw(_("Asset {0} is cancelled").format(asset.name))
|
||||
|
||||
if asset.company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company))
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_warehouse_details(self):
|
||||
for d in self.get("stock_items"):
|
||||
if d.item_code and d.warehouse:
|
||||
args = self.get_args_for_incoming_rate(d)
|
||||
warehouse_details = get_warehouse_details(args)
|
||||
d.update(warehouse_details)
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_asset_values(self):
|
||||
for d in self.get("asset_items"):
|
||||
if d.asset:
|
||||
finance_book = d.get("finance_book") or self.get("finance_book")
|
||||
d.current_asset_value = flt(get_current_asset_value(d.asset, finance_book=finance_book))
|
||||
d.asset_value = get_value_after_depreciation_on_disposal_date(
|
||||
d.asset, self.posting_date, finance_book=finance_book
|
||||
)
|
||||
|
||||
def get_args_for_incoming_rate(self, item):
|
||||
return frappe._dict(
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"qty": -1 * flt(item.stock_qty),
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no,
|
||||
"voucher_type": self.doctype,
|
||||
"voucher_no": self.name,
|
||||
"company": self.company,
|
||||
"allow_zero_valuation": cint(item.get("allow_zero_valuation_rate")),
|
||||
}
|
||||
)
|
||||
|
||||
def calculate_totals(self):
|
||||
self.stock_items_total = 0
|
||||
self.asset_items_total = 0
|
||||
self.service_items_total = 0
|
||||
|
||||
for d in self.stock_items:
|
||||
d.amount = flt(flt(d.stock_qty) * flt(d.valuation_rate), d.precision("amount"))
|
||||
self.stock_items_total += d.amount
|
||||
|
||||
for d in self.asset_items:
|
||||
d.asset_value = flt(flt(d.asset_value), d.precision("asset_value"))
|
||||
self.asset_items_total += d.asset_value
|
||||
|
||||
for d in self.service_items:
|
||||
d.amount = flt(flt(d.qty) * flt(d.rate), d.precision("amount"))
|
||||
self.service_items_total += d.amount
|
||||
|
||||
self.stock_items_total = flt(self.stock_items_total, self.precision("stock_items_total"))
|
||||
self.asset_items_total = flt(self.asset_items_total, self.precision("asset_items_total"))
|
||||
self.service_items_total = flt(self.service_items_total, self.precision("service_items_total"))
|
||||
|
||||
self.total_value = self.stock_items_total + self.asset_items_total + self.service_items_total
|
||||
self.total_value = flt(self.total_value, self.precision("total_value"))
|
||||
|
||||
self.target_qty = flt(self.target_qty, self.precision("target_qty"))
|
||||
self.target_incoming_rate = self.total_value / self.target_qty
|
||||
|
||||
def update_stock_ledger(self):
|
||||
sl_entries = []
|
||||
|
||||
for d in self.stock_items:
|
||||
sle = self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"actual_qty": -flt(d.stock_qty),
|
||||
},
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
|
||||
if self.entry_type == "Decapitalization" and not self.target_is_fixed_asset:
|
||||
sle = self.get_sl_entries(
|
||||
self,
|
||||
{
|
||||
"item_code": self.target_item_code,
|
||||
"warehouse": self.target_warehouse,
|
||||
"batch_no": self.target_batch_no,
|
||||
"serial_no": self.target_serial_no,
|
||||
"actual_qty": flt(self.target_qty),
|
||||
"incoming_rate": flt(self.target_incoming_rate),
|
||||
},
|
||||
)
|
||||
sl_entries.append(sle)
|
||||
|
||||
# reverse sl entries if cancel
|
||||
if self.docstatus == 2:
|
||||
sl_entries.reverse()
|
||||
|
||||
if sl_entries:
|
||||
self.make_sl_entries(sl_entries)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
|
||||
|
||||
if self.docstatus == 1:
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries()
|
||||
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||
elif self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
def get_gl_entries(
|
||||
self, warehouse_account=None, default_expense_account=None, default_cost_center=None
|
||||
):
|
||||
# Stock GL Entries
|
||||
gl_entries = []
|
||||
|
||||
self.warehouse_account = warehouse_account
|
||||
if not self.warehouse_account:
|
||||
self.warehouse_account = get_warehouse_account_map(self.company)
|
||||
|
||||
precision = self.get_debit_field_precision()
|
||||
self.sle_map = self.get_stock_ledger_details()
|
||||
|
||||
target_account = self.get_target_account()
|
||||
target_against = set()
|
||||
|
||||
self.get_gl_entries_for_consumed_stock_items(
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
self.get_gl_entries_for_consumed_asset_items(
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
self.get_gl_entries_for_consumed_service_items(
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
|
||||
return gl_entries
|
||||
|
||||
def get_target_account(self):
|
||||
if self.target_is_fixed_asset:
|
||||
return self.target_fixed_asset_account
|
||||
else:
|
||||
return self.warehouse_account[self.target_warehouse]["account"]
|
||||
|
||||
def get_gl_entries_for_consumed_stock_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
# Consumed Stock Items
|
||||
for item_row in self.stock_items:
|
||||
sle_list = self.sle_map.get(item_row.name)
|
||||
if sle_list:
|
||||
for sle in sle_list:
|
||||
stock_value_difference = flt(sle.stock_value_difference, precision)
|
||||
|
||||
if erpnext.is_perpetual_inventory_enabled(self.company):
|
||||
account = self.warehouse_account[sle.warehouse]["account"]
|
||||
else:
|
||||
account = self.get_company_default("default_expense_account")
|
||||
|
||||
target_against.add(account)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": target_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": -1 * stock_value_difference,
|
||||
},
|
||||
self.warehouse_account[sle.warehouse]["account_currency"],
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_consumed_asset_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
# Consumed Assets
|
||||
for item in self.asset_items:
|
||||
asset = self.get_asset(item)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.depreciate_asset(asset)
|
||||
asset.reload()
|
||||
|
||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
|
||||
asset, item.asset_value, item.get("finance_book") or self.get("finance_book")
|
||||
)
|
||||
|
||||
asset.db_set("disposal_date", self.posting_date)
|
||||
|
||||
self.set_consumed_asset_status(asset)
|
||||
|
||||
for gle in fixed_asset_gl_entries:
|
||||
gle["against"] = target_account
|
||||
gl_entries.append(self.get_gl_dict(gle, item=item))
|
||||
target_against.add(gle["account"])
|
||||
|
||||
def get_gl_entries_for_consumed_service_items(
|
||||
self, gl_entries, target_account, target_against, precision
|
||||
):
|
||||
# Service Expenses
|
||||
for item_row in self.service_items:
|
||||
expense_amount = flt(item_row.amount, precision)
|
||||
target_against.add(item_row.expense_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": item_row.expense_account,
|
||||
"against": target_account,
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": expense_amount,
|
||||
},
|
||||
item=item_row,
|
||||
)
|
||||
)
|
||||
|
||||
def get_gl_entries_for_target_item(self, gl_entries, target_against, precision):
|
||||
if self.target_is_fixed_asset:
|
||||
# Capitalization
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.target_fixed_asset_account,
|
||||
"against": ", ".join(target_against),
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
||||
"debit": flt(self.total_value, precision),
|
||||
"cost_center": self.get("cost_center"),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Target Stock Item
|
||||
sle_list = self.sle_map.get(self.name)
|
||||
for sle in sle_list:
|
||||
stock_value_difference = flt(sle.stock_value_difference, precision)
|
||||
account = self.warehouse_account[sle.warehouse]["account"]
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": account,
|
||||
"against": ", ".join(target_against),
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.get("project"),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"debit": stock_value_difference,
|
||||
},
|
||||
self.warehouse_account[sle.warehouse]["account_currency"],
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def update_target_asset(self):
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
if self.docstatus == 1 and self.entry_type == "Capitalization":
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.prepare_depreciation_data()
|
||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
asset_doc.save()
|
||||
elif self.docstatus == 2:
|
||||
for item in self.asset_items:
|
||||
asset = self.get_asset(item)
|
||||
asset.db_set("disposal_date", None)
|
||||
self.set_consumed_asset_status(asset)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_disposal(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
|
||||
def get_asset(self, item):
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
self.check_finance_books(item, asset)
|
||||
return asset
|
||||
|
||||
def set_consumed_asset_status(self, asset):
|
||||
if self.docstatus == 1:
|
||||
asset.set_status("Capitalized" if self.target_is_fixed_asset else "Decapitalized")
|
||||
else:
|
||||
asset.set_status()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_target_item_details(item_code=None, company=None):
|
||||
out = frappe._dict()
|
||||
|
||||
# Get Item Details
|
||||
item = frappe._dict()
|
||||
if item_code:
|
||||
item = frappe.get_cached_doc("Item", item_code)
|
||||
|
||||
# Set Item Details
|
||||
out.target_item_name = item.item_name
|
||||
out.target_stock_uom = item.stock_uom
|
||||
out.target_is_fixed_asset = cint(item.is_fixed_asset)
|
||||
out.target_has_batch_no = cint(item.has_batch_no)
|
||||
out.target_has_serial_no = cint(item.has_serial_no)
|
||||
|
||||
if out.target_is_fixed_asset:
|
||||
out.target_qty = 1
|
||||
out.target_warehouse = None
|
||||
else:
|
||||
out.target_asset = None
|
||||
|
||||
if not out.target_has_batch_no:
|
||||
out.target_batch_no = None
|
||||
if not out.target_has_serial_no:
|
||||
out.target_serial_no = ""
|
||||
|
||||
# Cost Center
|
||||
item_defaults = get_item_defaults(item.name, company)
|
||||
item_group_defaults = get_item_group_defaults(item.name, company)
|
||||
brand_defaults = get_brand_defaults(item.name, company)
|
||||
out.cost_center = get_default_cost_center(
|
||||
frappe._dict({"item_code": item.name, "company": company}),
|
||||
item_defaults,
|
||||
item_group_defaults,
|
||||
brand_defaults,
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_target_asset_details(asset=None, company=None):
|
||||
out = frappe._dict()
|
||||
|
||||
# Get Asset Details
|
||||
asset_details = frappe._dict()
|
||||
if asset:
|
||||
asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1)
|
||||
if not asset_details:
|
||||
frappe.throw(_("Asset {0} does not exist").format(asset))
|
||||
|
||||
# Re-set item code from Asset
|
||||
out.target_item_code = asset_details.item_code
|
||||
|
||||
# Set Asset Details
|
||||
out.asset_name = asset_details.asset_name
|
||||
|
||||
if asset_details.item_code:
|
||||
out.target_fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=asset_details.item_code, company=company
|
||||
)
|
||||
else:
|
||||
out.target_fixed_asset_account = None
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_consumed_stock_item_details(args):
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
out = frappe._dict()
|
||||
|
||||
item = frappe._dict()
|
||||
if args.item_code:
|
||||
item = frappe.get_cached_doc("Item", args.item_code)
|
||||
|
||||
out.item_name = item.item_name
|
||||
out.batch_no = None
|
||||
out.serial_no = ""
|
||||
|
||||
out.stock_qty = flt(args.stock_qty) or 1
|
||||
out.stock_uom = item.stock_uom
|
||||
|
||||
out.warehouse = get_item_warehouse(item, args, overwrite_warehouse=True) if item else None
|
||||
|
||||
# Cost Center
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
item_group_defaults = get_item_group_defaults(item.name, args.company)
|
||||
brand_defaults = get_brand_defaults(item.name, args.company)
|
||||
out.cost_center = get_default_cost_center(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
|
||||
if args.item_code and out.warehouse:
|
||||
incoming_rate_args = frappe._dict(
|
||||
{
|
||||
"item_code": args.item_code,
|
||||
"warehouse": out.warehouse,
|
||||
"posting_date": args.posting_date,
|
||||
"posting_time": args.posting_time,
|
||||
"qty": -1 * flt(out.stock_qty),
|
||||
"voucher_type": args.doctype,
|
||||
"voucher_no": args.name,
|
||||
"company": args.company,
|
||||
"serial_no": args.serial_no,
|
||||
"batch_no": args.batch_no,
|
||||
}
|
||||
)
|
||||
out.update(get_warehouse_details(incoming_rate_args))
|
||||
else:
|
||||
out.valuation_rate = 0
|
||||
out.actual_qty = 0
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_warehouse_details(args):
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
out = {}
|
||||
if args.warehouse and args.item_code:
|
||||
out = {
|
||||
"actual_qty": get_previous_sle(args).get("qty_after_transaction") or 0,
|
||||
"valuation_rate": get_incoming_rate(args, raise_error_if_no_rate=False),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_consumed_asset_details(args):
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
out = frappe._dict()
|
||||
|
||||
asset_details = frappe._dict()
|
||||
if args.asset:
|
||||
asset_details = frappe.db.get_value(
|
||||
"Asset", args.asset, ["asset_name", "item_code", "item_name"], as_dict=1
|
||||
)
|
||||
if not asset_details:
|
||||
frappe.throw(_("Asset {0} does not exist").format(args.asset))
|
||||
|
||||
out.item_code = asset_details.item_code
|
||||
out.asset_name = asset_details.asset_name
|
||||
out.item_name = asset_details.item_name
|
||||
|
||||
if args.asset:
|
||||
out.current_asset_value = flt(
|
||||
get_current_asset_value(args.asset, finance_book=args.finance_book)
|
||||
)
|
||||
out.asset_value = get_value_after_depreciation_on_disposal_date(
|
||||
args.asset, args.posting_date, finance_book=args.finance_book
|
||||
)
|
||||
else:
|
||||
out.current_asset_value = 0
|
||||
out.asset_value = 0
|
||||
|
||||
# Account
|
||||
if asset_details.item_code:
|
||||
out.fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=asset_details.item_code, company=args.company
|
||||
)
|
||||
else:
|
||||
out.fixed_asset_account = None
|
||||
|
||||
# Cost Center
|
||||
if asset_details.item_code:
|
||||
item = frappe.get_cached_doc("Item", asset_details.item_code)
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
item_group_defaults = get_item_group_defaults(item.name, args.company)
|
||||
brand_defaults = get_brand_defaults(item.name, args.company)
|
||||
out.cost_center = get_default_cost_center(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_service_item_details(args):
|
||||
if isinstance(args, string_types):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
out = frappe._dict()
|
||||
|
||||
item = frappe._dict()
|
||||
if args.item_code:
|
||||
item = frappe.get_cached_doc("Item", args.item_code)
|
||||
|
||||
out.item_name = item.item_name
|
||||
out.qty = flt(args.qty) or 1
|
||||
out.uom = item.purchase_uom or item.stock_uom
|
||||
|
||||
item_defaults = get_item_defaults(item.name, args.company)
|
||||
item_group_defaults = get_item_group_defaults(item.name, args.company)
|
||||
brand_defaults = get_brand_defaults(item.name, args.company)
|
||||
|
||||
out.expense_account = get_default_expense_account(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
out.cost_center = get_default_cost_center(
|
||||
args, item_defaults, item_group_defaults, brand_defaults
|
||||
)
|
||||
|
||||
return out
|
@ -0,0 +1,494 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint, flt, getdate, now_datetime
|
||||
|
||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
|
||||
from erpnext.assets.doctype.asset.test_asset import (
|
||||
create_asset,
|
||||
create_asset_data,
|
||||
set_depreciation_settings_in_company,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestAssetCapitalization(unittest.TestCase):
|
||||
def setUp(self):
|
||||
set_depreciation_settings_in_company()
|
||||
create_asset_data()
|
||||
create_asset_capitalization_data()
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_capitalization_with_perpetual_inventory(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
|
||||
# Variables
|
||||
consumed_asset_value = 100000
|
||||
|
||||
stock_rate = 1000
|
||||
stock_qty = 2
|
||||
stock_amount = 2000
|
||||
|
||||
service_rate = 500
|
||||
service_qty = 2
|
||||
service_amount = 1000
|
||||
|
||||
total_amount = 103000
|
||||
|
||||
# Create assets
|
||||
target_asset = create_asset(
|
||||
asset_name="Asset Capitalization Target Asset",
|
||||
submit=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
consumed_asset = create_asset(
|
||||
asset_name="Asset Capitalization Consumable Asset",
|
||||
asset_value=consumed_asset_value,
|
||||
submit=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
target_asset=target_asset.name,
|
||||
stock_qty=stock_qty,
|
||||
stock_rate=stock_rate,
|
||||
consumed_asset=consumed_asset.name,
|
||||
service_qty=service_qty,
|
||||
service_rate=service_rate,
|
||||
service_expense_account="Expenses Included In Asset Valuation - TCP1",
|
||||
company=company,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
|
||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
||||
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
||||
|
||||
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
|
||||
self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value)
|
||||
|
||||
self.assertEqual(asset_capitalization.service_items[0].amount, service_amount)
|
||||
self.assertEqual(asset_capitalization.service_items_total, service_amount)
|
||||
|
||||
self.assertEqual(asset_capitalization.total_value, total_amount)
|
||||
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset.reload()
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
|
||||
# Test Consumed Asset values
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||
|
||||
# Test General Ledger Entries
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - TCP1": 3000,
|
||||
"Expenses Included In Asset Valuation - TCP1": -1000,
|
||||
"_Test Warehouse - TCP1": -2000,
|
||||
}
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
|
||||
self.assertEqual(actual_gle, expected_gle)
|
||||
|
||||
# Test Stock Ledger Entries
|
||||
expected_sle = {
|
||||
("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): {
|
||||
"actual_qty": -stock_qty,
|
||||
"stock_value_difference": -stock_amount,
|
||||
}
|
||||
}
|
||||
actual_sle = get_actual_sle_dict(asset_capitalization.name)
|
||||
self.assertEqual(actual_sle, expected_sle)
|
||||
|
||||
# Cancel Asset Capitalization and make test entries and status are reversed
|
||||
asset_capitalization.cancel()
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Submitted")
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
def test_capitalization_with_periodical_inventory(self):
|
||||
company = "_Test Company"
|
||||
# Variables
|
||||
consumed_asset_value = 100000
|
||||
|
||||
stock_rate = 1000
|
||||
stock_qty = 2
|
||||
stock_amount = 2000
|
||||
|
||||
service_rate = 500
|
||||
service_qty = 2
|
||||
service_amount = 1000
|
||||
|
||||
total_amount = 103000
|
||||
|
||||
# Create assets
|
||||
target_asset = create_asset(
|
||||
asset_name="Asset Capitalization Target Asset",
|
||||
submit=1,
|
||||
warehouse="Stores - _TC",
|
||||
company=company,
|
||||
)
|
||||
consumed_asset = create_asset(
|
||||
asset_name="Asset Capitalization Consumable Asset",
|
||||
asset_value=consumed_asset_value,
|
||||
submit=1,
|
||||
warehouse="Stores - _TC",
|
||||
company=company,
|
||||
)
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
target_asset=target_asset.name,
|
||||
stock_qty=stock_qty,
|
||||
stock_rate=stock_rate,
|
||||
consumed_asset=consumed_asset.name,
|
||||
service_qty=service_qty,
|
||||
service_rate=service_rate,
|
||||
service_expense_account="Expenses Included In Asset Valuation - _TC",
|
||||
company=company,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
|
||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
||||
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
||||
|
||||
self.assertEqual(asset_capitalization.asset_items[0].asset_value, consumed_asset_value)
|
||||
self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value)
|
||||
|
||||
self.assertEqual(asset_capitalization.service_items[0].amount, service_amount)
|
||||
self.assertEqual(asset_capitalization.service_items_total, service_amount)
|
||||
|
||||
self.assertEqual(asset_capitalization.total_value, total_amount)
|
||||
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset.reload()
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
|
||||
# Test Consumed Asset values
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Capitalized")
|
||||
|
||||
# Test General Ledger Entries
|
||||
default_expense_account = frappe.db.get_value("Company", company, "default_expense_account")
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - _TC": 3000,
|
||||
"Expenses Included In Asset Valuation - _TC": -1000,
|
||||
default_expense_account: -2000,
|
||||
}
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
|
||||
self.assertEqual(actual_gle, expected_gle)
|
||||
|
||||
# Test Stock Ledger Entries
|
||||
expected_sle = {
|
||||
("Capitalization Source Stock Item", "_Test Warehouse - _TC"): {
|
||||
"actual_qty": -stock_qty,
|
||||
"stock_value_difference": -stock_amount,
|
||||
}
|
||||
}
|
||||
actual_sle = get_actual_sle_dict(asset_capitalization.name)
|
||||
self.assertEqual(actual_sle, expected_sle)
|
||||
|
||||
# Cancel Asset Capitalization and make test entries and status are reversed
|
||||
asset_capitalization.cancel()
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Submitted")
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
def test_decapitalization_with_depreciation(self):
|
||||
# Variables
|
||||
purchase_date = "2020-01-01"
|
||||
depreciation_start_date = "2020-12-31"
|
||||
capitalization_date = "2021-06-30"
|
||||
|
||||
total_number_of_depreciations = 3
|
||||
expected_value_after_useful_life = 10_000
|
||||
consumed_asset_purchase_value = 100_000
|
||||
consumed_asset_current_value = 70_000
|
||||
consumed_asset_value_before_disposal = 55_000
|
||||
|
||||
target_qty = 10
|
||||
target_incoming_rate = 5500
|
||||
|
||||
depreciation_before_disposal_amount = 15_000
|
||||
accumulated_depreciation = 45_000
|
||||
|
||||
# to accomodate for depreciation on disposal calculation minor difference
|
||||
consumed_asset_value_before_disposal = 55_123.29
|
||||
target_incoming_rate = 5512.329
|
||||
depreciation_before_disposal_amount = 14_876.71
|
||||
accumulated_depreciation = 44_876.71
|
||||
|
||||
# Create assets
|
||||
consumed_asset = create_depreciation_asset(
|
||||
asset_name="Asset Capitalization Consumable Asset",
|
||||
asset_value=consumed_asset_purchase_value,
|
||||
purchase_date=purchase_date,
|
||||
depreciation_start_date=depreciation_start_date,
|
||||
depreciation_method="Straight Line",
|
||||
total_number_of_depreciations=total_number_of_depreciations,
|
||||
frequency_of_depreciation=12,
|
||||
expected_value_after_useful_life=expected_value_after_useful_life,
|
||||
company="_Test Company with perpetual inventory",
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Decapitalization",
|
||||
posting_date=capitalization_date, # half a year
|
||||
target_item_code="Capitalization Target Stock Item",
|
||||
target_qty=target_qty,
|
||||
consumed_asset=consumed_asset.name,
|
||||
company="_Test Company with perpetual inventory",
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.entry_type, "Decapitalization")
|
||||
|
||||
self.assertEqual(
|
||||
asset_capitalization.asset_items[0].current_asset_value, consumed_asset_current_value
|
||||
)
|
||||
self.assertEqual(
|
||||
asset_capitalization.asset_items[0].asset_value, consumed_asset_value_before_disposal
|
||||
)
|
||||
self.assertEqual(asset_capitalization.asset_items_total, consumed_asset_value_before_disposal)
|
||||
|
||||
self.assertEqual(asset_capitalization.total_value, consumed_asset_value_before_disposal)
|
||||
self.assertEqual(asset_capitalization.target_incoming_rate, target_incoming_rate)
|
||||
|
||||
# Test Consumed Asset values
|
||||
consumed_asset.reload()
|
||||
self.assertEqual(consumed_asset.status, "Decapitalized")
|
||||
|
||||
consumed_depreciation_schedule = [
|
||||
d for d in consumed_asset.schedules if getdate(d.schedule_date) == getdate(capitalization_date)
|
||||
]
|
||||
self.assertTrue(
|
||||
consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry
|
||||
)
|
||||
self.assertEqual(
|
||||
consumed_depreciation_schedule[0].depreciation_amount, depreciation_before_disposal_amount
|
||||
)
|
||||
|
||||
# Test General Ledger Entries
|
||||
expected_gle = {
|
||||
"_Test Warehouse - TCP1": consumed_asset_value_before_disposal,
|
||||
"_Test Accumulated Depreciations - TCP1": accumulated_depreciation,
|
||||
"_Test Fixed Asset - TCP1": -consumed_asset_purchase_value,
|
||||
}
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
self.assertEqual(actual_gle, expected_gle)
|
||||
|
||||
# Cancel Asset Capitalization and make test entries and status are reversed
|
||||
asset_capitalization.reload()
|
||||
asset_capitalization.cancel()
|
||||
self.assertEqual(consumed_asset.db_get("status"), "Partially Depreciated")
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
|
||||
def create_asset_capitalization_data():
|
||||
create_item(
|
||||
"Capitalization Target Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0
|
||||
)
|
||||
create_item(
|
||||
"Capitalization Source Stock Item", is_stock_item=1, is_fixed_asset=0, is_purchase_item=0
|
||||
)
|
||||
create_item(
|
||||
"Capitalization Source Service Item", is_stock_item=0, is_fixed_asset=0, is_purchase_item=0
|
||||
)
|
||||
|
||||
|
||||
def create_asset_capitalization(**args):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
now = now_datetime()
|
||||
target_asset = frappe.get_doc("Asset", args.target_asset) if args.target_asset else frappe._dict()
|
||||
target_item_code = target_asset.item_code or args.target_item_code
|
||||
company = target_asset.company or args.company or "_Test Company"
|
||||
warehouse = args.warehouse or create_warehouse("_Test Warehouse", company=company)
|
||||
target_warehouse = args.target_warehouse or warehouse
|
||||
source_warehouse = args.source_warehouse or warehouse
|
||||
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
asset_capitalization.update(
|
||||
{
|
||||
"entry_type": args.entry_type or "Capitalization",
|
||||
"company": company,
|
||||
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
|
||||
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),
|
||||
"target_item_code": target_item_code,
|
||||
"target_asset": target_asset.name,
|
||||
"target_warehouse": target_warehouse,
|
||||
"target_qty": flt(args.target_qty) or 1,
|
||||
"target_batch_no": args.target_batch_no,
|
||||
"target_serial_no": args.target_serial_no,
|
||||
"finance_book": args.finance_book,
|
||||
}
|
||||
)
|
||||
|
||||
if args.posting_date or args.posting_time:
|
||||
asset_capitalization.set_posting_time = 1
|
||||
|
||||
if flt(args.stock_rate):
|
||||
asset_capitalization.append(
|
||||
"stock_items",
|
||||
{
|
||||
"item_code": args.stock_item or "Capitalization Source Stock Item",
|
||||
"warehouse": source_warehouse,
|
||||
"stock_qty": flt(args.stock_qty) or 1,
|
||||
"batch_no": args.stock_batch_no,
|
||||
"serial_no": args.stock_serial_no,
|
||||
},
|
||||
)
|
||||
|
||||
if args.consumed_asset:
|
||||
asset_capitalization.append(
|
||||
"asset_items",
|
||||
{
|
||||
"asset": args.consumed_asset,
|
||||
},
|
||||
)
|
||||
|
||||
if flt(args.service_rate):
|
||||
asset_capitalization.append(
|
||||
"service_items",
|
||||
{
|
||||
"item_code": args.service_item or "Capitalization Source Service Item",
|
||||
"expense_account": args.service_expense_account,
|
||||
"qty": flt(args.service_qty) or 1,
|
||||
"rate": flt(args.service_rate),
|
||||
},
|
||||
)
|
||||
|
||||
if args.submit:
|
||||
create_stock_reconciliation(asset_capitalization, stock_rate=args.stock_rate)
|
||||
|
||||
asset_capitalization.insert()
|
||||
|
||||
if args.submit:
|
||||
asset_capitalization.submit()
|
||||
|
||||
return asset_capitalization
|
||||
|
||||
|
||||
def create_stock_reconciliation(asset_capitalization, stock_rate=0):
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
EmptyStockReconciliationItemsError,
|
||||
create_stock_reconciliation,
|
||||
)
|
||||
|
||||
if not asset_capitalization.get("stock_items"):
|
||||
return
|
||||
|
||||
try:
|
||||
create_stock_reconciliation(
|
||||
item_code=asset_capitalization.stock_items[0].item_code,
|
||||
warehouse=asset_capitalization.stock_items[0].warehouse,
|
||||
qty=flt(asset_capitalization.stock_items[0].stock_qty),
|
||||
rate=flt(stock_rate),
|
||||
company=asset_capitalization.company,
|
||||
)
|
||||
except EmptyStockReconciliationItemsError:
|
||||
pass
|
||||
|
||||
|
||||
def create_depreciation_asset(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
asset = frappe.new_doc("Asset")
|
||||
asset.is_existing_asset = 1
|
||||
asset.calculate_depreciation = 1
|
||||
asset.asset_owner = "Company"
|
||||
|
||||
asset.company = args.company or "_Test Company"
|
||||
asset.item_code = args.item_code or "Macbook Pro"
|
||||
asset.asset_name = args.asset_name or asset.item_code
|
||||
asset.location = args.location or "Test Location"
|
||||
|
||||
asset.purchase_date = args.purchase_date or "2020-01-01"
|
||||
asset.available_for_use_date = args.available_for_use_date or asset.purchase_date
|
||||
|
||||
asset.gross_purchase_amount = args.asset_value or 100000
|
||||
asset.purchase_receipt_amount = asset.gross_purchase_amount
|
||||
|
||||
finance_book = asset.append("finance_books")
|
||||
finance_book.depreciation_start_date = args.depreciation_start_date or "2020-12-31"
|
||||
finance_book.depreciation_method = args.depreciation_method or "Straight Line"
|
||||
finance_book.total_number_of_depreciations = cint(args.total_number_of_depreciations) or 3
|
||||
finance_book.frequency_of_depreciation = cint(args.frequency_of_depreciation) or 12
|
||||
finance_book.expected_value_after_useful_life = flt(args.expected_value_after_useful_life)
|
||||
|
||||
if args.submit:
|
||||
asset.submit()
|
||||
|
||||
frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-")
|
||||
post_depreciation_entries(date=finance_book.depreciation_start_date)
|
||||
asset.load_from_db()
|
||||
|
||||
return asset
|
||||
|
||||
|
||||
def get_actual_gle_dict(name):
|
||||
return dict(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select account, sum(debit-credit) as diff
|
||||
from `tabGL Entry`
|
||||
where voucher_type = 'Asset Capitalization' and voucher_no = %s
|
||||
group by account
|
||||
having diff != 0
|
||||
""",
|
||||
name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_actual_sle_dict(name):
|
||||
sles = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
item_code, warehouse,
|
||||
sum(actual_qty) as actual_qty,
|
||||
sum(stock_value_difference) as stock_value_difference
|
||||
from `tabStock Ledger Entry`
|
||||
where voucher_type = 'Asset Capitalization' and voucher_no = %s
|
||||
group by item_code, warehouse
|
||||
having actual_qty != 0
|
||||
""",
|
||||
name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
sle_dict = {}
|
||||
for d in sles:
|
||||
sle_dict[(d.item_code, d.warehouse)] = {
|
||||
"actual_qty": d.actual_qty,
|
||||
"stock_value_difference": d.stock_value_difference,
|
||||
}
|
||||
|
||||
return sle_dict
|
@ -0,0 +1,128 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-09-05 15:52:10.124538",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"asset",
|
||||
"asset_name",
|
||||
"finance_book",
|
||||
"column_break_3",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"section_break_6",
|
||||
"current_asset_value",
|
||||
"asset_value",
|
||||
"column_break_9",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"fixed_asset_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "asset.asset_name",
|
||||
"fieldname": "asset_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "asset.item_code",
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Value"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "asset_value",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "fixed_asset_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Fixed Asset Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"fieldname": "current_asset_value",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Current Asset Value",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-12 14:30:02.915132",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization Asset Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssetCapitalizationAssetItem(Document):
|
||||
pass
|
@ -0,0 +1,122 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-09-06 13:32:08.642060",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"column_break_3",
|
||||
"expense_account",
|
||||
"section_break_6",
|
||||
"qty",
|
||||
"uom",
|
||||
"column_break_9",
|
||||
"rate",
|
||||
"amount",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "expense_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Expense Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Qty and Rate"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"default": "1",
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "stock_item_code.stock_uom",
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "UOM",
|
||||
"options": "UOM"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-08 15:52:08.598100",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization Service Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssetCapitalizationServiceItem(Document):
|
||||
pass
|
@ -0,0 +1,156 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-09-05 15:23:23.492310",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"column_break_3",
|
||||
"warehouse",
|
||||
"section_break_6",
|
||||
"stock_qty",
|
||||
"stock_uom",
|
||||
"actual_qty",
|
||||
"column_break_9",
|
||||
"valuation_rate",
|
||||
"amount",
|
||||
"batch_and_serial_no_section",
|
||||
"batch_no",
|
||||
"column_break_13",
|
||||
"serial_no",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Warehouse",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"options": "Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Qty and Rate"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "stock_item_code.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Stock UOM",
|
||||
"options": "UOM",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Valuation Rate",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_and_serial_no_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Batch and Serial No"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Actual Qty in Warehouse",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cost Center",
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-08 15:56:20.230548",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization Stock Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssetCapitalizationStockItem(Document):
|
||||
pass
|
@ -130,6 +130,17 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Asset",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Asset Capitalization",
|
||||
"link_count": 0,
|
||||
"link_to": "Asset Capitalization",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@ -172,7 +183,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2022-01-13 17:25:41.730628",
|
||||
"modified": "2022-01-13 18:25:41.730628",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Assets",
|
||||
@ -205,4 +216,4 @@
|
||||
}
|
||||
],
|
||||
"title": "Assets"
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,7 @@ from erpnext.accounts.party import (
|
||||
validate_party_frozen_disabled,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
|
||||
from erpnext.assets.doctype.asset.depreciation import make_depreciation_entry
|
||||
from erpnext.buying.utils import update_last_purchase_rate
|
||||
from erpnext.controllers.print_settings import (
|
||||
set_print_templates_for_item_table,
|
||||
@ -205,6 +206,10 @@ class AccountsController(TransactionBase):
|
||||
def on_trash(self):
|
||||
# delete sl and gl entries on deletion of transaction
|
||||
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
frappe.qb.from_(ple).delete().where(
|
||||
(ple.voucher_type == self.doctype) & (ple.voucher_no == self.name)
|
||||
).run()
|
||||
frappe.db.sql(
|
||||
"delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name)
|
||||
)
|
||||
@ -1870,6 +1875,99 @@ class AccountsController(TransactionBase):
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
def check_finance_books(self, item, asset):
|
||||
if (
|
||||
len(asset.finance_books) > 1
|
||||
and not item.get("finance_book")
|
||||
and not self.get("finance_book")
|
||||
and asset.finance_books[0].finance_book
|
||||
):
|
||||
frappe.throw(
|
||||
_("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_disposal=self.posting_date)
|
||||
asset.save()
|
||||
|
||||
make_depreciation_entry(asset.name, self.posting_date)
|
||||
|
||||
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()
|
||||
|
||||
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_disposal(self, asset):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
posting_date_of_original_disposal = self.get_posting_date_of_disposal_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 == posting_date_of_original_disposal:
|
||||
if not self.disposal_was_made_on_original_schedule_date(
|
||||
asset, schedule, row, posting_date_of_original_disposal
|
||||
) or self.disposal_happens_in_the_future(posting_date_of_original_disposal):
|
||||
|
||||
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
|
||||
asset.save()
|
||||
|
||||
def get_posting_date_of_disposal_entry(self):
|
||||
if self.doctype == "Sales Invoice" and self.return_against:
|
||||
return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
|
||||
else:
|
||||
return self.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 disposal_was_made_on_original_schedule_date(
|
||||
self, 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(self, posting_date_of_disposal):
|
||||
if posting_date_of_disposal > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
|
@ -314,7 +314,11 @@ class BuyingController(SubcontractingController):
|
||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
|
||||
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
rate = flt(
|
||||
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
|
||||
* (d.conversion_factor or 1),
|
||||
d.precision("rate"),
|
||||
)
|
||||
|
||||
if self.is_internal_transfer():
|
||||
if rate != d.rate:
|
||||
|
@ -1,193 +0,0 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.form import assign_to
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, flt, unique
|
||||
|
||||
from erpnext.setup.doctype.employee.employee import get_holiday_list_for_employee
|
||||
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
|
||||
class EmployeeBoardingController(Document):
|
||||
"""
|
||||
Create the project and the task for the boarding process
|
||||
Assign to the concerned person and roles as per the onboarding/separation template
|
||||
"""
|
||||
|
||||
def validate(self):
|
||||
# remove the task if linked before submitting the form
|
||||
if self.amended_from:
|
||||
for activity in self.activities:
|
||||
activity.task = ""
|
||||
|
||||
def on_submit(self):
|
||||
# create the project for the given employee onboarding
|
||||
project_name = _(self.doctype) + " : "
|
||||
if self.doctype == "Employee Onboarding":
|
||||
project_name += self.job_applicant
|
||||
else:
|
||||
project_name += self.employee
|
||||
|
||||
project = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Project",
|
||||
"project_name": project_name,
|
||||
"expected_start_date": self.date_of_joining
|
||||
if self.doctype == "Employee Onboarding"
|
||||
else self.resignation_letter_date,
|
||||
"department": self.department,
|
||||
"company": self.company,
|
||||
}
|
||||
).insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
|
||||
self.db_set("project", project.name)
|
||||
self.db_set("boarding_status", "Pending")
|
||||
self.reload()
|
||||
self.create_task_and_notify_user()
|
||||
|
||||
def create_task_and_notify_user(self):
|
||||
# create the task for the given project and assign to the concerned person
|
||||
holiday_list = self.get_holiday_list()
|
||||
|
||||
for activity in self.activities:
|
||||
if activity.task:
|
||||
continue
|
||||
|
||||
dates = self.get_task_dates(activity, holiday_list)
|
||||
|
||||
task = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Task",
|
||||
"project": self.project,
|
||||
"subject": activity.activity_name + " : " + self.employee_name,
|
||||
"description": activity.description,
|
||||
"department": self.department,
|
||||
"company": self.company,
|
||||
"task_weight": activity.task_weight,
|
||||
"exp_start_date": dates[0],
|
||||
"exp_end_date": dates[1],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
activity.db_set("task", task.name)
|
||||
|
||||
users = [activity.user] if activity.user else []
|
||||
if activity.role:
|
||||
user_list = frappe.db.sql_list(
|
||||
"""
|
||||
SELECT
|
||||
DISTINCT(has_role.parent)
|
||||
FROM
|
||||
`tabHas Role` has_role
|
||||
LEFT JOIN `tabUser` user
|
||||
ON has_role.parent = user.name
|
||||
WHERE
|
||||
has_role.parenttype = 'User'
|
||||
AND user.enabled = 1
|
||||
AND has_role.role = %s
|
||||
""",
|
||||
activity.role,
|
||||
)
|
||||
users = unique(users + user_list)
|
||||
|
||||
if "Administrator" in users:
|
||||
users.remove("Administrator")
|
||||
|
||||
# assign the task the users
|
||||
if users:
|
||||
self.assign_task_to_users(task, users)
|
||||
|
||||
def get_holiday_list(self):
|
||||
if self.doctype == "Employee Separation":
|
||||
return get_holiday_list_for_employee(self.employee)
|
||||
else:
|
||||
if self.employee:
|
||||
return get_holiday_list_for_employee(self.employee)
|
||||
else:
|
||||
if not self.holiday_list:
|
||||
frappe.throw(_("Please set the Holiday List."), frappe.MandatoryError)
|
||||
else:
|
||||
return self.holiday_list
|
||||
|
||||
def get_task_dates(self, activity, holiday_list):
|
||||
start_date = end_date = None
|
||||
|
||||
if activity.begin_on is not None:
|
||||
start_date = add_days(self.boarding_begins_on, activity.begin_on)
|
||||
start_date = self.update_if_holiday(start_date, holiday_list)
|
||||
|
||||
if activity.duration is not None:
|
||||
end_date = add_days(self.boarding_begins_on, activity.begin_on + activity.duration)
|
||||
end_date = self.update_if_holiday(end_date, holiday_list)
|
||||
|
||||
return [start_date, end_date]
|
||||
|
||||
def update_if_holiday(self, date, holiday_list):
|
||||
while is_holiday(holiday_list, date):
|
||||
date = add_days(date, 1)
|
||||
return date
|
||||
|
||||
def assign_task_to_users(self, task, users):
|
||||
for user in users:
|
||||
args = {
|
||||
"assign_to": [user],
|
||||
"doctype": task.doctype,
|
||||
"name": task.name,
|
||||
"description": task.description or task.subject,
|
||||
"notify": self.notify_users_by_email,
|
||||
}
|
||||
assign_to.add(args)
|
||||
|
||||
def on_cancel(self):
|
||||
# delete task project
|
||||
project = self.project
|
||||
for task in frappe.get_all("Task", filters={"project": project}):
|
||||
frappe.delete_doc("Task", task.name, force=1)
|
||||
frappe.delete_doc("Project", project, force=1)
|
||||
self.db_set("project", "")
|
||||
for activity in self.activities:
|
||||
activity.db_set("task", "")
|
||||
|
||||
frappe.msgprint(
|
||||
_("Linked Project {} and Tasks deleted.").format(project), alert=True, indicator="blue"
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_onboarding_details(parent, parenttype):
|
||||
return frappe.get_all(
|
||||
"Employee Boarding Activity",
|
||||
fields=[
|
||||
"activity_name",
|
||||
"role",
|
||||
"user",
|
||||
"required_for_employee_creation",
|
||||
"description",
|
||||
"task_weight",
|
||||
"begin_on",
|
||||
"duration",
|
||||
],
|
||||
filters={"parent": parent, "parenttype": parenttype},
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
|
||||
def update_employee_boarding_status(project):
|
||||
employee_onboarding = frappe.db.exists("Employee Onboarding", {"project": project.name})
|
||||
employee_separation = frappe.db.exists("Employee Separation", {"project": project.name})
|
||||
|
||||
if not (employee_onboarding or employee_separation):
|
||||
return
|
||||
|
||||
status = "Pending"
|
||||
if flt(project.percent_complete) > 0.0 and flt(project.percent_complete) < 100.0:
|
||||
status = "In Process"
|
||||
elif flt(project.percent_complete) == 100.0:
|
||||
status = "Completed"
|
||||
|
||||
if employee_onboarding:
|
||||
frappe.db.set_value("Employee Onboarding", employee_onboarding, "boarding_status", status)
|
||||
elif employee_separation:
|
||||
frappe.db.set_value("Employee Separation", employee_separation, "boarding_status", status)
|
@ -877,6 +877,7 @@ def make_return_stock_entry_for_subcontract(
|
||||
{
|
||||
order_doctype: {
|
||||
"doctype": "Stock Entry",
|
||||
"field_no_map": ["purchase_order", "subcontracting_order"],
|
||||
},
|
||||
},
|
||||
ignore_child_tables=True,
|
||||
|
@ -345,7 +345,8 @@
|
||||
"image_field": "website_image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-28 17:10:30.613251",
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-09-13 04:05:11.614087",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Website Item",
|
||||
|
@ -519,6 +519,8 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
|
||||
if not posting_date:
|
||||
posting_date = getdate()
|
||||
|
||||
precision = cint(frappe.db.get_default("currency_precision")) or 2
|
||||
|
||||
unpaid_accrued_entries = frappe.db.sql(
|
||||
"""
|
||||
SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
|
||||
@ -539,6 +541,13 @@ def get_accrued_interest_entries(against_loan, posting_date=None):
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
# Skip entries with zero interest amount & payable principal amount
|
||||
unpaid_accrued_entries = [
|
||||
d
|
||||
for d in unpaid_accrued_entries
|
||||
if flt(d.interest_amount, precision) > 0 or flt(d.payable_principal_amount, precision) > 0
|
||||
]
|
||||
|
||||
return unpaid_accrued_entries
|
||||
|
||||
|
||||
|
@ -11,17 +11,24 @@ frappe.query_reports["BOM Stock Calculated"] = {
|
||||
"options": "BOM",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_to_make",
|
||||
"label": __("Quantity to Make"),
|
||||
"fieldtype": "Int",
|
||||
"default": "1"
|
||||
},
|
||||
|
||||
{
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"label": __("Warehouse"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Warehouse",
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_to_make",
|
||||
"label": __("Quantity to Make"),
|
||||
"fieldtype": "Float",
|
||||
"default": "1.0",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_exploded_view",
|
||||
"label": __("Show exploded view"),
|
||||
"fieldtype": "Check"
|
||||
"fieldtype": "Check",
|
||||
"default": false,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -4,29 +4,31 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils.data import comma_and
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
# if not filters: filters = {}
|
||||
columns = get_columns()
|
||||
summ_data = []
|
||||
data = []
|
||||
|
||||
data = get_bom_stock(filters)
|
||||
bom_data = get_bom_data(filters)
|
||||
qty_to_make = filters.get("qty_to_make")
|
||||
|
||||
manufacture_details = get_manufacturer_records()
|
||||
for row in data:
|
||||
reqd_qty = qty_to_make * row.actual_qty
|
||||
last_pur_price = frappe.db.get_value("Item", row.item_code, "last_purchase_rate")
|
||||
|
||||
summ_data.append(get_report_data(last_pur_price, reqd_qty, row, manufacture_details))
|
||||
return columns, summ_data
|
||||
for row in bom_data:
|
||||
required_qty = qty_to_make * row.qty_per_unit
|
||||
last_purchase_rate = frappe.db.get_value("Item", row.item_code, "last_purchase_rate")
|
||||
|
||||
data.append(get_report_data(last_purchase_rate, required_qty, row, manufacture_details))
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
|
||||
to_build = row.to_build if row.to_build > 0 else 0
|
||||
diff_qty = to_build - reqd_qty
|
||||
def get_report_data(last_purchase_rate, required_qty, row, manufacture_details):
|
||||
qty_per_unit = row.qty_per_unit if row.qty_per_unit > 0 else 0
|
||||
difference_qty = row.actual_qty - required_qty
|
||||
return [
|
||||
row.item_code,
|
||||
row.description,
|
||||
@ -34,85 +36,126 @@ def get_report_data(last_pur_price, reqd_qty, row, manufacture_details):
|
||||
comma_and(
|
||||
manufacture_details.get(row.item_code, {}).get("manufacturer_part", []), add_quotes=False
|
||||
),
|
||||
qty_per_unit,
|
||||
row.actual_qty,
|
||||
str(to_build),
|
||||
reqd_qty,
|
||||
diff_qty,
|
||||
last_pur_price,
|
||||
required_qty,
|
||||
difference_qty,
|
||||
last_purchase_rate,
|
||||
]
|
||||
|
||||
|
||||
def get_columns():
|
||||
"""return columns"""
|
||||
columns = [
|
||||
_("Item") + ":Link/Item:100",
|
||||
_("Description") + "::150",
|
||||
_("Manufacturer") + "::250",
|
||||
_("Manufacturer Part Number") + "::250",
|
||||
_("Qty") + ":Float:50",
|
||||
_("Stock Qty") + ":Float:100",
|
||||
_("Reqd Qty") + ":Float:100",
|
||||
_("Diff Qty") + ":Float:100",
|
||||
_("Last Purchase Price") + ":Float:100",
|
||||
return [
|
||||
{
|
||||
"fieldname": "item",
|
||||
"label": _("Item"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"label": _("Description"),
|
||||
"fieldtype": "Data",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"fieldname": "manufacturer",
|
||||
"label": _("Manufacturer"),
|
||||
"fieldtype": "Data",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "manufacturer_part_number",
|
||||
"label": _("Manufacturer Part Number"),
|
||||
"fieldtype": "Data",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_per_unit",
|
||||
"label": _("Qty Per Unit"),
|
||||
"fieldtype": "Float",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
"fieldname": "available_qty",
|
||||
"label": _("Available Qty"),
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "required_qty",
|
||||
"label": _("Required Qty"),
|
||||
"fieldtype": "Float",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_qty",
|
||||
"label": _("Difference Qty"),
|
||||
"fieldtype": "Float",
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"fieldname": "last_purchase_rate",
|
||||
"label": _("Last Purchase Rate"),
|
||||
"fieldtype": "Float",
|
||||
"width": 160,
|
||||
},
|
||||
]
|
||||
return columns
|
||||
|
||||
|
||||
def get_bom_stock(filters):
|
||||
conditions = ""
|
||||
bom = filters.get("bom")
|
||||
|
||||
table = "`tabBOM Item`"
|
||||
qty_field = "qty"
|
||||
|
||||
def get_bom_data(filters):
|
||||
if filters.get("show_exploded_view"):
|
||||
table = "`tabBOM Explosion Item`"
|
||||
qty_field = "stock_qty"
|
||||
bom_item_table = "BOM Explosion Item"
|
||||
else:
|
||||
bom_item_table = "BOM Item"
|
||||
|
||||
bom_item = frappe.qb.DocType(bom_item_table)
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bom_item)
|
||||
.left_join(bin)
|
||||
.on(bom_item.item_code == bin.item_code)
|
||||
.select(
|
||||
bom_item.item_code,
|
||||
bom_item.description,
|
||||
bom_item.qty_consumed_per_unit.as_("qty_per_unit"),
|
||||
IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"),
|
||||
)
|
||||
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
|
||||
.groupby(bom_item.item_code)
|
||||
)
|
||||
|
||||
if filters.get("warehouse"):
|
||||
warehouse_details = frappe.db.get_value(
|
||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||
)
|
||||
|
||||
if warehouse_details:
|
||||
conditions += (
|
||||
" and exists (select name from `tabWarehouse` wh \
|
||||
where wh.lft >= %s and wh.rgt <= %s and ledger.warehouse = wh.name)"
|
||||
% (warehouse_details.lft, warehouse_details.rgt)
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.where(
|
||||
ExistsCriterion(
|
||||
frappe.qb.from_(wh)
|
||||
.select(wh.name)
|
||||
.where(
|
||||
(wh.lft >= warehouse_details.lft)
|
||||
& (wh.rgt <= warehouse_details.rgt)
|
||||
& (bin.warehouse == wh.name)
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
conditions += " and ledger.warehouse = %s" % frappe.db.escape(filters.get("warehouse"))
|
||||
query = query.where(bin.warehouse == frappe.db.escape(filters.get("warehouse")))
|
||||
|
||||
else:
|
||||
conditions += ""
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
bom_item.item_code,
|
||||
bom_item.description,
|
||||
bom_item.{qty_field},
|
||||
ifnull(sum(ledger.actual_qty), 0) as actual_qty,
|
||||
ifnull(sum(FLOOR(ledger.actual_qty / bom_item.{qty_field})), 0) as to_build
|
||||
FROM
|
||||
{table} AS bom_item
|
||||
LEFT JOIN `tabBin` AS ledger
|
||||
ON bom_item.item_code = ledger.item_code
|
||||
{conditions}
|
||||
|
||||
WHERE
|
||||
bom_item.parent = '{bom}' and bom_item.parenttype='BOM'
|
||||
|
||||
GROUP BY bom_item.item_code""".format(
|
||||
qty_field=qty_field, table=table, conditions=conditions, bom=bom
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_manufacturer_records():
|
||||
details = frappe.get_all(
|
||||
"Item Manufacturer", fields=["manufacturer", "manufacturer_part_no", "item_code"]
|
||||
)
|
||||
|
||||
manufacture_details = frappe._dict()
|
||||
for detail in details:
|
||||
dic = manufacture_details.setdefault(detail.get("item_code"), {})
|
||||
|
@ -0,0 +1,115 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
from erpnext.manufacturing.report.bom_stock_calculated.bom_stock_calculated import (
|
||||
execute as bom_stock_calculated_report,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
|
||||
class TestBOMStockCalculated(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.fg_item, self.rm_items = create_items()
|
||||
self.boms = create_boms(self.fg_item, self.rm_items)
|
||||
|
||||
def test_bom_stock_calculated(self):
|
||||
qty_to_make = 10
|
||||
|
||||
# Case 1: When Item(s) Qty and Stock Qty are equal.
|
||||
data = bom_stock_calculated_report(
|
||||
filters={
|
||||
"qty_to_make": qty_to_make,
|
||||
"bom": self.boms[0].name,
|
||||
}
|
||||
)[1]
|
||||
expected_data = get_expected_data(self.boms[0], qty_to_make)
|
||||
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||
|
||||
# Case 2: When Item(s) Qty and Stock Qty are different and BOM Qty is 1.
|
||||
data = bom_stock_calculated_report(
|
||||
filters={
|
||||
"qty_to_make": qty_to_make,
|
||||
"bom": self.boms[1].name,
|
||||
}
|
||||
)[1]
|
||||
expected_data = get_expected_data(self.boms[1], qty_to_make)
|
||||
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||
|
||||
# Case 3: When Item(s) Qty and Stock Qty are different and BOM Qty is greater than 1.
|
||||
data = bom_stock_calculated_report(
|
||||
filters={
|
||||
"qty_to_make": qty_to_make,
|
||||
"bom": self.boms[2].name,
|
||||
}
|
||||
)[1]
|
||||
expected_data = get_expected_data(self.boms[2], qty_to_make)
|
||||
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||
|
||||
|
||||
def create_items():
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item1 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"standard_rate": 100,
|
||||
"opening_stock": 100,
|
||||
"last_purchase_rate": 100,
|
||||
}
|
||||
).name
|
||||
rm_item2 = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"standard_rate": 200,
|
||||
"opening_stock": 200,
|
||||
"last_purchase_rate": 200,
|
||||
}
|
||||
).name
|
||||
|
||||
return fg_item, [rm_item1, rm_item2]
|
||||
|
||||
|
||||
def create_boms(fg_item, rm_items):
|
||||
def update_bom_items(bom, uom, conversion_factor):
|
||||
for item in bom.items:
|
||||
item.uom = uom
|
||||
item.conversion_factor = conversion_factor
|
||||
|
||||
return bom
|
||||
|
||||
bom1 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10)
|
||||
|
||||
bom2 = make_bom(item=fg_item, quantity=1, raw_materials=rm_items, rm_qty=10, do_not_submit=True)
|
||||
bom2 = update_bom_items(bom2, "Box", 10)
|
||||
bom2.save()
|
||||
bom2.submit()
|
||||
|
||||
bom3 = make_bom(item=fg_item, quantity=2, raw_materials=rm_items, rm_qty=10, do_not_submit=True)
|
||||
bom3 = update_bom_items(bom3, "Box", 10)
|
||||
bom3.save()
|
||||
bom3.submit()
|
||||
|
||||
return [bom1, bom2, bom3]
|
||||
|
||||
|
||||
def get_expected_data(bom, qty_to_make):
|
||||
expected_data = []
|
||||
|
||||
for idx in range(len(bom.items)):
|
||||
expected_data.append(
|
||||
[
|
||||
bom.items[idx].item_code,
|
||||
bom.items[idx].item_code,
|
||||
"",
|
||||
"",
|
||||
float(bom.items[idx].stock_qty / bom.quantity),
|
||||
float(100 * (idx + 1)),
|
||||
float(qty_to_make * (bom.items[idx].stock_qty / bom.quantity)),
|
||||
float((100 * (idx + 1)) - (qty_to_make * (bom.items[idx].stock_qty / bom.quantity))),
|
||||
float(100 * (idx + 1)),
|
||||
]
|
||||
)
|
||||
|
||||
return expected_data
|
@ -5,6 +5,7 @@ from typing import Dict, List, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
Filters = frappe._dict
|
||||
Row = frappe._dict
|
||||
@ -14,15 +15,50 @@ QueryArgs = Dict[str, str]
|
||||
|
||||
|
||||
def execute(filters: Filters) -> Tuple[Columns, Data]:
|
||||
filters = frappe._dict(filters or {})
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_data(filters: Filters) -> Data:
|
||||
query_args = get_query_args(filters)
|
||||
data = run_query(query_args)
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(wo)
|
||||
.inner_join(se)
|
||||
.on(wo.name == se.work_order)
|
||||
.select(
|
||||
wo.name,
|
||||
wo.status,
|
||||
wo.production_item,
|
||||
wo.qty,
|
||||
wo.produced_qty,
|
||||
wo.process_loss_qty,
|
||||
(wo.produced_qty - wo.process_loss_qty).as_("actual_produced_qty"),
|
||||
Sum(se.total_incoming_value).as_("total_fg_value"),
|
||||
Sum(se.total_outgoing_value).as_("total_rm_value"),
|
||||
)
|
||||
.where(
|
||||
(wo.process_loss_qty > 0)
|
||||
& (wo.company == filters.company)
|
||||
& (se.docstatus == 1)
|
||||
& (se.posting_date.between(filters.from_date, filters.to_date))
|
||||
)
|
||||
.groupby(se.work_order)
|
||||
)
|
||||
|
||||
if "item" in filters:
|
||||
query.where(wo.production_item == filters.item)
|
||||
|
||||
if "work_order" in filters:
|
||||
query.where(wo.name == filters.work_order)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
|
||||
update_data_with_total_pl_value(data)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@ -67,54 +103,7 @@ def get_columns() -> Columns:
|
||||
]
|
||||
|
||||
|
||||
def get_query_args(filters: Filters) -> QueryArgs:
|
||||
query_args = {}
|
||||
query_args.update(filters)
|
||||
query_args.update(get_filter_conditions(filters))
|
||||
return query_args
|
||||
|
||||
|
||||
def run_query(query_args: QueryArgs) -> Data:
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
wo.name, wo.status, wo.production_item, wo.qty,
|
||||
wo.produced_qty, wo.process_loss_qty,
|
||||
(wo.produced_qty - wo.process_loss_qty) as actual_produced_qty,
|
||||
sum(se.total_incoming_value) as total_fg_value,
|
||||
sum(se.total_outgoing_value) as total_rm_value
|
||||
FROM
|
||||
`tabWork Order` wo INNER JOIN `tabStock Entry` se
|
||||
ON wo.name=se.work_order
|
||||
WHERE
|
||||
process_loss_qty > 0
|
||||
AND wo.company = %(company)s
|
||||
AND se.docstatus = 1
|
||||
AND se.posting_date BETWEEN %(from_date)s AND %(to_date)s
|
||||
{item_filter}
|
||||
{work_order_filter}
|
||||
GROUP BY
|
||||
se.work_order
|
||||
""".format(
|
||||
**query_args
|
||||
),
|
||||
query_args,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
|
||||
def update_data_with_total_pl_value(data: Data) -> None:
|
||||
for row in data:
|
||||
value_per_unit_fg = row["total_fg_value"] / row["actual_produced_qty"]
|
||||
row["total_pl_value"] = row["process_loss_qty"] * value_per_unit_fg
|
||||
|
||||
|
||||
def get_filter_conditions(filters: Filters) -> QueryArgs:
|
||||
filter_conditions = dict(item_filter="", work_order_filter="")
|
||||
if "item" in filters:
|
||||
production_item = filters.get("item")
|
||||
filter_conditions.update({"item_filter": f"AND wo.production_item='{production_item}'"})
|
||||
if "work_order" in filters:
|
||||
work_order_name = filters.get("work_order")
|
||||
filter_conditions.update({"work_order_filter": f"AND wo.name='{work_order_name}'"})
|
||||
return filter_conditions
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@ -17,70 +18,70 @@ def execute(filters=None):
|
||||
def get_item_list(wo_list, filters):
|
||||
out = []
|
||||
|
||||
# Add a row for each item/qty
|
||||
for wo_details in wo_list:
|
||||
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
|
||||
if wo_list:
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType("BOM Item")
|
||||
|
||||
for wo_item_details in frappe.db.get_values(
|
||||
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
|
||||
):
|
||||
# Add a row for each item/qty
|
||||
for wo_details in wo_list:
|
||||
desc = frappe.db.get_value("BOM", wo_details.bom_no, "description")
|
||||
|
||||
item_list = frappe.db.sql(
|
||||
"""SELECT
|
||||
bom_item.item_code as item_code,
|
||||
ifnull(ledger.actual_qty*bom.quantity/bom_item.stock_qty,0) as build_qty
|
||||
FROM
|
||||
`tabBOM` as bom, `tabBOM Item` AS bom_item
|
||||
LEFT JOIN `tabBin` AS ledger
|
||||
ON bom_item.item_code = ledger.item_code
|
||||
AND ledger.warehouse = ifnull(%(warehouse)s,%(filterhouse)s)
|
||||
WHERE
|
||||
bom.name = bom_item.parent
|
||||
and bom_item.item_code = %(item_code)s
|
||||
and bom.name = %(bom)s
|
||||
GROUP BY
|
||||
bom_item.item_code""",
|
||||
{
|
||||
"bom": wo_details.bom_no,
|
||||
"warehouse": wo_item_details.source_warehouse,
|
||||
"filterhouse": filters.warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
for wo_item_details in frappe.db.get_values(
|
||||
"Work Order Item", {"parent": wo_details.name}, ["item_code", "source_warehouse"], as_dict=1
|
||||
):
|
||||
item_list = (
|
||||
frappe.qb.from_(bom)
|
||||
.from_(bom_item)
|
||||
.left_join(bin)
|
||||
.on(
|
||||
(bom_item.item_code == bin.item_code)
|
||||
& (bin.warehouse == IfNull(wo_item_details.source_warehouse, filters.warehouse))
|
||||
)
|
||||
.select(
|
||||
bom_item.item_code.as_("item_code"),
|
||||
IfNull(bin.actual_qty * bom.quantity / bom_item.stock_qty, 0).as_("build_qty"),
|
||||
)
|
||||
.where(
|
||||
(bom.name == bom_item.parent)
|
||||
& (bom_item.item_code == wo_item_details.item_code)
|
||||
& (bom.name == wo_details.bom_no)
|
||||
)
|
||||
.groupby(bom_item.item_code)
|
||||
).run(as_dict=1)
|
||||
|
||||
stock_qty = 0
|
||||
count = 0
|
||||
buildable_qty = wo_details.qty
|
||||
for item in item_list:
|
||||
count = count + 1
|
||||
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
|
||||
stock_qty = stock_qty + 1
|
||||
elif buildable_qty >= item.build_qty:
|
||||
buildable_qty = item.build_qty
|
||||
stock_qty = 0
|
||||
count = 0
|
||||
buildable_qty = wo_details.qty
|
||||
for item in item_list:
|
||||
count = count + 1
|
||||
if item.build_qty >= (wo_details.qty - wo_details.produced_qty):
|
||||
stock_qty = stock_qty + 1
|
||||
elif buildable_qty >= item.build_qty:
|
||||
buildable_qty = item.build_qty
|
||||
|
||||
if count == stock_qty:
|
||||
build = "Y"
|
||||
else:
|
||||
build = "N"
|
||||
if count == stock_qty:
|
||||
build = "Y"
|
||||
else:
|
||||
build = "N"
|
||||
|
||||
row = frappe._dict(
|
||||
{
|
||||
"work_order": wo_details.name,
|
||||
"status": wo_details.status,
|
||||
"req_items": cint(count),
|
||||
"instock": stock_qty,
|
||||
"description": desc,
|
||||
"source_warehouse": wo_item_details.source_warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
"bom_no": wo_details.bom_no,
|
||||
"qty": wo_details.qty,
|
||||
"buildable_qty": buildable_qty,
|
||||
"ready_to_build": build,
|
||||
}
|
||||
)
|
||||
row = frappe._dict(
|
||||
{
|
||||
"work_order": wo_details.name,
|
||||
"status": wo_details.status,
|
||||
"req_items": cint(count),
|
||||
"instock": stock_qty,
|
||||
"description": desc,
|
||||
"source_warehouse": wo_item_details.source_warehouse,
|
||||
"item_code": wo_item_details.item_code,
|
||||
"bom_no": wo_details.bom_no,
|
||||
"qty": wo_details.qty,
|
||||
"buildable_qty": buildable_qty,
|
||||
"ready_to_build": build,
|
||||
}
|
||||
)
|
||||
|
||||
out.append(row)
|
||||
out.append(row)
|
||||
|
||||
return out
|
||||
|
||||
|
@ -307,6 +307,7 @@ erpnext.patches.v13_0.job_card_status_on_hold
|
||||
erpnext.patches.v14_0.copy_is_subcontracted_value_to_is_old_subcontracting_flow
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.patches.v14_0.crm_ux_cleanup
|
||||
erpnext.patches.v14_0.migrate_existing_lead_notes_as_per_the_new_format
|
||||
erpnext.patches.v14_0.remove_india_localisation # 14-07-2022
|
||||
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
|
||||
erpnext.patches.v14_0.remove_hr_and_payroll_modules # 20-07-2022
|
||||
|
@ -0,0 +1,23 @@
|
||||
import frappe
|
||||
from frappe.utils import cstr, strip_html
|
||||
|
||||
|
||||
def execute():
|
||||
for doctype in ("Lead", "Prospect", "Opportunity"):
|
||||
if not frappe.db.has_column(doctype, "notes"):
|
||||
continue
|
||||
|
||||
dt = frappe.qb.DocType(doctype)
|
||||
records = (
|
||||
frappe.qb.from_(dt)
|
||||
.select(dt.name, dt.notes, dt.modified_by, dt.modified)
|
||||
.where(dt.notes.isnotnull() & dt.notes != "")
|
||||
).run(as_dict=True)
|
||||
|
||||
for d in records:
|
||||
if strip_html(cstr(d.notes)).strip():
|
||||
doc = frappe.get_doc(doctype, d.name)
|
||||
doc.append("notes", {"note": d.notes, "added_by": d.modified_by, "added_on": d.modified})
|
||||
doc.update_child_table("notes")
|
||||
|
||||
frappe.db.sql_ddl(f"alter table `tab{doctype}` drop column `notes`")
|
@ -10,7 +10,6 @@ from frappe.model.document import Document
|
||||
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext.controllers.employee_boarding_controller import update_employee_boarding_status
|
||||
from erpnext.controllers.queries import get_filters_cond
|
||||
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
@ -43,7 +42,6 @@ class Project(Document):
|
||||
self.send_welcome_email()
|
||||
self.update_costing()
|
||||
self.update_percent_complete()
|
||||
update_employee_boarding_status(self)
|
||||
|
||||
def copy_from_template(self):
|
||||
"""
|
||||
@ -145,7 +143,6 @@ class Project(Document):
|
||||
def update_project(self):
|
||||
"""Called externally by Task"""
|
||||
self.update_percent_complete()
|
||||
update_employee_boarding_status(self)
|
||||
self.update_costing()
|
||||
self.db_update()
|
||||
|
||||
|
@ -389,6 +389,7 @@ class Company(NestedSet):
|
||||
"capital_work_in_progress_account": "Capital Work in Progress",
|
||||
"asset_received_but_not_billed": "Asset Received But Not Billed",
|
||||
"expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation",
|
||||
"default_expense_account": "Cost of Goods Sold",
|
||||
}
|
||||
|
||||
if self.enable_perpetual_inventory:
|
||||
@ -398,7 +399,6 @@ class Company(NestedSet):
|
||||
"default_inventory_account": "Stock",
|
||||
"stock_adjustment_account": "Stock Adjustment",
|
||||
"expenses_included_in_valuation": "Expenses Included In Valuation",
|
||||
"default_expense_account": "Cost of Goods Sold",
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -396,7 +396,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "salary_information",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Salary Details",
|
||||
"label": "Salary",
|
||||
"oldfieldtype": "Section Break",
|
||||
"width": "50%"
|
||||
},
|
||||
@ -428,7 +428,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "contact_details",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contact"
|
||||
"label": "Address & Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "cell_number",
|
||||
@ -507,7 +507,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "personal_details",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Personal Details"
|
||||
"label": "Personal"
|
||||
},
|
||||
{
|
||||
"fieldname": "passport_number",
|
||||
@ -701,7 +701,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "attendance_and_leave_details",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Attendance and Leave Details"
|
||||
"label": "Attendance & Leaves"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_44",
|
||||
@ -726,7 +726,7 @@
|
||||
{
|
||||
"fieldname": "basic_details_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Basic Details"
|
||||
"label": "Overview"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_details_section",
|
||||
@ -810,7 +810,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2022-08-23 13:47:46.944993",
|
||||
"modified": "2022-09-13 10:27:14.579197",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Employee",
|
||||
|
@ -795,7 +795,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_code",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Customer Code",
|
||||
"no_copy": 1,
|
||||
@ -910,7 +910,8 @@
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-15 09:02:06.177691",
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-09-13 04:08:17.431731",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
@ -945,7 +945,12 @@ class Item(Document):
|
||||
if doctype == "Product Bundle":
|
||||
filters = {"new_item_code": self.name}
|
||||
|
||||
if doctype in (
|
||||
if linked_doc := frappe.db.get_value(
|
||||
doctype, filters, ["new_item_code as docname"], as_dict=True
|
||||
):
|
||||
return linked_doc.update({"doctype": doctype})
|
||||
|
||||
elif doctype in (
|
||||
"Purchase Invoice Item",
|
||||
"Sales Invoice Item",
|
||||
):
|
||||
|
@ -778,6 +778,44 @@ class TestItem(FrappeTestCase):
|
||||
item.has_batch_no = 1
|
||||
item.save()
|
||||
|
||||
def test_customer_codes_length(self):
|
||||
"""Check if item code with special characters are allowed."""
|
||||
item = make_item(properties={"item_code": "Test Item Code With Special Characters"})
|
||||
for row in range(3):
|
||||
item.append("customer_items", {"ref_code": frappe.generate_hash("", 120)})
|
||||
item.save()
|
||||
self.assertTrue(len(item.customer_code) > 140)
|
||||
|
||||
def test_update_is_stock_item(self):
|
||||
# Step - 1: Create an Item with Maintain Stock enabled
|
||||
item = make_item(properties={"is_stock_item": 1})
|
||||
|
||||
# Step - 2: Disable Maintain Stock
|
||||
item.is_stock_item = 0
|
||||
item.save()
|
||||
item.reload()
|
||||
self.assertEqual(item.is_stock_item, 0)
|
||||
|
||||
# Step - 3: Create Product Bundle
|
||||
pb = frappe.new_doc("Product Bundle")
|
||||
pb.new_item_code = item.name
|
||||
pb.flags.ignore_mandatory = True
|
||||
pb.save()
|
||||
|
||||
# Step - 4: Try to enable Maintain Stock, should throw a validation error
|
||||
item.is_stock_item = 1
|
||||
self.assertRaises(frappe.ValidationError, item.save)
|
||||
item.reload()
|
||||
|
||||
# Step - 5: Delete Product Bundle
|
||||
pb.delete()
|
||||
|
||||
# Step - 6: Again try to enable Maintain Stock
|
||||
item.is_stock_item = 1
|
||||
item.save()
|
||||
item.reload()
|
||||
self.assertEqual(item.is_stock_item, 1)
|
||||
|
||||
|
||||
def set_item_variant_settings(fields):
|
||||
doc = frappe.get_doc("Item Variant Settings")
|
||||
|
@ -183,7 +183,7 @@ class PickList(Document):
|
||||
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
|
||||
item_code = item.item_code
|
||||
reference = item.sales_order_item or item.material_request_item
|
||||
key = (item_code, item.uom, reference)
|
||||
key = (item_code, item.uom, item.warehouse, reference)
|
||||
|
||||
item.idx = None
|
||||
item.name = None
|
||||
|
@ -362,6 +362,12 @@ class PurchaseReceipt(BuyingController):
|
||||
if credit_currency == self.company_currency
|
||||
else flt(d.net_amount, d.precision("net_amount"))
|
||||
)
|
||||
|
||||
outgoing_amount = d.base_net_amount
|
||||
if self.is_internal_supplier and d.valuation_rate:
|
||||
outgoing_amount = d.valuation_rate * d.stock_qty
|
||||
credit_amount = outgoing_amount
|
||||
|
||||
if credit_amount:
|
||||
account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb
|
||||
|
||||
@ -369,7 +375,7 @@ class PurchaseReceipt(BuyingController):
|
||||
gl_entries=gl_entries,
|
||||
account=account,
|
||||
cost_center=d.cost_center,
|
||||
debit=-1 * flt(d.base_net_amount, d.precision("base_net_amount")),
|
||||
debit=-1 * flt(outgoing_amount, d.precision("base_net_amount")),
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=warehouse_account_name,
|
||||
@ -456,7 +462,7 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
# divisional loss adjustment
|
||||
valuation_amount_as_per_doc = (
|
||||
flt(d.base_net_amount, d.precision("base_net_amount"))
|
||||
flt(outgoing_amount, d.precision("base_net_amount"))
|
||||
+ flt(d.landed_cost_voucher_amount)
|
||||
+ flt(d.rm_supp_cost)
|
||||
+ flt(d.item_tax_amount)
|
||||
|
@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, cint, cstr, flt, today
|
||||
from pypika import functions as fn
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
@ -1156,6 +1157,125 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
if gle.account == account:
|
||||
self.assertEqual(gle.credit, 50)
|
||||
|
||||
def test_backdated_transaction_for_internal_transfer(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
|
||||
to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
|
||||
item_doc = create_item("Test Internal Transfer Item")
|
||||
|
||||
target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
|
||||
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
posting_date=add_days(today(), -1),
|
||||
warehouse=from_warehouse,
|
||||
qty=1,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
dn1 = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer=customer,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=1,
|
||||
rate=500,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=target_warehouse,
|
||||
)
|
||||
|
||||
self.assertEqual(dn1.items[0].rate, 100)
|
||||
|
||||
pr1 = make_inter_company_purchase_receipt(dn1.name)
|
||||
pr1.items[0].warehouse = to_warehouse
|
||||
self.assertEqual(pr1.items[0].rate, 100)
|
||||
pr1.submit()
|
||||
|
||||
# Backdated purchase receipt entry, the valuation rate should be updated for DN1 and PR1
|
||||
make_purchase_receipt(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
posting_date=add_days(today(), -2),
|
||||
warehouse=from_warehouse,
|
||||
qty=1,
|
||||
rate=200,
|
||||
)
|
||||
|
||||
dn_value = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Delivery Note", "voucher_no": dn1.name, "warehouse": target_warehouse},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertEqual(abs(dn_value), 200.00)
|
||||
|
||||
pr_value = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr1.name, "warehouse": to_warehouse},
|
||||
"stock_value_difference",
|
||||
)
|
||||
|
||||
self.assertEqual(abs(pr_value), 200.00)
|
||||
pr1.load_from_db()
|
||||
|
||||
self.assertEqual(pr1.items[0].valuation_rate, 200)
|
||||
self.assertEqual(pr1.items[0].rate, 100)
|
||||
|
||||
Gl = frappe.qb.DocType("GL Entry")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(Gl)
|
||||
.select(
|
||||
(fn.Sum(Gl.debit) - fn.Sum(Gl.credit)).as_("value"),
|
||||
)
|
||||
.where((Gl.voucher_type == pr1.doctype) & (Gl.voucher_no == pr1.name))
|
||||
).run(as_dict=True)
|
||||
|
||||
self.assertEqual(query[0].value, 0)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
from erpnext.selling.doctype.customer.test_customer import create_internal_customer
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
create_internal_customer(
|
||||
"_Test Internal Customer 2",
|
||||
company,
|
||||
company,
|
||||
)
|
||||
|
||||
create_internal_supplier(
|
||||
"_Test Internal Supplier 2",
|
||||
company,
|
||||
company,
|
||||
)
|
||||
|
||||
if not frappe.db.get_value("Company", company, "unrealized_profit_loss_account"):
|
||||
account = "Unrealized Profit and Loss - TCP1"
|
||||
if not frappe.db.exists("Account", account):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "Unrealized Profit and Loss",
|
||||
"parent_account": "Direct Income - TCP1",
|
||||
"company": company,
|
||||
"is_group": 0,
|
||||
"account_type": "Income Account",
|
||||
}
|
||||
).insert()
|
||||
|
||||
frappe.db.set_value("Company", company, "unrealized_profit_loss_account", account)
|
||||
|
||||
|
||||
def get_sl_entries(voucher_type, voucher_no):
|
||||
return frappe.db.sql(
|
||||
|
@ -815,7 +815,8 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
return {
|
||||
"filters": {
|
||||
"docstatus": 1,
|
||||
"company": me.frm.doc.company
|
||||
"company": me.frm.doc.company,
|
||||
"status": ["not in", ["Completed", "Closed"]]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
@ -117,6 +117,7 @@ class StockEntry(StockController):
|
||||
self.validate_work_order()
|
||||
self.validate_bom()
|
||||
self.validate_purchase_order()
|
||||
self.validate_subcontracting_order()
|
||||
|
||||
if self.purpose in ("Manufacture", "Repack"):
|
||||
self.mark_finished_and_scrap_items()
|
||||
@ -875,25 +876,24 @@ class StockEntry(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
parent = frappe.qb.DocType("Stock Entry")
|
||||
child = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
conditions = (
|
||||
(parent.docstatus == 1)
|
||||
& (child.item_code == se_item.item_code)
|
||||
& (
|
||||
(parent.purchase_order == self.purchase_order)
|
||||
if self.subcontract_data.order_doctype == "Purchase Order"
|
||||
else (parent.subcontracting_order == self.subcontracting_order)
|
||||
)
|
||||
)
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
se_detail = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
total_supplied = (
|
||||
frappe.qb.from_(parent)
|
||||
.inner_join(child)
|
||||
.on(parent.name == child.parent)
|
||||
.select(Sum(child.transfer_qty))
|
||||
.where(conditions)
|
||||
frappe.qb.from_(se)
|
||||
.inner_join(se_detail)
|
||||
.on(se.name == se_detail.parent)
|
||||
.select(Sum(se_detail.transfer_qty))
|
||||
.where(
|
||||
(se.purpose == "Send to Subcontractor")
|
||||
& (se.docstatus == 1)
|
||||
& (se_detail.item_code == se_item.item_code)
|
||||
& (
|
||||
(se.purchase_order == self.purchase_order)
|
||||
if self.subcontract_data.order_doctype == "Purchase Order"
|
||||
else (se.subcontracting_order == self.subcontracting_order)
|
||||
)
|
||||
)
|
||||
).run()[0][0]
|
||||
|
||||
if flt(total_supplied, precision) > flt(total_allowed, precision):
|
||||
@ -960,6 +960,20 @@ class StockEntry(StockController):
|
||||
)
|
||||
)
|
||||
|
||||
def validate_subcontracting_order(self):
|
||||
if self.get("subcontracting_order") and self.purpose in [
|
||||
"Send to Subcontractor",
|
||||
"Material Transfer",
|
||||
]:
|
||||
sco_status = frappe.db.get_value("Subcontracting Order", self.subcontracting_order, "status")
|
||||
|
||||
if sco_status == "Closed":
|
||||
frappe.throw(
|
||||
_("Cannot create Stock Entry against a closed Subcontracting Order {0}.").format(
|
||||
self.subcontracting_order
|
||||
)
|
||||
)
|
||||
|
||||
def mark_finished_and_scrap_items(self):
|
||||
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
|
||||
return
|
||||
|
@ -727,12 +727,21 @@ def create_stock_reconciliation(**args):
|
||||
sr.set_posting_time = 1
|
||||
sr.company = args.company or "_Test Company"
|
||||
sr.expense_account = args.expense_account or (
|
||||
"Stock Adjustment - _TC" if frappe.get_all("Stock Ledger Entry") else "Temporary Opening - _TC"
|
||||
(
|
||||
frappe.get_cached_value("Company", sr.company, "stock_adjustment_account")
|
||||
or frappe.get_cached_value(
|
||||
"Account", {"account_type": "Stock Adjustment", "company": sr.company}, "name"
|
||||
)
|
||||
)
|
||||
if frappe.get_all("Stock Ledger Entry", {"company": sr.company})
|
||||
else frappe.get_cached_value(
|
||||
"Account", {"account_type": "Temporary", "company": sr.company}, "name"
|
||||
)
|
||||
)
|
||||
sr.cost_center = (
|
||||
args.cost_center
|
||||
or frappe.get_cached_value("Company", sr.company, "cost_center")
|
||||
or "_Test Cost Center - _TC"
|
||||
or frappe.get_cached_value("Cost Center", filters={"is_group": 0, "company": sr.company})
|
||||
)
|
||||
|
||||
sr.append(
|
||||
|
@ -649,21 +649,25 @@ class update_entries_after(object):
|
||||
|
||||
elif (
|
||||
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
|
||||
and sle.actual_qty > 0
|
||||
and sle.voucher_detail_no
|
||||
and frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_internal_supplier")
|
||||
):
|
||||
sle_details = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{
|
||||
"voucher_type": sle.voucher_type,
|
||||
"voucher_no": sle.voucher_no,
|
||||
"dependant_sle_voucher_detail_no": sle.voucher_detail_no,
|
||||
},
|
||||
["stock_value_difference", "actual_qty"],
|
||||
as_dict=1,
|
||||
field = (
|
||||
"delivery_note_item" if sle.voucher_type == "Purchase Receipt" else "sales_invoice_item"
|
||||
)
|
||||
doctype = (
|
||||
"Delivery Note Item" if sle.voucher_type == "Purchase Receipt" else "Sales Invoice Item"
|
||||
)
|
||||
refernce_name = frappe.get_cached_value(
|
||||
sle.voucher_type + " Item", sle.voucher_detail_no, field
|
||||
)
|
||||
|
||||
rate = abs(sle_details.stock_value_difference / sle.actual_qty)
|
||||
if refernce_name:
|
||||
rate = frappe.get_cached_value(
|
||||
doctype,
|
||||
refernce_name,
|
||||
"incoming_rate",
|
||||
)
|
||||
else:
|
||||
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
||||
rate_field = "valuation_rate"
|
||||
@ -745,7 +749,12 @@ class update_entries_after(object):
|
||||
def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
|
||||
if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
|
||||
frappe.db.set_value(
|
||||
sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate
|
||||
sle.voucher_type + " Item",
|
||||
sle.voucher_detail_no,
|
||||
{
|
||||
"base_net_rate": outgoing_rate,
|
||||
"valuation_rate": outgoing_rate,
|
||||
},
|
||||
)
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
|
@ -107,7 +107,7 @@ frappe.ui.form.on('Subcontracting Order', {
|
||||
get_materials_from_supplier: function (frm) {
|
||||
let sco_rm_details = [];
|
||||
|
||||
if (frm.doc.supplied_items && frm.doc.per_received > 0) {
|
||||
if (frm.doc.status != "Closed" && frm.doc.supplied_items && frm.doc.per_received > 0) {
|
||||
frm.doc.supplied_items.forEach(d => {
|
||||
if (d.total_supplied_qty > 0 && d.total_supplied_qty != d.consumed_qty) {
|
||||
sco_rm_details.push(d.name);
|
||||
|
@ -502,6 +502,35 @@ class TestSubcontractingOrder(FrappeTestCase):
|
||||
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
def test_get_materials_from_supplier(self):
|
||||
# Create SCO
|
||||
sco = get_subcontracting_order()
|
||||
|
||||
# Transfer RM
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
|
||||
# Create SCR (Partial)
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.items[0].qty -= 5
|
||||
scr.save()
|
||||
scr.submit()
|
||||
|
||||
# Get RM from Supplier
|
||||
ste = get_materials_from_supplier(sco.name, [d.name for d in sco.supplied_items])
|
||||
ste.save()
|
||||
ste.submit()
|
||||
|
||||
sco.load_from_db()
|
||||
|
||||
self.assertEqual(sco.status, "Closed")
|
||||
self.assertEqual(sco.supplied_items[0].returned_qty, 5)
|
||||
|
||||
|
||||
def create_subcontracting_order(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -776,6 +776,9 @@ def on_communication_update(doc, status):
|
||||
if not parent.meta.has_field("service_level_agreement"):
|
||||
return
|
||||
|
||||
if not parent.get("service_level_agreement"):
|
||||
return
|
||||
|
||||
if (
|
||||
doc.sent_or_received == "Received" # a reply is received
|
||||
and parent.get("status") == "Open" # issue status is set as open from communication.py
|
||||
|
713
license.txt
713
license.txt
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user