mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 04:29:18 +00:00
Compare commits
182 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6931db98f1 | ||
|
|
fdb2e94b5c | ||
|
|
e1504efd40 | ||
|
|
95abd7908f | ||
|
|
022f85dd08 | ||
|
|
837a6b5f50 | ||
|
|
f6a550faae | ||
|
|
54f672e144 | ||
|
|
e8d082560a | ||
|
|
ac7d6d6d59 | ||
|
|
e2e89492e0 | ||
|
|
760eab961d | ||
|
|
3499089323 | ||
|
|
7db6988364 | ||
|
|
bfa93cd3f6 | ||
|
|
e23710bf00 | ||
|
|
5025850258 | ||
|
|
473610506c | ||
|
|
71cb7d37ee | ||
|
|
5b1016c17d | ||
|
|
d598dad50e | ||
|
|
76ef61c24f | ||
|
|
001c230688 | ||
|
|
44f7de0f31 | ||
|
|
c32258e4b6 | ||
|
|
1b94510f08 | ||
|
|
65b7fb1293 | ||
|
|
d1f6d62d72 | ||
|
|
77e7a6cde7 | ||
|
|
78e22af3ca | ||
|
|
7f903532f3 | ||
|
|
8d1eac89e3 | ||
|
|
d78316869b | ||
|
|
33becb7b32 | ||
|
|
b97fdbe6fc | ||
|
|
5699a8daa2 | ||
|
|
91a5bd8615 | ||
|
|
485cb7dd28 | ||
|
|
9d6b434d1f | ||
|
|
405d1528c3 | ||
|
|
f1814a1a2a | ||
|
|
9406ddbff0 | ||
|
|
bae6c5bf5f | ||
|
|
cf9acd1ff6 | ||
|
|
6f143d35aa | ||
|
|
d266423011 | ||
|
|
d2b22db500 | ||
|
|
c43d0b81e3 | ||
|
|
cf0ab51348 | ||
|
|
0590f21814 | ||
|
|
bbdf26c82e | ||
|
|
020d2e2ca6 | ||
|
|
f434548204 | ||
|
|
f9b2355066 | ||
|
|
2815952a03 | ||
|
|
33f4fae8cd | ||
|
|
d37a1811db | ||
|
|
8dd26949b7 | ||
|
|
26ad688584 | ||
|
|
48ceead9d0 | ||
|
|
9dd33739f9 | ||
|
|
e4ed1d684d | ||
|
|
573b159541 | ||
|
|
fb0b426fe4 | ||
|
|
63ff1f1eaa | ||
|
|
4d79a4e7b3 | ||
|
|
97f9656460 | ||
|
|
14ddcd6c24 | ||
|
|
9689c1fcec | ||
|
|
ee1255a716 | ||
|
|
b0ac097327 | ||
|
|
5d97a69e32 | ||
|
|
0aad942312 | ||
|
|
debfbc4761 | ||
|
|
3dc68e3b00 | ||
|
|
bec3e8ed96 | ||
|
|
4123e7b244 | ||
|
|
c9bcf79e83 | ||
|
|
677525b2cf | ||
|
|
8c83bbc096 | ||
|
|
a512d27dbb | ||
|
|
6c8a65e03b | ||
|
|
2d13dda49c | ||
|
|
cde848dc7f | ||
|
|
79e414cb97 | ||
|
|
f5245f6b3f | ||
|
|
dc804f216d | ||
|
|
bc0db696c9 | ||
|
|
06b04770fc | ||
|
|
b22ac137f5 | ||
|
|
eed58634ba | ||
|
|
24852e46c1 | ||
|
|
50521d4efe | ||
|
|
8fc705ea6a | ||
|
|
63274527d2 | ||
|
|
aba51ee352 | ||
|
|
4bd83b5058 | ||
|
|
75d3093aea | ||
|
|
669d692844 | ||
|
|
606c99e57c | ||
|
|
cf308912a1 | ||
|
|
f5718390b7 | ||
|
|
8954bd7759 | ||
|
|
3cbaea389b | ||
|
|
335cb5fd28 | ||
|
|
cd2d335256 | ||
|
|
1a69db0f80 | ||
|
|
84e4a2509c | ||
|
|
f4e1959cc7 | ||
|
|
7651ecbc2b | ||
|
|
e464f5e419 | ||
|
|
1e93d0bcc4 | ||
|
|
b886589657 | ||
|
|
3a670264b2 | ||
|
|
2fd500ce26 | ||
|
|
9422422dcc | ||
|
|
37fc82cd11 | ||
|
|
cb35218eec | ||
|
|
9531a45b94 | ||
|
|
fb41f5f88c | ||
|
|
b9647ac0a4 | ||
|
|
77fa0f68df | ||
|
|
ae8355c953 | ||
|
|
c42ef922d2 | ||
|
|
e58b3b11e9 | ||
|
|
8f0e10bfbe | ||
|
|
77d719af6e | ||
|
|
3f59518d01 | ||
|
|
24b1100c8f | ||
|
|
c29eab12df | ||
|
|
15d2024b8e | ||
|
|
1480acabb0 | ||
|
|
4c337a6f44 | ||
|
|
63f45739e0 | ||
|
|
55a9a8fd51 | ||
|
|
c1d40a6bfa | ||
|
|
bbc13c719e | ||
|
|
3b49079ad7 | ||
|
|
6fad2bad11 | ||
|
|
1ca0516fe5 | ||
|
|
04b8527ba8 | ||
|
|
98a9007e9f | ||
|
|
64a50cfabf | ||
|
|
587a965bdf | ||
|
|
52c0900576 | ||
|
|
67a43c353c | ||
|
|
cf9fc552f0 | ||
|
|
6947686141 | ||
|
|
2b38b780ba | ||
|
|
0ecd7d2bf5 | ||
|
|
9f1b9320e9 | ||
|
|
12a7cb21b5 | ||
|
|
643bb0511c | ||
|
|
e975a10a75 | ||
|
|
c3aeb2dec5 | ||
|
|
fe32787e6e | ||
|
|
f3b872a8e2 | ||
|
|
020aedb8b0 | ||
|
|
82e8606b3c | ||
|
|
6daea6ccb2 | ||
|
|
f7de825e89 | ||
|
|
04f0dfb691 | ||
|
|
95e0bf5e0e | ||
|
|
7f1483ad70 | ||
|
|
7fd4d3c882 | ||
|
|
cfb3a9eabf | ||
|
|
21c1c7194b | ||
|
|
81c4279a30 | ||
|
|
01b54134ae | ||
|
|
28756bf7b6 | ||
|
|
4962b67358 | ||
|
|
403ff697e9 | ||
|
|
6d7aa2ae94 | ||
|
|
2b30727fdc | ||
|
|
49f0f1ca09 | ||
|
|
8fe4a4d3aa | ||
|
|
6a8146ba8a | ||
|
|
b18678df4d | ||
|
|
8d813e3256 | ||
|
|
c6966f4e2d | ||
|
|
e483b4a78a | ||
|
|
92eabe3cf5 |
@@ -3,7 +3,7 @@ import inspect
|
||||
|
||||
import frappe
|
||||
|
||||
__version__ = "14.41.1"
|
||||
__version__ = "14.44.1"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -37,6 +37,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
||||
}
|
||||
)
|
||||
cle.flags.ignore_permissions = True
|
||||
cle.flags.ignore_links = True
|
||||
cle.submit()
|
||||
|
||||
|
||||
|
||||
@@ -301,3 +301,30 @@ def get_dimensions(with_cost_center_and_project=False):
|
||||
default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension
|
||||
|
||||
return dimension_filters, default_dimensions_map
|
||||
|
||||
|
||||
def create_accounting_dimensions_for_doctype(doctype):
|
||||
accounting_dimensions = frappe.db.get_all(
|
||||
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||
)
|
||||
|
||||
if not accounting_dimensions:
|
||||
return
|
||||
|
||||
for d in accounting_dimensions:
|
||||
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||
|
||||
if field:
|
||||
continue
|
||||
|
||||
df = {
|
||||
"fieldname": d.fieldname,
|
||||
"label": d.label,
|
||||
"fieldtype": "Link",
|
||||
"options": d.document_type,
|
||||
"insert_after": "accounting_dimensions_section",
|
||||
}
|
||||
|
||||
create_custom_field(doctype, df, ignore_validate=True)
|
||||
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
@@ -352,10 +352,11 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
|
||||
export_errored_rows(frm) {
|
||||
open_url_post(
|
||||
"/api/method/frappe.core.doctype.data_import.data_import.download_errored_template",
|
||||
"/api/method/erpnext.accounts.doctype.bank_statement_import.bank_statement_import.download_errored_template",
|
||||
{
|
||||
data_import_name: frm.doc.name,
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
},
|
||||
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"api_details_section",
|
||||
"disabled",
|
||||
"service_provider",
|
||||
"api_endpoint",
|
||||
"access_key",
|
||||
"url",
|
||||
"column_break_3",
|
||||
"help",
|
||||
@@ -77,12 +79,24 @@
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.service_provider == 'exchangerate.host';",
|
||||
"fieldname": "access_key",
|
||||
"fieldtype": "Data",
|
||||
"label": "Access Key"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-10 15:51:14.521174",
|
||||
"modified": "2023-10-04 15:30:25.333860",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
|
||||
@@ -18,11 +18,21 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
def set_parameters_and_result(self):
|
||||
if self.service_provider == "exchangerate.host":
|
||||
|
||||
if not self.access_key:
|
||||
frappe.throw(
|
||||
_("Access Key is required for Service Provider: {0}").format(
|
||||
frappe.bold(self.service_provider)
|
||||
)
|
||||
)
|
||||
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
self.api_endpoint = "https://api.exchangerate.host/convert"
|
||||
self.append("result_key", {"key": "result"})
|
||||
self.append("req_params", {"key": "access_key", "value": self.access_key})
|
||||
self.append("req_params", {"key": "amount", "value": "1"})
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
|
||||
@@ -50,8 +50,18 @@ frappe.ui.form.on("Journal Entry", {
|
||||
frm.trigger("make_inter_company_journal_entry");
|
||||
}, __('Make'));
|
||||
}
|
||||
},
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
},
|
||||
before_save: function(frm) {
|
||||
if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
|
||||
let payment_entry_references = frm.doc.accounts.filter(elem => (elem.reference_type == "Payment Entry"));
|
||||
if (payment_entry_references.length > 0) {
|
||||
let rows = payment_entry_references.map(x => "#"+x.idx);
|
||||
frappe.throw(__("Rows: {0} have 'Payment Entry' as reference_type. This should not be set manually.", [frappe.utils.comma_and(rows)]));
|
||||
}
|
||||
}
|
||||
},
|
||||
make_inter_company_journal_entry: function(frm) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __("Select Company"),
|
||||
|
||||
@@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
|
||||
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
@@ -152,6 +152,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.events.hide_unhide_fields(frm);
|
||||
frm.events.set_dynamic_labels(frm);
|
||||
frm.events.show_general_ledger(frm);
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
|
||||
@@ -107,6 +107,8 @@ class PaymentEntry(AccountsController):
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payments",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
self.make_gl_entries(cancel=1)
|
||||
@@ -227,16 +229,18 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
# if no payment template is used by invoice and has a custom term(no `payment_term`), then invoice outstanding will be in 'None' key
|
||||
latest = latest.get(d.payment_term) or latest.get(None)
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
elif (
|
||||
latest.outstanding_amount < latest.invoice_amount
|
||||
and flt(d.outstanding_amount, d.precision("outstanding_amount"))
|
||||
!= flt(latest.outstanding_amount, d.precision("outstanding_amount"))
|
||||
and d.payment_term == ""
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
@@ -1600,11 +1604,10 @@ def split_invoices_based_on_payment_terms(outstanding_invoices, company):
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"outstanding_amount": flt(d.outstanding_amount),
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"allocated_amount": payment_term_outstanding
|
||||
"outstanding_amount": payment_term_outstanding
|
||||
if payment_term_outstanding
|
||||
else d.outstanding_amount,
|
||||
"payment_term_outstanding": payment_term_outstanding,
|
||||
"payment_amount": payment_term.payment_amount,
|
||||
"payment_term": payment_term.payment_term,
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ from erpnext.accounts.utils import (
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries_for_regional
|
||||
|
||||
|
||||
class PaymentReconciliation(Document):
|
||||
@@ -62,7 +62,7 @@ class PaymentReconciliation(Document):
|
||||
if self.payment_name:
|
||||
condition += "name like '%%{0}%%'".format(self.payment_name)
|
||||
|
||||
payment_entries = get_advance_payment_entries(
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
@@ -350,6 +350,7 @@ class PaymentReconciliation(Document):
|
||||
)
|
||||
|
||||
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
||||
adjust_allocations_for_taxes(self)
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
@@ -650,3 +651,8 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
None,
|
||||
inv.cost_center,
|
||||
)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def adjust_allocations_for_taxes(doc):
|
||||
pass
|
||||
|
||||
@@ -249,7 +249,7 @@ class PaymentRequest(Document):
|
||||
if (
|
||||
party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
|
||||
):
|
||||
party_amount = ref_doc.base_grand_total
|
||||
party_amount = ref_doc.get("base_rounded_total") or ref_doc.get("base_grand_total")
|
||||
else:
|
||||
party_amount = self.grand_total
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"transaction_date",
|
||||
"posting_date",
|
||||
"fiscal_year",
|
||||
"year_start_date",
|
||||
"amended_from",
|
||||
"company",
|
||||
"column_break1",
|
||||
@@ -100,16 +101,22 @@
|
||||
"fieldtype": "Text",
|
||||
"label": "Error Message",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "year_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Year Start Date"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-20 14:51:04.714154",
|
||||
"modified": "2023-09-11 20:19:11.810533",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Period Closing Voucher",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -144,5 +151,6 @@
|
||||
"search_fields": "posting_date, fiscal_year",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "closing_account_head"
|
||||
}
|
||||
@@ -95,15 +95,23 @@ class PeriodClosingVoucher(AccountsController):
|
||||
|
||||
self.check_if_previous_year_closed()
|
||||
|
||||
pce = frappe.db.sql(
|
||||
"""select name from `tabPeriod Closing Voucher`
|
||||
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
|
||||
(self.posting_date, self.fiscal_year, self.company),
|
||||
pcv = frappe.qb.DocType("Period Closing Voucher")
|
||||
existing_entry = (
|
||||
frappe.qb.from_(pcv)
|
||||
.select(pcv.name)
|
||||
.where(
|
||||
(pcv.posting_date >= self.posting_date)
|
||||
& (pcv.fiscal_year == self.fiscal_year)
|
||||
& (pcv.docstatus == 1)
|
||||
& (pcv.company == self.company)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
if pce and pce[0][0]:
|
||||
|
||||
if existing_entry and existing_entry[0][0]:
|
||||
frappe.throw(
|
||||
_("Another Period Closing Entry {0} has been made after {1}").format(
|
||||
pce[0][0], self.posting_date
|
||||
existing_entry[0][0], self.posting_date
|
||||
)
|
||||
)
|
||||
|
||||
@@ -130,18 +138,27 @@ class PeriodClosingVoucher(AccountsController):
|
||||
frappe.enqueue(
|
||||
process_gl_entries,
|
||||
gl_entries=gl_entries,
|
||||
voucher_name=self.name,
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.enqueue(
|
||||
process_closing_entries,
|
||||
gl_entries=gl_entries,
|
||||
closing_entries=closing_entries,
|
||||
voucher_name=self.name,
|
||||
company=self.company,
|
||||
closing_date=self.posting_date,
|
||||
queue="long",
|
||||
timeout=3000,
|
||||
)
|
||||
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be processed in the background, it can take a few minutes."),
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
||||
process_gl_entries(gl_entries, self.name)
|
||||
process_closing_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
|
||||
|
||||
def get_grouped_gl_entries(self, get_opening_entries=False):
|
||||
closing_entries = []
|
||||
@@ -322,17 +339,12 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
def process_gl_entries(gl_entries, voucher_name):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
try:
|
||||
if gl_entries:
|
||||
make_gl_entries(gl_entries, merge_entries=False)
|
||||
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
@@ -340,6 +352,19 @@ def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closi
|
||||
frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
|
||||
|
||||
|
||||
def process_closing_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
|
||||
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
|
||||
make_closing_entries,
|
||||
)
|
||||
|
||||
try:
|
||||
if gl_entries + closing_entries:
|
||||
make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
frappe.log_error(e)
|
||||
|
||||
|
||||
def make_reverse_gl_entries(voucher_type, voucher_no):
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe.utils import add_months, today
|
||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.utils import get_fiscal_year, now
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
@@ -47,6 +47,20 @@ class ProcessStatementOfAccounts(Document):
|
||||
|
||||
|
||||
def get_report_pdf(doc, consolidated=True):
|
||||
statement_dict = get_statement_dict(doc)
|
||||
if not bool(statement_dict):
|
||||
return False
|
||||
elif consolidated:
|
||||
delimiter = '<div style="page-break-before: always;"></div>' if doc.include_break else ""
|
||||
result = delimiter.join(list(statement_dict.values()))
|
||||
return get_pdf(result, {"orientation": doc.orientation})
|
||||
else:
|
||||
for customer, statement_html in statement_dict.items():
|
||||
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
|
||||
return statement_dict
|
||||
|
||||
|
||||
def get_statement_dict(doc, get_statement_dict=False):
|
||||
statement_dict = {}
|
||||
ageing = ""
|
||||
|
||||
@@ -77,17 +91,11 @@ def get_report_pdf(doc, consolidated=True):
|
||||
if not res:
|
||||
continue
|
||||
|
||||
statement_dict[entry.customer] = get_html(doc, filters, entry, col, res, ageing)
|
||||
statement_dict[entry.customer] = (
|
||||
[res, ageing] if get_statement_dict else get_html(doc, filters, entry, col, res, ageing)
|
||||
)
|
||||
|
||||
if not bool(statement_dict):
|
||||
return False
|
||||
elif consolidated:
|
||||
result = "".join(list(statement_dict.values()))
|
||||
return get_pdf(result, {"orientation": doc.orientation})
|
||||
else:
|
||||
for customer, statement_html in statement_dict.items():
|
||||
statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation})
|
||||
return statement_dict
|
||||
return statement_dict
|
||||
|
||||
|
||||
def set_ageing(doc, entry):
|
||||
@@ -100,7 +108,8 @@ def set_ageing(doc, entry):
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
"customer": entry.customer,
|
||||
"party_type": "Customer",
|
||||
"party": [entry.customer],
|
||||
}
|
||||
)
|
||||
col1, ageing = get_ageing(ageing_filters)
|
||||
@@ -144,7 +153,7 @@ def get_ar_filters(doc, entry):
|
||||
return {
|
||||
"report_date": doc.posting_date if doc.posting_date else None,
|
||||
"party_type": "Customer",
|
||||
"party": entry.customer,
|
||||
"party": [entry.customer],
|
||||
"customer_name": entry.customer_name if entry.customer_name else None,
|
||||
"payment_terms_template": doc.payment_terms_template if doc.payment_terms_template else None,
|
||||
"sales_partner": doc.sales_partner if doc.sales_partner else None,
|
||||
|
||||
@@ -4,39 +4,107 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, getdate, today
|
||||
|
||||
from erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts import (
|
||||
get_statement_dict,
|
||||
send_emails,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestProcessStatementOfAccounts(unittest.TestCase):
|
||||
class TestProcessStatementOfAccounts(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_customer(customer_name="Other Customer")
|
||||
self.clear_old_entries()
|
||||
self.si = create_sales_invoice()
|
||||
self.process_soa = create_process_soa()
|
||||
create_sales_invoice(customer="Other Customer")
|
||||
|
||||
def test_process_soa_for_gl(self):
|
||||
"""Tests the utils for Statement of Accounts(General Ledger)"""
|
||||
process_soa = create_process_soa(
|
||||
name="_Test Process SOA for GL",
|
||||
customers=[{"customer": "_Test Customer"}, {"customer": "Other Customer"}],
|
||||
)
|
||||
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
|
||||
|
||||
# Checks if the statements are filtered based on the Customer
|
||||
self.assertIn("Other Customer", statement_dict)
|
||||
self.assertIn("_Test Customer", statement_dict)
|
||||
|
||||
# Checks if the correct number of receivable entries exist
|
||||
# 3 rows for opening and closing and 1 row for SI
|
||||
receivable_entries = statement_dict["_Test Customer"][0]
|
||||
self.assertEqual(len(receivable_entries), 4)
|
||||
|
||||
# Checks the amount for the receivable entry
|
||||
self.assertEqual(receivable_entries[1].voucher_no, self.si.name)
|
||||
self.assertEqual(receivable_entries[1].balance, 100)
|
||||
|
||||
def test_process_soa_for_ar(self):
|
||||
"""Tests the utils for Statement of Accounts(Accounts Receivable)"""
|
||||
process_soa = create_process_soa(name="_Test Process SOA for AR", report="Accounts Receivable")
|
||||
statement_dict = get_statement_dict(process_soa, get_statement_dict=True)
|
||||
|
||||
# Checks if the statements are filtered based on the Customer
|
||||
self.assertNotIn("Other Customer", statement_dict)
|
||||
self.assertIn("_Test Customer", statement_dict)
|
||||
|
||||
# Checks if the correct number of receivable entries exist
|
||||
receivable_entries = statement_dict["_Test Customer"][0]
|
||||
self.assertEqual(len(receivable_entries), 1)
|
||||
|
||||
# Checks the amount for the receivable entry
|
||||
self.assertEqual(receivable_entries[0].voucher_no, self.si.name)
|
||||
self.assertEqual(receivable_entries[0].total_due, 100)
|
||||
|
||||
# Checks the ageing summary for AR
|
||||
ageing_summary = statement_dict["_Test Customer"][1][0]
|
||||
expected_summary = frappe._dict(
|
||||
range1=100,
|
||||
range2=0,
|
||||
range3=0,
|
||||
range4=0,
|
||||
range5=0,
|
||||
)
|
||||
self.check_ageing_summary(ageing_summary, expected_summary)
|
||||
|
||||
def test_auto_email_for_process_soa_ar(self):
|
||||
send_emails(self.process_soa.name, from_scheduler=True)
|
||||
self.process_soa.load_from_db()
|
||||
self.assertEqual(self.process_soa.posting_date, getdate(add_days(today(), 7)))
|
||||
process_soa = create_process_soa(
|
||||
name="_Test Process SOA", enable_auto_email=1, report="Accounts Receivable"
|
||||
)
|
||||
send_emails(process_soa.name, from_scheduler=True)
|
||||
process_soa.load_from_db()
|
||||
self.assertEqual(process_soa.posting_date, getdate(add_days(today(), 7)))
|
||||
|
||||
def check_ageing_summary(self, ageing, expected_ageing):
|
||||
for age_range in expected_ageing:
|
||||
self.assertEqual(expected_ageing[age_range], ageing.get(age_range))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def create_process_soa():
|
||||
frappe.delete_doc_if_exists("Process Statement Of Accounts", "Test Process SOA")
|
||||
def create_process_soa(**args):
|
||||
args = frappe._dict(args)
|
||||
frappe.delete_doc_if_exists("Process Statement Of Accounts", args.name)
|
||||
process_soa = frappe.new_doc("Process Statement Of Accounts")
|
||||
soa_dict = {
|
||||
"name": "Test Process SOA",
|
||||
"company": "_Test Company",
|
||||
}
|
||||
soa_dict = frappe._dict(
|
||||
name=args.name,
|
||||
company=args.company or "_Test Company",
|
||||
customers=args.customers or [{"customer": "_Test Customer"}],
|
||||
enable_auto_email=1 if args.enable_auto_email else 0,
|
||||
frequency=args.frequency or "Weekly",
|
||||
report=args.report or "General Ledger",
|
||||
from_date=args.from_date or getdate(today()),
|
||||
to_date=args.to_date or getdate(today()),
|
||||
posting_date=args.posting_date or getdate(today()),
|
||||
include_ageing=1,
|
||||
)
|
||||
process_soa.update(soa_dict)
|
||||
process_soa.set("customers", [{"customer": "_Test Customer"}])
|
||||
process_soa.enable_auto_email = 1
|
||||
process_soa.frequency = "Weekly"
|
||||
process_soa.report = "Accounts Receivable"
|
||||
process_soa.save()
|
||||
return process_soa
|
||||
|
||||
@@ -59,6 +59,25 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
|
||||
if (this.frm.doc.repost_required && this.frm.doc.docstatus===1) {
|
||||
this.frm.set_intro(__("Accounting entries for this invoice need to be reposted. Please click on 'Repost' button to update."));
|
||||
this.frm.add_custom_button(__('Repost Accounting Entries'),
|
||||
() => {
|
||||
this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'repost_accounting_entries',
|
||||
freeze: true,
|
||||
freeze_message: __('Reposting...'),
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__('Accounting Entries are reposted.'));
|
||||
me.frm.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
}).removeClass('btn-default').addClass('btn-warning');
|
||||
}
|
||||
|
||||
if(!doc.is_return && doc.docstatus == 1 && doc.outstanding_amount != 0){
|
||||
if(doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
@@ -162,6 +181,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
|
||||
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
unblock_invoice() {
|
||||
@@ -460,6 +480,12 @@ cur_frm.set_query("expense_account", "items", function(doc) {
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.set_query("wip_composite_asset", "items", function() {
|
||||
return {
|
||||
filters: {'is_composite_asset': 1, 'docstatus': 0 }
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.cscript.expense_account = function(doc, cdt, cdn){
|
||||
var d = locals[cdt][cdn];
|
||||
if(d.idx == 1 && d.expense_account){
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
"use_transaction_date_exchange_rate",
|
||||
"column_break2",
|
||||
"buying_price_list",
|
||||
"price_list_currency",
|
||||
@@ -166,6 +167,7 @@
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"repost_required",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"update_auto_repeat_reference",
|
||||
@@ -190,8 +192,7 @@
|
||||
"inter_company_invoice_reference",
|
||||
"is_old_subcontracting_flow",
|
||||
"remarks",
|
||||
"connections_tab",
|
||||
"column_break_38"
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -987,6 +988,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "cash_bank_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Cash/Bank Account",
|
||||
@@ -1050,6 +1052,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "eval:flt(doc.write_off_amount)!=0",
|
||||
"fieldname": "write_off_account",
|
||||
"fieldtype": "Link",
|
||||
@@ -1213,6 +1216,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "No",
|
||||
"fieldname": "is_opening",
|
||||
"fieldtype": "Select",
|
||||
@@ -1345,6 +1349,7 @@
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"depends_on": "eval:doc.is_internal_supplier",
|
||||
"description": "Unrealized Profit/Loss account for intra-company transfers",
|
||||
"fieldname": "unrealized_profit_loss_account",
|
||||
@@ -1377,6 +1382,7 @@
|
||||
"depends_on": "eval:doc.is_subcontracted",
|
||||
"fieldname": "supplier_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Supplier Warehouse",
|
||||
"no_copy": 1,
|
||||
"options": "Warehouse",
|
||||
@@ -1494,10 +1500,6 @@
|
||||
"fieldname": "column_break_6",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_50",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -1568,13 +1570,29 @@
|
||||
"fieldname": "use_company_roundoff_cost_center",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Company Default Round Off Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "repost_required",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Repost Required",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "use_transaction_date_exchange_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Transaction Date Exchange Rate",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 17:23:59.145031",
|
||||
"modified": "2023-10-16 16:24:51.886231",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -11,6 +11,9 @@ from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate,
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
check_if_return_invoice_linked_with_payment_entry,
|
||||
get_total_in_party_account_currency,
|
||||
@@ -487,6 +490,11 @@ class PurchaseInvoice(BuyingController):
|
||||
_("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt)
|
||||
)
|
||||
|
||||
def validate_for_repost(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_expense_account()
|
||||
validate_docs_for_deferred_accounting([], [self.name])
|
||||
|
||||
def on_submit(self):
|
||||
super(PurchaseInvoice, self).on_submit()
|
||||
|
||||
@@ -529,6 +537,19 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
"cash_bank_account",
|
||||
"write_off_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
|
||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||
if not gl_entries:
|
||||
gl_entries = self.get_gl_entries()
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
@@ -33,7 +33,7 @@ test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Templ
|
||||
test_ignore = ["Serial No"]
|
||||
|
||||
|
||||
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
@@ -43,6 +43,9 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
def tearDownClass(self):
|
||||
unlink_payment_on_cancel_of_invoice(0)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_purchase_invoice_received_qty(self):
|
||||
"""
|
||||
1. Test if received qty is validated against accepted + rejected
|
||||
@@ -417,6 +420,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
self.assertEqual(tax.tax_amount, expected_values[i][1])
|
||||
self.assertEqual(tax.total, expected_values[i][2])
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_purchase_invoice_with_advance(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
@@ -471,6 +475,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
)
|
||||
)
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_invoice_with_advance_and_multi_payment_terms(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
@@ -1209,6 +1214,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_gain_loss_with_advance_entry(self):
|
||||
unlink_enabled = frappe.db.get_value(
|
||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||
@@ -1411,6 +1417,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
)
|
||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
@@ -1796,7 +1803,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
company="_Test Company",
|
||||
customer="_Test Supplier",
|
||||
do_not_save=True,
|
||||
do_not_submit=True,
|
||||
rate=1000,
|
||||
@@ -1826,6 +1832,32 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
clear_dimension_defaults("Branch")
|
||||
disable_dimension()
|
||||
|
||||
def test_repost_accounting_entries(self):
|
||||
pi = make_purchase_invoice(
|
||||
rate=1000,
|
||||
price_list_rate=1000,
|
||||
qty=1,
|
||||
)
|
||||
expected_gle = [
|
||||
["_Test Account Cost for Goods Sold - _TC", 1000, 0.0, nowdate()],
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
|
||||
pi.items[0].expense_account = "Service - _TC"
|
||||
pi.save()
|
||||
pi.load_from_db()
|
||||
self.assertTrue(pi.repost_required)
|
||||
pi.repost_accounting_entries()
|
||||
|
||||
expected_gle = [
|
||||
["Creditors - _TC", 0.0, 1000, nowdate()],
|
||||
["Service - _TC", 1000, 0.0, nowdate()],
|
||||
]
|
||||
check_gl_entries(self, pi.name, expected_gle, nowdate())
|
||||
pi.load_from_db()
|
||||
self.assertFalse(pi.repost_required)
|
||||
|
||||
|
||||
def check_gl_entries(
|
||||
doc,
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"manufacturer_part_no",
|
||||
"accounting",
|
||||
"expense_account",
|
||||
"wip_composite_asset",
|
||||
"col_break5",
|
||||
"is_fixed_asset",
|
||||
"asset_location",
|
||||
@@ -467,6 +468,7 @@
|
||||
"label": "Accounting"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "expense_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Expense Head",
|
||||
@@ -877,12 +879,18 @@
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply TDS"
|
||||
},
|
||||
{
|
||||
"fieldname": "wip_composite_asset",
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Composite Asset",
|
||||
"options": "Asset"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-04 17:22:21.501152",
|
||||
"modified": "2023-10-03 21:01:01.824892",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
@@ -892,4 +900,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -86,6 +86,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"columns": 2,
|
||||
"fieldname": "account_head",
|
||||
"fieldtype": "Link",
|
||||
@@ -97,6 +98,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": ":Company",
|
||||
"fieldname": "cost_center",
|
||||
"fieldtype": "Link",
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-27 15:47:58.975034",
|
||||
"modified": "2023-09-26 14:21:27.362567",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Accounting Ledger",
|
||||
@@ -77,5 +77,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -21,29 +21,8 @@ class RepostAccountingLedger(Document):
|
||||
|
||||
def validate_for_deferred_accounting(self):
|
||||
sales_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Sales Invoice"]
|
||||
docs_with_deferred_revenue = frappe.db.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
purchase_docs = [x.voucher_no for x in self.vouchers if x.voucher_type == "Purchase Invoice"]
|
||||
docs_with_deferred_expense = frappe.db.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
if docs_with_deferred_revenue or docs_with_deferred_expense:
|
||||
frappe.throw(
|
||||
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
|
||||
frappe.bold(
|
||||
comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue])
|
||||
)
|
||||
)
|
||||
)
|
||||
validate_docs_for_deferred_accounting(sales_docs, purchase_docs)
|
||||
|
||||
def validate_for_closed_fiscal_year(self):
|
||||
if self.vouchers:
|
||||
@@ -139,14 +118,17 @@ class RepostAccountingLedger(Document):
|
||||
return rendered_page
|
||||
|
||||
def on_submit(self):
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
account_repost_doc=self.name,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
)
|
||||
frappe.msgprint(_("Repost has started in the background"))
|
||||
if len(self.vouchers) > 1:
|
||||
job_name = "repost_accounting_ledger_" + self.name
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger.start_repost",
|
||||
account_repost_doc=self.name,
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
)
|
||||
frappe.msgprint(_("Repost has started in the background"))
|
||||
else:
|
||||
start_repost(self.name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -181,3 +163,26 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
doc.make_gl_entries()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
docs_with_deferred_revenue = frappe.db.get_all(
|
||||
"Sales Invoice Item",
|
||||
filters={"parent": ["in", sales_docs], "docstatus": 1, "enable_deferred_revenue": True},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
docs_with_deferred_expense = frappe.db.get_all(
|
||||
"Purchase Invoice Item",
|
||||
filters={"parent": ["in", purchase_docs], "docstatus": 1, "enable_deferred_expense": 1},
|
||||
fields=["parent"],
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
if docs_with_deferred_revenue or docs_with_deferred_expense:
|
||||
frappe.throw(
|
||||
_("Documents: {0} have deferred revenue/expense enabled for them. Cannot repost.").format(
|
||||
frappe.bold(comma_and([x[0] for x in docs_with_deferred_expense + docs_with_deferred_revenue]))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-08 07:38:40.079038",
|
||||
"modified": "2023-09-26 14:21:35.719727",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Repost Payment Ledger",
|
||||
@@ -155,5 +155,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -34,7 +34,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
super.onload();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger"];
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
@@ -177,8 +177,11 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
|
||||
make_maintenance_schedule() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.make_maintenance_schedule",
|
||||
|
||||
@@ -11,13 +11,13 @@ from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
get_loyalty_program_details_with_points,
|
||||
validate_loyalty_points,
|
||||
)
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
@@ -176,6 +176,12 @@ class SalesInvoice(SellingController):
|
||||
self.validate_account_for_change_amount()
|
||||
self.validate_income_account()
|
||||
|
||||
def validate_for_repost(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_account_for_change_amount()
|
||||
self.validate_income_account()
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def validate_fixed_asset(self):
|
||||
for d in self.get("items"):
|
||||
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
|
||||
@@ -401,6 +407,8 @@ class SalesInvoice(SellingController):
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payments",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
)
|
||||
|
||||
@@ -527,89 +535,22 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
needs_repost = 0
|
||||
|
||||
# Check if any field affecting accounting entry is altered
|
||||
doc_before_update = self.get_doc_before_save()
|
||||
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
|
||||
|
||||
# Check if opening entry check updated
|
||||
if doc_before_update.get("is_opening") != self.is_opening:
|
||||
needs_repost = 1
|
||||
|
||||
if not needs_repost:
|
||||
# Parent Level Accounts excluding party account
|
||||
for field in (
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
):
|
||||
if doc_before_update.get(field) != self.get(field):
|
||||
needs_repost = 1
|
||||
break
|
||||
|
||||
# Check for parent accounting dimensions
|
||||
for dimension in accounting_dimensions:
|
||||
if doc_before_update.get(dimension) != self.get(dimension):
|
||||
needs_repost = 1
|
||||
break
|
||||
|
||||
# Check for child tables
|
||||
if self.check_if_child_table_updated(
|
||||
"items",
|
||||
doc_before_update,
|
||||
("income_account", "expense_account", "discount_account"),
|
||||
accounting_dimensions,
|
||||
):
|
||||
needs_repost = 1
|
||||
|
||||
if self.check_if_child_table_updated(
|
||||
"taxes", doc_before_update, ("account_head",), accounting_dimensions
|
||||
):
|
||||
needs_repost = 1
|
||||
|
||||
self.validate_accounts()
|
||||
|
||||
# validate if deferred revenue is enabled for any item
|
||||
# Don't allow to update the invoice if deferred revenue is enabled
|
||||
for item in self.get("items"):
|
||||
if item.enable_deferred_revenue:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Deferred Revenue is enabled for item {0}. You cannot update the invoice after submission."
|
||||
).format(item.item_code)
|
||||
)
|
||||
|
||||
self.db_set("repost_required", needs_repost)
|
||||
|
||||
def check_if_child_table_updated(
|
||||
self, child_table, doc_before_update, fields_to_check, accounting_dimensions
|
||||
):
|
||||
# Check if any field affecting accounting entry is altered
|
||||
for index, item in enumerate(self.get(child_table)):
|
||||
for field in fields_to_check:
|
||||
if doc_before_update.get(child_table)[index].get(field) != item.get(field):
|
||||
return True
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if doc_before_update.get(child_table)[index].get(dimension) != item.get(dimension):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def repost_accounting_entries(self):
|
||||
if self.repost_required:
|
||||
self.docstatus = 2
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.docstatus = 1
|
||||
self.make_gl_entries()
|
||||
self.db_set("repost_required", 0)
|
||||
else:
|
||||
frappe.throw(_("No updates pending for reposting"))
|
||||
fields_to_check = [
|
||||
"additional_discount_account",
|
||||
"cash_bank_account",
|
||||
"account_for_change_amount",
|
||||
"write_off_account",
|
||||
"loyalty_redemption_account",
|
||||
"unrealized_profit_loss_account",
|
||||
]
|
||||
child_tables = {
|
||||
"items": ("income_account", "expense_account", "discount_account"),
|
||||
"taxes": ("account_head",),
|
||||
}
|
||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||
if self.needs_repost:
|
||||
self.validate_for_repost()
|
||||
self.db_set("repost_required", self.needs_repost)
|
||||
|
||||
def set_paid_amount(self):
|
||||
paid_amount = 0.0
|
||||
|
||||
@@ -7,7 +7,7 @@ import unittest
|
||||
import frappe
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
@@ -38,13 +38,17 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||
|
||||
|
||||
class TestSalesInvoice(unittest.TestCase):
|
||||
class TestSalesInvoice(FrappeTestCase):
|
||||
def setUp(self):
|
||||
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
|
||||
|
||||
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
||||
create_internal_parties()
|
||||
setup_accounts()
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def make(self):
|
||||
w = frappe.copy_doc(test_records[0])
|
||||
@@ -172,6 +176,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertRaises(frappe.LinkExistsError, si.cancel)
|
||||
unlink_payment_on_cancel_of_invoice()
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
@@ -1293,6 +1298,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
dn.submit()
|
||||
return dn
|
||||
|
||||
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||
def test_sales_invoice_with_advance(self):
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||
test_records as jv_test_records,
|
||||
@@ -2774,6 +2780,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
tds_payable_account = create_account(
|
||||
account_name="TDS Payable",
|
||||
account_type="Tax",
|
||||
parent_account="Duties and Taxes - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
|
||||
si.apply_discount_on = "Grand Total"
|
||||
si.additional_discount_account = additional_discount_account
|
||||
@@ -3072,8 +3085,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.commission_rate = commission_rate
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
@change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
|
||||
def test_sales_invoice_submission_post_account_freezing_date(self):
|
||||
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1))
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.posting_date = add_days(getdate(), 1)
|
||||
si.save()
|
||||
@@ -3082,8 +3095,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
|
||||
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
"""
|
||||
Test a case where duplicating the item with qty = 1 in the invoice
|
||||
@@ -3112,6 +3123,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"book_deferred_entries_via_journal_entry": 1,
|
||||
"submit_journal_entries": 1,
|
||||
},
|
||||
)
|
||||
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue",
|
||||
@@ -3119,11 +3137,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
acc_settings = frappe.get_single("Accounts Settings")
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 1
|
||||
acc_settings.submit_journal_entries = 1
|
||||
acc_settings.save()
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_expense = 1
|
||||
item.item_defaults[0].deferred_revenue_account = deferred_account
|
||||
@@ -3189,13 +3202,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_gle[i][2], gle.debit)
|
||||
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
acc_settings = frappe.get_single("Accounts Settings")
|
||||
acc_settings.book_deferred_entries_via_journal_entry = 0
|
||||
acc_settings.submit_journal_entries = 0
|
||||
acc_settings.save()
|
||||
|
||||
frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None)
|
||||
|
||||
def test_standalone_serial_no_return(self):
|
||||
si = create_sales_invoice(
|
||||
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils.data import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -90,10 +91,14 @@ def create_parties():
|
||||
customer.insert()
|
||||
|
||||
|
||||
class TestSubscription(unittest.TestCase):
|
||||
class TestSubscription(FrappeTestCase):
|
||||
def setUp(self):
|
||||
create_plan()
|
||||
create_parties()
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_create_subscription_with_trial_with_correct_period(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2023-08-22 10:28:10.196712",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"account",
|
||||
"party_type",
|
||||
"party",
|
||||
"reference_doctype",
|
||||
"reference_name",
|
||||
"allocated_amount",
|
||||
"account_currency",
|
||||
"unlinked"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"options": "reference_doctype"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"options": "account_currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "unlinked",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Unlinked",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_doctype",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "account",
|
||||
"fieldtype": "Data",
|
||||
"label": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party"
|
||||
},
|
||||
{
|
||||
"fieldname": "account_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Account Currency",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-05 09:33:28.620149",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payment Entries",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class UnreconcilePaymentEntries(Document):
|
||||
pass
|
||||
@@ -0,0 +1,316 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
self.create_usd_receivable_account()
|
||||
self.create_item()
|
||||
self.clear_old_entries()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_sales_invoice(self, do_not_submit=False):
|
||||
si = create_sales_invoice(
|
||||
item=self.item,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
debit_to=self.debit_to,
|
||||
posting_date=today(),
|
||||
parent_cost_center=self.cost_center,
|
||||
cost_center=self.cost_center,
|
||||
rate=100,
|
||||
price_list_rate=100,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return si
|
||||
|
||||
def create_payment_entry(self):
|
||||
pe = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
paid_from=self.debit_to,
|
||||
paid_to=self.cash,
|
||||
paid_amount=200,
|
||||
save=True,
|
||||
)
|
||||
return pe
|
||||
|
||||
def test_01_unreconcile_invoice(self):
|
||||
si1 = self.create_sales_invoice()
|
||||
si2 = self.create_sales_invoice()
|
||||
|
||||
pe = self.create_payment_entry()
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
|
||||
)
|
||||
# Allocation payment against both invoices
|
||||
pe.save().submit()
|
||||
|
||||
# Assert outstanding
|
||||
[doc.reload() for doc in [si1, si2, pe]]
|
||||
self.assertEqual(si1.outstanding_amount, 0)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(pe.unallocated_amount, 0)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([si1.name, si2.name], allocations)
|
||||
# unreconcile si1
|
||||
for x in unreconcile.allocations:
|
||||
if x.reference_name != si1.name:
|
||||
unreconcile.remove(x)
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert outstanding
|
||||
[doc.reload() for doc in [si1, si2, pe]]
|
||||
self.assertEqual(si1.outstanding_amount, 100)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
def test_02_unreconcile_one_payment_from_multi_payments(self):
|
||||
"""
|
||||
Scenario: 2 payments, both split against 2 different invoices
|
||||
Unreconcile only one payment from one invoice
|
||||
"""
|
||||
si1 = self.create_sales_invoice()
|
||||
si2 = self.create_sales_invoice()
|
||||
pe1 = self.create_payment_entry()
|
||||
pe1.paid_amount = 100
|
||||
# Allocate payment against both invoices
|
||||
pe1.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||
)
|
||||
pe1.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||
)
|
||||
pe1.save().submit()
|
||||
|
||||
pe2 = self.create_payment_entry()
|
||||
pe2.paid_amount = 100
|
||||
# Allocate payment against both invoices
|
||||
pe2.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||
)
|
||||
pe2.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||
)
|
||||
pe2.save().submit()
|
||||
|
||||
# Assert outstanding and unallocated
|
||||
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||
self.assertEqual(si1.outstanding_amount, 0.0)
|
||||
self.assertEqual(si2.outstanding_amount, 0.0)
|
||||
self.assertEqual(pe1.unallocated_amount, 0.0)
|
||||
self.assertEqual(pe2.unallocated_amount, 0.0)
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"company": self.company,
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([si1.name, si2.name], allocations)
|
||||
# unreconcile si1 from pe2
|
||||
for x in unreconcile.allocations:
|
||||
if x.reference_name != si1.name:
|
||||
unreconcile.remove(x)
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert outstanding and unallocated
|
||||
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||
self.assertEqual(si1.outstanding_amount, 50)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(len(pe1.references), 2)
|
||||
self.assertEqual(len(pe2.references), 1)
|
||||
self.assertEqual(pe1.unallocated_amount, 0)
|
||||
self.assertEqual(pe2.unallocated_amount, 50)
|
||||
|
||||
def test_03_unreconciliation_on_multi_currency_invoice(self):
|
||||
self.create_customer("_Test MC Customer USD", "USD")
|
||||
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||
si1.currency = "USD"
|
||||
si1.debit_to = self.debtors_usd
|
||||
si1.conversion_rate = 80
|
||||
si1.save().submit()
|
||||
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.currency = "USD"
|
||||
si2.debit_to = self.debtors_usd
|
||||
si2.conversion_rate = 80
|
||||
si2.save().submit()
|
||||
|
||||
pe = self.create_payment_entry()
|
||||
pe.paid_from = self.debtors_usd
|
||||
pe.paid_from_account_currency = "USD"
|
||||
pe.source_exchange_rate = 75
|
||||
pe.received_amount = 75 * 200
|
||||
pe.save()
|
||||
# Allocate payment against both invoices
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 100},
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 100},
|
||||
)
|
||||
pe.save().submit()
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([si1.name, si2.name], allocations)
|
||||
# unreconcile si1 from pe
|
||||
for x in unreconcile.allocations:
|
||||
if x.reference_name != si1.name:
|
||||
unreconcile.remove(x)
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert outstanding and unallocated
|
||||
[doc.reload() for doc in [si1, si2, pe]]
|
||||
self.assertEqual(si1.outstanding_amount, 100)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(len(pe.references), 1)
|
||||
self.assertEqual(pe.unallocated_amount, 100)
|
||||
|
||||
# Exc gain/loss JE should've been cancelled as well
|
||||
self.assertEqual(
|
||||
frappe.db.count(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
def test_04_unreconciliation_on_multi_currency_invoice(self):
|
||||
"""
|
||||
2 payments split against 2 foreign currency invoices
|
||||
"""
|
||||
self.create_customer("_Test MC Customer USD", "USD")
|
||||
si1 = self.create_sales_invoice(do_not_submit=True)
|
||||
si1.currency = "USD"
|
||||
si1.debit_to = self.debtors_usd
|
||||
si1.conversion_rate = 80
|
||||
si1.save().submit()
|
||||
|
||||
si2 = self.create_sales_invoice(do_not_submit=True)
|
||||
si2.currency = "USD"
|
||||
si2.debit_to = self.debtors_usd
|
||||
si2.conversion_rate = 80
|
||||
si2.save().submit()
|
||||
|
||||
pe1 = self.create_payment_entry()
|
||||
pe1.paid_from = self.debtors_usd
|
||||
pe1.paid_from_account_currency = "USD"
|
||||
pe1.source_exchange_rate = 75
|
||||
pe1.received_amount = 75 * 100
|
||||
pe1.save()
|
||||
# Allocate payment against both invoices
|
||||
pe1.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||
)
|
||||
pe1.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||
)
|
||||
pe1.save().submit()
|
||||
|
||||
pe2 = self.create_payment_entry()
|
||||
pe2.paid_from = self.debtors_usd
|
||||
pe2.paid_from_account_currency = "USD"
|
||||
pe2.source_exchange_rate = 75
|
||||
pe2.received_amount = 75 * 100
|
||||
pe2.save()
|
||||
# Allocate payment against both invoices
|
||||
pe2.append(
|
||||
"references",
|
||||
{"reference_doctype": si1.doctype, "reference_name": si1.name, "allocated_amount": 50},
|
||||
)
|
||||
pe2.append(
|
||||
"references",
|
||||
{"reference_doctype": si2.doctype, "reference_name": si2.name, "allocated_amount": 50},
|
||||
)
|
||||
pe2.save().submit()
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"company": self.company,
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
||||
}
|
||||
)
|
||||
unreconcile.add_references()
|
||||
self.assertEqual(len(unreconcile.allocations), 2)
|
||||
allocations = [x.reference_name for x in unreconcile.allocations]
|
||||
self.assertEquals([si1.name, si2.name], allocations)
|
||||
# unreconcile si1 from pe2
|
||||
for x in unreconcile.allocations:
|
||||
if x.reference_name != si1.name:
|
||||
unreconcile.remove(x)
|
||||
unreconcile.save().submit()
|
||||
|
||||
# Assert outstanding and unallocated
|
||||
[doc.reload() for doc in [si1, si2, pe1, pe2]]
|
||||
self.assertEqual(si1.outstanding_amount, 50)
|
||||
self.assertEqual(si2.outstanding_amount, 0)
|
||||
self.assertEqual(len(pe1.references), 2)
|
||||
self.assertEqual(len(pe2.references), 1)
|
||||
self.assertEqual(pe1.unallocated_amount, 0)
|
||||
self.assertEqual(pe2.unallocated_amount, 50)
|
||||
|
||||
# Exc gain/loss JE from PE1 should be available
|
||||
self.assertEqual(
|
||||
frappe.db.count(
|
||||
"Journal Entry Account",
|
||||
filters={"reference_type": si1.doctype, "reference_name": si1.name, "docstatus": 1},
|
||||
),
|
||||
1,
|
||||
)
|
||||
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Unreconcile Payments", {
|
||||
refresh(frm) {
|
||||
frm.set_query("voucher_type", function() {
|
||||
return {
|
||||
filters: {
|
||||
name: ["in", ["Payment Entry", "Journal Entry"]]
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
frm.set_query("voucher_no", function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
company: doc.company,
|
||||
docstatus: 1
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
get_allocations: function(frm) {
|
||||
frm.clear_table("allocations");
|
||||
frappe.call({
|
||||
method: "get_allocations_from_payment",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
r.message.forEach(x => {
|
||||
frm.add_child("allocations", x)
|
||||
})
|
||||
frm.refresh_fields();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:UNREC-{#####}",
|
||||
"creation": "2023-08-22 10:26:34.421423",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"voucher_type",
|
||||
"voucher_no",
|
||||
"get_allocations",
|
||||
"allocations",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Unreconcile Payments",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Voucher Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Voucher No",
|
||||
"options": "voucher_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "get_allocations",
|
||||
"fieldtype": "Button",
|
||||
"label": "Get Allocations"
|
||||
},
|
||||
{
|
||||
"fieldname": "allocations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Allocations",
|
||||
"options": "Unreconcile Payment Entries"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-28 17:42:50.261377",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payments",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"read": 1,
|
||||
"role": "Accounts User",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils.data import comma_and
|
||||
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
unlink_ref_doc_from_payment_entries,
|
||||
update_voucher_outstanding,
|
||||
)
|
||||
|
||||
|
||||
class UnreconcilePayments(Document):
|
||||
def validate(self):
|
||||
self.supported_types = ["Payment Entry", "Journal Entry"]
|
||||
if not self.voucher_type in self.supported_types:
|
||||
frappe.throw(_("Only {0} are supported").format(comma_and(self.supported_types)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_allocations_from_payment(self):
|
||||
allocated_references = []
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
allocated_references = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.against_voucher_type.as_("reference_doctype"),
|
||||
ple.against_voucher_no.as_("reference_name"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(
|
||||
(ple.docstatus == 1)
|
||||
& (ple.voucher_type == self.voucher_type)
|
||||
& (ple.voucher_no == self.voucher_no)
|
||||
& (ple.voucher_no != ple.against_voucher_no)
|
||||
)
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
return allocated_references
|
||||
|
||||
def add_references(self):
|
||||
allocations = self.get_allocations_from_payment()
|
||||
|
||||
for alloc in allocations:
|
||||
self.append("allocations", alloc)
|
||||
|
||||
def on_submit(self):
|
||||
# todo: more granular unreconciliation
|
||||
for alloc in self.allocations:
|
||||
doc = frappe.get_doc(alloc.reference_doctype, alloc.reference_name)
|
||||
unlink_ref_doc_from_payment_entries(doc, self.voucher_no)
|
||||
cancel_exchange_gain_loss_journal(doc, self.voucher_type, self.voucher_no)
|
||||
update_voucher_outstanding(
|
||||
alloc.reference_doctype, alloc.reference_name, alloc.account, alloc.party_type, alloc.party
|
||||
)
|
||||
frappe.db.set_value("Unreconcile Payment Entries", alloc.name, "unlinked", True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def doc_has_references(doctype: str = None, docname: str = None):
|
||||
if doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
return frappe.db.count(
|
||||
"Payment Ledger Entry",
|
||||
filters={"delinked": 0, "against_voucher_no": docname, "amount": ["<", 0]},
|
||||
)
|
||||
else:
|
||||
return frappe.db.count(
|
||||
"Payment Ledger Entry",
|
||||
filters={"delinked": 0, "voucher_no": docname, "against_voucher_no": ["!=", docname]},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_payments_for_doc(
|
||||
company: str = None, doctype: str = None, docname: str = None
|
||||
) -> list:
|
||||
if company and doctype and docname:
|
||||
_dt = doctype
|
||||
_dn = docname
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
if _dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
criteria = [
|
||||
(ple.company == company),
|
||||
(ple.delinked == 0),
|
||||
(ple.against_voucher_no == _dn),
|
||||
(ple.amount < 0),
|
||||
]
|
||||
|
||||
res = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.voucher_no, ple.against_voucher_no)
|
||||
.having(qb.Field("allocated_amount") > 0)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
return res
|
||||
else:
|
||||
criteria = [
|
||||
(ple.company == company),
|
||||
(ple.delinked == 0),
|
||||
(ple.voucher_no == _dn),
|
||||
(ple.against_voucher_no != _dn),
|
||||
]
|
||||
|
||||
query = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.company,
|
||||
ple.against_voucher_type.as_("voucher_type"),
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
Abs(Sum(ple.amount_in_account_currency)).as_("allocated_amount"),
|
||||
ple.account_currency,
|
||||
)
|
||||
.where(Criterion.all(criteria))
|
||||
.groupby(ple.against_voucher_no)
|
||||
)
|
||||
res = query.run(as_dict=True)
|
||||
return res
|
||||
return []
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_unreconcile_doc_for_selection(selections=None):
|
||||
if selections:
|
||||
selections = frappe.json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payments")
|
||||
unrecon.company = row.get("company")
|
||||
unrecon.voucher_type = row.get("voucher_type")
|
||||
unrecon.voucher_no = row.get("voucher_no")
|
||||
unrecon.add_references()
|
||||
|
||||
# remove unselected references
|
||||
unrecon.allocations = [
|
||||
x
|
||||
for x in unrecon.allocations
|
||||
if x.reference_doctype == row.get("against_voucher_type")
|
||||
and x.reference_name == row.get("against_voucher_no")
|
||||
]
|
||||
unrecon.save().submit()
|
||||
@@ -6,12 +6,7 @@ from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.contacts.doctype.address.address import (
|
||||
get_address_display,
|
||||
get_company_address,
|
||||
get_default_address,
|
||||
)
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||
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 Abs, Date, Sum
|
||||
@@ -133,6 +128,7 @@ def _get_party_details(
|
||||
party_address,
|
||||
company_address,
|
||||
shipping_address,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
set_contact_details(party_details, party, party_type)
|
||||
set_other_values(party_details, party, party_type)
|
||||
@@ -193,6 +189,8 @@ def set_address_details(
|
||||
party_address=None,
|
||||
company_address=None,
|
||||
shipping_address=None,
|
||||
*,
|
||||
ignore_permissions=False
|
||||
):
|
||||
billing_address_field = (
|
||||
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
||||
@@ -205,13 +203,17 @@ def set_address_details(
|
||||
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
|
||||
)
|
||||
# address display
|
||||
party_details.address_display = get_address_display(party_details[billing_address_field])
|
||||
party_details.address_display = render_address(
|
||||
party_details[billing_address_field], check_permissions=not ignore_permissions
|
||||
)
|
||||
# shipping address
|
||||
if party_type in ["Customer", "Lead"]:
|
||||
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
|
||||
party_type, party.name
|
||||
)
|
||||
party_details.shipping_address = get_address_display(party_details["shipping_address_name"])
|
||||
party_details.shipping_address = render_address(
|
||||
party_details["shipping_address_name"], check_permissions=not ignore_permissions
|
||||
)
|
||||
if doctype:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
|
||||
@@ -229,7 +231,7 @@ def set_address_details(
|
||||
if shipping_address:
|
||||
party_details.update(
|
||||
shipping_address=shipping_address,
|
||||
shipping_address_display=get_address_display(shipping_address),
|
||||
shipping_address_display=render_address(shipping_address),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address)
|
||||
)
|
||||
|
||||
@@ -238,7 +240,8 @@ def set_address_details(
|
||||
party_details.update(
|
||||
billing_address=party_details.company_address,
|
||||
billing_address_display=(
|
||||
party_details.company_address_display or get_address_display(party_details.company_address)
|
||||
party_details.company_address_display
|
||||
or render_address(party_details.company_address, check_permissions=False)
|
||||
),
|
||||
**get_fetch_values(doctype, "billing_address", party_details.company_address)
|
||||
)
|
||||
@@ -290,7 +293,34 @@ def set_contact_details(party_details, party, party_type):
|
||||
}
|
||||
)
|
||||
else:
|
||||
party_details.update(get_contact_details(party_details.contact_person))
|
||||
fields = [
|
||||
"name as contact_person",
|
||||
"salutation",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email_id as contact_email",
|
||||
"mobile_no as contact_mobile",
|
||||
"phone as contact_phone",
|
||||
"designation as contact_designation",
|
||||
"department as contact_department",
|
||||
]
|
||||
|
||||
contact_details = frappe.db.get_value(
|
||||
"Contact", party_details.contact_person, fields, as_dict=True
|
||||
)
|
||||
|
||||
contact_details.contact_display = " ".join(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
contact_details.get("salutation"),
|
||||
contact_details.get("first_name"),
|
||||
contact_details.get("last_name"),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
party_details.update(contact_details)
|
||||
|
||||
|
||||
def set_other_values(party_details, party, party_type):
|
||||
@@ -957,3 +987,13 @@ def add_party_account(party_type, party, company, account):
|
||||
doc.append("accounts", accounts)
|
||||
|
||||
doc.save()
|
||||
|
||||
|
||||
def render_address(address, check_permissions=True):
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render
|
||||
|
||||
return frappe.call(_render, address, check_permissions=check_permissions)
|
||||
|
||||
@@ -95,18 +95,11 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Party Type",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
'account_type': 'Payable'
|
||||
}
|
||||
};
|
||||
},
|
||||
on_change: () => {
|
||||
"fieldtype": "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
|
||||
}
|
||||
@@ -114,8 +107,15 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname": "supplier_group",
|
||||
@@ -164,3 +164,15 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
}
|
||||
|
||||
erpnext.utils.add_dimensions('Accounts Payable', 9);
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db.get_list(
|
||||
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
|
||||
).then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Supplier",
|
||||
"party": self.supplier,
|
||||
"party": [self.supplier],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
|
||||
@@ -72,18 +72,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Party Type",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
'account_type': 'Payable'
|
||||
}
|
||||
};
|
||||
},
|
||||
on_change: () => {
|
||||
"fieldtype": "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
frappe.query_report.toggle_filter_display('supplier_group', frappe.query_report.get_filter_value('party_type') !== "Supplier");
|
||||
}
|
||||
@@ -91,8 +84,15 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname":"payment_terms_template",
|
||||
@@ -122,3 +122,15 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
}
|
||||
|
||||
erpnext.utils.add_dimensions('Accounts Payable Summary', 9);
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db.get_list(
|
||||
"Party Type", {filters:{"account_type": "Payable"}, fields:['name']}
|
||||
).then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
frappe.provide("erpnext.utils");
|
||||
|
||||
frappe.query_reports["Accounts Receivable"] = {
|
||||
"filters": [
|
||||
{
|
||||
@@ -38,19 +40,11 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Party Type",
|
||||
"Default": "Customer",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
'account_type': 'Receivable'
|
||||
}
|
||||
};
|
||||
},
|
||||
on_change: () => {
|
||||
"fieldtype": "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
|
||||
}
|
||||
@@ -58,8 +52,15 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname": "party_account",
|
||||
@@ -192,3 +193,16 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
}
|
||||
|
||||
erpnext.utils.add_dimensions('Accounts Receivable', 9);
|
||||
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db.get_list(
|
||||
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
|
||||
).then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -116,7 +116,7 @@ class ReceivablePayableReport(object):
|
||||
# build all keys, since we want to exclude vouchers beyond the report date
|
||||
for ple in self.ple_entries:
|
||||
# get the balance object for voucher_type
|
||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||
if not key in self.voucher_balance:
|
||||
self.voucher_balance[key] = frappe._dict(
|
||||
voucher_type=ple.voucher_type,
|
||||
@@ -183,7 +183,7 @@ class ReceivablePayableReport(object):
|
||||
):
|
||||
return
|
||||
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
|
||||
# If payment is made against credit note
|
||||
# and credit note is made against a Sales Invoice
|
||||
@@ -192,13 +192,13 @@ class ReceivablePayableReport(object):
|
||||
if ple.against_voucher_no in self.return_entries:
|
||||
return_against = self.return_entries.get(ple.against_voucher_no)
|
||||
if return_against:
|
||||
key = (ple.against_voucher_type, return_against, ple.party)
|
||||
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
if not row:
|
||||
# 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 = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
|
||||
|
||||
row.party_type = ple.party_type
|
||||
return row
|
||||
@@ -801,7 +801,7 @@ class ReceivablePayableReport(object):
|
||||
self.qb_selection_filter.append(self.filters.party_type == self.ple.party_type)
|
||||
|
||||
if self.filters.get("party"):
|
||||
self.qb_selection_filter.append(self.filters.party == self.ple.party)
|
||||
self.qb_selection_filter.append(self.ple.party.isin(self.filters.party))
|
||||
|
||||
if self.filters.party_account:
|
||||
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, getdate, today
|
||||
|
||||
@@ -23,29 +24,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def create_usd_account(self):
|
||||
name = "Debtors USD"
|
||||
exists = frappe.db.get_list(
|
||||
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
|
||||
)
|
||||
if exists:
|
||||
self.debtors_usd = exists[0].name
|
||||
else:
|
||||
debtors = frappe.get_doc(
|
||||
"Account",
|
||||
frappe.db.get_list(
|
||||
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
|
||||
)[0].name,
|
||||
)
|
||||
|
||||
debtors_usd = frappe.new_doc("Account")
|
||||
debtors_usd.company = debtors.company
|
||||
debtors_usd.account_name = "Debtors USD"
|
||||
debtors_usd.account_currency = "USD"
|
||||
debtors_usd.parent_account = debtors.parent_account
|
||||
debtors_usd.account_type = debtors.account_type
|
||||
self.debtors_usd = debtors_usd.save().name
|
||||
|
||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
si = create_sales_invoice(
|
||||
@@ -573,7 +551,7 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"party": [self.customer],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
@@ -605,3 +583,132 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
for field in expected:
|
||||
with self.subTest(field=field):
|
||||
self.assertEqual(report_output.get(field), expected.get(field))
|
||||
|
||||
def test_multi_select_party_filter(self):
|
||||
self.customer1 = self.customer
|
||||
self.create_customer("_Test Customer 2")
|
||||
self.customer2 = self.customer
|
||||
self.create_customer("_Test Customer 3")
|
||||
self.customer3 = self.customer
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"party_type": "Customer",
|
||||
"party": [self.customer1, self.customer3],
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
si1 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si1.customer = self.customer1
|
||||
si1.save().submit()
|
||||
|
||||
si2 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si2.customer = self.customer2
|
||||
si2.save().submit()
|
||||
|
||||
si3 = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
|
||||
si3.customer = self.customer3
|
||||
si3.save().submit()
|
||||
|
||||
# check invoice grand total and invoiced column's value for 3 payment terms
|
||||
report = execute(filters)
|
||||
|
||||
expected_output = {self.customer1, self.customer3}
|
||||
self.assertEqual(len(report[1]), 2)
|
||||
output_for = set([x.party for x in report[1]])
|
||||
self.assertEqual(output_for, expected_output)
|
||||
|
||||
def test_report_output_if_party_is_missing(self):
|
||||
acc_name = "Additional Debtors"
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": acc_name, "company": self.company}
|
||||
):
|
||||
additional_receivable_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": acc_name,
|
||||
"parent_account": "Accounts Receivable - " + self.company_abbr,
|
||||
"company": self.company,
|
||||
"account_type": "Receivable",
|
||||
}
|
||||
).save()
|
||||
self.debtors2 = additional_receivable_acc.name
|
||||
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = self.company
|
||||
je.posting_date = today()
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 150,
|
||||
"credit_in_account_currency": 0,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.debtors2,
|
||||
"party_type": "Customer",
|
||||
"party": self.customer,
|
||||
"debit_in_account_currency": 200,
|
||||
"credit_in_account_currency": 0,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": self.cash,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 350,
|
||||
"cost_center": self.cost_center,
|
||||
},
|
||||
)
|
||||
je.save().submit()
|
||||
|
||||
# manually remove party from Payment Ledger
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run()
|
||||
|
||||
filters = {
|
||||
"company": self.company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
|
||||
report_ouput = execute(filters)[1]
|
||||
expected_data = [
|
||||
[self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0],
|
||||
[self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0],
|
||||
]
|
||||
self.assertEqual(len(report_ouput), 2)
|
||||
# fetch only required fields
|
||||
report_output = [
|
||||
[
|
||||
x.party_account,
|
||||
x.voucher_type,
|
||||
x.voucher_no,
|
||||
"Customer",
|
||||
self.customer,
|
||||
x.invoiced,
|
||||
x.paid,
|
||||
x.credit_note,
|
||||
x.outstanding,
|
||||
]
|
||||
for x in report_ouput
|
||||
]
|
||||
# use account name to sort
|
||||
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
||||
report_output = sorted(report_output, key=lambda x: x[0])
|
||||
self.assertEqual(expected_data, report_output)
|
||||
|
||||
@@ -72,19 +72,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldname":"party_type",
|
||||
"label": __("Party Type"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Party Type",
|
||||
"Default": "Customer",
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
'account_type': 'Receivable'
|
||||
}
|
||||
};
|
||||
},
|
||||
on_change: () => {
|
||||
"fieldtype": "Autocomplete",
|
||||
options: get_party_type_options(),
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value('party', "");
|
||||
frappe.query_report.toggle_filter_display('customer_group', frappe.query_report.get_filter_value('party_type') !== "Customer");
|
||||
}
|
||||
@@ -92,8 +84,15 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
{
|
||||
"fieldname":"party",
|
||||
"label": __("Party"),
|
||||
"fieldtype": "Dynamic Link",
|
||||
"options": "party_type",
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
if (!frappe.query_report.filters) return;
|
||||
|
||||
let party_type = frappe.query_report.get_filter_value('party_type');
|
||||
if (!party_type) return;
|
||||
|
||||
return frappe.db.get_link_options(party_type, txt);
|
||||
},
|
||||
},
|
||||
{
|
||||
"fieldname":"customer_group",
|
||||
@@ -151,3 +150,15 @@ frappe.query_reports["Accounts Receivable Summary"] = {
|
||||
}
|
||||
|
||||
erpnext.utils.add_dimensions('Accounts Receivable Summary', 9);
|
||||
|
||||
function get_party_type_options() {
|
||||
let options = [];
|
||||
frappe.db.get_list(
|
||||
"Party Type", {filters:{"account_type": "Receivable"}, fields:['name']}
|
||||
).then((res) => {
|
||||
res.forEach((party_type) => {
|
||||
options.push(party_type.name);
|
||||
});
|
||||
});
|
||||
return options;
|
||||
}
|
||||
@@ -99,13 +99,11 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
# Add all amount columns
|
||||
for k in list(self.party_total[d.party]):
|
||||
if k not in ["currency", "sales_person"]:
|
||||
|
||||
self.party_total[d.party][k] += d.get(k, 0.0)
|
||||
if isinstance(self.party_total[d.party][k], float):
|
||||
self.party_total[d.party][k] += d.get(k) or 0.0
|
||||
|
||||
# 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(
|
||||
@@ -124,6 +122,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
"total_due": 0.0,
|
||||
"future_amount": 0.0,
|
||||
"sales_person": [],
|
||||
"party_type": row.party_type,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -133,13 +132,12 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
|
||||
for key in ("territory", "customer_group", "supplier_group"):
|
||||
if row.get(key):
|
||||
self.party_total[row.party][key] = row.get(key)
|
||||
|
||||
self.party_total[row.party][key] = row.get(key, "")
|
||||
if row.sales_person:
|
||||
self.party_total[row.party].sales_person.append(row.sales_person)
|
||||
self.party_total[row.party].sales_person.append(row.get("sales_person", ""))
|
||||
|
||||
if self.filters.sales_partner:
|
||||
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner")
|
||||
self.party_total[row.party]["default_sales_partner"] = row.get("default_sales_partner", "")
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
|
||||
@@ -23,6 +23,7 @@ class TestBankReconciliationStatement(FrappeTestCase):
|
||||
"Payment Entry",
|
||||
]:
|
||||
frappe.db.delete(dt)
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
|
||||
def test_loan_entries_in_bank_reco_statement(self):
|
||||
create_loan_accounts()
|
||||
|
||||
@@ -133,15 +133,17 @@ class General_Payment_Ledger_Comparison(object):
|
||||
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.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances)
|
||||
self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances)
|
||||
self.diff = frappe._dict({})
|
||||
|
||||
for x in self.diff1:
|
||||
for x in self.variation_in_payment_ledger:
|
||||
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]}))
|
||||
for x in self.variation_in_general_ledger:
|
||||
self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update(
|
||||
frappe._dict({"pl_balance": x[4]})
|
||||
)
|
||||
|
||||
def generate_data(self):
|
||||
self.data = []
|
||||
|
||||
@@ -544,6 +544,8 @@ class GrossProfitGenerator(object):
|
||||
new_row.qty += flt(row.qty)
|
||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||
if self.filters.get("group_by") == "Sales Person":
|
||||
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
|
||||
|
||||
@@ -68,7 +68,11 @@ def get_result(
|
||||
tax_amount += entry.credit - entry.debit
|
||||
|
||||
if net_total_map.get(name):
|
||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||
if voucher_type == "Journal Entry":
|
||||
# back calcalute total amount from rate and tax_amount
|
||||
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||
else:
|
||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ def get_data(filters):
|
||||
.select(
|
||||
gle.voucher_type, gle.voucher_no, Sum(gle.debit).as_("debit"), Sum(gle.credit).as_("credit")
|
||||
)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.groupby(gle.voucher_no)
|
||||
)
|
||||
query = apply_filters(query, filters, gle)
|
||||
|
||||
@@ -663,7 +663,9 @@ def update_reference_in_payment_entry(
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
|
||||
def cancel_exchange_gain_loss_journal(
|
||||
parent_doc: dict | object, referenced_dt: str = None, referenced_dn: str = None
|
||||
) -> None:
|
||||
"""
|
||||
Cancel Exchange Gain/Loss for Sales/Purchase Invoice, if they have any.
|
||||
"""
|
||||
@@ -690,76 +692,147 @@ def cancel_exchange_gain_loss_journal(parent_doc: dict | object) -> None:
|
||||
as_list=1,
|
||||
)
|
||||
for doc in gain_loss_journals:
|
||||
frappe.get_doc("Journal Entry", doc[0]).cancel()
|
||||
gain_loss_je = frappe.get_doc("Journal Entry", doc[0])
|
||||
if referenced_dt and referenced_dn:
|
||||
references = [(x.reference_type, x.reference_name) for x in gain_loss_je.accounts]
|
||||
if (
|
||||
len(references) == 2
|
||||
and (referenced_dt, referenced_dn) in references
|
||||
and (parent_doc.doctype, parent_doc.name) in references
|
||||
):
|
||||
# only cancel JE generated against parent_doc and referenced_dn
|
||||
gain_loss_je.cancel()
|
||||
else:
|
||||
gain_loss_je.cancel()
|
||||
|
||||
|
||||
def unlink_ref_doc_from_payment_entries(ref_doc):
|
||||
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name)
|
||||
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabGL Entry`
|
||||
set against_voucher_type=null, against_voucher=null,
|
||||
modified=%s, modified_by=%s
|
||||
where against_voucher_type=%s and against_voucher=%s
|
||||
and voucher_no != ifnull(against_voucher, '')""",
|
||||
(now(), frappe.session.user, ref_doc.doctype, ref_doc.name),
|
||||
def update_accounting_ledgers_after_reference_removal(
|
||||
ref_type: str = None, ref_no: str = None, payment_name: str = None
|
||||
):
|
||||
# General Ledger
|
||||
gle = qb.DocType("GL Entry")
|
||||
gle_update_query = (
|
||||
qb.update(gle)
|
||||
.set(gle.against_voucher_type, None)
|
||||
.set(gle.against_voucher, None)
|
||||
.set(gle.modified, now())
|
||||
.set(gle.modified_by, frappe.session.user)
|
||||
.where((gle.against_voucher_type == ref_type) & (gle.against_voucher == ref_no))
|
||||
)
|
||||
|
||||
if payment_name:
|
||||
gle_update_query = gle_update_query.where(gle.voucher_no == payment_name)
|
||||
gle_update_query.run()
|
||||
|
||||
# Payment Ledger
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
ple_update_query = (
|
||||
qb.update(ple)
|
||||
.set(ple.against_voucher_type, ple.voucher_type)
|
||||
.set(ple.against_voucher_no, ple.voucher_no)
|
||||
.set(ple.modified, now())
|
||||
.set(ple.modified_by, frappe.session.user)
|
||||
.where(
|
||||
(ple.against_voucher_type == ref_type)
|
||||
& (ple.against_voucher_no == ref_no)
|
||||
& (ple.delinked == 0)
|
||||
)
|
||||
)
|
||||
|
||||
qb.update(ple).set(ple.against_voucher_type, ple.voucher_type).set(
|
||||
ple.against_voucher_no, ple.voucher_no
|
||||
).set(ple.modified, now()).set(ple.modified_by, frappe.session.user).where(
|
||||
(ple.against_voucher_type == ref_doc.doctype)
|
||||
& (ple.against_voucher_no == ref_doc.name)
|
||||
& (ple.delinked == 0)
|
||||
).run()
|
||||
if payment_name:
|
||||
ple_update_query = ple_update_query.where(ple.voucher_no == payment_name)
|
||||
ple_update_query.run()
|
||||
|
||||
|
||||
def remove_ref_from_advance_section(ref_doc: object = None):
|
||||
# TODO: this might need some testing
|
||||
if ref_doc.doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
ref_doc.set("advances", [])
|
||||
|
||||
frappe.db.sql(
|
||||
"""delete from `tab{0} Advance` where parent = %s""".format(ref_doc.doctype), ref_doc.name
|
||||
)
|
||||
adv_type = qb.DocType(f"{ref_doc.doctype} Advance")
|
||||
qb.from_(adv_type).delete().where(adv_type.parent == ref_doc.name).run()
|
||||
|
||||
|
||||
def remove_ref_doc_link_from_jv(ref_type, ref_no):
|
||||
linked_jv = frappe.db.sql_list(
|
||||
"""select parent from `tabJournal Entry Account`
|
||||
where reference_type=%s and reference_name=%s and docstatus < 2""",
|
||||
(ref_type, ref_no),
|
||||
def unlink_ref_doc_from_payment_entries(ref_doc: object = None, payment_name: str = None):
|
||||
remove_ref_doc_link_from_jv(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
remove_ref_doc_link_from_pe(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
update_accounting_ledgers_after_reference_removal(ref_doc.doctype, ref_doc.name, payment_name)
|
||||
remove_ref_from_advance_section(ref_doc)
|
||||
|
||||
|
||||
def remove_ref_doc_link_from_jv(
|
||||
ref_type: str = None, ref_no: str = None, payment_name: str = None
|
||||
):
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
|
||||
linked_jv = (
|
||||
qb.from_(jea)
|
||||
.select(jea.parent)
|
||||
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no) & (jea.docstatus.lt(2)))
|
||||
.run(as_list=1)
|
||||
)
|
||||
linked_jv = convert_to_list(linked_jv)
|
||||
# remove reference only from specified payment
|
||||
linked_jv = [x for x in linked_jv if x == payment_name] if payment_name else linked_jv
|
||||
|
||||
if linked_jv:
|
||||
frappe.db.sql(
|
||||
"""update `tabJournal Entry Account`
|
||||
set reference_type=null, reference_name = null,
|
||||
modified=%s, modified_by=%s
|
||||
where reference_type=%s and reference_name=%s
|
||||
and docstatus < 2""",
|
||||
(now(), frappe.session.user, ref_type, ref_no),
|
||||
update_query = (
|
||||
qb.update(jea)
|
||||
.set(jea.reference_type, None)
|
||||
.set(jea.reference_name, None)
|
||||
.set(jea.modified, now())
|
||||
.set(jea.modified_by, frappe.session.user)
|
||||
.where((jea.reference_type == ref_type) & (jea.reference_name == ref_no))
|
||||
)
|
||||
|
||||
if payment_name:
|
||||
update_query = update_query.where(jea.parent == payment_name)
|
||||
|
||||
update_query.run()
|
||||
|
||||
frappe.msgprint(_("Journal Entries {0} are un-linked").format("\n".join(linked_jv)))
|
||||
|
||||
|
||||
def remove_ref_doc_link_from_pe(ref_type, ref_no):
|
||||
linked_pe = frappe.db.sql_list(
|
||||
"""select parent from `tabPayment Entry Reference`
|
||||
where reference_doctype=%s and reference_name=%s and docstatus < 2""",
|
||||
(ref_type, ref_no),
|
||||
def convert_to_list(result):
|
||||
"""
|
||||
Convert tuple to list
|
||||
"""
|
||||
return [x[0] for x in result]
|
||||
|
||||
|
||||
def remove_ref_doc_link_from_pe(
|
||||
ref_type: str = None, ref_no: str = None, payment_name: str = None
|
||||
):
|
||||
per = qb.DocType("Payment Entry Reference")
|
||||
pay = qb.DocType("Payment Entry")
|
||||
|
||||
linked_pe = (
|
||||
qb.from_(per)
|
||||
.select(per.parent)
|
||||
.where(
|
||||
(per.reference_doctype == ref_type) & (per.reference_name == ref_no) & (per.docstatus.lt(2))
|
||||
)
|
||||
.run(as_list=1)
|
||||
)
|
||||
linked_pe = convert_to_list(linked_pe)
|
||||
# remove reference only from specified payment
|
||||
linked_pe = [x for x in linked_pe if x == payment_name] if payment_name else linked_pe
|
||||
|
||||
if linked_pe:
|
||||
frappe.db.sql(
|
||||
"""update `tabPayment Entry Reference`
|
||||
set allocated_amount=0, modified=%s, modified_by=%s
|
||||
where reference_doctype=%s and reference_name=%s
|
||||
and docstatus < 2""",
|
||||
(now(), frappe.session.user, ref_type, ref_no),
|
||||
update_query = (
|
||||
qb.update(per)
|
||||
.set(per.allocated_amount, 0)
|
||||
.set(per.modified, now())
|
||||
.set(per.modified_by, frappe.session.user)
|
||||
.where(
|
||||
(per.docstatus.lt(2) & (per.reference_doctype == ref_type) & (per.reference_name == ref_no))
|
||||
)
|
||||
)
|
||||
|
||||
if payment_name:
|
||||
update_query = update_query.where(per.parent == payment_name)
|
||||
|
||||
update_query.run()
|
||||
|
||||
for pe in linked_pe:
|
||||
try:
|
||||
pe_doc = frappe.get_doc("Payment Entry", pe)
|
||||
@@ -772,19 +845,13 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
|
||||
msg += _("Please cancel payment entry manually first")
|
||||
frappe.throw(msg, exc=PaymentEntryUnlinkError, title=_("Payment Unlink Error"))
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabPayment Entry` set total_allocated_amount=%s,
|
||||
base_total_allocated_amount=%s, unallocated_amount=%s, modified=%s, modified_by=%s
|
||||
where name=%s""",
|
||||
(
|
||||
pe_doc.total_allocated_amount,
|
||||
pe_doc.base_total_allocated_amount,
|
||||
pe_doc.unallocated_amount,
|
||||
now(),
|
||||
frappe.session.user,
|
||||
pe,
|
||||
),
|
||||
)
|
||||
qb.update(pay).set(pay.total_allocated_amount, pe_doc.total_allocated_amount).set(
|
||||
pay.base_total_allocated_amount, pe_doc.base_total_allocated_amount
|
||||
).set(pay.unallocated_amount, pe_doc.unallocated_amount).set(pay.modified, now()).set(
|
||||
pay.modified_by, frappe.session.user
|
||||
).where(
|
||||
pay.name == pe
|
||||
).run()
|
||||
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
|
||||
|
||||
@@ -147,6 +147,15 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
if (frm.doc.docstatus == 0) {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
|
||||
if (frm.doc.is_composite_asset && !frm.doc.capitalized_in) {
|
||||
$('.primary-action').prop('hidden', true);
|
||||
$('.form-message').text('Capitalize this asset to confirm');
|
||||
|
||||
frm.add_custom_button(__("Capitalize Asset"), function() {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -168,7 +177,7 @@ frappe.ui.form.on('Asset', {
|
||||
frm.set_df_property('purchase_invoice', 'read_only', 1);
|
||||
frm.set_df_property('purchase_receipt', 'read_only', 1);
|
||||
}
|
||||
else if (frm.doc.is_existing_asset) {
|
||||
else if (frm.doc.is_existing_asset || frm.doc.is_composite_asset) {
|
||||
frm.toggle_reqd('purchase_receipt', 0);
|
||||
frm.toggle_reqd('purchase_invoice', 0);
|
||||
}
|
||||
@@ -275,7 +284,7 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
|
||||
item_code: function(frm) {
|
||||
if(frm.doc.item_code && frm.doc.calculate_depreciation) {
|
||||
if(frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||
frm.trigger('set_finance_book');
|
||||
} else {
|
||||
frm.set_value('finance_books', []);
|
||||
@@ -287,7 +296,8 @@ frappe.ui.form.on('Asset', {
|
||||
method: "erpnext.assets.doctype.asset.asset.get_item_details",
|
||||
args: {
|
||||
item_code: frm.doc.item_code,
|
||||
asset_category: frm.doc.asset_category
|
||||
asset_category: frm.doc.asset_category,
|
||||
gross_purchase_amount: frm.doc.gross_purchase_amount
|
||||
},
|
||||
callback: function(r, rt) {
|
||||
if(r.message) {
|
||||
@@ -299,7 +309,17 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
is_existing_asset: function(frm) {
|
||||
frm.trigger("toggle_reference_doc");
|
||||
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
|
||||
},
|
||||
|
||||
is_composite_asset: function(frm) {
|
||||
if(frm.doc.is_composite_asset) {
|
||||
frm.set_value('gross_purchase_amount', 0);
|
||||
frm.set_df_property('gross_purchase_amount', 'read_only', 1);
|
||||
} else {
|
||||
frm.set_df_property('gross_purchase_amount', 'read_only', 0);
|
||||
}
|
||||
|
||||
frm.trigger("toggle_reference_doc");
|
||||
},
|
||||
|
||||
make_schedules_editable: function(frm) {
|
||||
@@ -360,6 +380,19 @@ frappe.ui.form.on('Asset', {
|
||||
});
|
||||
},
|
||||
|
||||
create_asset_capitalization: function(frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
"asset": frm.doc.name,
|
||||
},
|
||||
method: "erpnext.assets.doctype.asset.asset.create_asset_capitalization",
|
||||
callback: function(r) {
|
||||
var doclist = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
split_asset: function(frm) {
|
||||
const title = __('Split Asset');
|
||||
|
||||
@@ -415,7 +448,7 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
calculate_depreciation: function(frm) {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation ) {
|
||||
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||
frm.trigger("set_finance_book");
|
||||
} else {
|
||||
frm.set_value("finance_books", []);
|
||||
@@ -423,9 +456,11 @@ frappe.ui.form.on('Asset', {
|
||||
},
|
||||
|
||||
gross_purchase_amount: function(frm) {
|
||||
frm.doc.finance_books.forEach(d => {
|
||||
frm.events.set_depreciation_rate(frm, d);
|
||||
})
|
||||
if (frm.doc.finance_books) {
|
||||
frm.doc.finance_books.forEach(d => {
|
||||
frm.events.set_depreciation_rate(frm, d);
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
purchase_receipt: (frm) => {
|
||||
@@ -504,7 +539,21 @@ frappe.ui.form.on('Asset', {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
set_salvage_value_percentage_or_expected_value_after_useful_life: function(frm, row, salvage_value_percentage_changed, expected_value_after_useful_life_changed) {
|
||||
if (expected_value_after_useful_life_changed) {
|
||||
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
|
||||
const new_salvage_value_percentage = flt((row.expected_value_after_useful_life * 100) / frm.doc.gross_purchase_amount, precision("salvage_value_percentage", row));
|
||||
frappe.model.set_value(row.doctype, row.name, "salvage_value_percentage", new_salvage_value_percentage);
|
||||
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false;
|
||||
} else if (salvage_value_percentage_changed) {
|
||||
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = true;
|
||||
const new_expected_value_after_useful_life = flt(frm.doc.gross_purchase_amount * (row.salvage_value_percentage / 100), precision('gross_purchase_amount'));
|
||||
frappe.model.set_value(row.doctype, row.name, "expected_value_after_useful_life", new_expected_value_after_useful_life);
|
||||
frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Asset Finance Book', {
|
||||
@@ -516,9 +565,19 @@ frappe.ui.form.on('Asset Finance Book', {
|
||||
|
||||
expected_value_after_useful_life: function(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) {
|
||||
frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, false, true);
|
||||
}
|
||||
frm.events.set_depreciation_rate(frm, row);
|
||||
},
|
||||
|
||||
salvage_value_percentage: function(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
if (!frappe.flags.from_set_salvage_value_percentage_or_expected_value_after_useful_life) {
|
||||
frm.events.set_salvage_value_percentage_or_expected_value_after_useful_life(frm, row, true, false);
|
||||
}
|
||||
},
|
||||
|
||||
frequency_of_depreciation: function(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
frm.events.set_depreciation_rate(frm, row);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"asset_owner",
|
||||
"asset_owner_company",
|
||||
"is_existing_asset",
|
||||
"is_composite_asset",
|
||||
"supplier",
|
||||
"customer",
|
||||
"image",
|
||||
@@ -72,7 +73,8 @@
|
||||
"purchase_receipt_amount",
|
||||
"default_finance_book",
|
||||
"depr_entry_posting_status",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"capitalized_in"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -199,7 +201,7 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Purchase Date",
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -237,10 +239,12 @@
|
||||
"default": "0",
|
||||
"fieldname": "calculate_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Calculate Depreciation"
|
||||
"label": "Calculate Depreciation",
|
||||
"read_only_depends_on": "eval:doc.is_composite_asset && !doc.gross_purchase_amount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.is_composite_asset",
|
||||
"fieldname": "is_existing_asset",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Existing Asset"
|
||||
@@ -492,7 +496,7 @@
|
||||
"fieldname": "asset_quantity",
|
||||
"fieldtype": "Int",
|
||||
"label": "Asset Quantity",
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset"
|
||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
||||
},
|
||||
{
|
||||
"fieldname": "depr_entry_posting_status",
|
||||
@@ -510,6 +514,21 @@
|
||||
"fieldname": "is_fully_depreciated",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Fully Depreciated"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.is_existing_asset",
|
||||
"fieldname": "is_composite_asset",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Composite Asset"
|
||||
},
|
||||
{
|
||||
"fieldname": "capitalized_in",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Capitalized In",
|
||||
"options": "Asset Capitalization",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -538,7 +557,7 @@
|
||||
"table_fieldname": "accounts"
|
||||
}
|
||||
],
|
||||
"modified": "2023-08-10 20:25:09.913073",
|
||||
"modified": "2023-10-03 23:28:26.732269",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -204,7 +204,9 @@ class Asset(AccountsController):
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
|
||||
if self.item_code and not self.get("finance_books"):
|
||||
finance_books = get_item_details(self.item_code, self.asset_category)
|
||||
finance_books = get_item_details(
|
||||
self.item_code, self.asset_category, self.gross_purchase_amount
|
||||
)
|
||||
self.set("finance_books", finance_books)
|
||||
|
||||
def validate_finance_books(self):
|
||||
@@ -232,7 +234,7 @@ class Asset(AccountsController):
|
||||
if not self.asset_category:
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
|
||||
if not flt(self.gross_purchase_amount):
|
||||
if not flt(self.gross_purchase_amount) and not self.is_composite_asset:
|
||||
frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError)
|
||||
|
||||
if is_cwip_accounting_enabled(self.asset_category):
|
||||
@@ -1164,6 +1166,15 @@ def create_asset_repair(asset, asset_name):
|
||||
return asset_repair
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_capitalization(asset):
|
||||
asset_capitalization = frappe.new_doc("Asset Capitalization")
|
||||
asset_capitalization.update(
|
||||
{"target_asset": asset, "capitalization_method": "Choose a WIP composite asset"}
|
||||
)
|
||||
return asset_capitalization
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_asset_value_adjustment(asset, asset_category, company):
|
||||
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
||||
@@ -1195,7 +1206,7 @@ def transfer_asset(args):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_item_details(item_code, asset_category):
|
||||
def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
asset_category_doc = frappe.get_doc("Asset Category", asset_category)
|
||||
books = []
|
||||
for d in asset_category_doc.finance_books:
|
||||
@@ -1205,7 +1216,11 @@ def get_item_details(item_code, asset_category):
|
||||
"depreciation_method": d.depreciation_method,
|
||||
"total_number_of_depreciations": d.total_number_of_depreciations,
|
||||
"frequency_of_depreciation": d.frequency_of_depreciation,
|
||||
"start_date": nowdate(),
|
||||
"daily_depreciation": d.daily_depreciation,
|
||||
"salvage_value_percentage": d.salvage_value_percentage,
|
||||
"expected_value_after_useful_life": flt(gross_purchase_amount)
|
||||
* flt(d.salvage_value_percentage / 100),
|
||||
"depreciation_start_date": d.depreciation_start_date or nowdate(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1686,6 +1686,7 @@ def create_asset(**args):
|
||||
"location": args.location or "Test Location",
|
||||
"asset_owner": args.asset_owner or "Company",
|
||||
"is_existing_asset": args.is_existing_asset or 1,
|
||||
"is_composite_asset": args.is_composite_asset or 0,
|
||||
"asset_quantity": args.get("asset_quantity") or 1,
|
||||
"depr_entry_posting_status": args.depr_entry_posting_status or "",
|
||||
}
|
||||
|
||||
@@ -15,9 +15,15 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
|
||||
refresh() {
|
||||
this.show_general_ledger();
|
||||
|
||||
if ((this.frm.doc.stock_items && this.frm.doc.stock_items.length) || !this.frm.doc.target_is_fixed_asset) {
|
||||
this.show_stock_ledger();
|
||||
}
|
||||
|
||||
if (this.frm.doc.stock_items && !this.frm.doc.stock_items.length && this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") {
|
||||
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
|
||||
this.get_target_asset_details();
|
||||
}
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
@@ -34,18 +40,9 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
});
|
||||
|
||||
me.frm.set_query("target_asset", function() {
|
||||
var filters = {};
|
||||
|
||||
if (me.frm.doc.target_item_code) {
|
||||
filters['item_code'] = me.frm.doc.target_item_code;
|
||||
}
|
||||
|
||||
filters['status'] = ["not in", ["Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"]];
|
||||
filters['docstatus'] = 1;
|
||||
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
filters: {'is_composite_asset': 1, 'docstatus': 0 }
|
||||
}
|
||||
});
|
||||
|
||||
me.frm.set_query("asset", "asset_items", function() {
|
||||
@@ -104,6 +101,39 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
return this.get_target_item_details();
|
||||
}
|
||||
|
||||
target_asset() {
|
||||
if (this.frm.doc.target_asset && this.frm.doc.capitalization_method === "Choose a WIP composite asset") {
|
||||
this.set_consumed_stock_items_tagged_to_wip_composite_asset(this.frm.doc.target_asset);
|
||||
this.get_target_asset_details();
|
||||
}
|
||||
}
|
||||
|
||||
set_consumed_stock_items_tagged_to_wip_composite_asset(asset) {
|
||||
var me = this;
|
||||
|
||||
if (asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_items_tagged_to_wip_composite_asset",
|
||||
args: {
|
||||
asset: asset,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
me.frm.clear_table("stock_items");
|
||||
|
||||
for (let item of r.message) {
|
||||
me.frm.add_child("stock_items", item);
|
||||
}
|
||||
|
||||
refresh_field("stock_items");
|
||||
|
||||
me.calculate_totals();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
item_code(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
if (cdt === "Asset Capitalization Stock Item") {
|
||||
@@ -218,6 +248,26 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
}
|
||||
}
|
||||
|
||||
get_target_asset_details() {
|
||||
var me = this;
|
||||
|
||||
if (me.frm.doc.target_asset) {
|
||||
return me.frm.call({
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_target_asset_details",
|
||||
child: me.frm.doc,
|
||||
args: {
|
||||
asset: me.frm.doc.target_asset,
|
||||
company: me.frm.doc.company,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
me.frm.refresh_fields();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_consumed_stock_item_details(row) {
|
||||
var me = this;
|
||||
|
||||
|
||||
@@ -8,24 +8,25 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"company",
|
||||
"naming_series",
|
||||
"entry_type",
|
||||
"target_item_code",
|
||||
"target_asset",
|
||||
"target_item_name",
|
||||
"target_is_fixed_asset",
|
||||
"target_has_batch_no",
|
||||
"target_has_serial_no",
|
||||
"column_break_9",
|
||||
"target_asset_name",
|
||||
"capitalization_method",
|
||||
"target_item_code",
|
||||
"target_asset_location",
|
||||
"target_asset",
|
||||
"target_asset_name",
|
||||
"target_warehouse",
|
||||
"target_qty",
|
||||
"target_stock_uom",
|
||||
"target_batch_no",
|
||||
"target_serial_no",
|
||||
"column_break_5",
|
||||
"company",
|
||||
"finance_book",
|
||||
"posting_date",
|
||||
"posting_time",
|
||||
@@ -57,12 +58,13 @@
|
||||
"label": "Title"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_item_code && !doc.__islocal && doc.capitalization_method !== 'Choose a WIP composite asset') || ((doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization')",
|
||||
"fieldname": "target_item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Item Code",
|
||||
"options": "Item",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval:(doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset') || doc.entry_type=='Decapitalization'",
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.target_item_code && doc.target_item_name != doc.target_item_code",
|
||||
@@ -86,16 +88,18 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.target_asset && !doc.__islocal) || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"fieldname": "target_asset",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Target Asset",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset'",
|
||||
"no_copy": 1,
|
||||
"options": "Asset",
|
||||
"read_only": 1
|
||||
"read_only_depends_on": "eval:(doc.entry_type=='Decapitalization') || (doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset')"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"depends_on": "eval:(doc.target_asset_name && !doc.__islocal) || (doc.target_asset && doc.entry_type=='Capitalization' && doc.capitalization_method=='Choose a WIP composite asset')",
|
||||
"fetch_from": "target_asset.asset_name",
|
||||
"fieldname": "target_asset_name",
|
||||
"fieldtype": "Data",
|
||||
@@ -186,12 +190,14 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fieldname": "target_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Target Qty",
|
||||
"read_only_depends_on": "eval:doc.entry_type=='Capitalization'"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Decapitalization'",
|
||||
"fetch_from": "target_item_code.stock_uom",
|
||||
"fieldname": "target_stock_uom",
|
||||
"fieldtype": "Link",
|
||||
@@ -331,18 +337,26 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
|
||||
"fieldname": "target_asset_location",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Asset Location",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization' && doc.capitalization_method=='Create a new composite asset'",
|
||||
"options": "Location"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"fieldname": "capitalization_method",
|
||||
"fieldtype": "Select",
|
||||
"label": "Capitalization Method",
|
||||
"mandatory_depends_on": "eval:doc.entry_type=='Capitalization'",
|
||||
"options": "\nCreate a new composite asset\nChoose a WIP composite asset"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-22 14:17:07.995120",
|
||||
"modified": "2023-10-03 22:55:59.461456",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Capitalization",
|
||||
|
||||
@@ -53,6 +53,7 @@ class AssetCapitalization(StockController):
|
||||
self.validate_posting_time()
|
||||
self.set_missing_values(for_validate=True)
|
||||
self.validate_target_item()
|
||||
self.validate_target_asset()
|
||||
self.validate_consumed_stock_item()
|
||||
self.validate_consumed_asset_item()
|
||||
self.validate_service_item()
|
||||
@@ -63,12 +64,12 @@ class AssetCapitalization(StockController):
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_source_mandatory()
|
||||
if self.entry_type == "Capitalization":
|
||||
self.create_target_asset()
|
||||
self.create_target_asset()
|
||||
|
||||
def on_submit(self):
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries()
|
||||
self.update_target_asset()
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
|
||||
@@ -85,6 +86,11 @@ class AssetCapitalization(StockController):
|
||||
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
|
||||
self.set(k, v)
|
||||
|
||||
target_asset_details = get_target_asset_details(self.target_asset, self.company)
|
||||
for k, v in target_asset_details.items():
|
||||
if self.meta.has_field(k) and (not self.get(k) or k in force_fields):
|
||||
self.set(k, v)
|
||||
|
||||
for d in self.stock_items:
|
||||
args = self.as_dict()
|
||||
args.update(d.as_dict())
|
||||
@@ -146,6 +152,33 @@ class AssetCapitalization(StockController):
|
||||
|
||||
self.validate_item(target_item)
|
||||
|
||||
def validate_target_asset(self):
|
||||
if self.target_asset:
|
||||
target_asset = self.get_asset_for_validation(self.target_asset)
|
||||
|
||||
if not target_asset.is_composite_asset:
|
||||
frappe.throw(_("Target Asset {0} needs to be composite asset").format(target_asset.name))
|
||||
|
||||
if target_asset.item_code != self.target_item_code:
|
||||
frappe.throw(
|
||||
_("Asset {0} does not belong to Item {1}").format(self.target_asset, self.target_item_code)
|
||||
)
|
||||
|
||||
if target_asset.status in ("Scrapped", "Sold", "Capitalized", "Decapitalized"):
|
||||
frappe.throw(
|
||||
_("Target Asset {0} cannot be {1}").format(target_asset.name, target_asset.status)
|
||||
)
|
||||
|
||||
if target_asset.docstatus == 1:
|
||||
frappe.throw(_("Target Asset {0} cannot be submitted").format(target_asset.name))
|
||||
elif target_asset.docstatus == 2:
|
||||
frappe.throw(_("Target Asset {0} cannot be cancelled").format(target_asset.name))
|
||||
|
||||
if target_asset.company != self.company:
|
||||
frappe.throw(
|
||||
_("Target Asset {0} does not belong to company {1}").format(target_asset.name, self.company)
|
||||
)
|
||||
|
||||
def validate_consumed_stock_item(self):
|
||||
for d in self.stock_items:
|
||||
if d.item_code:
|
||||
@@ -170,7 +203,23 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
|
||||
asset = self.get_asset_for_validation(d.asset)
|
||||
self.validate_asset(asset)
|
||||
|
||||
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Consumed Asset {1} cannot be {2}").format(d.idx, asset.name, asset.status)
|
||||
)
|
||||
|
||||
if asset.docstatus == 0:
|
||||
frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be Draft").format(d.idx, asset.name))
|
||||
elif asset.docstatus == 2:
|
||||
frappe.throw(_("Row #{0}: Consumed Asset {1} cannot be cancelled").format(d.idx, asset.name))
|
||||
|
||||
if asset.company != self.company:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Consumed Asset {1} does not belong to company {2}").format(
|
||||
d.idx, asset.name, self.company
|
||||
)
|
||||
)
|
||||
|
||||
def validate_service_item(self):
|
||||
for d in self.service_items:
|
||||
@@ -205,21 +254,12 @@ class AssetCapitalization(StockController):
|
||||
|
||||
def get_asset_for_validation(self, asset):
|
||||
return frappe.db.get_value(
|
||||
"Asset", asset, ["name", "item_code", "company", "status", "docstatus"], as_dict=1
|
||||
"Asset",
|
||||
asset,
|
||||
["name", "item_code", "company", "status", "docstatus", "is_composite_asset"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
def validate_asset(self, asset):
|
||||
if asset.status in ("Draft", "Scrapped", "Sold", "Capitalized", "Decapitalized"):
|
||||
frappe.throw(_("Asset {0} is {1}").format(asset.name, asset.status))
|
||||
|
||||
if asset.docstatus == 0:
|
||||
frappe.throw(_("Asset {0} is Draft").format(asset.name))
|
||||
if asset.docstatus == 2:
|
||||
frappe.throw(_("Asset {0} is cancelled").format(asset.name))
|
||||
|
||||
if asset.company != self.company:
|
||||
frappe.throw(_("Asset {0} does not belong to company {1}").format(asset.name, self.company))
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_warehouse_details(self):
|
||||
for d in self.get("stock_items"):
|
||||
@@ -485,16 +525,25 @@ class AssetCapitalization(StockController):
|
||||
)
|
||||
|
||||
def create_target_asset(self):
|
||||
if (
|
||||
self.entry_type != "Capitalization"
|
||||
or self.capitalization_method != "Create a new composite asset"
|
||||
):
|
||||
return
|
||||
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
|
||||
asset_doc = frappe.new_doc("Asset")
|
||||
asset_doc.company = self.company
|
||||
asset_doc.item_code = self.target_item_code
|
||||
asset_doc.is_existing_asset = 1
|
||||
asset_doc.is_composite_asset = 1
|
||||
asset_doc.location = self.target_asset_location
|
||||
asset_doc.available_for_use_date = self.posting_date
|
||||
asset_doc.purchase_date = self.posting_date
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.capitalized_in = self.name
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.insert()
|
||||
|
||||
@@ -510,6 +559,28 @@ class AssetCapitalization(StockController):
|
||||
).format(get_link_to_form("Asset", asset_doc.name))
|
||||
)
|
||||
|
||||
def update_target_asset(self):
|
||||
if (
|
||||
self.entry_type != "Capitalization"
|
||||
or self.capitalization_method != "Choose a WIP composite asset"
|
||||
):
|
||||
return
|
||||
|
||||
total_target_asset_value = flt(self.total_value, self.precision("total_value"))
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.capitalized_in = self.name
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.save()
|
||||
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Asset {0} has been updated. Please set the depreciation details if any and submit it."
|
||||
).format(get_link_to_form("Asset", asset_doc.name))
|
||||
)
|
||||
|
||||
def restore_consumed_asset_items(self):
|
||||
for item in self.asset_items:
|
||||
asset = frappe.get_doc("Asset", item.asset)
|
||||
@@ -568,6 +639,33 @@ def get_target_item_details(item_code=None, company=None):
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_target_asset_details(asset=None, company=None):
|
||||
out = frappe._dict()
|
||||
|
||||
# Get Asset Details
|
||||
asset_details = frappe._dict()
|
||||
if asset:
|
||||
asset_details = frappe.db.get_value("Asset", asset, ["asset_name", "item_code"], as_dict=1)
|
||||
if not asset_details:
|
||||
frappe.throw(_("Asset {0} does not exist").format(asset))
|
||||
|
||||
# Re-set item code from Asset
|
||||
out.target_item_code = asset_details.item_code
|
||||
|
||||
# Set Asset Details
|
||||
out.asset_name = asset_details.asset_name
|
||||
|
||||
if asset_details.item_code:
|
||||
out.target_fixed_asset_account = get_asset_category_account(
|
||||
"fixed_asset_account", item=asset_details.item_code, company=company
|
||||
)
|
||||
else:
|
||||
out.target_fixed_asset_account = None
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_consumed_stock_item_details(args):
|
||||
if isinstance(args, string_types):
|
||||
@@ -716,3 +814,30 @@ def get_service_item_details(args):
|
||||
)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_items_tagged_to_wip_composite_asset(asset):
|
||||
fields = [
|
||||
"item_code",
|
||||
"item_name",
|
||||
"batch_no",
|
||||
"serial_no",
|
||||
"stock_qty",
|
||||
"stock_uom",
|
||||
"warehouse",
|
||||
"cost_center",
|
||||
"qty",
|
||||
"valuation_rate",
|
||||
"amount",
|
||||
]
|
||||
|
||||
pi_items = frappe.get_all(
|
||||
"Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields
|
||||
)
|
||||
|
||||
pr_items = frappe.get_all(
|
||||
"Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields
|
||||
)
|
||||
|
||||
return pi_items + pr_items
|
||||
|
||||
@@ -50,6 +50,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
capitalization_method="Create a new composite asset",
|
||||
target_item_code="Macbook Pro",
|
||||
target_asset_location="Test Location",
|
||||
stock_qty=stock_qty,
|
||||
@@ -139,6 +140,7 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
capitalization_method="Create a new composite asset",
|
||||
target_item_code="Macbook Pro",
|
||||
target_asset_location="Test Location",
|
||||
stock_qty=stock_qty,
|
||||
@@ -203,6 +205,77 @@ class TestAssetCapitalization(unittest.TestCase):
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
def test_capitalization_with_wip_composite_asset(self):
|
||||
company = "_Test Company with perpetual inventory"
|
||||
set_depreciation_settings_in_company(company=company)
|
||||
|
||||
stock_rate = 1000
|
||||
stock_qty = 2
|
||||
stock_amount = 2000
|
||||
|
||||
total_amount = 2000
|
||||
|
||||
wip_composite_asset = create_asset(
|
||||
asset_name="Asset Capitalization WIP Composite Asset",
|
||||
is_composite_asset=1,
|
||||
warehouse="Stores - TCP1",
|
||||
company=company,
|
||||
)
|
||||
|
||||
# Create and submit Asset Captitalization
|
||||
asset_capitalization = create_asset_capitalization(
|
||||
entry_type="Capitalization",
|
||||
capitalization_method="Choose a WIP composite asset",
|
||||
target_asset=wip_composite_asset.name,
|
||||
target_asset_location="Test Location",
|
||||
stock_qty=stock_qty,
|
||||
stock_rate=stock_rate,
|
||||
service_expense_account="Expenses Included In Asset Valuation - TCP1",
|
||||
company=company,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
# Test Asset Capitalization values
|
||||
self.assertEqual(asset_capitalization.entry_type, "Capitalization")
|
||||
self.assertEqual(asset_capitalization.capitalization_method, "Choose a WIP composite asset")
|
||||
self.assertEqual(asset_capitalization.target_qty, 1)
|
||||
|
||||
self.assertEqual(asset_capitalization.stock_items[0].valuation_rate, stock_rate)
|
||||
self.assertEqual(asset_capitalization.stock_items[0].amount, stock_amount)
|
||||
self.assertEqual(asset_capitalization.stock_items_total, stock_amount)
|
||||
|
||||
self.assertEqual(asset_capitalization.total_value, total_amount)
|
||||
self.assertEqual(asset_capitalization.target_incoming_rate, total_amount)
|
||||
|
||||
# Test Target Asset values
|
||||
target_asset = frappe.get_doc("Asset", asset_capitalization.target_asset)
|
||||
self.assertEqual(target_asset.gross_purchase_amount, total_amount)
|
||||
self.assertEqual(target_asset.purchase_receipt_amount, total_amount)
|
||||
|
||||
# Test General Ledger Entries
|
||||
expected_gle = {
|
||||
"_Test Fixed Asset - TCP1": 2000,
|
||||
"_Test Warehouse - TCP1": -2000,
|
||||
}
|
||||
actual_gle = get_actual_gle_dict(asset_capitalization.name)
|
||||
|
||||
self.assertEqual(actual_gle, expected_gle)
|
||||
|
||||
# Test Stock Ledger Entries
|
||||
expected_sle = {
|
||||
("Capitalization Source Stock Item", "_Test Warehouse - TCP1"): {
|
||||
"actual_qty": -stock_qty,
|
||||
"stock_value_difference": -stock_amount,
|
||||
}
|
||||
}
|
||||
actual_sle = get_actual_sle_dict(asset_capitalization.name)
|
||||
self.assertEqual(actual_sle, expected_sle)
|
||||
|
||||
# Cancel Asset Capitalization and make test entries and status are reversed
|
||||
asset_capitalization.cancel()
|
||||
self.assertFalse(get_actual_gle_dict(asset_capitalization.name))
|
||||
self.assertFalse(get_actual_sle_dict(asset_capitalization.name))
|
||||
|
||||
def test_decapitalization_with_depreciation(self):
|
||||
# Variables
|
||||
purchase_date = "2020-01-01"
|
||||
@@ -326,6 +399,7 @@ def create_asset_capitalization(**args):
|
||||
asset_capitalization.update(
|
||||
{
|
||||
"entry_type": args.entry_type or "Capitalization",
|
||||
"capitalization_method": args.capitalization_method or None,
|
||||
"company": company,
|
||||
"posting_date": args.posting_date or now.strftime("%Y-%m-%d"),
|
||||
"posting_time": args.posting_time or now.strftime("%H:%M:%S.%f"),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"column_break_5",
|
||||
"frequency_of_depreciation",
|
||||
"depreciation_start_date",
|
||||
"salvage_value_percentage",
|
||||
"expected_value_after_useful_life",
|
||||
"value_after_depreciation",
|
||||
"rate_of_depreciation"
|
||||
@@ -87,12 +88,17 @@
|
||||
"fieldname": "daily_depreciation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Daily Depreciation"
|
||||
},
|
||||
{
|
||||
"fieldname": "salvage_value_percentage",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Salvage Value Percentage"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 18:56:09.022246",
|
||||
"modified": "2023-09-29 15:39:52.740594",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"disable_last_purchase_rate",
|
||||
"show_pay_button",
|
||||
"use_transaction_date_exchange_rate",
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
@@ -164,6 +165,13 @@
|
||||
"fieldname": "over_order_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Order Allowance (%)"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.",
|
||||
"fieldname": "use_transaction_date_exchange_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Transaction Date Exchange Rate"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@@ -171,7 +179,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-02 17:02:14.404622",
|
||||
"modified": "2023-10-16 16:22:03.201078",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -475,6 +475,7 @@
|
||||
"depends_on": "eval:doc.is_subcontracted",
|
||||
"fieldname": "supplier_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Supplier Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
@@ -1272,7 +1273,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-09-13 16:21:07.361700",
|
||||
"modified": "2023-10-01 20:58:07.851037",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
||||
@@ -68,7 +68,7 @@ frappe.ui.form.on("Supplier", {
|
||||
}, __("View"));
|
||||
|
||||
frm.add_custom_button(__('Accounts Payable'), function () {
|
||||
frappe.set_route('query-report', 'Accounts Payable', { supplier: frm.doc.name });
|
||||
frappe.set_route('query-report', 'Accounts Payable', { party_type: "Supplier", party: frm.doc.name });
|
||||
}, __("View"));
|
||||
|
||||
frm.add_custom_button(__('Bank Account'), function () {
|
||||
|
||||
@@ -8,7 +8,7 @@ def get_data():
|
||||
"This is based on transactions against this Supplier. See timeline below for details"
|
||||
),
|
||||
"fieldname": "supplier",
|
||||
"non_standard_fieldnames": {"Payment Entry": "party_name", "Bank Account": "party"},
|
||||
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||
"transactions": [
|
||||
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
||||
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
||||
|
||||
@@ -6,7 +6,7 @@ import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import date_diff, flt, getdate
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ def get_data(filters):
|
||||
po_item.qty,
|
||||
po_item.received_qty,
|
||||
(po_item.qty - po_item.received_qty).as_("pending_qty"),
|
||||
IfNull(pi_item.qty, 0).as_("billed_qty"),
|
||||
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
|
||||
po_item.base_amount.as_("amount"),
|
||||
(po_item.received_qty * po_item.base_rate).as_("received_qty_amount"),
|
||||
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
|
||||
|
||||
@@ -35,8 +35,12 @@ def get_data(filters):
|
||||
sq_item.parent,
|
||||
sq_item.item_code,
|
||||
sq_item.qty,
|
||||
sq.currency,
|
||||
sq_item.stock_qty,
|
||||
sq_item.amount,
|
||||
sq_item.base_rate,
|
||||
sq_item.base_amount,
|
||||
sq.price_list_currency,
|
||||
sq_item.uom,
|
||||
sq_item.stock_uom,
|
||||
sq_item.request_for_quotation,
|
||||
@@ -105,7 +109,11 @@ def prepare_data(supplier_quotation_data, filters):
|
||||
"qty": data.get("qty"),
|
||||
"price": flt(data.get("amount") * exchange_rate, float_precision),
|
||||
"uom": data.get("uom"),
|
||||
"price_list_currency": data.get("price_list_currency"),
|
||||
"currency": data.get("currency"),
|
||||
"stock_uom": data.get("stock_uom"),
|
||||
"base_amount": flt(data.get("base_amount"), float_precision),
|
||||
"base_rate": flt(data.get("base_rate"), float_precision),
|
||||
"request_for_quotation": data.get("request_for_quotation"),
|
||||
"valid_till": data.get("valid_till"),
|
||||
"lead_time_days": data.get("lead_time_days"),
|
||||
@@ -183,6 +191,8 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
|
||||
|
||||
|
||||
def get_columns(filters):
|
||||
currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency")
|
||||
|
||||
group_by_columns = [
|
||||
{
|
||||
"fieldname": "supplier_name",
|
||||
@@ -203,11 +213,18 @@ def get_columns(filters):
|
||||
columns = [
|
||||
{"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 90},
|
||||
{"fieldname": "qty", "label": _("Quantity"), "fieldtype": "Float", "width": 80},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
"fieldname": "price",
|
||||
"label": _("Price"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"options": "currency",
|
||||
"width": 110,
|
||||
},
|
||||
{
|
||||
@@ -221,9 +238,23 @@ def get_columns(filters):
|
||||
"fieldname": "price_per_unit",
|
||||
"label": _("Price per Unit (Stock UOM)"),
|
||||
"fieldtype": "Currency",
|
||||
"options": "Company:company:default_currency",
|
||||
"options": "currency",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"fieldname": "base_amount",
|
||||
"label": _("Price ({0})").format(currency),
|
||||
"fieldtype": "Currency",
|
||||
"options": "price_list_currency",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"fieldname": "base_rate",
|
||||
"label": _("Price Per Unit ({0})").format(currency),
|
||||
"fieldtype": "Currency",
|
||||
"options": "price_list_currency",
|
||||
"width": 180,
|
||||
},
|
||||
{
|
||||
"fieldname": "quotation",
|
||||
"label": _("Supplier Quotation"),
|
||||
|
||||
@@ -12,6 +12,7 @@ from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
comma_and,
|
||||
flt,
|
||||
fmt_money,
|
||||
formatdate,
|
||||
@@ -180,6 +181,17 @@ class AccountsController(TransactionBase):
|
||||
self.validate_party_account_currency()
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||
if invalid_advances := [
|
||||
x for x in self.advances if not x.reference_type or not x.reference_name
|
||||
]:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
|
||||
).format(
|
||||
frappe.bold(comma_and([x.idx for x in invalid_advances])), frappe.bold(_("Advance Payments"))
|
||||
)
|
||||
)
|
||||
|
||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||
self.set_advances()
|
||||
@@ -211,12 +223,70 @@ class AccountsController(TransactionBase):
|
||||
def before_cancel(self):
|
||||
validate_einvoice_fields(self)
|
||||
|
||||
def _remove_references_in_unreconcile(self):
|
||||
upe = frappe.qb.DocType("Unreconcile Payment Entries")
|
||||
rows = (
|
||||
frappe.qb.from_(upe)
|
||||
.select(upe.name, upe.parent)
|
||||
.where((upe.reference_doctype == self.doctype) & (upe.reference_name == self.name))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if rows:
|
||||
references_map = frappe._dict()
|
||||
for x in rows:
|
||||
references_map.setdefault(x.parent, []).append(x.name)
|
||||
|
||||
for doc, rows in references_map.items():
|
||||
unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc)
|
||||
for row in rows:
|
||||
unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
|
||||
|
||||
unreconcile_doc.flags.ignore_validate_update_after_submit = True
|
||||
unreconcile_doc.flags.ignore_links = True
|
||||
unreconcile_doc.save(ignore_permissions=True)
|
||||
|
||||
# delete docs upon parent doc deletion
|
||||
unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name})
|
||||
for x in unreconcile_docs:
|
||||
_doc = frappe.get_doc("Unreconcile Payments", x.name)
|
||||
if _doc.docstatus == 1:
|
||||
_doc.cancel()
|
||||
_doc.delete()
|
||||
|
||||
def _remove_references_in_repost_doctypes(self):
|
||||
repost_doctypes = ["Repost Payment Ledger Items", "Repost Accounting Ledger Items"]
|
||||
|
||||
for _doctype in repost_doctypes:
|
||||
dt = frappe.qb.DocType(_doctype)
|
||||
rows = (
|
||||
frappe.qb.from_(dt)
|
||||
.select(dt.name, dt.parent, dt.parenttype)
|
||||
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if rows:
|
||||
references_map = frappe._dict()
|
||||
for x in rows:
|
||||
references_map.setdefault((x.parenttype, x.parent), []).append(x.name)
|
||||
|
||||
for doc, rows in references_map.items():
|
||||
repost_doc = frappe.get_doc(doc[0], doc[1])
|
||||
|
||||
for row in rows:
|
||||
if _doctype == "Repost Payment Ledger Items":
|
||||
repost_doc.remove(repost_doc.get("repost_vouchers", {"name": row})[0])
|
||||
else:
|
||||
repost_doc.remove(repost_doc.get("vouchers", {"name": row})[0])
|
||||
|
||||
repost_doc.flags.ignore_validate_update_after_submit = True
|
||||
repost_doc.flags.ignore_links = True
|
||||
repost_doc.save(ignore_permissions=True)
|
||||
|
||||
def on_trash(self):
|
||||
# delete references in 'Repost Payment Ledger'
|
||||
rpi = frappe.qb.DocType("Repost Payment Ledger Items")
|
||||
frappe.qb.from_(rpi).delete().where(
|
||||
(rpi.voucher_type == self.doctype) & (rpi.voucher_no == self.name)
|
||||
).run()
|
||||
self._remove_references_in_repost_doctypes()
|
||||
self._remove_references_in_unreconcile()
|
||||
|
||||
# delete sl and gl entries on deletion of transaction
|
||||
if frappe.db.get_single_value("Accounts Settings", "delete_linked_ledger_entries"):
|
||||
@@ -513,6 +583,17 @@ class AccountsController(TransactionBase):
|
||||
self.currency, self.company_currency, transaction_date, args
|
||||
)
|
||||
|
||||
if (
|
||||
self.currency
|
||||
and buying_or_selling == "Buying"
|
||||
and frappe.db.get_single_value("Buying Settings", "use_transaction_date_exchange_rate")
|
||||
and self.doctype == "Purchase Invoice"
|
||||
):
|
||||
self.use_transaction_date_exchange_rate = True
|
||||
self.conversion_rate = get_exchange_rate(
|
||||
self.currency, self.company_currency, transaction_date, args
|
||||
)
|
||||
|
||||
def set_missing_item_details(self, for_validate=False):
|
||||
"""set missing item values"""
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
@@ -536,6 +617,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.pricing_rules = []
|
||||
|
||||
selected_serial_nos_map = {}
|
||||
for item in self.get("items"):
|
||||
if item.get("item_code"):
|
||||
args = parent_dict.copy()
|
||||
@@ -547,6 +629,7 @@ class AccountsController(TransactionBase):
|
||||
args["ignore_pricing_rule"] = (
|
||||
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
|
||||
)
|
||||
args["ignore_serial_nos"] = selected_serial_nos_map.get(item.get("item_code"))
|
||||
|
||||
if not args.get("transaction_date"):
|
||||
args["transaction_date"] = args.get("posting_date")
|
||||
@@ -603,6 +686,11 @@ class AccountsController(TransactionBase):
|
||||
if ret.get("pricing_rules"):
|
||||
self.apply_pricing_rule_on_items(item, ret)
|
||||
self.set_pricing_rule_details(item, ret)
|
||||
|
||||
if ret.get("serial_no"):
|
||||
selected_serial_nos_map.setdefault(item.get("item_code"), []).extend(
|
||||
ret.get("serial_no").split("\n")
|
||||
)
|
||||
else:
|
||||
# Transactions line item without item code
|
||||
|
||||
@@ -909,7 +997,7 @@ class AccountsController(TransactionBase):
|
||||
party_type, party, party_account, amount_field, order_doctype, order_list, include_unallocated
|
||||
)
|
||||
|
||||
payment_entries = get_advance_payment_entries(
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
party_type, party, party_account, order_doctype, order_list, include_unallocated
|
||||
)
|
||||
|
||||
@@ -1406,7 +1494,7 @@ class AccountsController(TransactionBase):
|
||||
"account": self.additional_discount_account,
|
||||
"against": supplier_or_customer,
|
||||
dr_or_cr: self.base_discount_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
@@ -2126,6 +2214,45 @@ class AccountsController(TransactionBase):
|
||||
_("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx)
|
||||
)
|
||||
|
||||
def check_if_fields_updated(self, fields_to_check, child_tables):
|
||||
# Check if any field affecting accounting entry is altered
|
||||
doc_before_update = self.get_doc_before_save()
|
||||
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
|
||||
|
||||
# Check if opening entry check updated
|
||||
needs_repost = doc_before_update.get("is_opening") != self.is_opening
|
||||
|
||||
if not needs_repost:
|
||||
# Parent Level Accounts excluding party account
|
||||
fields_to_check += accounting_dimensions
|
||||
for field in fields_to_check:
|
||||
if doc_before_update.get(field) != self.get(field):
|
||||
needs_repost = 1
|
||||
break
|
||||
|
||||
if not needs_repost:
|
||||
# Check for child tables
|
||||
for table in child_tables:
|
||||
needs_repost = check_if_child_table_updated(
|
||||
doc_before_update.get(table), self.get(table), child_tables[table]
|
||||
)
|
||||
if needs_repost:
|
||||
break
|
||||
|
||||
return needs_repost
|
||||
|
||||
@frappe.whitelist()
|
||||
def repost_accounting_entries(self):
|
||||
if self.repost_required:
|
||||
repost_ledger = frappe.new_doc("Repost Accounting Ledger")
|
||||
repost_ledger.company = self.company
|
||||
repost_ledger.append("vouchers", {"voucher_type": self.doctype, "voucher_no": self.name})
|
||||
repost_ledger.insert()
|
||||
repost_ledger.submit()
|
||||
self.db_set("repost_required", 0)
|
||||
else:
|
||||
frappe.throw(_("No updates pending for reposting"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
@@ -2349,6 +2476,11 @@ def get_advance_journal_entries(
|
||||
return list(journal_entries)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_advance_payment_entries_for_regional(*args, **kwargs):
|
||||
return get_advance_payment_entries(*args, **kwargs)
|
||||
|
||||
|
||||
def get_advance_payment_entries(
|
||||
party_type,
|
||||
party,
|
||||
@@ -3006,6 +3138,23 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.set_status()
|
||||
|
||||
|
||||
def check_if_child_table_updated(
|
||||
child_table_before_update, child_table_after_update, fields_to_check
|
||||
):
|
||||
accounting_dimensions = get_accounting_dimensions() + ["cost_center", "project"]
|
||||
# Check if any field affecting accounting entry is altered
|
||||
for index, item in enumerate(child_table_after_update):
|
||||
for field in fields_to_check:
|
||||
if child_table_before_update[index].get(field) != item.get(field):
|
||||
return True
|
||||
|
||||
for dimension in accounting_dimensions:
|
||||
if child_table_before_update[index].get(dimension) != item.get(dimension):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def validate_regional(doc):
|
||||
pass
|
||||
|
||||
@@ -14,7 +14,8 @@ from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class QtyMismatchError(ValidationError):
|
||||
@@ -514,9 +515,20 @@ class BuyingController(SubcontractingController):
|
||||
)
|
||||
|
||||
if self.is_return:
|
||||
outgoing_rate = get_rate_for_return(
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
if get_valuation_method(d.item_code) == "Moving Average":
|
||||
previous_sle = get_previous_sle(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
}
|
||||
)
|
||||
outgoing_rate = flt(previous_sle.get("valuation_rate"))
|
||||
else:
|
||||
outgoing_rate = get_rate_for_return(
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
|
||||
sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1})
|
||||
if d.from_warehouse:
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime
|
||||
|
||||
from erpnext.accounts.party import render_address
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
@@ -282,7 +282,9 @@ class SellingController(StockController):
|
||||
last_valuation_rate_in_sales_uom = last_valuation_rate * (item.conversion_factor or 1)
|
||||
|
||||
if flt(item.base_net_rate) < flt(last_valuation_rate_in_sales_uom):
|
||||
throw_message(item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate")
|
||||
throw_message(
|
||||
item.idx, item.item_name, last_valuation_rate_in_sales_uom, "valuation rate (Moving Average)"
|
||||
)
|
||||
|
||||
def get_item_list(self):
|
||||
il = []
|
||||
@@ -589,7 +591,9 @@ class SellingController(StockController):
|
||||
|
||||
for address_field, address_display_field in address_dict.items():
|
||||
if self.get(address_field):
|
||||
self.set(address_display_field, get_address_display(self.get(address_field)))
|
||||
self.set(
|
||||
address_display_field, render_address(self.get(address_field), check_permissions=False)
|
||||
)
|
||||
|
||||
def validate_for_duplicate_items(self):
|
||||
check_list, chk_dupl_itm = [], []
|
||||
|
||||
@@ -190,7 +190,9 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
item.net_rate = item.rate
|
||||
|
||||
if not item.qty and self.doc.get("is_return"):
|
||||
if (
|
||||
not item.qty and self.doc.get("is_return") and self.doc.get("doctype") != "Purchase Receipt"
|
||||
):
|
||||
item.amount = flt(-1 * item.rate, item.precision("amount"))
|
||||
elif not item.qty and self.doc.get("is_debit_note"):
|
||||
item.amount = flt(item.rate, item.precision("amount"))
|
||||
|
||||
@@ -382,7 +382,7 @@ def get_lead_details(lead, posting_date=None, company=None):
|
||||
}
|
||||
)
|
||||
|
||||
set_address_details(out, lead, "Lead")
|
||||
set_address_details(out, lead, "Lead", company=company)
|
||||
|
||||
taxes_and_charges = set_taxes(
|
||||
None,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Concat_ws, Date
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -69,53 +70,41 @@ def get_columns():
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
`tabLead`.name,
|
||||
`tabLead`.lead_name,
|
||||
`tabLead`.status,
|
||||
`tabLead`.lead_owner,
|
||||
`tabLead`.territory,
|
||||
`tabLead`.source,
|
||||
`tabLead`.email_id,
|
||||
`tabLead`.mobile_no,
|
||||
`tabLead`.phone,
|
||||
`tabLead`.owner,
|
||||
`tabLead`.company,
|
||||
concat_ws(', ',
|
||||
trim(',' from `tabAddress`.address_line1),
|
||||
trim(',' from tabAddress.address_line2)
|
||||
) AS address,
|
||||
`tabAddress`.state,
|
||||
`tabAddress`.pincode,
|
||||
`tabAddress`.country
|
||||
FROM
|
||||
`tabLead` left join `tabDynamic Link` on (
|
||||
`tabLead`.name = `tabDynamic Link`.link_name and
|
||||
`tabDynamic Link`.parenttype = 'Address')
|
||||
left join `tabAddress` on (
|
||||
`tabAddress`.name=`tabDynamic Link`.parent)
|
||||
WHERE
|
||||
company = %(company)s
|
||||
AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s
|
||||
{conditions}
|
||||
ORDER BY
|
||||
`tabLead`.creation asc """.format(
|
||||
conditions=get_conditions(filters)
|
||||
),
|
||||
filters,
|
||||
as_dict=1,
|
||||
lead = frappe.qb.DocType("Lead")
|
||||
address = frappe.qb.DocType("Address")
|
||||
dynamic_link = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(lead)
|
||||
.left_join(dynamic_link)
|
||||
.on((lead.name == dynamic_link.link_name) & (dynamic_link.parenttype == "Address"))
|
||||
.left_join(address)
|
||||
.on(address.name == dynamic_link.parent)
|
||||
.select(
|
||||
lead.name,
|
||||
lead.lead_name,
|
||||
lead.status,
|
||||
lead.lead_owner,
|
||||
lead.territory,
|
||||
lead.source,
|
||||
lead.email_id,
|
||||
lead.mobile_no,
|
||||
lead.phone,
|
||||
lead.owner,
|
||||
lead.company,
|
||||
(Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"),
|
||||
address.state,
|
||||
address.pincode,
|
||||
address.country,
|
||||
)
|
||||
.where(lead.company == filters.company)
|
||||
.where(Date(lead.creation).between(filters.from_date, filters.to_date))
|
||||
)
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
|
||||
if filters.get("territory"):
|
||||
conditions.append(" and `tabLead`.territory=%(territory)s")
|
||||
query = query.where(lead.territory == filters.get("territory"))
|
||||
|
||||
if filters.get("status"):
|
||||
conditions.append(" and `tabLead`.status=%(status)s")
|
||||
query = query.where(lead.status == filters.get("status"))
|
||||
|
||||
return " ".join(conditions) if conditions else ""
|
||||
return query.run(as_dict=1)
|
||||
|
||||
@@ -17,7 +17,6 @@ from erpnext.e_commerce.shopping_cart.cart import (
|
||||
request_for_quotation,
|
||||
update_cart,
|
||||
)
|
||||
from erpnext.tests.utils import create_test_contact_and_address
|
||||
|
||||
|
||||
class TestShoppingCart(unittest.TestCase):
|
||||
@@ -28,7 +27,6 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
create_test_contact_and_address()
|
||||
self.enable_shopping_cart()
|
||||
if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
|
||||
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
|
||||
@@ -46,48 +44,57 @@ class TestShoppingCart(unittest.TestCase):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_get_cart_new_user(self):
|
||||
self.login_as_new_user()
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
# test if lead is created and quotation with new lead is fetched
|
||||
quotation = _get_cart_quotation()
|
||||
customer = frappe.get_doc("Customer", "_Test Customer 2")
|
||||
quotation = _get_cart_quotation(party=customer)
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(
|
||||
quotation.contact_person,
|
||||
frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")),
|
||||
frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")),
|
||||
)
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
|
||||
return quotation
|
||||
|
||||
def test_get_cart_customer(self):
|
||||
def validate_quotation():
|
||||
def test_get_cart_customer(self, customer="_Test Customer 2"):
|
||||
def validate_quotation(customer_name):
|
||||
# test if quotation with customer is fetched
|
||||
quotation = _get_cart_quotation()
|
||||
party = frappe.get_doc("Customer", customer_name)
|
||||
quotation = _get_cart_quotation(party=party)
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(quotation.party_name, "_Test Customer")
|
||||
self.assertEqual(quotation.party_name, customer_name)
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
return quotation
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
validate_quotation()
|
||||
|
||||
self.login_as_customer()
|
||||
quotation = validate_quotation()
|
||||
|
||||
quotation = validate_quotation(customer)
|
||||
return quotation
|
||||
|
||||
def test_add_to_cart(self):
|
||||
self.login_as_customer()
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
# clear existing quotations
|
||||
self.clear_existing_quotations()
|
||||
|
||||
# add first item
|
||||
update_cart("_Test Item", 1)
|
||||
|
||||
quotation = self.test_get_cart_customer()
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 1)
|
||||
@@ -95,7 +102,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
# add second item
|
||||
update_cart("_Test Item 2", 1)
|
||||
quotation = self.test_get_cart_customer()
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2")
|
||||
self.assertEqual(quotation.get("items")[1].qty, 1)
|
||||
self.assertEqual(quotation.get("items")[1].amount, 20)
|
||||
@@ -108,7 +115,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
# update first item
|
||||
update_cart("_Test Item", 5)
|
||||
quotation = self.test_get_cart_customer()
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 5)
|
||||
self.assertEqual(quotation.get("items")[0].amount, 50)
|
||||
@@ -121,7 +128,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
# remove first item
|
||||
update_cart("_Test Item", 0)
|
||||
quotation = self.test_get_cart_customer()
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 1)
|
||||
@@ -132,7 +139,17 @@ class TestShoppingCart(unittest.TestCase):
|
||||
@unittest.skip("Flaky in CI")
|
||||
def test_tax_rule(self):
|
||||
self.create_tax_rule()
|
||||
self.login_as_customer()
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
|
||||
quotation = self.create_quotation()
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
@@ -320,7 +337,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
if frappe.db.exists("User", email):
|
||||
return
|
||||
|
||||
frappe.get_doc(
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"user_type": "Website User",
|
||||
@@ -330,6 +347,40 @@ class TestShoppingCart(unittest.TestCase):
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
user.add_roles("Customer")
|
||||
|
||||
|
||||
def create_address_and_contact(**kwargs):
|
||||
if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": kwargs.get("address_title"),
|
||||
"address_type": kwargs.get("address_type") or "Office",
|
||||
"address_line1": kwargs.get("address_line1") or "Station Road",
|
||||
"city": kwargs.get("city") or "_Test City",
|
||||
"state": kwargs.get("state") or "Test State",
|
||||
"country": kwargs.get("country") or "India",
|
||||
"links": [
|
||||
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}):
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": kwargs.get("first_name"),
|
||||
"links": [
|
||||
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
|
||||
],
|
||||
}
|
||||
)
|
||||
contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True)
|
||||
contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True)
|
||||
contact.insert()
|
||||
|
||||
|
||||
test_dependencies = [
|
||||
"Sales Taxes and Charges Template",
|
||||
|
||||
@@ -517,6 +517,7 @@ accounting_dimension_doctypes = [
|
||||
"Sales Invoice Item",
|
||||
"Purchase Invoice Item",
|
||||
"Purchase Order Item",
|
||||
"Sales Order Item",
|
||||
"Journal Entry Account",
|
||||
"Material Request Item",
|
||||
"Delivery Note Item",
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the system won't create material requests for the available items.",
|
||||
"description": "If enabled, the system will create material requests even if the stock exists in the 'Raw Materials Warehouse'.",
|
||||
"fieldname": "ignore_existing_ordered_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ignore Available Stock"
|
||||
@@ -422,7 +422,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-28 13:37:43.926686",
|
||||
"modified": "2023-09-29 11:41:03.246059",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -1508,6 +1508,10 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
|
||||
from erpnext.stock.doctype.pick_list.pick_list import get_available_item_locations
|
||||
|
||||
stock_uom, purchase_uom = frappe.db.get_value(
|
||||
"Item", item.get("item_code"), ["stock_uom", "purchase_uom"]
|
||||
)
|
||||
|
||||
locations = get_available_item_locations(
|
||||
item.get("item_code"), warehouses, item.get("quantity"), company, ignore_validation=True
|
||||
)
|
||||
@@ -1518,6 +1522,10 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
|
||||
if required_qty <= 0:
|
||||
return
|
||||
|
||||
conversion_factor = 1.0
|
||||
if purchase_uom != stock_uom and purchase_uom == item["uom"]:
|
||||
conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"])
|
||||
|
||||
new_dict = copy.deepcopy(item)
|
||||
quantity = required_qty if d.get("qty") > required_qty else d.get("qty")
|
||||
|
||||
@@ -1530,25 +1538,14 @@ def get_materials_from_other_locations(item, warehouses, new_mr_items, company):
|
||||
}
|
||||
)
|
||||
|
||||
required_qty -= quantity
|
||||
required_qty -= quantity / conversion_factor
|
||||
new_mr_items.append(new_dict)
|
||||
|
||||
# raise purchase request for remaining qty
|
||||
if required_qty:
|
||||
stock_uom, purchase_uom = frappe.db.get_value(
|
||||
"Item", item["item_code"], ["stock_uom", "purchase_uom"]
|
||||
)
|
||||
|
||||
if purchase_uom != stock_uom and purchase_uom == item["uom"]:
|
||||
conversion_factor = get_uom_conversion_factor(item["item_code"], item["uom"])
|
||||
if not (conversion_factor or frappe.flags.show_qty_in_stock_uom):
|
||||
frappe.throw(
|
||||
_("UOM Conversion factor ({0} -> {1}) not found for item: {2}").format(
|
||||
purchase_uom, stock_uom, item["item_code"]
|
||||
)
|
||||
)
|
||||
|
||||
required_qty = required_qty / conversion_factor
|
||||
precision = frappe.get_precision("Material Request Plan Item", "quantity")
|
||||
if flt(required_qty, precision) > 0:
|
||||
required_qty = required_qty
|
||||
|
||||
if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
|
||||
required_qty = ceil(required_qty)
|
||||
@@ -1619,18 +1616,25 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
table = frappe.qb.DocType("Production Plan")
|
||||
child = frappe.qb.DocType("Material Request Plan Item")
|
||||
|
||||
non_completed_production_plans = get_non_completed_production_plans()
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0)))
|
||||
.select(Sum(child.required_bom_qty))
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (child.item_code == item_code)
|
||||
& (child.warehouse == warehouse)
|
||||
& (table.status.notin(["Completed", "Closed"]))
|
||||
)
|
||||
).run()
|
||||
)
|
||||
|
||||
if non_completed_production_plans:
|
||||
query = query.where(table.name.isin(non_completed_production_plans))
|
||||
|
||||
query = query.run()
|
||||
|
||||
if not query:
|
||||
return 0.0
|
||||
@@ -1638,7 +1642,9 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
reserved_qty_for_production_plan = flt(query[0][0])
|
||||
|
||||
reserved_qty_for_production = flt(
|
||||
get_reserved_qty_for_production(item_code, warehouse, check_production_plan=True)
|
||||
get_reserved_qty_for_production(
|
||||
item_code, warehouse, non_completed_production_plans, check_production_plan=True
|
||||
)
|
||||
)
|
||||
|
||||
if reserved_qty_for_production > reserved_qty_for_production_plan:
|
||||
@@ -1647,6 +1653,25 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
return reserved_qty_for_production_plan - reserved_qty_for_production
|
||||
|
||||
|
||||
def get_non_completed_production_plans():
|
||||
table = frappe.qb.DocType("Production Plan")
|
||||
child = frappe.qb.DocType("Production Plan Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(table.name)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.status.notin(["Completed", "Closed"]))
|
||||
& (child.planned_qty > child.ordered_qty)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return list(set([d.name for d in query]))
|
||||
|
||||
|
||||
def get_raw_materials_of_sub_assembly_items(
|
||||
item_details, company, bom_no, include_non_stock_items, sub_assembly_items, planned_qty=1
|
||||
):
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.utils import add_to_date, flt, getdate, now_datetime, nowdate
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_items_for_material_requests,
|
||||
get_non_completed_production_plans,
|
||||
get_sales_orders,
|
||||
get_warehouse_list,
|
||||
)
|
||||
@@ -1092,6 +1093,49 @@ class TestProductionPlan(FrappeTestCase):
|
||||
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
def test_resered_qty_for_production_plan_for_less_rm_qty(self):
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
|
||||
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
pln = create_production_plan(item_code="Test Production Item 1", planned_qty=10)
|
||||
|
||||
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
self.assertEqual(after_qty - before_qty, 10)
|
||||
|
||||
pln.make_work_order()
|
||||
|
||||
plans = []
|
||||
for row in frappe.get_all("Work Order", filters={"production_plan": pln.name}, fields=["name"]):
|
||||
wo_doc = frappe.get_doc("Work Order", row.name)
|
||||
wo_doc.source_warehouse = "_Test Warehouse - _TC"
|
||||
wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC"
|
||||
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
|
||||
for d in wo_doc.required_items:
|
||||
d.source_warehouse = "_Test Warehouse - _TC"
|
||||
d.required_qty -= 5
|
||||
make_stock_entry(
|
||||
item_code=d.item_code,
|
||||
qty=d.required_qty,
|
||||
rate=100,
|
||||
target="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
wo_doc.submit()
|
||||
plans.append(pln.name)
|
||||
|
||||
bin_name = get_or_make_bin("Raw Material Item 1", "_Test Warehouse - _TC")
|
||||
after_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
|
||||
|
||||
self.assertEqual(after_qty, before_qty)
|
||||
|
||||
completed_plans = get_non_completed_production_plans()
|
||||
for plan in plans:
|
||||
self.assertFalse(plan in completed_plans)
|
||||
|
||||
def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self):
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
@@ -1219,6 +1263,64 @@ class TestProductionPlan(FrappeTestCase):
|
||||
if row.item_code == "SubAssembly2 For SUB Test":
|
||||
self.assertEqual(row.quantity, 10)
|
||||
|
||||
def test_transfer_and_purchase_mrp_for_purchase_uom(self):
|
||||
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
bom_tree = {
|
||||
"Test FG Item INK PEN": {
|
||||
"Test RM Item INK": {},
|
||||
}
|
||||
}
|
||||
|
||||
parent_bom = create_nested_bom(bom_tree, prefix="")
|
||||
if not frappe.db.exists("UOM Conversion Detail", {"parent": "Test RM Item INK", "uom": "Kg"}):
|
||||
doc = frappe.get_doc("Item", "Test RM Item INK")
|
||||
doc.purchase_uom = "Kg"
|
||||
doc.append("uoms", {"uom": "Kg", "conversion_factor": 0.5})
|
||||
doc.save()
|
||||
|
||||
wh1 = create_warehouse("PNE Warehouse", company="_Test Company")
|
||||
wh2 = create_warehouse("MBE Warehouse", company="_Test Company")
|
||||
mrp_warhouse = create_warehouse("MRPBE Warehouse", company="_Test Company")
|
||||
|
||||
make_stock_entry(
|
||||
item_code="Test RM Item INK",
|
||||
qty=2,
|
||||
rate=100,
|
||||
target=wh1,
|
||||
)
|
||||
|
||||
make_stock_entry(
|
||||
item_code="Test RM Item INK",
|
||||
qty=2,
|
||||
rate=100,
|
||||
target=wh2,
|
||||
)
|
||||
|
||||
plan = create_production_plan(
|
||||
item_code=parent_bom.item,
|
||||
planned_qty=10,
|
||||
do_not_submit=1,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
plan.for_warehouse = mrp_warhouse
|
||||
|
||||
items = get_items_for_material_requests(
|
||||
plan.as_dict(), warehouses=[{"warehouse": wh1}, {"warehouse": wh2}]
|
||||
)
|
||||
|
||||
for row in items:
|
||||
row = frappe._dict(row)
|
||||
if row.material_request_type == "Material Transfer":
|
||||
self.assertTrue(row.from_warehouse in [wh1, wh2])
|
||||
self.assertEqual(row.quantity, 2)
|
||||
|
||||
if row.material_request_type == "Purchase":
|
||||
self.assertTrue(row.warehouse == mrp_warhouse)
|
||||
self.assertEqual(row.quantity, 12)
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
|
||||
@@ -362,10 +362,10 @@ class WorkOrder(Document):
|
||||
else:
|
||||
self.update_work_order_qty_in_so()
|
||||
|
||||
self.update_ordered_qty()
|
||||
self.update_reserved_qty_for_production()
|
||||
self.update_completed_qty_in_material_request()
|
||||
self.update_planned_qty()
|
||||
self.update_ordered_qty()
|
||||
self.create_job_card()
|
||||
|
||||
def on_cancel(self):
|
||||
@@ -1491,7 +1491,10 @@ def create_pick_list(source_name, target_doc=None, for_qty=None):
|
||||
|
||||
|
||||
def get_reserved_qty_for_production(
|
||||
item_code: str, warehouse: str, check_production_plan: bool = False
|
||||
item_code: str,
|
||||
warehouse: str,
|
||||
non_completed_production_plans: list = None,
|
||||
check_production_plan: bool = False,
|
||||
) -> float:
|
||||
"""Get total reserved quantity for any item in specified warehouse"""
|
||||
wo = frappe.qb.DocType("Work Order")
|
||||
@@ -1513,16 +1516,22 @@ def get_reserved_qty_for_production(
|
||||
& (wo_item.parent == wo.name)
|
||||
& (wo.docstatus == 1)
|
||||
& (wo_item.source_warehouse == warehouse)
|
||||
& (wo.status.notin(["Stopped", "Completed", "Closed"]))
|
||||
& (
|
||||
(wo_item.required_qty > wo_item.transferred_qty)
|
||||
| (wo_item.required_qty > wo_item.consumed_qty)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if check_production_plan:
|
||||
query = query.where(wo.production_plan.isnotnull())
|
||||
else:
|
||||
query = query.where(
|
||||
(wo.status.notin(["Stopped", "Completed", "Closed"]))
|
||||
& (
|
||||
(wo_item.required_qty > wo_item.transferred_qty)
|
||||
| (wo_item.required_qty > wo_item.consumed_qty)
|
||||
)
|
||||
)
|
||||
|
||||
if non_completed_production_plans:
|
||||
query = query.where(wo.production_plan.isin(non_completed_production_plans))
|
||||
|
||||
return query.run()[0][0] or 0.0
|
||||
|
||||
|
||||
@@ -341,5 +341,6 @@ execute:frappe.defaults.clear_default("fiscal_year")
|
||||
execute:frappe.db.set_single_value('Selling Settings', 'allow_negative_rates_for_items', 0)
|
||||
erpnext.patches.v14_0.correct_asset_value_if_je_with_workflow
|
||||
erpnext.patches.v14_0.migrate_deferred_accounts_to_item_defaults
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_sales_order_item
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
create_accounting_dimensions_for_doctype,
|
||||
)
|
||||
|
||||
|
||||
def execute():
|
||||
create_accounting_dimensions_for_doctype(doctype="Sales Order Item")
|
||||
@@ -60,7 +60,6 @@
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Subject",
|
||||
"reqd": 1,
|
||||
@@ -140,7 +139,6 @@
|
||||
"fieldname": "parent_task",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Parent Task",
|
||||
"options": "Task",
|
||||
"search_index": 1
|
||||
@@ -398,7 +396,7 @@
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2023-09-06 13:52:05.861175",
|
||||
"modified": "2023-09-28 13:52:05.861175",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Task",
|
||||
|
||||
@@ -1,156 +1,52 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2015-04-29 04:52:48.868079",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2015-04-29 04:52:48.868079",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"task",
|
||||
"column_break_2",
|
||||
"subject",
|
||||
"project"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "task",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Task",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Task",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "task",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Task",
|
||||
"options": "Task"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Subject",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fetch_from": "task.subject",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Project",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Text",
|
||||
"label": "Project",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-02-24 04:56:04.862502",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Task Depends On",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-17 12:45:21.536165",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Task Depends On",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -135,7 +135,15 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
}
|
||||
else {
|
||||
// allow for '0' qty on Credit/Debit notes
|
||||
let qty = item.qty || (me.frm.doc.is_debit_note ? 1 : -1);
|
||||
let qty = flt(item.qty);
|
||||
if (!qty) {
|
||||
qty = (me.frm.doc.is_debit_note ? 1 : -1);
|
||||
if (me.frm.doc.doctype !== "Purchase Receipt" && me.frm.doc.is_return === 1) {
|
||||
// In case of Purchase Receipt, qty can be 0 if all items are rejected
|
||||
qty = flt(item.qty);
|
||||
}
|
||||
}
|
||||
|
||||
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import "./utils/customer_quick_entry";
|
||||
import "./utils/supplier_quick_entry";
|
||||
import "./call_popup/call_popup";
|
||||
import "./utils/dimension_tree_filter";
|
||||
import "./utils/unreconcile.js";
|
||||
import "./utils/barcode_scanner";
|
||||
import "./telephony";
|
||||
import "./templates/call_link.html";
|
||||
|
||||
@@ -666,6 +666,9 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
}).show();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
erpnext.utils.map_current_doc = function(opts) {
|
||||
function _map() {
|
||||
if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) {
|
||||
|
||||
127
erpnext/public/js/utils/unreconcile.js
Normal file
127
erpnext/public/js/utils/unreconcile.js
Normal file
@@ -0,0 +1,127 @@
|
||||
frappe.provide('erpnext.accounts');
|
||||
|
||||
erpnext.accounts.unreconcile_payments = {
|
||||
add_unreconcile_btn(frm) {
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry"))
|
||||
|| !["Purchase Invoice", "Sales Invoice", "Journal Entry", "Payment Entry"].includes(frm.doc.doctype)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references",
|
||||
"args": {
|
||||
"doctype": frm.doc.doctype,
|
||||
"docname": frm.doc.name
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frm.add_custom_button(__("Un-Reconcile"), function() {
|
||||
erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
|
||||
}, __('Actions'));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
build_selection_map(frm, selections) {
|
||||
// assuming each row is an individual voucher
|
||||
// pass this to server side method that creates unreconcile doc for each row
|
||||
let selection_map = [];
|
||||
if (['Sales Invoice', 'Purchase Invoice'].includes(frm.doc.doctype)) {
|
||||
selection_map = selections.map(function(elem) {
|
||||
return {
|
||||
company: elem.company,
|
||||
voucher_type: elem.voucher_type,
|
||||
voucher_no: elem.voucher_no,
|
||||
against_voucher_type: frm.doc.doctype,
|
||||
against_voucher_no: frm.doc.name
|
||||
};
|
||||
});
|
||||
} else if (['Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) {
|
||||
selection_map = selections.map(function(elem) {
|
||||
return {
|
||||
company: elem.company,
|
||||
voucher_type: frm.doc.doctype,
|
||||
voucher_no: frm.doc.name,
|
||||
against_voucher_type: elem.voucher_type,
|
||||
against_voucher_no: elem.voucher_no,
|
||||
};
|
||||
});
|
||||
}
|
||||
return selection_map;
|
||||
},
|
||||
|
||||
build_unreconcile_dialog(frm) {
|
||||
if (['Sales Invoice', 'Purchase Invoice', 'Payment Entry', 'Journal Entry'].includes(frm.doc.doctype)) {
|
||||
let child_table_fields = [
|
||||
{ label: __("Voucher Type"), fieldname: "voucher_type", fieldtype: "Dynamic Link", options: "DocType", in_list_view: 1, read_only: 1},
|
||||
{ label: __("Voucher No"), fieldname: "voucher_no", fieldtype: "Link", options: "voucher_type", in_list_view: 1, read_only: 1 },
|
||||
{ label: __("Allocated Amount"), fieldname: "allocated_amount", fieldtype: "Currency", in_list_view: 1, read_only: 1 , options: "account_currency"},
|
||||
{ label: __("Currency"), fieldname: "account_currency", fieldtype: "Currency", read_only: 1},
|
||||
]
|
||||
let unreconcile_dialog_fields = [
|
||||
{
|
||||
label: __('Allocations'),
|
||||
fieldname: 'allocations',
|
||||
fieldtype: 'Table',
|
||||
read_only: 1,
|
||||
fields: child_table_fields,
|
||||
},
|
||||
];
|
||||
|
||||
// get linked payments
|
||||
frappe.call({
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc",
|
||||
"args": {
|
||||
"company": frm.doc.company,
|
||||
"doctype": frm.doc.doctype,
|
||||
"docname": frm.doc.name
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
// populate child table with allocations
|
||||
unreconcile_dialog_fields[0].data = r.message;
|
||||
unreconcile_dialog_fields[0].get_data = function(){ return r.message};
|
||||
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: 'Un-Reconcile Allocations',
|
||||
fields: unreconcile_dialog_fields,
|
||||
size: 'large',
|
||||
cannot_add_rows: true,
|
||||
primary_action_label: 'Un-Reconcile',
|
||||
primary_action(values) {
|
||||
|
||||
let selected_allocations = values.allocations.filter(x=>x.__checked);
|
||||
if (selected_allocations.length > 0) {
|
||||
let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations);
|
||||
erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map);
|
||||
d.hide();
|
||||
|
||||
} else {
|
||||
frappe.msgprint("No Selection");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
d.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
create_unreconcile_docs(selection_map) {
|
||||
frappe.call({
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection",
|
||||
"args": {
|
||||
"selections": selection_map
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -177,7 +177,8 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
|
||||
"parent": invoice.name,
|
||||
"item_tax_template": vat_setting.item_tax_template,
|
||||
},
|
||||
fields=["item_code", "base_net_amount"],
|
||||
fields=["item_code", "sum(base_net_amount) as base_net_amount"],
|
||||
group_by="item_code, item_tax_template",
|
||||
)
|
||||
|
||||
for item in invoice_items:
|
||||
|
||||
@@ -118,7 +118,7 @@ frappe.ui.form.on("Customer", {
|
||||
// custom buttons
|
||||
|
||||
frm.add_custom_button(__('Accounts Receivable'), function () {
|
||||
frappe.set_route('query-report', 'Accounts Receivable', {customer:frm.doc.name});
|
||||
frappe.set_route('query-report', 'Accounts Receivable', { party_type: "Customer", party: frm.doc.name });
|
||||
}, __('View'));
|
||||
|
||||
frm.add_custom_button(__('Accounting Ledger'), function () {
|
||||
|
||||
@@ -236,6 +236,7 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.margin_type || doc.discount_amount",
|
||||
"fieldname": "discount_and_margin",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Discount and Margin"
|
||||
@@ -667,7 +668,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-06 11:00:07.042364",
|
||||
"modified": "2023-09-27 14:02:12.332407",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation Item",
|
||||
|
||||
@@ -542,29 +542,37 @@ def close_or_unclose_sales_orders(names, status):
|
||||
|
||||
|
||||
def get_requested_item_qty(sales_order):
|
||||
return frappe._dict(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select sales_order_item, sum(qty)
|
||||
from `tabMaterial Request Item`
|
||||
where docstatus = 1
|
||||
and sales_order = %s
|
||||
group by sales_order_item
|
||||
""",
|
||||
sales_order,
|
||||
)
|
||||
)
|
||||
result = {}
|
||||
for d in frappe.db.get_all(
|
||||
"Material Request Item",
|
||||
filters={"docstatus": 1, "sales_order": sales_order},
|
||||
fields=["sales_order_item", "sum(qty) as qty", "sum(received_qty) as received_qty"],
|
||||
group_by="sales_order_item",
|
||||
):
|
||||
result[d.sales_order_item] = frappe._dict({"qty": d.qty, "received_qty": d.received_qty})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_material_request(source_name, target_doc=None):
|
||||
requested_item_qty = get_requested_item_qty(source_name)
|
||||
|
||||
def get_remaining_qty(so_item):
|
||||
return flt(
|
||||
flt(so_item.qty)
|
||||
- flt(requested_item_qty.get(so_item.name, {}).get("qty"))
|
||||
- max(
|
||||
flt(so_item.get("delivered_qty"))
|
||||
- flt(requested_item_qty.get(so_item.name, {}).get("received_qty")),
|
||||
0,
|
||||
)
|
||||
)
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
# 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) - flt(source.get("delivered_qty"))
|
||||
target.qty = get_remaining_qty(source)
|
||||
target.stock_qty = flt(target.qty) * flt(target.conversion_factor)
|
||||
|
||||
args = target.as_dict().copy()
|
||||
@@ -597,8 +605,8 @@ def make_material_request(source_name, target_doc=None):
|
||||
"Sales Order Item": {
|
||||
"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 - flt(doc.get("delivered_qty"))) > requested_item_qty.get(doc.name, 0),
|
||||
"condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code)
|
||||
and get_remaining_qty(item) > 0,
|
||||
"postprocess": update_item,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"total_weight",
|
||||
"column_break_21",
|
||||
"weight_uom",
|
||||
"accounting_dimensions_section",
|
||||
"warehouse_and_reference",
|
||||
"warehouse",
|
||||
"target_warehouse",
|
||||
@@ -868,12 +869,18 @@
|
||||
"label": "Production Plan Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-28 14:56:42.031636",
|
||||
"modified": "2023-10-17 18:18:26.475259",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
@@ -884,4 +891,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -121,6 +121,7 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
# Update Currency Exchange Rate
|
||||
settings = frappe.get_single("Currency Exchange Settings")
|
||||
settings.service_provider = "exchangerate.host"
|
||||
settings.access_key = "12345667890"
|
||||
settings.save()
|
||||
|
||||
# Update exchange
|
||||
|
||||
@@ -616,6 +616,7 @@
|
||||
"fieldname": "relieving_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Relieving Date",
|
||||
"no_copy": 1,
|
||||
"mandatory_depends_on": "eval:doc.status == \"Left\"",
|
||||
"oldfieldname": "relieving_date",
|
||||
"oldfieldtype": "Date"
|
||||
@@ -822,7 +823,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2023-03-30 15:57:05.174592",
|
||||
"modified": "2023-10-04 10:57:05.174592",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Employee",
|
||||
@@ -870,4 +871,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "employee_name"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ def after_install():
|
||||
add_app_name()
|
||||
setup_log_settings()
|
||||
hide_workspaces()
|
||||
update_roles()
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@@ -214,6 +215,12 @@ def hide_workspaces():
|
||||
frappe.db.set_value("Workspace", ws, "public", 0)
|
||||
|
||||
|
||||
def update_roles():
|
||||
website_user_roles = ("Customer", "Supplier")
|
||||
for role in website_user_roles:
|
||||
frappe.db.set_value("Role", role, "desk_access", 0)
|
||||
|
||||
|
||||
def create_default_role_profiles():
|
||||
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
||||
role_profile = frappe.new_doc("Role Profile")
|
||||
|
||||
@@ -81,6 +81,11 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
if entries:
|
||||
return flt(entries[0].exchange_rate)
|
||||
|
||||
if frappe.get_cached_value(
|
||||
"Currency Exchange Settings", "Currency Exchange Settings", "disabled"
|
||||
):
|
||||
return 0.00
|
||||
|
||||
try:
|
||||
cache = frappe.cache()
|
||||
key = "currency_exchange_rate_{0}:{1}:{2}".format(transaction_date, from_currency, to_currency)
|
||||
|
||||
@@ -144,6 +144,7 @@ class DeliveryNote(SellingController):
|
||||
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
|
||||
self.set_product_bundle_reference_in_packed_items() # should be called before `make_packing_list`
|
||||
make_packing_list(self)
|
||||
|
||||
if self._action != "submit" and not self.is_return:
|
||||
@@ -430,6 +431,17 @@ class DeliveryNote(SellingController):
|
||||
else:
|
||||
serial_nos.append(serial_no)
|
||||
|
||||
def set_product_bundle_reference_in_packed_items(self):
|
||||
if self.packed_items and ((self.is_return and self.return_against) or self.amended_from):
|
||||
if items_ref_map := {
|
||||
item.dn_detail or item.get("_amended_from"): item.name
|
||||
for item in self.items
|
||||
if item.dn_detail or item.get("_amended_from")
|
||||
}:
|
||||
for item in self.packed_items:
|
||||
if item.parent_detail_docname in items_ref_map:
|
||||
item.parent_detail_docname = items_ref_map[item.parent_detail_docname]
|
||||
|
||||
|
||||
def update_billed_amount_based_on_so(so_detail, update_modified=True):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.accounts.utils import get_balance_on
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import (
|
||||
automatically_fetch_payment_terms,
|
||||
@@ -268,8 +269,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
self.assertEqual(dn.items[0].returned_qty, 2)
|
||||
self.assertEqual(dn.per_returned, 40)
|
||||
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
return_dn_2 = make_return_doc("Delivery Note", dn.name)
|
||||
|
||||
# Check if unreturned amount is mapped in 2nd return
|
||||
@@ -361,8 +360,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.submit()
|
||||
self.assertEqual(dn.items[0].incoming_rate, 150)
|
||||
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
return_dn = make_return_doc(dn.doctype, dn.name)
|
||||
return_dn.items[0].warehouse = return_warehouse
|
||||
return_dn.save().submit()
|
||||
@@ -1182,7 +1179,6 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
)
|
||||
|
||||
def test_batch_expiry_for_delivery_note(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
item = make_item(
|
||||
@@ -1239,6 +1235,82 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
# Test - 1: ValidationError should be raised
|
||||
self.assertRaises(frappe.ValidationError, dn.submit)
|
||||
|
||||
def test_packed_items_for_return_delivery_note(self):
|
||||
# Step - 1: Create Items
|
||||
product_bundle_item = make_item(properties={"is_stock_item": 0}).name
|
||||
batch_item = make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "TEST-BATCH-.#####",
|
||||
}
|
||||
).name
|
||||
serial_item = make_item(
|
||||
properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TEST-SERIAL-.#####"}
|
||||
).name
|
||||
|
||||
# Step - 2: Inward Stock
|
||||
se1 = make_stock_entry(item_code=batch_item, target="_Test Warehouse - _TC", qty=3)
|
||||
serial_nos = (
|
||||
make_stock_entry(item_code=serial_item, target="_Test Warehouse - _TC", qty=3)
|
||||
.items[0]
|
||||
.serial_no
|
||||
)
|
||||
|
||||
# Step - 3: Create a Product Bundle
|
||||
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import (
|
||||
create_product_bundle_item,
|
||||
)
|
||||
|
||||
create_product_bundle_item(product_bundle_item, packed_items=[[batch_item, 1], [serial_item, 1]])
|
||||
|
||||
# Step - 4: Create a Delivery Note for the Product Bundle
|
||||
dn = create_delivery_note(
|
||||
item_code=product_bundle_item,
|
||||
warehouse="_Test Warehouse - _TC",
|
||||
qty=3,
|
||||
do_not_submit=True,
|
||||
)
|
||||
dn.packed_items[1].serial_no = serial_nos
|
||||
dn.save()
|
||||
dn.submit()
|
||||
|
||||
# Step - 5: Create a Return Delivery Note(Sales Return)
|
||||
return_dn = make_return_doc(dn.doctype, dn.name)
|
||||
return_dn.save()
|
||||
return_dn.submit()
|
||||
|
||||
self.assertEqual(return_dn.packed_items[0].batch_no, dn.packed_items[0].batch_no)
|
||||
self.assertEqual(return_dn.packed_items[1].serial_no, dn.packed_items[1].serial_no)
|
||||
|
||||
@change_settings("Stock Settings", {"automatically_set_serial_nos_based_on_fifo": 1})
|
||||
def test_delivery_note_for_repetitive_serial_item(self):
|
||||
# Step - 1: Create Serial Item
|
||||
item, warehouse = (
|
||||
make_item(
|
||||
properties={"is_stock_item": 1, "has_serial_no": 1, "serial_no_series": "TEST-SERIAL-.###"}
|
||||
).name,
|
||||
"_Test Warehouse - _TC",
|
||||
)
|
||||
|
||||
# Step - 2: Inward Stock
|
||||
make_stock_entry(item_code=item, target=warehouse, qty=5)
|
||||
|
||||
# Step - 3: Create Delivery Note with repetitive Serial Item
|
||||
dn = create_delivery_note(item_code=item, warehouse=warehouse, qty=2, do_not_save=True)
|
||||
dn.append("items", dn.items[0].as_dict())
|
||||
dn.items[1].qty = 3
|
||||
dn.save()
|
||||
dn.submit()
|
||||
|
||||
# Test - 1: Serial Nos should be different for each line item
|
||||
serial_nos = []
|
||||
for item in dn.items:
|
||||
for serial_no in item.serial_no.split("\n"):
|
||||
self.assertNotIn(serial_no, serial_nos)
|
||||
serial_nos.append(serial_no)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
|
||||
|
||||
@@ -741,7 +741,8 @@
|
||||
"label": "Against Delivery Note Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_qty_sec_break",
|
||||
@@ -868,7 +869,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-25 11:58:28.101919",
|
||||
"modified": "2023-10-16 16:18:18.013379",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
|
||||
@@ -37,7 +37,7 @@ frappe.ui.form.on('Inventory Dimension', {
|
||||
if (frm.doc.__onload && frm.doc.__onload.has_stock_ledger
|
||||
&& frm.doc.__onload.has_stock_ledger.length) {
|
||||
let allow_to_edit_fields = ['disabled', 'fetch_from_parent',
|
||||
'type_of_transaction', 'condition', 'mandatory_depends_on'];
|
||||
'type_of_transaction', 'condition', 'mandatory_depends_on', 'validate_negative_stock'];
|
||||
|
||||
frm.fields.forEach((field) => {
|
||||
if (!in_list(allow_to_edit_fields, field.df.fieldname)) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user