Merge branch 'develop' into asset_depreciation_schedule
This commit is contained in:
commit
72c2e77a5d
@ -302,7 +302,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
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 = (
|
||||
|
@ -137,7 +137,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
|
||||
)
|
||||
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(
|
||||
|
@ -26,7 +26,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
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 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
}
|
||||
},
|
||||
|
||||
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 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
|
||||
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 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
},
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -14,6 +14,9 @@
|
||||
"get_entries",
|
||||
"accounts",
|
||||
"section_break_6",
|
||||
"gain_loss_unbooked",
|
||||
"gain_loss_booked",
|
||||
"column_break_10",
|
||||
"total_gain_loss",
|
||||
"amended_from"
|
||||
],
|
||||
@ -59,13 +62,6 @@
|
||||
"fieldname": "section_break_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",
|
||||
@ -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",
|
||||
|
@ -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 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
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 @@ class ExchangeRateRevaluation(Document):
|
||||
|
||||
@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 @@ class ExchangeRateRevaluation(Document):
|
||||
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 @@ class ExchangeRateRevaluation(Document):
|
||||
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 @@ class ExchangeRateRevaluation(Document):
|
||||
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 @@ class ExchangeRateRevaluation(Document):
|
||||
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 @@ class ExchangeRateRevaluation(Document):
|
||||
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 @@ class ExchangeRateRevaluation(Document):
|
||||
{
|
||||
"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 @@ class ExchangeRateRevaluation(Document):
|
||||
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"],
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -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",
|
||||
|
@ -95,7 +95,15 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
# 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
|
||||
|
@ -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",
|
||||
|
@ -605,16 +605,18 @@ class JournalEntry(AccountsController):
|
||||
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
|
||||
@ -652,16 +654,17 @@ class JournalEntry(AccountsController):
|
||||
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"):
|
||||
@ -790,7 +793,7 @@ class JournalEntry(AccountsController):
|
||||
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)
|
||||
|
@ -252,10 +252,15 @@ def get_other_conditions(conditions, values, args):
|
||||
|
||||
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:
|
||||
|
@ -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",
|
||||
|
@ -199,7 +199,14 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
|
||||
# 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 @@ def process_debit_credit_difference(gl_map):
|
||||
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):
|
||||
|
@ -184,11 +184,9 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
err = err.save().submit()
|
||||
|
||||
# Submit JV for ERR
|
||||
jv = frappe.get_doc(err.make_jv_entry())
|
||||
jv = jv.save()
|
||||
for x in jv.accounts:
|
||||
x.cost_center = get_default_cost_center(jv.company)
|
||||
jv.submit()
|
||||
err_journals = err.make_jv_entries()
|
||||
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
|
||||
je = je.submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
@ -201,7 +199,7 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
report = execute(filters)
|
||||
|
||||
expected_data_for_err = [0, -5, 0, 5]
|
||||
row = [x for x in report[1] if x.voucher_type == jv.doctype and x.voucher_no == jv.name][0]
|
||||
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
|
||||
self.assertEqual(
|
||||
expected_data_for_err,
|
||||
[
|
||||
|
@ -109,8 +109,7 @@ class TestGeneralLedger(FrappeTestCase):
|
||||
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"
|
||||
|
@ -53,9 +53,6 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
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 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
"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,
|
||||
}
|
||||
)
|
||||
|
@ -101,11 +101,8 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
||||
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)
|
||||
|
@ -3,11 +3,14 @@ import unittest
|
||||
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 @@ class TestUtils(unittest.TestCase):
|
||||
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 = [
|
||||
{
|
||||
|
@ -611,11 +611,6 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
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 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
|
||||
payment_entry.set_gain_or_loss(account_details=account_details)
|
||||
|
||||
payment_entry.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)
|
||||
|
||||
|
@ -207,31 +207,36 @@ class PurchaseOrder(BuyingController):
|
||||
)
|
||||
|
||||
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 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
|
||||
)
|
||||
)
|
||||
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"):
|
||||
|
@ -584,7 +584,12 @@ class AccountsController(TransactionBase):
|
||||
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)
|
||||
|
@ -23,7 +23,7 @@ class SellingController(StockController):
|
||||
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()
|
||||
|
@ -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
|
||||
}
|
@ -72,6 +72,24 @@ def get_call_log(call_payload):
|
||||
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 @@ def create_call_log(call_payload):
|
||||
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 @@ def get_call_status(call_id):
|
||||
|
||||
|
||||
@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()
|
||||
|
@ -420,7 +420,6 @@ scheduler_events = {
|
||||
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
|
||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
|
||||
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
|
||||
"erpnext.stock.doctype.stock_entry.stock_entry.audit_incorrect_valuation_entries",
|
||||
],
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
|
@ -20,7 +20,7 @@ frappe.ui.form.on("Project", {
|
||||
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
|
||||
|
@ -25,12 +25,18 @@ class Timesheet(Document):
|
||||
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
|
||||
|
@ -355,12 +355,14 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldname: "deposit",
|
||||
fieldtype: "Currency",
|
||||
label: "Deposit",
|
||||
options: "currency",
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "withdrawal",
|
||||
fieldtype: "Currency",
|
||||
label: "Withdrawal",
|
||||
options: "currency",
|
||||
read_only: 1,
|
||||
},
|
||||
{
|
||||
@ -378,6 +380,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
fieldname: "allocated_amount",
|
||||
fieldtype: "Currency",
|
||||
label: "Allocated Amount",
|
||||
options: "Currency",
|
||||
read_only: 1,
|
||||
},
|
||||
|
||||
@ -385,8 +388,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
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,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -225,7 +225,8 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
|
||||
args: {
|
||||
item_code: item.item_code,
|
||||
warehouse: item.warehouse,
|
||||
company: doc.company
|
||||
company: doc.company,
|
||||
include_child_warehouses: true
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -272,7 +272,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
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,
|
||||
|
@ -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",
|
||||
|
@ -114,7 +114,6 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"print_width": "150px",
|
||||
"reqd": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
@ -865,7 +864,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-18 11:39:01.741665",
|
||||
"modified": "2022-12-25 02:51:10.247569",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
@ -102,6 +102,9 @@ erpnext.stock.ItemDashboard = class ItemDashboard {
|
||||
args: args,
|
||||
callback: function (r) {
|
||||
me.render(r.message);
|
||||
if(me.after_refresh) {
|
||||
me.after_refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -83,6 +83,7 @@ class TestItem(FrappeTestCase):
|
||||
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 @@ class TestItem(FrappeTestCase):
|
||||
"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 @@ class TestItem(FrappeTestCase):
|
||||
)
|
||||
|
||||
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 = [
|
||||
|
@ -51,7 +51,15 @@ frappe.ui.form.on('Pick List', {
|
||||
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) => {
|
||||
|
@ -135,6 +135,7 @@ class PickList(Document):
|
||||
|
||||
# reset
|
||||
self.delete_key("locations")
|
||||
updated_locations = frappe._dict()
|
||||
for item_doc in items:
|
||||
item_code = item_doc.item_code
|
||||
|
||||
@ -155,7 +156,26 @@ class PickList(Document):
|
||||
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.
|
||||
|
@ -112,6 +112,10 @@ frappe.ui.form.on('Stock Entry', {
|
||||
}
|
||||
});
|
||||
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 @@ frappe.ui.form.on('Stock Entry', {
|
||||
|
||||
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 @@ frappe.ui.form.on('Stock Entry', {
|
||||
}
|
||||
|
||||
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 @@ erpnext.stock.StockEntry = class StockEntry extends erpnext.stock.StockControlle
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -4,24 +4,12 @@
|
||||
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
comma_or,
|
||||
cstr,
|
||||
flt,
|
||||
format_time,
|
||||
formatdate,
|
||||
getdate,
|
||||
nowdate,
|
||||
today,
|
||||
)
|
||||
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import process_gl_map
|
||||
@ -2712,62 +2700,3 @@ def get_stock_entry_data(work_order):
|
||||
)
|
||||
.orderby(stock_entry.creation, stock_entry_detail.item_code, stock_entry_detail.idx)
|
||||
).run(as_dict=1)
|
||||
|
||||
|
||||
def audit_incorrect_valuation_entries():
|
||||
# Audit of stock transfer entries having incorrect valuation
|
||||
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
|
||||
|
||||
stock_entries = get_incorrect_stock_entries()
|
||||
|
||||
for stock_entry, values in stock_entries.items():
|
||||
reposting_data = frappe._dict(
|
||||
{
|
||||
"posting_date": values.posting_date,
|
||||
"posting_time": values.posting_time,
|
||||
"voucher_type": "Stock Entry",
|
||||
"voucher_no": stock_entry,
|
||||
"company": values.company,
|
||||
}
|
||||
)
|
||||
|
||||
create_repost_item_valuation_entry(reposting_data)
|
||||
|
||||
|
||||
def get_incorrect_stock_entries() -> Dict:
|
||||
stock_entry = frappe.qb.DocType("Stock Entry")
|
||||
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
|
||||
transfer_purposes = [
|
||||
"Material Transfer",
|
||||
"Material Transfer for Manufacture",
|
||||
"Send to Subcontractor",
|
||||
]
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(stock_entry)
|
||||
.inner_join(stock_ledger_entry)
|
||||
.on(stock_entry.name == stock_ledger_entry.voucher_no)
|
||||
.select(
|
||||
stock_entry.name,
|
||||
stock_entry.company,
|
||||
stock_entry.posting_date,
|
||||
stock_entry.posting_time,
|
||||
Sum(stock_ledger_entry.stock_value_difference).as_("stock_value"),
|
||||
)
|
||||
.where(
|
||||
(stock_entry.docstatus == 1)
|
||||
& (stock_entry.purpose.isin(transfer_purposes))
|
||||
& (stock_ledger_entry.modified > add_days(today(), -2))
|
||||
)
|
||||
.groupby(stock_ledger_entry.voucher_detail_no)
|
||||
.having(Sum(stock_ledger_entry.stock_value_difference) != 0)
|
||||
)
|
||||
|
||||
data = query.run(as_dict=True)
|
||||
stock_entries = {}
|
||||
|
||||
for row in data:
|
||||
if abs(row.stock_value) > 0.1 and row.name not in stock_entries:
|
||||
stock_entries.setdefault(row.name, row)
|
||||
|
||||
return stock_entries
|
||||
|
@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe.permissions import add_user_permission, remove_user_permission
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, now, nowdate, nowtime, today
|
||||
from frappe.utils import add_days, flt, nowdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.stock.doctype.item.test_item import (
|
||||
@ -17,8 +17,6 @@ from erpnext.stock.doctype.item.test_item import (
|
||||
from erpnext.stock.doctype.serial_no.serial_no import * # noqa
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import (
|
||||
FinishedGoodError,
|
||||
audit_incorrect_valuation_entries,
|
||||
get_incorrect_stock_entries,
|
||||
move_sample_to_retention_warehouse,
|
||||
)
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
@ -1616,44 +1614,6 @@ class TestStockEntry(FrappeTestCase):
|
||||
|
||||
self.assertRaises(BatchExpiredError, se.save)
|
||||
|
||||
def test_audit_incorrect_stock_entries(self):
|
||||
item_code = "Test Incorrect Valuation Rate Item - 001"
|
||||
create_item(item_code=item_code, is_stock_item=1)
|
||||
|
||||
make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Receipt",
|
||||
posting_date=add_days(nowdate(), -10),
|
||||
qty=2,
|
||||
rate=500,
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
transfer_entry = make_stock_entry(
|
||||
item_code=item_code,
|
||||
purpose="Material Transfer",
|
||||
qty=2,
|
||||
rate=500,
|
||||
from_warehouse="_Test Warehouse - _TC",
|
||||
to_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
sle_name = frappe.db.get_value(
|
||||
"Stock Ledger Entry", {"voucher_no": transfer_entry.name, "actual_qty": (">", 0)}, "name"
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Stock Ledger Entry", sle_name, {"modified": add_days(now(), -1), "stock_value_difference": 10}
|
||||
)
|
||||
|
||||
stock_entries = get_incorrect_stock_entries()
|
||||
self.assertTrue(transfer_entry.name in stock_entries)
|
||||
|
||||
audit_incorrect_valuation_entries()
|
||||
|
||||
stock_entries = get_incorrect_stock_entries()
|
||||
self.assertFalse(transfer_entry.name in stock_entries)
|
||||
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -102,9 +102,11 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
|
||||
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)
|
||||
|
||||
@ -1060,7 +1062,9 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa
|
||||
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 +1175,31 @@ def get_projected_qty(item_code, warehouse):
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
|
@ -55,6 +55,7 @@
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
frappe.boot = {{ boot }}
|
||||
frappe.ready(() => {
|
||||
$(document).on('click', '.address-card', (e) => {
|
||||
const $target = $(e.currentTarget);
|
||||
|
0
erpnext/www/book-appointment/__init__.py
Normal file
0
erpnext/www/book-appointment/__init__.py
Normal file
0
erpnext/www/book-appointment/verify/__init__.py
Normal file
0
erpnext/www/book-appointment/verify/__init__.py
Normal file
Loading…
x
Reference in New Issue
Block a user