Merge pull request #33271 from sonali8848/filters-on-bank-reconciliation
feat: Date filters on bank reconciliation tool
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 2b3d8cb..0c71b41 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -41,12 +41,17 @@
install_whktml() {
- wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
- sudo chmod o+x /usr/local/bin/wkhtmltopdf
+ if [ "$(lsb_release -rs)" = "22.04" ]; then
+ 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 apt install /tmp/wkhtmltox.deb
+ else
+ echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
+ exit 1
+ fi
}
install_whktml &
+wkpid=$!
+
cd ~/frappe-bench || exit
@@ -60,6 +65,8 @@
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
+wait $wkpid
+
bench start &> bench_run_logs.txt &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes
diff --git a/.github/helper/site_config_mariadb.json b/.github/helper/site_config_mariadb.json
index 49e7fcf..8c86f73 100644
--- a/.github/helper/site_config_mariadb.json
+++ b/.github/helper/site_config_mariadb.json
@@ -11,6 +11,6 @@
"root_login": "root",
"root_password": "root",
"host_name": "http://test_site:8000",
- "install_apps": ["erpnext"],
+ "install_apps": ["payments", "erpnext"],
"throttle_user_limit": 100
}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5a46002..37bb37e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -13,10 +13,10 @@
with:
fetch-depth: 0
persist-credentials: false
- - name: Setup Node.js v14
+ - name: Setup Node.js
uses: actions/setup-node@v2
with:
- node-version: 14
+ node-version: 18
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
@@ -28,4 +28,4 @@
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
- run: npx semantic-release
\ No newline at end of file
+ run: npx semantic-release
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index bbb8a7e..c70c76f 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -16,12 +16,12 @@
workflow_dispatch:
inputs:
user:
- description: 'user'
+ description: 'Frappe Framework repository user (add your username for forks)'
required: true
default: 'frappe'
type: string
branch:
- description: 'Branch name'
+ description: 'Frappe Framework branch'
default: 'develop'
required: false
type: string
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index f319003..45e04ee 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -378,7 +378,7 @@
return
# 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))
if via_journal_entry:
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js
index ceba99a..71f2dcc 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.js
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.js
@@ -37,14 +37,11 @@
refresh: function(frm) {
frm.disable_save();
+ frm.add_custom_button(__('Get Payment Entries'), () =>
+ frm.trigger("get_payment_entries")
+ );
- if (frm.doc.account && frm.doc.from_date && frm.doc.to_date) {
- frm.add_custom_button(__('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) {
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index 558a107..4ba6146 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -377,7 +377,7 @@
dict(
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,
)
gl_amount, transaction_amount = (
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index a788514..9b36c93 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -137,7 +137,7 @@
)
elif doc.payment_type == "Pay":
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(
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 325a356..220b747 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -485,6 +485,10 @@
"default_payable_account": frappe.db.get_value(
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
),
+ "default_provisional_account": frappe.db.get_value(
+ "Account",
+ {"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0},
+ ),
}
)
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
index 926a442..f72ecc9 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.js
@@ -26,7 +26,7 @@
doc: frm.doc,
callback: function(r) {
if (r.message) {
- frm.add_custom_button(__('Journal Entry'), function() {
+ frm.add_custom_button(__('Journal Entries'), function() {
return frm.events.make_jv(frm);
}, __('Create'));
}
@@ -35,10 +35,11 @@
}
},
- get_entries: function(frm) {
+ get_entries: function(frm, account) {
frappe.call({
method: "get_accounts_data",
doc: cur_frm.doc,
+ account: account,
callback: function(r){
frappe.model.clear_table(frm.doc, "accounts");
if(r.message) {
@@ -57,7 +58,6 @@
let total_gain_loss = 0;
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));
});
@@ -66,13 +66,19 @@
},
make_jv : function(frm) {
+ let revaluation_journal = null;
+ let zero_balance_journal = null;
frappe.call({
- method: "make_jv_entry",
+ method: "make_jv_entries",
doc: frm.doc,
+ freeze: true,
+ freeze_message: "Making Journal Entries...",
callback: function(r){
if (r.message) {
- var doc = frappe.model.sync(r.message)[0];
- frappe.set_route("Form", doc.doctype, doc.name);
+ let response = r.message;
+ if(response['revaluation_jv'] || response['zero_balance_jv']) {
+ frappe.msgprint(__("Journals have been created"));
+ }
}
}
});
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json
index e00b17e..0d198ca 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.json
@@ -14,6 +14,9 @@
"get_entries",
"accounts",
"section_break_6",
+ "gain_loss_unbooked",
+ "gain_loss_booked",
+ "column_break_10",
"total_gain_loss",
"amended_from"
],
@@ -60,13 +63,6 @@
"fieldtype": "Section Break"
},
{
- "fieldname": "total_gain_loss",
- "fieldtype": "Currency",
- "label": "Total Gain/Loss",
- "options": "Company:company:default_currency",
- "read_only": 1
- },
- {
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
@@ -74,11 +70,37 @@
"options": "Exchange Rate Revaluation",
"print_hide": 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,
"links": [],
- "modified": "2022-11-17 10:28:03.911554",
+ "modified": "2022-12-29 19:38:24.416529",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation",
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 68e828b..d67d59b 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -3,10 +3,12 @@
import frappe
-from frappe import _
+from frappe import _, qb
from frappe.model.document import Document
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
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
@@ -19,11 +21,25 @@
def set_total_gain_loss(self):
total_gain_loss = 0
+
+ gain_loss_booked = 0
+ gain_loss_unbooked = 0
+
for d in self.accounts:
- d.gain_loss = flt(
- 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"))
+ if not d.zero_balance:
+ d.gain_loss = flt(
+ 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"))
+
+ 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"))
+
+ 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"))
def validate_mandatory(self):
@@ -35,98 +51,206 @@
@frappe.whitelist()
def check_journal_entry_condition(self):
- total_debit = frappe.db.get_value(
- "Journal Entry Account",
- {"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1},
- "sum(debit) as sum",
+ exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
+
+ jea = qb.DocType("Journal Entry Account")
+ 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
- for d in self.accounts:
- total_amt = total_amt + d.new_balance_in_base_currency
+ if journals:
+ gle = qb.DocType("GL Entry")
+ 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:
- return True
+ if total_amt and total_amt[0][0] != self.total_gain_loss:
+ return True
+ else:
+ return False
- return False
+ return True
@frappe.whitelist()
- def get_accounts_data(self, account=None):
- accounts = []
+ def get_accounts_data(self):
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(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
)
- account_details = self.get_accounts_from_gle()
- for d in account_details:
- current_exchange_rate = (
- 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_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)
- 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,
- "current_exchange_rate": current_exchange_rate,
- "new_exchange_rate": new_exchange_rate,
- "new_balance_in_base_currency": new_balance_in_base_currency,
- }
+ if 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 = (
+ 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, posting_date)
+ 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)
+ 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": d.balance_in_account_currency,
+ "gain_loss": gain_loss,
+ }
+ )
- if not accounts:
- self.throw_invalid_response_message(account_details)
+ # Handle Accounts with '0' balance in Account/Base Currency
+ 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
- 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):
if account_details:
message = _("No outstanding invoices require exchange rate revaluation")
@@ -134,11 +258,7 @@
message = _("No outstanding invoices found")
frappe.msgprint(message)
- @frappe.whitelist()
- def make_jv_entry(self):
- if self.total_gain_loss == 0:
- return
-
+ def get_for_unrealized_gain_loss_account(self):
unrealized_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "unrealized_exchange_gain_loss_account"
)
@@ -146,6 +266,130 @@
frappe.throw(
_("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.voucher_type = "Exchange Rate Revaluation"
@@ -154,7 +398,7 @@
journal_entry.multi_currency = 1
journal_entry_accounts = []
- for d in self.accounts:
+ for d in accounts:
dr_or_cr = (
"debit_in_account_currency"
if d.get("balance_in_account_currency") > 0
@@ -179,6 +423,7 @@
dr_or_cr: flt(
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")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@@ -196,6 +441,7 @@
reverse_dr_or_cr: flt(
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")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@@ -206,8 +452,11 @@
{
"account": 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,
- "credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0,
+ "debit_in_account_currency": abs(self.gain_loss_unbooked)
+ 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,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@@ -217,42 +466,90 @@
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
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()
-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", account, ["account_currency", "account_type"]
)
+
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))
account_details = {}
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_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:
- balance_in_account_currency = get_balance_on(
- account, date=posting_date, party_type=party_type, party=party
+ if account_balance and (
+ account_balance[0].balance or account_balance[0].balance_in_account_currency
+ ):
+ account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
+ company, posting_date, account_balance
)
- current_exchange_rate = (
- balance / balance_in_account_currency if balance_in_account_currency else 0
- )
- 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(
+ row = account_with_new_balance[0]
+ account_details.update(
{
- "balance_in_base_currency": balance,
- "balance_in_account_currency": balance_in_account_currency,
- "current_exchange_rate": current_exchange_rate,
- "new_exchange_rate": new_exchange_rate,
- "new_balance_in_base_currency": new_balance_in_base_currency,
+ "balance_in_base_currency": row["balance_in_base_currency"],
+ "balance_in_account_currency": row["balance_in_account_currency"],
+ "current_exchange_rate": row["current_exchange_rate"],
+ "new_exchange_rate": row["new_exchange_rate"],
+ "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"],
}
)
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json
index 80e972b..2968359 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation_account/exchange_rate_revaluation_account.json
@@ -10,14 +10,21 @@
"party",
"column_break_2",
"account_currency",
+ "account_balances",
"balance_in_account_currency",
+ "column_break_46yz",
+ "new_balance_in_account_currency",
"balances",
"current_exchange_rate",
- "balance_in_base_currency",
- "column_break_9",
+ "column_break_xown",
"new_exchange_rate",
+ "column_break_9",
+ "balance_in_base_currency",
+ "column_break_ukce",
"new_balance_in_base_currency",
- "gain_loss"
+ "section_break_ngrs",
+ "gain_loss",
+ "zero_balance"
],
"fields": [
{
@@ -78,7 +85,7 @@
},
{
"fieldname": "column_break_9",
- "fieldtype": "Column Break"
+ "fieldtype": "Section Break"
},
{
"fieldname": "new_exchange_rate",
@@ -102,11 +109,45 @@
"label": "Gain/Loss",
"options": "Company:company:default_currency",
"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,
"links": [],
- "modified": "2022-11-17 10:26:18.302728",
+ "modified": "2022-12-29 19:38:52.915295",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation Account",
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index f312048..f07a4fa 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -95,7 +95,15 @@
)
# 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(
_("{0} {1}: Either debit or credit amount is required for {2}").format(
self.voucher_type, self.voucher_no, self.account
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 8e5ba37..3f69d5c 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -88,7 +88,7 @@
"label": "Entry Type",
"oldfieldname": "voucher_type",
"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,
"search_index": 1
},
@@ -539,7 +539,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2022-06-23 22:01:32.348337",
+ "modified": "2022-11-28 17:40:01.241908",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 1714fff..ea8b7d8 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -6,7 +6,7 @@
import frappe
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
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
@@ -23,6 +23,9 @@
get_stock_accounts,
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
@@ -34,9 +37,6 @@
def __init__(self, *args, **kwargs):
super(JournalEntry, self).__init__(*args, **kwargs)
- def get_feed(self):
- return self.voucher_type
-
def validate(self):
if self.voucher_type == "Opening Entry":
self.is_opening = "Yes"
@@ -286,16 +286,17 @@
for d in self.get("accounts"):
if d.reference_type == "Asset" and d.reference_name:
asset = frappe.get_doc("Asset", d.reference_name)
- for s in asset.get("schedules"):
- if s.journal_entry == self.name:
- s.db_set("journal_entry", None)
+ for row in asset.get("finance_books"):
+ depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
- idx = cint(s.finance_book_id) or 1
- finance_books = asset.get("finance_books")[idx - 1]
- finance_books.value_after_depreciation += s.depreciation_amount
- finance_books.db_update()
+ for s in depr_schedule or []:
+ if s.journal_entry == self.name:
+ s.db_set("journal_entry", None)
- asset.set_status()
+ row.value_after_depreciation += s.depreciation_amount
+ row.db_update()
+
+ asset.set_status()
def unlink_inter_company_jv(self):
if (
@@ -592,28 +593,30 @@
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else:
for d in self.get("accounts"):
- if flt(d.debit > 0):
+ if flt(d.debit) > 0:
accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0:
accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
- if flt(d.debit > 0):
+ if flt(d.debit) > 0:
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)))
def validate_debit_credit_amount(self):
- for d in self.get("accounts"):
- 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))
+ if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
+ for d in self.get("accounts"):
+ 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))
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
- if self.difference:
- frappe.throw(
- _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
- )
+ if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
+ if self.difference:
+ frappe.throw(
+ _("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
+ )
def set_total_debit_credit(self):
self.total_debit, self.total_credit, self.difference = 0, 0, 0
@@ -651,16 +654,17 @@
self.set_exchange_rate()
def set_amounts_in_company_currency(self):
- for d in self.get("accounts"):
- d.debit_in_account_currency = flt(
- d.debit_in_account_currency, d.precision("debit_in_account_currency")
- )
- d.credit_in_account_currency = flt(
- d.credit_in_account_currency, d.precision("credit_in_account_currency")
- )
+ if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
+ for d in self.get("accounts"):
+ d.debit_in_account_currency = flt(
+ d.debit_in_account_currency, d.precision("debit_in_account_currency")
+ )
+ d.credit_in_account_currency = flt(
+ d.credit_in_account_currency, d.precision("credit_in_account_currency")
+ )
- d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
- d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
+ d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
+ d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
def set_exchange_rate(self):
for d in self.get("accounts"):
@@ -759,7 +763,7 @@
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
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
@@ -789,7 +793,7 @@
def build_gl_map(self):
gl_map = []
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 = [x for x in r if x]
remarks = "\n".join(r)
@@ -837,7 +841,7 @@
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist()
- def get_balance(self):
+ def get_balance(self, difference_account=None):
if not self.get("accounts"):
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
else:
@@ -852,7 +856,13 @@
blank_row = d
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
if diff > 0:
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 3fc1adf..4a7a57b 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -305,6 +305,7 @@
"fieldname": "source_exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
+ "precision": "9",
"print_hide": 1,
"reqd": 1
},
@@ -334,6 +335,7 @@
"fieldname": "target_exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
+ "precision": "9",
"print_hide": 1,
"reqd": 1
},
@@ -731,7 +733,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-02-23 20:08:39.559814",
+ "modified": "2022-12-08 16:25:43.824051",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 9354e44..1cccbd9 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -247,7 +247,7 @@
self.set_target_exchange_rate(ref_doc)
def set_source_exchange_rate(self, ref_doc=None):
- if self.paid_from and not self.source_exchange_rate:
+ if self.paid_from:
if self.paid_from_account_currency == self.company_currency:
self.source_exchange_rate = 1
else:
@@ -622,7 +622,7 @@
self.payment_type == "Receive"
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
and self.total_allocated_amount
- < self.paid_amount + (total_deductions / self.source_exchange_rate)
+ < flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
):
self.unallocated_amount = (
self.base_received_amount + total_deductions - self.base_total_allocated_amount
@@ -632,7 +632,7 @@
self.payment_type == "Pay"
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
and self.total_allocated_amount
- < self.received_amount + (total_deductions / self.target_exchange_rate)
+ < flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
):
self.unallocated_amount = (
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
@@ -684,35 +684,34 @@
)
def validate_payment_against_negative_invoice(self):
- if (self.payment_type == "Pay" and self.party_type == "Customer") or (
- self.payment_type == "Receive" and self.party_type == "Supplier"
+ if (self.payment_type != "Pay" or self.party_type != "Customer") and (
+ self.payment_type != "Receive" or self.party_type != "Supplier"
):
+ return
- total_negative_outstanding = sum(
- abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
+ total_negative_outstanding = sum(
+ abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
+ )
+
+ paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
+ additional_charges = sum(flt(d.amount) for d in self.deductions)
+
+ if not total_negative_outstanding:
+ if self.party_type == "Customer":
+ msg = _("Cannot pay to Customer without any negative outstanding invoice")
+ else:
+ msg = _("Cannot receive from Supplier without any negative outstanding invoice")
+
+ frappe.throw(msg, InvalidPaymentEntry)
+
+ elif paid_amount - additional_charges > total_negative_outstanding:
+ frappe.throw(
+ _("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
+ total_negative_outstanding
+ ),
+ InvalidPaymentEntry,
)
- paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
- additional_charges = sum([flt(d.amount) for d in self.deductions])
-
- if not total_negative_outstanding:
- frappe.throw(
- _("Cannot {0} {1} {2} without any negative outstanding invoice").format(
- _(self.payment_type),
- (_("to") if self.party_type == "Customer" else _("from")),
- self.party_type,
- ),
- InvalidPaymentEntry,
- )
-
- elif paid_amount - additional_charges > total_negative_outstanding:
- frappe.throw(
- _("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
- total_negative_outstanding
- ),
- InvalidPaymentEntry,
- )
-
def set_title(self):
if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import.
@@ -1188,6 +1187,7 @@
ple = qb.DocType("Payment Ledger Entry")
common_filter = []
+ accounting_dimensions_filter = []
posting_and_due_date = []
# confirm that Supplier is not blocked
@@ -1217,7 +1217,7 @@
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
- common_filter.append(ple.cost_center == args.get("cost_center"))
+ accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"],
@@ -1243,6 +1243,7 @@
posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
+ accounting_dimensions=accounting_dimensions_filter,
)
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@@ -1639,7 +1640,7 @@
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
- if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
+ if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
if not party_type:
@@ -1757,6 +1758,8 @@
pe.setup_party_account_field()
pe.set_missing_values()
+ update_accounting_dimensions(pe, doc)
+
if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
@@ -1774,6 +1777,18 @@
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):
bank = get_default_bank_cash_account(
doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
index 8961167..3003c68 100644
--- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
+++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
@@ -25,7 +25,8 @@
"in_list_view": 1,
"label": "Type",
"options": "DocType",
- "reqd": 1
+ "reqd": 1,
+ "search_index": 1
},
{
"columns": 2,
@@ -35,7 +36,8 @@
"in_list_view": 1,
"label": "Name",
"options": "reference_doctype",
- "reqd": 1
+ "reqd": 1,
+ "search_index": 1
},
{
"fieldname": "due_date",
@@ -104,7 +106,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-09-26 17:06:55.597389",
+ "modified": "2022-12-12 12:31:44.919895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
@@ -113,5 +115,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js
index 8f09bc3..aff067e 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js
+++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.js
@@ -3,6 +3,7 @@
frappe.ui.form.on('Payment Gateway Account', {
refresh(frm) {
+ erpnext.utils.check_payments_app();
if(!frm.doc.__islocal) {
frm.set_df_property('payment_gateway', 'read_only', 1);
}
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index 0b334ae..d986f32 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -170,7 +170,7 @@
}
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) {
@@ -179,8 +179,12 @@
title: __("Select Difference Account"),
fields: [
{
- fieldname: "allocation", fieldtype: "Table", label: __("Allocation"),
- data: this.data, in_place_edit: true,
+ fieldname: "allocation",
+ fieldtype: "Table",
+ label: __("Allocation"),
+ data: this.data,
+ in_place_edit: true,
+ cannot_add_rows: true,
get_data: () => {
return this.data;
},
@@ -218,6 +222,10 @@
read_only: 1
}]
},
+ {
+ fieldtype: 'HTML',
+ options: "<b> New Journal Entry will be posted for the difference amount </b>"
+ }
],
primary_action: () => {
const args = dialog.get_values()["allocation"];
@@ -234,7 +242,7 @@
});
this.frm.doc.allocation.forEach(d => {
- if (d.difference_amount && !d.difference_account) {
+ if (d.difference_amount) {
dialog.fields_dict.allocation.df.data.push({
'docname': d.name,
'reference_name': d.reference_name,
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 52efd33..ac033f7 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -14,7 +14,6 @@
QueryPaymentLedger,
get_outstanding_invoices,
reconcile_against_document,
- update_reference_in_payment_entry,
)
from erpnext.controllers.accounts_controller import get_advance_payment_entries
@@ -23,6 +22,7 @@
def __init__(self, *args, **kwargs):
super(PaymentReconciliation, self).__init__(*args, **kwargs)
self.common_filter_conditions = []
+ self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
@frappe.whitelist()
@@ -79,12 +79,13 @@
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
)
+ # nosemgrep
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as reference_type, t1.name as reference_name,
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
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
@@ -193,6 +194,7 @@
posting_date=self.ple_posting_date_filter,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
+ accounting_dimensions=self.accounting_dimension_filter_conditions,
)
if self.invoice_limit:
@@ -213,26 +215,26 @@
inv.currency = entry.get("currency")
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
- def get_difference_amount(self, allocated_entry):
- if allocated_entry.get("reference_type") != "Payment Entry":
- return
+ def get_difference_amount(self, payment_entry, invoice, allocated_amount):
+ difference_amount = 0
+ 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 = (
- "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
+ return difference_amount
@frappe.whitelist()
def allocate_entries(self, args):
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 = []
for pay in args.get("payments"):
pay.update({"unreconciled_amount": pay.get("amount")})
@@ -246,7 +248,10 @@
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
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:
entries.append(res)
@@ -276,6 +281,7 @@
"amount": pay.get("amount"),
"allocated_amount": allocated_amount,
"difference_amount": pay.get("difference_amount"),
+ "currency": inv.get("currency"),
}
)
@@ -298,7 +304,11 @@
else:
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:
reconcile_against_document(entry_list)
@@ -309,6 +319,56 @@
msgprint(_("Successfully Reconciled"))
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):
return frappe._dict(
{
@@ -318,6 +378,7 @@
"against_voucher_type": row.get("invoice_type"),
"against_voucher": row.get("invoice_number"),
"account": self.receivable_payable_account,
+ "exchange_rate": row.get("exchange_rate"),
"party_type": self.party_type,
"party": self.party,
"is_advance": row.get("is_advance"),
@@ -342,6 +403,41 @@
if not self.get("payments"):
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):
unreconciled_invoices = frappe._dict()
@@ -381,7 +477,7 @@
self.common_filter_conditions.append(ple.company == self.company)
if self.get("cost_center") and (get_invoices or get_return_invoices):
- self.common_filter_conditions.append(ple.cost_center == self.cost_center)
+ self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center)
if get_invoices:
if self.from_invoice_date:
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index dae029b..2ba90b4 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -6,8 +6,10 @@
import frappe
from frappe import qb
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.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
@@ -20,6 +22,7 @@
self.create_item()
self.create_customer()
self.create_account()
+ self.create_cost_center()
self.clear_old_entries()
def tearDown(self):
@@ -72,33 +75,11 @@
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
- if frappe.db.exists("Customer", "_Test PR Customer"):
- self.customer = "_Test PR Customer"
- else:
- customer = frappe.new_doc("Customer")
- customer.customer_name = "_Test PR Customer"
- 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
+ self.customer = make_customer("_Test PR Customer")
+ self.customer2 = make_customer("_Test PR Customer 2")
+ self.customer3 = make_customer("_Test PR Customer 3", "EUR")
+ self.customer4 = make_customer("_Test PR Customer 4", "EUR")
+ self.customer5 = make_customer("_Test PR Customer 5", "EUR")
def create_account(self):
account_name = "Debtors EUR"
@@ -216,6 +197,22 @@
)
return je
+ def create_cost_center(self):
+ # Setup cost center
+ cc_name = "Sub"
+
+ self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company))
+
+ cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name})
+ if cc_exists:
+ self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name)
+ else:
+ sub_cc = frappe.new_doc("Cost Center")
+ sub_cc.cost_center_name = "Sub"
+ sub_cc.parent_cost_center = self.main_cc.parent_cost_center
+ sub_cc.company = self.main_cc.company
+ self.sub_cc = sub_cc.save()
+
def test_filter_min_max(self):
# check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300)
@@ -578,3 +575,188 @@
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount)
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):
+ """
+ Cost Center filter should not affect outstanding amount calculation
+ """
+
+ si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True)
+ si.cost_center = self.main_cc.name
+ si.submit()
+ pr = get_payment_entry(si.doctype, si.name)
+ pr.cost_center = self.sub_cc.name
+ pr = pr.save().submit()
+
+ pr = self.create_payment_reconciliation()
+ pr.cost_center = self.main_cc.name
+
+ pr.get_unreconciled_entries()
+
+ # check PR tool output
+ self.assertEqual(len(pr.get("invoices")), 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
diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
index 6a21692..0f7e47a 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
@@ -20,7 +20,9 @@
"section_break_5",
"difference_amount",
"column_break_7",
- "difference_account"
+ "difference_account",
+ "exchange_rate",
+ "currency"
],
"fields": [
{
@@ -37,7 +39,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
- "options": "Currency",
+ "options": "currency",
"reqd": 1
},
{
@@ -112,7 +114,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Unreconciled Amount",
- "options": "Currency",
+ "options": "currency",
"read_only": 1
},
{
@@ -120,7 +122,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Amount",
- "options": "Currency",
+ "options": "currency",
"read_only": 1
},
{
@@ -129,11 +131,24 @@
"hidden": 1,
"label": "Reference Row",
"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,
"links": [],
- "modified": "2021-10-06 11:48:59.616562",
+ "modified": "2022-12-24 21:01:14.882747",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",
@@ -141,5 +156,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
index 00c9e12..c4dbd7e 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_invoice/payment_reconciliation_invoice.json
@@ -11,7 +11,8 @@
"col_break1",
"amount",
"outstanding_amount",
- "currency"
+ "currency",
+ "exchange_rate"
],
"fields": [
{
@@ -62,11 +63,17 @@
"hidden": 1,
"label": "Currency",
"options": "Currency"
+ },
+ {
+ "fieldname": "exchange_rate",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Exchange Rate"
}
],
"istable": 1,
"links": [],
- "modified": "2021-08-24 22:42:40.923179",
+ "modified": "2022-11-08 18:18:02.502149",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Invoice",
@@ -75,5 +82,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
index add07e8..d300ea9 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_payment/payment_reconciliation_payment.json
@@ -15,7 +15,8 @@
"difference_amount",
"sec_break1",
"remark",
- "currency"
+ "currency",
+ "exchange_rate"
],
"fields": [
{
@@ -91,11 +92,17 @@
"label": "Difference Amount",
"options": "currency",
"read_only": 1
+ },
+ {
+ "fieldname": "exchange_rate",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Exchange Rate"
}
],
"istable": 1,
"links": [],
- "modified": "2021-08-30 10:51:48.140062",
+ "modified": "2022-11-08 18:18:36.268760",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",
@@ -103,5 +110,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.js b/erpnext/accounts/doctype/payment_request/payment_request.js
index 901ef19..e913912 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.js
+++ b/erpnext/accounts/doctype/payment_request/payment_request.js
@@ -42,7 +42,7 @@
});
}
- if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") {
+ if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") {
frm.add_custom_button(__('Create Payment Entry'), function(){
frappe.call({
method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry",
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.json b/erpnext/accounts/doctype/payment_request/payment_request.json
index 2f3516e..381f3fb 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.json
+++ b/erpnext/accounts/doctype/payment_request/payment_request.json
@@ -32,6 +32,10 @@
"iban",
"branch_code",
"swift_number",
+ "accounting_dimensions_section",
+ "cost_center",
+ "dimension_col_break",
+ "project",
"recipient_and_message",
"print_format",
"email_to",
@@ -362,13 +366,35 @@
"label": "Payment Channel",
"options": "\nEmail\nPhone",
"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,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-09-30 16:19:43.680025",
+ "modified": "2022-12-21 16:56:40.115737",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 8665b70..4fc12db 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -9,8 +9,10 @@
from frappe.model.document import Document
from frappe.utils import flt, get_url, nowdate
from frappe.utils.background_jobs import enqueue
-from payments.utils import get_payment_gateway_controller
+from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
+ get_accounting_dimensions,
+)
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_company_defaults,
get_payment_entry,
@@ -19,6 +21,14 @@
from erpnext.accounts.party import get_party_account, get_party_bank_account
from erpnext.accounts.utils import get_account_currency
from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription
+from erpnext.utilities import payment_app_import_guard
+
+
+def _get_payment_gateway_controller(*args, **kwargs):
+ with payment_app_import_guard():
+ from payments.utils import get_payment_gateway_controller
+
+ return get_payment_gateway_controller(*args, **kwargs)
class PaymentRequest(Document):
@@ -107,7 +117,7 @@
self.request_phone_payment()
def request_phone_payment(self):
- controller = get_payment_gateway_controller(self.payment_gateway)
+ controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount()
payment_record = dict(
@@ -156,7 +166,7 @@
def payment_gateway_validation(self):
try:
- controller = get_payment_gateway_controller(self.payment_gateway)
+ controller = _get_payment_gateway_controller(self.payment_gateway)
if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self)
else:
@@ -189,7 +199,7 @@
)
data.update({"company": frappe.defaults.get_defaults().company})
- controller = get_payment_gateway_controller(self.payment_gateway)
+ controller = _get_payment_gateway_controller(self.payment_gateway)
controller.validate_transaction_currency(self.currency)
if hasattr(controller, "validate_minimum_transaction_amount"):
@@ -254,6 +264,7 @@
payment_entry.update(
{
+ "mode_of_payment": self.mode_of_payment,
"reference_no": self.name,
"reference_date": nowdate(),
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
@@ -262,6 +273,17 @@
}
)
+ # 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:
company_details = get_company_defaults(ref_doc.company)
@@ -403,25 +425,22 @@
else ""
)
- existing_payment_request = None
- if args.order_type == "Shopping Cart":
- existing_payment_request = frappe.db.get_value(
- "Payment Request",
- {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
- )
+ draft_payment_request = frappe.db.get_value(
+ "Payment Request",
+ {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
+ )
- if existing_payment_request:
+ existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
+
+ if existing_payment_request_amount:
+ grand_total -= existing_payment_request_amount
+
+ if draft_payment_request:
frappe.db.set_value(
- "Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False
+ "Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
)
- pr = frappe.get_doc("Payment Request", existing_payment_request)
+ pr = frappe.get_doc("Payment Request", draft_payment_request)
else:
- if args.order_type != "Shopping Cart":
- existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
-
- if existing_payment_request_amount:
- grand_total -= existing_payment_request_amount
-
pr = frappe.new_doc("Payment Request")
pr.update(
{
@@ -444,6 +463,17 @@
}
)
+ # 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:
pr.flags.mute_email = True
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index b666e0d..2943500 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -255,7 +255,7 @@
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
- data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc)
+ data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data)
if (
@@ -292,7 +292,7 @@
pricing_rule.uom = row.uom
-def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
+def get_pricing_rule_for_item(args, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index d27f65e..5bb366a 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -1123,7 +1123,7 @@
"apply_on": args.apply_on or "Item Code",
"applicable_for": args.applicable_for,
"selling": args.selling or 0,
- "currency": "USD",
+ "currency": "INR",
"apply_discount_on_rate": args.apply_discount_on_rate or 0,
"buying": args.buying or 0,
"min_qty": args.min_qty or 0.0,
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index bb54b23..1ce780e 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -250,6 +250,22 @@
and ifnull(`tabPricing Rule`.valid_upto, '2500-12-31')"""
values["transaction_date"] = args.get("transaction_date")
+ if args.get("doctype") in [
+ "Quotation",
+ "Quotation Item",
+ "Sales Order",
+ "Sales Order Item",
+ "Delivery Note",
+ "Delivery Note Item",
+ "Sales Invoice",
+ "Sales Invoice Item",
+ "POS Invoice",
+ "POS Invoice Item",
+ ]:
+ conditions += """ and ifnull(`tabPricing Rule`.selling, 0) = 1"""
+ else:
+ conditions += """ and ifnull(`tabPricing Rule`.buying, 0) = 1"""
+
return conditions
@@ -669,7 +685,7 @@
item_details.free_item_data.append(free_item_data_args)
-def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
+def apply_pricing_rule_for_free_items(doc, pricing_rule_args):
if pricing_rule_args:
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index a03157e..6281400 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -64,12 +64,13 @@
"tax_withholding_net_total",
"base_tax_withholding_net_total",
"taxes_section",
+ "tax_category",
"taxes_and_charges",
"column_break_58",
- "tax_category",
- "column_break_49",
"shipping_rule",
+ "column_break_49",
"incoterm",
+ "named_place",
"section_break_51",
"taxes",
"totals",
@@ -1541,13 +1542,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-25 12:44:29.935567",
+ "modified": "2022-12-12 18:37:38.142688",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index b38bce7..4729d9c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -61,12 +61,13 @@
"total",
"net_total",
"taxes_section",
+ "tax_category",
"taxes_and_charges",
"column_break_38",
"shipping_rule",
- "incoterm",
"column_break_55",
- "tax_category",
+ "incoterm",
+ "named_place",
"section_break_40",
"taxes",
"section_break_43",
@@ -2122,6 +2123,12 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
}
],
"icon": "fa fa-file-text",
@@ -2134,7 +2141,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2022-12-05 16:18:14.532114",
+ "modified": "2022-12-12 18:34:33.409895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index c276be2..31cf120 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1185,11 +1185,24 @@
if asset.calculate_depreciation:
posting_date = frappe.db.get_value("Sales Invoice", self.return_against, "posting_date")
reverse_depreciation_entry_made_after_disposal(asset, posting_date)
- reset_depreciation_schedule(asset, self.posting_date)
+ 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:
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()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 855380e..e96847e 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -21,6 +21,9 @@
from erpnext.accounts.utils import PaymentEntryUnlinkError
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_depreciation_schedule.asset_depreciation_schedule import (
+ get_depr_schedule,
+)
from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
@@ -2774,7 +2777,7 @@
["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(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@@ -2805,7 +2808,7 @@
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(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@@ -2834,7 +2837,7 @@
["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(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index 62c3ced..35d19ed 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -890,7 +890,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-11-02 12:53:12.693217",
+ "modified": "2022-12-28 16:17:33.484531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 9dab4e9..8708342 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -280,7 +280,8 @@
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
self.validate_end_date()
self.validate_to_follow_calendar_months()
- self.cost_center = erpnext.get_default_cost_center(self.get("company"))
+ if not self.cost_center:
+ self.cost_center = erpnext.get_default_cost_center(self.get("company"))
def validate_trial_period(self):
"""
diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js
index 7d6f2ae..00727f1 100644
--- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.js
+++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.js
@@ -5,5 +5,9 @@
price_determination: function(frm) {
frm.toggle_reqd("cost", frm.doc.price_determination === 'Fixed rate');
frm.toggle_reqd("price_list", frm.doc.price_determination === 'Based on price list');
- }
+ },
+
+ subscription_plan: function (frm) {
+ erpnext.utils.check_payments_app();
+ },
});
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 30ed91b..b834d14 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -121,12 +121,24 @@
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
+ cost_center = get_cost_center(inv)
+ tax_row.update({"cost_center": cost_center})
+
if inv.doctype == "Purchase Invoice":
return tax_row, tax_deducted_on_advances, voucher_wise_amount
else:
return tax_row
+def get_cost_center(inv):
+ cost_center = frappe.get_cached_value("Company", inv.company, "cost_center")
+
+ if len(inv.get("taxes", [])) > 0:
+ cost_center = inv.get("taxes")[0].cost_center
+
+ return cost_center
+
+
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index c757057..41fdb6a 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -199,7 +199,14 @@
# filter zero debit and credit entries
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)
@@ -350,15 +357,26 @@
allowance = get_debit_credit_allowance(voucher_type, precision)
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
+
if abs(debit_credit_diff) > allowance:
- raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
+ 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)
elif abs(debit_credit_diff) >= (1.0 / (10**precision)):
make_round_off_gle(gl_map, debit_credit_diff, precision)
debit_credit_diff = get_debit_credit_difference(gl_map, precision)
if abs(debit_credit_diff) > allowance:
- raise_debit_credit_not_equal_error(debit_credit_diff, voucher_type, voucher_no)
+ 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)
def get_debit_credit_difference(gl_map, precision):
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index a195c57..94a1510 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -99,6 +99,9 @@
# Get return entries
self.get_return_entries()
+ # Get Exchange Rate Revaluations
+ self.get_exchange_rate_revaluations()
+
self.data = []
for ple in self.ple_entries:
@@ -251,7 +254,8 @@
row.invoice_grand_total = row.invoiced
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
- abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
+ (abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
+ or (row.voucher_no in self.err_journals)
):
# non-zero oustanding, we must consider this row
@@ -794,19 +798,19 @@
if self.filters.get("payment_terms_template"):
self.qb_selection_filter.append(
- self.ple.party_isin(
- qb.from_(self.customer).where(
- self.customer.payment_terms == self.filters.get("payment_terms_template")
- )
+ self.ple.party.isin(
+ qb.from_(self.customer)
+ .select(self.customer.name)
+ .where(self.customer.payment_terms == self.filters.get("payment_terms_template"))
)
)
if self.filters.get("sales_partner"):
self.qb_selection_filter.append(
- self.ple.party_isin(
- qb.from_(self.customer).where(
- self.customer.default_sales_partner == self.filters.get("payment_terms_template")
- )
+ self.ple.party.isin(
+ qb.from_(self.customer)
+ .select(self.customer.name)
+ .where(self.customer.default_sales_partner == self.filters.get("sales_partner"))
)
)
@@ -865,10 +869,15 @@
def get_party_details(self, party):
if not party in self.party_details:
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(
"Customer",
party,
- ["customer_name", "territory", "customer_group", "customer_primary_contact"],
+ fields,
as_dict=True,
)
else:
@@ -969,6 +978,9 @@
if self.filters.show_sales_person:
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":
self.add_column(
label=_("Supplier Group"),
@@ -1028,3 +1040,17 @@
"data": {"labels": self.ageing_column_labels, "datasets": rows},
"type": "percentage",
}
+
+ def get_exchange_rate_revaluations(self):
+ je = qb.DocType("Journal Entry")
+ results = (
+ qb.from_(je)
+ .select(je.name)
+ .where(
+ (je.company == self.filters.company)
+ & (je.posting_date.lte(self.filters.report_date))
+ & (je.voucher_type == "Exchange Rate Revaluation")
+ )
+ .run()
+ )
+ self.err_journals = [x[0] for x in results] if results else []
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index bac8bee..afd02a0 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -1,9 +1,10 @@
import unittest
import frappe
-from frappe.tests.utils import FrappeTestCase
-from frappe.utils import add_days, getdate, today
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, flt, getdate, today
+from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
@@ -17,10 +18,37 @@
frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
+ frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
+ frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
+
+ self.create_usd_account()
def tearDown(self):
frappe.db.rollback()
+ def create_usd_account(self):
+ name = "Debtors USD"
+ exists = frappe.db.get_list(
+ "Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
+ )
+ if exists:
+ self.debtors_usd = exists[0].name
+ else:
+ debtors = frappe.get_doc(
+ "Account",
+ frappe.db.get_list(
+ "Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
+ )[0].name,
+ )
+
+ debtors_usd = frappe.new_doc("Account")
+ debtors_usd.company = debtors.company
+ debtors_usd.account_name = "Debtors USD"
+ debtors_usd.account_currency = "USD"
+ debtors_usd.parent_account = debtors.parent_account
+ debtors_usd.account_type = debtors.account_type
+ self.debtors_usd = debtors_usd.save().name
+
def test_accounts_receivable(self):
filters = {
"company": "_Test Company 2",
@@ -33,7 +61,7 @@
}
# check invoice grand total and invoiced column's value for 3 payment terms
- name = make_sales_invoice()
+ name = make_sales_invoice().name
report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]]
@@ -118,8 +146,72 @@
],
)
+ @change_settings(
+ "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
+ )
+ def test_exchange_revaluation_for_party(self):
+ """
+ Exchange Revaluation for party on Receivable/Payable shoule be included
+ """
-def make_sales_invoice():
+ company = "_Test Company 2"
+ customer = "_Test Customer 2"
+
+ # Using Exchange Gain/Loss account for unrealized as well.
+ company_doc = frappe.get_doc("Company", company)
+ company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
+ company_doc.save()
+
+ si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
+ si.currency = "USD"
+ si.conversion_rate = 0.90
+ si.debit_to = self.debtors_usd
+ si = si.save().submit()
+
+ # Exchange Revaluation
+ err = frappe.new_doc("Exchange Rate Revaluation")
+ err.company = company
+ err.posting_date = today()
+ accounts = err.get_accounts_data()
+ err.extend("accounts", accounts)
+ err.accounts[0].new_exchange_rate = 0.95
+ row = err.accounts[0]
+ row.new_balance_in_base_currency = flt(
+ row.new_exchange_rate * flt(row.balance_in_account_currency)
+ )
+ row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
+ err.set_total_gain_loss()
+ err = err.save().submit()
+
+ # Submit JV for ERR
+ err_journals = err.make_jv_entries()
+ je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
+ je = je.submit()
+
+ filters = {
+ "company": company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+ report = execute(filters)
+
+ expected_data_for_err = [0, -5, 0, 5]
+ row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
+ self.assertEqual(
+ expected_data_for_err,
+ [
+ row.invoiced,
+ row.paid,
+ row.credit_note,
+ row.outstanding,
+ ],
+ )
+
+
+def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
@@ -134,22 +226,26 @@
do_not_save=1,
)
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
- )
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
- )
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
- )
+ if not no_payment_schedule:
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
+ )
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
+ )
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
+ )
- si.submit()
+ si = si.save()
- return si.name
+ if not do_not_submit:
+ si = si.submit()
+
+ return si
def make_payment(docname):
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index 889f5a2..29217b0 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -121,6 +121,9 @@
if 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):
self.columns = []
self.add_column(
@@ -160,6 +163,10 @@
)
if self.filters.show_sales_person:
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:
self.add_column(
label=_("Supplier Group"),
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index ad9b1ba..43b95dc 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -131,8 +131,8 @@
else
0
end), 0) as depreciation_amount_during_the_period
- from `tabAsset` a, `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, '') != ''
+ 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 ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != ''
group by a.asset_category
union
SELECT a.asset_category,
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py
index d4f2011..cb3c78a 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/cash_flow.py
@@ -8,6 +8,7 @@
from erpnext.accounts.report.financial_statements import (
get_columns,
+ get_cost_centers_with_children,
get_data,
get_filtered_list_for_consolidated_report,
get_period_list,
@@ -160,10 +161,11 @@
total = 0
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
+ filters.start_date = start_date
+ filters.end_date = period["to_date"]
+ filters.account_type = account_type
- amount = get_account_type_based_gl_data(
- company, start_date, period["to_date"], account_type, filters
- )
+ amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":
amount *= -1
@@ -175,7 +177,7 @@
return data
-def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
+def get_account_type_based_gl_data(company, filters=None):
cond = ""
filters = frappe._dict(filters or {})
@@ -191,17 +193,21 @@
frappe.db.escape(cstr(filters.finance_book))
)
+ if filters.get("cost_center"):
+ filters.cost_center = get_cost_centers_with_children(filters.cost_center)
+ cond += " and cost_center in %(cost_center)s"
+
gl_sum = frappe.db.sql_list(
"""
select sum(credit) - sum(debit)
from `tabGL Entry`
- where company=%s and posting_date >= %s and posting_date <= %s
+ where company=%(company)s and posting_date >= %(start_date)s and posting_date <= %(end_date)s
and voucher_type != 'Period Closing Voucher'
- and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond}
+ and account in ( SELECT name FROM tabAccount WHERE account_type = %(account_type)s) {cond}
""".format(
cond=cond
),
- (company, start_date, end_date, account_type),
+ filters,
)
return gl_sum[0] if gl_sum and gl_sum[0] else 0
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index e93fb61..ddee9fc 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -268,10 +268,12 @@
def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data = {}
total = 0
+ filters.account_type = account_type
+ filters.start_date = fiscal_year.year_start_date
+ filters.end_date = fiscal_year.year_end_date
+
for company in companies:
- amount = get_account_type_based_gl_data(
- company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters
- )
+ amount = get_account_type_based_gl_data(company, filters)
if amount and account_type == "Depreciation":
amount *= -1
@@ -533,12 +535,13 @@
],
filters={"company": company, "root_type": root_type},
):
- if account.account_name not in added_accounts:
+ if account.account_number:
+ account_key = account.account_number + "-" + account.account_name
+ else:
+ account_key = account.account_name
+
+ if account_key not in added_accounts:
accounts.append(account)
- if account.account_number:
- account_key = account.account_number + "-" + account.account_name
- else:
- account_key = account.account_name
added_accounts.append(account_key)
return accounts
diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
index 6b0d3c9..4765e3b 100644
--- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
+++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
@@ -26,6 +26,7 @@
)
self.get_gl_entries()
+ self.get_additional_columns()
self.get_return_invoices()
self.get_party_adjustment_amounts()
@@ -33,6 +34,42 @@
data = self.get_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):
columns = [
{
@@ -116,6 +153,35 @@
},
]
+ # 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
def get_data(self):
@@ -143,6 +209,12 @@
),
)
+ 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)
self.party_data[gle.party].closing_balance += amount
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index e3531b0..fc23127 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -239,7 +239,7 @@
):
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"):
conditions.append("project in %(project)s")
diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py
index b10e769..c563785 100644
--- a/erpnext/accounts/report/general_ledger/test_general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py
@@ -109,8 +109,7 @@
frappe.db.set_value(
"Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC"
)
- revaluation_jv = revaluation.make_jv_entry()
- revaluation_jv = frappe.get_doc(revaluation_jv)
+ revaluation_jv = revaluation.make_jv_for_revaluation()
revaluation_jv.cost_center = "_Test Cost Center - _TC"
for acc in revaluation_jv.get("accounts"):
acc.cost_center = "_Test Cost Center - _TC"
diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py
index dacc809..130b715 100644
--- a/erpnext/accounts/report/gross_profit/gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/gross_profit.py
@@ -439,6 +439,18 @@
row.delivery_note, frappe._dict()
)
row.item_row = row.dn_detail
+ # Update warehouse and base_amount from 'Packed Item' List
+ if product_bundles and not row.parent:
+ # For Packed Items, row.parent_invoice will be the Bundle name
+ product_bundle = product_bundles.get(row.parent_invoice)
+ if product_bundle:
+ for packed_item in product_bundle:
+ if (
+ packed_item.get("item_code") == row.item_code
+ and packed_item.get("parent_detail_docname") == row.item_row
+ ):
+ row.warehouse = packed_item.warehouse
+ row.base_amount = packed_item.base_amount
# get buying amount
if row.item_code in product_bundles:
@@ -503,7 +515,7 @@
invoice_portion = 100
elif row.invoice_portion:
invoice_portion = row.invoice_portion
- else:
+ elif row.payment_amount:
invoice_portion = row.payment_amount * 100 / row.base_net_amount
if i == 0:
@@ -589,7 +601,9 @@
buying_amount = 0.0
for packed_item in product_bundle:
if packed_item.get("parent_detail_docname") == row.item_row:
- buying_amount += self.get_buying_amount(row, packed_item.item_code)
+ packed_item_row = row.copy()
+ packed_item_row.warehouse = packed_item.warehouse
+ buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
return flt(buying_amount, self.currency_precision)
@@ -607,6 +621,7 @@
return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else:
return flt(row.qty) * self.get_average_buying_rate(row, item_code)
+ return 0.0
def get_buying_amount(self, row, item_code):
# IMP NOTE
@@ -921,12 +936,25 @@
def load_product_bundle(self):
self.product_bundles = {}
- for d in frappe.db.sql(
- """select parenttype, parent, parent_item,
- item_code, warehouse, -1*qty as total_qty, parent_detail_docname
- from `tabPacked Item` where docstatus=1""",
- as_dict=True,
- ):
+ pki = qb.DocType("Packed Item")
+
+ pki_query = (
+ frappe.qb.from_(pki)
+ .select(
+ pki.parenttype,
+ pki.parent,
+ pki.parent_item,
+ pki.item_code,
+ pki.warehouse,
+ (-1 * pki.qty).as_("total_qty"),
+ pki.rate,
+ (pki.rate * pki.qty).as_("base_amount"),
+ pki.parent_detail_docname,
+ )
+ .where(pki.docstatus == 1)
+ )
+
+ for d in pki_query.run(as_dict=True):
self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault(
d.parent, frappe._dict()
).setdefault(d.parent_item, []).append(d)
diff --git a/erpnext/accounts/report/gross_profit/test_gross_profit.py b/erpnext/accounts/report/gross_profit/test_gross_profit.py
index 0ea6b5c..fa11a41 100644
--- a/erpnext/accounts/report/gross_profit/test_gross_profit.py
+++ b/erpnext/accounts/report/gross_profit/test_gross_profit.py
@@ -6,6 +6,8 @@
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.gross_profit.gross_profit import execute
+from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -14,6 +16,7 @@
def setUp(self):
self.create_company()
self.create_item()
+ self.create_bundle()
self.create_customer()
self.create_sales_invoice()
self.clear_old_entries()
@@ -42,6 +45,7 @@
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "Stores - " + abbr
+ self.finished_warehouse = "Finished Goods - " + abbr
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
@@ -53,6 +57,23 @@
)
self.item = item if isinstance(item, str) else item.item_code
+ def create_bundle(self):
+ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
+
+ item2 = create_item(
+ item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
+ )
+ self.item2 = item2 if isinstance(item2, str) else item2.item_code
+
+ # This will be parent item
+ bundle = create_item(
+ item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
+ )
+ self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
+
+ # Create Product Bundle
+ self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
+
def create_customer(self):
name = "_Test GP Customer"
if frappe.db.exists("Customer", name):
@@ -93,6 +114,28 @@
)
return sinv
+ def create_delivery_note(
+ self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
+ ):
+ """
+ Helper function to populate default values in Delivery Note
+ """
+ dnote = create_delivery_note(
+ company=self.company,
+ customer=self.customer,
+ currency="INR",
+ item=item or self.item,
+ qty=qty,
+ rate=rate,
+ cost_center=self.cost_center,
+ warehouse=self.warehouse,
+ return_against=None,
+ expense_account=self.expense_account,
+ do_not_save=do_not_save,
+ do_not_submit=do_not_submit,
+ )
+ return dnote
+
def clear_old_entries(self):
doctype_list = [
"Sales Invoice",
@@ -207,3 +250,55 @@
}
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
+
+ def test_bundled_delivery_note_with_different_warehouses(self):
+ """
+ Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses
+ """
+ se = make_stock_entry(
+ company=self.company,
+ item_code=self.item,
+ target=self.warehouse,
+ qty=1,
+ basic_rate=100,
+ do_not_submit=True,
+ )
+ item = se.items[0]
+ se.append(
+ "items",
+ {
+ "item_code": self.item2,
+ "s_warehouse": "",
+ "t_warehouse": self.finished_warehouse,
+ "qty": 1,
+ "basic_rate": 100,
+ "conversion_factor": item.conversion_factor or 1.0,
+ "transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
+ "serial_no": item.serial_no,
+ "batch_no": item.batch_no,
+ "cost_center": item.cost_center,
+ "expense_account": item.expense_account,
+ },
+ )
+ se = se.save().submit()
+
+ # Make a Delivery note with Product bundle
+ # Packed Items will have different warehouses
+ dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True)
+ dnote.packed_items[1].warehouse = self.finished_warehouse
+ dnote = dnote.submit()
+
+ # make Sales Invoice for above delivery note
+ sinv = make_sales_invoice(dnote.name)
+ sinv = sinv.save().submit()
+
+ filters = frappe._dict(
+ company=self.company,
+ from_date=nowdate(),
+ to_date=nowdate(),
+ group_by="Invoice",
+ sales_invoice=sinv.name,
+ )
+
+ columns, data = execute(filters=filters)
+ self.assertGreater(len(data), 0)
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index c04b9c7..d34c213 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -53,9 +53,6 @@
item_details = get_item_details()
for d in item_list:
- if not d.stock_qty:
- continue
-
item_record = item_details.get(d.item_code)
purchase_receipt = None
@@ -94,7 +91,7 @@
"expense_account": expense_account,
"stock_qty": d.stock_qty,
"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,
}
)
diff --git a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
index f812977..5dc4c3d 100644
--- a/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
+++ b/erpnext/accounts/report/supplier_ledger_summary/supplier_ledger_summary.js
@@ -64,24 +64,6 @@
"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",
"label": __("Tax Id"),
"fieldtype": "Data",
diff --git a/erpnext/accounts/report/tax_detail/tax_detail.py b/erpnext/accounts/report/tax_detail/tax_detail.py
index ba8d307..ba733c2 100644
--- a/erpnext/accounts/report/tax_detail/tax_detail.py
+++ b/erpnext/accounts/report/tax_detail/tax_detail.py
@@ -234,8 +234,11 @@
if field in ["item_tax_rate", "base_net_amount"]:
return None
- if doctype == "GL Entry" and field in ["debit", "credit"]:
- column.update({"label": _("Amount"), "fieldname": "amount"})
+ if doctype == "GL Entry":
+ if field in ["debit", "credit"]:
+ column.update({"label": _("Amount"), "fieldname": "amount"})
+ elif field == "voucher_type":
+ column.update({"fieldtype": "Data", "options": ""})
if field == "taxes_and_charges":
column.update({"label": _("Taxes and Charges Template")})
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index d3cd290..97cc1c4 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -101,11 +101,8 @@
account_currency = entry["account_currency"]
if len(account_currencies) == 1 and account_currency == presentation_currency:
- if debit_in_account_currency:
- entry["debit"] = debit_in_account_currency
-
- if credit_in_account_currency:
- entry["credit"] = credit_in_account_currency
+ entry["debit"] = debit_in_account_currency
+ entry["credit"] = credit_in_account_currency
else:
date = currency_info["report_date"]
converted_debit_value = convert(debit, presentation_currency, company_currency, date)
diff --git a/erpnext/accounts/test/test_utils.py b/erpnext/accounts/test/test_utils.py
index 882cd69..3aca60e 100644
--- a/erpnext/accounts/test/test_utils.py
+++ b/erpnext/accounts/test/test_utils.py
@@ -3,11 +3,14 @@
import frappe
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.utils import (
get_future_stock_vouchers,
get_voucherwise_gl_entries,
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.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -73,6 +76,47 @@
sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(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 = [
{
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 41702d6..445dcc5 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -611,11 +611,6 @@
new_row.docstatus = 1
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:
account_details = {
"account": d.difference_account,
@@ -627,6 +622,11 @@
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:
payment_entry.save(ignore_permissions=True)
@@ -836,6 +836,7 @@
posting_date=None,
min_outstanding=None,
max_outstanding=None,
+ accounting_dimensions=None,
):
ple = qb.DocType("Payment Ledger Entry")
@@ -866,6 +867,7 @@
min_outstanding=min_outstanding,
max_outstanding=max_outstanding,
get_invoices=True,
+ accounting_dimensions=accounting_dimensions or [],
)
for d in invoice_list:
@@ -1615,6 +1617,7 @@
.where(ple.delinked == 0)
.where(Criterion.all(filter_on_voucher_no))
.where(Criterion.all(self.common_filter))
+ .where(Criterion.all(self.dimensions_filter))
.where(Criterion.all(self.voucher_posting_date))
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
)
@@ -1702,6 +1705,7 @@
max_outstanding=None,
get_payments=False,
get_invoices=False,
+ accounting_dimensions=None,
):
"""
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
@@ -1717,6 +1721,7 @@
self.reset()
self.vouchers = vouchers
self.common_filter = common_filter or []
+ self.dimensions_filter = accounting_dimensions or []
self.voucher_posting_date = posting_date or []
self.min_outstanding = min_outstanding
self.max_outstanding = max_outstanding
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 7e54219..b8185c9 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -76,7 +76,6 @@
refresh: function(frm) {
frappe.ui.form.trigger("Asset", "is_existing_asset");
frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1);
- frm.events.make_schedules_editable(frm);
if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
@@ -188,7 +187,11 @@
})
},
- 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 asset_values = [frm.doc.gross_purchase_amount];
var last_depreciation_date = frm.doc.purchase_date;
@@ -202,7 +205,20 @@
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);
var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount);
if(v.journal_entry) {
@@ -266,21 +282,6 @@
// 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) {
frappe.call({
args: {
@@ -476,7 +477,6 @@
depreciation_method: function(frm, cdt, cdn) {
const row = locals[cdt][cdn];
frm.events.set_depreciation_rate(frm, row);
- frm.events.make_schedules_editable(frm);
},
expected_value_after_useful_life: function(frm, cdt, cdn) {
@@ -512,41 +512,6 @@
}
});
-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) {
frappe.confirm(__("Do you really want to scrap this asset?"), function () {
frappe.call({
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index f0505ff..4bac303 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -52,8 +52,6 @@
"column_break_24",
"frequency_of_depreciation",
"next_depreciation_date",
- "section_break_14",
- "schedules",
"insurance_details",
"policy_number",
"insurer",
@@ -308,19 +306,6 @@
"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,
"fieldname": "insurance_details",
"fieldtype": "Section Break",
@@ -508,9 +493,14 @@
"group": "Value",
"link_doctype": "Asset Value Adjustment",
"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",
"module": "Assets",
"name": "Asset",
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index ca6be9b..df05d5e 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -8,14 +8,15 @@
import frappe
from frappe import _
from frappe.utils import (
- add_days,
add_months,
cint,
date_diff,
flt,
get_datetime,
get_last_day,
+ get_link_to_form,
getdate,
+ is_last_day_of_the_month,
month_diff,
nowdate,
today,
@@ -28,6 +29,16 @@
get_disposal_account_and_cost_center,
)
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
@@ -40,9 +51,9 @@
self.set_missing_values()
if not self.split_from:
self.prepare_depreciation_data()
+ update_draft_asset_depr_schedules(self)
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()
@@ -52,16 +63,24 @@
self.make_asset_movement()
if not self.booked_fixed_asset and self.validate_make_gl_entry():
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):
self.validate_cancellation()
self.cancel_movement_entries()
self.delete_depreciation_entries()
+ cancel_asset_depr_schedules(self)
self.set_status()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
make_reverse_gl_entries(voucher_type="Asset", voucher_no=self.name)
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):
if self.purchase_invoice or self.purchase_receipt:
reference_doc = "Purchase Invoice" if self.purchase_invoice else "Purchase Receipt"
@@ -79,12 +98,10 @@
_("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:
self.value_after_depreciation = 0
self.set_depreciation_rate()
- self.make_depreciation_schedule(date_of_disposal)
- self.set_accumulated_depreciation(date_of_disposal, date_of_return)
else:
self.finance_books = []
self.value_after_depreciation = flt(self.gross_purchase_amount) - flt(
@@ -223,148 +240,6 @@
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):
# value_after_depreciation - current Asset value
if self.docstatus == 1 and finance_book.value_after_depreciation:
@@ -376,58 +251,6 @@
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
def check_is_pro_rata(self, row):
has_pro_rata = False
@@ -512,83 +335,15 @@
).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):
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 = [
- d.accumulated_depreciation_amount
- for d in self.get("schedules")
- if cint(d.finance_book_id) == row.idx
+ d.accumulated_depreciation_amount for d in depr_schedule
]
if accumulated_depreciation_after_full_schedule:
@@ -637,10 +392,13 @@
movement.cancel()
def delete_depreciation_entries(self):
- for d in self.get("schedules"):
- if d.journal_entry:
- frappe.get_doc("Journal Entry", d.journal_entry).cancel()
- d.db_set("journal_entry", None)
+ 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:
+ frappe.get_doc("Journal Entry", d.journal_entry).cancel()
+ d.db_set("journal_entry", None)
self.db_set(
"value_after_depreciation",
@@ -1072,32 +830,6 @@
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()
def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name)
@@ -1109,12 +841,12 @@
remaining_qty = asset.asset_quantity - 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
-def update_existing_asset(asset, remaining_qty):
+def update_existing_asset(asset, remaining_qty, new_asset_name):
remaining_gross_purchase_amount = flt(
(asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity
)
@@ -1132,34 +864,49 @@
},
)
- for finance_book in asset.get("finance_books"):
+ for row in asset.get("finance_books"):
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(
- (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(
- "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(
"Asset Finance Book",
- finance_book.name,
+ row.name,
"expected_value_after_useful_life",
expected_value_after_useful_life,
)
- accumulated_depreciation = 0
+ 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)
- for term in asset.get("schedules"):
- depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity)
- frappe.db.set_value(
- "Depreciation Schedule", term.name, "depreciation_amount", depreciation_amount
+ set_draft_asset_depr_schedule_details(new_asset_depr_schedule_doc, asset, row)
+
+ accumulated_depreciation = 0
+
+ for term in new_asset_depr_schedule_doc.get("depreciation_schedule"):
+ depreciation_amount = flt((term.depreciation_amount * remaining_qty) / asset.asset_quantity)
+ term.depreciation_amount = depreciation_amount
+ accumulated_depreciation += depreciation_amount
+ term.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)
)
- accumulated_depreciation += depreciation_amount
- frappe.db.set_value(
- "Depreciation Schedule", term.name, "accumulated_depreciation_amount", accumulated_depreciation
- )
+ 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):
@@ -1173,31 +920,49 @@
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name
- accumulated_depreciation = 0
- for finance_book in new_asset.get("finance_books"):
- finance_book.value_after_depreciation = flt(
- (finance_book.value_after_depreciation * split_qty) / asset.asset_quantity
+ for row in new_asset.get("finance_books"):
+ row.value_after_depreciation = flt(
+ (row.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
+ row.expected_value_after_useful_life = flt(
+ (row.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)
- term.depreciation_amount = depreciation_amount
- accumulated_depreciation += depreciation_amount
- term.accumulated_depreciation_amount = accumulated_depreciation
-
new_asset.submit()
new_asset.set_status()
- for term in new_asset.get("schedules"):
- # Update references in JV
- if term.journal_entry:
- add_reference_in_jv_on_split(
- term.journal_entry, new_asset.name, asset.name, term.depreciation_amount
- )
+ 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
+
+ for term in new_asset_depr_schedule_doc.get("depreciation_schedule"):
+ depreciation_amount = flt((term.depreciation_amount * split_qty) / asset.asset_quantity)
+ term.depreciation_amount = depreciation_amount
+ accumulated_depreciation += depreciation_amount
+ term.accumulated_depreciation_amount = accumulated_depreciation
+
+ notes = _("This schedule was created when new Asset {0} was split from Asset {1}.").format(
+ 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
+
+ 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
+ if term.journal_entry:
+ add_reference_in_jv_on_split(
+ term.journal_entry, new_asset.name, asset.name, term.depreciation_amount
+ )
return new_asset
diff --git a/erpnext/assets/doctype/asset/depreciation.py b/erpnext/assets/doctype/asset/depreciation.py
index 9794170..7686c34 100644
--- a/erpnext/assets/doctype/asset/depreciation.py
+++ b/erpnext/assets/doctype/asset/depreciation.py
@@ -4,12 +4,18 @@
import frappe
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 (
get_checks_for_pl_and_bs_accounts,
)
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):
@@ -21,8 +27,11 @@
if not date:
date = today()
- for asset in get_depreciable_assets(date):
- make_depreciation_entry(asset, date)
+ for asset_name in get_depreciable_assets(date):
+ asset_doc = frappe.get_doc("Asset", asset_name)
+
+ make_depreciation_entry_for_all_asset_depr_schedules(asset_doc, date)
+
if commit:
frappe.db.commit()
@@ -30,21 +39,35 @@
def get_depreciable_assets(date):
return frappe.db.sql_list(
"""select distinct a.name
- from tabAsset a, `tabDepreciation Schedule` ds
- where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
+ from tabAsset a, `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
+ 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.calculate_depreciation = 1
+ and ds.schedule_date<=%s
and ifnull(ds.journal_entry, '')=''""",
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()
-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)
if not date:
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)
(
fixed_asset_account,
@@ -60,14 +83,14 @@
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):
je = frappe.new_doc("Journal Entry")
je.voucher_type = "Depreciation Entry"
je.naming_series = depreciation_series
je.posting_date = d.schedule_date
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)
credit_account, debit_account = get_credit_and_debit_accounts(
@@ -118,14 +141,14 @@
d.db_set("journal_entry", je.name)
- idx = cint(d.finance_book_id)
- finance_books = asset.get("finance_books")[idx - 1]
- finance_books.value_after_depreciation -= d.depreciation_amount
- finance_books.db_update()
+ idx = cint(asset_depr_schedule_doc.finance_book_id)
+ row = asset.get("finance_books")[idx - 1]
+ row.value_after_depreciation -= d.depreciation_amount
+ row.db_update()
asset.set_status()
- return asset
+ return asset_depr_schedule_doc
def get_depreciation_accounts(asset):
@@ -199,7 +222,11 @@
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()
depreciation_series = frappe.get_cached_value(
@@ -232,10 +259,15 @@
asset = frappe.get_doc("Asset", asset_name)
reverse_depreciation_entry_made_after_disposal(asset, asset.disposal_date)
- reset_depreciation_schedule(asset, asset.disposal_date)
je = asset.journal_entry_for_scrap
+ 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("journal_entry_for_scrap", None)
@@ -244,22 +276,28 @@
asset.set_status()
-def depreciate_asset(asset, date):
- asset.flags.ignore_validate_update_after_submit = True
- asset.prepare_depreciation_data(date_of_disposal=date)
- asset.save()
+def depreciate_asset(asset_doc, date, notes):
+ asset_doc.flags.ignore_validate_update_after_submit = True
- 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):
- asset.flags.ignore_validate_update_after_submit = True
+def reset_depreciation_schedule(asset_doc, date, notes):
+ asset_doc.flags.ignore_validate_update_after_submit = True
- # recreate original depreciation schedule of the asset
- asset.prepare_depreciation_data(date_of_return=date)
+ make_new_active_asset_depr_schedules_and_cancel_current_ones(
+ asset_doc, notes, date_of_return=date
+ )
- modify_depreciation_schedule_for_asset_repairs(asset)
- asset.save()
+ modify_depreciation_schedule_for_asset_repairs(asset_doc)
+
+ asset_doc.save()
def modify_depreciation_schedule_for_asset_repairs(asset):
@@ -271,35 +309,36 @@
if repair.increase_in_asset_life:
asset_repair = frappe.get_doc("Asset Repair", repair.name)
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):
- row = -1
- finance_book = asset.get("schedules")[0].get("finance_book")
- for schedule in asset.get("schedules"):
- if schedule.finance_book != finance_book:
- row = 0
- finance_book = schedule.finance_book
- else:
- row += 1
+ for row in asset.get("finance_books"):
+ asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
- if schedule.schedule_date == date:
- if not disposal_was_made_on_original_schedule_date(
- asset, schedule, row, date
- ) or disposal_happens_in_the_future(date):
+ for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
+ if schedule.schedule_date == date:
+ if not disposal_was_made_on_original_schedule_date(
+ schedule_idx, row, date
+ ) or disposal_happens_in_the_future(date):
- reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
- reverse_journal_entry.posting_date = nowdate()
- frappe.flags.is_reverse_depr_entry = True
- reverse_journal_entry.submit()
+ reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
+ reverse_journal_entry.posting_date = nowdate()
+ frappe.flags.is_reverse_depr_entry = True
+ reverse_journal_entry.submit()
- frappe.flags.is_reverse_depr_entry = False
- asset.flags.ignore_validate_update_after_submit = True
- schedule.journal_entry = None
- depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
- asset.finance_books[0].value_after_depreciation += depreciation_amount
- asset.save()
+ 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
+ schedule.journal_entry = None
+ depreciation_amount = get_depreciation_amount_in_je(reverse_journal_entry)
+ row.value_after_depreciation += depreciation_amount
+ asset_depr_schedule_doc.save()
+ asset.save()
def get_depreciation_amount_in_je(journal_entry):
@@ -310,15 +349,14 @@
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
-def disposal_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_disposal):
- for finance_book in asset.get("finance_books"):
- if schedule.finance_book == finance_book.finance_book:
- orginal_schedule_date = add_months(
- finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation)
- )
+def disposal_was_made_on_original_schedule_date(schedule_idx, row, posting_date_of_disposal):
+ orginal_schedule_date = add_months(
+ row.depreciation_start_date, schedule_idx * cint(row.frequency_of_depreciation)
+ )
- if orginal_schedule_date == posting_date_of_disposal:
- return True
+ if orginal_schedule_date == posting_date_of_disposal:
+ return True
+
return False
@@ -499,24 +537,27 @@
def get_value_after_depreciation_on_disposal_date(asset, disposal_date, finance_book=None):
asset_doc = frappe.get_doc("Asset", asset)
- if asset_doc.calculate_depreciation:
- asset_doc.prepare_depreciation_data(getdate(disposal_date))
-
- finance_book_id = 1
- if finance_book:
- for fb in asset_doc.finance_books:
- if fb.finance_book == finance_book:
- finance_book_id = fb.idx
- break
-
- asset_schedules = [
- sch for sch in asset_doc.schedules if cint(sch.finance_book_id) == finance_book_id
- ]
- accumulated_depr_amount = asset_schedules[-1].accumulated_depreciation_amount
-
- return flt(
- flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
- asset_doc.precision("gross_purchase_amount"),
- )
- else:
+ if not asset_doc.calculate_depreciation:
return flt(asset_doc.value_after_depreciation)
+
+ idx = 1
+ if finance_book:
+ for d in asset.finance_books:
+ if d.finance_book == finance_book:
+ idx = d.idx
+ break
+
+ row = asset_doc.finance_books[idx - 1]
+
+ temp_asset_depreciation_schedule = get_temp_asset_depr_schedule_doc(
+ asset_doc, row, getdate(disposal_date)
+ )
+
+ accumulated_depr_amount = temp_asset_depreciation_schedule.get("depreciation_schedule")[
+ -1
+ ].accumulated_depreciation_amount
+
+ return flt(
+ flt(asset_doc.gross_purchase_amount) - accumulated_depr_amount,
+ asset_doc.precision("gross_purchase_amount"),
+ )
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 2bec273..d61ef8e 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -27,6 +27,11 @@
restore_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 (
make_purchase_invoice as make_invoice,
)
@@ -205,6 +210,9 @@
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))
asset.load_from_db()
@@ -216,6 +224,11 @@
scrap_asset(asset.name)
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(
asset.gross_purchase_amount - asset.finance_books[0].value_after_depreciation,
@@ -256,6 +269,11 @@
self.assertSequenceEqual(gle, expected_gle)
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()
self.assertFalse(asset.journal_entry_for_scrap)
@@ -283,6 +301,9 @@
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))
si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company")
@@ -294,6 +315,12 @@
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(
asset.finance_books[0], 9000, get_last_day(add_months(purchase_date, 1)), date
)
@@ -370,6 +397,9 @@
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")
self.assertEqual(asset.asset_quantity, 10)
@@ -378,21 +408,31 @@
new_asset = split_asset(asset.name, 2)
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.gross_purchase_amount, 24000)
self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
self.assertEqual(new_asset.split_from, asset.name)
- self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000)
- self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000)
+ self.assertEqual(depr_schedule_of_new_asset[0].depreciation_amount, 4000)
+ self.assertEqual(depr_schedule_of_new_asset[1].depreciation_amount, 4000)
self.assertEqual(asset.asset_quantity, 8)
self.assertEqual(asset.gross_purchase_amount, 96000)
self.assertEqual(asset.opening_accumulated_depreciation, 16000)
- self.assertEqual(asset.schedules[0].depreciation_amount, 16000)
- self.assertEqual(asset.schedules[1].depreciation_amount, 16000)
+ self.assertEqual(depr_schedule_of_asset[0].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)
self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
@@ -629,7 +669,7 @@
schedules = [
[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)
@@ -651,7 +691,7 @@
expected_schedules = [["2032-12-31", 30000.0, 77095.89], ["2033-06-06", 12904.11, 90000.0]]
schedules = [
[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)
@@ -678,7 +718,7 @@
schedules = [
[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)
@@ -703,7 +743,7 @@
schedules = [
[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)
@@ -733,7 +773,7 @@
flt(d.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)
@@ -765,7 +805,7 @@
flt(d.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)
@@ -798,7 +838,7 @@
flt(d.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)
@@ -831,7 +871,7 @@
flt(d.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)
@@ -854,7 +894,7 @@
["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(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@@ -877,7 +917,7 @@
["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(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
@@ -885,7 +925,9 @@
def test_get_depreciation_amount(self):
"""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")
@@ -904,8 +946,8 @@
depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
self.assertEqual(depreciation_amount, 30000)
- def test_make_depreciation_schedule(self):
- """Tests if make_depreciation_schedule() returns the right values."""
+ def test_make_depr_schedule(self):
+ """Tests if make_depr_schedule() returns the right values."""
asset = create_asset(
item_code="Macbook Pro",
@@ -920,7 +962,7 @@
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(expected_values[i][1], schedule.depreciation_amount)
@@ -940,7 +982,7 @@
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)
def test_check_is_pro_rata(self):
@@ -1120,9 +1162,11 @@
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
- self.assertTrue(asset.schedules[0].journal_entry)
- self.assertFalse(asset.schedules[1].journal_entry)
- self.assertFalse(asset.schedules[2].journal_entry)
+ depr_schedule = get_depr_schedule(asset.name, "Active")
+
+ 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):
"""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 @@
post_depreciation_entries(date="2021-06-01")
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 = [
{"account": entry.account, "debit": entry.debit, "credit": entry.credit}
for entry in je.accounts
@@ -1177,7 +1221,7 @@
post_depreciation_entries(date="2021-06-01")
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 = [
{"account": entry.account, "debit": entry.debit, "credit": entry.credit}
for entry in je.accounts
@@ -1196,8 +1240,8 @@
depr_expense_account.parent_account = "Expenses - _TC"
depr_expense_account.save()
- def test_clear_depreciation_schedule(self):
- """Tests if clear_depreciation_schedule() works as expected."""
+ def test_clear_depr_schedule(self):
+ """Tests if clear_depr_schedule() works as expected."""
asset = create_asset(
item_code="Macbook Pro",
@@ -1213,17 +1257,20 @@
post_depreciation_entries(date="2021-06-01")
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.calculate_depreciation = 1
asset.append(
"finance_books",
{
+ "finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
@@ -1234,6 +1281,7 @@
asset.append(
"finance_books",
{
+ "finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
@@ -1244,6 +1292,7 @@
asset.append(
"finance_books",
{
+ "finance_book": "Test Finance Book 3",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
@@ -1256,15 +1305,23 @@
post_depreciation_entries(date="2020-04-01")
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:
- if schedule.idx <= 3:
- self.assertEqual(schedule.finance_book_id, "1")
- else:
- self.assertEqual(schedule.finance_book_id, "2")
+ asset_depr_schedule_doc_3 = get_asset_depr_schedule_doc(
+ asset.name, "Active", "Test Finance Book 3"
+ )
+ clear_depr_schedule(asset_depr_schedule_doc_3)
+ self.assertEqual(len(asset_depr_schedule_doc_3.get("depreciation_schedule")), 0)
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)
@@ -1273,6 +1330,7 @@
asset.append(
"finance_books",
{
+ "finance_book": "Test Finance Book 1",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
@@ -1283,6 +1341,7 @@
asset.append(
"finance_books",
{
+ "finance_book": "Test Finance Book 2",
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
@@ -1292,13 +1351,15 @@
)
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:
- if schedule.idx <= 3:
- self.assertEqual(schedule.finance_book_id, 1)
- else:
- self.assertEqual(schedule.finance_book_id, 2)
+ asset_depr_schedule_doc_2 = get_asset_depr_schedule_doc(
+ asset.name, "Draft", "Test Finance Book 2"
+ )
+ self.assertEqual(len(asset_depr_schedule_doc_2.get("depreciation_schedule")), 6)
def test_depreciation_entry_cancellation(self):
asset = create_asset(
@@ -1318,12 +1379,12 @@
asset.load_from_db()
# 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)
+
frappe.get_doc("Journal Entry", depr_entry).cancel()
- asset.load_from_db()
- depr_entry = asset.get("schedules")[0].journal_entry
+ depr_entry = get_depr_schedule(asset.name, "Active")[0].journal_entry
self.assertFalse(depr_entry)
def test_asset_expected_value_after_useful_life(self):
@@ -1338,7 +1399,7 @@
)
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(
@@ -1369,7 +1430,7 @@
asset.load_from_db()
# 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 = (
("_Test Accumulated Depreciations - _TC", 0.0, 30000.0),
@@ -1439,7 +1500,7 @@
"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))
@@ -1453,6 +1514,15 @@
if not frappe.db.exists("Location", "Test Location"):
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):
args = frappe._dict(args)
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 08355f0..7d3b645 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -7,7 +7,7 @@
# import erpnext
from frappe import _
-from frappe.utils import cint, flt
+from frappe.utils import cint, flt, get_link_to_form
from six import string_types
import erpnext
@@ -19,6 +19,9 @@
reverse_depreciation_entry_made_after_disposal,
)
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 (
get_current_asset_value,
)
@@ -427,7 +430,12 @@
asset = self.get_asset(item)
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()
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(
@@ -513,7 +521,12 @@
asset_doc.purchase_date = self.posting_date
asset_doc.gross_purchase_amount = total_target_asset_value
asset_doc.purchase_receipt_amount = total_target_asset_value
- asset_doc.prepare_depreciation_data()
+ 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.save()
elif self.docstatus == 2:
@@ -524,7 +537,12 @@
if asset.calculate_depreciation:
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):
asset = frappe.get_doc("Asset", item.asset)
diff --git a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
index 86861f0..4d519a6 100644
--- a/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/test_asset_capitalization.py
@@ -12,6 +12,9 @@
create_asset_data,
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
@@ -253,6 +256,9 @@
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
asset_capitalization = create_asset_capitalization(
entry_type="Decapitalization",
@@ -282,8 +288,18 @@
consumed_asset.reload()
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 = [
- 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(
consumed_depreciation_schedule and consumed_depreciation_schedule[0].journal_entry
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/__init__.py b/erpnext/assets/doctype/asset_depreciation_schedule/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/__init__.py
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
new file mode 100644
index 0000000..c28b2b3
--- /dev/null
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.js
@@ -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);
+ })
+};
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
new file mode 100644
index 0000000..af09cda
--- /dev/null
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
new file mode 100644
index 0000000..1446a6e
--- /dev/null
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -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")
+ )
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
new file mode 100644
index 0000000..024121d
--- /dev/null
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/test_asset_depreciation_schedule.py
@@ -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)
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py
index d5913c5..b8cd115 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.py
@@ -3,11 +3,15 @@
import frappe
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
from erpnext.accounts.general_ledger import make_gl_entries
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
@@ -52,8 +56,11 @@
):
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.prepare_depreciation_data()
+ make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
def before_cancel(self):
@@ -73,8 +80,11 @@
):
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.prepare_depreciation_data()
+ make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
self.asset_doc.save()
def check_repair_status(self):
@@ -279,8 +289,10 @@
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
- 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
asset.to_date = add_months(last_schedule_date, extra_months)
@@ -310,8 +322,10 @@
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
- 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
asset.to_date = add_months(last_schedule_date, -extra_months)
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 6e06f52..ff72aa9 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -12,6 +12,9 @@
create_asset_data,
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
@@ -232,13 +235,23 @@
def test_increase_in_asset_life(self):
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)
create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1)
+
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(
- asset.schedules[-1].accumulated_depreciation_amount,
+ second_asset_depr_schedule.get("depreciation_schedule")[-1].accumulated_depreciation_amount,
asset.finance_books[0].value_after_depreciation,
)
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 84aa8fa..262d552 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -5,13 +5,17 @@
import frappe
from frappe import _
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 (
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_schedule.asset_depreciation_schedule import (
+ get_asset_depr_schedule_doc,
+ get_depreciation_amount,
+ set_accumulated_depreciation,
+)
class AssetValueAdjustment(Document):
@@ -112,21 +116,40 @@
for d in asset.finance_books:
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"):
- 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)
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
from_date = self.date
else:
- no_of_depreciations = len(
- [
- s.name for s in asset.schedules if (cint(s.finance_book_id) == d.idx and not s.journal_entry)
- ]
- )
+ no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry])
value_after_depreciation = d.value_after_depreciation
- for data in asset.schedules:
- if cint(data.finance_book_id) == d.idx and not data.journal_entry:
+ for data in depr_schedule:
+ if not data.journal_entry:
if d.depreciation_method in ("Straight Line", "Manual"):
days = date_diff(data.schedule_date, from_date)
depreciation_amount = days * rate_per_day
@@ -140,10 +163,12 @@
d.db_update()
- asset.set_accumulated_depreciation(ignore_booked_entry=True)
- for asset_data in asset.schedules:
- if not asset_data.journal_entry:
- asset_data.db_update()
+ set_accumulated_depreciation(new_asset_depr_schedule_doc, d, ignore_booked_entry=True)
+ for asset_data in depr_schedule:
+ if not asset_data.journal_entry:
+ asset_data.db_update()
+
+ new_asset_depr_schedule_doc.submit()
@frappe.whitelist()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
index 62c6366..03dcea9 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
@@ -7,6 +7,9 @@
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_depreciation_schedule.asset_depreciation_schedule import (
+ get_asset_depr_schedule_doc,
+)
from erpnext.assets.doctype.asset_value_adjustment.asset_value_adjustment import (
get_current_asset_value,
)
@@ -73,12 +76,21 @@
)
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)
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name, current_asset_value=current_value, new_asset_value=50000.0
)
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 = (
("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
("_Test Depreciations - _TC", 50000.0, 0.0),
diff --git a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
index 35a2c9d..882c4bf 100644
--- a/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
+++ b/erpnext/assets/doctype/depreciation_schedule/depreciation_schedule.json
@@ -1,318 +1,84 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "autoname": "",
- "beta": 0,
- "creation": "2016-03-02 15:11:01.278862",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2016-03-02 15:11:01.278862",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "schedule_date",
+ "depreciation_amount",
+ "column_break_3",
+ "accumulated_depreciation_amount",
+ "journal_entry",
+ "make_depreciation_entry",
+ "depreciation_method"
+ ],
"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
- },
+ "fieldname": "schedule_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Schedule Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "schedule_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_standard_filter": 0,
- "label": "Schedule Date",
- "length": 0,
- "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
- },
+ "fieldname": "depreciation_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Depreciation Amount",
+ "options": "Company:company:default_currency",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "depreciation_amount",
- "fieldtype": "Currency",
- "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": "Depreciation Amount",
- "length": 0,
- "no_copy": 1,
- "options": "Company:company:default_currency",
- "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
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "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
- },
+ "fieldname": "accumulated_depreciation_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Accumulated Depreciation Amount",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "accumulated_depreciation_amount",
- "fieldtype": "Currency",
- "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": "Accumulated Depreciation Amount",
- "length": 0,
- "no_copy": 1,
- "options": "Company:company:default_currency",
- "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
- },
+ "depends_on": "eval:doc.docstatus==1",
+ "fieldname": "journal_entry",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Journal Entry",
+ "options": "Journal Entry",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.docstatus==1",
- "fieldname": "journal_entry",
- "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": "Journal Entry",
- "length": 0,
- "no_copy": 1,
- "options": "Journal Entry",
- "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_on_submit": 1,
+ "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())",
+ "fieldname": "make_depreciation_entry",
+ "fieldtype": "Button",
+ "label": "Make Depreciation Entry"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:(doc.docstatus==1 && !doc.journal_entry && doc.schedule_date <= get_today())",
- "fieldname": "make_depreciation_entry",
- "fieldtype": "Button",
- "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,
- "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",
- "fieldtype": "Select",
- "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",
- "length": 0,
- "no_copy": 1,
- "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
- "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
+ "fieldname": "depreciation_method",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Depreciation Method",
+ "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "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,
- "max_attachments": 0,
- "modified": "2018-05-10 15:12:41.679436",
- "modified_by": "Administrator",
- "module": "Assets",
- "name": "Depreciation Schedule",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2022-12-06 20:35:50.264281",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Depreciation Schedule",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index 6b14dce..bb50df0 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -176,15 +176,17 @@
return frappe._dict(
frappe.db.sql(
""" Select
- parent, SUM(depreciation_amount)
- FROM `tabDepreciation Schedule`
+ ads.asset, SUM(depreciation_amount)
+ FROM `tabAsset Depreciation Schedule` ads, `tabDepreciation Schedule` ds
WHERE
- parentfield='schedules'
- AND schedule_date<=%s
- AND journal_entry IS NOT NULL
- AND ifnull(finance_book, '')=%s
- GROUP BY parent""",
- (date, cstr(filters.finance_book or "")),
+ ds.parent = ads.name
+ AND ifnull(ads.finance_book, '')=%s
+ AND ads.docstatus=1
+ AND ds.parentfield='depreciation_schedule'
+ AND ds.schedule_date<=%s
+ AND ds.journal_entry IS NOT NULL
+ GROUP BY ads.asset""",
+ (cstr(filters.finance_book or ""), date),
)
)
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 28158a3..34417f7 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -9,8 +9,8 @@
"supplier_and_price_defaults_section",
"supp_master_name",
"supplier_group",
- "column_break_4",
"buying_price_list",
+ "column_break_4",
"maintain_same_rate_action",
"role_to_override_stop_action",
"transaction_settings_section",
@@ -20,6 +20,7 @@
"maintain_same_rate",
"allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice",
+ "disable_last_purchase_rate",
"subcontract",
"backflush_raw_materials_of_subcontract_based_on",
"column_break_11",
@@ -71,7 +72,7 @@
},
{
"fieldname": "subcontract",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Subcontracting Settings"
},
{
@@ -118,8 +119,8 @@
},
{
"fieldname": "supplier_and_price_defaults_section",
- "fieldtype": "Section Break",
- "label": "Supplier and Price Defaults"
+ "fieldtype": "Tab Break",
+ "label": "Naming Series and Price Defaults"
},
{
"fieldname": "column_break_4",
@@ -127,12 +128,18 @@
},
{
"fieldname": "transaction_settings_section",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Transaction Settings"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_last_purchase_rate",
+ "fieldtype": "Check",
+ "label": "Disable Last Purchase Rate"
}
],
"icon": "fa fa-cog",
@@ -140,7 +147,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-09-27 10:50:27.050252",
+ "modified": "2023-01-09 17:08:28.828173",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 06fdea0..47089f7 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -235,11 +235,11 @@
cur_frm.add_custom_button(__('Purchase Invoice'),
this.make_purchase_invoice, __('Create'));
- if(flt(doc.per_billed)==0 && doc.status != "Delivered") {
+ if(flt(doc.per_billed) < 100 && doc.status != "Delivered") {
cur_frm.add_custom_button(__('Payment'), cur_frm.cscript.make_payment_entry, __('Create'));
}
- if(flt(doc.per_billed)==0) {
+ if(flt(doc.per_billed) < 100) {
this.frm.add_custom_button(__('Payment Request'),
function() { me.make_payment_request() }, __('Create'));
}
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 9349626..e1dd679 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -62,12 +62,13 @@
"set_reserve_warehouse",
"supplied_items",
"taxes_section",
+ "tax_category",
"taxes_and_charges",
"column_break_53",
- "tax_category",
- "column_break_50",
"shipping_rule",
+ "column_break_50",
"incoterm",
+ "named_place",
"section_break_52",
"taxes",
"totals",
@@ -107,7 +108,7 @@
"contact_display",
"contact_mobile",
"contact_email",
- "company_shipping_address_section",
+ "shipping_address_section",
"shipping_address",
"column_break_99",
"shipping_address_display",
@@ -384,7 +385,7 @@
{
"fieldname": "shipping_address",
"fieldtype": "Link",
- "label": "Company Shipping Address",
+ "label": "Shipping Address",
"options": "Address",
"print_hide": 1
},
@@ -1207,11 +1208,6 @@
"label": "Address & Contact"
},
{
- "fieldname": "company_shipping_address_section",
- "fieldtype": "Section Break",
- "label": "Company Shipping Address"
- },
- {
"fieldname": "company_billing_address_section",
"fieldtype": "Section Break",
"label": "Company Billing Address"
@@ -1256,13 +1252,24 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
+ },
+ {
+ "fieldname": "shipping_address_section",
+ "fieldtype": "Section Break",
+ "label": "Shipping Address"
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-17 17:28:07.729943",
+ "modified": "2022-12-25 18:08:59.074182",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 4c10b48..2415aec 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -207,31 +207,32 @@
)
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:
+ if not item.fg_item:
+ frappe.throw(
+ _("Row #{0}: Finished Good Item is not specified for service item {1}").format(
+ item.idx, item.item_code
+ )
+ )
+ else:
+ if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
+ frappe.throw(
+ _("Row #{0}: Finished Good Item {1} must be a sub-contracted item").format(
+ item.idx, item.fg_item
+ )
+ )
+ elif not frappe.get_value("Item", item.fg_item, "default_bom"):
+ frappe.throw(
+ _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
+ )
+ if not item.fg_item_qty:
+ frappe.throw(_("Row #{0}: Finished Good Item Qty can not be zero").format(item.idx))
+ else:
for item in self.items:
- if not item.fg_item:
- frappe.throw(
- _("Row #{0}: Finished Good Item is not specified for service item {1}").format(
- item.idx, item.item_code
- )
- )
- else:
- if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
- frappe.throw(
- _(
- "Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}"
- ).format(item.idx, item.fg_item, item.item_code)
- )
- elif not frappe.get_value("Item", item.fg_item, "default_bom"):
- frappe.throw(
- _("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
- )
- if not item.fg_item_qty:
- frappe.throw(
- _("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format(
- item.idx, item.item_code
- )
- )
+ item.set("fg_item", None)
+ item.set("fg_item_qty", 0)
def get_schedule_dates(self):
for d in self.get("items"):
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 291d756..572d9d3 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -743,9 +743,9 @@
pe = get_payment_entry("Purchase Order", po_doc.name)
pe.mode_of_payment = "Cash"
pe.paid_from = "Cash - _TC"
- pe.source_exchange_rate = 80
- pe.target_exchange_rate = 1
- pe.paid_amount = po_doc.grand_total
+ pe.source_exchange_rate = 1
+ pe.target_exchange_rate = 80
+ pe.paid_amount = po_doc.base_grand_total
pe.save(ignore_permissions=True)
pe.submit()
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index 98c7dc9..a9f5afb 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -57,44 +57,96 @@
});
}, __("Tools"));
- frm.add_custom_button(__('Download PDF'), () => {
- var suppliers = [];
- const fields = [{
- fieldtype: 'Link',
- label: __('Select a Supplier'),
- fieldname: 'supplier',
- options: 'Supplier',
- reqd: 1,
- get_query: () => {
- return {
- filters: [
- ["Supplier", "name", "in", frm.doc.suppliers.map((row) => {return row.supplier;})]
- ]
- }
- }
- }];
-
- frappe.prompt(fields, data => {
- var child = locals[cdt][cdn]
-
- var w = window.open(
- frappe.urllib.get_full_url("/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?"
- +"doctype="+encodeURIComponent(frm.doc.doctype)
- +"&name="+encodeURIComponent(frm.doc.name)
- +"&supplier="+encodeURIComponent(data.supplier)
- +"&no_letterhead=0"));
- if(!w) {
- frappe.msgprint(__("Please enable pop-ups")); return;
- }
+ frm.add_custom_button(
+ __("Download PDF"),
+ () => {
+ frappe.prompt(
+ [
+ {
+ fieldtype: "Link",
+ label: "Select a Supplier",
+ fieldname: "supplier",
+ options: "Supplier",
+ reqd: 1,
+ default: frm.doc.suppliers?.length == 1 ? frm.doc.suppliers[0].supplier : "",
+ get_query: () => {
+ return {
+ filters: [
+ [
+ "Supplier",
+ "name",
+ "in",
+ frm.doc.suppliers.map((row) => {
+ return row.supplier;
+ }),
+ ],
+ ],
+ };
+ },
+ },
+ {
+ fieldtype: "Section Break",
+ label: "Print Settings",
+ fieldname: "print_settings",
+ collapsible: 1,
+ },
+ {
+ fieldtype: "Link",
+ label: "Print Format",
+ fieldname: "print_format",
+ options: "Print Format",
+ placeholder: "Standard",
+ get_query: () => {
+ return {
+ filters: {
+ doc_type: "Request for Quotation",
+ },
+ };
+ },
+ },
+ {
+ fieldtype: "Link",
+ label: "Language",
+ fieldname: "language",
+ options: "Language",
+ default: frappe.boot.lang,
+ },
+ {
+ fieldtype: "Link",
+ label: "Letter Head",
+ fieldname: "letter_head",
+ options: "Letter Head",
+ default: frm.doc.letter_head,
+ },
+ ],
+ (data) => {
+ var w = window.open(
+ frappe.urllib.get_full_url(
+ "/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" +
+ new URLSearchParams({
+ doctype: frm.doc.doctype,
+ name: frm.doc.name,
+ supplier: data.supplier,
+ print_format: data.print_format || "Standard",
+ language: data.language || frappe.boot.lang,
+ letter_head: data.letter_head || frm.doc.letter_head || "",
+ }).toString()
+ )
+ );
+ if (!w) {
+ frappe.msgprint(__("Please enable pop-ups"));
+ return;
+ }
+ },
+ "Download PDF for Supplier",
+ "Download"
+ );
},
- 'Download PDF for Supplier',
- 'Download');
- },
- __("Tools"));
+ __("Tools")
+ );
- frm.page.set_inner_btn_group_as_primary(__('Create'));
+ frm.page.set_inner_btn_group_as_primary(__("Create"));
}
-
},
make_supplier_quotation: function(frm) {
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index bdbc9ce..8e9ded9 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -216,6 +216,7 @@
recipients=data.email_id,
sender=sender,
attachments=attachments,
+ print_format=self.meta.default_print_format or "Standard",
send_email=True,
doctype=self.doctype,
name=self.name,
@@ -224,9 +225,7 @@
frappe.msgprint(_("Email Sent to Supplier {0}").format(data.supplier))
def get_attachments(self):
- attachments = [d.name for d in get_attachments(self.doctype, self.name)]
- attachments.append(frappe.attach_print(self.doctype, self.name, doc=self))
- return attachments
+ return [d.name for d in get_attachments(self.doctype, self.name)]
def update_rfq_supplier_status(self, sup_name=None):
for supplier in self.suppliers:
@@ -389,10 +388,17 @@
@frappe.whitelist()
-def get_pdf(doctype, name, supplier):
- doc = get_rfq_doc(doctype, name, supplier)
- if doc:
- download_pdf(doctype, name, doc=doc)
+def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None):
+ # permissions get checked in `download_pdf`
+ if doc := get_rfq_doc(doctype, name, supplier):
+ download_pdf(
+ doctype,
+ name,
+ print_format,
+ doc=doc,
+ language=language,
+ letter_head=letter_head or None,
+ )
def get_rfq_doc(doctype, name, supplier):
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index bebff1c..120b2f8 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -20,9 +20,6 @@
class Supplier(TransactionBase):
- def get_feed(self):
- return self.supplier_name
-
def onload(self):
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 7776ab8..c5b369b 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -40,12 +40,13 @@
"total",
"net_total",
"taxes_section",
+ "tax_category",
"taxes_and_charges",
"column_break_34",
- "tax_category",
- "column_break_36",
"shipping_rule",
+ "column_break_36",
"incoterm",
+ "named_place",
"section_break_38",
"taxes",
"totals",
@@ -830,6 +831,12 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
}
],
"icon": "fa fa-shopping-cart",
@@ -837,7 +844,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-17 17:27:32.179686",
+ "modified": "2022-12-12 18:35:39.740974",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 5a051e3..788dc49 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -197,7 +197,7 @@
validate_einvoice_fields(self)
- if self.doctype != "Material Request":
+ if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self)
def before_cancel(self):
@@ -584,7 +584,12 @@
if bool(uom) != bool(stock_uom): # xor
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":
self.set_expense_account(for_validate)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 2efa545..54f0d94 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -25,10 +25,6 @@
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
- def get_feed(self):
- if self.get("supplier_name"):
- return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
-
def validate(self):
super(BuyingController, self).validate()
if getattr(self, "supplier", None) and not self.supplier_name:
@@ -278,6 +274,9 @@
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
return
+ if not self.is_internal_transfer():
+ return
+
ref_doctype_map = {
"Purchase Order": "Sales Order Item",
"Purchase Receipt": "Delivery Note Item",
@@ -548,7 +547,9 @@
self.process_fixed_asset()
self.update_fixed_asset(field)
- if self.doctype in ["Purchase Order", "Purchase Receipt"]:
+ if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
+ "Buying Settings", "disable_last_purchase_rate"
+ ):
update_last_purchase_rate(self, is_submit=1)
def on_cancel(self):
@@ -557,7 +558,9 @@
if self.get("is_return"):
return
- if self.doctype in ["Purchase Order", "Purchase Receipt"]:
+ if self.doctype in ["Purchase Order", "Purchase Receipt"] and not frappe.db.get_single_value(
+ "Buying Settings", "disable_last_purchase_rate"
+ ):
update_last_purchase_rate(self, is_submit=0)
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 0ebc8d4..cd1168d 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -19,14 +19,11 @@
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"]
- def get_feed(self):
- return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total)
-
def onload(self):
super(SellingController, self).onload()
if self.doctype in ("Sales Order", "Delivery Note", "Sales Invoice"):
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):
super(SellingController, self).validate()
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 6e7d2b3..d497297 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -347,16 +347,21 @@
)
def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
- action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling")
+ if qty_or_amount == "qty":
+ msg = _("Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.")
+ else:
+ msg = _("Overbilling of {0} {1} ignored for item {2} because you have {3} role.")
- msg = _("{} of {} {} ignored for item {} because you have {} role.").format(
- action,
- _(item["target_ref_field"].title()),
- frappe.bold(item["reduce_by"]),
- frappe.bold(item.get("item_code")),
- role,
+ frappe.msgprint(
+ msg.format(
+ _(item["target_ref_field"].title()),
+ frappe.bold(item["reduce_by"]),
+ frappe.bold(item.get("item_code")),
+ role,
+ ),
+ indicator="orange",
+ alert=True,
)
- frappe.msgprint(msg, indicator="orange", alert=True)
def update_qty(self, update_modified=True):
"""Updates qty or amount at row level
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 8d67e30..a9561fe 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -74,24 +74,25 @@
)
if not is_stock_item:
- msg = f"Item {item.item_name} must be a stock item."
- frappe.throw(_(msg))
+ frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name))
if not is_sub_contracted_item:
- msg = f"Item {item.item_name} must be a subcontracted item."
- frappe.throw(_(msg))
+ frappe.throw(
+ _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
+ )
if item.bom:
bom = frappe.get_doc("BOM", item.bom)
if not bom.is_active:
- msg = f"Please select an active BOM for Item {item.item_name}."
- frappe.throw(_(msg))
+ frappe.throw(
+ _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name)
+ )
if bom.item != item.item_code:
- msg = f"Please select an valid BOM for Item {item.item_name}."
- frappe.throw(_(msg))
+ frappe.throw(
+ _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name)
+ )
else:
- msg = f"Please select a BOM for Item {item.item_name}."
- frappe.throw(_(msg))
+ frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name))
def __get_data_before_save(self):
item_dict = {}
@@ -829,6 +830,9 @@
order_doctype: {
"doctype": "Stock Entry",
"field_map": {
+ "supplier": "supplier",
+ "supplier_name": "supplier_name",
+ "supplier_address": "supplier_address",
"to_warehouse": "supplier_warehouse",
},
"field_no_map": [field_no_map],
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
index fe7b4e1..c26b064 100644
--- a/erpnext/crm/doctype/appointment/appointment.json
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -102,7 +102,7 @@
}
],
"links": [],
- "modified": "2021-06-30 13:09:14.228756",
+ "modified": "2022-12-15 11:11:02.131986",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
@@ -123,16 +123,6 @@
},
{
"create": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Guest",
- "share": 1
- },
- {
- "create": 1,
"delete": 1,
"email": 1,
"export": 1,
@@ -170,5 +160,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
index 6e7ba1f..bd49bdc 100644
--- a/erpnext/crm/doctype/appointment/appointment.py
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -6,7 +6,9 @@
import frappe
from frappe import _
+from frappe.desk.form.assign_to import add as add_assignment
from frappe.model.document import Document
+from frappe.share import add_docshare
from frappe.utils import get_url, getdate, now
from frappe.utils.verified_command import get_signed_params
@@ -130,21 +132,18 @@
self.party = lead.name
def auto_assign(self):
- from frappe.desk.form.assign_to import add as add_assignemnt
-
existing_assignee = self.get_assignee_from_latest_opportunity()
if existing_assignee:
# If the latest opportunity is assigned to someone
# Assign the appointment to the same
- add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [existing_assignee]})
+ self.assign_agent(existing_assignee)
return
if self._assign:
return
available_agents = _get_agents_sorted_by_asc_workload(getdate(self.scheduled_time))
for agent in available_agents:
if _check_agent_availability(agent, self.scheduled_time):
- agent = agent[0]
- add_assignemnt({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
+ self.assign_agent(agent[0])
break
def get_assignee_from_latest_opportunity(self):
@@ -199,9 +198,15 @@
params = {"email": self.customer_email, "appointment": self.name}
return get_url(verify_route + "?" + get_signed_params(params))
+ def assign_agent(self, agent):
+ if not frappe.has_permission(doc=self, user=agent):
+ add_docshare(self.doctype, self.name, agent, flags={"ignore_share_permission": True})
+
+ add_assignment({"doctype": self.doctype, "name": self.name, "assign_to": [agent]})
+
def _get_agents_sorted_by_asc_workload(date):
- appointments = frappe.db.get_list("Appointment", fields="*")
+ appointments = frappe.get_all("Appointment", fields="*")
agent_list = _get_agent_list_as_strings()
if not appointments:
return agent_list
@@ -226,7 +231,7 @@
def _check_agent_availability(agent_email, scheduled_time):
- appointemnts_at_scheduled_time = frappe.get_list(
+ appointemnts_at_scheduled_time = frappe.get_all(
"Appointment", filters={"scheduled_time": scheduled_time}
)
for appointment in appointemnts_at_scheduled_time:
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
index 4b26e49..436eb10 100644
--- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-08-27 10:56:48.309824",
"doctype": "DocType",
"editable_grid": 1,
@@ -101,7 +102,8 @@
}
],
"issingle": 1,
- "modified": "2019-11-26 12:14:17.669366",
+ "links": [],
+ "modified": "2022-12-15 11:10:13.517742",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment Booking Settings",
@@ -118,13 +120,6 @@
"write": 1
},
{
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Guest",
- "share": 1
- },
- {
"create": 1,
"email": 1,
"print": 1,
@@ -147,5 +142,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index 0d12499..b0ff5d4 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -14,9 +14,6 @@
class Lead(SellingController, CRMNote):
- def get_feed(self):
- return "{0}: {1}".format(_(self.status), self.lead_name)
-
def onload(self):
customer = frappe.db.get_value("Customer", {"lead_name": self.name})
self.get("__onload").is_customer = customer
@@ -453,6 +450,7 @@
"Lead",
or_filters={
"phone": ["like", "%{}".format(number)],
+ "whatsapp_no": ["like", "%{}".format(number)],
"mobile_no": ["like", "%{}".format(number)],
},
limit=1,
diff --git a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js
index 69b9cfa..c37fa2f 100644
--- a/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js
+++ b/erpnext/e_commerce/doctype/e_commerce_settings/e_commerce_settings.js
@@ -48,5 +48,11 @@
frm.set_value('default_customer_group', '');
frm.set_value('quotation_series', '');
}
+ },
+
+ enable_checkout: function(frm) {
+ if (frm.doc.enable_checkout) {
+ erpnext.utils.check_payments_app();
+ }
}
});
diff --git a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
index 72f47b5..0d42ca8 100644
--- a/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
+++ b/erpnext/erpnext_integrations/doctype/exotel_settings/exotel_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-05-21 07:41:53.536536",
"doctype": "DocType",
"engine": "InnoDB",
@@ -7,10 +8,14 @@
"section_break_2",
"account_sid",
"api_key",
- "api_token"
+ "api_token",
+ "section_break_6",
+ "map_custom_field_to_doctype",
+ "target_doctype"
],
"fields": [
{
+ "default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
@@ -18,7 +23,8 @@
{
"depends_on": "enabled",
"fieldname": "section_break_2",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Credentials"
},
{
"fieldname": "account_sid",
@@ -34,10 +40,31 @@
"fieldname": "api_key",
"fieldtype": "Data",
"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,
- "modified": "2019-05-22 06:25:18.026997",
+ "links": [],
+ "modified": "2022-12-14 17:24:50.176107",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Exotel Settings",
@@ -57,5 +84,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js
index b649d9d..2411297 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.js
@@ -2,4 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on('GoCardless Settings', {
+ refresh: function(frm) {
+ erpnext.utils.check_payments_app();
+ }
});
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json
index 9738106..cca3653 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.json
@@ -173,7 +173,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2018-02-12 14:18:47.209114",
+ "modified": "2022-02-12 14:18:47.209114",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "GoCardless Settings",
@@ -201,7 +201,6 @@
"write": 1
}
],
- "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
diff --git a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
index f9a293f..4a29a6a 100644
--- a/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
+++ b/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py
@@ -10,7 +10,8 @@
from frappe.integrations.utils import create_request_log
from frappe.model.document import Document
from frappe.utils import call_hook_method, cint, flt, get_url
-from payments.utils import create_payment_gateway
+
+from erpnext.utilities import payment_app_import_guard
class GoCardlessSettings(Document):
@@ -30,6 +31,9 @@
frappe.throw(e)
def on_update(self):
+ with payment_app_import_guard():
+ from payments.utils import create_payment_gateway
+
create_payment_gateway(
"GoCardless-" + self.gateway_name, settings="GoCardLess Settings", controller=self.gateway_name
)
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js
index 7c8ae5c..447d720 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.js
@@ -7,6 +7,8 @@
},
refresh: function(frm) {
+ erpnext.utils.check_payments_app();
+
frappe.realtime.on("refresh_mpesa_dashboard", function(){
frm.reload_doc();
frm.events.setup_account_balance_html(frm);
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
index b534783..a298e11 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
@@ -9,13 +9,13 @@
from frappe.integrations.utils import create_request_log
from frappe.model.document import Document
from frappe.utils import call_hook_method, fmt_money, get_request_site_address
-from payments.utils import create_payment_gateway
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_connector import MpesaConnector
from erpnext.erpnext_integrations.doctype.mpesa_settings.mpesa_custom_fields import (
create_custom_pos_fields,
)
from erpnext.erpnext_integrations.utils import create_mode_of_payment
+from erpnext.utilities import payment_app_import_guard
class MpesaSettings(Document):
@@ -30,6 +30,9 @@
)
def on_update(self):
+ with payment_app_import_guard():
+ from payments.utils import create_payment_gateway
+
create_custom_pos_fields()
create_payment_gateway(
"Mpesa-" + self.payment_gateway_name,
diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py
index fd0f783..0d40667 100644
--- a/erpnext/erpnext_integrations/exotel_integration.py
+++ b/erpnext/erpnext_integrations/exotel_integration.py
@@ -72,6 +72,24 @@
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):
call_log = frappe.new_doc("Call Log")
call_log.id = call_payload.get("CallSid")
@@ -79,6 +97,7 @@
call_log.medium = call_payload.get("To")
call_log.status = "Ringing"
setattr(call_log, "from", call_payload.get("CallFrom"))
+ map_custom_field(call_payload, call_log)
call_log.save(ignore_permissions=True)
frappe.db.commit()
return call_log
@@ -93,10 +112,10 @@
@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")
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()
diff --git a/erpnext/erpnext_integrations/stripe_integration.py b/erpnext/erpnext_integrations/stripe_integration.py
index 2d7e8a5..634e5c2 100644
--- a/erpnext/erpnext_integrations/stripe_integration.py
+++ b/erpnext/erpnext_integrations/stripe_integration.py
@@ -2,12 +2,16 @@
# For license information, please see license.txt
import frappe
-import stripe
from frappe import _
from frappe.integrations.utils import create_request_log
+from erpnext.utilities import payment_app_import_guard
+
def create_stripe_subscription(gateway_controller, data):
+ with payment_app_import_guard():
+ import stripe
+
stripe_settings = frappe.get_doc("Stripe Settings", gateway_controller)
stripe_settings.data = frappe._dict(data)
@@ -35,6 +39,9 @@
def create_subscription_on_stripe(stripe_settings):
+ with payment_app_import_guard():
+ import stripe
+
items = []
for payment_plan in stripe_settings.payment_plans:
plan = frappe.db.get_value("Subscription Plan", payment_plan.plan, "product_price_id")
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 92601b3..fd19d25 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -8,7 +8,6 @@
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
-required_apps = ["payments"]
develop_version = "14.x.x-develop"
diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
index 0d319bf..b900b21 100644
--- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
+++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.py
@@ -10,9 +10,6 @@
class MaintenanceVisit(TransactionBase):
- def get_feed(self):
- return _("To {0}").format(self.customer_name)
-
def validate_serial_no(self):
for d in self.get("purposes"):
if d.serial_no and not frappe.db.exists("Serial No", d.serial_no):
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index ecad41f..4dd8205 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -4,7 +4,7 @@
frappe.provide("erpnext.bom");
frappe.ui.form.on("BOM", {
- setup: function(frm) {
+ setup(frm) {
frm.custom_make_buttons = {
'Work Order': 'Work Order',
'Quality Inspection': 'Quality Inspection'
@@ -65,11 +65,11 @@
});
},
- onload_post_render: function(frm) {
+ onload_post_render(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},
- refresh: function(frm) {
+ refresh(frm) {
frm.toggle_enable("item", frm.doc.__islocal);
frm.set_indicator_formatter('item_code',
@@ -152,7 +152,7 @@
}
},
- make_work_order: function(frm) {
+ make_work_order(frm) {
frm.events.setup_variant_prompt(frm, "Work Order", (frm, item, data, variant_items) => {
frappe.call({
method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order",
@@ -164,7 +164,7 @@
variant_items: variant_items
},
freeze: true,
- callback: function(r) {
+ callback(r) {
if(r.message) {
let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
@@ -174,7 +174,7 @@
});
},
- make_variant_bom: function(frm) {
+ make_variant_bom(frm) {
frm.events.setup_variant_prompt(frm, "Variant BOM", (frm, item, data, variant_items) => {
frappe.call({
method: "erpnext.manufacturing.doctype.bom.bom.make_variant_bom",
@@ -185,7 +185,7 @@
variant_items: variant_items
},
freeze: true,
- callback: function(r) {
+ callback(r) {
if(r.message) {
let doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
@@ -195,7 +195,7 @@
}, true);
},
- setup_variant_prompt: function(frm, title, callback, skip_qty_field) {
+ setup_variant_prompt(frm, title, callback, skip_qty_field) {
const fields = [];
if (frm.doc.has_variants) {
@@ -205,7 +205,7 @@
fieldname: 'item',
options: "Item",
reqd: 1,
- get_query: function() {
+ get_query() {
return {
query: "erpnext.controllers.queries.item_query",
filters: {
@@ -273,7 +273,7 @@
fieldtype: "Link",
in_list_view: 1,
reqd: 1,
- get_query: function(data) {
+ get_query(data) {
if (!data.item_code) {
frappe.throw(__("Select template item"));
}
@@ -308,7 +308,7 @@
],
in_place_edit: true,
data: [],
- get_data: function () {
+ get_data () {
return [];
},
});
@@ -343,14 +343,14 @@
}
},
- make_quality_inspection: function(frm) {
+ make_quality_inspection(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection",
frm: frm
})
},
- update_cost: function(frm, save_doc=false) {
+ update_cost(frm, save_doc=false) {
return frappe.call({
doc: frm.doc,
method: "update_cost",
@@ -360,26 +360,26 @@
save: save_doc,
from_child_bom: false
},
- callback: function(r) {
+ callback(r) {
refresh_field("items");
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)) {
frm.set_value("plc_conversion_rate", 1.0);
}
},
- routing: function(frm) {
+ routing(frm) {
if (frm.doc.routing) {
frappe.call({
doc: frm.doc,
method: "get_routing",
freeze: true,
- callback: function(r) {
+ callback(r) {
if (!r.exc) {
frm.refresh_fields();
erpnext.bom.calculate_op_cost(frm.doc);
@@ -388,6 +388,16 @@
}
});
}
+ },
+
+ 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 @@
},
callback: function(r) {
d = locals[cdt][cdn];
- if (d.is_process_loss) {
- r.message.rate = 0;
- r.message.base_rate = 0;
- }
$.extend(d, r.message);
refresh_field("items");
@@ -717,10 +723,6 @@
frappe.ui.form.on("BOM Scrap Item", {
item_code(frm, 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);
- }
},
});
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index 0b44196..c31b69f 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -6,6 +6,7 @@
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "production_item_tab",
"item",
"company",
"item_name",
@@ -19,14 +20,15 @@
"quantity",
"image",
"currency_detail",
- "currency",
- "conversion_rate",
- "column_break_12",
"rm_cost_as_per",
"buying_price_list",
"price_list_currency",
"plc_conversion_rate",
+ "column_break_ivyw",
+ "currency",
+ "conversion_rate",
"section_break_21",
+ "operations_section_section",
"with_operations",
"column_break_23",
"transfer_material_against",
@@ -34,13 +36,14 @@
"operations_section",
"operations",
"materials_section",
- "inspection_required",
- "quality_inspection_template",
- "column_break_31",
- "section_break_33",
"items",
"scrap_section",
+ "scrap_items_section",
"scrap_items",
+ "process_loss_section",
+ "process_loss_percentage",
+ "column_break_ssj2",
+ "process_loss_qty",
"costing",
"operating_cost",
"raw_material_cost",
@@ -52,10 +55,14 @@
"column_break_26",
"total_cost",
"base_total_cost",
- "section_break_25",
+ "more_info_tab",
"description",
"column_break_27",
"has_variants",
+ "quality_inspection_section_break",
+ "inspection_required",
+ "column_break_dxp7",
+ "quality_inspection_template",
"section_break0",
"exploded_items",
"website_section",
@@ -68,7 +75,8 @@
"show_items",
"show_operations",
"web_long_description",
- "amended_from"
+ "amended_from",
+ "connections_tab"
],
"fields": [
{
@@ -183,7 +191,7 @@
{
"fieldname": "currency_detail",
"fieldtype": "Section Break",
- "label": "Currency and Price List"
+ "label": "Cost Configuration"
},
{
"fieldname": "company",
@@ -209,10 +217,6 @@
"reqd": 1
},
{
- "fieldname": "column_break_12",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -261,7 +265,7 @@
{
"fieldname": "materials_section",
"fieldtype": "Section Break",
- "label": "Materials",
+ "label": "Raw Materials",
"oldfieldtype": "Section Break"
},
{
@@ -276,18 +280,18 @@
{
"collapsible": 1,
"fieldname": "scrap_section",
- "fieldtype": "Section Break",
- "label": "Scrap"
+ "fieldtype": "Tab Break",
+ "label": "Scrap & Process Loss"
},
{
"fieldname": "scrap_items",
"fieldtype": "Table",
- "label": "Scrap Items",
+ "label": "Items",
"options": "BOM Scrap Item"
},
{
"fieldname": "costing",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Costing",
"oldfieldtype": "Section Break"
},
@@ -380,10 +384,6 @@
"read_only": 1
},
{
- "fieldname": "section_break_25",
- "fieldtype": "Section Break"
- },
- {
"fetch_from": "item.description",
"fieldname": "description",
"fieldtype": "Small Text",
@@ -478,8 +478,8 @@
},
{
"fieldname": "section_break_21",
- "fieldtype": "Section Break",
- "label": "Operations"
+ "fieldtype": "Tab Break",
+ "label": "Operations & Materials"
},
{
"fieldname": "column_break_23",
@@ -511,6 +511,7 @@
"fetch_from": "item.has_variants",
"fieldname": "has_variants",
"fieldtype": "Check",
+ "hidden": 1,
"in_list_view": 1,
"label": "Has Variants",
"no_copy": 1,
@@ -518,13 +519,63 @@
"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"
},
{
- "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",
- "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",
@@ -532,7 +583,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2022-01-30 21:27:54.727298",
+ "modified": "2023-01-03 18:42:27.732107",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index ca4f63d..53af28d 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -193,6 +193,7 @@
self.update_exploded_items(save=False)
self.update_stock_qty()
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()
def get_context(self, context):
@@ -233,6 +234,7 @@
"sequence_id",
"operation",
"workstation",
+ "workstation_type",
"description",
"time_in_mins",
"batch_size",
@@ -876,36 +878,19 @@
"""Get a complete tree representation preserving order of child items."""
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):
- for item in self.scrap_items:
- 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", self.uom, "must_be_whole_number")
- must_be_whole_number = frappe.get_value("UOM", item.stock_uom, "must_be_whole_number")
- if item.is_process_loss and must_be_whole_number:
- 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 self.process_loss_percentage and self.process_loss_percentage > 100:
+ frappe.throw(_("Process Loss Percentage cannot be greater than 100"))
- if item.is_process_loss and (item.stock_qty >= self.quantity):
- msg = _("Scrap/Loss Item: {0} should have Qty less than finished goods Quantity.").format(
- frappe.bold(item.item_code)
- )
-
- 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"))
+ if self.process_loss_qty and must_be_whole_number and self.process_loss_qty % 1 != 0:
+ 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.throw(msg, title=_("Invalid Process Loss Configuration"))
def get_bom_item_rate(args, bom_doc):
@@ -1053,7 +1038,7 @@
query = query.format(
table="BOM Scrap Item",
where_conditions="",
- select_columns=", item.description, is_process_loss",
+ select_columns=", item.description",
is_stock_item=is_stock_item,
qty_field="stock_qty",
)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index e34ac12..16f5c79 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -384,36 +384,16 @@
def test_bom_with_process_loss_item(self):
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(
- fg_item_non_whole, bom_item, scrap_qty=0.25, scrap_rate=0, fg_qty=1
- )
- bom_doc.submit()
-
bom_doc = create_bom_with_process_loss_item(
- fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0
+ fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0, process_loss_percentage=110
)
- # PL Item qty can't be >= FG Item qty
+ # PL can't be > 100
self.assertRaises(frappe.ValidationError, bom_doc.submit)
- bom_doc = create_bom_with_process_loss_item(
- 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
- )
+ bom_doc = create_bom_with_process_loss_item(fg_item_whole, bom_item, process_loss_percentage=20)
# Items with whole UOMs can't be PL Items
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):
query = partial(
item_query,
@@ -744,7 +724,7 @@
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.item = fg_item.item_code
@@ -759,19 +739,22 @@
"rate": 100.0,
},
)
- bom_doc.append(
- "scrap_items",
- {
- "item_code": fg_item.item_code,
- "qty": scrap_qty,
- "stock_qty": scrap_qty,
- "uom": fg_item.stock_uom,
- "stock_uom": fg_item.stock_uom,
- "rate": scrap_rate,
- "is_process_loss": is_process_loss,
- },
- )
+
+ if scrap_qty:
+ bom_doc.append(
+ "scrap_items",
+ {
+ "item_code": fg_item.item_code,
+ "qty": scrap_qty,
+ "stock_qty": scrap_qty,
+ "uom": fg_item.stock_uom,
+ "stock_uom": fg_item.stock_uom,
+ "rate": scrap_rate,
+ },
+ )
+
bom_doc.currency = "INR"
+ bom_doc.process_loss_percentage = process_loss_percentage
return bom_doc
diff --git a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json
index 7018082..b2ef19b 100644
--- a/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json
+++ b/erpnext/manufacturing/doctype/bom_scrap_item/bom_scrap_item.json
@@ -8,7 +8,6 @@
"item_code",
"column_break_2",
"item_name",
- "is_process_loss",
"quantity_and_rate",
"stock_qty",
"rate",
@@ -89,17 +88,11 @@
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
- },
- {
- "default": "0",
- "fieldname": "is_process_loss",
- "fieldtype": "Check",
- "label": "Is Process Loss"
}
],
"istable": 1,
"links": [],
- "modified": "2021-06-22 16:46:12.153311",
+ "modified": "2023-01-03 14:19:28.460965",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM Scrap Item",
@@ -108,5 +101,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 36466ff..729ed42 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -846,20 +846,20 @@
create_process_loss_bom_items,
)
- qty = 4
+ qty = 10
scrap_qty = 0.25 # bom item qty = 1, consider as 25% of FG
source_warehouse = "Stores - _TC"
wip_warehouse = "_Test Warehouse - _TC"
fg_item_non_whole, _, bom_item = create_process_loss_bom_items()
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"
if not frappe.db.exists("BOM", bom_no):
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()
@@ -883,19 +883,15 @@
# Testing stock entry values
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
- actual_fg_qty = qty - total_pl_qty
-
- 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)
+ wo.load_from_db()
+ self.assertEqual(wo.status, "In Process")
@timeout(seconds=60)
def test_job_card_scrap_item(self):
@@ -1154,6 +1150,36 @@
except frappe.MandatoryError:
self.fail("Batch generation causing failing in Work Order")
+ @change_settings("Manufacturing Settings", {"make_serial_no_batch_from_work_order": 1})
+ def test_auto_serial_no_creation(self):
+ from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
+
+ fg_item = frappe.generate_hash(length=20)
+ child_item = frappe.generate_hash(length=20)
+
+ bom_tree = {fg_item: {child_item: {}}}
+
+ create_nested_bom(bom_tree, prefix="")
+
+ item = frappe.get_doc("Item", fg_item)
+ item.has_serial_no = 1
+ item.serial_no_series = f"{item.name}.#####"
+ item.save()
+
+ try:
+ wo_order = make_wo_order_test_record(item=fg_item, qty=2, skip_transfer=True)
+ serial_nos = wo_order.serial_no
+ stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
+ stock_entry.set_work_order_details()
+ stock_entry.set_serial_no_batch_for_finished_good()
+ for row in stock_entry.items:
+ if row.item_code == fg_item:
+ self.assertTrue(row.serial_no)
+ self.assertEqual(sorted(get_serial_nos(row.serial_no)), sorted(get_serial_nos(serial_nos)))
+
+ except frappe.MandatoryError:
+ self.fail("Batch generation causing failing in Work Order")
+
@change_settings(
"Manufacturing Settings",
{"backflush_raw_materials_based_on": "Material Transferred for Manufacture"},
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 9452a63..25e16d6 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -14,13 +14,13 @@
"item_name",
"image",
"bom_no",
+ "sales_order",
"column_break1",
"company",
"qty",
"material_transferred_for_manufacturing",
"produced_qty",
"process_loss_qty",
- "sales_order",
"project",
"serial_no_and_batch_for_finished_good_section",
"has_serial_no",
@@ -28,6 +28,7 @@
"column_break_17",
"serial_no",
"batch_size",
+ "work_order_configuration",
"settings_section",
"allow_alternative_item",
"use_multi_level_bom",
@@ -42,7 +43,11 @@
"fg_warehouse",
"scrap_warehouse",
"required_items_section",
+ "materials_and_operations_tab",
"required_items",
+ "operations_section",
+ "operations",
+ "transfer_material_against",
"time",
"planned_start_date",
"planned_end_date",
@@ -51,9 +56,6 @@
"actual_start_date",
"actual_end_date",
"lead_time",
- "operations_section",
- "transfer_material_against",
- "operations",
"section_break_22",
"planned_operating_cost",
"actual_operating_cost",
@@ -72,12 +74,14 @@
"production_plan_item",
"production_plan_sub_assembly_item",
"product_bundle_item",
- "amended_from"
+ "amended_from",
+ "connections_tab"
],
"fields": [
{
"fieldname": "item",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
+ "label": "Production Item",
"options": "fa fa-gift"
},
{
@@ -236,7 +240,7 @@
{
"fieldname": "warehouses",
"fieldtype": "Section Break",
- "label": "Warehouses",
+ "label": "Warehouse",
"options": "fa fa-building"
},
{
@@ -390,8 +394,8 @@
{
"collapsible": 1,
"fieldname": "more_info",
- "fieldtype": "Section Break",
- "label": "More Information",
+ "fieldtype": "Tab Break",
+ "label": "More Info",
"options": "fa fa-file-text"
},
{
@@ -474,8 +478,7 @@
},
{
"fieldname": "settings_section",
- "fieldtype": "Section Break",
- "label": "Settings"
+ "fieldtype": "Section Break"
},
{
"fieldname": "column_break_18",
@@ -568,6 +571,22 @@
"no_copy": 1,
"non_negative": 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",
@@ -575,7 +594,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2022-01-24 21:18:12.160114",
+ "modified": "2023-01-03 14:16:35.427731",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 52753a0..ae9e9c6 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -246,21 +246,11 @@
status = "Draft"
elif self.docstatus == 1:
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"
- if stock_entries:
+ if flt(self.material_transferred_for_manufacturing) > 0:
status = "In Process"
- produced_qty = stock_entries.get("Manufacture")
- if flt(produced_qty) >= flt(self.qty):
- status = "Completed"
+ if flt(self.produced_qty) >= flt(self.qty):
+ status = "Completed"
else:
status = "Cancelled"
@@ -285,14 +275,7 @@
):
continue
- qty = flt(
- 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]
- )
+ qty = self.get_transferred_or_manufactured_qty(purpose)
completed_qty = self.qty + (allowance_percentage / 100 * self.qty)
if qty > completed_qty:
@@ -314,26 +297,30 @@
if self.production_plan:
self.update_production_plan_status()
- def set_process_loss_qty(self):
- process_loss_qty = flt(
- frappe.db.sql(
- """
- SELECT sum(qty) FROM `tabStock Entry Detail`
- WHERE
- is_process_loss=1
- AND parent IN (
- SELECT name FROM `tabStock Entry`
- WHERE
- work_order=%s
- AND purpose='Manufacture'
- AND docstatus=1
- )
- """,
- (self.name,),
- )[0][0]
+ 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 process_loss_qty is not None:
- self.db_set("process_loss_qty", process_loss_qty)
+
+ 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):
+ table = frappe.qb.DocType("Stock Entry")
+ process_loss_qty = (
+ frappe.qb.from_(table)
+ .select(Sum(table.process_loss_qty))
+ .where(
+ (table.work_order == self.name) & (table.purpose == "Manufacture") & (table.docstatus == 1)
+ )
+ ).run()[0][0]
+
+ self.db_set("process_loss_qty", flt(process_loss_qty))
def update_production_plan_status(self):
production_plan = frappe.get_doc("Production Plan", self.production_plan)
@@ -352,6 +339,7 @@
produced_qty = total_qty[0][0] if total_qty else 0
+ self.update_status()
production_plan.run_method(
"update_produced_pending_qty", produced_qty, self.production_plan_item
)
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index 1e1b435..cdf1541 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -4,7 +4,7 @@
import frappe
from frappe import _
-from frappe.query_builder.functions import Floor, Sum
+from frappe.query_builder.functions import Sum
from pypika.terms import ExistsCriterion
@@ -58,9 +58,9 @@
bom_item.description,
bom_item.stock_qty,
bom_item.stock_uom,
- bom_item.stock_qty * qty_to_produce / bom.quantity,
- Sum(bin.actual_qty).as_("actual_qty"),
- Sum(Floor(bin.actual_qty / (bom_item.stock_qty * qty_to_produce / bom.quantity))),
+ (bom_item.stock_qty / bom.quantity) * qty_to_produce,
+ Sum(bin.actual_qty),
+ Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
)
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
.groupby(bom_item.item_code)
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 0aad1d3..7495ab8 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -268,6 +268,7 @@
erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v15_0.delete_taxjar_doctypes
+erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
@@ -315,7 +316,10 @@
erpnext.patches.v14_0.fix_subcontracting_receipt_gl_entries
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
erpnext.patches.v13_0.update_schedule_type_in_loans
+erpnext.patches.v13_0.drop_unused_sle_index_parts
erpnext.patches.v14_0.create_accounting_dimensions_for_asset_capitalization
erpnext.patches.v14_0.update_partial_tds_fields
erpnext.patches.v14_0.create_incoterms_and_migrate_shipment
-erpnext.patches.v14_0.setup_clear_repost_logs
\ No newline at end of file
+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
diff --git a/erpnext/patches/v13_0/drop_unused_sle_index_parts.py b/erpnext/patches/v13_0/drop_unused_sle_index_parts.py
new file mode 100644
index 0000000..fa8a98c
--- /dev/null
+++ b/erpnext/patches/v13_0/drop_unused_sle_index_parts.py
@@ -0,0 +1,14 @@
+import frappe
+
+from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import on_doctype_update
+
+
+def execute():
+ try:
+ frappe.db.sql_ddl("ALTER TABLE `tabStock Ledger Entry` DROP INDEX `posting_sort_index`")
+ except Exception:
+ frappe.log_error("Failed to drop index")
+ return
+
+ # Recreate indexes
+ on_doctype_update()
diff --git a/erpnext/patches/v14_0/create_accounting_dimensions_for_payment_request.py b/erpnext/patches/v14_0/create_accounting_dimensions_for_payment_request.py
new file mode 100644
index 0000000..bede419
--- /dev/null
+++ b/erpnext/patches/v14_0/create_accounting_dimensions_for_payment_request.py
@@ -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)
diff --git a/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py b/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py
new file mode 100644
index 0000000..bce9255
--- /dev/null
+++ b/erpnext/patches/v14_0/update_entry_type_for_journal_entry.py
@@ -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()
diff --git a/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
new file mode 100644
index 0000000..5dc3cdd
--- /dev/null
+++ b/erpnext/patches/v15_0/create_asset_depreciation_schedules_from_assets.py
@@ -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()
diff --git a/erpnext/portal/utils.py b/erpnext/portal/utils.py
index 7be8c5d..c8b03e6 100644
--- a/erpnext/portal/utils.py
+++ b/erpnext/portal/utils.py
@@ -102,7 +102,7 @@
contact = frappe.new_doc("Contact")
contact.update({"first_name": fullname, "email_id": user})
contact.append("links", dict(link_doctype=doctype, link_name=party_name))
- contact.append("email_ids", dict(email_id=user))
+ contact.append("email_ids", dict(email_id=user, is_primary=True))
contact.flags.ignore_mandatory = True
contact.insert(ignore_permissions=True)
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index c48ed91..f366f77 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -20,7 +20,7 @@
onload: function (frm) {
const so = frm.get_docfield("sales_order");
so.get_route_options_for_new_doc = () => {
- if (frm.is_new()) return;
+ if (frm.is_new()) return {};
return {
"customer": frm.doc.customer,
"project_name": frm.doc.name
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index cbf2493..7d80ac1 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -7,6 +7,8 @@
from frappe import _
from frappe.desk.reportview import get_match_cond
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 erpnext import get_default_company
@@ -15,9 +17,6 @@
class Project(Document):
- def get_feed(self):
- return "{0}: {1}".format(_(self.status), frappe.safe_decode(self.project_name))
-
def onload(self):
self.set_onload(
"activity_summary",
@@ -300,17 +299,19 @@
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"""
+
+ timesheet_detail = frappe.qb.DocType("Timesheet Detail")
+
return dict(
- frappe.db.sql(
- """select unix_timestamp(from_time), count(*)
- from `tabTimesheet Detail` where project=%s
- and from_time > date_sub(curdate(), interval 1 year)
- and docstatus < 2
- group by date(from_time)""",
- name,
- )
+ frappe.qb.from_(timesheet_detail)
+ .select(UnixTimestamp(timesheet_detail.from_time), Count("*"))
+ .where(timesheet_detail.project == name)
+ .where(timesheet_detail.from_time > CurDate() - Interval(years=1))
+ .where(timesheet_detail.docstatus < 2)
+ .groupby(Date(timesheet_detail.from_time))
+ .run()
)
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index 79f1b3a..2dde542 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -20,9 +20,6 @@
class Task(NestedSet):
nsm_parent_field = "parent_task"
- def get_feed(self):
- return "{0}: {1}".format(_(self.status), self.subject)
-
def get_customer_details(self):
cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
if cust:
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index b9bb37a..1179364 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -25,12 +25,18 @@
def validate(self):
self.set_status()
self.validate_dates()
+ self.calculate_hours()
self.validate_time_logs()
self.update_cost()
self.calculate_total_amounts()
self.calculate_percentage_billed()
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):
self.total_hours = 0.0
self.total_billable_hours = 0.0
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
index 4295c69..51664f8 100644
--- a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -370,12 +370,14 @@
fieldname: "deposit",
fieldtype: "Currency",
label: "Deposit",
+ options: "currency",
read_only: 1,
},
{
fieldname: "withdrawal",
fieldtype: "Currency",
label: "Withdrawal",
+ options: "currency",
read_only: 1,
},
{
@@ -393,6 +395,7 @@
fieldname: "allocated_amount",
fieldtype: "Currency",
label: "Allocated Amount",
+ options: "Currency",
read_only: 1,
},
@@ -400,8 +403,17 @@
fieldname: "unallocated_amount",
fieldtype: "Currency",
label: "Unallocated Amount",
+ options: "Currency",
read_only: 1,
},
+ {
+ fieldname: "currency",
+ fieldtype: "Link",
+ label: "Currency",
+ options: "Currency",
+ read_only: 1,
+ hidden: 1,
+ }
];
}
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 09779d8..b0e08cc 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -225,7 +225,8 @@
args: {
item_code: item.item_code,
warehouse: item.warehouse,
- company: doc.company
+ company: doc.company,
+ include_child_warehouses: true
}
});
}
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 7481000..1f8a5e3 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -58,7 +58,7 @@
if (
in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype)
- && this.frm.doc.s_pos
+ && this.frm.doc.is_pos
&& this.frm.doc.is_return
) {
this.set_total_amount_to_default_mop();
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 46ac808..5c1c6d1 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -272,7 +272,7 @@
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function(row) {
- if(me.frm.is_new()) return;
+ if(me.frm.is_new()) return {};
return {
"inspection_type": inspection_type,
"reference_type": me.frm.doc.doctype,
@@ -298,7 +298,7 @@
}
make_payment_request() {
- var me = this;
+ let me = this;
const payment_request_type = (in_list(['Sales Order', 'Sales Invoice'], this.frm.doc.doctype))
? "Inward" : "Outward";
@@ -314,7 +314,7 @@
},
callback: function(r) {
if(!r.exc){
- var doc = frappe.model.sync(r.message);
+ frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
@@ -1130,10 +1130,13 @@
qty(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
- item.pricing_rules = ''
- this.conversion_factor(doc, cdt, cdn, true);
- this.calculate_stock_uom_rate(doc, cdt, cdn);
- this.apply_pricing_rule(item, true);
+ // item.pricing_rules = ''
+ frappe.run_serially([
+ () => this.remove_pricing_rule(item),
+ () => this.conversion_factor(doc, cdt, cdn, true),
+ () => this.calculate_stock_uom_rate(doc, cdt, cdn),
+ () => this.apply_pricing_rule(item, true)
+ ]);
}
calculate_stock_uom_rate(doc, cdt, cdn) {
@@ -1357,16 +1360,21 @@
var item_list = [];
$.each(this.frm.doc["items"] || [], function(i, d) {
- if (d.item_code && !d.is_free_item) {
- item_list.push({
- "doctype": d.doctype,
- "name": d.name,
- "item_code": d.item_code,
- "pricing_rules": d.pricing_rules,
- "parenttype": d.parenttype,
- "parent": d.parent,
- "price_list_rate": d.price_list_rate
- })
+ if (d.item_code) {
+ if (d.is_free_item) {
+ // Simply remove free items
+ me.frm.get_field("items").grid.grid_rows[i].remove();
+ } else {
+ item_list.push({
+ "doctype": d.doctype,
+ "name": d.name,
+ "item_code": d.item_code,
+ "pricing_rules": d.pricing_rules,
+ "parenttype": d.parenttype,
+ "parent": d.parent,
+ "price_list_rate": d.price_list_rate
+ })
+ }
}
});
return this.frm.call({
@@ -1683,7 +1691,7 @@
var valid = true;
$.each(["company", "customer"], function(i, fieldname) {
- if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && me.frm.doc.doctype != "Purchase Order") {
+ if(frappe.meta.has_field(me.frm.doc.doctype, fieldname) && !["Purchase Order","Purchase Invoice"].includes(me.frm.doc.doctype)) {
if (!me.frm.doc[fieldname]) {
frappe.msgprint(__("Please specify") + ": " +
frappe.meta.get_label(me.frm.doc.doctype, fieldname, me.frm.doc.name) +
diff --git a/erpnext/public/js/erpnext.bundle.js b/erpnext/public/js/erpnext.bundle.js
index 14a088e..7b230af 100644
--- a/erpnext/public/js/erpnext.bundle.js
+++ b/erpnext/public/js/erpnext.bundle.js
@@ -13,6 +13,7 @@
import "./agriculture/ternary_plot";
import "./templates/item_quick_entry.html";
import "./utils/item_quick_entry";
+import "./utils/contact_address_quick_entry";
import "./utils/customer_quick_entry";
import "./utils/supplier_quick_entry";
import "./call_popup/call_popup";
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 6d64625..d37b7bb 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -333,8 +333,18 @@
}
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
- }
+ },
+ // check if payments app is installed on site, if not warn user.
+ check_payments_app: () => {
+ if (frappe.boot.versions && !frappe.boot.versions.payments) {
+ const marketplace_link = '<a href="https://frappecloud.com/marketplace/apps/payments">Marketplace</a>'
+ const github_link = '<a href="https://github.com/frappe/payments/">GitHub</a>'
+ const msg = __("payments app is not installed. Please install it from {0} or {1}", [marketplace_link, github_link])
+ frappe.msgprint(msg);
+ }
+
+ },
});
erpnext.utils.select_alternate_items = function(opts) {
diff --git a/erpnext/public/js/utils/contact_address_quick_entry.js b/erpnext/public/js/utils/contact_address_quick_entry.js
new file mode 100644
index 0000000..adabf08
--- /dev/null
+++ b/erpnext/public/js/utils/contact_address_quick_entry.js
@@ -0,0 +1,100 @@
+frappe.provide('frappe.ui.form');
+
+frappe.ui.form.ContactAddressQuickEntryForm = class ContactAddressQuickEntryForm extends frappe.ui.form.QuickEntryForm {
+ constructor(doctype, after_insert, init_callback, doc, force) {
+ super(doctype, after_insert, init_callback, doc, force);
+ this.skip_redirect_on_error = true;
+ }
+
+ render_dialog() {
+ this.mandatory = this.mandatory.concat(this.get_variant_fields());
+ super.render_dialog();
+ }
+
+ insert() {
+ /**
+ * Using alias fieldnames because the doctype definition define "email_id" and "mobile_no" as readonly fields.
+ * Therefor, resulting in the fields being "hidden".
+ */
+ const map_field_names = {
+ "email_address": "email_id",
+ "mobile_number": "mobile_no",
+ };
+
+ Object.entries(map_field_names).forEach(([fieldname, new_fieldname]) => {
+ this.dialog.doc[new_fieldname] = this.dialog.doc[fieldname];
+ delete this.dialog.doc[fieldname];
+ });
+
+ return super.insert();
+ }
+
+ get_variant_fields() {
+ var variant_fields = [{
+ fieldtype: "Section Break",
+ label: __("Primary Contact Details"),
+ collapsible: 1
+ },
+ {
+ label: __("Email Id"),
+ fieldname: "email_address",
+ fieldtype: "Data",
+ options: "Email",
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("Mobile Number"),
+ fieldname: "mobile_number",
+ fieldtype: "Data"
+ },
+ {
+ fieldtype: "Section Break",
+ label: __("Primary Address Details"),
+ collapsible: 1
+ },
+ {
+ label: __("Address Line 1"),
+ fieldname: "address_line1",
+ fieldtype: "Data"
+ },
+ {
+ label: __("Address Line 2"),
+ fieldname: "address_line2",
+ fieldtype: "Data"
+ },
+ {
+ label: __("ZIP Code"),
+ fieldname: "pincode",
+ fieldtype: "Data"
+ },
+ {
+ fieldtype: "Column Break"
+ },
+ {
+ label: __("City"),
+ fieldname: "city",
+ fieldtype: "Data"
+ },
+ {
+ label: __("State"),
+ fieldname: "state",
+ fieldtype: "Data"
+ },
+ {
+ label: __("Country"),
+ fieldname: "country",
+ fieldtype: "Link",
+ options: "Country"
+ },
+ {
+ label: __("Customer POS Id"),
+ fieldname: "customer_pos_id",
+ fieldtype: "Data",
+ hidden: 1
+ }];
+
+ return variant_fields;
+ }
+}
diff --git a/erpnext/public/js/utils/customer_quick_entry.js b/erpnext/public/js/utils/customer_quick_entry.js
index d2c5c72..b253208 100644
--- a/erpnext/public/js/utils/customer_quick_entry.js
+++ b/erpnext/public/js/utils/customer_quick_entry.js
@@ -1,81 +1,3 @@
frappe.provide('frappe.ui.form');
-frappe.ui.form.CustomerQuickEntryForm = class CustomerQuickEntryForm extends frappe.ui.form.QuickEntryForm {
- constructor(doctype, after_insert, init_callback, doc, force) {
- super(doctype, after_insert, init_callback, doc, force);
- this.skip_redirect_on_error = true;
- }
-
- render_dialog() {
- this.mandatory = this.mandatory.concat(this.get_variant_fields());
- super.render_dialog();
- }
-
- get_variant_fields() {
- var variant_fields = [{
- fieldtype: "Section Break",
- label: __("Primary Contact Details"),
- collapsible: 1
- },
- {
- label: __("Email Id"),
- fieldname: "email_id",
- fieldtype: "Data"
- },
- {
- fieldtype: "Column Break"
- },
- {
- label: __("Mobile Number"),
- fieldname: "mobile_no",
- fieldtype: "Data"
- },
- {
- fieldtype: "Section Break",
- label: __("Primary Address Details"),
- collapsible: 1
- },
- {
- label: __("Address Line 1"),
- fieldname: "address_line1",
- fieldtype: "Data"
- },
- {
- label: __("Address Line 2"),
- fieldname: "address_line2",
- fieldtype: "Data"
- },
- {
- label: __("ZIP Code"),
- fieldname: "pincode",
- fieldtype: "Data"
- },
- {
- fieldtype: "Column Break"
- },
- {
- label: __("City"),
- fieldname: "city",
- fieldtype: "Data"
- },
- {
- label: __("State"),
- fieldname: "state",
- fieldtype: "Data"
- },
- {
- label: __("Country"),
- fieldname: "country",
- fieldtype: "Link",
- options: "Country"
- },
- {
- label: __("Customer POS Id"),
- fieldname: "customer_pos_id",
- fieldtype: "Data",
- hidden: 1
- }];
-
- return variant_fields;
- }
-}
+frappe.ui.form.CustomerQuickEntryForm = frappe.ui.form.ContactAddressQuickEntryForm;
diff --git a/erpnext/public/js/utils/supplier_quick_entry.js b/erpnext/public/js/utils/supplier_quick_entry.js
index 8d591a9..687b014 100644
--- a/erpnext/public/js/utils/supplier_quick_entry.js
+++ b/erpnext/public/js/utils/supplier_quick_entry.js
@@ -1,77 +1,3 @@
frappe.provide('frappe.ui.form');
-frappe.ui.form.SupplierQuickEntryForm = class SupplierQuickEntryForm extends frappe.ui.form.QuickEntryForm {
- constructor(doctype, after_insert, init_callback, doc, force) {
- super(doctype, after_insert, init_callback, doc, force);
- this.skip_redirect_on_error = true;
- }
-
- render_dialog() {
- this.mandatory = this.mandatory.concat(this.get_variant_fields());
- super.render_dialog();
- }
-
- get_variant_fields() {
- var variant_fields = [
- {
- fieldtype: "Section Break",
- label: __("Primary Contact Details"),
- collapsible: 1
- },
- {
- label: __("Email Id"),
- fieldname: "email_id",
- fieldtype: "Data"
- },
- {
- fieldtype: "Column Break"
- },
- {
- label: __("Mobile Number"),
- fieldname: "mobile_no",
- fieldtype: "Data"
- },
- {
- fieldtype: "Section Break",
- label: __("Primary Address Details"),
- collapsible: 1
- },
- {
- label: __("Address Line 1"),
- fieldname: "address_line1",
- fieldtype: "Data"
- },
- {
- label: __("Address Line 2"),
- fieldname: "address_line2",
- fieldtype: "Data"
- },
- {
- label: __("ZIP Code"),
- fieldname: "pincode",
- fieldtype: "Data"
- },
- {
- fieldtype: "Column Break"
- },
- {
- label: __("City"),
- fieldname: "city",
- fieldtype: "Data"
- },
- {
- label: __("State"),
- fieldname: "state",
- fieldtype: "Data"
- },
- {
- label: __("Country"),
- fieldname: "country",
- fieldtype: "Link",
- options: "Country"
- }
- ];
-
- return variant_fields;
- }
-};
+frappe.ui.form.SupplierQuickEntryForm = frappe.ui.form.ContactAddressQuickEntryForm;
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index d0eb377..d9dab33 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -6,7 +6,7 @@
import frappe
import frappe.defaults
-from frappe import _, msgprint
+from frappe import _, msgprint, qb
from frappe.contacts.address_and_contact import (
delete_contact_and_address,
load_address_and_contact,
@@ -27,9 +27,6 @@
class Customer(TransactionBase):
- def get_feed(self):
- return self.customer_name
-
def onload(self):
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)
@@ -732,12 +729,15 @@
@frappe.validate_and_sanitize_search_inputs
def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, filters):
customer = filters.get("customer")
- return frappe.db.sql(
- """
- select `tabContact`.name from `tabContact`, `tabDynamic Link`
- where `tabContact`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s
- and `tabDynamic Link`.link_doctype = 'Customer'
- and `tabContact`.name like %(txt)s
- """,
- {"customer": customer, "txt": "%%%s%%" % txt},
+
+ con = qb.DocType("Contact")
+ dlink = qb.DocType("Dynamic Link")
+
+ return (
+ qb.from_(con)
+ .join(dlink)
+ .on(con.name == dlink.parent)
+ .select(con.name, con.email_id)
+ .where((dlink.link_name == customer) & (con.name.like(f"%{txt}%")))
+ .run()
)
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index 08918f4..eb2c0a4 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -43,12 +43,13 @@
"total",
"net_total",
"taxes_section",
+ "tax_category",
"taxes_and_charges",
"column_break_36",
- "tax_category",
- "column_break_34",
"shipping_rule",
+ "column_break_34",
"incoterm",
+ "named_place",
"section_break_36",
"taxes",
"section_break_39",
@@ -1059,13 +1060,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
}
],
"icon": "fa fa-shopping-cart",
"idx": 82,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-17 17:20:54.984348",
+ "modified": "2022-12-12 18:32:28.671332",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py
index 6f0b381..b151dd5 100644
--- a/erpnext/selling/doctype/quotation/test_quotation.py
+++ b/erpnext/selling/doctype/quotation/test_quotation.py
@@ -30,6 +30,24 @@
self.assertTrue(sales_order.get("payment_schedule"))
+ def test_maintain_rate_in_sales_cycle_is_enforced(self):
+ from erpnext.selling.doctype.quotation.quotation import make_sales_order
+
+ maintain_rate = frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")
+ frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", 1)
+
+ quotation = frappe.copy_doc(test_records[0])
+ quotation.transaction_date = nowdate()
+ quotation.valid_till = add_months(quotation.transaction_date, 1)
+ quotation.insert()
+ quotation.submit()
+
+ sales_order = make_sales_order(quotation.name)
+ sales_order.items[0].rate = 1
+ self.assertRaises(frappe.ValidationError, sales_order.save)
+
+ frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", maintain_rate)
+
def test_make_sales_order_with_different_currency(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index 31a9589..ca7dfd2 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -90,7 +90,6 @@
"oldfieldtype": "Link",
"options": "Item",
"print_width": "150px",
- "reqd": 1,
"search_index": 1,
"width": "150px"
},
@@ -649,7 +648,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-07-15 12:40:51.074820",
+ "modified": "2022-12-25 02:49:53.926625",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 9ec32cb..ccea840 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -58,12 +58,13 @@
"total",
"net_total",
"taxes_section",
+ "tax_category",
"taxes_and_charges",
"column_break_38",
- "tax_category",
- "column_break_49",
"shipping_rule",
+ "column_break_49",
"incoterm",
+ "named_place",
"section_break_40",
"taxes",
"section_break_43",
@@ -1630,13 +1631,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-17 17:22:00.413878",
+ "modified": "2022-12-12 18:34:00.681780",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 78e2370..7c0601e 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -194,7 +194,7 @@
)
if cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")):
- self.validate_rate_with_reference_doc([["Quotation", "prev_docname", "quotation_item"]])
+ self.validate_rate_with_reference_doc([["Quotation", "prevdoc_docname", "quotation_item"]])
def update_enquiry_status(self, prevdoc, flag):
enq = frappe.db.sql(
@@ -1024,6 +1024,15 @@
]
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):
target.supplier = ""
target.apply_discount_on = ""
@@ -1031,8 +1040,14 @@
target.discount_amount = 0.0
target.inter_company_order_reference = ""
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("calculate_taxes_and_totals")
diff --git a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
index ace2e29..5c4b578 100644
--- a/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
+++ b/erpnext/selling/doctype/sales_order/sales_order_dashboard.py
@@ -12,7 +12,10 @@
"Auto Repeat": "reference_document",
"Maintenance Visit": "prevdoc_docname",
},
- "internal_links": {"Quotation": ["items", "prevdoc_docname"]},
+ "internal_links": {
+ "Quotation": ["items", "prevdoc_docname"],
+ "Material Request": ["items", "material_request"],
+ },
"transactions": [
{
"label": _("Fulfillment"),
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index b801de3..134b5ea 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -114,7 +114,6 @@
"oldfieldtype": "Link",
"options": "Item",
"print_width": "150px",
- "reqd": 1,
"width": "150px"
},
{
@@ -639,6 +638,7 @@
"width": "70px"
},
{
+ "allow_on_submit": 1,
"fieldname": "ordered_qty",
"fieldtype": "Float",
"label": "Ordered Qty",
@@ -865,7 +865,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-11-18 11:39:01.741665",
+ "modified": "2023-01-12 13:13:28.691585",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
index 91748bc..f3f931e 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.js
@@ -45,6 +45,12 @@
}
},
{
+ "fieldname": "warehouse",
+ "label": __("Warehouse"),
+ "fieldtype": "Link",
+ "options": "Warehouse"
+ },
+ {
"fieldname": "status",
"label": __("Status"),
"fieldtype": "MultiSelectList",
diff --git a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
index 720aa41..63d339a 100644
--- a/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
+++ b/erpnext/selling/report/sales_order_analysis/sales_order_analysis.py
@@ -53,6 +53,9 @@
if filters.get("status"):
conditions += " and so.status in %(status)s"
+ if filters.get("warehouse"):
+ conditions += " and soi.warehouse = %(warehouse)s"
+
return conditions
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index d6f2378..07ee289 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -70,9 +70,6 @@
self.abbr = self.abbr.strip()
- # if self.get('__islocal') and len(self.abbr) > 5:
- # frappe.throw(_("Abbreviation cannot have more than 5 characters"))
-
if not self.abbr.strip():
frappe.throw(_("Abbreviation is mandatory"))
diff --git a/erpnext/setup/doctype/customer_group/customer_group.json b/erpnext/setup/doctype/customer_group/customer_group.json
index 0e2ed9e..d6a431e 100644
--- a/erpnext/setup/doctype/customer_group/customer_group.json
+++ b/erpnext/setup/doctype/customer_group/customer_group.json
@@ -139,10 +139,11 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-02-08 17:01:52.162202",
+ "modified": "2022-12-24 11:15:17.142746",
"modified_by": "Administrator",
"module": "Setup",
"name": "Customer Group",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_customer_group",
"owner": "Administrator",
"permissions": [
@@ -198,10 +199,19 @@
"role": "Customer",
"select": 1,
"share": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
}
],
"search_fields": "parent_customer_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index 50f923d..2986087 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -123,6 +123,7 @@
"fieldname": "route",
"fieldtype": "Data",
"label": "Route",
+ "no_copy": 1,
"unique": 1
},
{
@@ -232,11 +233,10 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2022-03-09 12:27:11.055782",
+ "modified": "2023-01-05 12:21:30.458628",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
- "name_case": "Title Case",
"naming_rule": "By fieldname",
"nsm_parent_field": "parent_item_group",
"owner": "Administrator",
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 411176b..95bbf84 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -152,7 +152,7 @@
if from_item and frappe.request.environ.get("HTTP_REFERER"):
# base page after 'Home' will vary on Item page
- last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1]
+ last_page = frappe.request.environ["HTTP_REFERER"].split("/")[-1].split("?")[0]
if last_page and last_page in ("shop-by-category", "all-products"):
base_nav_page_title = " ".join(last_page.split("-")).title()
base_nav_page = {"name": _(base_nav_page_title), "route": "/" + last_page}
diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py
index 0082c70..beff7f5 100644
--- a/erpnext/setup/doctype/sales_person/sales_person.py
+++ b/erpnext/setup/doctype/sales_person/sales_person.py
@@ -2,8 +2,13 @@
# License: GNU General Public License v3. See license.txt
+from collections import defaultdict
+from itertools import chain
+
import frappe
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.nestedset import NestedSet, get_root_of
@@ -77,61 +82,31 @@
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 = {}
-
- out.update(
- dict(
- frappe.db.sql(
- """select
- unix_timestamp(dt.transaction_date), count(st.parenttype)
- from
- `tabSales Order` dt, `tabSales Team` st
- 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,
- )
+ return dict(
+ frappe.qb.from_(transaction)
+ .join(sales_team)
+ .on(transaction.name == sales_team.parent)
+ .select(UnixTimestamp(transaction[date_field]), Count("*"))
+ .where(sales_team.sales_person == name)
+ .where(transaction[date_field] > CurDate() - Interval(years=1))
+ .groupby(transaction[date_field])
+ .run()
)
- )
- sales_invoice = dict(
- frappe.db.sql(
- """select
- 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,
- )
- )
+ sales_order_activity = _fetch_activity("Sales Order", "transaction_date")
+ sales_invoice_activity = _fetch_activity("Sales Invoice", "posting_date")
+ delivery_note_activity = _fetch_activity("Delivery Note", "posting_date")
- for key in sales_invoice:
- if out.get(key):
- out[key] += sales_invoice[key]
- else:
- out[key] = sales_invoice[key]
+ merged_activities = defaultdict(int)
- delivery_note = dict(
- frappe.db.sql(
- """select
- unix_timestamp(dt.posting_date), count(st.parenttype)
- 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 ts, count in chain(
+ sales_order_activity.items(), sales_invoice_activity.items(), delivery_note_activity.items()
+ ):
+ merged_activities[ts] += count
- for key in delivery_note:
- if out.get(key):
- out[key] += delivery_note[key]
- else:
- out[key] = delivery_note[key]
-
- return out
+ return merged_activities
diff --git a/erpnext/setup/doctype/supplier_group/supplier_group.json b/erpnext/setup/doctype/supplier_group/supplier_group.json
index 9119bb9..b3ed608 100644
--- a/erpnext/setup/doctype/supplier_group/supplier_group.json
+++ b/erpnext/setup/doctype/supplier_group/supplier_group.json
@@ -6,6 +6,7 @@
"creation": "2013-01-10 16:34:24",
"doctype": "DocType",
"document_type": "Setup",
+ "engine": "InnoDB",
"field_order": [
"supplier_group_name",
"parent_supplier_group",
@@ -106,10 +107,11 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2020-03-18 18:10:49.228407",
+ "modified": "2022-12-24 11:16:12.486719",
"modified_by": "Administrator",
"module": "Setup",
"name": "Supplier Group",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_supplier_group",
"owner": "Administrator",
"permissions": [
@@ -156,8 +158,18 @@
"permlevel": 1,
"read": 1,
"role": "Purchase User"
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
}
],
"show_name_in_global_search": 1,
- "sort_order": "ASC"
+ "sort_field": "modified",
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/territory/territory.json b/erpnext/setup/doctype/territory/territory.json
index a25bda0..c3a4993 100644
--- a/erpnext/setup/doctype/territory/territory.json
+++ b/erpnext/setup/doctype/territory/territory.json
@@ -123,11 +123,12 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2021-02-08 17:10:03.767426",
+ "modified": "2022-12-24 11:16:39.964956",
"modified_by": "Administrator",
"module": "Setup",
"name": "Territory",
"name_case": "Title Case",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_territory",
"owner": "Administrator",
"permissions": [
@@ -175,10 +176,19 @@
"role": "Customer",
"select": 1,
"share": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
}
],
"search_fields": "parent_territory,territory_manager",
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
index c18a4b2..4256a7d 100644
--- a/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
+++ b/erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py
@@ -204,7 +204,7 @@
@frappe.whitelist()
def get_doctypes_to_be_ignored():
- doctypes_to_be_ignored_list = [
+ doctypes_to_be_ignored = [
"Account",
"Cost Center",
"Warehouse",
@@ -223,4 +223,7 @@
"Customer",
"Supplier",
]
- return doctypes_to_be_ignored_list
+
+ doctypes_to_be_ignored.extend(frappe.get_hooks("company_data_to_be_ignored") or [])
+
+ return doctypes_to_be_ignored
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 6e7622c..1be528f 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -102,6 +102,9 @@
args: args,
callback: function (r) {
me.render(r.message);
+ if(me.after_refresh) {
+ me.after_refresh();
+ }
}
});
}
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index c28f45a..9f409d4 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -162,6 +162,7 @@
.where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse")))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
.orderby(sle.creation, order=Order.desc)
+ .limit(1)
.run()
)
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 80e4bcb..165a56b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -57,12 +57,13 @@
"total",
"net_total",
"taxes_section",
+ "tax_category",
"taxes_and_charges",
"column_break_43",
- "tax_category",
- "column_break_39",
"shipping_rule",
+ "column_break_39",
"incoterm",
+ "named_place",
"section_break_41",
"taxes",
"section_break_44",
@@ -1388,13 +1389,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-17 17:22:42.860790",
+ "modified": "2022-12-12 18:38:53.067799",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index d1d228d..629e50e 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -22,7 +22,6 @@
"allow_alternative_item",
"is_stock_item",
"has_variants",
- "include_item_in_manufacturing",
"opening_stock",
"valuation_rate",
"standard_rate",
@@ -112,6 +111,7 @@
"quality_inspection_template",
"inspection_required_before_delivery",
"manufacturing",
+ "include_item_in_manufacturing",
"is_sub_contracted_item",
"default_bom",
"column_break_74",
@@ -911,7 +911,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2022-09-13 04:08:17.431731",
+ "modified": "2023-01-07 22:45:00.341745",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 20bc9d9..686e6cb 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -8,6 +8,8 @@
import frappe
from frappe import _
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 (
cint,
cstr,
@@ -164,10 +166,7 @@
if not self.is_stock_item or self.has_serial_no or self.has_batch_no:
return
- if not self.valuation_rate and self.standard_rate:
- self.valuation_rate = self.standard_rate
-
- if not self.valuation_rate and not self.is_customer_provided_item:
+ if not self.valuation_rate and not self.standard_rate and not self.is_customer_provided_item:
frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered"))
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -192,7 +191,7 @@
item_code=self.name,
target=default_warehouse,
qty=self.opening_stock,
- rate=self.valuation_rate,
+ rate=self.valuation_rate or self.standard_rate,
company=default.company,
posting_date=getdate(),
posting_time=nowtime(),
@@ -279,7 +278,7 @@
frappe.throw(_("Row #{0}: Maximum Net Rate cannot be greater than Minimum Net Rate"))
def update_template_tables(self):
- template = frappe.get_doc("Item", self.variant_of)
+ template = frappe.get_cached_doc("Item", self.variant_of)
# add item taxes from template
for d in template.get("taxes"):
@@ -997,18 +996,19 @@
).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."""
- items = frappe.db.sql(
- """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,
- )
+ sle = frappe.qb.DocType("Stock Ledger Entry")
- 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):
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index e1ee938..7e426ae 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -83,6 +83,7 @@
def test_get_item_details(self):
# delete modified item price record and make as per test_records
frappe.db.sql("""delete from `tabItem Price`""")
+ frappe.db.sql("""delete from `tabBin`""")
to_check = {
"item_code": "_Test Item",
@@ -103,9 +104,26 @@
"batch_no": None,
"uom": "_Test UOM",
"conversion_factor": 1.0,
+ "reserved_qty": 1,
+ "actual_qty": 5,
+ "ordered_qty": 10,
+ "projected_qty": 14,
}
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"
currency = frappe.get_cached_value("Company", company, "default_currency")
@@ -129,7 +147,7 @@
)
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):
expected_item_tax_template = [
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index afad751..94f63a5 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -22,9 +22,6 @@
class MaterialRequest(BuyingController):
- def get_feed(self):
- return
-
def check_if_already_pulled(self):
pass
diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py
index d606751..dbd8de4 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.py
+++ b/erpnext/stock/doctype/packed_item/packed_item.py
@@ -83,8 +83,8 @@
# 1. items were deleted
# 2. if bundle item replaced by another item (same no. of items but different items)
# we maintain list to track recurring item rows as well
- items_before_save = [item.item_code for item in doc_before_save.get("items")]
- items_after_save = [item.item_code for item in doc.get("items")]
+ items_before_save = [(item.name, item.item_code) for item in doc_before_save.get("items")]
+ items_after_save = [(item.name, item.item_code) for item in doc.get("items")]
reset_table = items_before_save != items_after_save
else:
# reset: if via Update Items OR
diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js
index 799406c..8213adb 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.js
+++ b/erpnext/stock/doctype/pick_list/pick_list.js
@@ -51,7 +51,15 @@
if (!(frm.doc.locations && frm.doc.locations.length)) {
frappe.msgprint(__('Add items in the Item Locations table'));
} 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) => {
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 5de7fed..9e6aead 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -100,6 +100,7 @@
item_table,
item.sales_order_item,
["picked_qty", stock_qty_field],
+ for_update=True,
)
if self.docstatus == 1:
@@ -118,7 +119,7 @@
def update_sales_order_picking_status(sales_orders: Set[str]) -> None:
for sales_order in sales_orders:
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()
def set_item_locations(self, save=False):
@@ -135,6 +136,7 @@
# reset
self.delete_key("locations")
+ updated_locations = frappe._dict()
for item_doc in items:
item_code = item_doc.item_code
@@ -155,7 +157,26 @@
for row in locations:
location = item_doc.as_dict()
location.update(row)
- self.append("locations", location)
+ 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)
# If table is empty on update after submit, set stock_qty, picked_qty to 0 so that indicator is red
# and give feedback to the user. This is to avoid empty Pick Lists.
@@ -192,13 +213,13 @@
if item_map.get(key):
item_map[key].qty += item.qty
- item_map[key].stock_qty += item.stock_qty
+ item_map[key].stock_qty += flt(item.stock_qty, item.precision("stock_qty"))
else:
item_map[key] = item
# maintain count of each item (useful to limit get query)
self.item_count_map.setdefault(item_code, 0)
- self.item_count_map[item_code] += item.stock_qty
+ self.item_count_map[item_code] += flt(item.stock_qty, item.precision("stock_qty"))
return item_map.values()
@@ -209,7 +230,8 @@
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
def before_print(self, settings=None):
- self.group_similar_items()
+ if self.group_same_items:
+ self.group_similar_items()
def group_similar_items(self):
group_item_qty = defaultdict(float)
@@ -242,7 +264,7 @@
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])
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(
item_table,
so_row,
@@ -441,7 +463,7 @@
sle.`batch_no`,
sle.`item_code`
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(
warehouse_condition=warehouse_condition
),
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index f552299..71663e8 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -445,6 +445,20 @@
pl.before_print()
self.assertEqual(len(pl.locations), 4)
+ # grouping should not happen if group_same_items is False
+ pl = frappe.get_doc(
+ doctype="Pick List",
+ group_same_items=False,
+ locations=[
+ _dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
+ _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
+ _dict(item_code="A", warehouse="X", qty=3, picked_qty=2),
+ _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2),
+ ],
+ )
+ pl.before_print()
+ self.assertEqual(len(pl.locations), 4)
+
# grouping should halve the number of items
pl = frappe.get_doc(
doctype="Pick List",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index ab91d7c..8f04358 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -58,12 +58,13 @@
"total",
"net_total",
"taxes_charges_section",
+ "tax_category",
"taxes_and_charges",
"shipping_col",
- "tax_category",
- "column_break_53",
"shipping_rule",
+ "column_break_53",
"incoterm",
+ "named_place",
"taxes_section",
"taxes",
"totals",
@@ -1225,13 +1226,19 @@
"fieldtype": "Link",
"label": "Incoterm",
"options": "Incoterm"
+ },
+ {
+ "depends_on": "incoterm",
+ "fieldname": "named_place",
+ "fieldtype": "Data",
+ "label": "Named Place"
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2022-11-17 17:29:30.067536",
+ "modified": "2022-12-12 18:40:32.447752",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index dc9f2b2..b634146 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1501,6 +1501,49 @@
self.assertTrue(return_pi.docstatus == 1)
+ def test_disable_last_purchase_rate(self):
+ from erpnext.stock.get_item_details import get_item_details
+
+ item = make_item(
+ "_Test Disable Last Purchase Rate",
+ {"is_purchase_item": 1, "is_stock_item": 1},
+ )
+
+ frappe.db.set_single_value("Buying Settings", "disable_last_purchase_rate", 1)
+
+ pr = make_purchase_receipt(
+ qty=1,
+ rate=100,
+ item_code=item.name,
+ )
+
+ args = pr.items[0].as_dict()
+ args.update(
+ {
+ "supplier": pr.supplier,
+ "doctype": pr.doctype,
+ "conversion_rate": pr.conversion_rate,
+ "currency": pr.currency,
+ "company": pr.company,
+ "posting_date": pr.posting_date,
+ "posting_time": pr.posting_time,
+ }
+ )
+
+ res = get_item_details(args)
+ self.assertEqual(res.get("last_purchase_rate"), 0)
+
+ frappe.db.set_single_value("Buying Settings", "disable_last_purchase_rate", 0)
+
+ pr = make_purchase_receipt(
+ qty=1,
+ rate=100,
+ item_code=item.name,
+ )
+
+ res = get_item_details(args)
+ self.assertEqual(res.get("last_purchase_rate"), 100)
+
def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index a2748d0..541d4d1 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -766,13 +766,13 @@
@frappe.whitelist()
def auto_fetch_serial_number(
- qty: float,
+ qty: int,
item_code: str,
warehouse: str,
posting_date: Optional[str] = None,
batch_nos: Optional[Union[str, List[str]]] = None,
for_doctype: Optional[str] = None,
- exclude_sr_nos: Optional[List[str]] = None,
+ exclude_sr_nos=None,
) -> List[str]:
filters = frappe._dict({"item_code": item_code, "warehouse": warehouse})
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index b910244..897fca3 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -112,6 +112,10 @@
}
});
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) {
@@ -129,7 +133,7 @@
let quality_inspection_field = frm.get_docfield("items", "quality_inspection");
quality_inspection_field.get_route_options_for_new_doc = function(row) {
- if (frm.is_new()) return;
+ if (frm.is_new()) return {};
return {
"inspection_type": "Incoming",
"reference_type": frm.doc.doctype,
@@ -326,7 +330,11 @@
}
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) {
@@ -939,7 +947,10 @@
method: "get_items",
callback: function(r) {
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);
+ }
}
});
}
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 7e9420d..9c0f1fc 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -7,7 +7,7 @@
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
- "items_section",
+ "stock_entry_details_tab",
"naming_series",
"stock_entry_type",
"outgoing_stock_entry",
@@ -26,15 +26,20 @@
"posting_time",
"set_posting_time",
"inspection_required",
- "from_bom",
"apply_putaway_rule",
- "sb1",
- "bom_no",
- "fg_completed_qty",
- "cb1",
+ "items_tab",
+ "bom_info_section",
+ "from_bom",
"use_multi_level_bom",
+ "bom_no",
+ "cb1",
+ "fg_completed_qty",
"get_items",
- "section_break_12",
+ "section_break_7qsm",
+ "process_loss_percentage",
+ "column_break_e92r",
+ "process_loss_qty",
+ "section_break_jwgn",
"from_warehouse",
"source_warehouse_address",
"source_address_display",
@@ -44,6 +49,7 @@
"target_address_display",
"sb0",
"scan_barcode",
+ "items_section",
"items",
"get_stock_and_rate",
"section_break_19",
@@ -54,6 +60,7 @@
"additional_costs_section",
"additional_costs",
"total_additional_costs",
+ "supplier_info_tab",
"contact_section",
"supplier",
"supplier_name",
@@ -61,7 +68,7 @@
"address_display",
"accounting_dimensions_section",
"project",
- "dimension_col_break",
+ "other_info_tab",
"printing_settings",
"select_print_heading",
"print_settings_col_break",
@@ -79,11 +86,6 @@
],
"fields": [
{
- "fieldname": "items_section",
- "fieldtype": "Section Break",
- "oldfieldtype": "Section Break"
- },
- {
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Series",
@@ -236,18 +238,13 @@
},
{
"default": "0",
- "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \t\t\t\t\t\"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
+ "depends_on": "eval:in_list([\"Material Issue\", \"Material Transfer\", \"Manufacture\", \"Repack\", \"Send to Subcontractor\", \"Material Transfer for Manufacture\", \"Material Consumption for Manufacture\"], doc.purpose)",
"fieldname": "from_bom",
"fieldtype": "Check",
"label": "From BOM",
"print_hide": 1
},
{
- "depends_on": "eval: doc.from_bom && (doc.purpose!==\"Sales Return\" && doc.purpose!==\"Purchase Return\")",
- "fieldname": "sb1",
- "fieldtype": "Section Break"
- },
- {
"depends_on": "from_bom",
"fieldname": "bom_no",
"fieldtype": "Link",
@@ -286,10 +283,6 @@
"print_hide": 1
},
{
- "fieldname": "section_break_12",
- "fieldtype": "Section Break"
- },
- {
"description": "Sets 'Source Warehouse' in each row of the items table.",
"fieldname": "from_warehouse",
"fieldtype": "Link",
@@ -411,7 +404,7 @@
"collapsible": 1,
"collapsible_depends_on": "total_additional_costs",
"fieldname": "additional_costs_section",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Additional Costs"
},
{
@@ -576,14 +569,10 @@
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
- "fieldtype": "Section Break",
+ "fieldtype": "Tab Break",
"label": "Accounting Dimensions"
},
{
- "fieldname": "dimension_col_break",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "pick_list",
"fieldtype": "Link",
"label": "Pick List",
@@ -621,6 +610,66 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "items_tab",
+ "fieldtype": "Tab Break",
+ "label": "Items"
+ },
+ {
+ "fieldname": "bom_info_section",
+ "fieldtype": "Section Break",
+ "label": "BOM Info"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_jwgn",
+ "fieldtype": "Section Break",
+ "label": "Default Warehouse"
+ },
+ {
+ "fieldname": "other_info_tab",
+ "fieldtype": "Tab Break",
+ "label": "Other Info"
+ },
+ {
+ "fieldname": "supplier_info_tab",
+ "fieldtype": "Tab Break",
+ "label": "Supplier Info"
+ },
+ {
+ "fieldname": "stock_entry_details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Details",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "fieldname": "section_break_7qsm",
+ "fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "process_loss_percentage",
+ "fieldname": "process_loss_qty",
+ "fieldtype": "Float",
+ "label": "Process Loss Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_e92r",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.from_bom && doc.fg_completed_qty",
+ "fetch_from": "bom_no.process_loss_percentage",
+ "fetch_if_empty": 1,
+ "fieldname": "process_loss_percentage",
+ "fieldtype": "Percent",
+ "label": "% Process Loss"
+ },
+ {
+ "fieldname": "items_section",
+ "fieldtype": "Section Break",
+ "label": "Items"
}
],
"icon": "fa fa-file-text",
@@ -628,7 +677,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-10-07 14:39:51.943770",
+ "modified": "2023-01-03 16:02:50.741816",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index f6c53f7..1755f28 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -83,9 +83,6 @@
}
)
- def get_feed(self):
- return self.stock_entry_type
-
def onload(self):
for item in self.get("items"):
item.update(get_bin_details(item.item_code, item.s_warehouse))
@@ -116,6 +113,7 @@
self.validate_warehouse()
self.validate_work_order()
self.validate_bom()
+ self.set_process_loss_qty()
self.validate_purchase_order()
self.validate_subcontracting_order()
@@ -126,7 +124,7 @@
self.validate_with_material_request()
self.validate_batch()
self.validate_inspection()
- # self.validate_fg_completed_qty()
+ self.validate_fg_completed_qty()
self.validate_difference_account()
self.set_job_card_data()
self.set_purpose_for_stock_entry()
@@ -388,11 +386,20 @@
item_wise_qty = {}
if self.purpose == "Manufacture" and self.work_order:
for d in self.items:
- if d.is_finished_item or d.is_process_loss:
+ if d.is_finished_item:
item_wise_qty.setdefault(d.item_code, []).append(d.qty)
+ precision = frappe.get_precision("Stock Entry Detail", "qty")
for item_code, qty_list in item_wise_qty.items():
- total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty"))
+ total = flt(sum(qty_list), precision)
+
+ if (self.fg_completed_qty - total) > 0:
+ self.process_loss_qty = flt(self.fg_completed_qty - total, precision)
+ self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty)
+
+ if self.process_loss_qty:
+ total += flt(self.process_loss_qty, precision)
+
if self.fg_completed_qty != total:
frappe.throw(
_("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format(
@@ -471,7 +478,7 @@
if self.purpose == "Manufacture":
if validate_for_manufacture:
- if d.is_finished_item or d.is_scrap_item or d.is_process_loss:
+ if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None
if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
@@ -648,9 +655,7 @@
outgoing_items_cost = self.set_rate_for_outgoing_items(
reset_outgoing_rate, raise_error_if_no_rate
)
- finished_item_qty = sum(
- d.transfer_qty for d in self.items if d.is_finished_item or d.is_process_loss
- )
+ finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
# Set basic rate for incoming items
for d in self.get("items"):
@@ -689,8 +694,6 @@
# do not round off basic rate to avoid precision loss
d.basic_rate = flt(d.basic_rate)
- if d.is_process_loss:
- d.basic_rate = flt(0.0)
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
@@ -992,7 +995,9 @@
)
def mark_finished_and_scrap_items(self):
- if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
+ if self.purpose != "Repack" and any(
+ [d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]
+ ):
return
finished_item = self.get_finished_item()
@@ -1241,7 +1246,6 @@
if self.work_order:
pro_doc = frappe.get_doc("Work Order", self.work_order)
_validate_work_order(pro_doc)
- pro_doc.run_method("update_status")
if self.fg_completed_qty:
pro_doc.run_method("update_work_order_qty")
@@ -1249,6 +1253,7 @@
pro_doc.run_method("update_planned_qty")
pro_doc.update_batch_produced_qty(self)
+ pro_doc.run_method("update_status")
if not pro_doc.operations:
pro_doc.set_actual_dates()
@@ -1469,11 +1474,11 @@
# add finished goods item
if self.purpose in ("Manufacture", "Repack"):
+ self.set_process_loss_qty()
self.load_items_from_bom()
self.set_scrap_items()
self.set_actual_qty()
- self.update_items_for_process_loss()
self.validate_customer_provided_item()
self.calculate_rate_and_amount(raise_error_if_no_rate=False)
@@ -1486,6 +1491,21 @@
self.add_to_stock_entry_detail(scrap_item_dict, bom_no=self.bom_no)
+ def set_process_loss_qty(self):
+ if self.purpose not in ("Manufacture", "Repack"):
+ return
+
+ self.process_loss_qty = 0.0
+ if not self.process_loss_percentage:
+ self.process_loss_percentage = frappe.get_cached_value(
+ "BOM", self.bom_no, "process_loss_percentage"
+ )
+
+ if self.process_loss_percentage:
+ self.process_loss_qty = flt(
+ (flt(self.fg_completed_qty) * flt(self.process_loss_percentage)) / 100
+ )
+
def set_work_order_details(self):
if not getattr(self, "pro_doc", None):
self.pro_doc = frappe._dict()
@@ -1518,7 +1538,7 @@
args = {
"to_warehouse": to_warehouse,
"from_warehouse": "",
- "qty": self.fg_completed_qty,
+ "qty": flt(self.fg_completed_qty) - flt(self.process_loss_qty),
"item_name": item.item_name,
"description": item.description,
"stock_uom": item.stock_uom,
@@ -1966,7 +1986,6 @@
)
se_child.is_finished_item = item_row.get("is_finished_item", 0)
se_child.is_scrap_item = item_row.get("is_scrap_item", 0)
- se_child.is_process_loss = item_row.get("is_process_loss", 0)
se_child.po_detail = item_row.get("po_detail")
se_child.sco_rm_detail = item_row.get("sco_rm_detail")
@@ -2213,42 +2232,17 @@
material_requests.append(material_request)
frappe.db.set_value("Material Request", material_request, "transfer_status", status)
- def update_items_for_process_loss(self):
- process_loss_dict = {}
- for d in self.get("items"):
- if not d.is_process_loss:
- continue
-
- scrap_warehouse = frappe.db.get_single_value(
- "Manufacturing Settings", "default_scrap_warehouse"
- )
- if scrap_warehouse is not None:
- d.t_warehouse = scrap_warehouse
- d.is_scrap_item = 0
-
- if d.item_code not in process_loss_dict:
- process_loss_dict[d.item_code] = [flt(0), flt(0)]
- process_loss_dict[d.item_code][0] += flt(d.transfer_qty)
- process_loss_dict[d.item_code][1] += flt(d.qty)
-
- for d in self.get("items"):
- if not d.is_finished_item or d.item_code not in process_loss_dict:
- continue
- # Assumption: 1 finished item has 1 row.
- d.transfer_qty -= process_loss_dict[d.item_code][0]
- d.qty -= process_loss_dict[d.item_code][1]
-
def set_serial_no_batch_for_finished_good(self):
- serial_nos = ""
+ serial_nos = []
if self.pro_doc.serial_no:
- serial_nos = self.get_serial_nos_for_fg()
+ serial_nos = self.get_serial_nos_for_fg() or []
for row in self.items:
if row.is_finished_item and row.item_code == self.pro_doc.production_item:
if serial_nos:
row.serial_no = "\n".join(serial_nos[0 : cint(row.qty)])
- def get_serial_nos_for_fg(self, args):
+ def get_serial_nos_for_fg(self):
fields = [
"`tabStock Entry`.`name`",
"`tabStock Entry Detail`.`qty`",
@@ -2264,9 +2258,7 @@
]
stock_entries = frappe.get_all("Stock Entry", fields=fields, filters=filters)
-
- if self.pro_doc.serial_no:
- return self.get_available_serial_nos(stock_entries)
+ return self.get_available_serial_nos(stock_entries)
def get_available_serial_nos(self, stock_entries):
used_serial_nos = []
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 95f4f5f..fe81a87 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -20,7 +20,6 @@
"is_finished_item",
"is_scrap_item",
"quality_inspection",
- "is_process_loss",
"subcontracted_item",
"section_break_8",
"description",
@@ -561,12 +560,6 @@
},
{
"default": "0",
- "fieldname": "is_process_loss",
- "fieldtype": "Check",
- "label": "Is Process Loss"
- },
- {
- "default": "0",
"depends_on": "barcode",
"fieldname": "has_item_scanned",
"fieldtype": "Check",
@@ -578,7 +571,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-11-02 13:00:34.258828",
+ "modified": "2023-01-03 14:51:16.575515",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index c64370d..052f778 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -221,14 +221,9 @@
def on_doctype_update():
- if not frappe.db.has_index("tabStock Ledger Entry", "posting_sort_index"):
- frappe.db.commit()
- frappe.db.add_index(
- "Stock Ledger Entry",
- fields=["posting_date", "posting_time", "name"],
- index_name="posting_sort_index",
- )
-
+ frappe.db.add_index(
+ "Stock Ledger Entry", fields=["posting_date", "posting_time"], index_name="posting_sort_index"
+ )
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 3a0b38a..398b3c9 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -715,8 +715,8 @@
def get_stock_balance_for(
item_code: str,
warehouse: str,
- posting_date: str,
- posting_time: str,
+ posting_date,
+ posting_time,
batch_no: Optional[str] = None,
with_valuation_rate: bool = True,
):
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 108611c..f7fcb30 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -102,9 +102,11 @@
elif out.get("warehouse"):
if doc and doc.get("doctype") == "Purchase Order":
# calculate company_total_stock only for po
- bin_details = get_bin_details(args.item_code, out.warehouse, args.company)
+ bin_details = get_bin_details(
+ args.item_code, out.warehouse, args.company, include_child_warehouses=True
+ )
else:
- bin_details = get_bin_details(args.item_code, out.warehouse)
+ bin_details = get_bin_details(args.item_code, out.warehouse, include_child_warehouses=True)
out.update(bin_details)
@@ -113,7 +115,7 @@
if args.get(key) is None:
args[key] = value
- data = get_pricing_rule_for_item(args, out.price_list_rate, doc, for_validate=for_validate)
+ data = get_pricing_rule_for_item(args, doc=doc, for_validate=for_validate)
out.update(data)
@@ -409,7 +411,9 @@
args.stock_qty = out.stock_qty
# calculate last purchase rate
- if args.get("doctype") in purchase_doctypes:
+ if args.get("doctype") in purchase_doctypes and not frappe.db.get_single_value(
+ "Buying Settings", "disable_last_purchase_rate"
+ ):
from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate
out.last_purchase_rate = item_last_purchase_rate(
@@ -811,6 +815,9 @@
flt(price_list_rate) * flt(args.plc_conversion_rate) / flt(args.conversion_rate)
)
+ if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
+ return out
+
if not out.price_list_rate and args.transaction_type == "buying":
from erpnext.stock.doctype.item.item import get_last_purchase_details
@@ -828,9 +835,9 @@
):
if frappe.has_permission("Item Price", "write"):
price_list_rate = (
- (args.rate + args.discount_amount) / args.get("conversion_factor")
+ (flt(args.rate) + flt(args.discount_amount)) / args.get("conversion_factor")
if args.get("conversion_factor")
- else (args.rate + args.discount_amount)
+ else (flt(args.rate) + flt(args.discount_amount))
)
item_price = frappe.db.get_value(
@@ -1060,7 +1067,9 @@
res[fieldname] = pos_profile.get(fieldname)
if res.get("warehouse"):
- res.actual_qty = get_bin_details(args.item_code, res.warehouse).get("actual_qty")
+ res.actual_qty = get_bin_details(
+ args.item_code, res.warehouse, include_child_warehouses=True
+ ).get("actual_qty")
return res
@@ -1171,16 +1180,31 @@
@frappe.whitelist()
-def get_bin_details(item_code, warehouse, company=None):
- bin_details = frappe.db.get_value(
- "Bin",
- {"item_code": item_code, "warehouse": warehouse},
- ["projected_qty", "actual_qty", "reserved_qty"],
- as_dict=True,
- cache=True,
- ) or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
+def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False):
+ bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0, "ordered_qty": 0}
+
+ if warehouse:
+ from frappe.query_builder.functions import Coalesce, Sum
+
+ from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
+
+ warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse]
+
+ bin = frappe.qb.DocType("Bin")
+ bin_details = (
+ frappe.qb.from_(bin)
+ .select(
+ Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"),
+ Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"),
+ Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"),
+ Coalesce(Sum(bin.ordered_qty), 0).as_("ordered_qty"),
+ )
+ .where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses)))
+ ).run(as_dict=True)[0]
+
if company:
bin_details["company_total_stock"] = get_company_total_stock(item_code, company)
+
return bin_details
@@ -1305,7 +1329,7 @@
item_doc = frappe.db.get_value("Item", args.item_code, ["name", "variant_of"], as_dict=1)
item_details = get_price_list_rate(args, item_doc)
- item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate))
+ item_details.update(get_pricing_rule_for_item(args))
return item_details
diff --git a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
index a6fc049..c4358b8 100644
--- a/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
+++ b/erpnext/stock/report/itemwise_recommended_reorder_level/itemwise_recommended_reorder_level.py
@@ -82,7 +82,7 @@
item.safety_stock,
item.lead_time_days,
)
- .where(item.is_stock_item == 1)
+ .where((item.is_stock_item == 1) & (item.disabled == 0))
)
if brand := filters.get("brand"):
diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
index 99f820e..106e877 100644
--- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
+++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
@@ -41,7 +41,7 @@
key = (d.voucher_type, d.voucher_no)
gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0)
- d.difference_value = abs(d.stock_value - d.account_value)
+ d.difference_value = d.stock_value - d.account_value
if abs(d.difference_value) > 0.1:
data.append(d)
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index e7f55e9..5d75bfd 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -470,8 +470,10 @@
item_code = %(item_code)s
and warehouse = %(warehouse)s
and is_cancelled = 0
- and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
-
+ and (
+ posting_date = %(posting_date)s and
+ time_format(posting_time, %(time_format)s) = time_format(%(posting_time)s, %(time_format)s)
+ )
order by
creation ASC
for update
@@ -1070,7 +1072,13 @@
and warehouse = %(warehouse)s
and is_cancelled = 0
{voucher_condition}
- and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+ and (
+ posting_date < %(posting_date)s or
+ (
+ posting_date = %(posting_date)s and
+ time_format(posting_time, %(time_format)s) < time_format(%(posting_time)s, %(time_format)s)
+ )
+ )
order by timestamp(posting_date, posting_time) desc, creation desc
limit 1
for update""".format(
@@ -1262,20 +1270,6 @@
(item_code, warehouse, voucher_no, voucher_type),
)
- if not last_valuation_rate:
- # Get valuation rate from last sle for the item against any warehouse
- last_valuation_rate = frappe.db.sql(
- """select valuation_rate
- from `tabStock Ledger Entry` force index (item_code)
- where
- item_code = %s
- AND valuation_rate > 0
- AND is_cancelled = 0
- AND NOT(voucher_no = %s AND voucher_type = %s)
- order by posting_date desc, posting_time desc, name desc limit 1""",
- (item_code, voucher_no, voucher_type),
- )
-
if last_valuation_rate:
return flt(last_valuation_rate[0][0])
@@ -1355,8 +1349,13 @@
and warehouse = %(warehouse)s
and voucher_no != %(voucher_no)s
and is_cancelled = 0
- and timestamp(posting_date, time_format(posting_time, %(time_format)s))
- > timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+ and (
+ posting_date > %(posting_date)s or
+ (
+ posting_date = %(posting_date)s and
+ time_format(posting_time, %(time_format)s) > time_format(%(posting_time)s, %(time_format)s)
+ )
+ )
{datetime_limit_condition}
""",
args,
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index bce5360..7e1915b 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -57,6 +57,7 @@
def before_validate(self):
super(SubcontractingReceipt, self).before_validate()
+ self.validate_items_qty()
self.set_items_bom()
self.set_items_cost_center()
self.set_items_expense_account()
@@ -157,7 +158,7 @@
total_qty = total_amount = 0
for item in self.items:
- if item.name in rm_supp_cost:
+ if item.qty and item.name in rm_supp_cost:
item.rm_supp_cost = rm_supp_cost[item.name]
item.rm_cost_per_qty = item.rm_supp_cost / item.qty
rm_supp_cost.pop(item.name)
@@ -194,6 +195,13 @@
).format(item.idx)
)
+ def validate_items_qty(self):
+ for item in self.items:
+ if not (item.qty or item.rejected_qty):
+ frappe.throw(
+ _("Row {0}: Accepted Qty and Rejected Qty can't be zero at the same time.").format(item.idx)
+ )
+
def set_items_bom(self):
if self.is_return:
for item in self.items:
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 7f3e0cf..f23419e 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -18,9 +18,6 @@
class Issue(Document):
- def get_feed(self):
- return "{0}: {1}".format(_(self.status), self.subject)
-
def validate(self):
if self.is_new() and self.via_customer_portal:
self.flags.create_communication = True
diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.py b/erpnext/support/doctype/warranty_claim/warranty_claim.py
index c86356f..ff63b77 100644
--- a/erpnext/support/doctype/warranty_claim/warranty_claim.py
+++ b/erpnext/support/doctype/warranty_claim/warranty_claim.py
@@ -10,9 +10,6 @@
class WarrantyClaim(TransactionBase):
- def get_feed(self):
- return _("{0}: From {1}").format(self.status, self.customer_name)
-
def validate(self):
if session["user"] != "Guest" and not self.customer:
frappe.throw(_("Customer is required"))
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html
index cf60017..a8188ec 100644
--- a/erpnext/templates/includes/cart/cart_address.html
+++ b/erpnext/templates/includes/cart/cart_address.html
@@ -55,6 +55,7 @@
{% endif %}
<script>
+frappe.boot = {{ boot }}
frappe.ready(() => {
$(document).on('click', '.address-card', (e) => {
const $target = $(e.currentTarget);
diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv
index 0fdd3c9..1014e27 100644
--- a/erpnext/translations/de.csv
+++ b/erpnext/translations/de.csv
@@ -1849,6 +1849,8 @@
Outstanding Cheques and Deposits to clear,Ausstehende Schecks und Anzahlungen zum verbuchen,
Outstanding for {0} cannot be less than zero ({1}),Ausstände für {0} können nicht kleiner als Null sein ({1}),
Outward taxable supplies(zero rated),Steuerpflichtige Lieferungen aus dem Ausland (null bewertet),
+Over Receipt/Delivery of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Annahme bzw. Lieferung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben."
+Overbilling of {0} {1} ignored for item {2} because you have {3} role.,"Überhöhte Abrechnung von Artikel {2} mit {0} {1} wurde ignoriert, weil Sie die Rolle {3} haben."
Overdue,Überfällig,
Overlap in scoring between {0} and {1},Überlappung beim Scoring zwischen {0} und {1},
Overlapping conditions found between:,Überlagernde Bedingungen gefunden zwischen:,
diff --git a/erpnext/translations/zh.csv b/erpnext/translations/zh.csv
index 716f1f2..d1f1b07 100644
--- a/erpnext/translations/zh.csv
+++ b/erpnext/translations/zh.csv
@@ -453,11 +453,11 @@
Cancel the journal entry {0} first,首先取消日记条目{0},
Canceled,取消,
"Cannot Submit, Employees left to mark attendance",无法提交,不能为已离职员工登记考勤,
-Cannot be a fixed asset item as Stock Ledger is created.,不能成为股票分类账创建的固定资产项目。,
+Cannot be a fixed asset item as Stock Ledger is created.,不能成为库存分类账创建的固定资产项目。,
Cannot cancel because submitted Stock Entry {0} exists,不能取消,因为提交的仓储记录{0}已经存在,
Cannot cancel transaction for Completed Work Order.,无法取消已完成工单的交易。,
Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3},无法取消{0} {1},因为序列号{2}不属于仓库{3},
-Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item,股票交易后不能更改属性。创建一个新项目并将库存转移到新项目,
+Cannot change Attributes after stock transaction. Make a new Item and transfer stock to the new Item,库存交易后不能更改属性。创建一个新项目并将库存转移到新项目,
Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved.,财年保存后便不能更改财年开始日期和结束日期,
Cannot change Service Stop Date for item in row {0},无法更改行{0}中项目的服务停止日期,
Cannot change Variant properties after stock transaction. You will have to make a new Item to do this.,存货业务发生后不能更改变体物料的属性。需要新建新物料。,
@@ -2309,7 +2309,7 @@
Received,收到,
Received On,收到的,
Received Quantity,收到的数量,
-Received Stock Entries,收到的股票条目,
+Received Stock Entries,收到的库存条目,
Receiver List is empty. Please create Receiver List,接收人列表为空。请创建接收人列表,
Recipients,收件人,
Reconcile,核消(对帐),
@@ -2783,7 +2783,7 @@
State/UT Tax,州/ UT税,
Statement of Account,对账单,
Status must be one of {0},状态必须是{0}中的一个,
-Stock,股票,
+Stock,库存,
Stock Adjustment,库存调整,
Stock Analytics,库存分析,
Stock Assets,库存资产,
@@ -2963,7 +2963,7 @@
The holiday on {0} is not between From Date and To Date,在{0}这个节日之间没有从日期和结束日期,
The name of the institute for which you are setting up this system.,对于要为其建立这个系统的该机构的名称。,
The name of your company for which you are setting up this system.,贵公司的名称,
-The number of shares and the share numbers are inconsistent,股份数量和股票数量不一致,
+The number of shares and the share numbers are inconsistent,股份数量和库存数量不一致,
The payment gateway account in plan {0} is different from the payment gateway account in this payment request,计划{0}中的支付网关帐户与此付款请求中的支付网关帐户不同,
The selected BOMs are not for the same item,所选物料清单不能用于同一个物料,
The selected item cannot have Batch,所选物料不能有批次,
@@ -3514,7 +3514,7 @@
Ageing Range 4,老化范围4,
Allocated amount cannot be greater than unadjusted amount,分配的金额不能大于未调整的金额,
Allocated amount cannot be negative,分配数量不能为负数,
-"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry",差异账户必须是资产/负债类型账户,因为此股票分录是开仓分录,
+"Difference Account must be a Asset/Liability type account, since this Stock Entry is an Opening Entry",差异账户必须是资产/负债类型账户,因为此库存分录是开仓分录,
Error in some rows,某些行出错,
Import Successful,导入成功,
Please save first,请先保存,
@@ -3531,7 +3531,7 @@
Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers,GSTIN无效!您输入的输入与UIN持有人或非居民OIDAR服务提供商的GSTIN格式不符,
Invoice Grand Total,发票总计,
Last carbon check date cannot be a future date,最后的碳检查日期不能是未来的日期,
-Make Stock Entry,进入股票,
+Make Stock Entry,进入库存,
Quality Feedback,质量反馈,
Quality Feedback Template,质量反馈模板,
Rules for applying different promotional schemes.,适用不同促销计划的规则。,
@@ -3626,7 +3626,7 @@
BOM Comparison Tool,BOM比较工具,
BOM recursion: {0} cannot be child of {1},BOM递归:{0}不能是{1}的子代,
BOM recursion: {0} cannot be parent or child of {1},BOM递归:{0}不能是{1}的父级或子级,
-Back to Home,回到家,
+Back to Home,回到主页,
Back to Messages,回到消息,
Bank Data mapper doesn't exist,银行数据映射器不存在,
Bank Details,银行明细,
@@ -3786,7 +3786,7 @@
Help Article,帮助文章,
"Helps you keep tracks of Contracts based on Supplier, Customer and Employee",帮助您根据供应商,客户和员工记录合同,
Helps you manage appointments with your leads,帮助您管理潜在客户的约会,
-Home,家,
+Home,主页,
IBAN is not valid,IBAN无效,
Import Data from CSV / Excel files.,从CSV / Excel文件导入数据。,
In Progress,进行中,
@@ -4064,8 +4064,8 @@
Status,状态,
Status must be Cancelled or Completed,状态必须已取消或已完成,
Stock Balance Report,库存余额报告,
-Stock Entry has been already created against this Pick List,已经根据此选择列表创建了股票输入,
-Stock Ledger ID,股票分类帐编号,
+Stock Entry has been already created against this Pick List,已经根据此选择列表创建了库存输入,
+Stock Ledger ID,库存分类帐编号,
Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.,库存值({0})和帐户余额({1})与帐户{2}及其链接的仓库不同步。,
Stores - {0},商店-{0},
Student with email {0} does not exist,电子邮件{0}的学生不存在,
@@ -6127,7 +6127,7 @@
Procedure Prescription,程序处方,
Service Unit,服务单位,
Consumables,耗材,
-Consume Stock,消费股票,
+Consume Stock,消费库存,
Invoice Consumables Separately,发票耗材分开,
Consumption Invoiced,消费发票,
Consumable Total Amount,耗材总量,
@@ -8285,8 +8285,8 @@
Warranty Period (Days),保修期限(天数),
Serial No Details,序列号信息,
MAT-STE-.YYYY.-,MAT-STE-.YYYY.-,
-Stock Entry Type,股票进入类型,
-Stock Entry (Outward GIT),股票进入(外向GIT),
+Stock Entry Type,库存进入类型,
+Stock Entry (Outward GIT),库存进入(外向GIT),
Material Consumption for Manufacture,生产所需的材料消耗,
Repack,包装,
Send to Subcontractor,发送给分包商,
@@ -8318,8 +8318,8 @@
BOM No. for a Finished Good Item,成品物料的物料清单编号,
Material Request used to make this Stock Entry,创建此手工库存移动的材料申请,
Subcontracted Item,外包物料,
-Against Stock Entry,反对股票进入,
-Stock Entry Child,股票入境儿童,
+Against Stock Entry,反对库存进入,
+Stock Entry Child,库存入境儿童,
PO Supplied Item,PO提供的物品,
Reference Purchase Receipt,参考购买收据,
Stock Ledger Entry,库存分类帐分录,
@@ -8571,7 +8571,7 @@
Serial No Status,序列号状态,
Serial No Warranty Expiry,序列号/保修到期,
Stock Ageing,库存账龄,
-Stock and Account Value Comparison,股票和账户价值比较,
+Stock and Account Value Comparison,库存和账户价值比较,
Stock Projected Qty,预期可用库存,
Student and Guardian Contact Details,学生和监护人联系方式,
Student Batch-Wise Attendance,学生按批考勤,
@@ -9655,7 +9655,7 @@
Notify by Email on Creation of Automatic Material Request,通过电子邮件通知创建自动物料请求,
Allow Material Transfer from Delivery Note to Sales Invoice,允许物料从交货单转移到销售发票,
Allow Material Transfer from Purchase Receipt to Purchase Invoice,允许从收货到采购发票的物料转移,
-Freeze Stocks Older Than (Days),冻结大于(天)的股票,
+Freeze Stocks Older Than (Days),冻结大于(天)的库存,
Role Allowed to Edit Frozen Stock,允许角色编辑冻结库存,
The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount,付款条目{0}的未分配金额大于银行交易的未分配金额,
Payment Received,已收到付款,
@@ -9698,7 +9698,7 @@
Item {0} {1},项目{0} {1},
Last Stock Transaction for item {0} under warehouse {1} was on {2}.,仓库{1}下项目{0}的上次库存交易在{2}上。,
Stock Transactions for Item {0} under warehouse {1} cannot be posted before this time.,在此之前,不能过帐仓库{1}下物料{0}的库存交易。,
-Posting future stock transactions are not allowed due to Immutable Ledger,由于总帐不可变,不允许过帐未来的股票交易,
+Posting future stock transactions are not allowed due to Immutable Ledger,由于总帐不可变,不允许过帐未来的库存交易,
A BOM with name {0} already exists for item {1}.,项目{1}的名称为{0}的BOM已存在。,
{0}{1} Did you rename the item? Please contact Administrator / Tech support,{0} {1}您是否重命名了该项目?请联系管理员/技术支持,
At row #{0}: the sequence id {1} cannot be less than previous row sequence id {2},在第{0}行:序列ID {1}不能小于上一行的序列ID {2},
diff --git a/erpnext/utilities/__init__.py b/erpnext/utilities/__init__.py
index c2b4229..24bfdc6 100644
--- a/erpnext/utilities/__init__.py
+++ b/erpnext/utilities/__init__.py
@@ -1,6 +1,9 @@
## temp utility
+from contextlib import contextmanager
+
import frappe
+from frappe import _
from frappe.utils import cstr
from erpnext.utilities.activation import get_level
@@ -35,3 +38,16 @@
domain = frappe.get_cached_value("Company", cstr(company), "domain")
return {"company": company, "domain": domain, "activation": get_level()}
+
+
+@contextmanager
+def payment_app_import_guard():
+ marketplace_link = '<a href="https://frappecloud.com/marketplace/apps/payments">Marketplace</a>'
+ github_link = '<a href="https://github.com/frappe/payments/">GitHub</a>'
+ msg = _("payments app is not installed. Please install it from {} or {}").format(
+ marketplace_link, github_link
+ )
+ try:
+ yield
+ except ImportError:
+ frappe.throw(msg, title=_("Missing Payments App"))
diff --git a/erpnext/utilities/product.py b/erpnext/utilities/product.py
index 04ee0b3..afe9654 100644
--- a/erpnext/utilities/product.py
+++ b/erpnext/utilities/product.py
@@ -110,6 +110,7 @@
"conversion_rate": 1,
"for_shopping_cart": True,
"currency": frappe.db.get_value("Price List", price_list, "currency"),
+ "doctype": "Quotation",
}
)
diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js
index 46ac155..d02cdad 100644
--- a/erpnext/www/book_appointment/index.js
+++ b/erpnext/www/book_appointment/index.js
@@ -2,8 +2,6 @@
initialise_select_date();
})
-window.holiday_list = [];
-
async function initialise_select_date() {
navigate_to_page(1);
await get_global_variables();
@@ -20,7 +18,6 @@
window.timezones = (await frappe.call({
method:'erpnext.www.book_appointment.index.get_timezones'
})).message;
- window.holiday_list = window.appointment_settings.holiday_list;
}
function setup_timezone_selector() {
diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py
index 06e99da..dfca946 100644
--- a/erpnext/www/book_appointment/index.py
+++ b/erpnext/www/book_appointment/index.py
@@ -26,8 +26,12 @@
@frappe.whitelist(allow_guest=True)
def get_appointment_settings():
- settings = frappe.get_doc("Appointment Booking Settings")
- settings.holiday_list = frappe.get_doc("Holiday List", settings.holiday_list)
+ settings = frappe.get_cached_value(
+ "Appointment Booking Settings",
+ None,
+ ["advance_booking_days", "appointment_duration", "success_redirect_url"],
+ as_dict=True,
+ )
return settings
@@ -106,7 +110,7 @@
appointment.customer_details = contact.get("notes", None)
appointment.customer_email = contact.get("email", None)
appointment.status = "Open"
- appointment.insert()
+ appointment.insert(ignore_permissions=True)
return appointment
diff --git a/erpnext/www/book_appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py
index 1a5ba9d..3beb866 100644
--- a/erpnext/www/book_appointment/verify/index.py
+++ b/erpnext/www/book_appointment/verify/index.py
@@ -2,7 +2,6 @@
from frappe.utils.verified_command import verify_request
-@frappe.whitelist(allow_guest=True)
def get_context(context):
if not verify_request():
context.success = False