mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-20 19:32:30 +00:00
Compare commits
107 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
259f3422d5 | ||
|
|
4f0bb5e643 | ||
|
|
240d866ef4 | ||
|
|
0e87c86aab | ||
|
|
00b9df0bc5 | ||
|
|
cd1c175439 | ||
|
|
2ca2e67812 | ||
|
|
c26a52d791 | ||
|
|
5881960001 | ||
|
|
9c108a8ef7 | ||
|
|
5b04708164 | ||
|
|
d76c2c5738 | ||
|
|
07f235cf7d | ||
|
|
7adad4272a | ||
|
|
f9981d1ff3 | ||
|
|
8770aa5955 | ||
|
|
2d7d86039a | ||
|
|
bdfbccd38e | ||
|
|
a234b8932e | ||
|
|
2216875bd6 | ||
|
|
67393694de | ||
|
|
0d7a4b6ff6 | ||
|
|
e0fb398be1 | ||
|
|
cf2a3e2fc5 | ||
|
|
a79b30e45f | ||
|
|
769d7d7554 | ||
|
|
674dba8cd7 | ||
|
|
1ca9aca0d5 | ||
|
|
8e8e59cecb | ||
|
|
47d0e76999 | ||
|
|
46bb309b8a | ||
|
|
dfd356a174 | ||
|
|
2d43ba0c90 | ||
|
|
a09777c406 | ||
|
|
c1819a4b21 | ||
|
|
caa4f33169 | ||
|
|
e6dca0668c | ||
|
|
b033b3b0d6 | ||
|
|
75012c17d4 | ||
|
|
4605bc51ae | ||
|
|
8c57d56240 | ||
|
|
8501a1182a | ||
|
|
fb32120e36 | ||
|
|
d1590f266b | ||
|
|
710ec18086 | ||
|
|
a9dcf46882 | ||
|
|
e8f24b8b4e | ||
|
|
fcda6818ed | ||
|
|
f748eb74f1 | ||
|
|
5f677b9629 | ||
|
|
d34f1c1776 | ||
|
|
2dee97aa70 | ||
|
|
5ac7dca2ec | ||
|
|
ee02cde58d | ||
|
|
14dc40360d | ||
|
|
09af485d54 | ||
|
|
80eb8754db | ||
|
|
43b85c5711 | ||
|
|
04f9915009 | ||
|
|
782a4d1fa8 | ||
|
|
07f1b42c64 | ||
|
|
3a024294a9 | ||
|
|
5b562961e3 | ||
|
|
8f72a6814b | ||
|
|
99e7406b5b | ||
|
|
516191bf2b | ||
|
|
33a947726d | ||
|
|
29ddd26ba1 | ||
|
|
26a0b3b380 | ||
|
|
289dc36696 | ||
|
|
cd3b85ccff | ||
|
|
09966d10d4 | ||
|
|
9c8e2a33e9 | ||
|
|
a799e1b217 | ||
|
|
4fad4c3fa4 | ||
|
|
57b19a523e | ||
|
|
b72232b358 | ||
|
|
32bdb7cccd | ||
|
|
7b3bcd3bc4 | ||
|
|
cfa07bed74 | ||
|
|
2844d849e0 | ||
|
|
93bd4c7ff3 | ||
|
|
6d051f5732 | ||
|
|
4ac60cd73b | ||
|
|
e8b93ef32f | ||
|
|
ddcd2cfe80 | ||
|
|
e25f4ffa50 | ||
|
|
50ef35845a | ||
|
|
1a6be5e19b | ||
|
|
be94402338 | ||
|
|
800417eeed | ||
|
|
dc603c3df0 | ||
|
|
8bf957b9a1 | ||
|
|
ade13e6d36 | ||
|
|
c3c7dd89fb | ||
|
|
b105ba11b8 | ||
|
|
faf692d344 | ||
|
|
aadd3f9e69 | ||
|
|
4b49864995 | ||
|
|
e802f0c352 | ||
|
|
619b0feb5f | ||
|
|
32fea643cd | ||
|
|
02428b446d | ||
|
|
479cab0336 | ||
|
|
f07b87c5eb | ||
|
|
c3b21a6c30 | ||
|
|
c9daa85985 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.32.1"
|
||||
__version__ = "14.34.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -341,7 +341,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
"enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
)
|
||||
|
||||
accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
|
||||
accounts_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item,
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"report_settings_sb",
|
||||
@@ -408,6 +409,13 @@
|
||||
"fieldname": "enable_fuzzy_matching",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Fuzzy Matching"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ",
|
||||
"fieldname": "ignore_account_closing_balance",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Account Closing Balance"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -415,7 +423,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-15 18:47:46.430291",
|
||||
"modified": "2023-07-27 15:05:34.000264",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -14,21 +14,32 @@ from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
def on_update(self):
|
||||
frappe.clear_cache()
|
||||
|
||||
def validate(self):
|
||||
frappe.db.set_default(
|
||||
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
|
||||
)
|
||||
old_doc = self.get_doc_before_save()
|
||||
clear_cache = False
|
||||
|
||||
frappe.db.set_default(
|
||||
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
|
||||
)
|
||||
if old_doc.add_taxes_from_item_tax_template != self.add_taxes_from_item_tax_template:
|
||||
frappe.db.set_default(
|
||||
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
|
||||
)
|
||||
clear_cache = True
|
||||
|
||||
if old_doc.enable_common_party_accounting != self.enable_common_party_accounting:
|
||||
frappe.db.set_default(
|
||||
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
|
||||
)
|
||||
clear_cache = True
|
||||
|
||||
self.validate_stale_days()
|
||||
self.enable_payment_schedule_in_print()
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||
self.enable_payment_schedule_in_print()
|
||||
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
def validate_stale_days(self):
|
||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
||||
|
||||
@@ -141,7 +141,7 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
|
||||
)
|
||||
|
||||
if points_to_redeem > loyalty_program_details.loyalty_points:
|
||||
frappe.throw(_("You don't have enought Loyalty Points to redeem"))
|
||||
frappe.throw(_("You don't have enough Loyalty Points to redeem"))
|
||||
|
||||
loyalty_amount = flt(points_to_redeem * loyalty_program_details.conversion_factor)
|
||||
|
||||
|
||||
@@ -894,12 +894,12 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if(frm.doc.payment_type == "Receive"
|
||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions
|
||||
&& frm.doc.total_allocated_amount < frm.doc.paid_amount + (total_deductions / frm.doc.source_exchange_rate)) {
|
||||
unallocated_amount = (frm.doc.base_received_amount + total_deductions + frm.doc.base_total_taxes_and_charges
|
||||
unallocated_amount = (frm.doc.base_received_amount + total_deductions + flt(frm.doc.base_total_taxes_and_charges)
|
||||
- frm.doc.base_total_allocated_amount) / frm.doc.source_exchange_rate;
|
||||
} else if (frm.doc.payment_type == "Pay"
|
||||
&& frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions
|
||||
&& frm.doc.total_allocated_amount < frm.doc.received_amount + (total_deductions / frm.doc.target_exchange_rate)) {
|
||||
unallocated_amount = (frm.doc.base_paid_amount + frm.doc.base_total_taxes_and_charges - (total_deductions
|
||||
unallocated_amount = (frm.doc.base_paid_amount + flt(frm.doc.base_total_taxes_and_charges) - (total_deductions
|
||||
+ frm.doc.base_total_allocated_amount)) / frm.doc.target_exchange_rate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,12 +234,14 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
if d.payment_term and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
|
||||
if (
|
||||
d.payment_term
|
||||
and (
|
||||
(flt(d.allocated_amount)) > 0
|
||||
and latest.payment_term_outstanding
|
||||
and (flt(d.allocated_amount) > flt(latest.payment_term_outstanding))
|
||||
)
|
||||
and self.term_based_allocation_enabled_for_reference(d.reference_doctype, d.reference_name)
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
@@ -249,6 +251,9 @@ class PaymentEntry(AccountsController):
|
||||
)
|
||||
)
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
@@ -631,7 +636,9 @@ class PaymentEntry(AccountsController):
|
||||
if not self.apply_tax_withholding_amount:
|
||||
return
|
||||
|
||||
net_total = self.paid_amount
|
||||
order_amount = self.get_order_net_total()
|
||||
|
||||
net_total = flt(order_amount) + flt(self.unallocated_amount)
|
||||
|
||||
# Adding args as purchase invoice to get TDS amount
|
||||
args = frappe._dict(
|
||||
@@ -676,6 +683,20 @@ class PaymentEntry(AccountsController):
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def get_order_net_total(self):
|
||||
if self.party_type == "Supplier":
|
||||
doctype = "Purchase Order"
|
||||
else:
|
||||
doctype = "Sales Order"
|
||||
|
||||
docnames = [d.reference_name for d in self.references if d.reference_doctype == doctype]
|
||||
|
||||
tax_withholding_net_total = frappe.db.get_value(
|
||||
doctype, {"name": ["in", docnames]}, ["sum(base_tax_withholding_net_total)"]
|
||||
)
|
||||
|
||||
return tax_withholding_net_total
|
||||
|
||||
def apply_taxes(self):
|
||||
self.initialize_taxes()
|
||||
self.determine_exclusive_rate()
|
||||
|
||||
@@ -1156,6 +1156,52 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
si3.cancel()
|
||||
si3.delete()
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"unlink_payment_on_cancellation_of_invoice": 1,
|
||||
"delete_linked_ledger_entries": 1,
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
},
|
||||
)
|
||||
def test_overallocation_validation_shouldnt_misfire(self):
|
||||
"""
|
||||
Overallocation validation shouldn't fire for Template without "Allocate Payment based on Payment Terms" enabled
|
||||
|
||||
"""
|
||||
customer = create_customer()
|
||||
create_payment_terms_template()
|
||||
|
||||
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||
template.allocate_payment_based_on_payment_terms = 0
|
||||
template.save()
|
||||
|
||||
# Validate allocation on base/company currency
|
||||
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
|
||||
si.payment_terms_template = "Test Receivable Template"
|
||||
si.save().submit()
|
||||
|
||||
si.reload()
|
||||
pe = get_payment_entry(si.doctype, si.name).save()
|
||||
# There will no term based allocation
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.references[0].payment_term, None)
|
||||
self.assertEqual(flt(pe.references[0].allocated_amount), flt(si.grand_total))
|
||||
pe.save()
|
||||
|
||||
# specify a term
|
||||
pe.references[0].payment_term = template.terms[0].payment_term
|
||||
# no validation error should be thrown
|
||||
pe.save()
|
||||
|
||||
pe.paid_amount = si.grand_total + 1
|
||||
pe.references[0].allocated_amount = si.grand_total + 1
|
||||
self.assertRaises(frappe.ValidationError, pe.save)
|
||||
|
||||
template = frappe.get_doc("Payment Terms Template", "Test Receivable Template")
|
||||
template.allocate_payment_based_on_payment_terms = 1
|
||||
template.save()
|
||||
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc("Payment Entry")
|
||||
|
||||
@@ -232,7 +232,7 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if (
|
||||
cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate"))
|
||||
cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate"))
|
||||
and not self.is_return
|
||||
and not self.is_internal_supplier
|
||||
):
|
||||
@@ -976,30 +976,6 @@ class PurchaseInvoice(BuyingController):
|
||||
item.item_tax_amount, item.precision("item_tax_amount")
|
||||
)
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
precision_loss = self.get("base_net_total") - flt(
|
||||
self.get("net_total") * self.conversion_rate, self.precision("net_total")
|
||||
)
|
||||
|
||||
if precision_loss:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": self.supplier,
|
||||
"credit": precision_loss,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else self.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get_asset_gl_entry(self, gl_entries):
|
||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
|
||||
@@ -1670,6 +1670,52 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
rate = flt(sle.stock_value_difference) / flt(sle.actual_qty)
|
||||
self.assertAlmostEqual(returned_inv.items[0].rate, rate)
|
||||
|
||||
def test_payment_allocation_for_payment_terms(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_pr_against_po,
|
||||
create_purchase_order,
|
||||
)
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
make_purchase_invoice as make_pi_from_pr,
|
||||
)
|
||||
|
||||
automatically_fetch_payment_terms()
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
0,
|
||||
)
|
||||
|
||||
po = create_purchase_order(do_not_save=1)
|
||||
po.payment_terms_template = "_Test Payment Term Template"
|
||||
po.save()
|
||||
po.submit()
|
||||
|
||||
pr = create_pr_against_po(po.name, received_qty=4)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 1000)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
1,
|
||||
)
|
||||
pi = make_pi_from_pr(pr.name)
|
||||
self.assertEqual(pi.payment_schedule[0].payment_amount, 2500)
|
||||
|
||||
automatically_fetch_payment_terms(enable=0)
|
||||
frappe.db.set_value(
|
||||
"Payment Terms Template",
|
||||
"_Test Payment Term Template",
|
||||
"allocate_payment_based_on_payment_terms",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -1075,6 +1075,7 @@ class SalesInvoice(SellingController):
|
||||
self.make_internal_transfer_gl_entries(gl_entries)
|
||||
|
||||
self.make_item_gl_entries(gl_entries)
|
||||
self.make_precision_loss_gl_entry(gl_entries)
|
||||
self.make_discount_gl_entries(gl_entries)
|
||||
|
||||
# merge gl entries before adding pos entries
|
||||
|
||||
@@ -15,6 +15,7 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
"Delivery Note": ["items", "delivery_note"],
|
||||
"Timesheet": ["timesheets", "time_sheet"],
|
||||
},
|
||||
"transactions": [
|
||||
|
||||
@@ -2049,28 +2049,27 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(si.total_taxes_and_charges, 228.82)
|
||||
self.assertEqual(si.rounding_adjustment, -0.01)
|
||||
|
||||
expected_values = dict(
|
||||
(d[0], d)
|
||||
for d in [
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
]
|
||||
)
|
||||
expected_values = [
|
||||
["_Test Account Service Tax - _TC", 0.0, 114.41],
|
||||
["_Test Account VAT - _TC", 0.0, 114.41],
|
||||
[si.debit_to, 1500, 0.0],
|
||||
["Round Off - _TC", 0.01, 0.01],
|
||||
["Sales - _TC", 0.0, 1271.18],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for gle in gl_entries:
|
||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[i][0], gle.account)
|
||||
self.assertEqual(expected_values[i][1], gle.debit)
|
||||
self.assertEqual(expected_values[i][2], gle.credit)
|
||||
|
||||
def test_rounding_adjustment_3(self):
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
@@ -2125,13 +2124,14 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
["Round Off - _TC", 0.01, 0],
|
||||
["Round Off - _TC", 0.02, 0.01],
|
||||
]
|
||||
)
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit
|
||||
"""select account, sum(debit) as debit, sum(credit) as credit
|
||||
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||
group by account
|
||||
order by account asc""",
|
||||
si.name,
|
||||
as_dict=1,
|
||||
@@ -3316,6 +3316,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
def test_sales_return_negative_rate(self):
|
||||
si = create_sales_invoice(is_return=1, qty=-2, rate=-10, do_not_save=True)
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
si.items[0].rate = 10
|
||||
si.save()
|
||||
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
|
||||
@@ -476,7 +476,12 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
threshold = tax_details.get("threshold", 0)
|
||||
cumulative_threshold = tax_details.get("cumulative_threshold", 0)
|
||||
|
||||
if (threshold and inv.tax_withholding_net_total >= threshold) or (
|
||||
if inv.doctype != "Payment Entry":
|
||||
tax_withholding_net_total = inv.base_tax_withholding_net_total
|
||||
else:
|
||||
tax_withholding_net_total = inv.tax_withholding_net_total
|
||||
|
||||
if (threshold and tax_withholding_net_total >= threshold) or (
|
||||
cumulative_threshold and supp_credit_amt >= cumulative_threshold
|
||||
):
|
||||
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(
|
||||
|
||||
@@ -321,6 +321,42 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in reversed(orders):
|
||||
d.cancel()
|
||||
|
||||
def test_tds_deduction_for_po_via_payment_entry(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier8", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
)
|
||||
order = create_purchase_order(supplier="Test TDS Supplier8", rate=40000, do_not_save=True)
|
||||
|
||||
# Add some tax on the order
|
||||
order.append(
|
||||
"taxes",
|
||||
{
|
||||
"category": "Total",
|
||||
"charge_type": "Actual",
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"tax_amount": 8000,
|
||||
"description": "Test",
|
||||
"add_deduct_tax": "Add",
|
||||
},
|
||||
)
|
||||
|
||||
order.save()
|
||||
|
||||
order.apply_tds = 1
|
||||
order.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
order.submit()
|
||||
|
||||
self.assertEqual(order.taxes[0].tax_amount, 4000)
|
||||
|
||||
payment = get_payment_entry(order.doctype, order.name)
|
||||
payment.apply_tax_withholding_amount = 1
|
||||
payment.tax_withholding_category = "Cumulative Threshold TDS"
|
||||
payment.submit()
|
||||
self.assertEqual(payment.taxes[0].tax_amount, 4000)
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category"
|
||||
@@ -578,6 +614,7 @@ def create_records():
|
||||
"Test TDS Supplier5",
|
||||
"Test TDS Supplier6",
|
||||
"Test TDS Supplier7",
|
||||
"Test TDS Supplier8",
|
||||
]:
|
||||
if frappe.db.exists("Supplier", name):
|
||||
continue
|
||||
|
||||
@@ -105,7 +105,8 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
|
||||
if not gl_map:
|
||||
return []
|
||||
|
||||
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
|
||||
if gl_map[0].voucher_type != "Period Closing Voucher":
|
||||
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
|
||||
|
||||
if merge_entries:
|
||||
gl_map = merge_similar_entries(gl_map, precision)
|
||||
|
||||
@@ -14,6 +14,7 @@ from frappe.contacts.doctype.address.address import (
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -883,32 +884,35 @@ def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None
|
||||
party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
|
||||
):
|
||||
cond = "1=1"
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.select(gle.party)
|
||||
.where(
|
||||
(gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
|
||||
)
|
||||
.groupby(gle.party)
|
||||
)
|
||||
if account_type == "Receivable":
|
||||
query = query.select(Sum(gle.credit).as_("amount"))
|
||||
else:
|
||||
query = query.select(Sum(gle.debit).as_("amount"))
|
||||
|
||||
if posting_date:
|
||||
if future_payment:
|
||||
cond = "(posting_date <= '{0}' OR DATE(creation) <= '{0}')" "".format(posting_date)
|
||||
query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
|
||||
else:
|
||||
cond = "posting_date <= '{0}'".format(posting_date)
|
||||
query = query.where(gle.posting_date <= posting_date)
|
||||
|
||||
if company:
|
||||
cond += "and company = {0}".format(frappe.db.escape(company))
|
||||
query = query.where(gle.company == company)
|
||||
|
||||
if party:
|
||||
cond += "and party = {0}".format(frappe.db.escape(party))
|
||||
query = query.where(gle.party == party)
|
||||
|
||||
data = frappe.db.sql(
|
||||
""" SELECT party, sum({0}) as amount
|
||||
FROM `tabGL Entry`
|
||||
WHERE
|
||||
party_type = %s and against_voucher is null
|
||||
and is_cancelled = 0
|
||||
and {1} GROUP BY party""".format(
|
||||
("credit") if party_type == "Customer" else "debit", cond
|
||||
),
|
||||
party_type,
|
||||
)
|
||||
data = query.run(as_dict=True)
|
||||
if data:
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"party_type": "Supplier",
|
||||
"account_type": "Payable",
|
||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||
}
|
||||
return ReceivablePayableReport(filters).run(args)
|
||||
|
||||
@@ -9,7 +9,7 @@ from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_sum
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"party_type": "Supplier",
|
||||
"account_type": "Payable",
|
||||
"naming_by": ["Buying Settings", "supp_master_name"],
|
||||
}
|
||||
return AccountsReceivableSummary(filters).run(args)
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import OrderedDict
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date
|
||||
from frappe.query_builder.functions import Date, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
@@ -34,7 +34,7 @@ from erpnext.accounts.utils import get_currency_precision
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"party_type": "Customer",
|
||||
"account_type": "Receivable",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
return ReceivablePayableReport(filters).run(args)
|
||||
@@ -70,8 +70,11 @@ class ReceivablePayableReport(object):
|
||||
"Company", self.filters.get("company"), "default_currency"
|
||||
)
|
||||
self.currency_precision = get_currency_precision() or 2
|
||||
self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit"
|
||||
self.party_type = self.filters.party_type
|
||||
self.dr_or_cr = "debit" if self.filters.account_type == "Receivable" else "credit"
|
||||
self.account_type = self.filters.account_type
|
||||
self.party_type = frappe.db.get_all(
|
||||
"Party Type", {"account_type": self.account_type}, pluck="name"
|
||||
)
|
||||
self.party_details = {}
|
||||
self.invoices = set()
|
||||
self.skip_total_row = 0
|
||||
@@ -197,6 +200,7 @@ class ReceivablePayableReport(object):
|
||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
||||
|
||||
row.party_type = ple.party_type
|
||||
return row
|
||||
|
||||
def update_voucher_balance(self, ple):
|
||||
@@ -207,8 +211,9 @@ class ReceivablePayableReport(object):
|
||||
return
|
||||
|
||||
# amount in "Party Currency", if its supplied. If not, amount in company currency
|
||||
if self.filters.get(scrub(self.party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
@@ -362,7 +367,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_invoice_details(self):
|
||||
self.invoice_details = frappe._dict()
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
si_list = frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, po_no
|
||||
@@ -390,7 +395,7 @@ class ReceivablePayableReport(object):
|
||||
d.sales_person
|
||||
)
|
||||
|
||||
if self.party_type == "Supplier":
|
||||
if self.account_type == "Payable":
|
||||
for pi in frappe.db.sql(
|
||||
"""
|
||||
select name, due_date, bill_no, bill_date
|
||||
@@ -421,8 +426,10 @@ class ReceivablePayableReport(object):
|
||||
# customer / supplier name
|
||||
party_details = self.get_party_details(row.party) or {}
|
||||
row.update(party_details)
|
||||
if self.filters.get(scrub(self.filters.party_type)):
|
||||
row.currency = row.account_currency
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
row.currency = row.account_currency
|
||||
break
|
||||
else:
|
||||
row.currency = self.company_currency
|
||||
|
||||
@@ -532,65 +539,67 @@ class ReceivablePayableReport(object):
|
||||
self.future_payments.setdefault((d.invoice_no, d.party), []).append(d)
|
||||
|
||||
def get_future_payments_from_payment_entry(self):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
ref.reference_name as invoice_no,
|
||||
payment_entry.party,
|
||||
payment_entry.party_type,
|
||||
payment_entry.posting_date as future_date,
|
||||
ref.allocated_amount as future_amount,
|
||||
payment_entry.reference_no as future_ref
|
||||
from
|
||||
`tabPayment Entry` as payment_entry inner join `tabPayment Entry Reference` as ref
|
||||
on
|
||||
(ref.parent = payment_entry.name)
|
||||
where
|
||||
payment_entry.docstatus < 2
|
||||
and payment_entry.posting_date > %s
|
||||
and payment_entry.party_type = %s
|
||||
""",
|
||||
(self.filters.report_date, self.party_type),
|
||||
as_dict=1,
|
||||
)
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||
return (
|
||||
frappe.qb.from_(pe)
|
||||
.inner_join(pe_ref)
|
||||
.on(pe_ref.parent == pe.name)
|
||||
.select(
|
||||
(pe_ref.reference_name).as_("invoice_no"),
|
||||
pe.party,
|
||||
pe.party_type,
|
||||
(pe.posting_date).as_("future_date"),
|
||||
(pe_ref.allocated_amount).as_("future_amount"),
|
||||
(pe.reference_no).as_("future_ref"),
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus < 2)
|
||||
& (pe.posting_date > self.filters.report_date)
|
||||
& (pe.party_type.isin(self.party_type))
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
def get_future_payments_from_journal_entry(self):
|
||||
if self.filters.get("party"):
|
||||
amount_field = (
|
||||
"jea.debit_in_account_currency - jea.credit_in_account_currency"
|
||||
if self.party_type == "Supplier"
|
||||
else "jea.credit_in_account_currency - jea.debit_in_account_currency"
|
||||
)
|
||||
else:
|
||||
amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit"
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
jea.reference_name as invoice_no,
|
||||
je = frappe.qb.DocType("Journal Entry")
|
||||
jea = frappe.qb.DocType("Journal Entry Account")
|
||||
query = (
|
||||
frappe.qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
jea.reference_name.as_("invoice_no"),
|
||||
jea.party,
|
||||
jea.party_type,
|
||||
je.posting_date as future_date,
|
||||
sum('{0}') as future_amount,
|
||||
je.cheque_no as future_ref
|
||||
from
|
||||
`tabJournal Entry` as je inner join `tabJournal Entry Account` as jea
|
||||
on
|
||||
(jea.parent = je.name)
|
||||
where
|
||||
je.docstatus < 2
|
||||
and je.posting_date > %s
|
||||
and jea.party_type = %s
|
||||
and jea.reference_name is not null and jea.reference_name != ''
|
||||
group by je.name, jea.reference_name
|
||||
having future_amount > 0
|
||||
""".format(
|
||||
amount_field
|
||||
),
|
||||
(self.filters.report_date, self.party_type),
|
||||
as_dict=1,
|
||||
je.posting_date.as_("future_date"),
|
||||
je.cheque_no.as_("future_ref"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus < 2)
|
||||
& (je.posting_date > self.filters.report_date)
|
||||
& (jea.party_type.isin(self.party_type))
|
||||
& (jea.reference_name.isnotnull())
|
||||
& (jea.reference_name != "")
|
||||
)
|
||||
)
|
||||
|
||||
if self.filters.get("party"):
|
||||
if self.account_type == "Payable":
|
||||
query = query.select(
|
||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
|
||||
)
|
||||
else:
|
||||
query = query.select(
|
||||
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
|
||||
)
|
||||
|
||||
query = query.having(qb.Field("future_amount") > 0)
|
||||
return query.run(as_dict=True)
|
||||
|
||||
def allocate_future_payments(self, row):
|
||||
# future payments are captured in additional columns
|
||||
# this method allocates pending future payments against a voucher to
|
||||
@@ -619,13 +628,17 @@ class ReceivablePayableReport(object):
|
||||
row.future_ref = ", ".join(row.future_ref)
|
||||
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doctype = "Sales Invoice" if self.account_type == "Receivable" else "Purchase Invoice"
|
||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||
party_field = scrub(self.filters.party_type)
|
||||
if self.filters.get(party_field):
|
||||
filters.update({party_field: self.filters.get(party_field)})
|
||||
or_filters = {}
|
||||
for party_type in self.party_type:
|
||||
party_field = scrub(party_type)
|
||||
if self.filters.get(party_field):
|
||||
or_filters.update({party_field: self.filters.get(party_field)})
|
||||
self.return_entries = frappe._dict(
|
||||
frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1)
|
||||
frappe.get_all(
|
||||
doctype, filters=filters, or_filters=or_filters, fields=["name", "return_against"], as_list=1
|
||||
)
|
||||
)
|
||||
|
||||
def set_ageing(self, row):
|
||||
@@ -716,6 +729,7 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(self.qb_selection_filter))
|
||||
.where(Criterion.any(self.or_filters))
|
||||
)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
@@ -746,16 +760,18 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def prepare_conditions(self):
|
||||
self.qb_selection_filter = []
|
||||
party_type_field = scrub(self.party_type)
|
||||
self.qb_selection_filter.append(self.ple.party_type == self.party_type)
|
||||
self.or_filters = []
|
||||
for party_type in self.party_type:
|
||||
party_type_field = scrub(party_type)
|
||||
self.or_filters.append(self.ple.party_type == party_type)
|
||||
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
self.add_common_filters(party_type_field=party_type_field)
|
||||
|
||||
if party_type_field == "customer":
|
||||
self.add_customer_filters()
|
||||
if party_type_field == "customer":
|
||||
self.add_customer_filters()
|
||||
|
||||
elif party_type_field == "supplier":
|
||||
self.add_supplier_filters()
|
||||
elif party_type_field == "supplier":
|
||||
self.add_supplier_filters()
|
||||
|
||||
if self.filters.cost_center:
|
||||
self.get_cost_center_conditions()
|
||||
@@ -784,11 +800,10 @@ class ReceivablePayableReport(object):
|
||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||
else:
|
||||
# get GL with "receivable" or "payable" account_type
|
||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
||||
accounts = [
|
||||
d.name
|
||||
for d in frappe.get_all(
|
||||
"Account", filters={"account_type": account_type, "company": self.filters.company}
|
||||
"Account", filters={"account_type": self.account_type, "company": self.filters.company}
|
||||
)
|
||||
]
|
||||
|
||||
@@ -878,7 +893,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_party_details(self, party):
|
||||
if not party in self.party_details:
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
fields = ["customer_name", "territory", "customer_group", "customer_primary_contact"]
|
||||
|
||||
if self.filters.get("sales_partner"):
|
||||
@@ -901,14 +916,20 @@ class ReceivablePayableReport(object):
|
||||
self.columns = []
|
||||
self.add_column("Posting Date", fieldtype="Date")
|
||||
self.add_column(
|
||||
label=_(self.party_type),
|
||||
label="Party Type",
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label="Party",
|
||||
fieldname="party",
|
||||
fieldtype="Link",
|
||||
options=self.party_type,
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
width=180,
|
||||
)
|
||||
self.add_column(
|
||||
label="Receivable Account" if self.party_type == "Customer" else "Payable Account",
|
||||
label=self.account_type + " Account",
|
||||
fieldname="party_account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
@@ -916,13 +937,19 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
if self.account_type == "Payable":
|
||||
label = "Supplier Name"
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
label = "Customer Name"
|
||||
fieldname = "customer_name"
|
||||
self.add_column(
|
||||
_("{0} Name").format(self.party_type),
|
||||
fieldname=scrub(self.party_type) + "_name",
|
||||
label=label,
|
||||
fieldname=fieldname,
|
||||
fieldtype="Data",
|
||||
)
|
||||
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
self.add_column(
|
||||
_("Customer Contact"),
|
||||
fieldname="customer_primary_contact",
|
||||
@@ -942,7 +969,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
self.add_column(label="Due Date", fieldtype="Date")
|
||||
|
||||
if self.party_type == "Supplier":
|
||||
if self.account_type == "Payable":
|
||||
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
|
||||
self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date")
|
||||
|
||||
@@ -952,7 +979,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
||||
self.add_column(_("Paid Amount"), fieldname="paid")
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
self.add_column(_("Credit Note"), fieldname="credit_note")
|
||||
else:
|
||||
# note: fieldname is still `credit_note`
|
||||
@@ -970,7 +997,7 @@ class ReceivablePayableReport(object):
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
if self.filters.account_type == "Receivable":
|
||||
self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data")
|
||||
|
||||
# comma separated list of linked delivery notes
|
||||
@@ -991,7 +1018,7 @@ class ReceivablePayableReport(object):
|
||||
if self.filters.sales_partner:
|
||||
self.add_column(label=_("Sales Partner"), fieldname="default_sales_partner", fieldtype="Data")
|
||||
|
||||
if self.filters.party_type == "Supplier":
|
||||
if self.filters.account_type == "Payable":
|
||||
self.add_column(
|
||||
label=_("Supplier Group"),
|
||||
fieldname="supplier_group",
|
||||
|
||||
@@ -12,7 +12,7 @@ from erpnext.accounts.report.accounts_receivable.accounts_receivable import Rece
|
||||
|
||||
def execute(filters=None):
|
||||
args = {
|
||||
"party_type": "Customer",
|
||||
"account_type": "Receivable",
|
||||
"naming_by": ["Selling Settings", "cust_master_name"],
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ def execute(filters=None):
|
||||
|
||||
class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
def run(self, args):
|
||||
self.party_type = args.get("party_type")
|
||||
self.account_type = args.get("account_type")
|
||||
self.party_type = frappe.db.get_all(
|
||||
"Party Type", {"account_type": self.account_type}, pluck="name"
|
||||
)
|
||||
self.party_naming_by = frappe.db.get_value(
|
||||
args.get("naming_by")[0], None, args.get("naming_by")[1]
|
||||
)
|
||||
@@ -35,13 +38,19 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
self.get_party_total(args)
|
||||
|
||||
party = None
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
party = self.filters.get(scrub(party_type))
|
||||
|
||||
party_advance_amount = (
|
||||
get_partywise_advanced_payment_amount(
|
||||
self.party_type,
|
||||
self.filters.report_date,
|
||||
self.filters.show_future_payments,
|
||||
self.filters.company,
|
||||
party=self.filters.get(scrub(self.party_type)),
|
||||
party=party,
|
||||
account_type=self.account_type,
|
||||
)
|
||||
or {}
|
||||
)
|
||||
@@ -57,9 +66,13 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
row.party = party
|
||||
if self.party_naming_by == "Naming Series":
|
||||
row.party_name = frappe.get_cached_value(
|
||||
self.party_type, party, scrub(self.party_type) + "_name"
|
||||
)
|
||||
if self.account_type == "Payable":
|
||||
doctype = "Supplier"
|
||||
fieldname = "supplier_name"
|
||||
else:
|
||||
doctype = "Customer"
|
||||
fieldname = "customer_name"
|
||||
row.party_name = frappe.get_cached_value(doctype, party, fieldname)
|
||||
|
||||
row.update(party_dict)
|
||||
|
||||
@@ -93,6 +106,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
# set territory, customer_group, sales person etc
|
||||
self.set_party_details(d)
|
||||
self.party_total[d.party].update({"party_type": d.party_type})
|
||||
|
||||
def init_party_total(self, row):
|
||||
self.party_total.setdefault(
|
||||
@@ -131,17 +145,27 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
self.add_column(
|
||||
label=_(self.party_type),
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
fieldtype="Data",
|
||||
width=100,
|
||||
)
|
||||
self.add_column(
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Link",
|
||||
options=self.party_type,
|
||||
fieldtype="Dynamic Link",
|
||||
options="party_type",
|
||||
width=180,
|
||||
)
|
||||
|
||||
if self.party_naming_by == "Naming Series":
|
||||
self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data")
|
||||
self.add_column(
|
||||
label=_("Supplier Name") if self.account_type == "Payable" else _("Customer Name"),
|
||||
fieldname="party_name",
|
||||
fieldtype="Data",
|
||||
)
|
||||
|
||||
credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note"
|
||||
credit_debit_label = "Credit Note" if self.account_type == "Receivable" else "Debit Note"
|
||||
|
||||
self.add_column(_("Advance Amount"), fieldname="advance")
|
||||
self.add_column(_("Invoiced Amount"), fieldname="invoiced")
|
||||
@@ -159,7 +183,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
self.add_column(label=_("Future Payment Amount"), fieldname="future_amount")
|
||||
self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance")
|
||||
|
||||
if self.party_type == "Customer":
|
||||
if self.account_type == "Receivable":
|
||||
self.add_column(
|
||||
label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory"
|
||||
)
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2016-04-08 14:49:58.133098",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:08:26.084484",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Asset Depreciation Ledger",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Asset",
|
||||
"report_name": "Asset Depreciation Ledger",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2016-04-08 14:49:58.133098",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2023-07-26 21:05:33.554778",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Asset Depreciation Ledger",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Asset",
|
||||
"report_name": "Asset Depreciation Ledger",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2016-04-08 14:56:37.235981",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:08:18.660476",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Asset Depreciations and Balances",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Asset",
|
||||
"report_name": "Asset Depreciations and Balances",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2016-04-08 14:56:37.235981",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 2,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2023-07-26 21:04:54.751077",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Asset Depreciations and Balances",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Asset",
|
||||
"report_name": "Asset Depreciations and Balances",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
|
||||
51
erpnext/accounts/report/balance_sheet/test_balance_sheet.py
Normal file
51
erpnext/accounts/report/balance_sheet/test_balance_sheet.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.report.balance_sheet.balance_sheet import execute
|
||||
|
||||
|
||||
class TestBalanceSheet(FrappeTestCase):
|
||||
def test_balance_sheet(self):
|
||||
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,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 6'")
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 6'")
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company="_Test Company 6",
|
||||
warehouse="Finished Goods - _TC6",
|
||||
expense_account="Cost of Goods Sold - _TC6",
|
||||
cost_center="Main - _TC6",
|
||||
qty=10,
|
||||
rate=100,
|
||||
)
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company 6",
|
||||
debit_to="Debtors - _TC6",
|
||||
income_account="Sales - _TC6",
|
||||
cost_center="Main - _TC6",
|
||||
qty=5,
|
||||
rate=110,
|
||||
)
|
||||
filters = frappe._dict(
|
||||
company="_Test Company 6",
|
||||
period_start_date=today(),
|
||||
period_end_date=today(),
|
||||
periodicity="Yearly",
|
||||
)
|
||||
result = execute(filters)[1]
|
||||
for account_dict in result:
|
||||
if account_dict.get("account") == "Current Liabilities - _TC6":
|
||||
self.assertEqual(account_dict.total, 1000)
|
||||
if account_dict.get("account") == "Current Assets - _TC6":
|
||||
self.assertEqual(account_dict.total, 550)
|
||||
@@ -6,6 +6,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.utils import cint, flt, getdate
|
||||
|
||||
import erpnext
|
||||
@@ -364,6 +365,7 @@ def get_data(
|
||||
accounts_by_name,
|
||||
accounts,
|
||||
ignore_closing_entries=False,
|
||||
root_type=root_type,
|
||||
)
|
||||
|
||||
calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
|
||||
@@ -608,6 +610,7 @@ def set_gl_entries_by_account(
|
||||
accounts_by_name,
|
||||
accounts,
|
||||
ignore_closing_entries=False,
|
||||
root_type=None,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
|
||||
@@ -615,7 +618,6 @@ def set_gl_entries_by_account(
|
||||
"Company", filters.get("company"), ["lft", "rgt"]
|
||||
)
|
||||
|
||||
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters)
|
||||
companies = frappe.db.sql(
|
||||
""" select name, default_currency from `tabCompany`
|
||||
where lft >= %(company_lft)s and rgt <= %(company_rgt)s""",
|
||||
@@ -631,27 +633,43 @@ def set_gl_entries_by_account(
|
||||
)
|
||||
|
||||
for d in companies:
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
|
||||
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
|
||||
acc.account_name, acc.account_number
|
||||
from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
|
||||
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
|
||||
order by gl.account, gl.posting_date""".format(
|
||||
additional_conditions=additional_conditions
|
||||
),
|
||||
{
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"lft": root_lft,
|
||||
"rgt": root_rgt,
|
||||
"company": d.name,
|
||||
"finance_book": filters.get("finance_book"),
|
||||
"company_fb": frappe.db.get_value("Company", d.name, "default_finance_book"),
|
||||
},
|
||||
as_dict=True,
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
account = frappe.qb.DocType("Account")
|
||||
query = (
|
||||
frappe.qb.from_(gle)
|
||||
.inner_join(account)
|
||||
.on(account.name == gle.account)
|
||||
.select(
|
||||
gle.posting_date,
|
||||
gle.account,
|
||||
gle.debit,
|
||||
gle.credit,
|
||||
gle.is_opening,
|
||||
gle.company,
|
||||
gle.fiscal_year,
|
||||
gle.debit_in_account_currency,
|
||||
gle.credit_in_account_currency,
|
||||
gle.account_currency,
|
||||
account.account_name,
|
||||
account.account_number,
|
||||
)
|
||||
.where(
|
||||
(gle.company == d.name)
|
||||
& (gle.is_cancelled == 0)
|
||||
& (gle.posting_date <= to_date)
|
||||
& (account.lft >= root_lft)
|
||||
& (account.rgt <= root_rgt)
|
||||
)
|
||||
.orderby(gle.account, gle.posting_date)
|
||||
)
|
||||
|
||||
if root_type:
|
||||
query = query.where(account.root_type == root_type)
|
||||
additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters, d)
|
||||
if additional_conditions:
|
||||
query = query.where(Criterion.all(additional_conditions))
|
||||
gl_entries = query.run(as_dict=True)
|
||||
|
||||
if filters and filters.get("presentation_currency") != d.default_currency:
|
||||
currency_info["company"] = d.name
|
||||
currency_info["company_currency"] = d.default_currency
|
||||
@@ -721,23 +739,25 @@ def validate_entries(key, entry, accounts_by_name, accounts):
|
||||
accounts.insert(idx + 1, args)
|
||||
|
||||
|
||||
def get_additional_conditions(from_date, ignore_closing_entries, filters):
|
||||
def get_additional_conditions(from_date, ignore_closing_entries, filters, d):
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
additional_conditions = []
|
||||
|
||||
if ignore_closing_entries:
|
||||
additional_conditions.append("ifnull(gl.voucher_type, '')!='Period Closing Voucher'")
|
||||
additional_conditions.append((gle.voucher_type != "Period Closing Voucher"))
|
||||
|
||||
if from_date:
|
||||
additional_conditions.append("gl.posting_date >= %(from_date)s")
|
||||
additional_conditions.append(gle.posting_date >= from_date)
|
||||
|
||||
finance_book = filters.get("finance_book")
|
||||
company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
additional_conditions.append(
|
||||
"(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||
)
|
||||
additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
|
||||
else:
|
||||
additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)")
|
||||
additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
|
||||
|
||||
return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else ""
|
||||
return additional_conditions
|
||||
|
||||
|
||||
def add_total_row(out, root_type, balance_must_be, companies, company_currency):
|
||||
|
||||
@@ -2,6 +2,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
@@ -10,16 +11,15 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
|
||||
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
|
||||
Deferred_Revenue_and_Expense_Report,
|
||||
)
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
class TestDeferredRevenueAndExpense(FrappeTestCase, AccountsTestMixin):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
clear_accounts_and_items()
|
||||
create_company()
|
||||
self.maxDiff = None
|
||||
|
||||
def clear_old_entries(self):
|
||||
@@ -51,55 +51,58 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
if deferred_invoices:
|
||||
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
|
||||
|
||||
def test_deferred_revenue(self):
|
||||
self.clear_old_entries()
|
||||
def setup_deferred_accounts_and_items(self):
|
||||
# created deferred expense accounts, if not found
|
||||
self.deferred_revenue_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - " + self.company_abbr,
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
# created deferred expense accounts, if not found
|
||||
deferred_revenue_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _CD",
|
||||
company="_Test Company DR",
|
||||
self.deferred_expense_account = create_account(
|
||||
account_name="Deferred Expense",
|
||||
parent_account="Current Assets - " + self.company_abbr,
|
||||
company=self.company,
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer("_Test Customer")
|
||||
self.create_supplier("_Test Furniture Supplier")
|
||||
self.setup_deferred_accounts_and_items()
|
||||
self.clear_old_entries()
|
||||
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Customer DR"
|
||||
customer.type = "Individual"
|
||||
customer.insert()
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
item = create_item(
|
||||
"_Test Internet Subscription",
|
||||
is_stock_item=0,
|
||||
warehouse="All Warehouses - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||
def test_deferred_revenue(self):
|
||||
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_revenue_account
|
||||
item.deferred_revenue_account = self.deferred_revenue_account
|
||||
item.no_of_months = 3
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item.name,
|
||||
company="_Test Company DR",
|
||||
customer="_Test Customer DR",
|
||||
debit_to="Debtors - _CD",
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date="2021-05-01",
|
||||
parent_cost_center="Main - _CD",
|
||||
cost_center="Main - _CD",
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
do_not_save=True,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
)
|
||||
|
||||
si.items[0].income_account = "Sales - _CD"
|
||||
si.items[0].income_account = self.income_account
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2021-05-01"
|
||||
si.items[0].service_end_date = "2021-08-01"
|
||||
si.items[0].deferred_revenue_account = deferred_revenue_account
|
||||
si.items[0].income_account = "Sales - _CD"
|
||||
si.items[0].deferred_revenue_account = self.deferred_revenue_account
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
@@ -110,7 +113,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
start_date="2021-05-01",
|
||||
end_date="2021-08-01",
|
||||
type="Income",
|
||||
company="_Test Company DR",
|
||||
company=self.company,
|
||||
)
|
||||
)
|
||||
pda.insert()
|
||||
@@ -120,7 +123,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2021-05-01",
|
||||
"period_end_date": "2021-08-01",
|
||||
@@ -142,57 +145,36 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||
def test_deferred_expense(self):
|
||||
self.clear_old_entries()
|
||||
|
||||
# created deferred expense accounts, if not found
|
||||
deferred_expense_account = create_account(
|
||||
account_name="Deferred Expense",
|
||||
parent_account="Current Assets - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
supplier = create_supplier(
|
||||
supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
|
||||
)
|
||||
supplier.save()
|
||||
|
||||
item = create_item(
|
||||
"_Test Office Desk",
|
||||
is_stock_item=0,
|
||||
warehouse="All Warehouses - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
self.create_item("_Test Office Desk", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_expense = 1
|
||||
item.deferred_expense_account = deferred_expense_account
|
||||
item.deferred_expense_account = self.deferred_expense_account
|
||||
item.no_of_months_exp = 3
|
||||
item.save()
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=item.name,
|
||||
company="_Test Company DR",
|
||||
supplier="_Test Furniture Supplier",
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
supplier=self.supplier,
|
||||
is_return=False,
|
||||
update_stock=False,
|
||||
posting_date=frappe.utils.datetime.date(2021, 5, 1),
|
||||
parent_cost_center="Main - _CD",
|
||||
cost_center="Main - _CD",
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
do_not_save=True,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
warehouse="All Warehouses - _CD",
|
||||
warehouse=self.warehouse,
|
||||
qty=1,
|
||||
)
|
||||
pi.set_posting_time = True
|
||||
pi.items[0].enable_deferred_expense = 1
|
||||
pi.items[0].service_start_date = "2021-05-01"
|
||||
pi.items[0].service_end_date = "2021-08-01"
|
||||
pi.items[0].deferred_expense_account = deferred_expense_account
|
||||
pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
|
||||
pi.items[0].deferred_expense_account = self.deferred_expense_account
|
||||
pi.items[0].expense_account = self.expense_account
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
@@ -203,7 +185,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
start_date="2021-05-01",
|
||||
end_date="2021-08-01",
|
||||
type="Expense",
|
||||
company="_Test Company DR",
|
||||
company=self.company,
|
||||
)
|
||||
)
|
||||
pda.insert()
|
||||
@@ -213,7 +195,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2021-05-01",
|
||||
"period_end_date": "2021-08-01",
|
||||
@@ -235,52 +217,31 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
@change_settings("Accounts Settings", {"book_deferred_entries_based_on": "Months"})
|
||||
def test_zero_months(self):
|
||||
self.clear_old_entries()
|
||||
# created deferred expense accounts, if not found
|
||||
deferred_revenue_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.book_deferred_entries_based_on = "Months"
|
||||
acc_settings.save()
|
||||
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test Customer DR"
|
||||
customer.type = "Individual"
|
||||
customer.insert()
|
||||
|
||||
item = create_item(
|
||||
"_Test Internet Subscription",
|
||||
is_stock_item=0,
|
||||
warehouse="All Warehouses - _CD",
|
||||
company="_Test Company DR",
|
||||
)
|
||||
self.create_item("_Test Internet Subscription", 0, self.warehouse, self.company)
|
||||
item = frappe.get_doc("Item", self.item)
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_revenue_account
|
||||
item.deferred_revenue_account = self.deferred_revenue_account
|
||||
item.no_of_months = 0
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(
|
||||
item=item.name,
|
||||
company="_Test Company DR",
|
||||
customer="_Test Customer DR",
|
||||
debit_to="Debtors - _CD",
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date="2021-05-01",
|
||||
parent_cost_center="Main - _CD",
|
||||
cost_center="Main - _CD",
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
do_not_save=True,
|
||||
rate=300,
|
||||
price_list_rate=300,
|
||||
)
|
||||
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].income_account = "Sales - _CD"
|
||||
si.items[0].deferred_revenue_account = deferred_revenue_account
|
||||
si.items[0].income_account = "Sales - _CD"
|
||||
si.items[0].income_account = self.income_account
|
||||
si.items[0].deferred_revenue_account = self.deferred_revenue_account
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
@@ -291,7 +252,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
start_date="2021-05-01",
|
||||
end_date="2021-08-01",
|
||||
type="Income",
|
||||
company="_Test Company DR",
|
||||
company=self.company,
|
||||
)
|
||||
)
|
||||
pda.insert()
|
||||
@@ -301,7 +262,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
|
||||
self.filters = frappe._dict(
|
||||
{
|
||||
"company": frappe.defaults.get_user_default("Company"),
|
||||
"company": self.company,
|
||||
"filter_based_on": "Date Range",
|
||||
"period_start_date": "2021-05-01",
|
||||
"period_end_date": "2021-08-01",
|
||||
@@ -322,30 +283,3 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||
]
|
||||
self.assertEqual(report.period_total, expected)
|
||||
|
||||
|
||||
def create_company():
|
||||
company = frappe.db.exists("Company", "_Test Company DR")
|
||||
if not company:
|
||||
company = frappe.new_doc("Company")
|
||||
company.company_name = "_Test Company DR"
|
||||
company.default_currency = "INR"
|
||||
company.chart_of_accounts = "Standard"
|
||||
company.insert()
|
||||
|
||||
|
||||
def clear_accounts_and_items():
|
||||
item = qb.DocType("Item")
|
||||
account = qb.DocType("Account")
|
||||
customer = qb.DocType("Customer")
|
||||
supplier = qb.DocType("Supplier")
|
||||
|
||||
qb.from_(account).delete().where(
|
||||
(account.account_name == "Deferred Revenue")
|
||||
| (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
|
||||
).run()
|
||||
qb.from_(item).delete().where(
|
||||
(item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
|
||||
).run()
|
||||
qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
|
||||
qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
|
||||
|
||||
@@ -188,6 +188,7 @@ def get_data(
|
||||
filters,
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=ignore_closing_entries,
|
||||
root_type=root_type,
|
||||
)
|
||||
|
||||
calculate_values(
|
||||
@@ -417,23 +418,44 @@ def set_gl_entries_by_account(
|
||||
gl_entries_by_account,
|
||||
ignore_closing_entries=False,
|
||||
ignore_opening_entries=False,
|
||||
root_type=None,
|
||||
):
|
||||
"""Returns a dict like { "account": [gl entries], ... }"""
|
||||
gl_entries = []
|
||||
|
||||
account_filters = {
|
||||
"company": company,
|
||||
"is_group": 0,
|
||||
"lft": (">=", root_lft),
|
||||
"rgt": ("<=", root_rgt),
|
||||
}
|
||||
|
||||
if root_type:
|
||||
account_filters.update(
|
||||
{
|
||||
"root_type": root_type,
|
||||
}
|
||||
)
|
||||
|
||||
accounts_list = frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": company, "is_group": 0, "lft": (">=", root_lft), "rgt": ("<=", root_rgt)},
|
||||
filters=account_filters,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if accounts_list:
|
||||
# For balance sheet
|
||||
if not from_date:
|
||||
from_date = filters["period_start_date"]
|
||||
ignore_closing_balances = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_account_closing_balance"
|
||||
)
|
||||
if not from_date and not ignore_closing_balances:
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", from_date)},
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"company": filters.company,
|
||||
"posting_date": ("<", filters["period_start_date"]),
|
||||
},
|
||||
fields=["posting_date", "name"],
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname":"account",
|
||||
"label": __("Account"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"options": "Account",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Account', txt, {
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
account_type: ['in', ["Receivable", "Payable"]]
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"voucher_no",
|
||||
"label": __("Voucher No"),
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["General and Payment Ledger Comparison"] = {
|
||||
"filters": get_filters()
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-08-02 17:30:29.494907",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2023-08-02 17:30:29.494907",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "General and Payment Ledger Comparison",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "General and Payment Ledger Comparison",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"role": "Auditor"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
|
||||
class General_Payment_Ledger_Comparison(object):
|
||||
"""
|
||||
A Utility report to compare Voucher-wise balance between General and Payment Ledger
|
||||
"""
|
||||
|
||||
def __init__(self, filters=None):
|
||||
self.filters = filters
|
||||
self.gle = []
|
||||
self.ple = []
|
||||
|
||||
def get_accounts(self):
|
||||
receivable_accounts = [
|
||||
x[0]
|
||||
for x in frappe.db.get_all(
|
||||
"Account",
|
||||
filters={"company": self.filters.company, "account_type": "Receivable"},
|
||||
as_list=True,
|
||||
)
|
||||
]
|
||||
payable_accounts = [
|
||||
x[0]
|
||||
for x in frappe.db.get_all(
|
||||
"Account", filters={"company": self.filters.company, "account_type": "Payable"}, as_list=True
|
||||
)
|
||||
]
|
||||
|
||||
self.account_types = frappe._dict(
|
||||
{
|
||||
"receivable": frappe._dict({"accounts": receivable_accounts, "gle": [], "ple": []}),
|
||||
"payable": frappe._dict({"accounts": payable_accounts, "gle": [], "ple": []}),
|
||||
}
|
||||
)
|
||||
|
||||
def generate_filters(self):
|
||||
if self.filters.account:
|
||||
self.account_types.receivable.accounts = []
|
||||
self.account_types.payable.accounts = []
|
||||
|
||||
for acc in frappe.db.get_all(
|
||||
"Account", filters={"name": ["in", self.filters.account]}, fields=["name", "account_type"]
|
||||
):
|
||||
if acc.account_type == "Receivable":
|
||||
self.account_types.receivable.accounts.append(acc.name)
|
||||
else:
|
||||
self.account_types.payable.accounts.append(acc.name)
|
||||
|
||||
def get_gle(self):
|
||||
gle = qb.DocType("GL Entry")
|
||||
|
||||
for acc_type, val in self.account_types.items():
|
||||
if val.accounts:
|
||||
|
||||
filter_criterion = []
|
||||
if self.filters.voucher_no:
|
||||
filter_criterion.append((gle.voucher_no == self.filters.voucher_no))
|
||||
|
||||
if self.filters.period_start_date:
|
||||
filter_criterion.append(gle.posting_date.gte(self.filters.period_start_date))
|
||||
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(gle.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
if acc_type == "receivable":
|
||||
outstanding = (Sum(gle.debit) - Sum(gle.credit)).as_("outstanding")
|
||||
else:
|
||||
outstanding = (Sum(gle.credit) - Sum(gle.debit)).as_("outstanding")
|
||||
|
||||
self.account_types[acc_type].gle = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
gle.company,
|
||||
gle.account,
|
||||
gle.voucher_no,
|
||||
gle.party,
|
||||
outstanding,
|
||||
)
|
||||
.where(
|
||||
(gle.company == self.filters.company)
|
||||
& (gle.is_cancelled == 0)
|
||||
& (gle.account.isin(val.accounts))
|
||||
)
|
||||
.where(Criterion.all(filter_criterion))
|
||||
.groupby(gle.company, gle.account, gle.voucher_no, gle.party)
|
||||
.run()
|
||||
)
|
||||
|
||||
def get_ple(self):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
for acc_type, val in self.account_types.items():
|
||||
if val.accounts:
|
||||
|
||||
filter_criterion = []
|
||||
if self.filters.voucher_no:
|
||||
filter_criterion.append((ple.voucher_no == self.filters.voucher_no))
|
||||
|
||||
if self.filters.period_start_date:
|
||||
filter_criterion.append(ple.posting_date.gte(self.filters.period_start_date))
|
||||
|
||||
if self.filters.period_end_date:
|
||||
filter_criterion.append(ple.posting_date.lte(self.filters.period_end_date))
|
||||
|
||||
self.account_types[acc_type].ple = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company, ple.account, ple.voucher_no, ple.party, Sum(ple.amount).as_("outstanding")
|
||||
)
|
||||
.where(
|
||||
(ple.company == self.filters.company)
|
||||
& (ple.delinked == 0)
|
||||
& (ple.account.isin(val.accounts))
|
||||
)
|
||||
.where(Criterion.all(filter_criterion))
|
||||
.groupby(ple.company, ple.account, ple.voucher_no, ple.party)
|
||||
.run()
|
||||
)
|
||||
|
||||
def compare(self):
|
||||
self.gle_balances = set()
|
||||
self.ple_balances = set()
|
||||
|
||||
# consolidate both receivable and payable balances in one set
|
||||
for acc_type, val in self.account_types.items():
|
||||
self.gle_balances = set(val.gle) | self.gle_balances
|
||||
self.ple_balances = set(val.ple) | self.ple_balances
|
||||
|
||||
self.diff1 = self.gle_balances.difference(self.ple_balances)
|
||||
self.diff2 = self.ple_balances.difference(self.gle_balances)
|
||||
self.diff = frappe._dict({})
|
||||
|
||||
for x in self.diff1:
|
||||
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
||||
|
||||
for x in self.diff2:
|
||||
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
|
||||
|
||||
def generate_data(self):
|
||||
self.data = []
|
||||
for key, val in self.diff.items():
|
||||
self.data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"voucher_no": key[2],
|
||||
"party": key[3],
|
||||
"gl_balance": val.gl_balance,
|
||||
"pl_balance": val.pl_balance,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
options = None
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Voucher No"),
|
||||
fieldname="voucher_no",
|
||||
fieldtype="Data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Party"),
|
||||
fieldname="party",
|
||||
fieldtype="Data",
|
||||
options=options,
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("GL Balance"),
|
||||
fieldname="gl_balance",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
self.columns.append(
|
||||
dict(
|
||||
label=_("Payment Ledger Balance"),
|
||||
fieldname="pl_balance",
|
||||
fieldtype="Currency",
|
||||
options="Company:company:default_currency",
|
||||
width="100",
|
||||
)
|
||||
)
|
||||
|
||||
def run(self):
|
||||
self.get_accounts()
|
||||
self.generate_filters()
|
||||
self.get_gle()
|
||||
self.get_ple()
|
||||
self.compare()
|
||||
self.generate_data()
|
||||
self.get_columns()
|
||||
|
||||
return self.columns, self.data
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
|
||||
rpt = General_Payment_Ledger_Comparison(filters)
|
||||
columns, data = rpt.run()
|
||||
|
||||
return columns, data
|
||||
@@ -0,0 +1,100 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.report.general_and_payment_ledger_comparison.general_and_payment_ledger_comparison import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestGeneralAndPaymentLedger(FrappeTestCase, AccountsTestMixin):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.cleanup()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def cleanup(self):
|
||||
doctypes = []
|
||||
doctypes.append(qb.DocType("GL Entry"))
|
||||
doctypes.append(qb.DocType("Payment Ledger Entry"))
|
||||
doctypes.append(qb.DocType("Sales Invoice"))
|
||||
|
||||
for doctype in doctypes:
|
||||
qb.from_(doctype).delete().where(doctype.company == self.company).run()
|
||||
|
||||
def test_01_basic_report_functionality(self):
|
||||
sinv = create_sales_invoice(
|
||||
company=self.company,
|
||||
debit_to=self.debit_to,
|
||||
expense_account=self.expense_account,
|
||||
cost_center=self.cost_center,
|
||||
income_account=self.income_account,
|
||||
warehouse=self.warehouse,
|
||||
)
|
||||
|
||||
# manually edit the payment ledger entry
|
||||
ple = frappe.db.get_all(
|
||||
"Payment Ledger Entry", filters={"voucher_no": sinv.name, "delinked": 0}
|
||||
)[0]
|
||||
frappe.db.set_value("Payment Ledger Entry", ple.name, "amount", sinv.grand_total - 1)
|
||||
|
||||
filters = frappe._dict({"company": self.company})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
|
||||
expected = {
|
||||
"voucher_no": sinv.name,
|
||||
"party": sinv.customer,
|
||||
"gl_balance": sinv.grand_total,
|
||||
"pl_balance": sinv.grand_total - 1,
|
||||
}
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
# account filter
|
||||
filters = frappe._dict({"company": self.company, "account": self.debit_to})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict({"company": self.company, "account": self.creditors})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
|
||||
# voucher_no filter
|
||||
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict({"company": self.company, "voucher_no": sinv.name + "-1"})
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
|
||||
# date range filter
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"period_start_date": sinv.posting_date,
|
||||
"period_end_date": sinv.posting_date,
|
||||
}
|
||||
)
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual(len(data), 1)
|
||||
self.assertEqual(expected, data[0])
|
||||
|
||||
filters = frappe._dict(
|
||||
{
|
||||
"company": self.company,
|
||||
"period_start_date": add_days(sinv.posting_date, -1),
|
||||
"period_end_date": add_days(sinv.posting_date, -1),
|
||||
}
|
||||
)
|
||||
columns, data = execute(filters=filters)
|
||||
self.assertEqual([], data)
|
||||
@@ -50,20 +50,20 @@ def get_pos_entries(filters, group_by_field):
|
||||
order_by = "p.posting_date"
|
||||
select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", ""
|
||||
if group_by_field == "mode_of_payment":
|
||||
select_mop_field = ", sip.mode_of_payment"
|
||||
select_mop_field = ", sip.mode_of_payment, sip.base_amount - IF(sip.type='Cash', p.change_amount, 0) as paid_amount"
|
||||
from_sales_invoice_payment = ", `tabSales Invoice Payment` sip"
|
||||
group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount, 0) != 0 AND"
|
||||
group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount - IF(sip.type='Cash', p.change_amount, 0), 0) != 0 AND"
|
||||
order_by += ", sip.mode_of_payment"
|
||||
|
||||
elif group_by_field:
|
||||
order_by += ", p.{}".format(group_by_field)
|
||||
select_mop_field = ", p.base_paid_amount - p.change_amount as paid_amount "
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
p.posting_date, p.name as pos_invoice, p.pos_profile,
|
||||
p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount as paid_amount,
|
||||
p.customer, p.is_return {select_mop_field}
|
||||
p.owner, p.customer, p.is_return, p.base_grand_total as grand_total {select_mop_field}
|
||||
FROM
|
||||
`tabPOS Invoice` p {from_sales_invoice_payment}
|
||||
WHERE
|
||||
|
||||
@@ -12,10 +12,28 @@ frappe.query_reports["TDS Payable Monthly"] = {
|
||||
"default": frappe.defaults.get_default('company')
|
||||
},
|
||||
{
|
||||
"fieldname":"supplier",
|
||||
"label": __("Supplier"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier",
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["Supplier", "Customer"],
|
||||
"reqd": 1,
|
||||
"default": "Supplier",
|
||||
"on_change": function(){
|
||||
frappe.query_report.set_filter_value("party", "");
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"get_options": function() {
|
||||
var party_type = frappe.query_report.get_filter_value('party_type');
|
||||
var party = frappe.query_report.get_filter_value('party');
|
||||
if(party && !party_type) {
|
||||
frappe.throw(__("Please select Party Type first"));
|
||||
}
|
||||
return party_type;
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
|
||||
@@ -33,77 +33,94 @@ def validate_filters(filters):
|
||||
def get_result(
|
||||
filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map, invoice_net_total_map
|
||||
):
|
||||
supplier_map = get_supplier_pan_map()
|
||||
party_map = get_party_pan_map(filters.get("party_type"))
|
||||
tax_rate_map = get_tax_rate_map(filters)
|
||||
gle_map = get_gle_map(tds_docs)
|
||||
|
||||
out = []
|
||||
for name, details in gle_map.items():
|
||||
tds_deducted, total_amount_credited = 0, 0
|
||||
tax_amount, total_amount = 0, 0
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
for entry in details:
|
||||
supplier = entry.party or entry.against
|
||||
party = entry.party or entry.against
|
||||
posting_date = entry.posting_date
|
||||
voucher_type = entry.voucher_type
|
||||
|
||||
if voucher_type == "Journal Entry":
|
||||
suppliers = journal_entry_party_map.get(name)
|
||||
if suppliers:
|
||||
supplier = suppliers[0]
|
||||
party_list = journal_entry_party_map.get(name)
|
||||
if party_list:
|
||||
party = party_list[0]
|
||||
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
if entry.account in tds_accounts:
|
||||
tds_deducted += entry.credit - entry.debit
|
||||
tax_amount += entry.credit - entry.debit
|
||||
|
||||
if invoice_net_total_map.get(name):
|
||||
total_amount_credited = invoice_net_total_map.get(name)
|
||||
total_amount = invoice_net_total_map.get(name)
|
||||
else:
|
||||
total_amount_credited += entry.credit
|
||||
total_amount += entry.credit
|
||||
|
||||
if tax_amount:
|
||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
||||
party_name = "supplier_name"
|
||||
party_type = "supplier_type"
|
||||
table_name = "Supplier"
|
||||
else:
|
||||
party_name = "customer_name"
|
||||
party_type = "customer_type"
|
||||
table_name = "Customer"
|
||||
|
||||
if tds_deducted:
|
||||
row = {
|
||||
"pan"
|
||||
if frappe.db.has_column("Supplier", "pan")
|
||||
else "tax_id": supplier_map.get(supplier, {}).get("pan"),
|
||||
"supplier": supplier_map.get(supplier, {}).get("name"),
|
||||
if frappe.db.has_column(table_name, "pan")
|
||||
else "tax_id": party_map.get(party, {}).get("pan"),
|
||||
"party": party_map.get(party, {}).get("name"),
|
||||
}
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
row.update({"supplier_name": supplier_map.get(supplier, {}).get("supplier_name")})
|
||||
row.update({"party_name": party_map.get(party, {}).get(party_name)})
|
||||
|
||||
row.update(
|
||||
{
|
||||
"section_code": tax_withholding_category,
|
||||
"entity_type": supplier_map.get(supplier, {}).get("supplier_type"),
|
||||
"tds_rate": rate,
|
||||
"total_amount_credited": total_amount_credited,
|
||||
"tds_deducted": tds_deducted,
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"tax_amount": tax_amount,
|
||||
"transaction_date": posting_date,
|
||||
"transaction_type": voucher_type,
|
||||
"ref_no": name,
|
||||
}
|
||||
)
|
||||
|
||||
out.append(row)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def get_supplier_pan_map():
|
||||
supplier_map = frappe._dict()
|
||||
suppliers = frappe.db.get_all(
|
||||
"Supplier", fields=["name", "pan", "supplier_type", "supplier_name", "tax_withholding_category"]
|
||||
)
|
||||
def get_party_pan_map(party_type):
|
||||
party_map = frappe._dict()
|
||||
|
||||
for d in suppliers:
|
||||
supplier_map[d.name] = d
|
||||
fields = ["name", "tax_withholding_category"]
|
||||
if party_type == "Supplier":
|
||||
fields += ["supplier_type", "supplier_name"]
|
||||
else:
|
||||
fields += ["customer_type", "customer_name"]
|
||||
|
||||
return supplier_map
|
||||
if frappe.db.has_column(party_type, "pan"):
|
||||
fields.append("pan")
|
||||
|
||||
party_details = frappe.db.get_all(party_type, fields=fields)
|
||||
|
||||
for party in party_details:
|
||||
party.party_type = party_type
|
||||
party_map[party.name] = party
|
||||
|
||||
return party_map
|
||||
|
||||
|
||||
def get_gle_map(documents):
|
||||
@@ -131,17 +148,17 @@ def get_columns(filters):
|
||||
columns = [
|
||||
{"label": _(frappe.unscrub(pan)), "fieldname": pan, "fieldtype": "Data", "width": 90},
|
||||
{
|
||||
"label": _("Supplier"),
|
||||
"options": "Supplier",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"label": _(filters.get("party_type")),
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"width": 180,
|
||||
},
|
||||
]
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
columns.append(
|
||||
{"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 180}
|
||||
{"label": _("Party Name"), "fieldname": "party_name", "fieldtype": "Data", "width": 180}
|
||||
)
|
||||
|
||||
columns.extend(
|
||||
@@ -153,17 +170,22 @@ def get_columns(filters):
|
||||
"fieldtype": "Link",
|
||||
"width": 180,
|
||||
},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 180},
|
||||
{"label": _("TDS Rate %"), "fieldname": "tds_rate", "fieldtype": "Percent", "width": 90},
|
||||
{"label": _("Entity Type"), "fieldname": "entity_type", "fieldtype": "Data", "width": 120},
|
||||
{
|
||||
"label": _("Total Amount Credited"),
|
||||
"fieldname": "total_amount_credited",
|
||||
"label": _("TDS Rate %") if filters.get("party_type") == "Supplier" else _("TCS Rate %"),
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Percent",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Total Amount"),
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
{
|
||||
"label": _("Amount of TDS Deducted"),
|
||||
"fieldname": "tds_deducted",
|
||||
"label": _("TDS Amount") if filters.get("party_type") == "Supplier" else _("TCS Amount"),
|
||||
"fieldname": "tax_amount",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
},
|
||||
@@ -173,13 +195,13 @@ def get_columns(filters):
|
||||
"fieldtype": "Date",
|
||||
"width": 90,
|
||||
},
|
||||
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 90},
|
||||
{"label": _("Transaction Type"), "fieldname": "transaction_type", "width": 100},
|
||||
{
|
||||
"label": _("Reference No."),
|
||||
"fieldname": "ref_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "transaction_type",
|
||||
"width": 90,
|
||||
"width": 180,
|
||||
},
|
||||
]
|
||||
)
|
||||
@@ -190,6 +212,7 @@ def get_columns(filters):
|
||||
def get_tds_docs(filters):
|
||||
tds_documents = []
|
||||
purchase_invoices = []
|
||||
sales_invoices = []
|
||||
payment_entries = []
|
||||
journal_entries = []
|
||||
tax_category_map = frappe._dict()
|
||||
@@ -209,10 +232,13 @@ def get_tds_docs(filters):
|
||||
"against": ("not in", bank_accounts),
|
||||
}
|
||||
|
||||
if filters.get("supplier"):
|
||||
party = frappe.get_all(filters.get("party_type"), pluck="name")
|
||||
query_filters.update({"against": ("in", party)})
|
||||
|
||||
if filters.get("party"):
|
||||
del query_filters["account"]
|
||||
del query_filters["against"]
|
||||
or_filters = {"against": filters.get("supplier"), "party": filters.get("supplier")}
|
||||
or_filters = {"against": filters.get("party"), "party": filters.get("party")}
|
||||
|
||||
tds_docs = frappe.get_all(
|
||||
"GL Entry",
|
||||
@@ -224,6 +250,8 @@ def get_tds_docs(filters):
|
||||
for d in tds_docs:
|
||||
if d.voucher_type == "Purchase Invoice":
|
||||
purchase_invoices.append(d.voucher_no)
|
||||
if d.voucher_type == "Sales Invoice":
|
||||
sales_invoices.append(d.voucher_no)
|
||||
elif d.voucher_type == "Payment Entry":
|
||||
payment_entries.append(d.voucher_no)
|
||||
elif d.voucher_type == "Journal Entry":
|
||||
@@ -234,6 +262,9 @@ def get_tds_docs(filters):
|
||||
if purchase_invoices:
|
||||
get_doc_info(purchase_invoices, "Purchase Invoice", tax_category_map, invoice_net_total_map)
|
||||
|
||||
if sales_invoices:
|
||||
get_doc_info(sales_invoices, "Sales Invoice", tax_category_map, invoice_net_total_map)
|
||||
|
||||
if payment_entries:
|
||||
get_doc_info(payment_entries, "Payment Entry", tax_category_map)
|
||||
|
||||
@@ -267,6 +298,8 @@ def get_journal_entry_party_map(journal_entries):
|
||||
def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None):
|
||||
if doctype == "Purchase Invoice":
|
||||
fields = ["name", "tax_withholding_category", "base_tax_withholding_net_total"]
|
||||
if doctype == "Sales Invoice":
|
||||
fields = ["name", "base_net_total"]
|
||||
else:
|
||||
fields = ["name", "tax_withholding_category"]
|
||||
|
||||
@@ -276,6 +309,8 @@ def get_doc_info(vouchers, doctype, tax_category_map, invoice_net_total_map=None
|
||||
tax_category_map.update({entry.name: entry.tax_withholding_category})
|
||||
if doctype == "Purchase Invoice":
|
||||
invoice_net_total_map.update({entry.name: entry.base_tax_withholding_net_total})
|
||||
if doctype == "Sales Invoice":
|
||||
invoice_net_total_map.update({entry.name: entry.base_net_total})
|
||||
|
||||
|
||||
def get_tax_rate_map(filters):
|
||||
|
||||
@@ -142,14 +142,20 @@ def get_opening_balances(filters):
|
||||
def get_rootwise_opening_balances(filters, report_type):
|
||||
gle = []
|
||||
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
|
||||
fields=["posting_date", "name"],
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
last_period_closing_voucher = ""
|
||||
ignore_closing_balances = frappe.db.get_single_value(
|
||||
"Accounts Settings", "ignore_account_closing_balance"
|
||||
)
|
||||
|
||||
if not ignore_closing_balances:
|
||||
last_period_closing_voucher = frappe.db.get_all(
|
||||
"Period Closing Voucher",
|
||||
filters={"docstatus": 1, "company": filters.company, "posting_date": ("<", filters.from_date)},
|
||||
fields=["posting_date", "name"],
|
||||
order_by="posting_date desc",
|
||||
limit=1,
|
||||
)
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
|
||||
if last_period_closing_voucher:
|
||||
|
||||
80
erpnext/accounts/test/accounts_mixin.py
Normal file
80
erpnext/accounts/test/accounts_mixin.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class AccountsTestMixin:
|
||||
def create_customer(self, customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = customer_name
|
||||
customer.type = "Individual"
|
||||
|
||||
if currency:
|
||||
customer.default_currency = currency
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
else:
|
||||
self.customer = customer_name
|
||||
|
||||
def create_supplier(self, supplier_name, currency=None):
|
||||
if not frappe.db.exists("Supplier", supplier_name):
|
||||
supplier = frappe.new_doc("Supplier")
|
||||
supplier.supplier_name = supplier_name
|
||||
supplier.supplier_type = "Individual"
|
||||
supplier.supplier_group = "Local"
|
||||
|
||||
if currency:
|
||||
supplier.default_currency = currency
|
||||
supplier.save()
|
||||
self.supplier = supplier.name
|
||||
else:
|
||||
self.supplier = supplier_name
|
||||
|
||||
def create_item(self, item_name, is_stock=0, warehouse=None, company=None):
|
||||
item = create_item(item_name, is_stock_item=is_stock, warehouse=warehouse, company=company)
|
||||
self.item = item.name
|
||||
|
||||
def create_company(self, company_name="_Test Company", abbr="_TC"):
|
||||
self.company_abbr = abbr
|
||||
if frappe.db.exists("Company", company_name):
|
||||
company = frappe.get_doc("Company", company_name)
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": company_name,
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "Stores - " + abbr
|
||||
self.finished_warehouse = "Finished Goods - " + abbr
|
||||
self.income_account = "Sales - " + abbr
|
||||
self.expense_account = "Cost of Goods Sold - " + abbr
|
||||
self.debit_to = "Debtors - " + abbr
|
||||
self.debit_usd = "Debtors USD - " + abbr
|
||||
self.cash = "Cash - " + abbr
|
||||
self.creditors = "Creditors - " + abbr
|
||||
|
||||
# create bank account
|
||||
bank_account = "HDFC - " + abbr
|
||||
if frappe.db.exists("Account", bank_account):
|
||||
self.bank = bank_account
|
||||
else:
|
||||
bank_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - " + abbr,
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
bank_acc.save()
|
||||
self.bank = bank_acc.name
|
||||
@@ -570,7 +570,11 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
# new row with references
|
||||
new_row = journal_entry.append("accounts")
|
||||
|
||||
new_row.update((frappe.copy_doc(jv_detail)).as_dict())
|
||||
# Copy field values into new row
|
||||
[
|
||||
new_row.set(field, jv_detail.get(field))
|
||||
for field in frappe.get_meta("Journal Entry Account").get_fieldnames_with_value()
|
||||
]
|
||||
|
||||
new_row.set(d["dr_or_cr"], d["allocated_amount"])
|
||||
new_row.set(
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"column_break_33",
|
||||
"opening_accumulated_depreciation",
|
||||
"number_of_depreciations_booked",
|
||||
"is_fully_depreciated",
|
||||
"section_break_36",
|
||||
"finance_books",
|
||||
"section_break_33",
|
||||
@@ -205,6 +206,7 @@
|
||||
"fieldname": "disposal_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Disposal Date",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -244,19 +246,17 @@
|
||||
"label": "Is Existing Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_existing_asset",
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "opening_accumulated_depreciation",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Opening Accumulated Depreciation",
|
||||
"no_copy": 1,
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.is_existing_asset && doc.opening_accumulated_depreciation)",
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "number_of_depreciations_booked",
|
||||
"fieldtype": "Int",
|
||||
"label": "Number of Depreciations Booked",
|
||||
"no_copy": 1
|
||||
"label": "Number of Depreciations Booked"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -502,6 +502,13 @@
|
||||
"options": "\nSuccessful\nFailed",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:(doc.is_existing_asset)",
|
||||
"fieldname": "is_fully_depreciated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Fully Depreciated"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -530,7 +537,7 @@
|
||||
"table_fieldname": "accounts"
|
||||
}
|
||||
],
|
||||
"modified": "2023-03-30 15:07:41.542374",
|
||||
"modified": "2023-07-28 15:47:01.137996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
@@ -574,4 +581,4 @@
|
||||
"states": [],
|
||||
"title_field": "asset_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -148,17 +148,33 @@ class Asset(AccountsController):
|
||||
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center:
|
||||
return
|
||||
|
||||
cost_center_company = frappe.db.get_value("Cost Center", self.cost_center, "company")
|
||||
if cost_center_company != self.company:
|
||||
frappe.throw(
|
||||
_("Selected Cost Center {} doesn't belongs to {}").format(
|
||||
frappe.bold(self.cost_center), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
if self.cost_center:
|
||||
cost_center_company, cost_center_is_group = frappe.db.get_value(
|
||||
"Cost Center", self.cost_center, ["company", "is_group"]
|
||||
)
|
||||
if cost_center_company != self.company:
|
||||
frappe.throw(
|
||||
_("Cost Center {} doesn't belong to Company {}").format(
|
||||
frappe.bold(self.cost_center), frappe.bold(self.company)
|
||||
),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
if cost_center_is_group:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cost Center {} is a group cost center and group cost centers cannot be used in transactions"
|
||||
).format(frappe.bold(self.cost_center)),
|
||||
title=_("Invalid Cost Center"),
|
||||
)
|
||||
|
||||
else:
|
||||
if not frappe.get_cached_value("Company", self.company, "depreciation_cost_center"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
|
||||
).format(frappe.bold(self.company)),
|
||||
title=_("Missing Cost Center"),
|
||||
)
|
||||
|
||||
def validate_in_use_date(self):
|
||||
if not self.available_for_use_date:
|
||||
@@ -207,8 +223,11 @@ class Asset(AccountsController):
|
||||
|
||||
if not self.calculate_depreciation:
|
||||
return
|
||||
elif not self.finance_books:
|
||||
frappe.throw(_("Enter depreciation details"))
|
||||
else:
|
||||
if not self.finance_books:
|
||||
frappe.throw(_("Enter depreciation details"))
|
||||
if self.is_fully_depreciated:
|
||||
frappe.throw(_("Depreciation cannot be calculated for fully depreciated assets"))
|
||||
|
||||
if self.is_existing_asset:
|
||||
return
|
||||
@@ -588,7 +607,7 @@ class Asset(AccountsController):
|
||||
depreciable_amount = flt(self.gross_purchase_amount) - flt(row.expected_value_after_useful_life)
|
||||
if flt(self.opening_accumulated_depreciation) > depreciable_amount:
|
||||
frappe.throw(
|
||||
_("Opening Accumulated Depreciation must be less than equal to {0}").format(
|
||||
_("Opening Accumulated Depreciation must be less than or equal to {0}").format(
|
||||
depreciable_amount
|
||||
)
|
||||
)
|
||||
@@ -793,7 +812,9 @@ class Asset(AccountsController):
|
||||
expected_value_after_useful_life = self.finance_books[idx].expected_value_after_useful_life
|
||||
value_after_depreciation = self.finance_books[idx].value_after_depreciation
|
||||
|
||||
if flt(value_after_depreciation) <= expected_value_after_useful_life:
|
||||
if (
|
||||
flt(value_after_depreciation) <= expected_value_after_useful_life or self.is_fully_depreciated
|
||||
):
|
||||
status = "Fully Depreciated"
|
||||
elif flt(value_after_depreciation) < flt(self.gross_purchase_amount):
|
||||
status = "Partially Depreciated"
|
||||
@@ -941,7 +962,9 @@ class Asset(AccountsController):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_manual_depreciation_entries(self):
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(self)
|
||||
(_, _, depreciation_expense_account) = get_depreciation_accounts(
|
||||
self.asset_category, self.company
|
||||
)
|
||||
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
|
||||
@@ -1180,10 +1203,10 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non
|
||||
def make_journal_entry(asset_name):
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
fixed_asset_account,
|
||||
_,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset)
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import Order
|
||||
from frappe.query_builder.functions import Max, Min
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
@@ -36,9 +38,40 @@ def post_depreciation_entries(date=None):
|
||||
failed_asset_names = []
|
||||
error_log_names = []
|
||||
|
||||
for asset_name in get_depreciable_assets(date):
|
||||
depreciable_assets = get_depreciable_assets(date)
|
||||
|
||||
credit_and_debit_accounts_for_asset_category_and_company = {}
|
||||
depreciation_cost_center_and_depreciation_series_for_company = (
|
||||
get_depreciation_cost_center_and_depreciation_series_for_company()
|
||||
)
|
||||
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
|
||||
for asset in depreciable_assets:
|
||||
asset_name, asset_category, asset_company, sch_start_idx, sch_end_idx = asset
|
||||
|
||||
if (
|
||||
asset_category,
|
||||
asset_company,
|
||||
) not in credit_and_debit_accounts_for_asset_category_and_company:
|
||||
credit_and_debit_accounts_for_asset_category_and_company.update(
|
||||
{
|
||||
(asset_category, asset_company): get_credit_and_debit_accounts_for_asset_category_and_company(
|
||||
asset_category, asset_company
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
make_depreciation_entry(asset_name, date)
|
||||
make_depreciation_entry(
|
||||
asset_name,
|
||||
date,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
credit_and_debit_accounts_for_asset_category_and_company[(asset_category, asset_company)],
|
||||
depreciation_cost_center_and_depreciation_series_for_company[asset_company],
|
||||
accounting_dimensions,
|
||||
)
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
@@ -54,115 +87,226 @@ def post_depreciation_entries(date=None):
|
||||
|
||||
|
||||
def get_depreciable_assets(date):
|
||||
return frappe.db.sql_list(
|
||||
"""select distinct a.name
|
||||
from tabAsset a, `tabDepreciation Schedule` ds
|
||||
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
|
||||
and a.status in ('Submitted', 'Partially Depreciated')
|
||||
and ifnull(ds.journal_entry, '')=''""",
|
||||
date,
|
||||
a = frappe.qb.DocType("Asset")
|
||||
ds = frappe.qb.DocType("Depreciation Schedule")
|
||||
|
||||
res = (
|
||||
frappe.qb.from_(a)
|
||||
.join(ds)
|
||||
.on(a.name == ds.parent)
|
||||
.select(a.name, a.asset_category, a.company, Min(ds.idx) - 1, Max(ds.idx))
|
||||
.where(a.calculate_depreciation == 1)
|
||||
.where(a.docstatus == 1)
|
||||
.where(a.status.isin(["Submitted", "Partially Depreciated"]))
|
||||
.where(ds.journal_entry.isnull())
|
||||
.where(ds.schedule_date <= date)
|
||||
.groupby(a.name)
|
||||
.orderby(a.creation, order=Order.desc)
|
||||
)
|
||||
|
||||
acc_frozen_upto = get_acc_frozen_upto()
|
||||
if acc_frozen_upto:
|
||||
res = res.where(ds.schedule_date > acc_frozen_upto)
|
||||
|
||||
res = res.run()
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def get_acc_frozen_upto():
|
||||
acc_frozen_upto = frappe.db.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
if not acc_frozen_upto:
|
||||
return
|
||||
|
||||
frozen_accounts_modifier = frappe.db.get_single_value(
|
||||
"Accounts Settings", "frozen_accounts_modifier"
|
||||
)
|
||||
|
||||
if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
|
||||
return getdate(acc_frozen_upto)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def get_credit_and_debit_accounts_for_asset_category_and_company(asset_category, company):
|
||||
(
|
||||
_,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset_category, company)
|
||||
|
||||
credit_account, debit_account = get_credit_and_debit_accounts(
|
||||
accumulated_depreciation_account, depreciation_expense_account
|
||||
)
|
||||
|
||||
return (credit_account, debit_account)
|
||||
|
||||
|
||||
def get_depreciation_cost_center_and_depreciation_series_for_company():
|
||||
company_names = frappe.db.get_all("Company", pluck="name")
|
||||
|
||||
res = {}
|
||||
|
||||
for company_name in company_names:
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", company_name, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
res.update({company_name: (depreciation_cost_center, depreciation_series)})
|
||||
|
||||
return res
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_depreciation_entry(asset_name, date=None):
|
||||
def make_depreciation_entry(
|
||||
asset_name,
|
||||
date=None,
|
||||
sch_start_idx=None,
|
||||
sch_end_idx=None,
|
||||
credit_and_debit_accounts=None,
|
||||
depreciation_cost_center_and_depreciation_series=None,
|
||||
accounting_dimensions=None,
|
||||
):
|
||||
frappe.has_permission("Journal Entry", throw=True)
|
||||
|
||||
if not date:
|
||||
date = today()
|
||||
|
||||
asset = frappe.get_doc("Asset", asset_name)
|
||||
(
|
||||
fixed_asset_account,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
if credit_and_debit_accounts:
|
||||
credit_account, debit_account = credit_and_debit_accounts
|
||||
else:
|
||||
credit_account, debit_account = get_credit_and_debit_accounts_for_asset_category_and_company(
|
||||
asset.asset_category, asset.company
|
||||
)
|
||||
|
||||
if depreciation_cost_center_and_depreciation_series:
|
||||
depreciation_cost_center, depreciation_series = depreciation_cost_center_and_depreciation_series
|
||||
else:
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
)
|
||||
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
if not accounting_dimensions:
|
||||
accounting_dimensions = get_checks_for_pl_and_bs_accounts()
|
||||
|
||||
for d in asset.get("schedules"):
|
||||
if not d.journal_entry and getdate(d.schedule_date) <= getdate(date):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = d.schedule_date
|
||||
je.company = asset.company
|
||||
je.finance_book = d.finance_book
|
||||
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
|
||||
depreciation_posting_error = None
|
||||
|
||||
credit_account, debit_account = get_credit_and_debit_accounts(
|
||||
accumulated_depreciation_account, depreciation_expense_account
|
||||
for d in asset.get("schedules")[sch_start_idx or 0 : sch_end_idx or len(asset.get("schedules"))]:
|
||||
try:
|
||||
_make_journal_entry_for_depreciation(
|
||||
asset,
|
||||
date,
|
||||
d,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
depreciation_cost_center,
|
||||
depreciation_series,
|
||||
credit_account,
|
||||
debit_account,
|
||||
accounting_dimensions,
|
||||
)
|
||||
|
||||
credit_entry = {
|
||||
"account": credit_account,
|
||||
"credit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"debit_in_account_currency": d.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
|
||||
credit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
|
||||
debit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
je.append("accounts", credit_entry)
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
|
||||
d.db_set("journal_entry", je.name)
|
||||
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
idx = cint(d.finance_book_id)
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation -= d.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
asset.db_set("depr_entry_posting_status", "Successful")
|
||||
frappe.db.commit()
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
depreciation_posting_error = e
|
||||
|
||||
asset.set_status()
|
||||
|
||||
return asset
|
||||
if not depreciation_posting_error:
|
||||
asset.db_set("depr_entry_posting_status", "Successful")
|
||||
return asset
|
||||
|
||||
raise depreciation_posting_error
|
||||
|
||||
|
||||
def get_depreciation_accounts(asset):
|
||||
def _make_journal_entry_for_depreciation(
|
||||
asset,
|
||||
date,
|
||||
depr_schedule,
|
||||
sch_start_idx,
|
||||
sch_end_idx,
|
||||
depreciation_cost_center,
|
||||
depreciation_series,
|
||||
credit_account,
|
||||
debit_account,
|
||||
accounting_dimensions,
|
||||
):
|
||||
if not (sch_start_idx and sch_end_idx) and not (
|
||||
not depr_schedule.journal_entry and getdate(depr_schedule.schedule_date) <= getdate(date)
|
||||
):
|
||||
return
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.voucher_type = "Depreciation Entry"
|
||||
je.naming_series = depreciation_series
|
||||
je.posting_date = depr_schedule.schedule_date
|
||||
je.company = asset.company
|
||||
je.finance_book = depr_schedule.finance_book
|
||||
je.remark = "Depreciation Entry against {0} worth {1}".format(
|
||||
asset.name, depr_schedule.depreciation_amount
|
||||
)
|
||||
|
||||
credit_entry = {
|
||||
"account": credit_account,
|
||||
"credit_in_account_currency": depr_schedule.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"debit_in_account_currency": depr_schedule.depreciation_amount,
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
"cost_center": depreciation_cost_center,
|
||||
}
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_bs"):
|
||||
credit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
if asset.get(dimension["fieldname"]) or dimension.get("mandatory_for_pl"):
|
||||
debit_entry.update(
|
||||
{
|
||||
dimension["fieldname"]: asset.get(dimension["fieldname"])
|
||||
or dimension.get("default_dimension")
|
||||
}
|
||||
)
|
||||
|
||||
je.append("accounts", credit_entry)
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
|
||||
je.flags.ignore_permissions = True
|
||||
je.flags.planned_depr_entry = True
|
||||
je.save()
|
||||
|
||||
depr_schedule.db_set("journal_entry", je.name)
|
||||
|
||||
if not je.meta.get_workflow():
|
||||
je.submit()
|
||||
idx = cint(depr_schedule.finance_book_id)
|
||||
finance_books = asset.get("finance_books")[idx - 1]
|
||||
finance_books.value_after_depreciation -= depr_schedule.depreciation_amount
|
||||
finance_books.db_update()
|
||||
|
||||
|
||||
def get_depreciation_accounts(asset_category, company):
|
||||
fixed_asset_account = accumulated_depreciation_account = depreciation_expense_account = None
|
||||
|
||||
accounts = frappe.db.get_value(
|
||||
"Asset Category Account",
|
||||
filters={"parent": asset.asset_category, "company_name": asset.company},
|
||||
filters={"parent": asset_category, "company_name": company},
|
||||
fieldname=[
|
||||
"fixed_asset_account",
|
||||
"accumulated_depreciation_account",
|
||||
@@ -178,7 +322,7 @@ def get_depreciation_accounts(asset):
|
||||
|
||||
if not accumulated_depreciation_account or not depreciation_expense_account:
|
||||
accounts = frappe.get_cached_value(
|
||||
"Company", asset.company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
"Company", company, ["accumulated_depreciation_account", "depreciation_expense_account"]
|
||||
)
|
||||
|
||||
if not accumulated_depreciation_account:
|
||||
@@ -193,7 +337,7 @@ def get_depreciation_accounts(asset):
|
||||
):
|
||||
frappe.throw(
|
||||
_("Please set Depreciation related Accounts in Asset Category {0} or Company {1}").format(
|
||||
asset.asset_category, asset.company
|
||||
asset_category, company
|
||||
)
|
||||
)
|
||||
|
||||
@@ -369,6 +513,15 @@ def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
|
||||
for account in reverse_journal_entry.accounts:
|
||||
account.update(
|
||||
{
|
||||
"reference_type": "Asset",
|
||||
"reference_name": asset.name,
|
||||
}
|
||||
)
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
@@ -524,8 +677,8 @@ def get_gl_entries_on_asset_disposal(
|
||||
|
||||
|
||||
def get_asset_details(asset, finance_book=None):
|
||||
fixed_asset_account, accumulated_depr_account, depr_expense_account = get_depreciation_accounts(
|
||||
asset
|
||||
fixed_asset_account, accumulated_depr_account, _ = get_depreciation_accounts(
|
||||
asset.asset_category, asset.company
|
||||
)
|
||||
disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(asset.company)
|
||||
depreciation_cost_center = asset.cost_center or depreciation_cost_center
|
||||
|
||||
@@ -325,7 +325,7 @@ class AssetCapitalization(StockController):
|
||||
gl_entries = self.get_gl_entries()
|
||||
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||
make_gl_entries(gl_entries, merge_entries=False, from_repost=from_repost)
|
||||
elif self.docstatus == 2:
|
||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
|
||||
@@ -355,9 +355,6 @@ class AssetCapitalization(StockController):
|
||||
gl_entries, target_account, target_against, precision
|
||||
)
|
||||
|
||||
if not self.stock_items and not self.service_items and self.are_all_asset_items_non_depreciable:
|
||||
return []
|
||||
|
||||
self.get_gl_entries_for_target_item(gl_entries, target_against, precision)
|
||||
|
||||
return gl_entries
|
||||
|
||||
@@ -50,10 +50,10 @@ class AssetValueAdjustment(Document):
|
||||
def make_depreciation_entry(self):
|
||||
asset = frappe.get_doc("Asset", self.asset)
|
||||
(
|
||||
fixed_asset_account,
|
||||
_,
|
||||
accumulated_depreciation_account,
|
||||
depreciation_expense_account,
|
||||
) = get_depreciation_accounts(asset)
|
||||
) = get_depreciation_accounts(asset.asset_category, asset.company)
|
||||
|
||||
depreciation_cost_center, depreciation_series = frappe.get_cached_value(
|
||||
"Company", asset.company, ["depreciation_cost_center", "series_for_depreciation_entry"]
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2019-09-23 16:35:02.836134",
|
||||
"disable_prepared_report": 1,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2019-10-22 13:00:31.539726",
|
||||
"letterhead": null,
|
||||
"modified": "2023-07-26 21:03:20.722628",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Fixed Asset Register",
|
||||
|
||||
@@ -369,7 +369,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "qty"]
|
||||
child_columns: ["item_code", "qty", "ordered_qty"]
|
||||
})
|
||||
}, __("Get Items From"));
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"col_break_email_1",
|
||||
"email_template",
|
||||
"preview",
|
||||
"send_attached_files",
|
||||
"sec_break_email_2",
|
||||
"message_for_supplier",
|
||||
"terms_section_break",
|
||||
@@ -285,13 +286,20 @@
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"label": "Named Place"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, all files attached to this document will be attached to each email",
|
||||
"fieldname": "send_attached_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Attached Files"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-31 23:22:06.684694",
|
||||
"modified": "2023-07-27 16:41:48.468873",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Request for Quotation",
|
||||
|
||||
@@ -209,7 +209,9 @@ class RequestforQuotation(BuyingController):
|
||||
if preview:
|
||||
return message
|
||||
|
||||
attachments = self.get_attachments()
|
||||
attachments = None
|
||||
if self.send_attached_files:
|
||||
attachments = self.get_attachments()
|
||||
|
||||
self.send_email(data, sender, subject, message, attachments)
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
apply_pricing_rule_on_transaction,
|
||||
get_applied_pricing_rules,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import (
|
||||
get_party_account,
|
||||
get_party_account_currency,
|
||||
@@ -1023,6 +1024,33 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
)
|
||||
|
||||
def make_precision_loss_gl_entry(self, gl_entries):
|
||||
round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(
|
||||
self.company, "Purchase Invoice", self.name, self.use_company_roundoff_cost_center
|
||||
)
|
||||
|
||||
precision_loss = self.get("base_net_total") - flt(
|
||||
self.get("net_total") * self.conversion_rate, self.precision("net_total")
|
||||
)
|
||||
|
||||
credit_or_debit = "credit" if self.doctype == "Purchase Invoice" else "debit"
|
||||
against = self.supplier if self.doctype == "Purchase Invoice" else self.customer
|
||||
|
||||
if precision_loss:
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": round_off_account,
|
||||
"against": against,
|
||||
credit_or_debit: precision_loss,
|
||||
"cost_center": round_off_cost_center
|
||||
if self.use_company_roundoff_cost_center
|
||||
else self.cost_center or round_off_cost_center,
|
||||
"remarks": _("Net total calculation precision loss"),
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def update_against_document_in_jv(self):
|
||||
"""
|
||||
Links invoice and advance voucher:
|
||||
@@ -1672,8 +1700,13 @@ class AccountsController(TransactionBase):
|
||||
)
|
||||
self.append("payment_schedule", data)
|
||||
|
||||
allocate_payment_based_on_payment_terms = frappe.db.get_value(
|
||||
"Payment Terms Template", self.payment_terms_template, "allocate_payment_based_on_payment_terms"
|
||||
)
|
||||
|
||||
if not (
|
||||
automatically_fetch_payment_terms
|
||||
and allocate_payment_based_on_payment_terms
|
||||
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype)
|
||||
):
|
||||
for d in self.get("payment_schedule"):
|
||||
@@ -2217,21 +2250,29 @@ def get_advance_payment_entries(
|
||||
reference_condition = ""
|
||||
order_list = []
|
||||
|
||||
if not condition:
|
||||
condition = ""
|
||||
|
||||
payment_entries_against_order = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
'Payment Entry' as reference_type, t1.name as reference_name,
|
||||
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
|
||||
t2.reference_name as against_order, t1.posting_date,
|
||||
t1.{0} as currency, t1.{4} as exchange_rate
|
||||
t1.{0} as currency, t1.{5} as exchange_rate
|
||||
from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
|
||||
where
|
||||
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
|
||||
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
|
||||
and t2.reference_doctype = %s {2}
|
||||
order by t1.posting_date {3}
|
||||
and t2.reference_doctype = %s {2} {3}
|
||||
order by t1.posting_date {4}
|
||||
""".format(
|
||||
currency_field, party_account_field, reference_condition, limit_cond, exchange_rate_field
|
||||
currency_field,
|
||||
party_account_field,
|
||||
reference_condition,
|
||||
condition,
|
||||
limit_cond,
|
||||
exchange_rate_field,
|
||||
),
|
||||
[party_account, payment_type, party_type, party, order_doctype] + order_list,
|
||||
as_dict=1,
|
||||
|
||||
@@ -475,6 +475,10 @@ class BuyingController(SubcontractingController):
|
||||
if d.item_code not in stock_items:
|
||||
continue
|
||||
|
||||
rejected_qty = 0.0
|
||||
if flt(d.rejected_qty) != 0:
|
||||
rejected_qty = flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty"))
|
||||
|
||||
if d.warehouse:
|
||||
pr_qty = flt(flt(d.qty) * flt(d.conversion_factor), d.precision("stock_qty"))
|
||||
|
||||
@@ -495,6 +499,11 @@ class BuyingController(SubcontractingController):
|
||||
},
|
||||
)
|
||||
|
||||
if flt(rejected_qty) != 0:
|
||||
from_warehouse_sle["actual_qty"] += -1 * rejected_qty
|
||||
if d.rejected_serial_no:
|
||||
from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip()
|
||||
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
|
||||
sle = self.get_sl_entries(
|
||||
@@ -520,6 +529,7 @@ class BuyingController(SubcontractingController):
|
||||
else 0,
|
||||
}
|
||||
)
|
||||
|
||||
sl_entries.append(sle)
|
||||
|
||||
if d.from_warehouse and (
|
||||
@@ -530,23 +540,30 @@ class BuyingController(SubcontractingController):
|
||||
d, {"actual_qty": -1 * pr_qty, "warehouse": d.from_warehouse, "recalculate_rate": 1}
|
||||
)
|
||||
|
||||
if flt(rejected_qty) != 0:
|
||||
from_warehouse_sle["actual_qty"] += -1 * rejected_qty
|
||||
if d.rejected_serial_no:
|
||||
from_warehouse_sle["serial_no"] += "\n" + cstr(d.rejected_serial_no).strip()
|
||||
|
||||
sl_entries.append(from_warehouse_sle)
|
||||
|
||||
if flt(d.rejected_qty) != 0:
|
||||
if flt(rejected_qty) != 0:
|
||||
sl_entries.append(
|
||||
self.get_sl_entries(
|
||||
d,
|
||||
{
|
||||
"warehouse": d.rejected_warehouse,
|
||||
"actual_qty": flt(flt(d.rejected_qty) * flt(d.conversion_factor), d.precision("stock_qty")),
|
||||
"actual_qty": rejected_qty,
|
||||
"serial_no": cstr(d.rejected_serial_no).strip(),
|
||||
"incoming_rate": 0.0,
|
||||
"allow_zero_valuation_rate": True,
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
if self.get("is_old_subcontracting_flow"):
|
||||
self.make_sl_entries_for_supplier_warehouse(sl_entries)
|
||||
|
||||
self.make_sl_entries(
|
||||
sl_entries,
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
|
||||
@@ -233,6 +233,9 @@ class StatusUpdater(Document):
|
||||
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
|
||||
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
|
||||
|
||||
if hasattr(d, "item_code") and hasattr(d, "rate") and d.rate < 0:
|
||||
frappe.throw(_("For an item {0}, rate must be a positive number").format(d.item_code))
|
||||
|
||||
if d.doctype == args["source_dt"] and d.get(args["join_field"]):
|
||||
args["name"] = d.get(args["join_field"])
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@ class SubcontractingController(StockController):
|
||||
"allow_zero_valuation": 1,
|
||||
}
|
||||
)
|
||||
rm_obj.rate = bom_item.rate if self.backflush_based_on == "BOM" else get_incoming_rate(args)
|
||||
rm_obj.rate = get_incoming_rate(args)
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
rm_obj.required_qty = qty
|
||||
|
||||
@@ -185,7 +185,7 @@ class Lead(SellingController, CRMNote):
|
||||
"last_name": self.last_name,
|
||||
"salutation": self.salutation,
|
||||
"gender": self.gender,
|
||||
"job_title": self.job_title,
|
||||
"designation": self.job_title,
|
||||
"company_name": self.company_name,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -413,11 +413,11 @@ scheduler_events = {
|
||||
],
|
||||
},
|
||||
"all": [
|
||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||
"erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts",
|
||||
],
|
||||
"hourly": [
|
||||
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
],
|
||||
@@ -432,7 +432,6 @@ scheduler_events = {
|
||||
"erpnext.controllers.accounts_controller.update_invoice_status",
|
||||
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
|
||||
"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
|
||||
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
|
||||
"erpnext.stock.doctype.serial_no.serial_no.update_maintenance_status",
|
||||
"erpnext.buying.doctype.supplier_scorecard.supplier_scorecard.refresh_scorecards",
|
||||
"erpnext.setup.doctype.company.company.cache_companies_monthly_sales_history",
|
||||
@@ -459,6 +458,7 @@ 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.assets.doctype.asset.depreciation.post_depreciation_entries",
|
||||
],
|
||||
"monthly_long": [
|
||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||
|
||||
@@ -93,6 +93,7 @@ class BOMUpdateLog(Document):
|
||||
else:
|
||||
frappe.enqueue(
|
||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||
queue="long",
|
||||
update_doc=self,
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
|
||||
@@ -157,12 +157,19 @@ def get_next_higher_level_boms(
|
||||
def get_leaf_boms() -> List[str]:
|
||||
"Get BOMs that have no dependencies."
|
||||
|
||||
return frappe.db.sql_list(
|
||||
"""select name from `tabBOM` bom
|
||||
where docstatus=1 and is_active=1
|
||||
and not exists(select bom_no from `tabBOM Item`
|
||||
where parent=bom.name and ifnull(bom_no, '')!='')"""
|
||||
)
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
bom_item = frappe.qb.DocType("BOM Item")
|
||||
|
||||
boms = (
|
||||
frappe.qb.from_(bom)
|
||||
.left_join(bom_item)
|
||||
.on((bom.name == bom_item.parent) & (bom_item.bom_no != ""))
|
||||
.select(bom.name)
|
||||
.where((bom.docstatus == 1) & (bom.is_active == 1) & (bom_item.bom_no.isnull()))
|
||||
.distinct()
|
||||
).run(pluck=True)
|
||||
|
||||
return boms
|
||||
|
||||
|
||||
def _generate_dependence_map() -> defaultdict:
|
||||
|
||||
@@ -498,12 +498,12 @@ class JobCard(Document):
|
||||
if self.for_quantity and flt(total_completed_qty, precision) != flt(
|
||||
self.for_quantity, precision
|
||||
):
|
||||
total_completed_qty = bold(_("Total Completed Qty"))
|
||||
total_completed_qty_label = bold(_("Total Completed Qty"))
|
||||
qty_to_manufacture = bold(_("Qty to Manufacture"))
|
||||
|
||||
frappe.throw(
|
||||
_("The {0} ({1}) must be equal to {2} ({3})").format(
|
||||
total_completed_qty,
|
||||
total_completed_qty_label,
|
||||
bold(flt(total_completed_qty, precision)),
|
||||
qty_to_manufacture,
|
||||
bold(self.for_quantity),
|
||||
|
||||
@@ -9,19 +9,25 @@ frappe.ui.form.on('Production Plan', {
|
||||
item.temporary_name = item.name;
|
||||
});
|
||||
},
|
||||
|
||||
setup(frm) {
|
||||
frm.trigger("setup_queries");
|
||||
|
||||
frm.custom_make_buttons = {
|
||||
'Work Order': 'Work Order / Subcontract PO',
|
||||
'Material Request': 'Material Request',
|
||||
};
|
||||
},
|
||||
|
||||
frm.fields_dict['po_items'].grid.get_field('warehouse').get_query = function(doc) {
|
||||
setup_queries(frm) {
|
||||
frm.set_query("sales_order", "sales_orders", () => {
|
||||
return {
|
||||
query: "erpnext.manufacturing.doctype.production_plan.production_plan.sales_order_query",
|
||||
filters: {
|
||||
company: doc.company
|
||||
company: frm.doc.company,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query('for_warehouse', function(doc) {
|
||||
return {
|
||||
@@ -42,32 +48,40 @@ frappe.ui.form.on('Production Plan', {
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) {
|
||||
frm.set_query("item_code", "po_items", (doc, cdt, cdn) => {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters:{
|
||||
'is_stock_item': 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.fields_dict['po_items'].grid.get_field('bom_no').get_query = function(doc, cdt, cdn) {
|
||||
frm.set_query("bom_no", "po_items", (doc, cdt, cdn) => {
|
||||
var d = locals[cdt][cdn];
|
||||
if (d.item_code) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.bom",
|
||||
filters:{'item': cstr(d.item_code), 'docstatus': 1}
|
||||
filters:{'item': d.item_code, 'docstatus': 1}
|
||||
}
|
||||
} else frappe.msgprint(__("Please enter Item first"));
|
||||
}
|
||||
});
|
||||
|
||||
frm.fields_dict['mr_items'].grid.get_field('warehouse').get_query = function(doc) {
|
||||
frm.set_query("warehouse", "mr_items", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query("warehouse", "po_items", (doc) => {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
refresh(frm) {
|
||||
@@ -436,7 +450,7 @@ frappe.ui.form.on("Production Plan Item", {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Material Request Plan Item", {
|
||||
@@ -467,31 +481,36 @@ frappe.ui.form.on("Material Request Plan Item", {
|
||||
|
||||
frappe.ui.form.on("Production Plan Sales Order", {
|
||||
sales_order(frm, cdt, cdn) {
|
||||
const { sales_order } = locals[cdt][cdn];
|
||||
let row = locals[cdt][cdn];
|
||||
const sales_order = row.sales_order;
|
||||
if (!sales_order) {
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details",
|
||||
args: { sales_order },
|
||||
callback(r) {
|
||||
const {transaction_date, customer, grand_total} = r.message;
|
||||
frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date);
|
||||
frappe.model.set_value(cdt, cdn, 'customer', customer);
|
||||
frappe.model.set_value(cdt, cdn, 'grand_total', grand_total);
|
||||
}
|
||||
});
|
||||
|
||||
if (row.sales_order) {
|
||||
frm.call({
|
||||
method: "validate_sales_orders",
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
sales_order: row.sales_order,
|
||||
},
|
||||
callback(r) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_so_details",
|
||||
args: { sales_order },
|
||||
callback(r) {
|
||||
const {transaction_date, customer, grand_total} = r.message;
|
||||
frappe.model.set_value(cdt, cdn, 'sales_order_date', transaction_date);
|
||||
frappe.model.set_value(cdt, cdn, 'customer', customer);
|
||||
frappe.model.set_value(cdt, cdn, 'grand_total', grand_total);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.fields_dict['sales_orders'].grid.get_field("sales_order").get_query = function() {
|
||||
return{
|
||||
filters: [
|
||||
['Sales Order','docstatus', '=' ,1]
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
frappe.tour['Production Plan'] = [
|
||||
{
|
||||
fieldname: "get_items_from",
|
||||
|
||||
@@ -228,10 +228,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "To know more about projected quantity, <a href=\"https://erpnext.com/docs/user/manual/en/stock/projected-quantity\" style=\"text-decoration: underline;\" target=\"_blank\">click here</a>.",
|
||||
"description": "If enabled, the system won't create material requests for the available items.",
|
||||
"fieldname": "ignore_existing_ordered_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Existing Projected Quantity"
|
||||
"label": "Ignore Available Stock"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_25",
|
||||
@@ -339,7 +339,7 @@
|
||||
"depends_on": "eval:doc.get_items_from == 'Sales Order'",
|
||||
"fieldname": "combine_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consolidate Items"
|
||||
"label": "Consolidate Sales Order Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_25",
|
||||
@@ -399,7 +399,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "System consider the projected quantity to check available or will be available sub-assembly items ",
|
||||
"description": "If this checkbox is enabled, then the system won\u2019t run the MRP for the available sub-assembly items.",
|
||||
"fieldname": "skip_available_sub_assembly_item",
|
||||
"fieldtype": "Check",
|
||||
"label": "Skip Available Sub Assembly Items"
|
||||
@@ -422,7 +422,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-22 23:36:31.770517",
|
||||
"modified": "2023-07-28 13:37:43.926686",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -39,6 +39,36 @@ class ProductionPlan(Document):
|
||||
self.set_status()
|
||||
self._rename_temporary_references()
|
||||
validate_uom_is_integer(self, "stock_uom", "planned_qty")
|
||||
self.validate_sales_orders()
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_sales_orders(self, sales_order=None):
|
||||
sales_orders = []
|
||||
|
||||
if sales_order:
|
||||
sales_orders.append(sales_order)
|
||||
else:
|
||||
sales_orders = [row.sales_order for row in self.sales_orders if row.sales_order]
|
||||
|
||||
data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders})
|
||||
|
||||
title = _("Production Plan Already Submitted")
|
||||
if not data:
|
||||
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)
|
||||
msg = _("No items are available in sales orders {0} for production").format(sales_orders)
|
||||
|
||||
frappe.throw(msg, title=title)
|
||||
|
||||
data = [d[0] for d in data]
|
||||
|
||||
for sales_order in sales_orders:
|
||||
if sales_order not in data:
|
||||
frappe.throw(
|
||||
_("No items are available in the sales order {0} for production").format(sales_order),
|
||||
title=title,
|
||||
)
|
||||
|
||||
def set_pending_qty_in_row_without_reference(self):
|
||||
"Set Pending Qty in independent rows (not from SO or MR)."
|
||||
@@ -205,6 +235,7 @@ class ProductionPlan(Document):
|
||||
).as_("pending_qty"),
|
||||
so_item.description,
|
||||
so_item.name,
|
||||
so_item.bom_no,
|
||||
)
|
||||
.distinct()
|
||||
.where(
|
||||
@@ -342,7 +373,7 @@ class ProductionPlan(Document):
|
||||
"item_code": data.item_code,
|
||||
"description": data.description or item_details.description,
|
||||
"stock_uom": item_details and item_details.stock_uom or "",
|
||||
"bom_no": item_details and item_details.bom_no or "",
|
||||
"bom_no": data.bom_no or item_details and item_details.bom_no or "",
|
||||
"planned_qty": data.pending_qty,
|
||||
"pending_qty": data.pending_qty,
|
||||
"planned_start_date": now_datetime(),
|
||||
@@ -401,11 +432,50 @@ class ProductionPlan(Document):
|
||||
|
||||
def on_submit(self):
|
||||
self.update_bin_qty()
|
||||
self.update_sales_order()
|
||||
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
self.delete_draft_work_order()
|
||||
self.update_bin_qty()
|
||||
self.update_sales_order()
|
||||
|
||||
def update_sales_order(self):
|
||||
sales_orders = [row.sales_order for row in self.po_items if row.sales_order]
|
||||
if sales_orders:
|
||||
so_wise_planned_qty = self.get_so_wise_planned_qty(sales_orders)
|
||||
|
||||
for row in self.po_items:
|
||||
if not row.sales_order and not row.sales_order_item:
|
||||
continue
|
||||
|
||||
key = (row.sales_order, row.sales_order_item)
|
||||
frappe.db.set_value(
|
||||
"Sales Order Item",
|
||||
row.sales_order_item,
|
||||
"production_plan_qty",
|
||||
flt(so_wise_planned_qty.get(key)),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_so_wise_planned_qty(sales_orders):
|
||||
so_wise_planned_qty = frappe._dict()
|
||||
data = frappe.get_all(
|
||||
"Production Plan Item",
|
||||
fields=["sales_order", "sales_order_item", "SUM(planned_qty) as qty"],
|
||||
filters={
|
||||
"sales_order": ("in", sales_orders),
|
||||
"docstatus": 1,
|
||||
"sales_order_item": ("is", "set"),
|
||||
},
|
||||
group_by="sales_order, sales_order_item",
|
||||
)
|
||||
|
||||
for row in data:
|
||||
key = (row.sales_order, row.sales_order_item)
|
||||
so_wise_planned_qty[key] = row.qty
|
||||
|
||||
return so_wise_planned_qty
|
||||
|
||||
def update_bin_qty(self):
|
||||
for d in self.mr_items:
|
||||
@@ -719,6 +789,9 @@ class ProductionPlan(Document):
|
||||
sub_assembly_items_store = [] # temporary store to process all subassembly items
|
||||
|
||||
for row in self.po_items:
|
||||
if self.skip_available_sub_assembly_item and not row.warehouse:
|
||||
frappe.throw(_("Row #{0}: Please select the FG Warehouse in Assembly Items").format(row.idx))
|
||||
|
||||
if not row.item_code:
|
||||
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
|
||||
|
||||
@@ -1142,7 +1215,7 @@ def get_sales_orders(self):
|
||||
& (so.docstatus == 1)
|
||||
& (so.status.notin(["Stopped", "Closed"]))
|
||||
& (so.company == self.company)
|
||||
& (so_item.qty > so_item.work_order_qty)
|
||||
& (so_item.qty > so_item.production_plan_qty)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1566,7 +1639,6 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
def get_raw_materials_of_sub_assembly_items(
|
||||
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
|
||||
):
|
||||
|
||||
bei = frappe.qb.DocType("BOM Item")
|
||||
bom = frappe.qb.DocType("BOM")
|
||||
item = frappe.qb.DocType("Item")
|
||||
@@ -1609,7 +1681,10 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
|
||||
for item in items:
|
||||
key = (item.item_code, item.bom_no)
|
||||
if item.bom_no and key in sub_assembly_items:
|
||||
if item.bom_no and key not in sub_assembly_items:
|
||||
continue
|
||||
|
||||
if item.bom_no:
|
||||
planned_qty = flt(sub_assembly_items[key])
|
||||
get_raw_materials_of_sub_assembly_items(
|
||||
item_details,
|
||||
@@ -1626,3 +1701,42 @@ def get_raw_materials_of_sub_assembly_items(
|
||||
item_details.setdefault(item.get("item_code"), item)
|
||||
|
||||
return item_details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def sales_order_query(
|
||||
doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None
|
||||
):
|
||||
frappe.has_permission("Production Plan", throw=True)
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
so_table = frappe.qb.DocType("Sales Order")
|
||||
table = frappe.qb.DocType("Sales Order Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(so_table)
|
||||
.join(table)
|
||||
.on(table.parent == so_table.name)
|
||||
.select(table.parent)
|
||||
.distinct()
|
||||
.where((table.qty > table.production_plan_qty) & (table.docstatus == 1))
|
||||
)
|
||||
|
||||
if filters.get("company"):
|
||||
query = query.where(so_table.company == filters.get("company"))
|
||||
|
||||
if filters.get("sales_orders"):
|
||||
query = query.where(so_table.name.isin(filters.get("sales_orders")))
|
||||
|
||||
if txt:
|
||||
query = query.where(table.parent.like(f"%{txt}%"))
|
||||
|
||||
if page_len:
|
||||
query = query.limit(page_len)
|
||||
|
||||
if start:
|
||||
query = query.offset(start)
|
||||
|
||||
return query.run()
|
||||
|
||||
@@ -225,6 +225,102 @@ class TestProductionPlan(FrappeTestCase):
|
||||
|
||||
self.assertEqual(sales_orders, [])
|
||||
|
||||
def test_donot_allow_to_make_multiple_pp_against_same_so(self):
|
||||
item = "Test SO Production Item 1"
|
||||
create_item(item)
|
||||
|
||||
raw_material = "Test SO RM Production Item 1"
|
||||
create_item(raw_material)
|
||||
|
||||
if not frappe.db.get_value("BOM", {"item": item}):
|
||||
make_bom(item=item, raw_materials=[raw_material])
|
||||
|
||||
so = make_sales_order(item_code=item, qty=4)
|
||||
pln = frappe.new_doc("Production Plan")
|
||||
pln.company = so.company
|
||||
pln.get_items_from = "Sales Order"
|
||||
|
||||
pln.append(
|
||||
"sales_orders",
|
||||
{
|
||||
"sales_order": so.name,
|
||||
"sales_order_date": so.transaction_date,
|
||||
"customer": so.customer,
|
||||
"grand_total": so.grand_total,
|
||||
},
|
||||
)
|
||||
|
||||
pln.get_so_items()
|
||||
pln.submit()
|
||||
|
||||
pln = frappe.new_doc("Production Plan")
|
||||
pln.company = so.company
|
||||
pln.get_items_from = "Sales Order"
|
||||
|
||||
pln.append(
|
||||
"sales_orders",
|
||||
{
|
||||
"sales_order": so.name,
|
||||
"sales_order_date": so.transaction_date,
|
||||
"customer": so.customer,
|
||||
"grand_total": so.grand_total,
|
||||
},
|
||||
)
|
||||
|
||||
pln.get_so_items()
|
||||
self.assertRaises(frappe.ValidationError, pln.save)
|
||||
|
||||
def test_so_based_bill_of_material(self):
|
||||
item = "Test SO Production Item 1"
|
||||
create_item(item)
|
||||
|
||||
raw_material = "Test SO RM Production Item 1"
|
||||
create_item(raw_material)
|
||||
|
||||
bom1 = make_bom(item=item, raw_materials=[raw_material])
|
||||
|
||||
so = make_sales_order(item_code=item, qty=4)
|
||||
|
||||
# Create new BOM and assign to new sales order
|
||||
bom2 = make_bom(item=item, raw_materials=[raw_material])
|
||||
so2 = make_sales_order(item_code=item, qty=4)
|
||||
|
||||
pln1 = frappe.new_doc("Production Plan")
|
||||
pln1.company = so.company
|
||||
pln1.get_items_from = "Sales Order"
|
||||
|
||||
pln1.append(
|
||||
"sales_orders",
|
||||
{
|
||||
"sales_order": so.name,
|
||||
"sales_order_date": so.transaction_date,
|
||||
"customer": so.customer,
|
||||
"grand_total": so.grand_total,
|
||||
},
|
||||
)
|
||||
|
||||
pln1.get_so_items()
|
||||
|
||||
self.assertEqual(pln1.po_items[0].bom_no, bom1.name)
|
||||
|
||||
pln2 = frappe.new_doc("Production Plan")
|
||||
pln2.company = so2.company
|
||||
pln2.get_items_from = "Sales Order"
|
||||
|
||||
pln2.append(
|
||||
"sales_orders",
|
||||
{
|
||||
"sales_order": so2.name,
|
||||
"sales_order_date": so2.transaction_date,
|
||||
"customer": so2.customer,
|
||||
"grand_total": so2.grand_total,
|
||||
},
|
||||
)
|
||||
|
||||
pln2.get_so_items()
|
||||
|
||||
self.assertEqual(pln2.po_items[0].bom_no, bom2.name)
|
||||
|
||||
def test_production_plan_combine_items(self):
|
||||
"Test combining FG items in Production Plan."
|
||||
item = "Test Production Item 1"
|
||||
|
||||
@@ -114,7 +114,7 @@ class Workstation(Document):
|
||||
|
||||
if schedule_date in tuple(get_holidays(self.holiday_list)):
|
||||
schedule_date = add_days(schedule_date, 1)
|
||||
self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
|
||||
return self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True)
|
||||
|
||||
return schedule_date
|
||||
|
||||
|
||||
@@ -510,30 +510,41 @@ erpnext.SerialNoBatchSelector = class SerialNoBatchSelector {
|
||||
if(!list_value) {
|
||||
new_line = '';
|
||||
} else {
|
||||
me.serial_list = list_value.replace(/\n/g, ' ').match(/\S+/g) || [];
|
||||
me.serial_list = list_value.split(/\n/g) || [];
|
||||
}
|
||||
|
||||
if(!me.serial_list.includes(new_number)) {
|
||||
this.set_new_description('');
|
||||
serial_no_list_field.set_value(me.serial_list.join('\n') + new_line + new_number);
|
||||
me.serial_list = serial_no_list_field.get_value().replace(/\n/g, ' ').match(/\S+/g) || [];
|
||||
me.serial_list = serial_no_list_field.get_value().split(/\n/g) || [];
|
||||
} else {
|
||||
this.set_new_description(new_number + ' is already selected.');
|
||||
}
|
||||
|
||||
me.serial_list = me.serial_list.filter(serial => {
|
||||
if (serial) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
qty_field.set_input(me.serial_list.length);
|
||||
this.$input.val("");
|
||||
this.in_local_change = 0;
|
||||
}
|
||||
},
|
||||
{fieldtype: 'Column Break'},
|
||||
{fieldtype: 'Section Break'},
|
||||
{
|
||||
fieldname: 'serial_no',
|
||||
fieldtype: 'Small Text',
|
||||
fieldtype: 'Text',
|
||||
label: __(me.has_batch && !me.has_serial_no ? 'Selected Batch Numbers' : 'Selected Serial Numbers'),
|
||||
onchange: function() {
|
||||
me.serial_list = this.get_value()
|
||||
.replace(/\n/g, ' ').match(/\S+/g) || [];
|
||||
me.serial_list = this.get_value().split(/\n/g);
|
||||
me.serial_list = me.serial_list.filter(serial => {
|
||||
if (serial) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
this.layout.fields_dict.qty.set_input(me.serial_list.length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,10 +62,10 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "process_owner.full_name",
|
||||
"fetch_from": "procedure.process_owner_full_name",
|
||||
"fieldname": "full_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"read_only": 1,
|
||||
"label": "Full Name"
|
||||
},
|
||||
{
|
||||
@@ -81,7 +81,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-26 15:27:47.247814",
|
||||
"modified": "2023-07-31 08:10:47.247814",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Quality Management",
|
||||
"name": "Non Conformance",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:{####}",
|
||||
"creation": "2019-05-26 15:03:43.996455",
|
||||
"doctype": "DocType",
|
||||
@@ -12,7 +13,6 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fetch_from": "goal.objective",
|
||||
"fieldname": "objective",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
@@ -38,14 +38,17 @@
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-05-26 16:12:54.832058",
|
||||
"links": [],
|
||||
"modified": "2023-07-28 18:10:23.351246",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Quality Management",
|
||||
"name": "Quality Goal Objective",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -56,6 +56,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Open",
|
||||
"columns": 2,
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
@@ -67,7 +68,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-27 16:28:20.908637",
|
||||
"modified": "2023-07-31 09:20:20.908637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Quality Management",
|
||||
"name": "Quality Review Objective",
|
||||
@@ -76,4 +77,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ class LowerDeductionCertificate(Document):
|
||||
"supplier": self.supplier,
|
||||
"tax_withholding_category": self.tax_withholding_category,
|
||||
"name": ("!=", self.name),
|
||||
"company": self.company,
|
||||
},
|
||||
["name", "valid_from", "valid_upto"],
|
||||
as_dict=True,
|
||||
|
||||
@@ -556,7 +556,7 @@ def make_material_request(source_name, target_doc=None):
|
||||
# qty is for packed items, because packed items don't have stock_qty field
|
||||
qty = source.get("qty")
|
||||
target.project = source_parent.project
|
||||
target.qty = qty - requested_item_qty.get(source.name, 0) - source.delivered_qty
|
||||
target.qty = qty - requested_item_qty.get(source.name, 0) - flt(source.get("delivered_qty"))
|
||||
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
|
||||
|
||||
args = target.as_dict().copy()
|
||||
@@ -590,7 +590,7 @@ def make_material_request(source_name, target_doc=None):
|
||||
"doctype": "Material Request Item",
|
||||
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
|
||||
"condition": lambda doc: not frappe.db.exists("Product Bundle", doc.item_code)
|
||||
and (doc.stock_qty - doc.delivered_qty) > requested_item_qty.get(doc.name, 0),
|
||||
and (doc.stock_qty - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0),
|
||||
"postprocess": update_item,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -552,6 +552,26 @@ class TestSalesOrder(FrappeTestCase):
|
||||
workflow.is_active = 0
|
||||
workflow.save()
|
||||
|
||||
def test_material_request_for_product_bundle(self):
|
||||
# Create the Material Request from the sales order for the Packing Items
|
||||
# Check whether the material request has the correct packing item or not.
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New 1"):
|
||||
bundle_item = make_item("_Test Product Bundle Item New 1", {"is_stock_item": 0})
|
||||
bundle_item.append(
|
||||
"item_defaults", {"company": "_Test Company", "default_warehouse": "_Test Warehouse - _TC"}
|
||||
)
|
||||
bundle_item.save(ignore_permissions=True)
|
||||
|
||||
make_item("_Packed Item New 2", {"is_stock_item": 1})
|
||||
make_product_bundle("_Test Product Bundle Item New 1", ["_Packed Item New 2"], 2)
|
||||
|
||||
so = make_sales_order(
|
||||
item_code="_Test Product Bundle Item New 1",
|
||||
)
|
||||
|
||||
mr = make_material_request(so.name)
|
||||
self.assertEqual(mr.items[0].item_code, "_Packed Item New 2")
|
||||
|
||||
def test_bin_details_of_packed_item(self):
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Test Product Bundle Item New"):
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
"actual_qty",
|
||||
"ordered_qty",
|
||||
"planned_qty",
|
||||
"production_plan_qty",
|
||||
"column_break_69",
|
||||
"work_order_qty",
|
||||
"delivered_qty",
|
||||
@@ -860,12 +861,19 @@
|
||||
"fieldname": "material_request_item",
|
||||
"fieldtype": "Data",
|
||||
"label": "Material Request Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "production_plan_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Production Plan Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-25 02:51:10.247569",
|
||||
"modified": "2023-07-28 14:56:42.031636",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
||||
@@ -6,12 +6,9 @@ import json
|
||||
from datetime import date
|
||||
|
||||
import frappe
|
||||
from babel import Locale
|
||||
from frappe import _, throw
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import formatdate, getdate, today
|
||||
from holidays import country_holidays
|
||||
from holidays.utils import list_supported_countries
|
||||
|
||||
|
||||
class OverlapError(frappe.ValidationError):
|
||||
@@ -40,6 +37,8 @@ class HolidayList(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_supported_countries(self):
|
||||
from holidays.utils import list_supported_countries
|
||||
|
||||
subdivisions_by_country = list_supported_countries()
|
||||
countries = [
|
||||
{"value": country, "label": local_country_name(country)}
|
||||
@@ -52,6 +51,8 @@ class HolidayList(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_local_holidays(self):
|
||||
from holidays import country_holidays
|
||||
|
||||
if not self.country:
|
||||
throw(_("Please select a country"))
|
||||
|
||||
@@ -169,4 +170,6 @@ def is_holiday(holiday_list, date=None):
|
||||
|
||||
def local_country_name(country_code: str) -> str:
|
||||
"""Return the localized country name for the given country code."""
|
||||
return Locale.parse(frappe.local.lang).territories.get(country_code, country_code)
|
||||
from babel import Locale
|
||||
|
||||
return Locale.parse(frappe.local.lang, sep="-").territories.get(country_code, country_code)
|
||||
|
||||
@@ -8,6 +8,8 @@ from datetime import date, timedelta
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
|
||||
from erpnext.setup.doctype.holiday_list.holiday_list import local_country_name
|
||||
|
||||
|
||||
class TestHolidayList(unittest.TestCase):
|
||||
def test_holiday_list(self):
|
||||
@@ -58,6 +60,16 @@ class TestHolidayList(unittest.TestCase):
|
||||
self.assertIn(date(2023, 4, 10), holidays)
|
||||
self.assertNotIn(date(2023, 5, 1), holidays)
|
||||
|
||||
def test_localized_country_names(self):
|
||||
lang = frappe.local.lang
|
||||
frappe.local.lang = "en-gb"
|
||||
self.assertEqual(local_country_name("IN"), "India")
|
||||
self.assertEqual(local_country_name("DE"), "Germany")
|
||||
|
||||
frappe.local.lang = "de"
|
||||
self.assertEqual(local_country_name("DE"), "Deutschland")
|
||||
frappe.local.lang = lang
|
||||
|
||||
|
||||
def make_holiday_list(
|
||||
name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None
|
||||
|
||||
@@ -24,9 +24,6 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
no_breadcrumbs=1,
|
||||
)
|
||||
|
||||
def autoname(self):
|
||||
self.name = self.item_group_name
|
||||
|
||||
def validate(self):
|
||||
super(ItemGroup, self).validate()
|
||||
|
||||
@@ -76,7 +73,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
|
||||
return self.route
|
||||
|
||||
def on_trash(self):
|
||||
NestedSet.on_trash(self)
|
||||
NestedSet.on_trash(self, allow_root_deletion=True)
|
||||
WebsiteGenerator.on_trash(self)
|
||||
self.delete_child_item_groups_key()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ def get_data():
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "against_sales_order"],
|
||||
"Sales Invoice": ["items", "against_sales_invoice"],
|
||||
"Material Request": ["items", "material_request"],
|
||||
"Purchase Order": ["items", "purchase_order"],
|
||||
},
|
||||
|
||||
@@ -395,16 +395,16 @@ class Item(Document):
|
||||
|
||||
def validate_warehouse_for_reorder(self):
|
||||
"""Validate Reorder level table for duplicate and conditional mandatory"""
|
||||
warehouse = []
|
||||
warehouse_material_request_type: list[tuple[str, str]] = []
|
||||
for d in self.get("reorder_levels"):
|
||||
if not d.warehouse_group:
|
||||
d.warehouse_group = d.warehouse
|
||||
if d.get("warehouse") and d.get("warehouse") not in warehouse:
|
||||
warehouse += [d.get("warehouse")]
|
||||
if (d.get("warehouse"), d.get("material_request_type")) not in warehouse_material_request_type:
|
||||
warehouse_material_request_type += [(d.get("warehouse"), d.get("material_request_type"))]
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Row {0}: An Reorder entry already exists for this warehouse {1}").format(
|
||||
d.idx, d.warehouse
|
||||
_("Row #{0}: A reorder entry already exists for warehouse {1} with reorder type {2}.").format(
|
||||
d.idx, d.warehouse, d.material_request_type
|
||||
),
|
||||
DuplicateReorderRows,
|
||||
)
|
||||
|
||||
@@ -228,7 +228,8 @@ class MaterialRequest(BuyingController):
|
||||
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
||||
|
||||
if mr_qty_allowance:
|
||||
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
|
||||
allowed_qty = flt((d.qty + (d.qty * (mr_qty_allowance / 100))), d.precision("ordered_qty"))
|
||||
|
||||
if d.ordered_qty and d.ordered_qty > allowed_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
|
||||
@@ -45,7 +45,7 @@ class PriceList(Document):
|
||||
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
currency_changed = self.currency != doc_before_save.currency
|
||||
affects_cart = self.name == frappe.get_cached_value("E Commerce Settings", None, "price_list")
|
||||
affects_cart = self.name == frappe.db.get_single_value("E Commerce Settings", "price_list")
|
||||
|
||||
if currency_changed and affects_cart:
|
||||
validate_cart_settings()
|
||||
|
||||
@@ -1258,6 +1258,70 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
|
||||
self.assertEqual(query[0].value, 0)
|
||||
|
||||
def test_rejected_qty_for_internal_transfer(self):
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
customer = "_Test Internal Customer 2"
|
||||
company = "_Test Company with perpetual inventory"
|
||||
|
||||
from_warehouse = create_warehouse("_Test Internal From Warehouse New", company=company)
|
||||
to_warehouse = create_warehouse("_Test Internal To Warehouse New", company=company)
|
||||
rejected_warehouse = create_warehouse(
|
||||
"_Test Rejected Internal To Warehouse New", company=company
|
||||
)
|
||||
item_doc = make_item(
|
||||
"Test Internal Transfer Item DS",
|
||||
{
|
||||
"is_purchase_item": 1,
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SBNS.#####",
|
||||
},
|
||||
)
|
||||
|
||||
target_warehouse = create_warehouse("_Test Internal GIT Warehouse New", company=company)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
posting_date=add_days(today(), -1),
|
||||
warehouse=from_warehouse,
|
||||
qty=2,
|
||||
rate=100,
|
||||
)
|
||||
|
||||
dn1 = create_delivery_note(
|
||||
item_code=item_doc.name,
|
||||
company=company,
|
||||
customer=customer,
|
||||
serial_no=pr.items[0].serial_no,
|
||||
cost_center="Main - TCP1",
|
||||
expense_account="Cost of Goods Sold - TCP1",
|
||||
qty=2,
|
||||
rate=500,
|
||||
warehouse=from_warehouse,
|
||||
target_warehouse=target_warehouse,
|
||||
)
|
||||
|
||||
sns = get_serial_nos(dn1.items[0].serial_no)
|
||||
|
||||
self.assertEqual(len(sns), 2)
|
||||
|
||||
pr1 = make_inter_company_purchase_receipt(dn1.name)
|
||||
pr1.items[0].qty = 1.0
|
||||
pr1.items[0].rejected_qty = 1.0
|
||||
pr1.items[0].serial_no = sns[0]
|
||||
pr1.items[0].rejected_serial_no = sns[1]
|
||||
pr1.items[0].warehouse = to_warehouse
|
||||
pr1.items[0].rejected_warehouse = rejected_warehouse
|
||||
pr1.submit()
|
||||
|
||||
rejected_serial_no_wh = frappe.get_cached_value("Serial No", sns[1], "warehouse")
|
||||
|
||||
self.assertEqual(rejected_warehouse, rejected_serial_no_wh)
|
||||
|
||||
def test_backdated_transaction_for_internal_transfer_in_trasit_warehouse_for_purchase_receipt(
|
||||
self,
|
||||
):
|
||||
|
||||
@@ -194,7 +194,7 @@ class StockEntry(StockController):
|
||||
return False
|
||||
|
||||
# If line items are more than 100 or record is older than 6 months
|
||||
if len(self.items) > 100 or month_diff(nowdate(), self.posting_date) > 6:
|
||||
if len(self.items) > 50 or month_diff(nowdate(), self.posting_date) > 6:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -607,7 +607,7 @@ class StockReconciliation(StockController):
|
||||
)
|
||||
|
||||
if sl_entries:
|
||||
self.make_sl_entries(sl_entries)
|
||||
self.make_sl_entries(sl_entries, allow_negative_stock=True)
|
||||
|
||||
|
||||
def get_batch_qty_for_stock_reco(
|
||||
|
||||
@@ -16,7 +16,7 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
sle_count = frappe.db.count("Stock Ledger Entry", {"is_cancelled": 0})
|
||||
sle_count = frappe.db.count("Stock Ledger Entry")
|
||||
|
||||
if sle_count > SLE_COUNT_LIMIT and not filters.get("item_code") and not filters.get("warehouse"):
|
||||
frappe.throw(_("Please select either the Item or Warehouse filter to generate the report."))
|
||||
|
||||
@@ -430,10 +430,9 @@ class StockBalanceReport(object):
|
||||
{
|
||||
"label": _("Valuation Rate"),
|
||||
"fieldname": "val_rate",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"width": 90,
|
||||
"convertible": "rate",
|
||||
"options": "currency",
|
||||
},
|
||||
{
|
||||
"label": _("Company"),
|
||||
|
||||
@@ -196,7 +196,7 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("Avg Rate (Balance Stock)"),
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"width": 180,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate",
|
||||
@@ -204,7 +204,7 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("Valuation Rate"),
|
||||
"fieldname": "in_out_rate",
|
||||
"fieldtype": "Currency",
|
||||
"fieldtype": "Float",
|
||||
"width": 140,
|
||||
"options": "Company:company:default_currency",
|
||||
"convertible": "rate",
|
||||
|
||||
@@ -74,6 +74,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
|
||||
|
||||
args = sle_doc.as_dict()
|
||||
args["allow_zero_valuation_rate"] = sle.get("allow_zero_valuation_rate") or False
|
||||
|
||||
if sle.get("voucher_type") == "Stock Reconciliation":
|
||||
# preserve previous_qty_after_transaction for qty reposting
|
||||
@@ -109,6 +110,7 @@ def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_vou
|
||||
"sle_id": args.get("name"),
|
||||
"creation": args.get("creation"),
|
||||
},
|
||||
allow_zero_rate=args.get("allow_zero_valuation_rate") or False,
|
||||
allow_negative_stock=allow_negative_stock,
|
||||
via_landed_cost_voucher=via_landed_cost_voucher,
|
||||
)
|
||||
@@ -288,6 +290,8 @@ def update_args_in_repost_item_valuation(
|
||||
frappe.publish_realtime(
|
||||
"item_reposting_progress",
|
||||
{"name": doc.name, "items_to_be_repost": json.dumps(args, default=str), "current_index": index},
|
||||
doctype=doc.doctype,
|
||||
docname=doc.name,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -594,6 +594,67 @@ class TestSubcontractingReceipt(FrappeTestCase):
|
||||
self.assertNotEqual(scr.supplied_items[0].rate, prev_cost)
|
||||
self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate)
|
||||
|
||||
def test_subcontracting_receipt_raw_material_rate(self):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
# Step - 1: Set Backflush Based On as "BOM"
|
||||
set_backflush_based_on("BOM")
|
||||
|
||||
# Step - 2: Create FG and RM Items
|
||||
fg_item = make_item(properties={"is_stock_item": 1, "is_sub_contracted_item": 1}).name
|
||||
rm_item1 = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item2 = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
# Step - 3: Create BOM for FG Item
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item1, rm_item2])
|
||||
for rm_item in bom.items:
|
||||
self.assertEqual(rm_item.rate, 0)
|
||||
self.assertEqual(rm_item.amount, 0)
|
||||
bom = bom.name
|
||||
|
||||
# Step - 4: Create PO and SCO
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 1",
|
||||
"qty": 100,
|
||||
"rate": 100,
|
||||
"fg_item": fg_item,
|
||||
"fg_item_qty": 100,
|
||||
},
|
||||
]
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
for rm_item in sco.supplied_items:
|
||||
self.assertEqual(rm_item.rate, 0)
|
||||
self.assertEqual(rm_item.amount, 0)
|
||||
|
||||
# Step - 5: Inward Raw Materials
|
||||
rm_items = get_rm_items(sco.supplied_items)
|
||||
for rm_item in rm_items:
|
||||
rm_item["rate"] = 100
|
||||
itemwise_details = make_stock_in_entry(rm_items=rm_items)
|
||||
|
||||
# Step - 6: Transfer RM's to Subcontractor
|
||||
se = make_stock_transfer_entry(
|
||||
sco_no=sco.name,
|
||||
rm_items=rm_items,
|
||||
itemwise_details=copy.deepcopy(itemwise_details),
|
||||
)
|
||||
for item in se.items:
|
||||
self.assertEqual(item.qty, 100)
|
||||
self.assertEqual(item.basic_rate, 100)
|
||||
self.assertEqual(item.amount, item.qty * item.basic_rate)
|
||||
|
||||
# Step - 7: Create Subcontracting Receipt
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.save()
|
||||
scr.submit()
|
||||
scr.load_from_db()
|
||||
for rm_item in scr.supplied_items:
|
||||
self.assertEqual(rm_item.consumed_qty, 100)
|
||||
self.assertEqual(rm_item.rate, 100)
|
||||
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
|
||||
|
||||
|
||||
def make_return_subcontracting_receipt(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Jy kan nie projektipe 'eksterne' uitvee nie,
|
||||
You cannot edit root node.,U kan nie wortelknoop wysig nie.,
|
||||
You cannot restart a Subscription that is not cancelled.,U kan nie 'n intekening herlaai wat nie gekanselleer is nie.,
|
||||
You don't have enought Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie,
|
||||
You don't have enough Loyalty Points to redeem,U het nie genoeg lojaliteitspunte om te verkoop nie,
|
||||
You have already assessed for the assessment criteria {}.,U het reeds geassesseer vir die assesseringskriteria ().,
|
||||
You have already selected items from {0} {1},Jy het reeds items gekies van {0} {1},
|
||||
You have been invited to collaborate on the project: {0},U is genooi om saam te werk aan die projek: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',የፕሮጀክት አይነት «ውጫዊ» ን መሰረዝ አይችሉም.,
|
||||
You cannot edit root node.,የስር ሥፍራ ማረም አይችሉም.,
|
||||
You cannot restart a Subscription that is not cancelled.,የማይሰረዝ የደንበኝነት ምዝገባን ዳግም ማስጀመር አይችሉም.,
|
||||
You don't have enought Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም,
|
||||
You don't have enough Loyalty Points to redeem,ለማስመለስ በቂ የታማኝነት ነጥቦች የሉዎትም,
|
||||
You have already assessed for the assessment criteria {}.,ቀድሞውንም ግምገማ መስፈርት ከገመገምን {}.,
|
||||
You have already selected items from {0} {1},ከዚህ ቀደም ከ ንጥሎች ተመርጠዋል ሊሆን {0} {1},
|
||||
You have been invited to collaborate on the project: {0},እርስዎ ፕሮጀክት ላይ ተባበር ተጋብዘዋል: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',لا يمكنك حذف مشروع من نوع 'خارجي',
|
||||
You cannot edit root node.,لا يمكنك تحرير عقدة الجذر.,
|
||||
You cannot restart a Subscription that is not cancelled.,لا يمكنك إعادة تشغيل اشتراك غير ملغى.,
|
||||
You don't have enought Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها,
|
||||
You don't have enough Loyalty Points to redeem,ليس لديك ما يكفي من نقاط الولاء لاستردادها,
|
||||
You have already assessed for the assessment criteria {}.,لقد سبق أن قيمت معايير التقييم {}.,
|
||||
You have already selected items from {0} {1},لقد حددت العناصر من {0} {1},
|
||||
You have been invited to collaborate on the project: {0},لقد وجهت الدعوة إلى التعاون في هذا المشروع: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Не можете да изтриете Тип на проекта "Външен",
|
||||
You cannot edit root node.,Не можете да редактирате корен възел.,
|
||||
You cannot restart a Subscription that is not cancelled.,"Не можете да рестартирате абонамент, който не е анулиран.",
|
||||
You don't have enought Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите",
|
||||
You don't have enough Loyalty Points to redeem,"Нямате достатъчно точки за лоялност, за да осребрите",
|
||||
You have already assessed for the assessment criteria {}.,Вече оценихте критериите за оценка {}.,
|
||||
You have already selected items from {0} {1},Вие вече сте избрали елементи от {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Вие сте били поканени да си сътрудничат по проекта: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',আপনি প্রকল্প প্রকার 'বহিরাগত' মুছে ফেলতে পারবেন না,
|
||||
You cannot edit root node.,আপনি রুট নোড সম্পাদনা করতে পারবেন না।,
|
||||
You cannot restart a Subscription that is not cancelled.,আপনি সাবস্ক্রিপশনটি বাতিল না করা পুনরায় শুরু করতে পারবেন না,
|
||||
You don't have enought Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enought না,
|
||||
You don't have enough Loyalty Points to redeem,আপনি বিক্রি করার জন্য আনুগত্য পয়েন্ট enough না,
|
||||
You have already assessed for the assessment criteria {}.,"আপনি ইতিমধ্যে মূল্যায়ন মানদণ্ডের জন্য মূল্যায়ন করে নিলে, {}।",
|
||||
You have already selected items from {0} {1},আপনি ইতিমধ্যে থেকে আইটেম নির্বাচন করা আছে {0} {1},
|
||||
You have been invited to collaborate on the project: {0},আপনি প্রকল্পের সহযোগীতা করার জন্য আমন্ত্রণ জানানো হয়েছে: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Ne možete obrisati tip projekta 'Spoljni',
|
||||
You cannot edit root node.,Ne možete uređivati root čvor.,
|
||||
You cannot restart a Subscription that is not cancelled.,Ne možete ponovo pokrenuti pretplatu koja nije otkazana.,
|
||||
You don't have enought Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup,
|
||||
You don't have enough Loyalty Points to redeem,Ne iskoristite Loyalty Points za otkup,
|
||||
You have already assessed for the assessment criteria {}.,Ste već ocijenili za kriterije procjene {}.,
|
||||
You have already selected items from {0} {1},Vi ste već odabrane stavke iz {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Vi ste pozvani da surađuju na projektu: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',No es pot eliminar el tipus de projecte 'Extern',
|
||||
You cannot edit root node.,No podeu editar el node arrel.,
|
||||
You cannot restart a Subscription that is not cancelled.,No podeu reiniciar una subscripció que no es cancel·la.,
|
||||
You don't have enought Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar,
|
||||
You don't have enough Loyalty Points to redeem,No teniu punts de fidelització previstos per bescanviar,
|
||||
You have already assessed for the assessment criteria {}.,Vostè ja ha avaluat pels criteris d'avaluació {}.,
|
||||
You have already selected items from {0} {1},Ja ha seleccionat articles de {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Se li ha convidat a col·laborar en el projecte: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Nelze odstranit typ projektu "Externí",
|
||||
You cannot edit root node.,Nelze upravit kořenový uzel.,
|
||||
You cannot restart a Subscription that is not cancelled.,"Nelze znovu spustit odběr, který není zrušen.",
|
||||
You don't have enought Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění,
|
||||
You don't have enough Loyalty Points to redeem,Nemáte dostatečné věrnostní body k uplatnění,
|
||||
You have already assessed for the assessment criteria {}.,Již jste hodnotili kritéria hodnocení {}.,
|
||||
You have already selected items from {0} {1},Již jste vybrané položky z {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Byli jste pozváni ke spolupráci na projektu: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Du kan ikke slette Project Type 'Ekstern',
|
||||
You cannot edit root node.,Du kan ikke redigere root node.,
|
||||
You cannot restart a Subscription that is not cancelled.,"Du kan ikke genstarte en abonnement, der ikke annulleres.",
|
||||
You don't have enought Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse,
|
||||
You don't have enough Loyalty Points to redeem,Du har ikke nok loyalitetspoint til at indløse,
|
||||
You have already assessed for the assessment criteria {}.,Du har allerede vurderet for bedømmelseskriterierne {}.,
|
||||
You have already selected items from {0} {1},Du har allerede valgt elementer fra {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Du er blevet inviteret til at samarbejde om sag: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1147,6 +1147,7 @@ Get Items from Prescriptions,Holen Sie sich Artikel aus Verordnungen,
|
||||
Get Items from Product Bundle,Artikel aus dem Produkt-Bundle übernehmen,
|
||||
Get Suppliers,Holen Sie sich Lieferanten,
|
||||
Get Suppliers By,Holen Sie sich Lieferanten durch,
|
||||
Get Supplier Group Details,Werte aus Lieferantengruppe übernehmen,
|
||||
Get Updates,Newsletter abonnieren,
|
||||
Get customers from,Holen Sie Kunden von,
|
||||
Get from Patient Encounter,Von der Patientenbegegnung erhalten,
|
||||
@@ -3351,7 +3352,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Sie können den Projekttyp 'Extern' nicht löschen,
|
||||
You cannot edit root node.,Sie können den Stammknoten nicht bearbeiten.,
|
||||
You cannot restart a Subscription that is not cancelled.,Sie können ein nicht abgebrochenes Abonnement nicht neu starten.,
|
||||
You don't have enought Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen,
|
||||
You don't have enough Loyalty Points to redeem,Sie haben nicht genügend Treuepunkte zum Einlösen,
|
||||
You have already assessed for the assessment criteria {}.,Sie haben bereits für die Bewertungskriterien beurteilt.,
|
||||
You have already selected items from {0} {1},Sie haben bereits Elemente aus {0} {1} gewählt,
|
||||
You have been invited to collaborate on the project: {0},Sie wurden zur Zusammenarbeit für das Projekt {0} eingeladen.,
|
||||
@@ -5066,6 +5067,7 @@ Credit To,Gutschreiben auf,
|
||||
Party Account Currency,Währung des Kontos der Partei,
|
||||
Against Expense Account,Zu Aufwandskonto,
|
||||
Inter Company Invoice Reference,Unternehmensübergreifende Rechnungsreferenz,
|
||||
Internal Supplier,Interner Lieferant,
|
||||
Is Internal Supplier,Ist interner Lieferant,
|
||||
Start date of current invoice's period,Startdatum der laufenden Rechnungsperiode,
|
||||
End date of current invoice's period,Schlußdatum der laufenden Eingangsrechnungsperiode,
|
||||
@@ -5514,6 +5516,8 @@ Tracking,Verfolgung,
|
||||
Ref SQ,Ref-SQ,
|
||||
Inter Company Order Reference,Inter Company Bestellreferenz,
|
||||
Supplier Part Number,Lieferanten-Artikelnummer,
|
||||
Supplier Primary Contact,Hauptkontakt des Lieferanten,
|
||||
Supplier Primary Address,Hauptadresse des Lieferanten,
|
||||
Billed Amt,Rechnungsbetrag,
|
||||
Warehouse and Reference,Lager und Referenz,
|
||||
To be delivered to customer,Zur Auslieferung an den Kunden,
|
||||
@@ -7672,7 +7676,7 @@ Accounts Manager,Buchhalter,
|
||||
Allow Sales Invoice Creation Without Sales Order,Ermöglichen Sie die Erstellung von Kundenrechnungen ohne Auftrag,
|
||||
Allow Sales Invoice Creation Without Delivery Note,Ermöglichen Sie die Erstellung einer Ausgangsrechnung ohne Lieferschein,
|
||||
Default Price List,Standardpreisliste,
|
||||
Primary Address and Contact Detail,Primäre Adresse und Kontaktdetails,
|
||||
Primary Address and Contact,Hauptadresse und -kontakt,
|
||||
"Select, to make the customer searchable with these fields","Wählen Sie, um den Kunden mit diesen Feldern durchsuchbar zu machen",
|
||||
Customer Primary Contact,Hauptkontakt des Kunden,
|
||||
"Reselect, if the chosen contact is edited after save","Wählen Sie erneut, wenn der ausgewählte Kontakt nach dem Speichern bearbeitet wird",
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Δεν μπορείτε να διαγράψετε τον τύπο έργου 'Εξωτερικό',
|
||||
You cannot edit root node.,Δεν μπορείτε να επεξεργαστείτε τον κόμβο ρίζας.,
|
||||
You cannot restart a Subscription that is not cancelled.,Δεν μπορείτε να κάνετε επανεκκίνηση μιας συνδρομής που δεν ακυρώνεται.,
|
||||
You don't have enought Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση,
|
||||
You don't have enough Loyalty Points to redeem,Δεν διαθέτετε σημεία αφοσίωσης για εξαργύρωση,
|
||||
You have already assessed for the assessment criteria {}.,Έχετε ήδη αξιολογήσει τα κριτήρια αξιολόγησης {}.,
|
||||
You have already selected items from {0} {1},Έχετε ήδη επιλεγμένα αντικείμενα από {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Έχετε προσκληθεί να συνεργαστούν για το έργο: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',No puede eliminar Tipo de proyecto 'Externo',
|
||||
You cannot edit root node.,No puedes editar el nodo raíz.,
|
||||
You cannot restart a Subscription that is not cancelled.,No puede reiniciar una suscripción que no está cancelada.,
|
||||
You don't have enought Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear,
|
||||
You don't have enough Loyalty Points to redeem,No tienes suficientes puntos de lealtad para canjear,
|
||||
You have already assessed for the assessment criteria {}.,Ya ha evaluado los criterios de evaluación {}.,
|
||||
You have already selected items from {0} {1},Ya ha seleccionado artículos de {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Se le ha invitado a colaborar en el proyecto: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Te ei saa projekti tüübi "Väline" kustutada,
|
||||
You cannot edit root node.,Sa ei saa redigeerida juursõlme.,
|
||||
You cannot restart a Subscription that is not cancelled.,Te ei saa tellimust uuesti katkestada.,
|
||||
You don't have enought Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada",
|
||||
You don't have enough Loyalty Points to redeem,"Teil pole lojaalsuspunkte, mida soovite lunastada",
|
||||
You have already assessed for the assessment criteria {}.,Olete juba hinnanud hindamise kriteeriumid {}.,
|
||||
You have already selected items from {0} {1},Olete juba valitud objektide {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Sind on kutsutud koostööd projekti: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',شما نمیتوانید نوع پروژه «خارجی» را حذف کنید,
|
||||
You cannot edit root node.,نمی توانید گره ریشه را ویرایش کنید,
|
||||
You cannot restart a Subscription that is not cancelled.,شما نمی توانید اشتراک را لغو کنید.,
|
||||
You don't have enought Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید,
|
||||
You don't have enough Loyalty Points to redeem,شما نمیتوانید امتیازات وفاداری خود را به دست آورید,
|
||||
You have already assessed for the assessment criteria {}.,شما در حال حاضر برای معیارهای ارزیابی ارزیابی {}.,
|
||||
You have already selected items from {0} {1},شما در حال حاضر اقلام از انتخاب {0} {1},
|
||||
You have been invited to collaborate on the project: {0},از شما دعوت شده برای همکاری در این پروژه: {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Et voi poistaa projektityyppiä "Ulkoinen",
|
||||
You cannot edit root node.,Et voi muokata juurisolmua.,
|
||||
You cannot restart a Subscription that is not cancelled.,"Et voi uudelleenkäynnistää tilausta, jota ei peruuteta.",
|
||||
You don't have enought Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi,
|
||||
You don't have enough Loyalty Points to redeem,Sinulla ei ole tarpeeksi Loyalty Pointsia lunastettavaksi,
|
||||
You have already assessed for the assessment criteria {}.,Olet jo arvioitu arviointikriteerit {}.,
|
||||
You have already selected items from {0} {1},Olet jo valitut kohteet {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Sinut on kutsuttu yhteistyöhön projektissa {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -3338,7 +3338,7 @@ You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global S
|
||||
You cannot delete Project Type 'External',Vous ne pouvez pas supprimer le Type de Projet 'Externe',
|
||||
You cannot edit root node.,Vous ne pouvez pas modifier le nœud racine.,
|
||||
You cannot restart a Subscription that is not cancelled.,Vous ne pouvez pas redémarrer un abonnement qui n'est pas annulé.,
|
||||
You don't have enought Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger,
|
||||
You don't have enough Loyalty Points to redeem,Vous n'avez pas assez de points de fidélité à échanger,
|
||||
You have already assessed for the assessment criteria {}.,Vous avez déjà évalué les critères d'évaluation {}.,
|
||||
You have already selected items from {0} {1},Vous avez déjà choisi des articles de {0} {1},
|
||||
You have been invited to collaborate on the project: {0},Vous avez été invité à collaborer sur le projet : {0},
|
||||
|
||||
|
Can't render this file because it is too large.
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user