Merge pull request #36861 from frappe/version-14-hotfix

chore: release v14
This commit is contained in:
Deepesh Garg
2023-08-30 19:24:48 +05:30
committed by GitHub
27 changed files with 972 additions and 268 deletions

View File

@@ -3,6 +3,296 @@
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, today
class TestExchangeRateRevaluation(unittest.TestCase):
pass
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.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.stock.doctype.item.test_item import create_item
class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
def setUp(self):
self.create_company()
self.create_usd_receivable_account()
self.create_item()
self.create_customer()
self.clear_old_entries()
self.set_system_and_company_settings()
def tearDown(self):
frappe.db.rollback()
def set_system_and_company_settings(self):
# set number and currency precision
system_settings = frappe.get_doc("System Settings")
system_settings.float_precision = 2
system_settings.currency_precision = 2
system_settings.save()
# Using Exchange Gain/Loss account for unrealized as well.
company_doc = frappe.get_doc("Company", self.company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_01_revaluation_of_forex_balance(self):
"""
Test Forex account balance and Journal creation post Revaluation
"""
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = (self.company,)
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
row = err.accounts[0]
row.new_exchange_rate = 85
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()
# Create JV for ERR
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
je = je.submit()
je.reload()
self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
self.assertEqual(je.total_debit, 8500.0)
self.assertEqual(je.total_credit, 8500.0)
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=["sum(debit)-sum(credit) as balance"],
)[0]
self.assertEqual(acc_balance.balance, 8500.0)
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_02_accounts_only_with_base_currency_balance(self):
"""
Test Revaluation on Forex account with balance only in base currency
"""
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
pe = get_payment_entry(si.doctype, si.name)
pe.source_exchange_rate = 85
pe.received_amount = 8500
pe.save().submit()
# Cancel the auto created gain/loss JE to simulate balance only in base currency
je = frappe.db.get_all(
"Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
)[0]
frappe.get_doc("Journal Entry", je).cancel()
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = (self.company,)
err.posting_date = today()
err.fetch_and_calculate_accounts_data()
err = err.save().submit()
# Create JV for ERR
self.assertTrue(err.check_journal_entry_condition())
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
je.reload()
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
self.assertEqual(len(je.accounts), 2)
# Only base currency fields will be posted to
for acc in je.accounts:
self.assertEqual(acc.debit_in_account_currency, 0)
self.assertEqual(acc.credit_in_account_currency, 0)
self.assertEqual(je.total_debit, 500.0)
self.assertEqual(je.total_credit, 500.0)
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
],
)[0]
# account shouldn't have balance in base and account currency
self.assertEqual(acc_balance.balance, 0.0)
self.assertEqual(acc_balance.balance_in_account_currency, 0.0)
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_03_accounts_only_with_account_currency_balance(self):
"""
Test Revaluation on Forex account with balance only in account currency
"""
precision = frappe.db.get_single_value("System Settings", "currency_precision")
# posting on previous date to make sure that ERR picks up the Payment entry's exchange
# rate while calculating gain/loss for account currency balance
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=add_days(today(), -1),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = 95
pe.source_exchange_rate = 84.211
pe.received_amount = 8000
pe.references = []
pe.save().submit()
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
],
)[0]
# account should have balance only in account currency
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = (self.company,)
err.posting_date = today()
err.fetch_and_calculate_accounts_data()
err.set_total_gain_loss()
err = err.save().submit()
# Create JV for ERR
self.assertTrue(err.check_journal_entry_condition())
err_journals = err.make_jv_entries()
je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
je = je.submit()
je.reload()
self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
self.assertEqual(len(je.accounts), 2)
# Only account currency fields will be posted to
for acc in je.accounts:
self.assertEqual(flt(acc.debit, precision), 0.0)
self.assertEqual(flt(acc.credit, precision), 0.0)
row = [x for x in je.accounts if x.account == self.debtors_usd][0]
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
row = [x for x in je.accounts if x.account != self.debtors_usd][0]
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
self.assertEqual(flt(je.total_debit, precision), 0.0)
self.assertEqual(flt(je.total_credit, precision), 0.0)
acc_balance = frappe.db.get_all(
"GL Entry",
filters={"account": self.debtors_usd, "is_cancelled": 0},
fields=[
"sum(debit)-sum(credit) as balance",
"sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
],
)[0]
# account shouldn't have balance in base and account currency post revaluation
self.assertEqual(flt(acc_balance.balance, precision), 0.0)
self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0)
@change_settings(
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_04_get_account_details_function(self):
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debtors_usd,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_submit=1,
)
si.currency = "USD"
si.conversion_rate = 80
si.save().submit()
from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import (
get_account_details,
)
account_details = get_account_details(
self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05
)
# not checking for new exchange rate and balances as it is dependent on live exchange rates
expected_data = {
"account_currency": "USD",
"balance_in_base_currency": 8000.0,
"balance_in_account_currency": 100.0,
"current_exchange_rate": 80.0,
"zero_balance": False,
"new_balance_in_account_currency": 100.0,
}
for key, val in expected_data.items():
self.assertEqual(expected_data.get(key), account_details.get(key))

View File

@@ -1867,10 +1867,15 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
if not total_amount:
if party_account_currency == company_currency:
# for handling cases that don't have multi-currency (base field)
total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
total_amount = (
ref_doc.get("base_rounded_total")
or ref_doc.get("rounded_total")
or ref_doc.get("base_grand_total")
or ref_doc.get("grand_total")
)
exchange_rate = 1
else:
total_amount = ref_doc.get("grand_total")
total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total")
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc.

View File

@@ -1201,6 +1201,24 @@ class TestPaymentEntry(FrappeTestCase):
template.allocate_payment_based_on_payment_terms = 1
template.save()
def test_allocation_validation_for_sales_order(self):
so = make_sales_order(do_not_save=True)
so.items[0].rate = 99.55
so.save().submit()
self.assertGreater(so.rounded_total, 0.0)
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_from = "Debtors - _TC"
pe.paid_amount = 45.55
pe.references[0].allocated_amount = 45.55
pe.save().submit()
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
pe.paid_from = "Debtors - _TC"
# No validation error should be thrown here.
pe.save().submit()
so.reload()
self.assertEqual(so.advance_paid, so.rounded_total)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@@ -151,6 +151,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.refresh();
}
invoice_name() {
this.frm.trigger("get_unreconciled_entries");
}
payment_name() {
this.frm.trigger("get_unreconciled_entries");
}
clear_child_tables() {
this.frm.clear_table("invoices");
this.frm.clear_table("payments");

View File

@@ -26,8 +26,10 @@
"bank_cash_account",
"cost_center",
"sec_break1",
"invoice_name",
"invoices",
"column_break_15",
"payment_name",
"payments",
"sec_break2",
"allocation"
@@ -136,6 +138,7 @@
"label": "Minimum Invoice Amount"
},
{
"default": "50",
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "invoice_limit",
"fieldtype": "Int",
@@ -166,6 +169,7 @@
"label": "Maximum Payment Amount"
},
{
"default": "50",
"description": "System will fetch all the entries if limit value is zero.",
"fieldname": "payment_limit",
"fieldtype": "Int",
@@ -185,13 +189,23 @@
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "invoice_name",
"fieldtype": "Data",
"label": "Filter on Invoice"
},
{
"fieldname": "payment_name",
"fieldtype": "Data",
"label": "Filter on Payment"
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"links": [],
"modified": "2022-04-29 15:37:10.246831",
"modified": "2023-08-15 05:35:50.109290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
@@ -218,4 +232,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -5,6 +5,7 @@
import frappe
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
@@ -58,6 +59,9 @@ class PaymentReconciliation(Document):
def get_payment_entries(self):
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True)
if self.payment_name:
condition += "name like '%%{0}%%'".format(self.payment_name)
payment_entries = get_advance_payment_entries(
self.party_type,
self.party,
@@ -73,6 +77,9 @@ class PaymentReconciliation(Document):
def get_jv_entries(self):
condition = self.get_conditions()
if self.payment_name:
condition += f" and t1.name like '%%{self.payment_name}%%'"
if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' "
@@ -130,6 +137,15 @@ class PaymentReconciliation(Document):
def get_return_invoices(self):
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doc = qb.DocType(voucher_type)
conditions = []
conditions.append(doc.docstatus == 1)
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
conditions.append(doc.is_return == 1)
if self.payment_name:
conditions.append(doc.name.like(f"%{self.payment_name}%"))
self.return_invoices = (
qb.from_(doc)
.select(
@@ -137,11 +153,7 @@ class PaymentReconciliation(Document):
doc.name.as_("voucher_no"),
doc.return_against,
)
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
)
.where(Criterion.all(conditions))
.run(as_dict=True)
)
@@ -210,6 +222,8 @@ class PaymentReconciliation(Document):
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,
limit=self.invoice_limit,
voucher_no=self.invoice_name,
)
cr_dr_notes = (

View File

@@ -768,21 +768,22 @@ class PurchaseInvoice(BuyingController):
# Amount added through landed-cost-voucher
if landed_cost_entries:
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": item.expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]),
"credit_in_account_currency": flt(amount["amount"]),
"project": item.project or self.project,
},
item=item,
if (item.item_code, item.name) in landed_cost_entries:
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
gl_entries.append(
self.get_gl_dict(
{
"account": account,
"against": item.expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(amount["base_amount"]),
"credit_in_account_currency": flt(amount["amount"]),
"project": item.project or self.project,
},
item=item,
)
)
)
# sub-contracting warehouse
if flt(item.rm_supp_cost):

View File

@@ -268,9 +268,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
)
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
tax_amount = net_total * tax_details.rate / 100
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
tax_amount = net_total * tax_details.rate / 100
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}

View File

@@ -8,20 +8,17 @@ 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
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestAccountsReceivable(FrappeTestCase):
class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def setUp(self):
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
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()
self.create_company()
self.create_customer()
self.create_item()
self.create_usd_receivable_account()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
@@ -49,29 +46,84 @@ class TestAccountsReceivable(FrappeTestCase):
debtors_usd.account_type = debtors.account_type
self.debtors_usd = debtors_usd.save().name
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
si = create_sales_invoice(
item=self.item,
company=self.company,
customer=self.customer,
debit_to=self.debit_to,
posting_date=today(),
parent_cost_center=self.cost_center,
cost_center=self.cost_center,
rate=100,
price_list_rate=100,
do_not_save=1,
)
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 = si.save()
if not do_not_submit:
si = si.submit()
return si
def create_payment_entry(self, docname):
pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
pe.paid_from = self.debit_to
pe.insert()
pe.submit()
def create_credit_note(self, docname):
credit_note = create_sales_invoice(
company=self.company,
customer=self.customer,
item=self.item,
qty=-1,
debit_to=self.debit_to,
cost_center=self.cost_center,
is_return=1,
return_against=docname,
)
return credit_note
def test_accounts_receivable(self):
filters = {
"company": "_Test Company 2",
"company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
name = make_sales_invoice().name
si = self.create_sales_invoice()
name = si.name
report = execute(filters)
expected_data = [[100, 30], [100, 50], [100, 20]]
expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
for i in range(3):
row = report[1][i - 1]
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after payment
make_payment(name)
self.create_payment_entry(si.name)
report = execute(filters)
expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]
@@ -84,10 +136,10 @@ class TestAccountsReceivable(FrappeTestCase):
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
make_credit_note(name)
self.create_credit_note(si.name)
report = execute(filters)
expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"]
expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
row = report[1][0]
self.assertEqual(
@@ -108,21 +160,20 @@ class TestAccountsReceivable(FrappeTestCase):
"""
so = make_sales_order(
company="_Test Company 2",
customer="_Test Customer 2",
warehouse="Finished Goods - _TC2",
currency="EUR",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
company=self.company,
customer=self.customer,
warehouse=self.warehouse,
debit_to=self.debit_to,
income_account=self.income_account,
expense_account=self.expense_account,
cost_center=self.cost_center,
)
pe = get_payment_entry(so.doctype, so.name)
pe = pe.save().submit()
filters = {
"company": "_Test Company 2",
"company": self.company,
"based_on_payment_terms": 0,
"report_date": today(),
"range1": 30,
@@ -147,34 +198,32 @@ class TestAccountsReceivable(FrappeTestCase):
)
@change_settings(
"Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
"Accounts Settings",
{"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_exchange_revaluation_for_party(self):
"""
Exchange Revaluation for party on Receivable/Payable shoule be included
Exchange Revaluation for party on Receivable/Payable should be included
"""
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 = frappe.get_doc("Company", self.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 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
si.conversion_rate = 0.90
si.conversion_rate = 80
si.debit_to = self.debtors_usd
si = si.save().submit()
# Exchange Revaluation
err = frappe.new_doc("Exchange Rate Revaluation")
err.company = company
err.company = self.company
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
err.accounts[0].new_exchange_rate = 0.95
err.accounts[0].new_exchange_rate = 85
row = err.accounts[0]
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
@@ -189,7 +238,7 @@ class TestAccountsReceivable(FrappeTestCase):
je = je.submit()
filters = {
"company": company,
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
@@ -198,7 +247,7 @@ class TestAccountsReceivable(FrappeTestCase):
}
report = execute(filters)
expected_data_for_err = [0, -5, 0, 5]
expected_data_for_err = [0, -500, 0, 500]
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,
@@ -214,46 +263,43 @@ class TestAccountsReceivable(FrappeTestCase):
"""
Payment against credit/debit note should be considered against the parent invoice
"""
company = "_Test Company 2"
customer = "_Test Customer 2"
si1 = make_sales_invoice()
si1 = self.create_sales_invoice()
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
pe.paid_from = "Debtors - _TC2"
pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash)
pe.paid_from = self.debit_to
pe.insert()
pe.submit()
cr_note = make_credit_note(si1.name)
cr_note = self.create_credit_note(si1.name)
si2 = make_sales_invoice()
si2 = self.create_sales_invoice()
# manually link cr_note with si2 using journal entry
je = frappe.new_doc("Journal Entry")
je.company = company
je.company = self.company
je.voucher_type = "Credit Note"
je.posting_date = today()
debit_account = "Debtors - _TC2"
debit_entry = {
"account": debit_account,
"account": self.debit_to,
"party_type": "Customer",
"party": customer,
"party": self.customer,
"debit": 100,
"debit_in_account_currency": 100,
"reference_type": cr_note.doctype,
"reference_name": cr_note.name,
"cost_center": "Main - _TC2",
"cost_center": self.cost_center,
}
credit_entry = {
"account": debit_account,
"account": self.debit_to,
"party_type": "Customer",
"party": customer,
"party": self.customer,
"credit": 100,
"credit_in_account_currency": 100,
"reference_type": si2.doctype,
"reference_name": si2.name,
"cost_center": "Main - _TC2",
"cost_center": self.cost_center,
}
je.append("accounts", debit_entry)
@@ -261,7 +307,7 @@ class TestAccountsReceivable(FrappeTestCase):
je = je.save().submit()
filters = {
"company": company,
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
@@ -271,64 +317,254 @@ class TestAccountsReceivable(FrappeTestCase):
report = execute(filters)
self.assertEqual(report[1], [])
def test_group_by_party(self):
si1 = self.create_sales_invoice(do_not_submit=True)
si1.posting_date = add_days(today(), -1)
si1.save().submit()
si2 = self.create_sales_invoice(do_not_submit=True)
si2.items[0].rate = 85
si2.save().submit()
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator")
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"group_by_party": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 5)
si = create_sales_invoice(
company="_Test Company 2",
customer="_Test Customer 2",
currency="EUR",
warehouse="Finished Goods - _TC2",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
do_not_save=1,
)
# assert voucher rows
expected_voucher_rows = [
[100.0, 100.0, 100.0, 100.0],
[85.0, 85.0, 85.0, 85.0],
]
voucher_rows = []
for x in report[0:2]:
voucher_rows.append(
[x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency]
)
self.assertEqual(expected_voucher_rows, voucher_rows)
if not no_payment_schedule:
si.append(
"payment_schedule",
dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
# assert total rows
expected_total_rows = [
[self.customer, 185.0, 185.0], # party total
{}, # empty row for padding
["Total", 185.0, 185.0], # grand total
]
party_total_row = report[2]
self.assertEqual(
expected_total_rows[0],
[
party_total_row.get("party"),
party_total_row.get("invoiced"),
party_total_row.get("outstanding"),
],
)
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),
empty_row = report[3]
self.assertEqual(expected_total_rows[1], empty_row)
grand_total_row = report[4]
self.assertEqual(
expected_total_rows[2],
[
grand_total_row.get("party"),
grand_total_row.get("invoiced"),
grand_total_row.get("outstanding"),
],
)
si = si.save()
def test_future_payments(self):
si = self.create_sales_invoice()
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.paid_amount = 90.0
pe.references[0].allocated_amount = 90.0
pe.save().submit()
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"show_future_payments": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
if not do_not_submit:
si = si.submit()
expected_data = [100.0, 100.0, 10.0, 90.0]
return si
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
pe.cancel()
# full payment in future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.save().submit()
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, 0.0, 100.0]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
)
def make_payment(docname):
pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40)
pe.paid_from = "Debtors - _TC2"
pe.insert()
pe.submit()
pe.cancel()
# over payment in future date
pe = get_payment_entry(si.doctype, si.name)
pe.posting_date = add_days(today(), 1)
pe.paid_amount = 110
pe.save().submit()
report = execute(filters)[1]
self.assertEqual(len(report), 2)
expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]]
for idx, row in enumerate(report):
self.assertEqual(
expected_data[idx],
[row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
)
def test_sales_person(self):
sales_person = (
frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True})
.insert()
.submit()
)
si = self.create_sales_invoice(do_not_submit=True)
si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
si.save().submit()
def make_credit_note(docname):
credit_note = create_sales_invoice(
company="_Test Company 2",
customer="_Test Customer 2",
currency="EUR",
qty=-1,
warehouse="Finished Goods - _TC2",
debit_to="Debtors - _TC2",
income_account="Sales - _TC2",
expense_account="Cost of Goods Sold - _TC2",
cost_center="Main - _TC2",
is_return=1,
return_against=docname,
)
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"sales_person": sales_person.name,
"show_sales_person": True,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
return credit_note
expected_data = [100.0, 100.0, sales_person.name]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person])
def test_cost_center_filter(self):
si = self.create_sales_invoice()
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"cost_center": self.cost_center,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, self.cost_center]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center])
def test_customer_group_filter(self):
si = self.create_sales_invoice()
cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"customer_group": cus_group,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, cus_group]
row = report[0]
self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group])
filters.update({"customer_group": "Individual"})
report = execute(filters)[1]
self.assertEqual(len(report), 0)
def test_party_account_filter(self):
si1 = self.create_sales_invoice()
self.customer2 = (
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "Jane Doe",
"type": "Individual",
"default_currency": "USD",
}
)
.insert()
.submit()
)
si2 = self.create_sales_invoice(do_not_submit=True)
si2.posting_date = add_days(today(), -1)
si2.customer = self.customer2
si2.currency = "USD"
si2.conversion_rate = 80
si2.debit_to = self.debtors_usd
si2.save().submit()
# Filter on company currency receivable account
filters = {
"company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
"party_account": self.debit_to,
}
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [100.0, 100.0, self.debit_to, si1.currency]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
)
# Filter on USD receivable account
filters.update({"party_account": self.debtors_usd})
report = execute(filters)[1]
self.assertEqual(len(report), 1)
expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
row = report[0]
self.assertEqual(
expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
)
# without filter on party account
filters.pop("party_account")
report = execute(filters)[1]
self.assertEqual(len(report), 2)
expected_data = [
[8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency],
[100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency],
]
for idx, row in enumerate(report):
self.assertEqual(
expected_data[idx],
[
row.invoiced,
row.outstanding,
row.invoiced_in_account_currency,
row.outstanding_in_account_currency,
row.party_account,
row.account_currency,
],
)

View File

@@ -58,6 +58,9 @@ def get_data(filters):
def get_asset_categories(filters):
condition = ""
if filters.get("asset_category"):
condition += " and asset_category = %(asset_category)s"
return frappe.db.sql(
"""
SELECT asset_category,
@@ -98,15 +101,25 @@ def get_asset_categories(filters):
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
group by asset_category
""",
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
""".format(
condition
),
{
"to_date": filters.to_date,
"from_date": filters.from_date,
"company": filters.company,
"asset_category": filters.get("asset_category"),
},
as_dict=1,
)
def get_assets(filters):
condition = ""
if filters.get("asset_category"):
condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
return frappe.db.sql(
"""
SELECT results.asset_category,
@@ -138,7 +151,7 @@ def get_assets(filters):
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
group by a.asset_category
union
SELECT a.asset_category,
@@ -154,10 +167,12 @@ def get_assets(filters):
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
group by a.asset_category) as results
group by results.asset_category
""",
""".format(
condition
),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
as_dict=1,
)

View File

@@ -257,7 +257,7 @@ def get_tds_docs(filters):
}
party = frappe.get_all(filters.get("party_type"), pluck="name")
query_filters.update({"against": ("in", party)})
or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"})
if filters.get("party"):
del query_filters["account"]
@@ -294,7 +294,7 @@ def get_tds_docs(filters):
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
get_doc_info(journal_entries, "Journal Entry", tax_category_map)
get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
return (
tds_documents,
@@ -309,7 +309,11 @@ def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
{
"parent": ("in", journal_entries),
"party_type": ("in", ("Supplier", "Customer")),
"party": ("is", "set"),
},
["parent", "party"],
):
if d.parent not in journal_entry_party_map:
@@ -320,41 +324,29 @@ def get_journal_entry_party_map(journal_entries):
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
if doctype == "Purchase Invoice":
fields = [
"name",
"tax_withholding_category",
"base_tax_withholding_net_total",
"grand_total",
"base_total",
]
elif doctype == "Sales Invoice":
fields = ["name", "base_net_total", "grand_total", "base_total"]
elif doctype == "Payment Entry":
fields = [
"name",
"tax_withholding_category",
"paid_amount",
"paid_amount_after_tax",
"base_paid_amount",
]
else:
fields = ["name", "tax_withholding_category"]
common_fields = ["name", "tax_withholding_category"]
fields_dict = {
"Purchase Invoice": ["base_tax_withholding_net_total", "grand_total", "base_total"],
"Sales Invoice": ["base_net_total", "grand_total", "base_total"],
"Payment Entry": ["paid_amount", "paid_amount_after_tax", "base_paid_amount"],
"Journal Entry": ["total_amount"],
}
entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields)
entries = frappe.get_all(
doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
)
for entry in entries:
tax_category_map.update({entry.name: entry.tax_withholding_category})
if doctype == "Purchase Invoice":
net_total_map.update(
{entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]}
)
value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]
elif doctype == "Sales Invoice":
net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]})
value = [entry.base_net_total, entry.grand_total, entry.base_total]
elif doctype == "Payment Entry":
net_total_map.update(
{entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]}
)
value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
else:
value = [entry.total_amount] * 3
net_total_map.update({entry.name: value})
def get_tax_rate_map(filters):

View File

@@ -60,7 +60,6 @@ class AccountsTestMixin:
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
self.debit_usd = "Debtors USD - " + abbr
self.cash = "Cash - " + abbr
self.creditors = "Creditors - " + abbr
self.retained_earnings = "Retained Earnings - " + abbr
@@ -105,6 +104,28 @@ class AccountsTestMixin:
new_acc.save()
setattr(self, acc.attribute_name, new_acc.name)
def create_usd_receivable_account(self):
account_name = "Debtors USD"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Receivable - " + self.company_abbr
acc.company = self.company
acc.account_currency = "USD"
acc.account_type = "Receivable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.debtors_usd = acc.name
def clear_old_entries(self):
doctype_list = [
"GL Entry",
@@ -113,6 +134,8 @@ class AccountsTestMixin:
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
"Sales Order",
"Exchange Rate Revaluation",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()

View File

@@ -884,7 +884,9 @@ def get_outstanding_invoices(
min_outstanding=None,
max_outstanding=None,
accounting_dimensions=None,
vouchers=None,
vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering
limit=None, # passed by reconciliation tool
voucher_no=None, # filter passed by reconciliation tool
):
ple = qb.DocType("Payment Ledger Entry")
@@ -917,6 +919,8 @@ def get_outstanding_invoices(
max_outstanding=max_outstanding,
get_invoices=True,
accounting_dimensions=accounting_dimensions or [],
limit=limit,
voucher_no=voucher_no,
)
for d in invoice_list:
@@ -1648,12 +1652,13 @@ class QueryPaymentLedger(object):
self.voucher_posting_date = []
self.min_outstanding = None
self.max_outstanding = None
self.limit = self.voucher_no = None
def reset(self):
# clear filters
self.vouchers.clear()
self.common_filter.clear()
self.min_outstanding = self.max_outstanding = None
self.min_outstanding = self.max_outstanding = self.limit = None
# clear result
self.voucher_outstandings.clear()
@@ -1667,6 +1672,7 @@ class QueryPaymentLedger(object):
filter_on_voucher_no = []
filter_on_against_voucher_no = []
if self.vouchers:
voucher_types = set([x.voucher_type for x in self.vouchers])
voucher_nos = set([x.voucher_no for x in self.vouchers])
@@ -1677,6 +1683,10 @@ class QueryPaymentLedger(object):
filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
if self.voucher_no:
filter_on_voucher_no.append(ple.voucher_no.like(f"%{self.voucher_no}%"))
filter_on_against_voucher_no.append(ple.against_voucher_no.like(f"%{self.voucher_no}%"))
# build outstanding amount filter
filter_on_outstanding_amount = []
if self.min_outstanding:
@@ -1792,6 +1802,11 @@ class QueryPaymentLedger(object):
)
)
if self.limit:
self.cte_query_voucher_amount_and_outstanding = (
self.cte_query_voucher_amount_and_outstanding.limit(self.limit)
)
# execute SQL
self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
@@ -1805,6 +1820,8 @@ class QueryPaymentLedger(object):
get_payments=False,
get_invoices=False,
accounting_dimensions=None,
limit=None,
voucher_no=None,
):
"""
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
@@ -1826,6 +1843,8 @@ class QueryPaymentLedger(object):
self.max_outstanding = max_outstanding
self.get_payments = get_payments
self.get_invoices = get_invoices
self.limit = limit
self.voucher_no = voucher_no
self.query_for_outstanding()
return self.voucher_outstandings

View File

@@ -156,6 +156,8 @@ def get_data(filters):
def prepare_chart_data(data, filters):
if not data:
return
labels_values_map = {}
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
filters_filter_based_on = "Date Range"

View File

@@ -200,9 +200,9 @@ class AccountsController(TransactionBase):
# apply tax withholding only if checked and applicable
self.set_tax_withholding()
validate_regional(self)
validate_einvoice_fields(self)
with temporary_flag("company", self.company):
validate_regional(self)
validate_einvoice_fields(self)
if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self)

View File

@@ -345,6 +345,8 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
elif doctype == "Purchase Invoice":
# look for Print Heading "Debit Note"
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
if source.tax_withholding_category:
doc.set_onload("supplier_tds", source.tax_withholding_category)
for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual":

View File

@@ -388,7 +388,7 @@ class SellingController(StockController):
for d in self.get("items"):
if d.get(ref_fieldname):
status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status")
if status in ("Closed", "On Hold"):
if status in ("Closed", "On Hold") and not self.is_return:
frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status))
def update_reserved_qty(self):
@@ -404,7 +404,9 @@ class SellingController(StockController):
if so and so_item_rows:
sales_order = frappe.get_doc("Sales Order", so)
if sales_order.status in ["Closed", "Cancelled"]:
if (sales_order.status == "Closed" and not self.is_return) or sales_order.status in [
"Cancelled"
]:
frappe.throw(
_("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError
)

View File

@@ -53,7 +53,7 @@ class ProductionPlan(Document):
data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders})
title = _("Production Plan Already Submitted")
if not data:
if not data and sales_orders:
msg = _("No items are available in the sales order {0} for production").format(sales_orders[0])
if len(sales_orders) > 1:
sales_orders = ", ".join(sales_orders)

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
import erpnext
@@ -19,19 +20,7 @@ class LandedCostVoucher(Document):
self.set("items", [])
for pr in self.get("purchase_receipts"):
if pr.receipt_document_type and pr.receipt_document:
pr_items = frappe.db.sql(
"""select pr_item.item_code, pr_item.description,
pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name,
pr_item.cost_center, pr_item.is_fixed_asset
from `tab{doctype} Item` pr_item where parent = %s
and exists(select name from tabItem
where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1))
""".format(
doctype=pr.receipt_document_type
),
pr.receipt_document,
as_dict=True,
)
pr_items = get_pr_items(pr)
for d in pr_items:
item = self.append("items")
@@ -247,3 +236,30 @@ class LandedCostVoucher(Document):
),
tuple([item.valuation_rate] + serial_nos),
)
def get_pr_items(purchase_receipt):
item = frappe.qb.DocType("Item")
pr_item = frappe.qb.DocType(purchase_receipt.receipt_document_type + " Item")
return (
frappe.qb.from_(pr_item)
.inner_join(item)
.on(item.name == pr_item.item_code)
.select(
pr_item.item_code,
pr_item.description,
pr_item.qty,
pr_item.base_rate,
pr_item.base_amount,
pr_item.name,
pr_item.cost_center,
pr_item.is_fixed_asset,
ConstantColumn(purchase_receipt.receipt_document_type).as_("receipt_document_type"),
ConstantColumn(purchase_receipt.receipt_document).as_("receipt_document"),
)
.where(
(pr_item.parent == purchase_receipt.receipt_document)
& ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
)
.run(as_dict=True)
)

View File

@@ -6,6 +6,8 @@ def get_data():
"fieldname": "material_request",
"internal_links": {
"Sales Order": ["items", "sales_order"],
"Project": ["items", "project"],
"Cost Center": ["items", "cost_center"],
},
"transactions": [
{
@@ -15,5 +17,6 @@ def get_data():
{"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]},
{"label": _("Manufacturing"), "items": ["Work Order"]},
{"label": _("Internal Transfer"), "items": ["Sales Order"]},
{"label": _("Accounting Dimensions"), "items": ["Project", "Cost Center"]},
],
}

View File

@@ -472,27 +472,28 @@ class PurchaseReceipt(BuyingController):
# Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in landed_cost_entries[(d.item_code, d.name)].items():
account_currency = get_account_currency(account)
credit_amount = (
flt(amount["base_amount"])
if (amount["base_amount"] or account_currency != self.company_currency)
else flt(amount["amount"])
)
if (d.item_code, d.name) in landed_cost_entries:
for account, amount in landed_cost_entries[(d.item_code, d.name)].items():
account_currency = get_account_currency(account)
credit_amount = (
flt(amount["base_amount"])
if (amount["base_amount"] or account_currency != self.company_currency)
else flt(amount["amount"])
)
self.add_gl_entry(
gl_entries=gl_entries,
account=account,
cost_center=d.cost_center,
debit=0.0,
credit=credit_amount,
remarks=remarks,
against_account=warehouse_account_name,
credit_in_account_currency=flt(amount["amount"]),
account_currency=account_currency,
project=d.project,
item=d,
)
self.add_gl_entry(
gl_entries=gl_entries,
account=account,
cost_center=d.cost_center,
debit=0.0,
credit=credit_amount,
remarks=remarks,
against_account=warehouse_account_name,
credit_in_account_currency=flt(amount["amount"]),
account_currency=account_currency,
project=d.project,
item=d,
)
if d.rate_difference_with_purchase_invoice and stock_rbnb:
account_currency = get_account_currency(stock_rbnb)

View File

@@ -71,6 +71,14 @@ frappe.query_reports["Stock Balance"] = {
"width": "80",
"options": "Warehouse Type"
},
{
"fieldname": "valuation_field_type",
"label": __("Valuation Field Type"),
"fieldtype": "Select",
"width": "80",
"options": "Currency\nFloat",
"default": "Currency"
},
{
"fieldname":"include_uom",
"label": __("Include UOM"),

View File

@@ -430,9 +430,12 @@ class StockBalanceReport(object):
{
"label": _("Valuation Rate"),
"fieldname": "val_rate",
"fieldtype": "Float",
"fieldtype": self.filters.valuation_field_type or "Currency",
"width": 90,
"convertible": "rate",
"options": "Company:company:default_currency"
if self.filters.valuation_field_type == "Currency"
else None,
},
{
"label": _("Company"),

View File

@@ -82,7 +82,15 @@ frappe.query_reports["Stock Ledger"] = {
"label": __("Include UOM"),
"fieldtype": "Link",
"options": "UOM"
}
},
{
"fieldname": "valuation_field_type",
"label": __("Valuation Field Type"),
"fieldtype": "Select",
"width": "80",
"options": "Currency\nFloat",
"default": "Currency"
},
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);

View File

@@ -196,17 +196,21 @@ def get_columns(filters):
{
"label": _("Avg Rate (Balance Stock)"),
"fieldname": "valuation_rate",
"fieldtype": "Float",
"fieldtype": filters.valuation_field_type,
"width": 180,
"options": "Company:company:default_currency",
"options": "Company:company:default_currency"
if filters.valuation_field_type == "Currency"
else None,
"convertible": "rate",
},
{
"label": _("Valuation Rate"),
"fieldname": "in_out_rate",
"fieldtype": "Float",
"fieldtype": filters.valuation_field_type,
"width": 140,
"options": "Company:company:default_currency",
"options": "Company:company:default_currency"
if filters.valuation_field_type == "Currency"
else None,
"convertible": "rate",
},
{

View File

@@ -268,17 +268,24 @@ class SubcontractingReceipt(SubcontractingController):
status = "Draft"
elif self.docstatus == 1:
status = "Completed"
if self.is_return:
status = "Return"
return_against = frappe.get_doc("Subcontracting Receipt", self.return_against)
return_against.run_method("update_status")
elif self.per_returned == 100:
status = "Return Issued"
elif self.docstatus == 2:
status = "Cancelled"
if self.is_return:
frappe.get_doc("Subcontracting Receipt", self.return_against).update_status(
update_modified=update_modified
)
if status:
frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified)
frappe.db.set_value(
"Subcontracting Receipt", self.name, "status", status, update_modified=update_modified
)
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map

View File

@@ -3536,7 +3536,7 @@ Quality Feedback Template,Modèle de commentaires sur la qualité,
Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels.,
Shift,Décalage,
Show {0},Montrer {0},
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf &quot;-&quot;, &quot;#&quot;, &quot;.&quot;, &quot;/&quot;, &quot;{{&quot; Et &quot;}}&quot; non autorisés dans les séries de nommage {0}",
"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf &quot;-&quot;, &quot;#&quot;, &quot;.&quot;, &quot;/&quot;, &quot;{{&quot; Et &quot;}}&quot; non autorisés dans les masques de numérotation {0}",
Target Details,Détails de la cible,
{0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}.,
API,API,
@@ -3551,7 +3551,7 @@ Importing {0} of {1},Importer {0} de {1},
Invalid URL,URL invalide,
Landscape,Paysage,
Last Sync On,Dernière synchronisation le,
Naming Series,Nom de série,
Naming Series,Masque de numérotation,
No data to export,Aucune donnée à exporter,
Portrait,Portrait,
Print Heading,Imprimer Titre,
@@ -4282,7 +4282,7 @@ Please set {0},Veuillez définir {0},supplier
Draft,Brouillon,"docstatus,=,0"
Cancelled,Annulé,"docstatus,=,2"
Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation&gt; Paramètres de l'éducation,
Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir la série de noms pour {0} via Configuration&gt; Paramètres&gt; Série de noms,
Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir le masque de numérotation pour {0} via Configuration&gt; Paramètres&gt; Série de noms,
UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -&gt; {1}) introuvable pour l'article: {2},
Item Code > Item Group > Brand,Code article&gt; Groupe d'articles&gt; Marque,
Customer > Customer Group > Territory,Client&gt; Groupe de clients&gt; Territoire,
@@ -4297,7 +4297,7 @@ Fetch Serial Numbers based on FIFO,Récupérer les numéros de série basés sur
Current Odometer Value should be greater than Last Odometer Value {0},La valeur actuelle de l'odomètre doit être supérieure à la dernière valeur de l'odomètre {0},
No additional expenses has been added,Aucune dépense supplémentaire n'a été ajoutée,
Asset{} {assets_link} created for {},Élément {} {assets_link} créé pour {},
Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: la série de noms d'éléments est obligatoire pour la création automatique de l'élément {},
Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: Le masque de numérotation d'éléments est obligatoire pour la création automatique de l'élément {},
Assets not created for {0}. You will have to create asset manually.,Éléments non créés pour {0}. Vous devrez créer un actif manuellement.,
{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} a des écritures comptables dans la devise {2} pour l'entreprise {3}. Veuillez sélectionner un compte à recevoir ou à payer avec la devise {2}.,
Invalid Account,Compte invalide,
@@ -4321,7 +4321,7 @@ Advanced Settings,Réglages avancés,
Path,Chemin,
Components,Composants,
Verified By,Vérifié Par,
Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0},
Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0},
Filter Based On,Filtre basé sur,
Reqd by date,Reqd par date,
Manufacturer Part Number <b>{0}</b> is invalid,Le numéro de <b>pièce du</b> fabricant <b>{0}</b> n'est pas valide,
@@ -5933,7 +5933,7 @@ Student Admission Program,Programme d'admission des étudiants,
Minimum Age,Âge Minimum,
Maximum Age,Âge Maximum,
Application Fee,Frais de Dossier,
Naming Series (for Student Applicant),Nom de série (pour un candidat étudiant),
Naming Series (for Student Applicant),Masque de numérotation (pour un candidat étudiant),
LMS Only,LMS seulement,
EDU-APP-.YYYY.-,EDU-APP-YYYY.-,
Application Status,État de la Demande,
@@ -6424,7 +6424,7 @@ Hotel Reservation User,Utilisateur chargé des réservations d'hôtel,
Hotel Room Reservation Item,Article de réservation de la chambre d'hôtel,
Hotel Settings,Paramètres d'Hotel,
Default Taxes and Charges,Taxes et frais par défaut,
Default Invoice Naming Series,Numéro de série par défaut pour les factures,
Default Invoice Naming Series,Masque de numérotation par défaut pour les factures,
Additional Salary,Salaire supplémentaire,
HR,RH,
HR-ADS-.YY.-.MM.-,HR-ADS-.YY .-. MM.-,
@@ -8034,7 +8034,7 @@ Default Unit of Measure,Unité de Mesure par Défaut,
Maintain Stock,Maintenir Stock,
Standard Selling Rate,Prix de Vente Standard,
Auto Create Assets on Purchase,Création automatique d'actifs à l'achat,
Asset Naming Series,Nom de série de l'actif,
Asset Naming Series,Masque de numérotation de l'actif,
Over Delivery/Receipt Allowance (%),Surlivrance / indemnité de réception (%),
Barcodes,Codes-barres,
Shelf Life In Days,Durée de conservation en jours,
@@ -8053,7 +8053,7 @@ Serial Nos and Batches,N° de Série et Lots,
Has Batch No,A un Numéro de Lot,
Automatically Create New Batch,Créer un Nouveau Lot Automatiquement,
Batch Number Series,Série de numéros de lots,
"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si la série est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec cette série. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe de la série dans les paramètres de stock.",
"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si le masque est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec ce masque. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe du masque dans les paramètres de stock.",
Has Expiry Date,A une date d'expiration,
Retain Sample,Conserver l'échantillon,
Max Sample Quantity,Quantité maximum d'échantillon,
@@ -8353,8 +8353,8 @@ Inter Warehouse Transfer Settings,Paramètres de transfert entre entrepôts,
Freeze Stock Entries,Geler les Entrées de Stocks,
Stock Frozen Upto,Stock Gelé Jusqu'au,
Batch Identification,Identification par lots,
Use Naming Series,Utiliser la série de noms,
Naming Series Prefix,Préfix du nom de série,
Use Naming Series,Utiliser le masque de numérotation,
Naming Series Prefix,Préfix du masque de numérotation,
UOM Category,Catégorie d'unité de mesure (UdM),
UOM Conversion Detail,Détails de Conversion de l'UdM,
Variant Field,Champ de Variante,
@@ -8824,7 +8824,7 @@ Is Inter State,Est Inter State,
Purchase Details,Détails d'achat,
Depreciation Posting Date,Date comptable de l'amortissement,
"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Par défaut, le nom du fournisseur est défini selon le nom du fournisseur saisi. Si vous souhaitez que les fournisseurs soient nommés par un",
choose the 'Naming Series' option.,choisissez l'option 'Naming Series'.,
choose the 'Naming Series' option.,choisissez l'option 'Masque de numérotation'.,
Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Configurez la liste de prix par défaut lors de la création d'une nouvelle transaction d'achat. Les prix des articles seront extraits de cette liste de prix.,
"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat ou un reçu sans créer d'abord une Commande d'Achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case «Autoriser la création de facture d'achat sans commmande d'achat» dans la fiche fournisseur.",
"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat sans créer d'abord un reçu d'achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case &quot;Autoriser la création de facture d'achat sans reçu d'achat&quot; dans la fiche fournisseur.",
@@ -9858,14 +9858,14 @@ Is Rate Adjustment Entry (Debit Note),Est un justement du prix de la note de dé
Issue a debit note with 0 qty against an existing Sales Invoice,Creer une note de débit avec une quatité à O pour la facture
Control Historical Stock Transactions,Controle de l'historique des stransaction de stock
No stock transactions can be created or modified before this date.,Aucune transaction ne peux être créée ou modifié avant cette date.
Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées
Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée
"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.","Les utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire"
Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent
Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix
Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions
Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries
Stock transactions that are older than the mentioned days cannot be modified.,Les transactions de stock plus ancienne que le nombre de jours ci-dessus ne peuvent être modifiées,
Role Allowed to Create/Edit Back-dated Transactions,Rôle autorisé à créer et modifier des transactions anti-datée,
"If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",Les utilisateur de ce role pourront creer et modifier des transactions dans le passé. Si vide tout les utilisateurs pourrons le faire
Auto Insert Item Price If Missing,Création du prix de l'article dans les listes de prix si abscent,
Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix,
Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock,
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions,
Have Default Naming Series for Batch ID?,Masque de numérotation par défaut pour les Lots ou Séries,
"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités"
Allowed Items,Articles autorisés
Party Specific Item,Restriction d'article disponible
@@ -9892,34 +9892,46 @@ Interview Feedback,Retour d'entretien
Journal Energy Point,Historique des points d'énergies
Billing Address Details,Adresse de facturation (détails)
Supplier Address Details,Adresse Fournisseur (détails)
Retail,Commerce
Users,Utilisateurs
Permission Manager,Gestion des permissions
Fetch Timesheet,Récuprer les temps saisis
Get Supplier Group Details,Appliquer les informations depuis le Groupe de fournisseur
Quality Inspection(s),Inspection(s) Qualité
Set Advances and Allocate (FIFO),Affecter les encours au réglement
Apply Putaway Rule,Appliquer la régle de routage d'entrepot
Delete Transactions,Supprimer les transactions
Default Payment Discount Account,Compte par défaut des paiements de remise
Unrealized Profit / Loss Account,Compte de perte
Enable Provisional Accounting For Non Stock Items,Activer la provision pour les articles non stockés
Publish in Website,Publier sur le Site Web
List View,Vue en liste
Allow Excess Material Transfer,Autoriser les transfert de stock supérieurs à l'attendue
Allow transferring raw materials even after the Required Quantity is fulfilled,Autoriser les transfert de matiéres premiére mais si la quantité requise est atteinte
Add Corrective Operation Cost in Finished Good Valuation,Ajouter des opérations de correction de coût pour la valorisation des produits finis
Make Serial No / Batch from Work Order,Générer des numéros de séries / lots depuis les Ordres de Fabrications
System will automatically create the serial numbers / batch for the Finished Good on submission of work order,le systéme va créer des numéros de séries / lots à la validation des produit finis depuis les Ordres de Fabrications
Allow material consumptions without immediately manufacturing finished goods against a Work Order,Autoriser la consommation sans immédiatement fabriqué les produit fini dans les ordres de fabrication
Quality Inspection Parameter,Paramétre des Inspection Qualité
Parameter Group,Groupe de paramétre
E Commerce Settings,Paramétrage E-Commerce
Follow these steps to create a landing page for your store:,Suivez les intructions suivantes pour créer votre page d'accueil de boutique en ligne
Show Price in Quotation,Afficher les prix sur les devis
Add-ons,Extensions
Enable Wishlist,Activer la liste de souhaits
Enable Reviews and Ratings,Activer les avis et notes
Enable Recommendations,Activer les recommendations
Item Search Settings,Paramétrage de la recherche d'article
Purchase demande,Demande de materiel
Retail,Commerce,
Users,Utilisateurs,
Permission Manager,Gestion des permissions,
Fetch Timesheet,Récuprer les temps saisis,
Get Supplier Group Details,Appliquer les informations depuis le Groupe de fournisseur,
Quality Inspection(s),Inspection(s) Qualite,
Set Advances and Allocate (FIFO),Affecter les encours au réglement,
Apply Putaway Rule,Appliquer la régle de routage d'entrepot,
Delete Transactions,Supprimer les transactions,
Default Payment Discount Account,Compte par défaut des paiements de remise,
Unrealized Profit / Loss Account,Compte de perte,
Enable Provisional Accounting For Non Stock Items,Activer la provision pour les articles non stockés,
Publish in Website,Publier sur le Site Web,
List View,Vue en liste,
Allow Excess Material Transfer,Autoriser les transfert de stock supérieurs à l'attendue,
Allow transferring raw materials even after the Required Quantity is fulfilled,Autoriser les transfert de matiéres premiére mais si la quantité requise est atteinte,
Add Corrective Operation Cost in Finished Good Valuation,Ajouter des opérations de correction de coût pour la valorisation des produits finis,
Make Serial No / Batch from Work Order,Générer des numéros de séries / lots depuis les Ordres de Fabrications,
System will automatically create the serial numbers / batch for the Finished Good on submission of work order,le systéme va créer des numéros de séries / lots à la validation des produit finis depuis les Ordres de Fabrications,
Allow material consumptions without immediately manufacturing finished goods against a Work Order,Autoriser la consommation sans immédiatement fabriqué les produit fini dans les ordres de fabrication,
Quality Inspection Parameter,Paramétre des Inspection Qualite,
Parameter Group,Groupe de paramétre,
E Commerce Settings,Paramétrage E-Commerce,
Follow these steps to create a landing page for your store:,Suivez les intructions suivantes pour créer votre page d'accueil de boutique en ligne,
Show Price in Quotation,Afficher les prix sur les devis,
Add-ons,Extensions,
Enable Wishlist,Activer la liste de souhaits,
Enable Reviews and Ratings,Activer les avis et notes,
Enable Recommendations,Activer les recommendations,
Item Search Settings,Paramétrage de la recherche d'article,
Purchase demande,Demande de materiel,
Internal Customer,Client interne
Internal Supplier,Fournisseur interne
Contact & Address,Contact et Adresse
Primary Address and Contact,Adresse et contact principal
Supplier Primary Contact,Contact fournisseur principal
Supplier Primary Address,Adresse fournisseur principal
From Opportunity,Depuis l'opportunité
Default Receivable Accounts,Compte de débit par défaut
Receivable Accounts,Compte de débit
Mention if a non-standard receivable account,Veuillez mentionner s'il s'agit d'un compte débiteur non standard
Allow Purchase,Autoriser à l'achat
Inventory Settings,Paramétrage de l'inventaire
Can't render this file because it is too large.