Merge branch 'develop' into better_handling_of_duplicate_bundle_items

This commit is contained in:
Sagar Sharma 2023-01-09 21:48:48 +05:30 committed by GitHub
commit b56c1ed050
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 3332 additions and 1702 deletions

View File

@ -41,12 +41,17 @@ fi
install_whktml() { install_whktml() {
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz if [ "$(lsb_release -rs)" = "22.04" ]; then
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf sudo apt install /tmp/wkhtmltox.deb
sudo chmod o+x /usr/local/bin/wkhtmltopdf else
echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
exit 1
fi
} }
install_whktml & install_whktml &
wkpid=$!
cd ~/frappe-bench || exit cd ~/frappe-bench || exit
@ -60,6 +65,8 @@ bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
wait $wkpid
bench start &> bench_run_logs.txt & bench start &> bench_run_logs.txt &
CI=Yes bench build --app frappe & CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes bench --site test_site reinstall --yes

View File

@ -13,10 +13,10 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Node.js v14 - name: Setup Node.js
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 14 node-version: 18
- name: Setup dependencies - name: Setup dependencies
run: | run: |
npm install @semantic-release/git @semantic-release/exec --no-save npm install @semantic-release/git @semantic-release/exec --no-save

View File

@ -16,12 +16,12 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
user: user:
description: 'user' description: 'Frappe Framework repository user (add your username for forks)'
required: true required: true
default: 'frappe' default: 'frappe'
type: string type: string
branch: branch:
description: 'Branch name' description: 'Frappe Framework branch'
default: 'develop' default: 'develop'
required: false required: false
type: string type: string

View File

@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
return return
# check if books nor frozen till endate: # check if books nor frozen till endate:
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto): if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1)) end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry: if via_journal_entry:

View File

@ -37,14 +37,11 @@ frappe.ui.form.on("Bank Clearance", {
refresh: function(frm) { refresh: function(frm) {
frm.disable_save(); frm.disable_save();
if (frm.doc.account && frm.doc.from_date && frm.doc.to_date) {
frm.add_custom_button(__('Get Payment Entries'), () => frm.add_custom_button(__('Get Payment Entries'), () =>
frm.trigger("get_payment_entries") frm.trigger("get_payment_entries")
); );
frm.change_custom_button_type('Get Payment Entries', null, 'primary'); frm.change_custom_button_type('Get Payment Entries', null, 'primary');
}
}, },
update_clearance_date: function(frm) { update_clearance_date: function(frm) {

View File

@ -302,7 +302,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
dict( dict(
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"] account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
), ),
["credit", "debit"], ["credit_in_account_currency as credit", "debit_in_account_currency as debit"],
as_dict=1, as_dict=1,
) )
gl_amount, transaction_amount = ( gl_amount, transaction_amount = (

View File

@ -137,7 +137,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
) )
elif doc.payment_type == "Pay": elif doc.payment_type == "Pay":
paid_amount_field = ( paid_amount_field = (
"paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount" "paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
) )
return frappe.db.get_value( return frappe.db.get_value(

View File

@ -26,7 +26,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
doc: frm.doc, doc: frm.doc,
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
frm.add_custom_button(__('Journal Entry'), function() { frm.add_custom_button(__('Journal Entries'), function() {
return frm.events.make_jv(frm); return frm.events.make_jv(frm);
}, __('Create')); }, __('Create'));
} }
@ -35,10 +35,11 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
} }
}, },
get_entries: function(frm) { get_entries: function(frm, account) {
frappe.call({ frappe.call({
method: "get_accounts_data", method: "get_accounts_data",
doc: cur_frm.doc, doc: cur_frm.doc,
account: account,
callback: function(r){ callback: function(r){
frappe.model.clear_table(frm.doc, "accounts"); frappe.model.clear_table(frm.doc, "accounts");
if(r.message) { if(r.message) {
@ -57,7 +58,6 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
let total_gain_loss = 0; let total_gain_loss = 0;
frm.doc.accounts.forEach((d) => { frm.doc.accounts.forEach((d) => {
d.gain_loss = flt(d.new_balance_in_base_currency, precision("new_balance_in_base_currency", d)) - flt(d.balance_in_base_currency, precision("balance_in_base_currency", d));
total_gain_loss += flt(d.gain_loss, precision("gain_loss", d)); total_gain_loss += flt(d.gain_loss, precision("gain_loss", d));
}); });
@ -66,13 +66,19 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
}, },
make_jv : function(frm) { make_jv : function(frm) {
let revaluation_journal = null;
let zero_balance_journal = null;
frappe.call({ frappe.call({
method: "make_jv_entry", method: "make_jv_entries",
doc: frm.doc, doc: frm.doc,
freeze: true,
freeze_message: "Making Journal Entries...",
callback: function(r){ callback: function(r){
if (r.message) { if (r.message) {
var doc = frappe.model.sync(r.message)[0]; let response = r.message;
frappe.set_route("Form", doc.doctype, doc.name); if(response['revaluation_jv'] || response['zero_balance_jv']) {
frappe.msgprint(__("Journals have been created"));
}
} }
} }
}); });

View File

@ -14,6 +14,9 @@
"get_entries", "get_entries",
"accounts", "accounts",
"section_break_6", "section_break_6",
"gain_loss_unbooked",
"gain_loss_booked",
"column_break_10",
"total_gain_loss", "total_gain_loss",
"amended_from" "amended_from"
], ],
@ -59,13 +62,6 @@
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{
"fieldname": "total_gain_loss",
"fieldtype": "Currency",
"label": "Total Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@ -74,11 +70,37 @@
"options": "Exchange Rate Revaluation", "options": "Exchange Rate Revaluation",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "gain_loss_unbooked",
"fieldtype": "Currency",
"label": "Gain/Loss from Revaluation",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"description": "Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency",
"fieldname": "gain_loss_booked",
"fieldtype": "Currency",
"label": "Gain/Loss already booked",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "total_gain_loss",
"fieldtype": "Currency",
"label": "Total Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-17 10:28:03.911554", "modified": "2022-12-29 19:38:24.416529",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Exchange Rate Revaluation", "name": "Exchange Rate Revaluation",

View File

@ -3,10 +3,12 @@
import frappe import frappe
from frappe import _ from frappe import _, qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils import flt from frappe.query_builder import Criterion, Order
from frappe.query_builder.functions import NullIf, Sum
from frappe.utils import flt, get_link_to_form
import erpnext import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
@ -19,11 +21,25 @@ class ExchangeRateRevaluation(Document):
def set_total_gain_loss(self): def set_total_gain_loss(self):
total_gain_loss = 0 total_gain_loss = 0
gain_loss_booked = 0
gain_loss_unbooked = 0
for d in self.accounts: for d in self.accounts:
if not d.zero_balance:
d.gain_loss = flt( d.gain_loss = flt(
d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency") d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency")) ) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
if d.zero_balance:
gain_loss_booked += flt(d.gain_loss, d.precision("gain_loss"))
else:
gain_loss_unbooked += flt(d.gain_loss, d.precision("gain_loss"))
total_gain_loss += flt(d.gain_loss, d.precision("gain_loss")) total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))
self.gain_loss_booked = gain_loss_booked
self.gain_loss_unbooked = gain_loss_unbooked
self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss")) self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))
def validate_mandatory(self): def validate_mandatory(self):
@ -35,37 +51,143 @@ class ExchangeRateRevaluation(Document):
@frappe.whitelist() @frappe.whitelist()
def check_journal_entry_condition(self): def check_journal_entry_condition(self):
total_debit = frappe.db.get_value( exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
"Journal Entry Account",
{"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1}, jea = qb.DocType("Journal Entry Account")
"sum(debit) as sum", journals = (
qb.from_(jea)
.select(jea.parent)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
)
.run()
) )
total_amt = 0 if journals:
for d in self.accounts: gle = qb.DocType("GL Entry")
total_amt = total_amt + d.new_balance_in_base_currency total_amt = (
qb.from_(gle)
.select((Sum(gle.credit) - Sum(gle.debit)).as_("total_amount"))
.where(
(gle.voucher_type == "Journal Entry")
& (gle.voucher_no.isin(journals))
& (gle.account == exchange_gain_loss_account)
& (gle.is_cancelled == 0)
)
.run()
)
if total_amt != total_debit: if total_amt and total_amt[0][0] != self.total_gain_loss:
return True return True
else:
return False return False
return True
@frappe.whitelist() @frappe.whitelist()
def get_accounts_data(self, account=None): def get_accounts_data(self):
accounts = []
self.validate_mandatory() self.validate_mandatory()
company_currency = erpnext.get_company_currency(self.company) account_details = self.get_account_balance_from_gle(
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
)
accounts_with_new_balance = self.calculate_new_account_balance(
self.company, self.posting_date, account_details
)
if not accounts_with_new_balance:
self.throw_invalid_response_message(account_details)
return accounts_with_new_balance
@staticmethod
def get_account_balance_from_gle(company, posting_date, account, party_type, party):
account_details = []
if company and posting_date:
company_currency = erpnext.get_company_currency(company)
acc = qb.DocType("Account")
if account:
accounts = [account]
else:
res = (
qb.from_(acc)
.select(acc.name)
.where(
(acc.is_group == 0)
& (acc.report_type == "Balance Sheet")
& (acc.root_type.isin(["Asset", "Liability", "Equity"]))
& (acc.account_type != "Stock")
& (acc.company == company)
& (acc.account_currency != company_currency)
)
.orderby(acc.name)
.run(as_list=True)
)
accounts = [x[0] for x in res]
if accounts:
having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
)
gle = qb.DocType("GL Entry")
# conditions
conditions = []
conditions.append(gle.account.isin(accounts))
conditions.append(gle.posting_date.lte(posting_date))
conditions.append(gle.is_cancelled == 0)
if party_type:
conditions.append(gle.party_type == party_type)
if party:
conditions.append(gle.party == party)
account_details = (
qb.from_(gle)
.select(
gle.account,
gle.party_type,
gle.party,
gle.account_currency,
(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_(
"balance_in_account_currency"
),
(Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
(Sum(gle.debit) - Sum(gle.credit) == 0)
^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_(
"zero_balance"
),
)
.where(Criterion.all(conditions))
.groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, ""))
.having(having_clause)
.orderby(gle.account)
.run(as_dict=True)
)
return account_details
@staticmethod
def calculate_new_account_balance(company, posting_date, account_details):
accounts = []
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision( precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"), frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency, company_currency,
) )
account_details = self.get_accounts_from_gle() if account_details:
for d in account_details: # Handle Accounts with balance in both Account/Base Currency
for d in [x for x in account_details if not x.zero_balance]:
current_exchange_rate = ( current_exchange_rate = (
d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0 d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
) )
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date) new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate) new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision) gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
if gain_loss: if gain_loss:
@ -77,56 +199,58 @@ class ExchangeRateRevaluation(Document):
"account_currency": d.account_currency, "account_currency": d.account_currency,
"balance_in_base_currency": d.balance, "balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency, "balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate, "current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate, "new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency, "new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": d.balance_in_account_currency,
"gain_loss": gain_loss,
} }
) )
if not accounts: # Handle Accounts with '0' balance in Account/Base Currency
self.throw_invalid_response_message(account_details) for d in [x for x in account_details if x.zero_balance]:
# TODO: Set new balance in Base/Account currency
if d.balance > 0:
current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0'
new_balance_in_base_currency = 0 # this will be '0'
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
else:
new_exchange_rate = 0
new_balance_in_base_currency = 0
new_balance_in_account_currency = 0
current_exchange_rate = calculate_exchange_rate_using_last_gle(
company, d.account, d.party_type, d.party
)
gain_loss = new_balance_in_account_currency - (
current_exchange_rate * d.balance_in_account_currency
)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": new_balance_in_account_currency,
"gain_loss": gain_loss,
}
)
return accounts return accounts
def get_accounts_from_gle(self):
company_currency = erpnext.get_company_currency(self.company)
accounts = frappe.db.sql_list(
"""
select name
from tabAccount
where is_group = 0
and report_type = 'Balance Sheet'
and root_type in ('Asset', 'Liability', 'Equity')
and account_type != 'Stock'
and company=%s
and account_currency != %s
order by name""",
(self.company, company_currency),
)
account_details = []
if accounts:
account_details = frappe.db.sql(
"""
select
account, party_type, party, account_currency,
sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency,
sum(debit) - sum(credit) as balance
from `tabGL Entry`
where account in (%s)
and posting_date <= %s
and is_cancelled = 0
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit)
order by account
"""
% (", ".join(["%s"] * len(accounts)), "%s"),
tuple(accounts + [self.posting_date]),
as_dict=1,
)
return account_details
def throw_invalid_response_message(self, account_details): def throw_invalid_response_message(self, account_details):
if account_details: if account_details:
message = _("No outstanding invoices require exchange rate revaluation") message = _("No outstanding invoices require exchange rate revaluation")
@ -134,11 +258,7 @@ class ExchangeRateRevaluation(Document):
message = _("No outstanding invoices found") message = _("No outstanding invoices found")
frappe.msgprint(message) frappe.msgprint(message)
@frappe.whitelist() def get_for_unrealized_gain_loss_account(self):
def make_jv_entry(self):
if self.total_gain_loss == 0:
return
unrealized_exchange_gain_loss_account = frappe.get_cached_value( unrealized_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "unrealized_exchange_gain_loss_account" "Company", self.company, "unrealized_exchange_gain_loss_account"
) )
@ -146,6 +266,130 @@ class ExchangeRateRevaluation(Document):
frappe.throw( frappe.throw(
_("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company) _("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
) )
return unrealized_exchange_gain_loss_account
@frappe.whitelist()
def make_jv_entries(self):
zero_balance_jv = self.make_jv_for_zero_balance()
if zero_balance_jv:
frappe.msgprint(
f"Zero Balance Journal: {get_link_to_form('Journal Entry', zero_balance_jv.name)}"
)
revaluation_jv = self.make_jv_for_revaluation()
if revaluation_jv:
frappe.msgprint(
f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}"
)
return {
"revaluation_jv": revaluation_jv.name if revaluation_jv else None,
"zero_balance_jv": zero_balance_jv.name if zero_balance_jv else None,
}
def make_jv_for_zero_balance(self):
if self.gain_loss_booked == 0:
return
accounts = [x for x in self.accounts if x.zero_balance]
if not accounts:
return
unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = self.company
journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1
journal_entry_accounts = []
for d in accounts:
journal_account = frappe._dict(
{
"account": d.get("account"),
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": flt(
d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
),
"exchange_rate": 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
# Account Currency has balance
if d.get("balance_in_account_currency") and not d.get("new_balance_in_account_currency"):
dr_or_cr = (
"credit_in_account_currency"
if d.get("balance_in_account_currency") > 0
else "debit_in_account_currency"
)
reverse_dr_or_cr = (
"debit_in_account_currency"
if dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
)
journal_account.update(
{
dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
reverse_dr_or_cr: 0,
"debit": 0,
"credit": 0,
}
)
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
# Base currency has balance
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account.update(
{
dr_or_cr: flt(
abs(d.get("balance_in_base_currency")), d.precision("balance_in_base_currency")
),
reverse_dr_or_cr: 0,
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
}
)
journal_entry_accounts.append(journal_account)
journal_entry_accounts.append(
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_total_debit_credit()
journal_entry.save()
return journal_entry
def make_jv_for_revaluation(self):
if self.gain_loss_unbooked == 0:
return
accounts = [x for x in self.accounts if not x.zero_balance]
if not accounts:
return
unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journal_entry = frappe.new_doc("Journal Entry") journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Rate Revaluation" journal_entry.voucher_type = "Exchange Rate Revaluation"
@ -154,7 +398,7 @@ class ExchangeRateRevaluation(Document):
journal_entry.multi_currency = 1 journal_entry.multi_currency = 1
journal_entry_accounts = [] journal_entry_accounts = []
for d in self.accounts: for d in accounts:
dr_or_cr = ( dr_or_cr = (
"debit_in_account_currency" "debit_in_account_currency"
if d.get("balance_in_account_currency") > 0 if d.get("balance_in_account_currency") > 0
@ -179,6 +423,7 @@ class ExchangeRateRevaluation(Document):
dr_or_cr: flt( dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
), ),
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")), "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
@ -196,6 +441,7 @@ class ExchangeRateRevaluation(Document):
reverse_dr_or_cr: flt( reverse_dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency") abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
), ),
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")), "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
@ -206,8 +452,11 @@ class ExchangeRateRevaluation(Document):
{ {
"account": unrealized_exchange_gain_loss_account, "account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account), "balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0, "debit_in_account_currency": abs(self.gain_loss_unbooked)
"credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0, if self.gain_loss_unbooked < 0
else 0,
"credit_in_account_currency": self.gain_loss_unbooked if self.gain_loss_unbooked > 0 else 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1, "exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
@ -217,42 +466,90 @@ class ExchangeRateRevaluation(Document):
journal_entry.set("accounts", journal_entry_accounts) journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency() journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit() journal_entry.set_total_debit_credit()
return journal_entry.as_dict() journal_entry.save()
return journal_entry
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
"""
Use last GL entry to calculate exchange rate
"""
last_exchange_rate = None
if company and account:
gl = qb.DocType("GL Entry")
# build conditions
conditions = []
conditions.append(gl.company == company)
conditions.append(gl.account == account)
conditions.append(gl.is_cancelled == 0)
if party_type:
conditions.append(gl.party_type == party_type)
if party:
conditions.append(gl.party == party)
voucher_type, voucher_no = (
qb.from_(gl)
.select(gl.voucher_type, gl.voucher_no)
.where(Criterion.all(conditions))
.orderby(gl.posting_date, order=Order.desc)
.limit(1)
.run()[0]
)
last_exchange_rate = (
qb.from_(gl)
.select((gl.debit - gl.credit) / (gl.debit_in_account_currency - gl.credit_in_account_currency))
.where(
(gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account)
)
.orderby(gl.posting_date, order=Order.desc)
.limit(1)
.run()[0][0]
)
return last_exchange_rate
@frappe.whitelist() @frappe.whitelist()
def get_account_details(account, company, posting_date, party_type=None, party=None): def get_account_details(company, posting_date, account, party_type=None, party=None):
if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory"))
account_currency, account_type = frappe.get_cached_value( account_currency, account_type = frappe.get_cached_value(
"Account", account, ["account_currency", "account_type"] "Account", account, ["account_currency", "account_type"]
) )
if account_type in ["Receivable", "Payable"] and not (party_type and party): if account_type in ["Receivable", "Payable"] and not (party_type and party):
frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type)) frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))
account_details = {} account_details = {}
company_currency = erpnext.get_company_currency(company) company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(
account, date=posting_date, party_type=party_type, party=party, in_account_currency=False
)
account_details = { account_details = {
"account_currency": account_currency, "account_currency": account_currency,
} }
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
)
if balance: if account_balance and (
balance_in_account_currency = get_balance_on( account_balance[0].balance or account_balance[0].balance_in_account_currency
account, date=posting_date, party_type=party_type, party=party ):
account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
company, posting_date, account_balance
) )
current_exchange_rate = ( row = account_with_new_balance[0]
balance / balance_in_account_currency if balance_in_account_currency else 0 account_details.update(
)
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
account_details = account_details.update(
{ {
"balance_in_base_currency": balance, "balance_in_base_currency": row["balance_in_base_currency"],
"balance_in_account_currency": balance_in_account_currency, "balance_in_account_currency": row["balance_in_account_currency"],
"current_exchange_rate": current_exchange_rate, "current_exchange_rate": row["current_exchange_rate"],
"new_exchange_rate": new_exchange_rate, "new_exchange_rate": row["new_exchange_rate"],
"new_balance_in_base_currency": new_balance_in_base_currency, "new_balance_in_base_currency": row["new_balance_in_base_currency"],
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
"zero_balance": row["zero_balance"],
"gain_loss": row["gain_loss"],
} }
) )

View File

@ -10,14 +10,21 @@
"party", "party",
"column_break_2", "column_break_2",
"account_currency", "account_currency",
"account_balances",
"balance_in_account_currency", "balance_in_account_currency",
"column_break_46yz",
"new_balance_in_account_currency",
"balances", "balances",
"current_exchange_rate", "current_exchange_rate",
"balance_in_base_currency", "column_break_xown",
"column_break_9",
"new_exchange_rate", "new_exchange_rate",
"column_break_9",
"balance_in_base_currency",
"column_break_ukce",
"new_balance_in_base_currency", "new_balance_in_base_currency",
"gain_loss" "section_break_ngrs",
"gain_loss",
"zero_balance"
], ],
"fields": [ "fields": [
{ {
@ -78,7 +85,7 @@
}, },
{ {
"fieldname": "column_break_9", "fieldname": "column_break_9",
"fieldtype": "Column Break" "fieldtype": "Section Break"
}, },
{ {
"fieldname": "new_exchange_rate", "fieldname": "new_exchange_rate",
@ -102,11 +109,45 @@
"label": "Gain/Loss", "label": "Gain/Loss",
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"description": "This Account has '0' balance in either Base Currency or Account Currency",
"fieldname": "zero_balance",
"fieldtype": "Check",
"label": "Zero Balance"
},
{
"fieldname": "new_balance_in_account_currency",
"fieldtype": "Currency",
"label": "New Balance In Account Currency",
"options": "account_currency",
"read_only": 1
},
{
"fieldname": "account_balances",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_46yz",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_xown",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ukce",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ngrs",
"fieldtype": "Section Break"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-17 10:26:18.302728", "modified": "2022-12-29 19:38:52.915295",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Exchange Rate Revaluation Account", "name": "Exchange Rate Revaluation Account",

View File

@ -95,7 +95,15 @@ class GLEntry(Document):
) )
# Zero value transaction is not allowed # Zero value transaction is not allowed
if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))): if not (
flt(self.debit, self.precision("debit"))
or flt(self.credit, self.precision("credit"))
or (
self.voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
)
):
frappe.throw( frappe.throw(
_("{0} {1}: Either debit or credit amount is required for {2}").format( _("{0} {1}: Either debit or credit amount is required for {2}").format(
self.voucher_type, self.voucher_no, self.account self.voucher_type, self.voucher_no, self.account

View File

@ -88,7 +88,7 @@
"label": "Entry Type", "label": "Entry Type",
"oldfieldname": "voucher_type", "oldfieldname": "voucher_type",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense", "options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@ -539,7 +539,7 @@
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-06-23 22:01:32.348337", "modified": "2022-11-28 17:40:01.241908",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@ -6,7 +6,7 @@ import json
import frappe import frappe
from frappe import _, msgprint, scrub from frappe import _, msgprint, scrub
from frappe.utils import cint, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
import erpnext import erpnext
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
@ -23,6 +23,9 @@ from erpnext.accounts.utils import (
get_stock_accounts, get_stock_accounts,
get_stock_and_account_balance, get_stock_and_account_balance,
) )
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@ -283,14 +286,15 @@ class JournalEntry(AccountsController):
for d in self.get("accounts"): for d in self.get("accounts"):
if d.reference_type == "Asset" and d.reference_name: if d.reference_type == "Asset" and d.reference_name:
asset = frappe.get_doc("Asset", d.reference_name) asset = frappe.get_doc("Asset", d.reference_name)
for s in asset.get("schedules"): for row in asset.get("finance_books"):
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
for s in depr_schedule or []:
if s.journal_entry == self.name: if s.journal_entry == self.name:
s.db_set("journal_entry", None) s.db_set("journal_entry", None)
idx = cint(s.finance_book_id) or 1 row.value_after_depreciation += s.depreciation_amount
finance_books = asset.get("finance_books")[idx - 1] row.db_update()
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
asset.set_status() asset.set_status()
@ -589,24 +593,26 @@ class JournalEntry(AccountsController):
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field) d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else: else:
for d in self.get("accounts"): for d in self.get("accounts"):
if flt(d.debit > 0): if flt(d.debit) > 0:
accounts_debited.append(d.party or d.account) accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: if flt(d.credit) > 0:
accounts_credited.append(d.party or d.account) accounts_credited.append(d.party or d.account)
for d in self.get("accounts"): for d in self.get("accounts"):
if flt(d.debit > 0): if flt(d.debit) > 0:
d.against_account = ", ".join(list(set(accounts_credited))) d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): if flt(d.credit) > 0:
d.against_account = ", ".join(list(set(accounts_debited))) d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self): def validate_debit_credit_amount(self):
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
for d in self.get("accounts"): for d in self.get("accounts"):
if not flt(d.debit) and not flt(d.credit): if not flt(d.debit) and not flt(d.credit):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx)) frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self): def validate_total_debit_and_credit(self):
self.set_total_debit_credit() self.set_total_debit_credit()
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
if self.difference: if self.difference:
frappe.throw( frappe.throw(
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference) _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
@ -648,6 +654,7 @@ class JournalEntry(AccountsController):
self.set_exchange_rate() self.set_exchange_rate()
def set_amounts_in_company_currency(self): def set_amounts_in_company_currency(self):
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
for d in self.get("accounts"): for d in self.get("accounts"):
d.debit_in_account_currency = flt( d.debit_in_account_currency = flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency") d.debit_in_account_currency, d.precision("debit_in_account_currency")
@ -756,7 +763,7 @@ class JournalEntry(AccountsController):
pay_to_recd_from = d.party pay_to_recd_from = d.party
if pay_to_recd_from and pay_to_recd_from == d.party: if pay_to_recd_from and pay_to_recd_from == d.party:
party_amount += d.debit_in_account_currency or d.credit_in_account_currency party_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
party_account_currency = d.account_currency party_account_currency = d.account_currency
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]: elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
@ -786,7 +793,7 @@ class JournalEntry(AccountsController):
def build_gl_map(self): def build_gl_map(self):
gl_map = [] gl_map = []
for d in self.get("accounts"): for d in self.get("accounts"):
if d.debit or d.credit: if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark] r = [d.user_remark, self.remark]
r = [x for x in r if x] r = [x for x in r if x]
remarks = "\n".join(r) remarks = "\n".join(r)
@ -834,7 +841,7 @@ class JournalEntry(AccountsController):
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding) make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist() @frappe.whitelist()
def get_balance(self): def get_balance(self, difference_account=None):
if not self.get("accounts"): if not self.get("accounts"):
msgprint(_("'Entries' cannot be empty"), raise_exception=True) msgprint(_("'Entries' cannot be empty"), raise_exception=True)
else: else:
@ -849,7 +856,13 @@ class JournalEntry(AccountsController):
blank_row = d blank_row = d
if not blank_row: if not blank_row:
blank_row = self.append("accounts", {}) blank_row = self.append(
"accounts",
{
"account": difference_account,
"cost_center": erpnext.get_default_cost_center(self.company),
},
)
blank_row.exchange_rate = 1 blank_row.exchange_rate = 1
if diff > 0: if diff > 0:

View File

@ -1758,6 +1758,8 @@ def get_payment_entry(
pe.setup_party_account_field() pe.setup_party_account_field()
pe.set_missing_values() pe.set_missing_values()
update_accounting_dimensions(pe, doc)
if party_account and bank: if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc) pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts() pe.set_amounts()
@ -1775,6 +1777,18 @@ def get_payment_entry(
return pe return pe
def update_accounting_dimensions(pe, doc):
"""
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
"""
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
for dimension in get_accounting_dimensions():
pe.set(dimension, doc.get(dimension))
def get_bank_cash_account(doc, bank_account): def get_bank_cash_account(doc, bank_account):
bank = get_default_bank_cash_account( bank = get_default_bank_cash_account(
doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account

View File

@ -170,7 +170,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
} }
reconcile() { reconcile() {
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account); var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount);
if (show_dialog && show_dialog.length) { if (show_dialog && show_dialog.length) {
@ -179,8 +179,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
title: __("Select Difference Account"), title: __("Select Difference Account"),
fields: [ fields: [
{ {
fieldname: "allocation", fieldtype: "Table", label: __("Allocation"), fieldname: "allocation",
data: this.data, in_place_edit: true, fieldtype: "Table",
label: __("Allocation"),
data: this.data,
in_place_edit: true,
cannot_add_rows: true,
get_data: () => { get_data: () => {
return this.data; return this.data;
}, },
@ -218,6 +222,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
read_only: 1 read_only: 1
}] }]
}, },
{
fieldtype: 'HTML',
options: "<b> New Journal Entry will be posted for the difference amount </b>"
}
], ],
primary_action: () => { primary_action: () => {
const args = dialog.get_values()["allocation"]; const args = dialog.get_values()["allocation"];
@ -234,7 +242,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}); });
this.frm.doc.allocation.forEach(d => { this.frm.doc.allocation.forEach(d => {
if (d.difference_amount && !d.difference_account) { if (d.difference_amount) {
dialog.fields_dict.allocation.df.data.push({ dialog.fields_dict.allocation.df.data.push({
'docname': d.name, 'docname': d.name,
'reference_name': d.reference_name, 'reference_name': d.reference_name,

View File

@ -14,7 +14,6 @@ from erpnext.accounts.utils import (
QueryPaymentLedger, QueryPaymentLedger,
get_outstanding_invoices, get_outstanding_invoices,
reconcile_against_document, reconcile_against_document,
update_reference_in_payment_entry,
) )
from erpnext.controllers.accounts_controller import get_advance_payment_entries from erpnext.controllers.accounts_controller import get_advance_payment_entries
@ -80,12 +79,13 @@ class PaymentReconciliation(Document):
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1" "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
) )
# nosemgrep
journal_entries = frappe.db.sql( journal_entries = frappe.db.sql(
""" """
select select
"Journal Entry" as reference_type, t1.name as reference_name, "Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row, t1.posting_date, t1.remark as remarks, t2.name as reference_row,
{dr_or_cr} as amount, t2.is_advance, {dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
t2.account_currency as currency t2.account_currency as currency
from from
`tabJournal Entry` t1, `tabJournal Entry Account` t2 `tabJournal Entry` t1, `tabJournal Entry Account` t2
@ -215,26 +215,26 @@ class PaymentReconciliation(Document):
inv.currency = entry.get("currency") inv.currency = entry.get("currency")
inv.outstanding_amount = flt(entry.get("outstanding_amount")) inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, allocated_entry): def get_difference_amount(self, payment_entry, invoice, allocated_amount):
if allocated_entry.get("reference_type") != "Payment Entry": difference_amount = 0
return if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
dr_or_cr = ( return difference_amount
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
row = self.get_payment_details(allocated_entry, dr_or_cr)
doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
update_reference_in_payment_entry(row, doc, do_not_save=True)
return doc.difference_amount
@frappe.whitelist() @frappe.whitelist()
def allocate_entries(self, args): def allocate_entries(self, args):
self.validate_entries() self.validate_entries()
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"))
default_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account"
)
entries = [] entries = []
for pay in args.get("payments"): for pay in args.get("payments"):
pay.update({"unreconciled_amount": pay.get("amount")}) pay.update({"unreconciled_amount": pay.get("amount")})
@ -248,7 +248,10 @@ class PaymentReconciliation(Document):
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount")) inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
pay["amount"] = 0 pay["amount"] = 0
res.difference_amount = self.get_difference_amount(res) inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate")
if pay.get("amount") == 0: if pay.get("amount") == 0:
entries.append(res) entries.append(res)
@ -278,6 +281,7 @@ class PaymentReconciliation(Document):
"amount": pay.get("amount"), "amount": pay.get("amount"),
"allocated_amount": allocated_amount, "allocated_amount": allocated_amount,
"difference_amount": pay.get("difference_amount"), "difference_amount": pay.get("difference_amount"),
"currency": inv.get("currency"),
} }
) )
@ -300,7 +304,11 @@ class PaymentReconciliation(Document):
else: else:
reconciled_entry = entry_list reconciled_entry = entry_list
reconciled_entry.append(self.get_payment_details(row, dr_or_cr)) payment_details = self.get_payment_details(row, dr_or_cr)
reconciled_entry.append(payment_details)
if payment_details.difference_amount:
self.make_difference_entry(payment_details)
if entry_list: if entry_list:
reconcile_against_document(entry_list) reconcile_against_document(entry_list)
@ -311,6 +319,56 @@ class PaymentReconciliation(Document):
msgprint(_("Successfully Reconciled")) msgprint(_("Successfully Reconciled"))
self.get_unreconciled_entries() self.get_unreconciled_entries()
def make_difference_entry(self, row):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = self.company
journal_entry.posting_date = nowdate()
journal_entry.multi_currency = 1
party_account_currency = frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
)
difference_account_currency = frappe.get_cached_value(
"Account", row.difference_account, "account_currency"
)
# Account Currency has balance
dr_or_cr = "debit" if self.party_type == "Customer" else "debit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account = frappe._dict(
{
"account": self.receivable_payable_account,
"party_type": self.party_type,
"party": self.party,
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"reference_type": row.against_voucher_type,
"reference_name": row.against_voucher,
dr_or_cr: flt(row.difference_amount),
dr_or_cr + "_in_account_currency": 0,
}
)
journal_entry.append("accounts", journal_account)
journal_account = frappe._dict(
{
"account": row.difference_account,
"account_currency": difference_account_currency,
"exchange_rate": 1,
"cost_center": erpnext.get_default_cost_center(self.company),
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
}
)
journal_entry.append("accounts", journal_account)
journal_entry.save()
journal_entry.submit()
def get_payment_details(self, row, dr_or_cr): def get_payment_details(self, row, dr_or_cr):
return frappe._dict( return frappe._dict(
{ {
@ -320,6 +378,7 @@ class PaymentReconciliation(Document):
"against_voucher_type": row.get("invoice_type"), "against_voucher_type": row.get("invoice_type"),
"against_voucher": row.get("invoice_number"), "against_voucher": row.get("invoice_number"),
"account": self.receivable_payable_account, "account": self.receivable_payable_account,
"exchange_rate": row.get("exchange_rate"),
"party_type": self.party_type, "party_type": self.party_type,
"party": self.party, "party": self.party,
"is_advance": row.get("is_advance"), "is_advance": row.get("is_advance"),
@ -344,6 +403,41 @@ class PaymentReconciliation(Document):
if not self.get("payments"): if not self.get("payments"):
frappe.throw(_("No records found in the Payments table")) frappe.throw(_("No records found in the Payments table"))
def get_invoice_exchange_map(self, invoices):
sales_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice"
]
purchase_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice"
]
invoice_exchange_map = frappe._dict()
if sales_invoices:
sales_invoice_map = frappe._dict(
frappe.db.get_all(
"Sales Invoice",
filters={"name": ("in", sales_invoices)},
fields=["name", "conversion_rate"],
as_list=1,
)
)
invoice_exchange_map.update(sales_invoice_map)
if purchase_invoices:
purchase_invoice_map = frappe._dict(
frappe.db.get_all(
"Purchase Invoice",
filters={"name": ("in", purchase_invoices)},
fields=["name", "conversion_rate"],
as_list=1,
)
)
invoice_exchange_map.update(purchase_invoice_map)
return invoice_exchange_map
def validate_allocation(self): def validate_allocation(self):
unreconciled_invoices = frappe._dict() unreconciled_invoices = frappe._dict()

View File

@ -6,7 +6,7 @@ import unittest
import frappe import frappe
from frappe import qb from frappe import qb
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate from frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@ -75,33 +75,11 @@ class TestPaymentReconciliation(FrappeTestCase):
self.item = item if isinstance(item, str) else item.item_code self.item = item if isinstance(item, str) else item.item_code
def create_customer(self): def create_customer(self):
if frappe.db.exists("Customer", "_Test PR Customer"): self.customer = make_customer("_Test PR Customer")
self.customer = "_Test PR Customer" self.customer2 = make_customer("_Test PR Customer 2")
else: self.customer3 = make_customer("_Test PR Customer 3", "EUR")
customer = frappe.new_doc("Customer") self.customer4 = make_customer("_Test PR Customer 4", "EUR")
customer.customer_name = "_Test PR Customer" self.customer5 = make_customer("_Test PR Customer 5", "EUR")
customer.type = "Individual"
customer.save()
self.customer = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 2"):
self.customer2 = "_Test PR Customer 2"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 2"
customer.type = "Individual"
customer.save()
self.customer2 = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 3"):
self.customer3 = "_Test PR Customer 3"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 3"
customer.type = "Individual"
customer.default_currency = "EUR"
customer.save()
self.customer3 = customer.name
def create_account(self): def create_account(self):
account_name = "Debtors EUR" account_name = "Debtors EUR"
@ -598,6 +576,156 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pr.payments[0].amount, amount) self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR") self.assertEqual(pr.payments[0].currency, "EUR")
def test_difference_amount_via_journal_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = 0
je1.accounts[0].credit = 0
je1.accounts[0].debit_in_account_currency = 8000
je1.accounts[0].debit = 8000
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = 0
je2.accounts[0].credit = 0
je2.accounts[0].debit_in_account_currency = 16000
je2.accounts[0].debit = 16000
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je2.save()
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
# Test exact payment allocation
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Test partial payment allocation (with excess payment entry)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Check if difference journal entry gets generated for difference amount after reconciliation
pr.reconcile()
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(debit) as amount",
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), -500)
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer5
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Payment Entry
pe1 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
paid_to=self.bank,
paid_amount=100,
)
pe1.source_exchange_rate = 80
pe1.received_amount = 8000
pe1.save()
pe1.submit()
pe2 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
paid_to=self.bank,
paid_amount=200,
)
pe2.source_exchange_rate = 80
pe2.received_amount = 16000
pe2.save()
pe2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer5
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
def test_differing_cost_center_on_invoice_and_payment(self): def test_differing_cost_center_on_invoice_and_payment(self):
""" """
Cost Center filter should not affect outstanding amount calculation Cost Center filter should not affect outstanding amount calculation
@ -618,3 +746,17 @@ class TestPaymentReconciliation(FrappeTestCase):
# check PR tool output # check PR tool output
self.assertEqual(len(pr.get("invoices")), 0) self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0) self.assertEqual(len(pr.get("payments")), 0)
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
if currency:
customer.default_currency = currency
customer.save()
return customer.name
else:
return customer_name

View File

@ -20,7 +20,9 @@
"section_break_5", "section_break_5",
"difference_amount", "difference_amount",
"column_break_7", "column_break_7",
"difference_account" "difference_account",
"exchange_rate",
"currency"
], ],
"fields": [ "fields": [
{ {
@ -37,7 +39,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Allocated Amount", "label": "Allocated Amount",
"options": "Currency", "options": "currency",
"reqd": 1 "reqd": 1
}, },
{ {
@ -112,7 +114,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"label": "Unreconciled Amount", "label": "Unreconciled Amount",
"options": "Currency", "options": "currency",
"read_only": 1 "read_only": 1
}, },
{ {
@ -120,7 +122,7 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"label": "Amount", "label": "Amount",
"options": "Currency", "options": "currency",
"read_only": 1 "read_only": 1
}, },
{ {
@ -129,11 +131,24 @@
"hidden": 1, "hidden": 1,
"label": "Reference Row", "label": "Reference Row",
"read_only": 1 "read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-10-06 11:48:59.616562", "modified": "2022-12-24 21:01:14.882747",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Allocation", "name": "Payment Reconciliation Allocation",
@ -141,5 +156,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -11,7 +11,8 @@
"col_break1", "col_break1",
"amount", "amount",
"outstanding_amount", "outstanding_amount",
"currency" "currency",
"exchange_rate"
], ],
"fields": [ "fields": [
{ {
@ -62,11 +63,17 @@
"hidden": 1, "hidden": 1,
"label": "Currency", "label": "Currency",
"options": "Currency" "options": "Currency"
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "Exchange Rate"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-24 22:42:40.923179", "modified": "2022-11-08 18:18:02.502149",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Invoice", "name": "Payment Reconciliation Invoice",
@ -75,5 +82,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -15,7 +15,8 @@
"difference_amount", "difference_amount",
"sec_break1", "sec_break1",
"remark", "remark",
"currency" "currency",
"exchange_rate"
], ],
"fields": [ "fields": [
{ {
@ -91,11 +92,17 @@
"label": "Difference Amount", "label": "Difference Amount",
"options": "currency", "options": "currency",
"read_only": 1 "read_only": 1
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "Exchange Rate"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-30 10:51:48.140062", "modified": "2022-11-08 18:18:36.268760",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Payment", "name": "Payment Reconciliation Payment",
@ -103,5 +110,6 @@
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -32,6 +32,10 @@
"iban", "iban",
"branch_code", "branch_code",
"swift_number", "swift_number",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"recipient_and_message", "recipient_and_message",
"print_format", "print_format",
"email_to", "email_to",
@ -362,13 +366,35 @@
"label": "Payment Channel", "label": "Payment Channel",
"options": "\nEmail\nPhone", "options": "\nEmail\nPhone",
"read_only": 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"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-09-30 16:19:43.680025", "modified": "2022-12-21 16:56:40.115737",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Request", "name": "Payment Request",

View File

@ -10,6 +10,9 @@ from frappe.model.document import Document
from frappe.utils import flt, get_url, nowdate from frappe.utils import flt, get_url, nowdate
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.payment_entry.payment_entry import ( from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_company_defaults, get_company_defaults,
get_payment_entry, get_payment_entry,
@ -270,6 +273,17 @@ class PaymentRequest(Document):
} }
) )
# Update dimensions
payment_entry.update(
{
"cost_center": self.get("cost_center"),
"project": self.get("project"),
}
)
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
if payment_entry.difference_amount: if payment_entry.difference_amount:
company_details = get_company_defaults(ref_doc.company) company_details = get_company_defaults(ref_doc.company)
@ -449,6 +463,17 @@ def make_payment_request(**args):
} }
) )
# Update dimensions
pr.update(
{
"cost_center": ref_doc.get("cost_center"),
"project": ref_doc.get("project"),
}
)
for dimension in get_accounting_dimensions():
pr.update({dimension: ref_doc.get(dimension)})
if args.order_type == "Shopping Cart" or args.mute_email: if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True pr.flags.mute_email = True

View File

@ -252,10 +252,15 @@ def get_other_conditions(conditions, values, args):
if args.get("doctype") in [ if args.get("doctype") in [
"Quotation", "Quotation",
"Quotation Item",
"Sales Order", "Sales Order",
"Sales Order Item",
"Delivery Note", "Delivery Note",
"Delivery Note Item",
"Sales Invoice", "Sales Invoice",
"Sales Invoice Item",
"POS Invoice", "POS Invoice",
"POS Invoice Item",
]: ]:
conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1""" conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1"""
else: else:

View File

@ -1185,11 +1185,24 @@ class SalesInvoice(SellingController):
if asset.calculate_depreciation: if asset.calculate_depreciation:
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
reverse_depreciation_entry_made_after_disposal(asset, posting_date) reverse_depreciation_entry_made_after_disposal(asset, posting_date)
reset_depreciation_schedule(asset, self.posting_date) notes = _(
"This schedule was created when Asset {0} was returned after being sold through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
reset_depreciation_schedule(asset, self.posting_date, notes)
asset.reload()
else: else:
if asset.calculate_depreciation: if asset.calculate_depreciation:
depreciate_asset(asset, self.posting_date) notes = _(
"This schedule was created when Asset {0} was sold through Sales Invoice {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.doctype, self.get("name")),
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload() asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(

View File

@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
from erpnext.accounts.utils import PaymentEntryUnlinkError from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import update_invoice_status from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
@ -2774,7 +2777,7 @@ class TestSalesInvoice(unittest.TestCase):
["2021-09-30", 5041.1, 26407.22], ["2021-09-30", 5041.1, 26407.22],
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@ -2805,7 +2808,7 @@ class TestSalesInvoice(unittest.TestCase):
expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]] expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@ -2834,7 +2837,7 @@ class TestSalesInvoice(unittest.TestCase):
["2025-06-06", 18633.88, 100000.0, False], ["2025-06-06", 18633.88, 100000.0, False],
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)

View File

@ -890,7 +890,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-02 12:53:12.693217", "modified": "2022-12-28 16:17:33.484531",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -199,7 +199,14 @@ def merge_similar_entries(gl_map, precision=None):
# filter zero debit and credit entries # filter zero debit and credit entries
merged_gl_map = filter( merged_gl_map = filter(
lambda x: flt(x.debit, precision) != 0 or flt(x.credit, precision) != 0, merged_gl_map lambda x: flt(x.debit, precision) != 0
or flt(x.credit, precision) != 0
or (
x.voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", x.voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
),
merged_gl_map,
) )
merged_gl_map = list(merged_gl_map) merged_gl_map = list(merged_gl_map)
@ -350,7 +357,13 @@ def process_debit_credit_difference(gl_map):
allowance = get_debit_credit_allowance(voucher_type, precision) allowance = get_debit_credit_allowance(voucher_type, precision)
debit_credit_diff = get_debit_credit_difference(gl_map, precision) debit_credit_diff = get_debit_credit_difference(gl_map, precision)
if abs(debit_credit_diff) > allowance: if abs(debit_credit_diff) > allowance:
if not (
voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
):
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
elif abs(debit_credit_diff) >= (1.0 / (10**precision)): elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
@ -358,6 +371,11 @@ def process_debit_credit_difference(gl_map):
debit_credit_diff = get_debit_credit_difference(gl_map, precision) debit_credit_diff = get_debit_credit_difference(gl_map, precision)
if abs(debit_credit_diff) > allowance: if abs(debit_credit_diff) > allowance:
if not (
voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
):
raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no) raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)

View File

@ -810,7 +810,7 @@ class ReceivablePayableReport(object):
self.ple.party.isin( self.ple.party.isin(
qb.from_(self.customer) qb.from_(self.customer)
.select(self.customer.name) .select(self.customer.name)
.where(self.customer.default_sales_partner == self.filters.get("payment_terms_template")) .where(self.customer.default_sales_partner == self.filters.get("sales_partner"))
) )
) )
@ -869,10 +869,15 @@ class ReceivablePayableReport(object):
def get_party_details(self, party): def get_party_details(self, party):
if not party in self.party_details: if not party in self.party_details:
if self.party_type == "Customer": if self.party_type == "Customer":
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
if self.filters.get("sales_partner"):
fields.append("default_sales_partner")
self.party_details[party] = frappe.db.get_value( self.party_details[party] = frappe.db.get_value(
"Customer", "Customer",
party, party,
["customer_name", "territory", "customer_group", "customer_primary_contact"], fields,
as_dict=True, as_dict=True,
) )
else: else:
@ -973,6 +978,9 @@ class ReceivablePayableReport(object):
if self.filters.show_sales_person: if self.filters.show_sales_person:
self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data")
if self.filters.sales_partner:
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
if self.filters.party_type == "Supplier": if self.filters.party_type == "Supplier":
self.add_column( self.add_column(
label=_("Supplier Group"), label=_("Supplier Group"),

View File

@ -184,11 +184,9 @@ class TestAccountsReceivable(FrappeTestCase):
err = err.save().submit() err = err.save().submit()
# Submit JV for ERR # Submit JV for ERR
jv = frappe.get_doc(err.make_jv_entry()) err_journals = err.make_jv_entries()
jv = jv.save() je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
for x in jv.accounts: je = je.submit()
x.cost_center = get_default_cost_center(jv.company)
jv.submit()
filters = { filters = {
"company": company, "company": company,
@ -201,7 +199,7 @@ class TestAccountsReceivable(FrappeTestCase):
report = execute(filters) report = execute(filters)
expected_data_for_err = [0, -5, 0, 5] expected_data_for_err = [0, -5, 0, 5]
row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0] row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
self.assertEqual( self.assertEqual(
expected_data_for_err, expected_data_for_err,
[ [

View File

@ -121,6 +121,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
if row.sales_person: if row.sales_person:
self.party_total[row.party].sales_person.append(row.sales_person) self.party_total[row.party].sales_person.append(row.sales_person)
if self.filters.sales_partner:
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner")
def get_columns(self): def get_columns(self):
self.columns = [] self.columns = []
self.add_column( self.add_column(
@ -160,6 +163,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
) )
if self.filters.show_sales_person: if self.filters.show_sales_person:
self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data")
if self.filters.sales_partner:
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
else: else:
self.add_column( self.add_column(
label=_("Supplier Group"), label=_("Supplier Group"),

View File

@ -131,8 +131,8 @@ def get_assets(filters):
else else
0 0
end), 0) as depreciation_amount_during_the_period end), 0) as depreciation_amount_during_the_period
from `tabAsset` a, `tabDepreciation Schedule` ds from `tabAsset` a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and a.name = ds.parent and ifnull(ds.journal_entry, '') != '' where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != ''
group by a.asset_category group by a.asset_category
union union
SELECT a.asset_category, SELECT a.asset_category,

View File

@ -26,6 +26,7 @@ class PartyLedgerSummaryReport(object):
) )
self.get_gl_entries() self.get_gl_entries()
self.get_additional_columns()
self.get_return_invoices() self.get_return_invoices()
self.get_party_adjustment_amounts() self.get_party_adjustment_amounts()
@ -33,6 +34,42 @@ class PartyLedgerSummaryReport(object):
data = self.get_data() data = self.get_data()
return columns, data return columns, data
def get_additional_columns(self):
"""
Additional Columns for 'User Permission' based access control
"""
from frappe import qb
if self.filters.party_type == "Customer":
self.territories = frappe._dict({})
self.customer_group = frappe._dict({})
customer = qb.DocType("Customer")
result = (
frappe.qb.from_(customer)
.select(
customer.name, customer.territory, customer.customer_group, customer.default_sales_partner
)
.where((customer.disabled == 0))
.run(as_dict=True)
)
for x in result:
self.territories[x.name] = x.territory
self.customer_group[x.name] = x.customer_group
else:
self.supplier_group = frappe._dict({})
supplier = qb.DocType("Supplier")
result = (
frappe.qb.from_(supplier)
.select(supplier.name, supplier.supplier_group)
.where((supplier.disabled == 0))
.run(as_dict=True)
)
for x in result:
self.supplier_group[x.name] = x.supplier_group
def get_columns(self): def get_columns(self):
columns = [ columns = [
{ {
@ -116,6 +153,35 @@ class PartyLedgerSummaryReport(object):
}, },
] ]
# Hidden columns for handling 'User Permissions'
if self.filters.party_type == "Customer":
columns += [
{
"label": _("Territory"),
"fieldname": "territory",
"fieldtype": "Link",
"options": "Territory",
"hidden": 1,
},
{
"label": _("Customer Group"),
"fieldname": "customer_group",
"fieldtype": "Link",
"options": "Customer Group",
"hidden": 1,
},
]
else:
columns += [
{
"label": _("Supplier Group"),
"fieldname": "supplier_group",
"fieldtype": "Link",
"options": "Supplier Group",
"hidden": 1,
}
]
return columns return columns
def get_data(self): def get_data(self):
@ -143,6 +209,12 @@ class PartyLedgerSummaryReport(object):
), ),
) )
if self.filters.party_type == "Customer":
self.party_data[gle.party].update({"territory": self.territories.get(gle.party)})
self.party_data[gle.party].update({"customer_group": self.customer_group.get(gle.party)})
else:
self.party_data[gle.party].update({"supplier_group": self.supplier_group.get(gle.party)})
amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr)
self.party_data[gle.party].closing_balance += amount self.party_data[gle.party].closing_balance += amount

View File

@ -239,7 +239,7 @@ def get_conditions(filters):
): ):
conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')")
conditions.append("(posting_date <=%(to_date)s)") conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')")
if filters.get("project"): if filters.get("project"):
conditions.append("project in %(project)s") conditions.append("project in %(project)s")

View File

@ -109,8 +109,7 @@ class TestGeneralLedger(FrappeTestCase):
frappe.db.set_value( frappe.db.set_value(
"Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
) )
revaluation_jv = revaluation.make_jv_entry() revaluation_jv = revaluation.make_jv_for_revaluation()
revaluation_jv = frappe.get_doc(revaluation_jv)
revaluation_jv.cost_center = "_Test Cost Center - _TC" revaluation_jv.cost_center = "_Test Cost Center - _TC"
for acc in revaluation_jv.get("accounts"): for acc in revaluation_jv.get("accounts"):
acc.cost_center = "_Test Cost Center - _TC" acc.cost_center = "_Test Cost Center - _TC"

View File

@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
item_details = get_item_details() item_details = get_item_details()
for d in item_list: for d in item_list:
if not d.stock_qty:
continue
item_record = item_details.get(d.item_code) item_record = item_details.get(d.item_code)
purchase_receipt = None purchase_receipt = None
@ -94,7 +91,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
"expense_account": expense_account, "expense_account": expense_account,
"stock_qty": d.stock_qty, "stock_qty": d.stock_qty,
"stock_uom": d.stock_uom, "stock_uom": d.stock_uom,
"rate": d.base_net_amount / d.stock_qty, "rate": d.base_net_amount / d.stock_qty if d.stock_qty else d.base_net_amount,
"amount": d.base_net_amount, "amount": d.base_net_amount,
} }
) )

View File

@ -63,24 +63,6 @@ frappe.query_reports["Supplier Ledger Summary"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Payment Terms Template" "options": "Payment Terms Template"
}, },
{
"fieldname":"territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory"
},
{
"fieldname":"sales_partner",
"label": __("Sales Partner"),
"fieldtype": "Link",
"options": "Sales Partner"
},
{
"fieldname":"sales_person",
"label": __("Sales Person"),
"fieldtype": "Link",
"options": "Sales Person"
},
{ {
"fieldname":"tax_id", "fieldname":"tax_id",
"label": __("Tax Id"), "label": __("Tax Id"),

View File

@ -101,10 +101,7 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
account_currency = entry["account_currency"] account_currency = entry["account_currency"]
if len(account_currencies) == 1 and account_currency == presentation_currency: if len(account_currencies) == 1 and account_currency == presentation_currency:
if debit_in_account_currency:
entry["debit"] = debit_in_account_currency entry["debit"] = debit_in_account_currency
if credit_in_account_currency:
entry["credit"] = credit_in_account_currency entry["credit"] = credit_in_account_currency
else: else:
date = currency_info["report_date"] date = currency_info["report_date"]

View File

@ -3,11 +3,14 @@ import unittest
import frappe import frappe
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.party import get_party_shipping_address from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import ( from erpnext.accounts.utils import (
get_future_stock_vouchers, get_future_stock_vouchers,
get_voucherwise_gl_entries, get_voucherwise_gl_entries,
sort_stock_vouchers_by_posting_date, sort_stock_vouchers_by_posting_date,
update_reference_in_payment_entry,
) )
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@ -73,6 +76,47 @@ class TestUtils(unittest.TestCase):
sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers))) sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers)))
self.assertEqual(sorted_vouchers, vouchers) self.assertEqual(sorted_vouchers, vouchers)
def test_update_reference_in_payment_entry(self):
item = make_item().name
purchase_invoice = make_purchase_invoice(
item=item, supplier="_Test Supplier USD", currency="USD", conversion_rate=82.32
)
purchase_invoice.submit()
payment_entry = get_payment_entry(purchase_invoice.doctype, purchase_invoice.name)
payment_entry.target_exchange_rate = 62.9
payment_entry.paid_amount = 15725
payment_entry.deductions = []
payment_entry.insert()
self.assertEqual(payment_entry.difference_amount, -4855.00)
payment_entry.references = []
payment_entry.submit()
payment_reconciliation = frappe.new_doc("Payment Reconciliation")
payment_reconciliation.company = payment_entry.company
payment_reconciliation.party_type = "Supplier"
payment_reconciliation.party = purchase_invoice.supplier
payment_reconciliation.receivable_payable_account = payment_entry.paid_to
payment_reconciliation.get_unreconciled_entries()
payment_reconciliation.allocate_entries(
{
"payments": [d.__dict__ for d in payment_reconciliation.payments],
"invoices": [d.__dict__ for d in payment_reconciliation.invoices],
}
)
for d in payment_reconciliation.invoices:
# Reset invoice outstanding_amount because allocate_entries will zero this value out.
d.outstanding_amount = d.amount
for d in payment_reconciliation.allocation:
d.difference_account = "Exchange Gain/Loss - _TC"
payment_reconciliation.reconcile()
payment_entry.load_from_db()
self.assertEqual(len(payment_entry.references), 1)
self.assertEqual(payment_entry.difference_amount, 0)
ADDRESS_RECORDS = [ ADDRESS_RECORDS = [
{ {

View File

@ -611,11 +611,6 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
new_row.docstatus = 1 new_row.docstatus = 1
new_row.update(reference_details) new_row.update(reference_details)
payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_amounts()
if d.difference_amount and d.difference_account: if d.difference_amount and d.difference_account:
account_details = { account_details = {
"account": d.difference_account, "account": d.difference_account,
@ -627,6 +622,11 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
payment_entry.set_gain_or_loss(account_details=account_details) payment_entry.set_gain_or_loss(account_details=account_details)
payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_amounts()
if not do_not_save: if not do_not_save:
payment_entry.save(ignore_permissions=True) payment_entry.save(ignore_permissions=True)

View File

@ -76,7 +76,6 @@ frappe.ui.form.on('Asset', {
refresh: function(frm) { refresh: function(frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset"); frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
frm.events.make_schedules_editable(frm);
if (frm.doc.docstatus==1) { if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
@ -188,7 +187,11 @@ frappe.ui.form.on('Asset', {
}) })
}, },
setup_chart: function(frm) { setup_chart: async function(frm) {
if(frm.doc.finance_books.length > 1) {
return
}
var x_intervals = [frm.doc.purchase_date]; var x_intervals = [frm.doc.purchase_date];
var asset_values = [frm.doc.gross_purchase_amount]; var asset_values = [frm.doc.gross_purchase_amount];
var last_depreciation_date = frm.doc.purchase_date; var last_depreciation_date = frm.doc.purchase_date;
@ -202,7 +205,20 @@ frappe.ui.form.on('Asset', {
flt(frm.doc.opening_accumulated_depreciation)); flt(frm.doc.opening_accumulated_depreciation));
} }
$.each(frm.doc.schedules || [], function(i, v) { let depr_schedule = [];
if (frm.doc.finance_books.length == 1) {
depr_schedule = (await frappe.call(
"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
{
asset_name: frm.doc.name,
status: frm.doc.docstatus ? "Active" : "Draft",
finance_book: frm.doc.finance_books[0].finance_book || null
}
)).message;
}
$.each(depr_schedule || [], function(i, v) {
x_intervals.push(v.schedule_date); x_intervals.push(v.schedule_date);
var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount); var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount);
if(v.journal_entry) { if(v.journal_entry) {
@ -266,21 +282,6 @@ frappe.ui.form.on('Asset', {
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
}, },
opening_accumulated_depreciation: function(frm) {
erpnext.asset.set_accumulated_depreciation(frm);
},
make_schedules_editable: function(frm) {
if (frm.doc.finance_books) {
var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0
? true : false;
frm.toggle_enable("schedules", is_editable);
frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable);
frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable);
}
},
make_sales_invoice: function(frm) { make_sales_invoice: function(frm) {
frappe.call({ frappe.call({
args: { args: {
@ -476,7 +477,6 @@ frappe.ui.form.on('Asset Finance Book', {
depreciation_method: function(frm, cdt, cdn) { depreciation_method: function(frm, cdt, cdn) {
const row = locals[cdt][cdn]; const row = locals[cdt][cdn];
frm.events.set_depreciation_rate(frm, row); frm.events.set_depreciation_rate(frm, row);
frm.events.make_schedules_editable(frm);
}, },
expected_value_after_useful_life: function(frm, cdt, cdn) { expected_value_after_useful_life: function(frm, cdt, cdn) {
@ -512,41 +512,6 @@ frappe.ui.form.on('Asset Finance Book', {
} }
}); });
frappe.ui.form.on('Depreciation Schedule', {
make_depreciation_entry: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
if (!row.journal_entry) {
frappe.call({
method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry",
args: {
"asset_name": frm.doc.name,
"date": row.schedule_date
},
callback: function(r) {
frappe.model.sync(r.message);
frm.refresh();
}
})
}
},
depreciation_amount: function(frm, cdt, cdn) {
erpnext.asset.set_accumulated_depreciation(frm);
}
})
erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name,
"accumulated_depreciation_amount", accumulated_depreciation);
})
};
erpnext.asset.scrap_asset = function(frm) { erpnext.asset.scrap_asset = function(frm) {
frappe.confirm(__("Do you really want to scrap this asset?"), function () { frappe.confirm(__("Do you really want to scrap this asset?"), function () {
frappe.call({ frappe.call({

View File

@ -52,8 +52,6 @@
"column_break_24", "column_break_24",
"frequency_of_depreciation", "frequency_of_depreciation",
"next_depreciation_date", "next_depreciation_date",
"section_break_14",
"schedules",
"insurance_details", "insurance_details",
"policy_number", "policy_number",
"insurer", "insurer",
@ -307,19 +305,6 @@
"label": "Next Depreciation Date", "label": "Next Depreciation Date",
"no_copy": 1 "no_copy": 1
}, },
{
"depends_on": "calculate_depreciation",
"fieldname": "section_break_14",
"fieldtype": "Section Break",
"label": "Depreciation Schedule"
},
{
"fieldname": "schedules",
"fieldtype": "Table",
"label": "Depreciation Schedule",
"no_copy": 1,
"options": "Depreciation Schedule"
},
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "insurance_details", "fieldname": "insurance_details",
@ -508,9 +493,14 @@
"group": "Value", "group": "Value",
"link_doctype": "Asset Value Adjustment", "link_doctype": "Asset Value Adjustment",
"link_fieldname": "asset" "link_fieldname": "asset"
},
{
"group": "Depreciation",
"link_doctype": "Asset Depreciation Schedule",
"link_fieldname": "asset"
} }
], ],
"modified": "2022-07-20 10:15:12.887372", "modified": "2022-11-25 12:47:19.689702",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -8,14 +8,15 @@ import math
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import ( from frappe.utils import (
add_days,
add_months, add_months,
cint, cint,
date_diff, date_diff,
flt, flt,
get_datetime, get_datetime,
get_last_day, get_last_day,
get_link_to_form,
getdate, getdate,
is_last_day_of_the_month,
month_diff, month_diff,
nowdate, nowdate,
today, today,
@ -28,6 +29,16 @@ from erpnext.assets.doctype.asset.depreciation import (
get_disposal_account_and_cost_center, get_disposal_account_and_cost_center,
) )
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
cancel_asset_depr_schedules,
convert_draft_asset_depr_schedules_into_active,
get_asset_depr_schedule_doc,
get_depr_schedule,
make_draft_asset_depr_schedules,
make_draft_asset_depr_schedules_if_not_present,
set_draft_asset_depr_schedule_details,
update_draft_asset_depr_schedules,
)
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@ -40,8 +51,8 @@ class Asset(AccountsController):
self.set_missing_values() self.set_missing_values()
if not self.split_from: if not self.split_from:
self.prepare_depreciation_data() self.prepare_depreciation_data()
update_draft_asset_depr_schedules(self)
self.validate_gross_and_purchase_amount() self.validate_gross_and_purchase_amount()
if self.get("schedules"):
self.validate_expected_value_after_useful_life() self.validate_expected_value_after_useful_life()
self.status = self.get_status() self.status = self.get_status()
@ -52,16 +63,24 @@ class Asset(AccountsController):
self.make_asset_movement() self.make_asset_movement()
if not self.booked_fixed_asset and self.validate_make_gl_entry(): if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries() self.make_gl_entries()
if not self.split_from:
make_draft_asset_depr_schedules_if_not_present(self)
convert_draft_asset_depr_schedules_into_active(self)
def on_cancel(self): def on_cancel(self):
self.validate_cancellation() self.validate_cancellation()
self.cancel_movement_entries() self.cancel_movement_entries()
self.delete_depreciation_entries() self.delete_depreciation_entries()
cancel_asset_depr_schedules(self)
self.set_status() self.set_status()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name) make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
self.db_set("booked_fixed_asset", 0) self.db_set("booked_fixed_asset", 0)
def after_insert(self):
if not self.split_from:
make_draft_asset_depr_schedules(self)
def validate_asset_and_reference(self): def validate_asset_and_reference(self):
if self.purchase_invoice or self.purchase_receipt: if self.purchase_invoice or self.purchase_receipt:
reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt" reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt"
@ -79,12 +98,10 @@ class Asset(AccountsController):
_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name) _("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)
) )
def prepare_depreciation_data(self, date_of_disposal=None, date_of_return=None): def prepare_depreciation_data(self):
if self.calculate_depreciation: if self.calculate_depreciation:
self.value_after_depreciation = 0 self.value_after_depreciation = 0
self.set_depreciation_rate() self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_disposal)
self.set_accumulated_depreciation(date_of_disposal, date_of_return)
else: else:
self.finance_books = [] self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt( self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
@ -223,148 +240,6 @@ class Asset(AccountsController):
self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation") self.get_depreciation_rate(d, on_validate=True), d.precision("rate_of_depreciation")
) )
def make_depreciation_schedule(self, date_of_disposal):
if "Manual" not in [d.depreciation_method for d in self.finance_books] and not self.get(
"schedules"
):
self.schedules = []
if not self.available_for_use_date:
return
start = self.clear_depreciation_schedule()
for finance_book in self.get("finance_books"):
self._make_depreciation_schedule(finance_book, start, date_of_disposal)
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)
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - cint(
self.number_of_depreciations_booked
)
has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
)
if should_get_last_day:
schedule_date = get_last_day(schedule_date)
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
# if asset is being sold
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_disposal
)
if depreciation_amount > 0:
self._add_depreciation_row(
date_of_disposal,
depreciation_amount,
finance_book.depreciation_method,
finance_book.finance_book,
finance_book.idx,
)
break
# For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n == 0:
from_date = add_days(
self.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book, depreciation_amount, from_date, finance_book.depreciation_start_date
)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(finance_book.depreciation_start_date, -months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(
self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation),
)
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(
finance_book, depreciation_amount, schedule_date, self.to_date
)
depreciation_amount = self.get_adjusted_depreciation_amount(
depreciation_amount_without_pro_rata, depreciation_amount, finance_book.finance_book
)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount:
continue
value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and (
(
n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life
)
or value_after_depreciation < finance_book.expected_value_after_useful_life
):
depreciation_amount += value_after_depreciation - finance_book.expected_value_after_useful_life
skip_row = True
if depreciation_amount > 0:
self._add_depreciation_row(
schedule_date,
depreciation_amount,
finance_book.depreciation_method,
finance_book.finance_book,
finance_book.idx,
)
def _add_depreciation_row(
self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id
):
self.append(
"schedules",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": depreciation_method,
"finance_book": finance_book,
"finance_book_id": finance_book_id,
},
)
def _get_value_after_depreciation(self, finance_book): def _get_value_after_depreciation(self, finance_book):
# value_after_depreciation - current Asset value # value_after_depreciation - current Asset value
if self.docstatus == 1 and finance_book.value_after_depreciation: if self.docstatus == 1 and finance_book.value_after_depreciation:
@ -376,58 +251,6 @@ class Asset(AccountsController):
return value_after_depreciation return value_after_depreciation
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
def clear_depreciation_schedule(self):
start = []
num_of_depreciations_completed = 0
depr_schedule = []
for schedule in self.get("schedules"):
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to ensure that start will only be updated once for each FB
if len(start) == (int(schedule.finance_book_id) - 1):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
if len(start) == (len(self.finance_books) - 1):
start.append(num_of_depreciations_completed)
# when the Depreciation Schedule is being created for the first time
if start == []:
start = [0] * len(self.finance_books)
else:
self.schedules = depr_schedule
return start
def get_from_date(self, finance_book):
if not self.get("schedules"):
return self.available_for_use_date
if len(self.finance_books) == 1:
return self.schedules[-1].schedule_date
from_date = ""
for schedule in self.get("schedules"):
if schedule.finance_book == finance_book:
from_date = schedule.schedule_date
if from_date:
return from_date
# since depr for available_for_use_date is not yet booked
return add_days(self.available_for_use_date, -1)
# if it returns True, depreciation_amount will not be equal for the first and last rows # if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row): def check_is_pro_rata(self, row):
has_pro_rata = False has_pro_rata = False
@ -512,83 +335,15 @@ class Asset(AccountsController):
).format(row.idx) ).format(row.idx)
) )
# to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(
self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book
):
if not self.opening_accumulated_depreciation:
depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
if (
depreciation_amount_for_first_row + depreciation_amount_for_last_row
!= depreciation_amount_without_pro_rata
):
depreciation_amount_for_last_row = (
depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
)
return depreciation_amount_for_last_row
def get_depreciation_amount_for_first_row(self, finance_book):
if self.has_only_one_finance_book():
return self.schedules[0].depreciation_amount
else:
for schedule in self.schedules:
if schedule.finance_book == finance_book:
return schedule.depreciation_amount
def has_only_one_finance_book(self):
if len(self.finance_books) == 1:
return True
def set_accumulated_depreciation(
self, date_of_sale=None, date_of_return=None, ignore_booked_entry=False
):
straight_line_idx = [
d.idx for d in self.get("schedules") if d.depreciation_method == "Straight Line"
]
finance_books = []
for i, d in enumerate(self.get("schedules")):
if ignore_booked_entry and d.journal_entry:
continue
if int(d.finance_book_id) not in finance_books:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
finance_books.append(int(d.finance_book_id))
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line
if (
straight_line_idx
and i == max(straight_line_idx) - 1
and not date_of_sale
and not date_of_return
):
book = self.get("finance_books")[cint(d.finance_book_id) - 1]
depreciation_amount += flt(
value_after_depreciation - flt(book.expected_value_after_useful_life),
d.precision("depreciation_amount"),
)
d.depreciation_amount = depreciation_amount
accumulated_depreciation += d.depreciation_amount
d.accumulated_depreciation_amount = flt(
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
)
def get_value_after_depreciation(self, idx):
return flt(self.get("finance_books")[cint(idx) - 1].value_after_depreciation)
def validate_expected_value_after_useful_life(self): def validate_expected_value_after_useful_life(self):
for row in self.get("finance_books"): for row in self.get("finance_books"):
depr_schedule = get_depr_schedule(self.name, "Draft", row.finance_book)
if not depr_schedule:
continue
accumulated_depreciation_after_full_schedule = [ accumulated_depreciation_after_full_schedule = [
d.accumulated_depreciation_amount d.accumulated_depreciation_amount for d in depr_schedule
for d in self.get("schedules")
if cint(d.finance_book_id) == row.idx
] ]
if accumulated_depreciation_after_full_schedule: if accumulated_depreciation_after_full_schedule:
@ -637,7 +392,10 @@ class Asset(AccountsController):
movement.cancel() movement.cancel()
def delete_depreciation_entries(self): def delete_depreciation_entries(self):
for d in self.get("schedules"): for row in self.get("finance_books"):
depr_schedule = get_depr_schedule(self.name, "Active", row.finance_book)
for d in depr_schedule or []:
if d.journal_entry: if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel() frappe.get_doc("Journal Entry", d.journal_entry).cancel()
d.db_set("journal_entry", None) d.db_set("journal_entry", None)
@ -1072,32 +830,6 @@ def get_total_days(date, frequency):
return date_diff(date, period_start_date) return date_diff(date, period_start_date)
def is_last_day_of_the_month(date):
last_day_of_the_month = get_last_day(date)
return getdate(last_day_of_the_month) == getdate(date)
@erpnext.allow_regional
def get_depreciation_amount(asset, depreciable_value, row):
if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
depreciation_amount = (
flt(asset.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
return depreciation_amount
@frappe.whitelist() @frappe.whitelist()
def split_asset(asset_name, split_qty): def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
@ -1109,12 +841,12 @@ def split_asset(asset_name, split_qty):
remaining_qty = asset.asset_quantity - split_qty remaining_qty = asset.asset_quantity - split_qty
new_asset = create_new_asset_after_split(asset, split_qty) new_asset = create_new_asset_after_split(asset, split_qty)
update_existing_asset(asset, remaining_qty) update_existing_asset(asset, remaining_qty, new_asset.name)
return new_asset return new_asset
def update_existing_asset(asset, remaining_qty): def update_existing_asset(asset, remaining_qty, new_asset_name):
remaining_gross_purchase_amount = flt( remaining_gross_purchase_amount = flt(
(asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity (asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity
) )
@ -1132,34 +864,49 @@ def update_existing_asset(asset, remaining_qty):
}, },
) )
for finance_book in asset.get("finance_books"): for row in asset.get("finance_books"):
value_after_depreciation = flt( value_after_depreciation = flt(
(finance_book.value_after_depreciation * remaining_qty) / asset.asset_quantity (row.value_after_depreciation * remaining_qty) / asset.asset_quantity
) )
expected_value_after_useful_life = flt( expected_value_after_useful_life = flt(
(finance_book.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity (row.expected_value_after_useful_life * remaining_qty) / asset.asset_quantity
) )
frappe.db.set_value( frappe.db.set_value(
"Asset Finance Book", finance_book.name, "value_after_depreciation", value_after_depreciation "Asset Finance Book", row.name, "value_after_depreciation", value_after_depreciation
) )
frappe.db.set_value( frappe.db.set_value(
"Asset Finance Book", "Asset Finance Book",
finance_book.name, row.name,
"expected_value_after_useful_life", "expected_value_after_useful_life",
expected_value_after_useful_life, expected_value_after_useful_life,
) )
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
asset.name, "Active", row.finance_book
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, asset, row)
accumulated_depreciation = 0 accumulated_depreciation = 0
for term in asset.get("schedules"): for term in new_asset_depr_schedule_doc.get("depreciation_schedule"):
depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity) depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity)
frappe.db.set_value( term.depreciation_amount = depreciation_amount
"Depreciation Schedule", term.name, "depreciation_amount", depreciation_amount
)
accumulated_depreciation += depreciation_amount accumulated_depreciation += depreciation_amount
frappe.db.set_value( term.accumulated_depreciation_amount = accumulated_depreciation
"Depreciation Schedule", term.name, "accumulated_depreciation_amount", accumulated_depreciation
notes = _(
"This schedule was created when Asset {0} was updated after being split into new Asset {1}."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(asset.doctype, new_asset_name)
) )
new_asset_depr_schedule_doc.notes = notes
current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
current_asset_depr_schedule_doc.cancel()
new_asset_depr_schedule_doc.submit()
def create_new_asset_after_split(asset, split_qty): def create_new_asset_after_split(asset, split_qty):
@ -1173,26 +920,44 @@ def create_new_asset_after_split(asset, split_qty):
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name new_asset.split_from = asset.name
for row in new_asset.get("finance_books"):
row.value_after_depreciation = flt(
(row.value_after_depreciation * split_qty) / asset.asset_quantity
)
row.expected_value_after_useful_life = flt(
(row.expected_value_after_useful_life * split_qty) / asset.asset_quantity
)
new_asset.submit()
new_asset.set_status()
for row in new_asset.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
asset.name, "Active", row.finance_book
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, new_asset, row)
accumulated_depreciation = 0 accumulated_depreciation = 0
for finance_book in new_asset.get("finance_books"): for term in new_asset_depr_schedule_doc.get("depreciation_schedule"):
finance_book.value_after_depreciation = flt(
(finance_book.value_after_depreciation * split_qty) / asset.asset_quantity
)
finance_book.expected_value_after_useful_life = flt(
(finance_book.expected_value_after_useful_life * split_qty) / asset.asset_quantity
)
for term in new_asset.get("schedules"):
depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity) depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity)
term.depreciation_amount = depreciation_amount term.depreciation_amount = depreciation_amount
accumulated_depreciation += depreciation_amount accumulated_depreciation += depreciation_amount
term.accumulated_depreciation_amount = accumulated_depreciation term.accumulated_depreciation_amount = accumulated_depreciation
new_asset.submit() notes = _("This schedule was created when new Asset {0} was split from Asset {1}.").format(
new_asset.set_status() get_link_to_form(new_asset.doctype, new_asset.name), get_link_to_form(asset.doctype, asset.name)
)
new_asset_depr_schedule_doc.notes = notes
for term in new_asset.get("schedules"): new_asset_depr_schedule_doc.submit()
for row in new_asset.get("finance_books"):
depr_schedule = get_depr_schedule(new_asset.name, "Active", row.finance_book)
for term in depr_schedule:
# Update references in JV # Update references in JV
if term.journal_entry: if term.journal_entry:
add_reference_in_jv_on_split( add_reference_in_jv_on_split(

View File

@ -4,12 +4,18 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import add_months, cint, flt, getdate, nowdate, today from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, nowdate, today
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, get_checks_for_pl_and_bs_accounts,
) )
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
get_asset_depr_schedule_name,
get_temp_asset_depr_schedule_doc,
make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
def post_depreciation_entries(date=None, commit=True): def post_depreciation_entries(date=None, commit=True):
@ -21,8 +27,11 @@ def post_depreciation_entries(date=None, commit=True):
if not date: if not date:
date = today() date = today()
for asset in get_depreciable_assets(date): for asset_name in get_depreciable_assets(date):
make_depreciation_entry(asset, date) asset_doc = frappe.get_doc("Asset", asset_name)
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
if commit: if commit:
frappe.db.commit() frappe.db.commit()
@ -30,21 +39,35 @@ def post_depreciation_entries(date=None, commit=True):
def get_depreciable_assets(date): def get_depreciable_assets(date):
return frappe.db.sql_list( return frappe.db.sql_list(
"""select distinct a.name """select distinct a.name
from tabAsset a, `tabDepreciation Schedule` ds from tabAsset a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 where a.name = ads.asset and ads.name = ds.parent and a.docstatus=1 and ads.docstatus=1
and a.status in ('Submitted', 'Partially Depreciated') and a.status in ('Submitted', 'Partially Depreciated')
and a.calculate_depreciation = 1
and ds.schedule_date<=%s
and ifnull(ds.journal_entry, '')=''""", and ifnull(ds.journal_entry, '')=''""",
date, date,
) )
def make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date=None):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Active", row.finance_book
)
make_depreciation_entry(asset_depr_schedule_name, date)
@frappe.whitelist() @frappe.whitelist()
def make_depreciation_entry(asset_name, date=None): def make_depreciation_entry(asset_depr_schedule_name, date=None):
frappe.has_permission("Journal Entry", throw=True) frappe.has_permission("Journal Entry", throw=True)
if not date: if not date:
date = today() date = today()
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
asset_name = asset_depr_schedule_doc.asset
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
( (
fixed_asset_account, fixed_asset_account,
@ -60,14 +83,14 @@ def make_depreciation_entry(asset_name, date=None):
accounting_dimensions = get_checks_for_pl_and_bs_accounts() accounting_dimensions = get_checks_for_pl_and_bs_accounts()
for d in asset.get("schedules"): for d in asset_depr_schedule_doc.get("depreciation_schedule"):
if not d.journal_entry and getdate(d.schedule_date) <= getdate(date): if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
je = frappe.new_doc("Journal Entry") je = frappe.new_doc("Journal Entry")
je.voucher_type = "Depreciation Entry" je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series je.naming_series = depreciation_series
je.posting_date = d.schedule_date je.posting_date = d.schedule_date
je.company = asset.company je.company = asset.company
je.finance_book = d.finance_book je.finance_book = asset_depr_schedule_doc.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
credit_account, debit_account = get_credit_and_debit_accounts( credit_account, debit_account = get_credit_and_debit_accounts(
@ -118,14 +141,14 @@ def make_depreciation_entry(asset_name, date=None):
d.db_set("journal_entry", je.name) d.db_set("journal_entry", je.name)
idx = cint(d.finance_book_id) idx = cint(asset_depr_schedule_doc.finance_book_id)
finance_books = asset.get("finance_books")[idx - 1] row = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation -= d.depreciation_amount row.value_after_depreciation -= d.depreciation_amount
finance_books.db_update() row.db_update()
asset.set_status() asset.set_status()
return asset return asset_depr_schedule_doc
def get_depreciation_accounts(asset): def get_depreciation_accounts(asset):
@ -199,7 +222,11 @@ def scrap_asset(asset_name):
date = today() date = today()
depreciate_asset(asset, date) notes = _("This schedule was created when Asset {0} was scrapped.").format(
get_link_to_form(asset.doctype, asset.name)
)
depreciate_asset(asset, date, notes)
asset.reload() asset.reload()
depreciation_series = frappe.get_cached_value( depreciation_series = frappe.get_cached_value(
@ -232,10 +259,15 @@ def restore_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date) reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
reset_depreciation_schedule(asset, asset.disposal_date)
je = asset.journal_entry_for_scrap je = asset.journal_entry_for_scrap
notes = _("This schedule was created when Asset {0} was restored.").format(
get_link_to_form(asset.doctype, asset.name)
)
reset_depreciation_schedule(asset, asset.disposal_date, notes)
asset.db_set("disposal_date", None) asset.db_set("disposal_date", None)
asset.db_set("journal_entry_for_scrap", None) asset.db_set("journal_entry_for_scrap", None)
@ -244,22 +276,28 @@ def restore_asset(asset_name):
asset.set_status() asset.set_status()
def depreciate_asset(asset, date): def depreciate_asset(asset_doc, date, notes):
asset.flags.ignore_validate_update_after_submit = True asset_doc.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(date_of_disposal=date)
asset.save()
make_depreciation_entry(asset.name, date) make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset_doc, notes, date_of_disposal=date
)
asset_doc.save()
make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
def reset_depreciation_schedule(asset, date): def reset_depreciation_schedule(asset_doc, date, notes):
asset.flags.ignore_validate_update_after_submit = True asset_doc.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset.prepare_depreciation_data(date_of_return=date) asset_doc, notes, date_of_return=date
)
modify_depreciation_schedule_for_asset_repairs(asset) modify_depreciation_schedule_for_asset_repairs(asset_doc)
asset.save()
asset_doc.save()
def modify_depreciation_schedule_for_asset_repairs(asset): def modify_depreciation_schedule_for_asset_repairs(asset):
@ -271,22 +309,21 @@ def modify_depreciation_schedule_for_asset_repairs(asset):
if repair.increase_in_asset_life: if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name) asset_repair = frappe.get_doc("Asset Repair", repair.name)
asset_repair.modify_depreciation_schedule() asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data() notes = _("This schedule was created when Asset {0} went through Asset Repair {1}.").format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(asset_repair.doctype, asset_repair.name),
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset, notes)
def reverse_depreciation_entry_made_after_disposal(asset, date): def reverse_depreciation_entry_made_after_disposal(asset, date):
row = -1 for row in asset.get("finance_books"):
finance_book = asset.get("schedules")[0].get("finance_book") asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
for schedule in asset.get("schedules"):
if schedule.finance_book != finance_book:
row = 0
finance_book = schedule.finance_book
else:
row += 1
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if schedule.schedule_date == date: if schedule.schedule_date == date:
if not disposal_was_made_on_original_schedule_date( if not disposal_was_made_on_original_schedule_date(
asset, schedule, row, date schedule_idx, row, date
) or disposal_happens_in_the_future(date): ) or disposal_happens_in_the_future(date):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
@ -295,10 +332,12 @@ def reverse_depreciation_entry_made_after_disposal(asset, date):
reverse_journal_entry.submit() reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False frappe.flags.is_reverse_depr_entry = False
asset_depr_schedule_doc.flags.ignore_validate_update_after_submit = True
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None schedule.journal_entry = None
depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry) depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
asset.finance_books[0].value_after_depreciation += depreciation_amount row.value_after_depreciation += depreciation_amount
asset_depr_schedule_doc.save()
asset.save() asset.save()
@ -310,15 +349,14 @@ def get_depreciation_amount_in_je(journal_entry):
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal): def disposal_was_made_on_original_schedule_date(schedule_idx, 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( orginal_schedule_date = add_months(
finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation)
) )
if orginal_schedule_date == posting_date_of_disposal: if orginal_schedule_date == posting_date_of_disposal:
return True return True
return False return False
@ -499,24 +537,27 @@ def get_disposal_account_and_cost_center(company):
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None): def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
asset_doc = frappe.get_doc("Asset", asset) asset_doc = frappe.get_doc("Asset", asset)
if asset_doc.calculate_depreciation: if not asset_doc.calculate_depreciation:
asset_doc.prepare_depreciation_data(getdate(disposal_date)) return flt(asset_doc.value_after_depreciation)
finance_book_id = 1 idx = 1
if finance_book: if finance_book:
for fb in asset_doc.finance_books: for d in asset.finance_books:
if fb.finance_book == finance_book: if d.finance_book == finance_book:
finance_book_id = fb.idx idx = d.idx
break break
asset_schedules = [ row = asset_doc.finance_books[idx - 1]
sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id
] temp_asset_depreciation_schedule = get_temp_asset_depr_schedule_doc(
accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount asset_doc, row, getdate(disposal_date)
)
accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[
-1
].accumulated_depreciation_amount
return flt( return flt(
flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount, flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
asset_doc.precision("gross_purchase_amount"), asset_doc.precision("gross_purchase_amount"),
) )
else:
return flt(asset_doc.value_after_depreciation)

View File

@ -27,6 +27,11 @@ from erpnext.assets.doctype.asset.depreciation import (
restore_asset, restore_asset,
scrap_asset, scrap_asset,
) )
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
clear_depr_schedule,
get_asset_depr_schedule_doc,
get_depr_schedule,
)
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_invoice, make_purchase_invoice as make_invoice,
) )
@ -205,6 +210,9 @@ class TestAsset(AssetSetup):
submit=1, submit=1,
) )
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
post_depreciation_entries(date=add_months(purchase_date, 2)) post_depreciation_entries(date=add_months(purchase_date, 2))
asset.load_from_db() asset.load_from_db()
@ -216,6 +224,11 @@ class TestAsset(AssetSetup):
scrap_asset(asset.name) scrap_asset(asset.name)
asset.load_from_db() asset.load_from_db()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
accumulated_depr_amount = flt( accumulated_depr_amount = flt(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation, asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
@ -256,6 +269,11 @@ class TestAsset(AssetSetup):
self.assertSequenceEqual(gle, expected_gle) self.assertSequenceEqual(gle, expected_gle)
restore_asset(asset.name) restore_asset(asset.name)
second_asset_depr_schedule.load_from_db()
third_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(third_asset_depr_schedule.status, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Cancelled")
asset.load_from_db() asset.load_from_db()
self.assertFalse(asset.journal_entry_for_scrap) self.assertFalse(asset.journal_entry_for_scrap)
@ -283,6 +301,9 @@ class TestAsset(AssetSetup):
submit=1, submit=1,
) )
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
post_depreciation_entries(date=add_months(purchase_date, 2)) post_depreciation_entries(date=add_months(purchase_date, 2))
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
@ -294,6 +315,12 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
pro_rata_amount, _, _ = asset.get_pro_rata_amt( pro_rata_amount, _, _ = asset.get_pro_rata_amt(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
) )
@ -370,6 +397,9 @@ class TestAsset(AssetSetup):
submit=1, submit=1,
) )
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
post_depreciation_entries(date="2021-01-01") post_depreciation_entries(date="2021-01-01")
self.assertEqual(asset.asset_quantity, 10) self.assertEqual(asset.asset_quantity, 10)
@ -378,21 +408,31 @@ class TestAsset(AssetSetup):
new_asset = split_asset(asset.name, 2) new_asset = split_asset(asset.name, 2)
asset.load_from_db() asset.load_from_db()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
first_asset_depr_schedule_of_new_asset = get_asset_depr_schedule_doc(new_asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule_of_new_asset.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
depr_schedule_of_asset = second_asset_depr_schedule.get("depreciation_schedule")
depr_schedule_of_new_asset = first_asset_depr_schedule_of_new_asset.get("depreciation_schedule")
self.assertEqual(new_asset.asset_quantity, 2) self.assertEqual(new_asset.asset_quantity, 2)
self.assertEqual(new_asset.gross_purchase_amount, 24000) self.assertEqual(new_asset.gross_purchase_amount, 24000)
self.assertEqual(new_asset.opening_accumulated_depreciation, 4000) self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
self.assertEqual(new_asset.split_from, asset.name) self.assertEqual(new_asset.split_from, asset.name)
self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000) self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000)
self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000) self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000)
self.assertEqual(asset.asset_quantity, 8) self.assertEqual(asset.asset_quantity, 8)
self.assertEqual(asset.gross_purchase_amount, 96000) self.assertEqual(asset.gross_purchase_amount, 96000)
self.assertEqual(asset.opening_accumulated_depreciation, 16000) self.assertEqual(asset.opening_accumulated_depreciation, 16000)
self.assertEqual(asset.schedules[0].depreciation_amount, 16000) self.assertEqual(depr_schedule_of_asset[0].depreciation_amount, 16000)
self.assertEqual(asset.schedules[1].depreciation_amount, 16000) self.assertEqual(depr_schedule_of_asset[1].depreciation_amount, 16000)
journal_entry = asset.schedules[0].journal_entry journal_entry = depr_schedule_of_asset[0].journal_entry
jv = frappe.get_doc("Journal Entry", journal_entry) jv = frappe.get_doc("Journal Entry", journal_entry)
self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000) self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
@ -629,7 +669,7 @@ class TestDepreciationMethods(AssetSetup):
schedules = [ schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules") for d in get_depr_schedule(asset.name, "Draft")
] ]
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
@ -651,7 +691,7 @@ class TestDepreciationMethods(AssetSetup):
expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]] expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]]
schedules = [ schedules = [
[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] [cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules") for d in get_depr_schedule(asset.name, "Draft")
] ]
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
@ -678,7 +718,7 @@ class TestDepreciationMethods(AssetSetup):
schedules = [ schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules") for d in get_depr_schedule(asset.name, "Draft")
] ]
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
@ -703,7 +743,7 @@ class TestDepreciationMethods(AssetSetup):
schedules = [ schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in asset.get("schedules") for d in get_depr_schedule(asset.name, "Draft")
] ]
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
@ -733,7 +773,7 @@ class TestDepreciationMethods(AssetSetup):
flt(d.depreciation_amount, 2), flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2),
] ]
for d in asset.get("schedules") for d in get_depr_schedule(asset.name, "Draft")
] ]
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
@ -765,7 +805,7 @@ class TestDepreciationMethods(AssetSetup):
flt(d.depreciation_amount, 2), flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2),
] ]
for d in asset.get("schedules") for d in get_depr_schedule(asset.name, "Draft")
] ]
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
@ -798,7 +838,7 @@ class TestDepreciationMethods(AssetSetup):
flt(d.depreciation_amount, 2), flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2),
] ]
for d in asset.get("schedules") for d in get_depr_schedule(asset.name, "Draft")
] ]
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
@ -831,7 +871,7 @@ class TestDepreciationMethods(AssetSetup):
flt(d.depreciation_amount, 2), flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2),
] ]
for d in asset.get("schedules") for d in get_depr_schedule(asset.name, "Draft")
] ]
self.assertEqual(schedules, expected_schedules) self.assertEqual(schedules, expected_schedules)
@ -854,7 +894,7 @@ class TestDepreciationBasics(AssetSetup):
["2022-12-31", 30000, 90000], ["2022-12-31", 30000, 90000],
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@ -877,7 +917,7 @@ class TestDepreciationBasics(AssetSetup):
["2023-01-01", 15000, 90000], ["2023-01-01", 15000, 90000],
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@ -885,7 +925,9 @@ class TestDepreciationBasics(AssetSetup):
def test_get_depreciation_amount(self): def test_get_depreciation_amount(self):
"""Tests if get_depreciation_amount() returns the right value.""" """Tests if get_depreciation_amount() returns the right value."""
from erpnext.assets.doctype.asset.asset import get_depreciation_amount from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depreciation_amount,
)
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31") asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31")
@ -904,8 +946,8 @@ class TestDepreciationBasics(AssetSetup):
depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0]) depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
self.assertEqual(depreciation_amount, 30000) self.assertEqual(depreciation_amount, 30000)
def test_make_depreciation_schedule(self): def test_make_depr_schedule(self):
"""Tests if make_depreciation_schedule() returns the right values.""" """Tests if make_depr_schedule() returns the right values."""
asset = create_asset( asset = create_asset(
item_code="Macbook Pro", item_code="Macbook Pro",
@ -920,7 +962,7 @@ class TestDepreciationBasics(AssetSetup):
expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]] expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(get_depr_schedule(asset.name, "Draft")):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount) self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
@ -940,7 +982,7 @@ class TestDepreciationBasics(AssetSetup):
expected_values = [30000.0, 60000.0, 90000.0] expected_values = [30000.0, 60000.0, 90000.0]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(get_depr_schedule(asset.name, "Draft")):
self.assertEqual(expected_values[i], schedule.accumulated_depreciation_amount) self.assertEqual(expected_values[i], schedule.accumulated_depreciation_amount)
def test_check_is_pro_rata(self): def test_check_is_pro_rata(self):
@ -1120,9 +1162,11 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2021-06-01") post_depreciation_entries(date="2021-06-01")
asset.load_from_db() asset.load_from_db()
self.assertTrue(asset.schedules[0].journal_entry) depr_schedule = get_depr_schedule(asset.name, "Active")
self.assertFalse(asset.schedules[1].journal_entry)
self.assertFalse(asset.schedules[2].journal_entry) self.assertTrue(depr_schedule[0].journal_entry)
self.assertFalse(depr_schedule[1].journal_entry)
self.assertFalse(depr_schedule[2].journal_entry)
def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self): def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self):
"""Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account.""" """Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account."""
@ -1141,7 +1185,7 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2021-06-01") post_depreciation_entries(date="2021-06-01")
asset.load_from_db() asset.load_from_db()
je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry)
accounting_entries = [ accounting_entries = [
{"account": entry.account, "debit": entry.debit, "credit": entry.credit} {"account": entry.account, "debit": entry.debit, "credit": entry.credit}
for entry in je.accounts for entry in je.accounts
@ -1177,7 +1221,7 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2021-06-01") post_depreciation_entries(date="2021-06-01")
asset.load_from_db() asset.load_from_db()
je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry) je = frappe.get_doc("Journal Entry", get_depr_schedule(asset.name, "Active")[0].journal_entry)
accounting_entries = [ accounting_entries = [
{"account": entry.account, "debit": entry.debit, "credit": entry.credit} {"account": entry.account, "debit": entry.debit, "credit": entry.credit}
for entry in je.accounts for entry in je.accounts
@ -1196,8 +1240,8 @@ class TestDepreciationBasics(AssetSetup):
depr_expense_account.parent_account = "Expenses - _TC" depr_expense_account.parent_account = "Expenses - _TC"
depr_expense_account.save() depr_expense_account.save()
def test_clear_depreciation_schedule(self): def test_clear_depr_schedule(self):
"""Tests if clear_depreciation_schedule() works as expected.""" """Tests if clear_depr_schedule() works as expected."""
asset = create_asset( asset = create_asset(
item_code="Macbook Pro", item_code="Macbook Pro",
@ -1213,17 +1257,20 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2021-06-01") post_depreciation_entries(date="2021-06-01")
asset.load_from_db() asset.load_from_db()
asset.clear_depreciation_schedule() asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEqual(len(asset.schedules), 1) clear_depr_schedule(asset_depr_schedule_doc)
def test_clear_depreciation_schedule_for_multiple_finance_books(self): self.assertEqual(len(asset_depr_schedule_doc.get("depreciation_schedule")), 1)
def test_clear_depr_schedule_for_multiple_finance_books(self):
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1) asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.append( asset.append(
"finance_books", "finance_books",
{ {
"finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"frequency_of_depreciation": 1, "frequency_of_depreciation": 1,
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
@ -1234,6 +1281,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append( asset.append(
"finance_books", "finance_books",
{ {
"finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"frequency_of_depreciation": 1, "frequency_of_depreciation": 1,
"total_number_of_depreciations": 6, "total_number_of_depreciations": 6,
@ -1244,6 +1292,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append( asset.append(
"finance_books", "finance_books",
{ {
"finance_book": "Test Finance Book 3",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"frequency_of_depreciation": 12, "frequency_of_depreciation": 12,
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
@ -1256,15 +1305,23 @@ class TestDepreciationBasics(AssetSetup):
post_depreciation_entries(date="2020-04-01") post_depreciation_entries(date="2020-04-01")
asset.load_from_db() asset.load_from_db()
asset.clear_depreciation_schedule() asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 1"
)
clear_depr_schedule(asset_depr_schedule_doc_1)
self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3)
self.assertEqual(len(asset.schedules), 6) asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(
asset.name, "Active", "Test Finance Book 2"
)
clear_depr_schedule(asset_depr_schedule_doc_2)
self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 3)
for schedule in asset.schedules: asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc(
if schedule.idx <= 3: asset.name, "Active", "Test Finance Book 3"
self.assertEqual(schedule.finance_book_id, "1") )
else: clear_depr_schedule(asset_depr_schedule_doc_3)
self.assertEqual(schedule.finance_book_id, "2") self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0)
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self): def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1) asset = create_asset(item_code="Macbook Pro", available_for_use_date="2019-12-31", do_not_save=1)
@ -1273,6 +1330,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append( asset.append(
"finance_books", "finance_books",
{ {
"finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"frequency_of_depreciation": 12, "frequency_of_depreciation": 12,
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,
@ -1283,6 +1341,7 @@ class TestDepreciationBasics(AssetSetup):
asset.append( asset.append(
"finance_books", "finance_books",
{ {
"finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line", "depreciation_method": "Straight Line",
"frequency_of_depreciation": 12, "frequency_of_depreciation": 12,
"total_number_of_depreciations": 6, "total_number_of_depreciations": 6,
@ -1292,13 +1351,15 @@ class TestDepreciationBasics(AssetSetup):
) )
asset.save() asset.save()
self.assertEqual(len(asset.schedules), 9) asset_depr_schedule_doc_1 = get_asset_depr_schedule_doc(
asset.name, "Draft", "Test Finance Book 1"
)
self.assertEqual(len(asset_depr_schedule_doc_1.get("depreciation_schedule")), 3)
for schedule in asset.schedules: asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(
if schedule.idx <= 3: asset.name, "Draft", "Test Finance Book 2"
self.assertEqual(schedule.finance_book_id, 1) )
else: self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 6)
self.assertEqual(schedule.finance_book_id, 2)
def test_depreciation_entry_cancellation(self): def test_depreciation_entry_cancellation(self):
asset = create_asset( asset = create_asset(
@ -1318,12 +1379,12 @@ class TestDepreciationBasics(AssetSetup):
asset.load_from_db() asset.load_from_db()
# cancel depreciation entry # cancel depreciation entry
depr_entry = asset.get("schedules")[0].journal_entry depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
self.assertTrue(depr_entry) self.assertTrue(depr_entry)
frappe.get_doc("Journal Entry", depr_entry).cancel() frappe.get_doc("Journal Entry", depr_entry).cancel()
asset.load_from_db() depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
depr_entry = asset.get("schedules")[0].journal_entry
self.assertFalse(depr_entry) self.assertFalse(depr_entry)
def test_asset_expected_value_after_useful_life(self): def test_asset_expected_value_after_useful_life(self):
@ -1338,7 +1399,7 @@ class TestDepreciationBasics(AssetSetup):
) )
accumulated_depreciation_after_full_schedule = max( accumulated_depreciation_after_full_schedule = max(
d.accumulated_depreciation_amount for d in asset.get("schedules") d.accumulated_depreciation_amount for d in get_depr_schedule(asset.name, "Draft")
) )
asset_value_after_full_schedule = flt(asset.gross_purchase_amount) - flt( asset_value_after_full_schedule = flt(asset.gross_purchase_amount) - flt(
@ -1369,7 +1430,7 @@ class TestDepreciationBasics(AssetSetup):
asset.load_from_db() asset.load_from_db()
# check depreciation entry series # check depreciation entry series
self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") self.assertEqual(get_depr_schedule(asset.name, "Active")[0].journal_entry[:4], "DEPR")
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0),
@ -1439,7 +1500,7 @@ class TestDepreciationBasics(AssetSetup):
"2020-07-15", "2020-07-15",
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(get_depr_schedule(asset.name, "Active")):
self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date)) self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date))
@ -1453,6 +1514,15 @@ def create_asset_data():
if not frappe.db.exists("Location", "Test Location"): if not frappe.db.exists("Location", "Test Location"):
frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert() frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 1"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 2"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert()
if not frappe.db.exists("Finance Book", "Test Finance Book 3"):
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert()
def create_asset(**args): def create_asset(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -7,7 +7,7 @@ import frappe
# import erpnext # import erpnext
from frappe import _ from frappe import _
from frappe.utils import cint, flt from frappe.utils import cint, flt, get_link_to_form
from six import string_types from six import string_types
import erpnext import erpnext
@ -19,6 +19,9 @@ from erpnext.assets.doctype.asset.depreciation import (
reverse_depreciation_entry_made_after_disposal, reverse_depreciation_entry_made_after_disposal,
) )
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
get_current_asset_value, get_current_asset_value,
) )
@ -427,7 +430,12 @@ class AssetCapitalization(StockController):
asset = self.get_asset(item) asset = self.get_asset(item)
if asset.calculate_depreciation: if asset.calculate_depreciation:
depreciate_asset(asset, self.posting_date) notes = _(
"This schedule was created when Asset {0} was consumed when Asset Capitalization {1} was submitted."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.get("name"))
)
depreciate_asset(asset, self.posting_date, notes)
asset.reload() asset.reload()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
@ -513,7 +521,12 @@ class AssetCapitalization(StockController):
asset_doc.purchase_date = self.posting_date asset_doc.purchase_date = self.posting_date
asset_doc.gross_purchase_amount = total_target_asset_value asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value asset_doc.purchase_receipt_amount = total_target_asset_value
asset_doc.prepare_depreciation_data() notes = _(
"This schedule was created when target Asset {0} was updated when Asset Capitalization {1} was submitted."
).format(
get_link_to_form(asset_doc.doctype, asset_doc.name), get_link_to_form(self.doctype, self.name)
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(asset_doc, notes)
asset_doc.flags.ignore_validate_update_after_submit = True asset_doc.flags.ignore_validate_update_after_submit = True
asset_doc.save() asset_doc.save()
elif self.docstatus == 2: elif self.docstatus == 2:
@ -524,7 +537,12 @@ class AssetCapitalization(StockController):
if asset.calculate_depreciation: if asset.calculate_depreciation:
reverse_depreciation_entry_made_after_disposal(asset, self.posting_date) reverse_depreciation_entry_made_after_disposal(asset, self.posting_date)
reset_depreciation_schedule(asset, self.posting_date) notes = _(
"This schedule was created when Asset {0} was restored when Asset Capitalization {1} was cancelled."
).format(
get_link_to_form(asset.doctype, asset.name), get_link_to_form(self.doctype, self.name)
)
reset_depreciation_schedule(asset, self.posting_date, notes)
def get_asset(self, item): def get_asset(self, item):
asset = frappe.get_doc("Asset", item.asset) asset = frappe.get_doc("Asset", item.asset)

View File

@ -12,6 +12,9 @@ from erpnext.assets.doctype.asset.test_asset import (
create_asset_data, create_asset_data,
set_depreciation_settings_in_company, set_depreciation_settings_in_company,
) )
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
@ -253,6 +256,9 @@ class TestAssetCapitalization(unittest.TestCase):
submit=1, submit=1,
) )
first_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
# Create and submit Asset Captitalization # Create and submit Asset Captitalization
asset_capitalization = create_asset_capitalization( asset_capitalization = create_asset_capitalization(
entry_type="Decapitalization", entry_type="Decapitalization",
@ -282,8 +288,18 @@ class TestAssetCapitalization(unittest.TestCase):
consumed_asset.reload() consumed_asset.reload()
self.assertEqual(consumed_asset.status, "Decapitalized") self.assertEqual(consumed_asset.status, "Decapitalized")
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(consumed_asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
depr_schedule_of_consumed_asset = second_asset_depr_schedule.get("depreciation_schedule")
consumed_depreciation_schedule = [ consumed_depreciation_schedule = [
d for d in consumed_asset.schedules if getdate(d.schedule_date) == getdate(capitalization_date) d
for d in depr_schedule_of_consumed_asset
if getdate(d.schedule_date) == getdate(capitalization_date)
] ]
self.assertTrue( self.assertTrue(
consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry

View File

@ -0,0 +1,51 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide("erpnext.asset");
frappe.ui.form.on('Asset Depreciation Schedule', {
onload: function(frm) {
frm.events.make_schedules_editable(frm);
},
make_schedules_editable: function(frm) {
var is_editable = frm.doc.depreciation_method == "Manual" ? true : false;
frm.toggle_enable("depreciation_schedule", is_editable);
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_editable);
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_editable);
}
});
frappe.ui.form.on('Depreciation Schedule', {
make_depreciation_entry: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
if (!row.journal_entry) {
frappe.call({
method: "erpnext.assets.doctype.asset.depreciation.make_depreciation_entry",
args: {
"asset_depr_schedule_name": frm.doc.name,
"date": row.schedule_date
},
callback: function(r) {
frappe.model.sync(r.message);
frm.refresh();
}
})
}
},
depreciation_amount: function(frm, cdt, cdn) {
erpnext.asset.set_accumulated_depreciation(frm);
}
});
erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name,
"accumulated_depreciation_amount", accumulated_depreciation);
})
};

View File

@ -0,0 +1,202 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2022-10-31 15:03:35.424877",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"asset",
"naming_series",
"column_break_2",
"opening_accumulated_depreciation",
"finance_book",
"finance_book_id",
"depreciation_details_section",
"depreciation_method",
"total_number_of_depreciations",
"rate_of_depreciation",
"column_break_8",
"frequency_of_depreciation",
"expected_value_after_useful_life",
"depreciation_schedule_section",
"depreciation_schedule",
"details_section",
"notes",
"status",
"amended_from"
],
"fields": [
{
"fieldname": "asset",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"options": "Asset",
"reqd": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "ACC-ADS-.YYYY.-"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Asset Depreciation Schedule",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"collapsible": 1,
"fieldname": "depreciation_details_section",
"fieldtype": "Section Break",
"label": "Depreciation Details"
},
{
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
},
{
"fieldname": "depreciation_method",
"fieldtype": "Select",
"label": "Depreciation Method",
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
"read_only": 1
},
{
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
"description": "In Percentage",
"fieldname": "rate_of_depreciation",
"fieldtype": "Percent",
"label": "Rate of Depreciation",
"read_only": 1
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"depends_on": "total_number_of_depreciations",
"fieldname": "total_number_of_depreciations",
"fieldtype": "Int",
"label": "Total Number of Depreciations",
"read_only": 1
},
{
"fieldname": "depreciation_schedule_section",
"fieldtype": "Section Break",
"label": "Depreciation Schedule"
},
{
"fieldname": "depreciation_schedule",
"fieldtype": "Table",
"label": "Depreciation Schedule",
"options": "Depreciation Schedule"
},
{
"collapsible": 1,
"collapsible_depends_on": "notes",
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"fieldname": "notes",
"fieldtype": "Small Text",
"label": "Notes",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Draft\nActive\nCancelled",
"read_only": 1
},
{
"depends_on": "frequency_of_depreciation",
"fieldname": "frequency_of_depreciation",
"fieldtype": "Int",
"label": "Frequency of Depreciation (Months)",
"read_only": 1
},
{
"fieldname": "expected_value_after_useful_life",
"fieldtype": "Currency",
"label": "Expected Value After Useful Life",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "finance_book_id",
"fieldtype": "Int",
"hidden": 1,
"label": "Finance Book Id",
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-01-02 15:38:30.766779",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",
"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": "Accounts User",
"share": 1,
"submit": 1,
"write": 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": []
}

View File

@ -0,0 +1,516 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import (
add_days,
add_months,
cint,
date_diff,
flt,
get_last_day,
is_last_day_of_the_month,
)
import erpnext
class AssetDepreciationSchedule(Document):
def before_save(self):
if not self.finance_book_id:
self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(
self.asset, self.finance_book
)
def validate(self):
self.validate_another_asset_depr_schedule_does_not_exist()
def validate_another_asset_depr_schedule_does_not_exist(self):
finance_book_filter = ["finance_book", "is", "not set"]
if self.finance_book:
finance_book_filter = ["finance_book", "=", self.finance_book]
asset_depr_schedule = frappe.db.exists(
"Asset Depreciation Schedule",
[
["asset", "=", self.asset],
finance_book_filter,
["docstatus", "<", 2],
],
)
if asset_depr_schedule and asset_depr_schedule != self.name:
if self.finance_book:
frappe.throw(
_(
"Asset Depreciation Schedule {0} for Asset {1} and Finance Book {2} already exists."
).format(asset_depr_schedule, self.asset, self.finance_book)
)
else:
frappe.throw(
_("Asset Depreciation Schedule {0} for Asset {1} already exists.").format(
asset_depr_schedule, self.asset
)
)
def on_submit(self):
self.db_set("status", "Active")
def before_cancel(self):
if not self.flags.should_not_cancel_depreciation_entries:
self.cancel_depreciation_entries()
def cancel_depreciation_entries(self):
for d in self.get("depreciation_schedule"):
if d.journal_entry:
frappe.get_doc("Journal Entry", d.journal_entry).cancel()
def on_cancel(self):
self.db_set("status", "Cancelled")
def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name):
asset_doc = frappe.get_doc("Asset", asset_name)
finance_book_filter = ["finance_book", "is", "not set"]
if fb_name:
finance_book_filter = ["finance_book", "=", fb_name]
asset_finance_book_name = frappe.db.get_value(
doctype="Asset Finance Book",
filters=[["parent", "=", asset_name], finance_book_filter],
)
asset_finance_book_doc = frappe.get_doc("Asset Finance Book", asset_finance_book_name)
prepare_draft_asset_depr_schedule_data(self, asset_doc, asset_finance_book_doc)
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
for row in asset_doc.get("finance_books"):
draft_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Draft", row.finance_book
)
active_asset_depr_schedule_name = get_asset_depr_schedule_name(
asset_doc.name, "Active", row.finance_book
)
if not draft_asset_depr_schedule_name and not active_asset_depr_schedule_name:
make_draft_asset_depr_schedule(asset_doc, row)
def make_draft_asset_depr_schedules(asset_doc):
for row in asset_doc.get("finance_books"):
make_draft_asset_depr_schedule(asset_doc, row)
def make_draft_asset_depr_schedule(asset_doc, row):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row)
asset_depr_schedule_doc.insert()
def update_draft_asset_depr_schedules(asset_doc):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book)
if not asset_depr_schedule_doc:
continue
prepare_draft_asset_depr_schedule_data(asset_depr_schedule_doc, asset_doc, row)
asset_depr_schedule_doc.save()
def prepare_draft_asset_depr_schedule_data(
asset_depr_schedule_doc,
asset_doc,
row,
date_of_disposal=None,
date_of_return=None,
update_asset_finance_book_row=True,
):
set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row)
make_depr_schedule(
asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row
)
set_accumulated_depreciation(asset_depr_schedule_doc, row, date_of_disposal, date_of_return)
def set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset_doc, row):
asset_depr_schedule_doc.asset = asset_doc.name
asset_depr_schedule_doc.finance_book = row.finance_book
asset_depr_schedule_doc.finance_book_id = row.idx
asset_depr_schedule_doc.opening_accumulated_depreciation = (
asset_doc.opening_accumulated_depreciation
)
asset_depr_schedule_doc.depreciation_method = row.depreciation_method
asset_depr_schedule_doc.total_number_of_depreciations = row.total_number_of_depreciations
asset_depr_schedule_doc.frequency_of_depreciation = row.frequency_of_depreciation
asset_depr_schedule_doc.rate_of_depreciation = row.rate_of_depreciation
asset_depr_schedule_doc.expected_value_after_useful_life = row.expected_value_after_useful_life
asset_depr_schedule_doc.status = "Draft"
def convert_draft_asset_depr_schedules_into_active(asset_doc):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Draft", row.finance_book)
if not asset_depr_schedule_doc:
continue
asset_depr_schedule_doc.submit()
def cancel_asset_depr_schedules(asset_doc):
for row in asset_doc.get("finance_books"):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_doc.name, "Active", row.finance_book)
if not asset_depr_schedule_doc:
continue
asset_depr_schedule_doc.cancel()
def make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset_doc, notes, date_of_disposal=None, date_of_return=None
):
for row in asset_doc.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
asset_doc.name, "Active", row.finance_book
)
if not current_asset_depr_schedule_doc:
frappe.throw(
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
asset_doc.name, row.finance_book
)
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
make_depr_schedule(new_asset_depr_schedule_doc, asset_doc, row, date_of_disposal)
set_accumulated_depreciation(new_asset_depr_schedule_doc, row, date_of_disposal, date_of_return)
new_asset_depr_schedule_doc.notes = notes
current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
current_asset_depr_schedule_doc.cancel()
new_asset_depr_schedule_doc.submit()
def get_temp_asset_depr_schedule_doc(
asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False
):
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
prepare_draft_asset_depr_schedule_data(
asset_depr_schedule_doc,
asset_doc,
row,
date_of_disposal,
date_of_return,
update_asset_finance_book_row,
)
return asset_depr_schedule_doc
def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
finance_book_filter = ["finance_book", "is", "not set"]
if finance_book:
finance_book_filter = ["finance_book", "=", finance_book]
return frappe.db.get_value(
doctype="Asset Depreciation Schedule",
filters=[
["asset", "=", asset_name],
finance_book_filter,
["status", "=", status],
],
)
@frappe.whitelist()
def get_depr_schedule(asset_name, status, finance_book=None):
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset_name, status, finance_book)
if not asset_depr_schedule_doc:
return
return asset_depr_schedule_doc.get("depreciation_schedule")
def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book)
if not asset_depr_schedule_name:
return
asset_depr_schedule_doc = frappe.get_doc("Asset Depreciation Schedule", asset_depr_schedule_name)
return asset_depr_schedule_doc
def make_depr_schedule(
asset_depr_schedule_doc, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
):
if row.depreciation_method != "Manual" and not asset_depr_schedule_doc.get(
"depreciation_schedule"
):
asset_depr_schedule_doc.depreciation_schedule = []
if not asset_doc.available_for_use_date:
return
start = clear_depr_schedule(asset_depr_schedule_doc)
_make_depr_schedule(
asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
)
def clear_depr_schedule(asset_depr_schedule_doc):
start = 0
num_of_depreciations_completed = 0
depr_schedule = []
for schedule in asset_depr_schedule_doc.get("depreciation_schedule"):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start = num_of_depreciations_completed
break
asset_depr_schedule_doc.depreciation_schedule = depr_schedule
return start
def _make_depr_schedule(
asset_depr_schedule_doc, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
):
asset_doc.validate_asset_finance_books(row)
value_after_depreciation = asset_doc._get_value_after_depreciation(row)
row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row:
row.db_update()
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
asset_doc.number_of_depreciations_booked
)
has_pro_rata = asset_doc.check_is_pro_rata(row)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
should_get_last_day = is_last_day_of_the_month(row.depreciation_start_date)
for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
depreciation_amount = get_depreciation_amount(asset_doc, value_after_depreciation, row)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
if should_get_last_day:
schedule_date = get_last_day(schedule_date)
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, -row.frequency_of_depreciation + 1)
# if asset is being sold or scrapped
if date_of_disposal:
from_date = asset_doc.available_for_use_date
if asset_depr_schedule_doc.depreciation_schedule:
from_date = asset_depr_schedule_doc.depreciation_schedule[-1].schedule_date
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
row, depreciation_amount, from_date, date_of_disposal
)
if depreciation_amount > 0:
add_depr_schedule_row(
asset_depr_schedule_doc,
date_of_disposal,
depreciation_amount,
row.depreciation_method,
)
break
# For first row
if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0:
from_date = add_days(
asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
row, depreciation_amount, from_date, row.depreciation_start_date
)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing
# month difference between use date and start date
monthly_schedule_date = add_months(row.depreciation_start_date, -months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not asset_doc.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months(
asset_doc.available_for_use_date,
(n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
)
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = asset_doc.get_pro_rata_amt(
row, depreciation_amount, schedule_date, asset_doc.to_date
)
depreciation_amount = get_adjusted_depreciation_amount(
asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount
)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount:
continue
value_after_depreciation -= flt(
depreciation_amount, asset_doc.precision("gross_purchase_amount")
)
# Adjust depreciation amount in the last period based on the expected value after useful life
if row.expected_value_after_useful_life and (
(
n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != row.expected_value_after_useful_life
)
or value_after_depreciation < row.expected_value_after_useful_life
):
depreciation_amount += value_after_depreciation - row.expected_value_after_useful_life
skip_row = True
if depreciation_amount > 0:
add_depr_schedule_row(
asset_depr_schedule_doc,
schedule_date,
depreciation_amount,
row.depreciation_method,
)
# to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(
asset_depr_schedule_doc, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row
):
if not asset_depr_schedule_doc.opening_accumulated_depreciation:
depreciation_amount_for_first_row = get_depreciation_amount_for_first_row(
asset_depr_schedule_doc
)
if (
depreciation_amount_for_first_row + depreciation_amount_for_last_row
!= depreciation_amount_without_pro_rata
):
depreciation_amount_for_last_row = (
depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
)
return depreciation_amount_for_last_row
def get_depreciation_amount_for_first_row(asset_depr_schedule_doc):
return asset_depr_schedule_doc.get("depreciation_schedule")[0].depreciation_amount
@erpnext.allow_regional
def get_depreciation_amount(asset_doc, depreciable_value, row):
if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time
if not asset_doc.flags.increase_in_asset_life:
depreciation_amount = (
flt(asset_doc.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (
flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
) / (date_diff(asset_doc.to_date, asset_doc.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
return depreciation_amount
def add_depr_schedule_row(
asset_depr_schedule_doc,
schedule_date,
depreciation_amount,
depreciation_method,
):
asset_depr_schedule_doc.append(
"depreciation_schedule",
{
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": depreciation_method,
},
)
def set_accumulated_depreciation(
asset_depr_schedule_doc,
row,
date_of_disposal=None,
date_of_return=None,
ignore_booked_entry=False,
):
straight_line_idx = [
d.idx
for d in asset_depr_schedule_doc.get("depreciation_schedule")
if d.depreciation_method == "Straight Line"
]
accumulated_depreciation = flt(asset_depr_schedule_doc.opening_accumulated_depreciation)
value_after_depreciation = flt(row.value_after_depreciation)
for i, d in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
if ignore_booked_entry and d.journal_entry:
continue
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line
if (
straight_line_idx
and i == max(straight_line_idx) - 1
and not date_of_disposal
and not date_of_return
):
depreciation_amount += flt(
value_after_depreciation - flt(row.expected_value_after_useful_life),
d.precision("depreciation_amount"),
)
d.depreciation_amount = depreciation_amount
accumulated_depreciation += d.depreciation_amount
d.accumulated_depreciation_amount = flt(
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
)

View File

@ -0,0 +1,27 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
class TestAssetDepreciationSchedule(FrappeTestCase):
def setUp(self):
create_asset_data()
def test_throw_error_if_another_asset_depr_schedule_exist(self):
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
second_asset_depr_schedule = frappe.get_doc(
{"doctype": "Asset Depreciation Schedule", "asset": asset.name, "finance_book": None}
)
self.assertRaises(frappe.ValidationError, second_asset_depr_schedule.insert)

View File

@ -3,11 +3,15 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import add_months, cint, flt, getdate, time_diff_in_hours from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours
import erpnext import erpnext
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@ -52,8 +56,11 @@ class AssetRepair(AccountsController):
): ):
self.modify_depreciation_schedule() self.modify_depreciation_schedule()
notes = _("This schedule was created when Asset Repair {0} was submitted.").format(
get_link_to_form(self.doctype, self.name)
)
self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data() make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save() self.asset_doc.save()
def before_cancel(self): def before_cancel(self):
@ -73,8 +80,11 @@ class AssetRepair(AccountsController):
): ):
self.revert_depreciation_schedule_on_cancellation() self.revert_depreciation_schedule_on_cancellation()
notes = _("This schedule was created when Asset Repair {0} was cancelled.").format(
get_link_to_form(self.doctype, self.name)
)
self.asset_doc.flags.ignore_validate_update_after_submit = True self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data() make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save() self.asset_doc.save()
def check_repair_status(self): def check_repair_status(self):
@ -279,8 +289,10 @@ class AssetRepair(AccountsController):
asset.number_of_depreciations_booked asset.number_of_depreciations_booked
) )
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
# the Schedule Date in the final row of the old Depreciation Schedule # the Schedule Date in the final row of the old Depreciation Schedule
last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date
# the Schedule Date in the final row of the new Depreciation Schedule # the Schedule Date in the final row of the new Depreciation Schedule
asset.to_date = add_months(last_schedule_date, extra_months) asset.to_date = add_months(last_schedule_date, extra_months)
@ -310,8 +322,10 @@ class AssetRepair(AccountsController):
asset.number_of_depreciations_booked asset.number_of_depreciations_booked
) )
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
# the Schedule Date in the final row of the modified Depreciation Schedule # the Schedule Date in the final row of the modified Depreciation Schedule
last_schedule_date = asset.schedules[len(asset.schedules) - 1].schedule_date last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date
# the Schedule Date in the final row of the original Depreciation Schedule # the Schedule Date in the final row of the original Depreciation Schedule
asset.to_date = add_months(last_schedule_date, -extra_months) asset.to_date = add_months(last_schedule_date, -extra_months)

View File

@ -12,6 +12,9 @@ from erpnext.assets.doctype.asset.test_asset import (
create_asset_data, create_asset_data,
set_depreciation_settings_in_company, set_depreciation_settings_in_company,
) )
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
@ -232,13 +235,23 @@ class TestAssetRepair(unittest.TestCase):
def test_increase_in_asset_life(self): def test_increase_in_asset_life(self):
asset = create_asset(calculate_depreciation=1, submit=1) asset = create_asset(calculate_depreciation=1, submit=1)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
initial_num_of_depreciations = num_of_depreciations(asset) initial_num_of_depreciations = num_of_depreciations(asset)
create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1)
asset.reload() asset.reload()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset)) self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset))
self.assertEqual( self.assertEqual(
asset.schedules[-1].accumulated_depreciation_amount, second_asset_depr_schedule.get("depreciation_schedule")[-1].accumulated_depreciation_amount,
asset.finance_books[0].value_after_depreciation, asset.finance_books[0].value_after_depreciation,
) )

View File

@ -5,13 +5,17 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, date_diff, flt, formatdate, getdate from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts, get_checks_for_pl_and_bs_accounts,
) )
from erpnext.assets.doctype.asset.asset import get_depreciation_amount
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
get_depreciation_amount,
set_accumulated_depreciation,
)
class AssetValueAdjustment(Document): class AssetValueAdjustment(Document):
@ -112,21 +116,40 @@ class AssetValueAdjustment(Document):
for d in asset.finance_books: for d in asset.finance_books:
d.value_after_depreciation = asset_value d.value_after_depreciation = asset_value
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
asset.name, "Active", d.finance_book
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
new_asset_depr_schedule_doc.status = "Draft"
new_asset_depr_schedule_doc.docstatus = 0
current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
current_asset_depr_schedule_doc.cancel()
notes = _(
"This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
).format(
get_link_to_form(asset.doctype, asset.name),
get_link_to_form(self.get("doctype"), self.get("name")),
)
new_asset_depr_schedule_doc.notes = notes
new_asset_depr_schedule_doc.insert()
depr_schedule = new_asset_depr_schedule_doc.get("depreciation_schedule")
if d.depreciation_method in ("Straight Line", "Manual"): if d.depreciation_method in ("Straight Line", "Manual"):
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx) end_date = max(s.schedule_date for s in depr_schedule)
total_days = date_diff(end_date, self.date) total_days = date_diff(end_date, self.date)
rate_per_day = flt(d.value_after_depreciation) / flt(total_days) rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
from_date = self.date from_date = self.date
else: else:
no_of_depreciations = len( no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry])
[
s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry)
]
)
value_after_depreciation = d.value_after_depreciation value_after_depreciation = d.value_after_depreciation
for data in asset.schedules: for data in depr_schedule:
if cint(data.finance_book_id) == d.idx and not data.journal_entry: if not data.journal_entry:
if d.depreciation_method in ("Straight Line", "Manual"): if d.depreciation_method in ("Straight Line", "Manual"):
days = date_diff(data.schedule_date, from_date) days = date_diff(data.schedule_date, from_date)
depreciation_amount = days * rate_per_day depreciation_amount = days * rate_per_day
@ -140,11 +163,13 @@ class AssetValueAdjustment(Document):
d.db_update() d.db_update()
asset.set_accumulated_depreciation(ignore_booked_entry=True) set_accumulated_depreciation(new_asset_depr_schedule_doc, d, ignore_booked_entry=True)
for asset_data in asset.schedules: for asset_data in depr_schedule:
if not asset_data.journal_entry: if not asset_data.journal_entry:
asset_data.db_update() asset_data.db_update()
new_asset_depr_schedule_doc.submit()
@frappe.whitelist() @frappe.whitelist()
def get_current_asset_value(asset, finance_book=None): def get_current_asset_value(asset, finance_book=None):

View File

@ -7,6 +7,9 @@ import frappe
from frappe.utils import add_days, get_last_day, nowdate from frappe.utils import add_days, get_last_day, nowdate
from erpnext.assets.doctype.asset.test_asset import create_asset_data from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import ( from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
get_current_asset_value, get_current_asset_value,
) )
@ -73,12 +76,21 @@ class TestAssetValueAdjustment(unittest.TestCase):
) )
asset_doc.submit() asset_doc.submit()
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
current_value = get_current_asset_value(asset_doc.name) current_value = get_current_asset_value(asset_doc.name)
adj_doc = make_asset_value_adjustment( adj_doc = make_asset_value_adjustment(
asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0 asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
) )
adj_doc.submit() adj_doc.submit()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(second_asset_depr_schedule.status, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
expected_gle = ( expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 50000.0), ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
("_Test Depreciations - _TC", 50000.0, 0.0), ("_Test Depreciations - _TC", 50000.0, 0.0),

View File

@ -1,318 +1,84 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1, "allow_rename": 1,
"autoname": "",
"beta": 0,
"creation": "2016-03-02 15:11:01.278862", "creation": "2016-03-02 15:11:01.278862",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"schedule_date",
"depreciation_amount",
"column_break_3",
"accumulated_depreciation_amount",
"journal_entry",
"make_depreciation_entry",
"depreciation_method"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "finance_book",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Finance Book",
"length": 0,
"no_copy": 0,
"options": "Finance Book",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "schedule_date", "fieldname": "schedule_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Schedule Date", "label": "Schedule Date",
"length": 0, "reqd": 1
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "depreciation_amount", "fieldname": "depreciation_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Depreciation Amount", "label": "Depreciation Amount",
"length": 0,
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "accumulated_depreciation_amount", "fieldname": "accumulated_depreciation_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Accumulated Depreciation Amount", "label": "Accumulated Depreciation Amount",
"length": 0,
"no_copy": 1,
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.docstatus==1", "depends_on": "eval:doc.docstatus==1",
"fieldname": "journal_entry", "fieldname": "journal_entry",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Journal Entry", "label": "Journal Entry",
"length": 0,
"no_copy": 1,
"options": "Journal Entry", "options": "Journal Entry",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 1, "allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())", "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())",
"fieldname": "make_depreciation_entry", "fieldname": "make_depreciation_entry",
"fieldtype": "Button", "fieldtype": "Button",
"hidden": 0, "label": "Make Depreciation Entry"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Make Depreciation Entry",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "finance_book_id",
"fieldtype": "Data",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Finance Book Id",
"length": 0,
"no_copy": 1,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "depreciation_method", "fieldname": "depreciation_method",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 1, "hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Depreciation Method", "label": "Depreciation Method",
"length": 0,
"no_copy": 1,
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
"permlevel": 0,
"precision": "",
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2018-05-10 15:12:41.679436", "modified": "2022-12-06 20:35:50.264281",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Depreciation Schedule", "name": "Depreciation Schedule",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "states": []
"track_seen": 0
} }

View File

@ -176,15 +176,17 @@ def get_finance_book_value_map(filters):
return frappe._dict( return frappe._dict(
frappe.db.sql( frappe.db.sql(
""" Select """ Select
parent, SUM(depreciation_amount) ads.asset, SUM(depreciation_amount)
FROM `tabDepreciation Schedule` FROM `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
WHERE WHERE
parentfield='schedules' ds.parent = ads.name
AND schedule_date<=%s AND ifnull(ads.finance_book, '')=%s
AND journal_entry IS NOT NULL AND ads.docstatus=1
AND ifnull(finance_book, '')=%s AND ds.parentfield='depreciation_schedule'
GROUP BY parent""", AND ds.schedule_date<=%s
(date, cstr(filters.finance_book or "")), AND ds.journal_entry IS NOT NULL
GROUP BY ads.asset""",
(cstr(filters.finance_book or ""), date),
) )
) )

View File

@ -108,7 +108,7 @@
"contact_display", "contact_display",
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
"company_shipping_address_section", "shipping_address_section",
"shipping_address", "shipping_address",
"column_break_99", "column_break_99",
"shipping_address_display", "shipping_address_display",
@ -385,7 +385,7 @@
{ {
"fieldname": "shipping_address", "fieldname": "shipping_address",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company Shipping Address", "label": "Shipping Address",
"options": "Address", "options": "Address",
"print_hide": 1 "print_hide": 1
}, },
@ -1207,11 +1207,6 @@
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "Address & Contact" "label": "Address & Contact"
}, },
{
"fieldname": "company_shipping_address_section",
"fieldtype": "Section Break",
"label": "Company Shipping Address"
},
{ {
"fieldname": "company_billing_address_section", "fieldname": "company_billing_address_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -1263,13 +1258,18 @@
"fieldname": "named_place", "fieldname": "named_place",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Named Place" "label": "Named Place"
},
{
"fieldname": "shipping_address_section",
"fieldtype": "Section Break",
"label": "Shipping Address"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-12 18:36:37.455134", "modified": "2022-12-25 18:08:59.074182",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -207,7 +207,8 @@ class PurchaseOrder(BuyingController):
) )
def validate_fg_item_for_subcontracting(self): def validate_fg_item_for_subcontracting(self):
if self.is_subcontracted and not self.is_old_subcontracting_flow: if self.is_subcontracted:
if not self.is_old_subcontracting_flow:
for item in self.items: for item in self.items:
if not item.fg_item: if not item.fg_item:
frappe.throw( frappe.throw(
@ -232,6 +233,10 @@ class PurchaseOrder(BuyingController):
item.idx, item.item_code item.idx, item.item_code
) )
) )
else:
for item in self.items:
item.set("fg_item", None)
item.set("fg_item_qty", 0)
def get_schedule_dates(self): def get_schedule_dates(self):
for d in self.get("items"): for d in self.get("items"):

View File

@ -584,7 +584,12 @@ class AccountsController(TransactionBase):
if bool(uom) != bool(stock_uom): # xor if bool(uom) != bool(stock_uom): # xor
item.stock_uom = item.uom = uom or stock_uom item.stock_uom = item.uom = uom or stock_uom
item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom")) # UOM cannot be zero so substitute as 1
item.conversion_factor = (
get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
or item.get("conversion_factor")
or 1
)
if self.doctype == "Purchase Invoice": if self.doctype == "Purchase Invoice":
self.set_expense_account(for_validate) self.set_expense_account(for_validate)

View File

@ -23,7 +23,7 @@ class SellingController(StockController):
super(SellingController, self).onload() super(SellingController, self).onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"): if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
for item in self.get("items"): for item in self.get("items"):
item.update(get_bin_details(item.item_code, item.warehouse)) item.update(get_bin_details(item.item_code, item.warehouse, include_child_warehouses=True))
def validate(self): def validate(self):
super(SellingController, self).validate() super(SellingController, self).validate()

View File

@ -829,6 +829,9 @@ def make_rm_stock_entry(
order_doctype: { order_doctype: {
"doctype": "Stock Entry", "doctype": "Stock Entry",
"field_map": { "field_map": {
"supplier": "supplier",
"supplier_name": "supplier_name",
"supplier_address": "supplier_address",
"to_warehouse": "supplier_warehouse", "to_warehouse": "supplier_warehouse",
}, },
"field_no_map": [field_no_map], "field_no_map": [field_no_map],

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-05-21 07:41:53.536536", "creation": "2019-05-21 07:41:53.536536",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
@ -7,10 +8,14 @@
"section_break_2", "section_break_2",
"account_sid", "account_sid",
"api_key", "api_key",
"api_token" "api_token",
"section_break_6",
"map_custom_field_to_doctype",
"target_doctype"
], ],
"fields": [ "fields": [
{ {
"default": "0",
"fieldname": "enabled", "fieldname": "enabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enabled" "label": "Enabled"
@ -18,7 +23,8 @@
{ {
"depends_on": "enabled", "depends_on": "enabled",
"fieldname": "section_break_2", "fieldname": "section_break_2",
"fieldtype": "Section Break" "fieldtype": "Section Break",
"label": "Credentials"
}, },
{ {
"fieldname": "account_sid", "fieldname": "account_sid",
@ -34,10 +40,31 @@
"fieldname": "api_key", "fieldname": "api_key",
"fieldtype": "Data", "fieldtype": "Data",
"label": "API Key" "label": "API Key"
},
{
"depends_on": "enabled",
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"label": "Custom Field"
},
{
"default": "0",
"fieldname": "map_custom_field_to_doctype",
"fieldtype": "Check",
"label": "Map Custom Field to DocType"
},
{
"depends_on": "map_custom_field_to_doctype",
"fieldname": "target_doctype",
"fieldtype": "Link",
"label": "Target DocType",
"mandatory_depends_on": "map_custom_field_to_doctype",
"options": "DocType"
} }
], ],
"issingle": 1, "issingle": 1,
"modified": "2019-05-22 06:25:18.026997", "links": [],
"modified": "2022-12-14 17:24:50.176107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Exotel Settings", "name": "Exotel Settings",
@ -57,5 +84,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -72,6 +72,24 @@ def get_call_log(call_payload):
return frappe.get_doc("Call Log", call_log_id) return frappe.get_doc("Call Log", call_log_id)
def map_custom_field(call_payload, call_log):
field_value = call_payload.get("CustomField")
if not field_value:
return call_log
settings = get_exotel_settings()
target_doctype = settings.target_doctype
mapping_enabled = settings.map_custom_field_to_doctype
if not mapping_enabled or not target_doctype:
return call_log
call_log.append("links", {"link_doctype": target_doctype, "link_name": field_value})
return call_log
def create_call_log(call_payload): def create_call_log(call_payload):
call_log = frappe.new_doc("Call Log") call_log = frappe.new_doc("Call Log")
call_log.id = call_payload.get("CallSid") call_log.id = call_payload.get("CallSid")
@ -79,6 +97,7 @@ def create_call_log(call_payload):
call_log.medium = call_payload.get("To") call_log.medium = call_payload.get("To")
call_log.status = "Ringing" call_log.status = "Ringing"
setattr(call_log, "from", call_payload.get("CallFrom")) setattr(call_log, "from", call_payload.get("CallFrom"))
map_custom_field(call_payload, call_log)
call_log.save(ignore_permissions=True) call_log.save(ignore_permissions=True)
frappe.db.commit() frappe.db.commit()
return call_log return call_log
@ -93,10 +112,10 @@ def get_call_status(call_id):
@frappe.whitelist() @frappe.whitelist()
def make_a_call(from_number, to_number, caller_id): def make_a_call(from_number, to_number, caller_id, **kwargs):
endpoint = get_exotel_endpoint("Calls/connect.json?details=true") endpoint = get_exotel_endpoint("Calls/connect.json?details=true")
response = requests.post( response = requests.post(
endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id} endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id, **kwargs}
) )
return response.json() return response.json()

View File

@ -420,7 +420,6 @@ scheduler_events = {
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event", "erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
"erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries",
], ],
"monthly_long": [ "monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.accounts.deferred_revenue.process_deferred_accounting",

View File

@ -4,7 +4,7 @@
frappe.provide("erpnext.bom"); frappe.provide("erpnext.bom");
frappe.ui.form.on("BOM", { frappe.ui.form.on("BOM", {
setup: function(frm) { setup(frm) {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Work Order': 'Work Order', 'Work Order': 'Work Order',
'Quality Inspection': 'Quality Inspection' 'Quality Inspection': 'Quality Inspection'
@ -65,11 +65,11 @@ frappe.ui.form.on("BOM", {
}); });
}, },
onload_post_render: function(frm) { onload_post_render(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty"); frm.get_field("items").grid.set_multiple_add("item_code", "qty");
}, },
refresh: function(frm) { refresh(frm) {
frm.toggle_enable("item", frm.doc.__islocal); frm.toggle_enable("item", frm.doc.__islocal);
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
@ -152,7 +152,7 @@ frappe.ui.form.on("BOM", {
} }
}, },
make_work_order: function(frm) { make_work_order(frm) {
frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => { frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order",
@ -164,7 +164,7 @@ frappe.ui.form.on("BOM", {
variant_items: variant_items variant_items: variant_items
}, },
freeze: true, freeze: true,
callback: function(r) { callback(r) {
if(r.message) { if(r.message) {
let doc = frappe.model.sync(r.message)[0]; let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name); frappe.set_route("Form", doc.doctype, doc.name);
@ -174,7 +174,7 @@ frappe.ui.form.on("BOM", {
}); });
}, },
make_variant_bom: function(frm) { make_variant_bom(frm) {
frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => { frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => {
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom", method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom",
@ -185,7 +185,7 @@ frappe.ui.form.on("BOM", {
variant_items: variant_items variant_items: variant_items
}, },
freeze: true, freeze: true,
callback: function(r) { callback(r) {
if(r.message) { if(r.message) {
let doc = frappe.model.sync(r.message)[0]; let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name); frappe.set_route("Form", doc.doctype, doc.name);
@ -195,7 +195,7 @@ frappe.ui.form.on("BOM", {
}, true); }, true);
}, },
setup_variant_prompt: function(frm, title, callback, skip_qty_field) { setup_variant_prompt(frm, title, callback, skip_qty_field) {
const fields = []; const fields = [];
if (frm.doc.has_variants) { if (frm.doc.has_variants) {
@ -205,7 +205,7 @@ frappe.ui.form.on("BOM", {
fieldname: 'item', fieldname: 'item',
options: "Item", options: "Item",
reqd: 1, reqd: 1,
get_query: function() { get_query() {
return { return {
query: "erpnext.controllers.queries.item_query", query: "erpnext.controllers.queries.item_query",
filters: { filters: {
@ -273,7 +273,7 @@ frappe.ui.form.on("BOM", {
fieldtype: "Link", fieldtype: "Link",
in_list_view: 1, in_list_view: 1,
reqd: 1, reqd: 1,
get_query: function(data) { get_query(data) {
if (!data.item_code) { if (!data.item_code) {
frappe.throw(__("Select template item")); frappe.throw(__("Select template item"));
} }
@ -308,7 +308,7 @@ frappe.ui.form.on("BOM", {
], ],
in_place_edit: true, in_place_edit: true,
data: [], data: [],
get_data: function () { get_data () {
return []; return [];
}, },
}); });
@ -343,14 +343,14 @@ frappe.ui.form.on("BOM", {
} }
}, },
make_quality_inspection: function(frm) { make_quality_inspection(frm) {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection", method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection",
frm: frm frm: frm
}) })
}, },
update_cost: function(frm, save_doc=false) { update_cost(frm, save_doc=false) {
return frappe.call({ return frappe.call({
doc: frm.doc, doc: frm.doc,
method: "update_cost", method: "update_cost",
@ -360,26 +360,26 @@ frappe.ui.form.on("BOM", {
save: save_doc, save: save_doc,
from_child_bom: false from_child_bom: false
}, },
callback: function(r) { callback(r) {
refresh_field("items"); refresh_field("items");
if(!r.exc) frm.refresh_fields(); if(!r.exc) frm.refresh_fields();
} }
}); });
}, },
rm_cost_as_per: function(frm) { rm_cost_as_per(frm) {
if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) { if (in_list(["Valuation Rate", "Last Purchase Rate"], frm.doc.rm_cost_as_per)) {
frm.set_value("plc_conversion_rate", 1.0); frm.set_value("plc_conversion_rate", 1.0);
} }
}, },
routing: function(frm) { routing(frm) {
if (frm.doc.routing) { if (frm.doc.routing) {
frappe.call({ frappe.call({
doc: frm.doc, doc: frm.doc,
method: "get_routing", method: "get_routing",
freeze: true, freeze: true,
callback: function(r) { callback(r) {
if (!r.exc) { if (!r.exc) {
frm.refresh_fields(); frm.refresh_fields();
erpnext.bom.calculate_op_cost(frm.doc); erpnext.bom.calculate_op_cost(frm.doc);
@ -388,6 +388,16 @@ frappe.ui.form.on("BOM", {
} }
}); });
} }
},
process_loss_percentage(frm) {
let qty = 0.0
if (frm.doc.process_loss_percentage) {
qty = (frm.doc.quantity * frm.doc.process_loss_percentage) / 100;
}
frm.set_value("process_loss_qty", qty);
frm.set_value("add_process_loss_cost_in_fg", qty ? 1: 0);
} }
}); });
@ -479,10 +489,6 @@ var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
}, },
callback: function(r) { callback: function(r) {
d = locals[cdt][cdn]; d = locals[cdt][cdn];
if (d.is_process_loss) {
r.message.rate = 0;
r.message.base_rate = 0;
}
$.extend(d, r.message); $.extend(d, r.message);
refresh_field("items"); refresh_field("items");
@ -717,10 +723,6 @@ frappe.tour['BOM'] = [
frappe.ui.form.on("BOM Scrap Item", { frappe.ui.form.on("BOM Scrap Item", {
item_code(frm, cdt, cdn) { item_code(frm, cdt, cdn) {
const { item_code } = locals[cdt][cdn]; const { item_code } = locals[cdt][cdn];
if (item_code === frm.doc.item) {
locals[cdt][cdn].is_process_loss = 1;
trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code);
}
}, },
}); });

View File

@ -6,6 +6,7 @@
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"production_item_tab",
"item", "item",
"company", "company",
"item_name", "item_name",
@ -19,14 +20,15 @@
"quantity", "quantity",
"image", "image",
"currency_detail", "currency_detail",
"currency",
"conversion_rate",
"column_break_12",
"rm_cost_as_per", "rm_cost_as_per",
"buying_price_list", "buying_price_list",
"price_list_currency", "price_list_currency",
"plc_conversion_rate", "plc_conversion_rate",
"column_break_ivyw",
"currency",
"conversion_rate",
"section_break_21", "section_break_21",
"operations_section_section",
"with_operations", "with_operations",
"column_break_23", "column_break_23",
"transfer_material_against", "transfer_material_against",
@ -34,13 +36,14 @@
"operations_section", "operations_section",
"operations", "operations",
"materials_section", "materials_section",
"inspection_required",
"quality_inspection_template",
"column_break_31",
"section_break_33",
"items", "items",
"scrap_section", "scrap_section",
"scrap_items_section",
"scrap_items", "scrap_items",
"process_loss_section",
"process_loss_percentage",
"column_break_ssj2",
"process_loss_qty",
"costing", "costing",
"operating_cost", "operating_cost",
"raw_material_cost", "raw_material_cost",
@ -52,10 +55,14 @@
"column_break_26", "column_break_26",
"total_cost", "total_cost",
"base_total_cost", "base_total_cost",
"section_break_25", "more_info_tab",
"description", "description",
"column_break_27", "column_break_27",
"has_variants", "has_variants",
"quality_inspection_section_break",
"inspection_required",
"column_break_dxp7",
"quality_inspection_template",
"section_break0", "section_break0",
"exploded_items", "exploded_items",
"website_section", "website_section",
@ -68,7 +75,8 @@
"show_items", "show_items",
"show_operations", "show_operations",
"web_long_description", "web_long_description",
"amended_from" "amended_from",
"connections_tab"
], ],
"fields": [ "fields": [
{ {
@ -183,7 +191,7 @@
{ {
"fieldname": "currency_detail", "fieldname": "currency_detail",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Currency and Price List" "label": "Cost Configuration"
}, },
{ {
"fieldname": "company", "fieldname": "company",
@ -208,10 +216,6 @@
"precision": "9", "precision": "9",
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{ {
"fieldname": "currency", "fieldname": "currency",
"fieldtype": "Link", "fieldtype": "Link",
@ -261,7 +265,7 @@
{ {
"fieldname": "materials_section", "fieldname": "materials_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Materials", "label": "Raw Materials",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
{ {
@ -276,18 +280,18 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "scrap_section", "fieldname": "scrap_section",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Scrap" "label": "Scrap & Process Loss"
}, },
{ {
"fieldname": "scrap_items", "fieldname": "scrap_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Scrap Items", "label": "Items",
"options": "BOM Scrap Item" "options": "BOM Scrap Item"
}, },
{ {
"fieldname": "costing", "fieldname": "costing",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Costing", "label": "Costing",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break"
}, },
@ -379,10 +383,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "section_break_25",
"fieldtype": "Section Break"
},
{ {
"fetch_from": "item.description", "fetch_from": "item.description",
"fieldname": "description", "fieldname": "description",
@ -478,8 +478,8 @@
}, },
{ {
"fieldname": "section_break_21", "fieldname": "section_break_21",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Operations" "label": "Operations & Materials"
}, },
{ {
"fieldname": "column_break_23", "fieldname": "column_break_23",
@ -511,6 +511,7 @@
"fetch_from": "item.has_variants", "fetch_from": "item.has_variants",
"fieldname": "has_variants", "fieldname": "has_variants",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Has Variants", "label": "Has Variants",
"no_copy": 1, "no_copy": 1,
@ -518,13 +519,63 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "column_break_31", "fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "operations_section_section",
"fieldtype": "Section Break",
"label": "Operations"
},
{
"fieldname": "process_loss_section",
"fieldtype": "Section Break",
"label": "Process Loss"
},
{
"fieldname": "process_loss_percentage",
"fieldtype": "Percent",
"label": "% Process Loss"
},
{
"fieldname": "process_loss_qty",
"fieldtype": "Float",
"label": "Process Loss Qty",
"read_only": 1
},
{
"fieldname": "column_break_ssj2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"fieldname": "section_break_33", "fieldname": "more_info_tab",
"fieldtype": "Tab Break",
"label": "More Info"
},
{
"fieldname": "column_break_dxp7",
"fieldtype": "Column Break"
},
{
"fieldname": "quality_inspection_section_break",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1 "label": "Quality Inspection"
},
{
"fieldname": "production_item_tab",
"fieldtype": "Tab Break",
"label": "Production Item"
},
{
"fieldname": "column_break_ivyw",
"fieldtype": "Column Break"
},
{
"fieldname": "scrap_items_section",
"fieldtype": "Section Break",
"label": "Scrap Items"
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
@ -532,7 +583,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-01-30 21:27:54.727298", "modified": "2023-01-03 18:42:27.732107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@ -193,6 +193,7 @@ class BOM(WebsiteGenerator):
self.update_exploded_items(save=False) self.update_exploded_items(save=False)
self.update_stock_qty() self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False) self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
self.set_process_loss_qty()
self.validate_scrap_items() self.validate_scrap_items()
def get_context(self, context): def get_context(self, context):
@ -233,6 +234,7 @@ class BOM(WebsiteGenerator):
"sequence_id", "sequence_id",
"operation", "operation",
"workstation", "workstation",
"workstation_type",
"description", "description",
"time_in_mins", "time_in_mins",
"batch_size", "batch_size",
@ -876,36 +878,19 @@ class BOM(WebsiteGenerator):
"""Get a complete tree representation preserving order of child items.""" """Get a complete tree representation preserving order of child items."""
return BOMTree(self.name) return BOMTree(self.name)
def set_process_loss_qty(self):
if self.process_loss_percentage:
self.process_loss_qty = flt(self.quantity) * flt(self.process_loss_percentage) / 100
def validate_scrap_items(self): def validate_scrap_items(self):
for item in self.scrap_items: must_be_whole_number = frappe.get_value("UOM", self.uom, "must_be_whole_number")
msg = ""
if item.item_code == self.item and not item.is_process_loss:
msg = _(
"Scrap/Loss Item: {0} should have Is Process Loss checked as it is the same as the item to be manufactured or repacked."
).format(frappe.bold(item.item_code))
elif item.item_code != self.item and item.is_process_loss:
msg = _(
"Scrap/Loss Item: {0} should not have Is Process Loss checked as it is different from the item to be manufactured or repacked"
).format(frappe.bold(item.item_code))
must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number") if self.process_loss_percentage and self.process_loss_percentage > 100:
if item.is_process_loss and must_be_whole_number: frappe.throw(_("Process Loss Percentage cannot be greater than 100"))
msg = _(
"Item: {0} with Stock UOM: {1} cannot be a Scrap/Loss Item as {1} is a whole UOM."
).format(frappe.bold(item.item_code), frappe.bold(item.stock_uom))
if item.is_process_loss and (item.stock_qty >= self.quantity): if self.process_loss_qty and must_be_whole_number and self.process_loss_qty % 1 != 0:
msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.").format( msg = f"Item: {frappe.bold(self.item)} with Stock UOM: {frappe.bold(self.uom)} can't have fractional process loss qty as UOM {frappe.bold(self.uom)} is a whole Number."
frappe.bold(item.item_code) frappe.throw(msg, title=_("Invalid Process Loss Configuration"))
)
if item.is_process_loss and (item.rate > 0):
msg = _(
"Scrap/Loss Item: {0} should have Rate set to 0 because Is Process Loss is checked."
).format(frappe.bold(item.item_code))
if msg:
frappe.throw(msg, title=_("Note"))
def get_bom_item_rate(args, bom_doc): def get_bom_item_rate(args, bom_doc):
@ -1053,7 +1038,7 @@ def get_bom_items_as_dict(
query = query.format( query = query.format(
table="BOM Scrap Item", table="BOM Scrap Item",
where_conditions="", where_conditions="",
select_columns=", item.description, is_process_loss", select_columns=", item.description",
is_stock_item=is_stock_item, is_stock_item=is_stock_item,
qty_field="stock_qty", qty_field="stock_qty",
) )

View File

@ -384,36 +384,16 @@ class TestBOM(FrappeTestCase):
def test_bom_with_process_loss_item(self): def test_bom_with_process_loss_item(self):
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
if not frappe.db.exists("BOM", f"BOM-{fg_item_non_whole.item_code}-001"):
bom_doc = create_bom_with_process_loss_item( bom_doc = create_bom_with_process_loss_item(
fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1 fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0, process_loss_percentage=110
) )
bom_doc.submit() # PL can't be > 100
bom_doc = create_bom_with_process_loss_item(
fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0
)
# PL Item qty can't be >= FG Item qty
self.assertRaises(frappe.ValidationError, bom_doc.submit) self.assertRaises(frappe.ValidationError, bom_doc.submit)
bom_doc = create_bom_with_process_loss_item( bom_doc = create_bom_with_process_loss_item(fg_item_whole, bom_item, process_loss_percentage=20)
fg_item_non_whole, bom_item, scrap_qty=1, scrap_rate=100
)
# PL Item rate has to be 0
self.assertRaises(frappe.ValidationError, bom_doc.submit)
bom_doc = create_bom_with_process_loss_item(
fg_item_whole, bom_item, scrap_qty=0.25, scrap_rate=0
)
# Items with whole UOMs can't be PL Items # Items with whole UOMs can't be PL Items
self.assertRaises(frappe.ValidationError, bom_doc.submit) self.assertRaises(frappe.ValidationError, bom_doc.submit)
bom_doc = create_bom_with_process_loss_item(
fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, is_process_loss=0
)
# FG Items in Scrap/Loss Table should have Is Process Loss set
self.assertRaises(frappe.ValidationError, bom_doc.submit)
def test_bom_item_query(self): def test_bom_item_query(self):
query = partial( query = partial(
item_query, item_query,
@ -744,7 +724,7 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
def create_bom_with_process_loss_item( def create_bom_with_process_loss_item(
fg_item, bom_item, scrap_qty, scrap_rate, fg_qty=2, is_process_loss=1 fg_item, bom_item, scrap_qty=0, scrap_rate=0, fg_qty=2, process_loss_percentage=0
): ):
bom_doc = frappe.new_doc("BOM") bom_doc = frappe.new_doc("BOM")
bom_doc.item = fg_item.item_code bom_doc.item = fg_item.item_code
@ -759,6 +739,8 @@ def create_bom_with_process_loss_item(
"rate": 100.0, "rate": 100.0,
}, },
) )
if scrap_qty:
bom_doc.append( bom_doc.append(
"scrap_items", "scrap_items",
{ {
@ -768,10 +750,11 @@ def create_bom_with_process_loss_item(
"uom": fg_item.stock_uom, "uom": fg_item.stock_uom,
"stock_uom": fg_item.stock_uom, "stock_uom": fg_item.stock_uom,
"rate": scrap_rate, "rate": scrap_rate,
"is_process_loss": is_process_loss,
}, },
) )
bom_doc.currency = "INR" bom_doc.currency = "INR"
bom_doc.process_loss_percentage = process_loss_percentage
return bom_doc return bom_doc

View File

@ -8,7 +8,6 @@
"item_code", "item_code",
"column_break_2", "column_break_2",
"item_name", "item_name",
"is_process_loss",
"quantity_and_rate", "quantity_and_rate",
"stock_qty", "stock_qty",
"rate", "rate",
@ -89,17 +88,11 @@
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "is_process_loss",
"fieldtype": "Check",
"label": "Is Process Loss"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-22 16:46:12.153311", "modified": "2023-01-03 14:19:28.460965",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Scrap Item", "name": "BOM Scrap Item",
@ -108,5 +101,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -846,20 +846,20 @@ class TestWorkOrder(FrappeTestCase):
create_process_loss_bom_items, create_process_loss_bom_items,
) )
qty = 4 qty = 10
scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG
source_warehouse = "Stores - _TC" source_warehouse = "Stores - _TC"
wip_warehouse = "_Test Warehouse - _TC" wip_warehouse = "_Test Warehouse - _TC"
fg_item_non_whole, _, bom_item = create_process_loss_bom_items() fg_item_non_whole, _, bom_item = create_process_loss_bom_items()
test_stock_entry.make_stock_entry( test_stock_entry.make_stock_entry(
item_code=bom_item.item_code, target=source_warehouse, qty=4, basic_rate=100 item_code=bom_item.item_code, target=source_warehouse, qty=qty, basic_rate=100
) )
bom_no = f"BOM-{fg_item_non_whole.item_code}-001" bom_no = f"BOM-{fg_item_non_whole.item_code}-001"
if not frappe.db.exists("BOM", bom_no): if not frappe.db.exists("BOM", bom_no):
bom_doc = create_bom_with_process_loss_item( bom_doc = create_bom_with_process_loss_item(
fg_item_non_whole, bom_item, scrap_qty=scrap_qty, scrap_rate=0, fg_qty=1, is_process_loss=1 fg_item_non_whole, bom_item, fg_qty=1, process_loss_percentage=10
) )
bom_doc.submit() bom_doc.submit()
@ -883,19 +883,15 @@ class TestWorkOrder(FrappeTestCase):
# Testing stock entry values # Testing stock entry values
items = se.get("items") items = se.get("items")
self.assertEqual(len(items), 3, "There should be 3 items including process loss.") self.assertEqual(len(items), 2, "There should be 3 items including process loss.")
fg_item = items[1]
source_item, fg_item, pl_item = items self.assertEqual(fg_item.qty, qty - 1)
self.assertEqual(se.process_loss_percentage, 10)
self.assertEqual(se.process_loss_qty, 1)
total_pl_qty = qty * scrap_qty wo.load_from_db()
actual_fg_qty = qty - total_pl_qty self.assertEqual(wo.status, "In Process")
self.assertEqual(pl_item.qty, total_pl_qty)
self.assertEqual(fg_item.qty, actual_fg_qty)
# Testing Work Order values
self.assertEqual(frappe.db.get_value("Work Order", wo.name, "produced_qty"), qty)
self.assertEqual(frappe.db.get_value("Work Order", wo.name, "process_loss_qty"), total_pl_qty)
@timeout(seconds=60) @timeout(seconds=60)
def test_job_card_scrap_item(self): def test_job_card_scrap_item(self):

View File

@ -14,13 +14,13 @@
"item_name", "item_name",
"image", "image",
"bom_no", "bom_no",
"sales_order",
"column_break1", "column_break1",
"company", "company",
"qty", "qty",
"material_transferred_for_manufacturing", "material_transferred_for_manufacturing",
"produced_qty", "produced_qty",
"process_loss_qty", "process_loss_qty",
"sales_order",
"project", "project",
"serial_no_and_batch_for_finished_good_section", "serial_no_and_batch_for_finished_good_section",
"has_serial_no", "has_serial_no",
@ -28,6 +28,7 @@
"column_break_17", "column_break_17",
"serial_no", "serial_no",
"batch_size", "batch_size",
"work_order_configuration",
"settings_section", "settings_section",
"allow_alternative_item", "allow_alternative_item",
"use_multi_level_bom", "use_multi_level_bom",
@ -42,7 +43,11 @@
"fg_warehouse", "fg_warehouse",
"scrap_warehouse", "scrap_warehouse",
"required_items_section", "required_items_section",
"materials_and_operations_tab",
"required_items", "required_items",
"operations_section",
"operations",
"transfer_material_against",
"time", "time",
"planned_start_date", "planned_start_date",
"planned_end_date", "planned_end_date",
@ -51,9 +56,6 @@
"actual_start_date", "actual_start_date",
"actual_end_date", "actual_end_date",
"lead_time", "lead_time",
"operations_section",
"transfer_material_against",
"operations",
"section_break_22", "section_break_22",
"planned_operating_cost", "planned_operating_cost",
"actual_operating_cost", "actual_operating_cost",
@ -72,12 +74,14 @@
"production_plan_item", "production_plan_item",
"production_plan_sub_assembly_item", "production_plan_sub_assembly_item",
"product_bundle_item", "product_bundle_item",
"amended_from" "amended_from",
"connections_tab"
], ],
"fields": [ "fields": [
{ {
"fieldname": "item", "fieldname": "item",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "Production Item",
"options": "fa fa-gift" "options": "fa fa-gift"
}, },
{ {
@ -236,7 +240,7 @@
{ {
"fieldname": "warehouses", "fieldname": "warehouses",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Warehouses", "label": "Warehouse",
"options": "fa fa-building" "options": "fa fa-building"
}, },
{ {
@ -390,8 +394,8 @@
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "more_info", "fieldname": "more_info",
"fieldtype": "Section Break", "fieldtype": "Tab Break",
"label": "More Information", "label": "More Info",
"options": "fa fa-file-text" "options": "fa fa-file-text"
}, },
{ {
@ -474,8 +478,7 @@
}, },
{ {
"fieldname": "settings_section", "fieldname": "settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Settings"
}, },
{ {
"fieldname": "column_break_18", "fieldname": "column_break_18",
@ -568,6 +571,22 @@
"no_copy": 1, "no_copy": 1,
"non_negative": 1, "non_negative": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "connections_tab",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
},
{
"fieldname": "work_order_configuration",
"fieldtype": "Tab Break",
"label": "Configuration"
},
{
"fieldname": "materials_and_operations_tab",
"fieldtype": "Tab Break",
"label": "Materials & Operations"
} }
], ],
"icon": "fa fa-cogs", "icon": "fa fa-cogs",
@ -575,7 +594,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-01-24 21:18:12.160114", "modified": "2023-01-03 14:16:35.427731",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@ -246,20 +246,10 @@ class WorkOrder(Document):
status = "Draft" status = "Draft"
elif self.docstatus == 1: elif self.docstatus == 1:
if status != "Stopped": if status != "Stopped":
stock_entries = frappe._dict(
frappe.db.sql(
"""select purpose, sum(fg_completed_qty)
from `tabStock Entry` where work_order=%s and docstatus=1
group by purpose""",
self.name,
)
)
status = "Not Started" status = "Not Started"
if stock_entries: if flt(self.material_transferred_for_manufacturing) > 0:
status = "In Process" status = "In Process"
produced_qty = stock_entries.get("Manufacture") if flt(self.produced_qty) >= flt(self.qty):
if flt(produced_qty) >= flt(self.qty):
status = "Completed" status = "Completed"
else: else:
status = "Cancelled" status = "Cancelled"
@ -285,14 +275,7 @@ class WorkOrder(Document):
): ):
continue continue
qty = flt( qty = self.get_transferred_or_manufactured_qty(purpose)
frappe.db.sql(
"""select sum(fg_completed_qty)
from `tabStock Entry` where work_order=%s and docstatus=1
and purpose=%s""",
(self.name, purpose),
)[0][0]
)
completed_qty = self.qty + (allowance_percentage / 100 * self.qty) completed_qty = self.qty + (allowance_percentage / 100 * self.qty)
if qty > completed_qty: if qty > completed_qty:
@ -314,26 +297,30 @@ class WorkOrder(Document):
if self.production_plan: if self.production_plan:
self.update_production_plan_status() self.update_production_plan_status()
def get_transferred_or_manufactured_qty(self, purpose):
table = frappe.qb.DocType("Stock Entry")
query = frappe.qb.from_(table).where(
(table.work_order == self.name) & (table.docstatus == 1) & (table.purpose == purpose)
)
if purpose == "Manufacture":
query = query.select(Sum(table.fg_completed_qty) - Sum(table.process_loss_qty))
else:
query = query.select(Sum(table.fg_completed_qty))
return flt(query.run()[0][0])
def set_process_loss_qty(self): def set_process_loss_qty(self):
process_loss_qty = flt( table = frappe.qb.DocType("Stock Entry")
frappe.db.sql( process_loss_qty = (
""" frappe.qb.from_(table)
SELECT sum(qty) FROM `tabStock Entry Detail` .select(Sum(table.process_loss_qty))
WHERE .where(
is_process_loss=1 (table.work_order == self.name) & (table.purpose == "Manufacture") & (table.docstatus == 1)
AND parent IN (
SELECT name FROM `tabStock Entry`
WHERE
work_order=%s
AND purpose='Manufacture'
AND docstatus=1
) )
""", ).run()[0][0]
(self.name,),
)[0][0] self.db_set("process_loss_qty", flt(process_loss_qty))
)
if process_loss_qty is not None:
self.db_set("process_loss_qty", process_loss_qty)
def update_production_plan_status(self): def update_production_plan_status(self):
production_plan = frappe.get_doc("Production Plan", self.production_plan) production_plan = frappe.get_doc("Production Plan", self.production_plan)
@ -352,6 +339,7 @@ class WorkOrder(Document):
produced_qty = total_qty[0][0] if total_qty else 0 produced_qty = total_qty[0][0] if total_qty else 0
self.update_status()
production_plan.run_method( production_plan.run_method(
"update_produced_pending_qty", produced_qty, self.production_plan_item "update_produced_pending_qty", produced_qty, self.production_plan_item
) )

View File

@ -268,6 +268,7 @@ erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v15_0.delete_taxjar_doctypes erpnext.patches.v15_0.delete_taxjar_doctypes
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
[post_model_sync] [post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings') execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
@ -319,3 +320,5 @@ erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization
erpnext.patches.v14_0.update_partial_tds_fields erpnext.patches.v14_0.update_partial_tds_fields
erpnext.patches.v14_0.create_incoterms_and_migrate_shipment erpnext.patches.v14_0.create_incoterms_and_migrate_shipment
erpnext.patches.v14_0.setup_clear_repost_logs erpnext.patches.v14_0.setup_clear_repost_logs
erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
erpnext.patches.v14_0.update_entry_type_for_journal_entry

View File

@ -0,0 +1,31 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def execute():
accounting_dimensions = frappe.db.get_all(
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
)
if not accounting_dimensions:
return
doctype = "Payment Request"
for d in accounting_dimensions:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
if field:
continue
df = {
"fieldname": d.fieldname,
"label": d.label,
"fieldtype": "Link",
"options": d.document_type,
"insert_after": "accounting_dimensions_section",
}
create_custom_field(doctype, df, ignore_validate=True)
frappe.clear_cache(doctype=doctype)

View File

@ -0,0 +1,18 @@
import frappe
def execute():
"""
Update Propery Setters for Journal Entry with new 'Entry Type'
"""
new_voucher_type = "Exchange Gain Or Loss"
prop_setter = frappe.db.get_list(
"Property Setter",
filters={"doc_type": "Journal Entry", "field_name": "voucher_type", "property": "options"},
)
if prop_setter:
property_setter_doc = frappe.get_doc("Property Setter", prop_setter[0].get("name"))
if new_voucher_type not in property_setter_doc.value.split("\n"):
property_setter_doc.value += "\n" + new_voucher_type
property_setter_doc.save()

View File

@ -0,0 +1,80 @@
import frappe
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
set_draft_asset_depr_schedule_details,
)
def execute():
frappe.reload_doc("assets", "doctype", "Asset Depreciation Schedule")
assets = get_details_of_draft_or_submitted_depreciable_assets()
for asset in assets:
finance_book_rows = get_details_of_asset_finance_books_rows(asset.name)
for fb_row in finance_book_rows:
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
set_draft_asset_depr_schedule_details(asset_depr_schedule_doc, asset, fb_row)
asset_depr_schedule_doc.insert()
if asset.docstatus == 1:
asset_depr_schedule_doc.submit()
update_depreciation_schedules(asset.name, asset_depr_schedule_doc.name, fb_row.idx)
def get_details_of_draft_or_submitted_depreciable_assets():
asset = frappe.qb.DocType("Asset")
records = (
frappe.qb.from_(asset)
.select(asset.name, asset.opening_accumulated_depreciation, asset.docstatus)
.where(asset.calculate_depreciation == 1)
.where(asset.docstatus < 2)
).run(as_dict=True)
return records
def get_details_of_asset_finance_books_rows(asset_name):
afb = frappe.qb.DocType("Asset Finance Book")
records = (
frappe.qb.from_(afb)
.select(
afb.finance_book,
afb.idx,
afb.depreciation_method,
afb.total_number_of_depreciations,
afb.frequency_of_depreciation,
afb.rate_of_depreciation,
afb.expected_value_after_useful_life,
)
.where(afb.parent == asset_name)
).run(as_dict=True)
return records
def update_depreciation_schedules(asset_name, asset_depr_schedule_name, fb_row_idx):
ds = frappe.qb.DocType("Depreciation Schedule")
depr_schedules = (
frappe.qb.from_(ds)
.select(ds.name)
.where((ds.parent == asset_name) & (ds.finance_book_id == str(fb_row_idx)))
.orderby(ds.idx)
).run(as_dict=True)
for idx, depr_schedule in enumerate(depr_schedules, start=1):
(
frappe.qb.update(ds)
.set(ds.idx, idx)
.set(ds.parent, asset_depr_schedule_name)
.set(ds.parentfield, "depreciation_schedule")
.set(ds.parenttype, "Asset Depreciation Schedule")
.where(ds.name == depr_schedule.name)
).run()

View File

@ -20,7 +20,7 @@ frappe.ui.form.on("Project", {
onload: function (frm) { onload: function (frm) {
const so = frm.get_docfield("sales_order"); const so = frm.get_docfield("sales_order");
so.get_route_options_for_new_doc = () => { so.get_route_options_for_new_doc = () => {
if (frm.is_new()) return; if (frm.is_new()) return {};
return { return {
"customer": frm.doc.customer, "customer": frm.doc.customer,
"project_name": frm.doc.name "project_name": frm.doc.name

View File

@ -7,6 +7,8 @@ from email_reply_parser import EmailReplyParser
from frappe import _ from frappe import _
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Interval
from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
from erpnext import get_default_company from erpnext import get_default_company
@ -297,17 +299,19 @@ class Project(Document):
user.welcome_email_sent = 1 user.welcome_email_sent = 1
def get_timeline_data(doctype, name): def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
"""Return timeline for attendance""" """Return timeline for attendance"""
timesheet_detail = frappe.qb.DocType("Timesheet Detail")
return dict( return dict(
frappe.db.sql( frappe.qb.from_(timesheet_detail)
"""select unix_timestamp(from_time), count(*) .select(UnixTimestamp(timesheet_detail.from_time), Count("*"))
from `tabTimesheet Detail` where project=%s .where(timesheet_detail.project == name)
and from_time > date_sub(curdate(), interval 1 year) .where(timesheet_detail.from_time > CurDate() - Interval(years=1))
and docstatus < 2 .where(timesheet_detail.docstatus < 2)
group by date(from_time)""", .groupby(Date(timesheet_detail.from_time))
name, .run()
)
) )

View File

@ -25,12 +25,18 @@ class Timesheet(Document):
def validate(self): def validate(self):
self.set_status() self.set_status()
self.validate_dates() self.validate_dates()
self.calculate_hours()
self.validate_time_logs() self.validate_time_logs()
self.update_cost() self.update_cost()
self.calculate_total_amounts() self.calculate_total_amounts()
self.calculate_percentage_billed() self.calculate_percentage_billed()
self.set_dates() self.set_dates()
def calculate_hours(self):
for row in self.time_logs:
if row.to_time and row.from_time:
row.hours = time_diff_in_hours(row.to_time, row.from_time)
def calculate_total_amounts(self): def calculate_total_amounts(self):
self.total_hours = 0.0 self.total_hours = 0.0
self.total_billable_hours = 0.0 self.total_billable_hours = 0.0

View File

@ -355,12 +355,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "deposit", fieldname: "deposit",
fieldtype: "Currency", fieldtype: "Currency",
label: "Deposit", label: "Deposit",
options: "currency",
read_only: 1, read_only: 1,
}, },
{ {
fieldname: "withdrawal", fieldname: "withdrawal",
fieldtype: "Currency", fieldtype: "Currency",
label: "Withdrawal", label: "Withdrawal",
options: "currency",
read_only: 1, read_only: 1,
}, },
{ {
@ -378,6 +380,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "allocated_amount", fieldname: "allocated_amount",
fieldtype: "Currency", fieldtype: "Currency",
label: "Allocated Amount", label: "Allocated Amount",
options: "Currency",
read_only: 1, read_only: 1,
}, },
@ -385,8 +388,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
fieldname: "unallocated_amount", fieldname: "unallocated_amount",
fieldtype: "Currency", fieldtype: "Currency",
label: "Unallocated Amount", label: "Unallocated Amount",
options: "Currency",
read_only: 1, read_only: 1,
}, },
{
fieldname: "currency",
fieldtype: "Link",
label: "Currency",
options: "Currency",
read_only: 1,
hidden: 1,
}
]; ];
} }

View File

@ -225,7 +225,8 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
args: { args: {
item_code: item.item_code, item_code: item.item_code,
warehouse: item.warehouse, warehouse: item.warehouse,
company: doc.company company: doc.company,
include_child_warehouses: true
} }
}); });
} }

View File

@ -272,7 +272,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection"); let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function(row) { quality_inspection_field.get_route_options_for_new_doc = function(row) {
if(me.frm.is_new()) return; if(me.frm.is_new()) return {};
return { return {
"inspection_type": inspection_type, "inspection_type": inspection_type,
"reference_type": me.frm.doc.doctype, "reference_type": me.frm.doc.doctype,

View File

@ -737,7 +737,7 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil
qb.from_(con) qb.from_(con)
.join(dlink) .join(dlink)
.on(con.name == dlink.parent) .on(con.name == dlink.parent)
.select(con.name, con.full_name, con.email_id) .select(con.name, con.email_id)
.where((dlink.link_name == customer) & (con.name.like(f"%{txt}%"))) .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%")))
.run() .run()
) )

View File

@ -90,7 +90,6 @@
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Item", "options": "Item",
"print_width": "150px", "print_width": "150px",
"reqd": 1,
"search_index": 1, "search_index": 1,
"width": "150px" "width": "150px"
}, },
@ -649,7 +648,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-07-15 12:40:51.074820", "modified": "2022-12-25 02:49:53.926625",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation Item", "name": "Quotation Item",

View File

@ -1024,6 +1024,15 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
] ]
items_to_map = list(set(items_to_map)) items_to_map = list(set(items_to_map))
def is_drop_ship_order(target):
drop_ship = True
for item in target.items:
if not item.delivered_by_supplier:
drop_ship = False
break
return drop_ship
def set_missing_values(source, target): def set_missing_values(source, target):
target.supplier = "" target.supplier = ""
target.apply_discount_on = "" target.apply_discount_on = ""
@ -1031,8 +1040,14 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.discount_amount = 0.0 target.discount_amount = 0.0
target.inter_company_order_reference = "" target.inter_company_order_reference = ""
target.shipping_rule = "" target.shipping_rule = ""
target.customer = ""
target.customer_name = "" if is_drop_ship_order(target):
target.customer = source.customer
target.customer_name = source.customer_name
target.shipping_address = source.shipping_address_name
else:
target.customer = target.customer_name = target.shipping_address = None
target.run_method("set_missing_values") target.run_method("set_missing_values")
target.run_method("calculate_taxes_and_totals") target.run_method("calculate_taxes_and_totals")

View File

@ -114,7 +114,6 @@
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Item", "options": "Item",
"print_width": "150px", "print_width": "150px",
"reqd": 1,
"width": "150px" "width": "150px"
}, },
{ {
@ -865,7 +864,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-18 11:39:01.741665", "modified": "2022-12-25 02:51:10.247569",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@ -139,10 +139,11 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2021-02-08 17:01:52.162202", "modified": "2022-12-24 11:15:17.142746",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Customer Group", "name": "Customer Group",
"naming_rule": "By fieldname",
"nsm_parent_field": "parent_customer_group", "nsm_parent_field": "parent_customer_group",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
@ -198,10 +199,19 @@
"role": "Customer", "role": "Customer",
"select": 1, "select": 1,
"share": 1 "share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1
} }
], ],
"search_fields": "parent_customer_group", "search_fields": "parent_customer_group",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -123,6 +123,7 @@
"fieldname": "route", "fieldname": "route",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Route", "label": "Route",
"no_copy": 1,
"unique": 1 "unique": 1
}, },
{ {
@ -232,11 +233,10 @@
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"max_attachments": 3, "max_attachments": 3,
"modified": "2022-03-09 12:27:11.055782", "modified": "2023-01-05 12:21:30.458628",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Item Group", "name": "Item Group",
"name_case": "Title Case",
"naming_rule": "By fieldname", "naming_rule": "By fieldname",
"nsm_parent_field": "parent_item_group", "nsm_parent_field": "parent_item_group",
"owner": "Administrator", "owner": "Administrator",

View File

@ -2,8 +2,13 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from collections import defaultdict
from itertools import chain
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder import Interval
from frappe.query_builder.functions import Count, CurDate, UnixTimestamp
from frappe.utils import flt from frappe.utils import flt
from frappe.utils.nestedset import NestedSet, get_root_of from frappe.utils.nestedset import NestedSet, get_root_of
@ -77,61 +82,31 @@ def on_doctype_update():
frappe.db.add_index("Sales Person", ["lft", "rgt"]) frappe.db.add_index("Sales Person", ["lft", "rgt"])
def get_timeline_data(doctype, name): def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
def _fetch_activity(doctype: str, date_field: str):
sales_team = frappe.qb.DocType("Sales Team")
transaction = frappe.qb.DocType(doctype)
out = {} return dict(
frappe.qb.from_(transaction)
out.update( .join(sales_team)
dict( .on(transaction.name == sales_team.parent)
frappe.db.sql( .select(UnixTimestamp(transaction[date_field]), Count("*"))
"""select .where(sales_team.sales_person == name)
unix_timestamp(dt.transaction_date), count(st.parenttype) .where(transaction[date_field] > CurDate() - Interval(years=1))
from .groupby(transaction[date_field])
`tabSales Order` dt, `tabSales Team` st .run()
where
st.sales_person = %s and st.parent = dt.name and dt.transaction_date > date_sub(curdate(), interval 1 year)
group by dt.transaction_date """,
name,
)
)
) )
sales_invoice = dict( sales_order_activity = _fetch_activity("Sales Order", "transaction_date")
frappe.db.sql( sales_invoice_activity = _fetch_activity("Sales Invoice", "posting_date")
"""select delivery_note_activity = _fetch_activity("Delivery Note", "posting_date")
unix_timestamp(dt.posting_date), count(st.parenttype)
from
`tabSales Invoice` dt, `tabSales Team` st
where
st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year)
group by dt.posting_date """,
name,
)
)
for key in sales_invoice: merged_activities = defaultdict(int)
if out.get(key):
out[key] += sales_invoice[key]
else:
out[key] = sales_invoice[key]
delivery_note = dict( for ts, count in chain(
frappe.db.sql( sales_order_activity.items(), sales_invoice_activity.items(), delivery_note_activity.items()
"""select ):
unix_timestamp(dt.posting_date), count(st.parenttype) merged_activities[ts] += count
from
`tabDelivery Note` dt, `tabSales Team` st
where
st.sales_person = %s and st.parent = dt.name and dt.posting_date > date_sub(curdate(), interval 1 year)
group by dt.posting_date """,
name,
)
)
for key in delivery_note: return merged_activities
if out.get(key):
out[key] += delivery_note[key]
else:
out[key] = delivery_note[key]
return out

View File

@ -6,6 +6,7 @@
"creation": "2013-01-10 16:34:24", "creation": "2013-01-10 16:34:24",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"supplier_group_name", "supplier_group_name",
"parent_supplier_group", "parent_supplier_group",
@ -106,10 +107,11 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-03-18 18:10:49.228407", "modified": "2022-12-24 11:16:12.486719",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Supplier Group", "name": "Supplier Group",
"naming_rule": "By fieldname",
"nsm_parent_field": "parent_supplier_group", "nsm_parent_field": "parent_supplier_group",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
@ -156,8 +158,18 @@
"permlevel": 1, "permlevel": 1,
"read": 1, "read": 1,
"role": "Purchase User" "role": "Purchase User"
},
{
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1
} }
], ],
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_order": "ASC" "sort_field": "modified",
"sort_order": "ASC",
"states": []
} }

View File

@ -123,11 +123,12 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2021-02-08 17:10:03.767426", "modified": "2022-12-24 11:16:39.964956",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Territory", "name": "Territory",
"name_case": "Title Case", "name_case": "Title Case",
"naming_rule": "By fieldname",
"nsm_parent_field": "parent_territory", "nsm_parent_field": "parent_territory",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
@ -175,10 +176,19 @@
"role": "Customer", "role": "Customer",
"select": 1, "select": 1,
"share": 1 "share": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1
} }
], ],
"search_fields": "parent_territory,territory_manager", "search_fields": "parent_territory,territory_manager",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": []
} }

View File

@ -102,6 +102,9 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
args: args, args: args,
callback: function (r) { callback: function (r) {
me.render(r.message); me.render(r.message);
if(me.after_refresh) {
me.after_refresh();
}
} }
}); });
} }

View File

@ -22,7 +22,6 @@
"allow_alternative_item", "allow_alternative_item",
"is_stock_item", "is_stock_item",
"has_variants", "has_variants",
"include_item_in_manufacturing",
"opening_stock", "opening_stock",
"valuation_rate", "valuation_rate",
"standard_rate", "standard_rate",
@ -112,6 +111,7 @@
"quality_inspection_template", "quality_inspection_template",
"inspection_required_before_delivery", "inspection_required_before_delivery",
"manufacturing", "manufacturing",
"include_item_in_manufacturing",
"is_sub_contracted_item", "is_sub_contracted_item",
"default_bom", "default_bom",
"column_break_74", "column_break_74",
@ -911,7 +911,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2022-09-13 04:08:17.431731", "modified": "2023-01-07 22:45:00.341745",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -8,6 +8,8 @@ from typing import Dict, List, Optional
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Interval
from frappe.query_builder.functions import Count, CurDate, UnixTimestamp
from frappe.utils import ( from frappe.utils import (
cint, cint,
cstr, cstr,
@ -997,18 +999,19 @@ def make_item_price(item, price_list_name, item_price):
).insert() ).insert()
def get_timeline_data(doctype, name): def get_timeline_data(doctype: str, name: str) -> dict[int, int]:
"""get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page.""" """get timeline data based on Stock Ledger Entry. This is displayed as heatmap on the item page."""
items = frappe.db.sql( sle = frappe.qb.DocType("Stock Ledger Entry")
"""select unix_timestamp(posting_date), count(*)
from `tabStock Ledger Entry`
where item_code=%s and posting_date > date_sub(curdate(), interval 1 year)
group by posting_date""",
name,
)
return dict(items) return dict(
frappe.qb.from_(sle)
.select(UnixTimestamp(sle.posting_date), Count("*"))
.where(sle.item_code == name)
.where(sle.posting_date > CurDate() - Interval(years=1))
.groupby(sle.posting_date)
.run()
)
def validate_end_of_life(item_code, end_of_life=None, disabled=None): def validate_end_of_life(item_code, end_of_life=None, disabled=None):

View File

@ -83,6 +83,7 @@ class TestItem(FrappeTestCase):
def test_get_item_details(self): def test_get_item_details(self):
# delete modified item price record and make as per test_records # delete modified item price record and make as per test_records
frappe.db.sql("""delete from `tabItem Price`""") frappe.db.sql("""delete from `tabItem Price`""")
frappe.db.sql("""delete from `tabBin`""")
to_check = { to_check = {
"item_code": "_Test Item", "item_code": "_Test Item",
@ -103,9 +104,26 @@ class TestItem(FrappeTestCase):
"batch_no": None, "batch_no": None,
"uom": "_Test UOM", "uom": "_Test UOM",
"conversion_factor": 1.0, "conversion_factor": 1.0,
"reserved_qty": 1,
"actual_qty": 5,
"ordered_qty": 10,
"projected_qty": 14,
} }
make_test_objects("Item Price") make_test_objects("Item Price")
make_test_objects(
"Bin",
[
{
"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC",
"reserved_qty": 1,
"actual_qty": 5,
"ordered_qty": 10,
"projected_qty": 14,
}
],
)
company = "_Test Company" company = "_Test Company"
currency = frappe.get_cached_value("Company", company, "default_currency") currency = frappe.get_cached_value("Company", company, "default_currency")
@ -129,7 +147,7 @@ class TestItem(FrappeTestCase):
) )
for key, value in to_check.items(): for key, value in to_check.items():
self.assertEqual(value, details.get(key)) self.assertEqual(value, details.get(key), key)
def test_item_tax_template(self): def test_item_tax_template(self):
expected_item_tax_template = [ expected_item_tax_template = [

View File

@ -51,7 +51,15 @@ frappe.ui.form.on('Pick List', {
if (!(frm.doc.locations && frm.doc.locations.length)) { if (!(frm.doc.locations && frm.doc.locations.length)) {
frappe.msgprint(__('Add items in the Item Locations table')); frappe.msgprint(__('Add items in the Item Locations table'));
} else { } else {
frm.call('set_item_locations', {save: save}); frappe.call({
method: "set_item_locations",
doc: frm.doc,
args: {
"save": save,
},
freeze: 1,
freeze_message: __("Setting Item Locations..."),
});
} }
}, },
get_item_locations: (frm) => { get_item_locations: (frm) => {

View File

@ -100,6 +100,7 @@ class PickList(Document):
item_table, item_table,
item.sales_order_item, item.sales_order_item,
["picked_qty", stock_qty_field], ["picked_qty", stock_qty_field],
for_update=True,
) )
if self.docstatus == 1: if self.docstatus == 1:
@ -118,7 +119,7 @@ class PickList(Document):
def update_sales_order_picking_status(sales_orders: Set[str]) -> None: def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
for sales_order in sales_orders: for sales_order in sales_orders:
if sales_order: if sales_order:
frappe.get_doc("Sales Order", sales_order).update_picking_status() frappe.get_doc("Sales Order", sales_order, for_update=True).update_picking_status()
@frappe.whitelist() @frappe.whitelist()
def set_item_locations(self, save=False): def set_item_locations(self, save=False):
@ -135,6 +136,7 @@ class PickList(Document):
# reset # reset
self.delete_key("locations") self.delete_key("locations")
updated_locations = frappe._dict()
for item_doc in items: for item_doc in items:
item_code = item_doc.item_code item_code = item_doc.item_code
@ -155,6 +157,25 @@ class PickList(Document):
for row in locations: for row in locations:
location = item_doc.as_dict() location = item_doc.as_dict()
location.update(row) location.update(row)
key = (
location.item_code,
location.warehouse,
location.uom,
location.batch_no,
location.serial_no,
location.sales_order_item or location.material_request_item,
)
if key not in updated_locations:
updated_locations.setdefault(key, location)
else:
updated_locations[key].qty += location.qty
updated_locations[key].stock_qty += location.stock_qty
for location in updated_locations.values():
if location.picked_qty > location.stock_qty:
location.picked_qty = location.stock_qty
self.append("locations", location) self.append("locations", location)
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red # If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
@ -242,7 +263,7 @@ class PickList(Document):
for so_row, item_code in product_bundles.items(): for so_row, item_code in product_bundles.items():
picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code]) picked_qty = self._compute_picked_qty_for_bundle(so_row, product_bundle_qty_map[item_code])
item_table = "Sales Order Item" item_table = "Sales Order Item"
already_picked = frappe.db.get_value(item_table, so_row, "picked_qty") already_picked = frappe.db.get_value(item_table, so_row, "picked_qty", for_update=True)
frappe.db.set_value( frappe.db.set_value(
item_table, item_table,
so_row, so_row,
@ -441,7 +462,7 @@ def get_available_item_locations_for_batched_item(
sle.`batch_no`, sle.`batch_no`,
sle.`item_code` sle.`item_code`
HAVING `qty` > 0 HAVING `qty` > 0
ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation` ORDER BY IFNULL(batch.`expiry_date`, '2200-01-01'), batch.`creation`, sle.`batch_no`, sle.`warehouse`
""".format( """.format(
warehouse_condition=warehouse_condition warehouse_condition=warehouse_condition
), ),

View File

@ -112,6 +112,10 @@ frappe.ui.form.on('Stock Entry', {
} }
}); });
attach_bom_items(frm.doc.bom_no); attach_bom_items(frm.doc.bom_no);
if(!check_should_not_attach_bom_items(frm.doc.bom_no)) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
}, },
setup_quality_inspection: function(frm) { setup_quality_inspection: function(frm) {
@ -129,7 +133,7 @@ frappe.ui.form.on('Stock Entry', {
let quality_inspection_field = frm.get_docfield("items", "quality_inspection"); let quality_inspection_field = frm.get_docfield("items", "quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function(row) { quality_inspection_field.get_route_options_for_new_doc = function(row) {
if (frm.is_new()) return; if (frm.is_new()) return {};
return { return {
"inspection_type": "Incoming", "inspection_type": "Incoming",
"reference_type": frm.doc.doctype, "reference_type": frm.doc.doctype,
@ -326,7 +330,11 @@ frappe.ui.form.on('Stock Entry', {
} }
frm.trigger("setup_quality_inspection"); frm.trigger("setup_quality_inspection");
attach_bom_items(frm.doc.bom_no) attach_bom_items(frm.doc.bom_no);
if(!check_should_not_attach_bom_items(frm.doc.bom_no)) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
}, },
before_save: function(frm) { before_save: function(frm) {
@ -939,7 +947,10 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
method: "get_items", method: "get_items",
callback: function(r) { callback: function(r) {
if(!r.exc) refresh_field("items"); if(!r.exc) refresh_field("items");
if(me.frm.doc.bom_no) attach_bom_items(me.frm.doc.bom_no) if(me.frm.doc.bom_no) {
attach_bom_items(me.frm.doc.bom_no);
erpnext.accounts.dimensions.update_dimension(me.frm, me.frm.doctype);
}
} }
}); });
} }

Some files were not shown because too many files have changed in this diff Show More