Merge branch 'develop' into make-against-field-dynamic
This commit is contained in:
commit
4a047fefb8
@ -1,4 +1,6 @@
|
||||
{
|
||||
"country_code": "hu",
|
||||
"name": "Hungary - Chart of Accounts for Microenterprises",
|
||||
"tree": {
|
||||
"SZ\u00c1MLAOSZT\u00c1LY BEFEKTETETT ESZK\u00d6Z\u00d6K": {
|
||||
"account_number": 1,
|
||||
@ -1651,4 +1653,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -424,7 +424,9 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
vouchers = json.loads(vouchers)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
transaction.add_payment_entries(vouchers)
|
||||
return frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
transaction.save()
|
||||
|
||||
return transaction
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -13,6 +13,7 @@
|
||||
"status",
|
||||
"bank_account",
|
||||
"company",
|
||||
"amended_from",
|
||||
"section_break_4",
|
||||
"deposit",
|
||||
"withdrawal",
|
||||
@ -25,10 +26,10 @@
|
||||
"transaction_id",
|
||||
"transaction_type",
|
||||
"section_break_14",
|
||||
"column_break_oufv",
|
||||
"payment_entries",
|
||||
"section_break_18",
|
||||
"allocated_amount",
|
||||
"amended_from",
|
||||
"column_break_17",
|
||||
"unallocated_amount",
|
||||
"party_section",
|
||||
@ -138,10 +139,12 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
@ -157,10 +160,12 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "unallocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Unallocated Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_section",
|
||||
@ -225,11 +230,15 @@
|
||||
"fieldname": "bank_party_account_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Account No. (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_oufv",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-06 13:58:12.821411",
|
||||
"modified": "2023-11-18 18:32:47.203694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
@ -2,78 +2,73 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
|
||||
class BankTransaction(StatusUpdater):
|
||||
def after_insert(self):
|
||||
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
def before_validate(self):
|
||||
self.update_allocated_amount()
|
||||
|
||||
def on_submit(self):
|
||||
self.clear_linked_payment_entries()
|
||||
def validate(self):
|
||||
self.validate_duplicate_references()
|
||||
|
||||
def validate_duplicate_references(self):
|
||||
"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
|
||||
if not self.payment_entries:
|
||||
return
|
||||
|
||||
pe = []
|
||||
for row in self.payment_entries:
|
||||
reference = (row.payment_document, row.payment_entry)
|
||||
if reference in pe:
|
||||
frappe.throw(
|
||||
_("{0} {1} is allocated twice in this Bank Transaction").format(
|
||||
row.payment_document, row.payment_entry
|
||||
)
|
||||
)
|
||||
pe.append(reference)
|
||||
|
||||
def update_allocated_amount(self):
|
||||
self.allocated_amount = (
|
||||
sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0
|
||||
)
|
||||
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount
|
||||
|
||||
def before_submit(self):
|
||||
self.allocate_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
self.auto_set_party()
|
||||
|
||||
_saving_flag = False
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||
def on_update_after_submit(self):
|
||||
"Run on save(). Avoid recursion caused by multiple saves"
|
||||
if not self._saving_flag:
|
||||
self._saving_flag = True
|
||||
self.clear_linked_payment_entries()
|
||||
self.update_allocations()
|
||||
self._saving_flag = False
|
||||
def before_update_after_submit(self):
|
||||
self.validate_duplicate_references()
|
||||
self.allocate_payment_entries()
|
||||
self.update_allocated_amount()
|
||||
|
||||
def on_cancel(self):
|
||||
self.clear_linked_payment_entries(for_cancel=True)
|
||||
self.set_status(update=True)
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
|
||||
def update_allocations(self):
|
||||
"The doctype does not allow modifications after submission, so write to the db direct"
|
||||
if self.payment_entries:
|
||||
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
|
||||
else:
|
||||
allocated_amount = 0.0
|
||||
|
||||
amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
self.db_set("allocated_amount", flt(allocated_amount))
|
||||
self.db_set("unallocated_amount", amount - flt(allocated_amount))
|
||||
self.reload()
|
||||
self.set_status(update=True)
|
||||
|
||||
def add_payment_entries(self, vouchers):
|
||||
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||
if 0.0 >= self.unallocated_amount:
|
||||
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
frappe.throw(_("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
|
||||
added = False
|
||||
for voucher in vouchers:
|
||||
# Can't add same voucher twice
|
||||
found = False
|
||||
for pe in self.payment_entries:
|
||||
if (
|
||||
pe.payment_document == voucher["payment_doctype"]
|
||||
and pe.payment_entry == voucher["payment_name"]
|
||||
):
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
pe = {
|
||||
self.append(
|
||||
"payment_entries",
|
||||
{
|
||||
"payment_document": voucher["payment_doctype"],
|
||||
"payment_entry": voucher["payment_name"],
|
||||
"allocated_amount": 0.0, # Temporary
|
||||
}
|
||||
child = self.append("payment_entries", pe)
|
||||
added = True
|
||||
|
||||
# runs on_update_after_submit
|
||||
if added:
|
||||
self.save()
|
||||
},
|
||||
)
|
||||
|
||||
def allocate_payment_entries(self):
|
||||
"""Refactored from bank reconciliation tool.
|
||||
@ -90,6 +85,7 @@ class BankTransaction(StatusUpdater):
|
||||
- clear means: set the latest transaction date as clearance date
|
||||
"""
|
||||
remaining_amount = self.unallocated_amount
|
||||
to_remove = []
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
@ -99,49 +95,39 @@ class BankTransaction(StatusUpdater):
|
||||
if 0.0 == unallocated_amount:
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
to_remove.append(payment_entry)
|
||||
|
||||
elif remaining_amount <= 0.0:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
to_remove.append(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
|
||||
payment_entry.db_set("allocated_amount", unallocated_amount)
|
||||
elif 0.0 < unallocated_amount <= remaining_amount:
|
||||
payment_entry.allocated_amount = unallocated_amount
|
||||
remaining_amount -= unallocated_amount
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
|
||||
payment_entry.db_set("allocated_amount", remaining_amount)
|
||||
elif 0.0 < unallocated_amount:
|
||||
payment_entry.allocated_amount = remaining_amount
|
||||
remaining_amount = 0.0
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
self.reload()
|
||||
|
||||
def db_delete_payment_entry(self, payment_entry):
|
||||
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
|
||||
for payment_entry in to_remove:
|
||||
self.remove(to_remove)
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_payment_entries(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
self.remove_payment_entry(payment_entry)
|
||||
# runs on_update_after_submit
|
||||
self.save()
|
||||
|
||||
self.save() # runs before_update_after_submit
|
||||
|
||||
def remove_payment_entry(self, payment_entry):
|
||||
"Clear payment entry and clearance"
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
self.remove(payment_entry)
|
||||
|
||||
def clear_linked_payment_entries(self, for_cancel=False):
|
||||
if for_cancel:
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel)
|
||||
else:
|
||||
self.allocate_payment_entries()
|
||||
|
||||
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
||||
clearance_date = None if for_cancel else self.date
|
||||
set_voucher_clearance(
|
||||
@ -162,11 +148,10 @@ class BankTransaction(StatusUpdater):
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
if result:
|
||||
party_type, party = result
|
||||
frappe.db.set_value(
|
||||
"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
|
||||
)
|
||||
if not result:
|
||||
return
|
||||
|
||||
self.party_type, self.party = result
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -198,9 +183,7 @@ def get_clearance_details(transaction, payment_entry):
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
frappe.throw(
|
||||
frappe._("Voucher {0} value is broken: {1}").format(
|
||||
payment_entry.payment_entry, gle["amount"]
|
||||
)
|
||||
_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
|
||||
)
|
||||
|
||||
unmatched_gles -= 1
|
||||
@ -221,7 +204,7 @@ def get_clearance_details(transaction, payment_entry):
|
||||
|
||||
def get_related_bank_gl_entries(doctype, docname):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
||||
@ -239,7 +222,6 @@ def get_related_bank_gl_entries(doctype, docname):
|
||||
dict(doctype=doctype, docname=docname),
|
||||
as_dict=True,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
@ -365,6 +347,7 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||
if clearance_date:
|
||||
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
|
||||
bt.add_payment_entries(vouchers)
|
||||
bt.save()
|
||||
else:
|
||||
for pe in bt.payment_entries:
|
||||
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
|
||||
|
@ -113,7 +113,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
|
||||
if as_dict:
|
||||
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
|
||||
else:
|
||||
if not row[1]:
|
||||
if not row[1] and len(row) > 1:
|
||||
row[1] = row[0]
|
||||
row[3] = row[2]
|
||||
data.append(row)
|
||||
|
@ -509,7 +509,7 @@ class JournalEntry(AccountsController):
|
||||
).format(d.reference_name, d.account)
|
||||
)
|
||||
else:
|
||||
dr_or_cr = "debit" if d.credit > 0 else "credit"
|
||||
dr_or_cr = "debit" if flt(d.credit) > 0 else "credit"
|
||||
valid = False
|
||||
for jvd in against_entries:
|
||||
if flt(jvd[dr_or_cr]) > 0:
|
||||
|
@ -17,11 +17,10 @@ class ProcessSubscription(Document):
|
||||
|
||||
|
||||
def create_subscription_process(
|
||||
subscription: str | None, posting_date: Union[str, datetime.date] | None
|
||||
subscription: str | None = None, posting_date: Union[str, datetime.date] | None = None
|
||||
):
|
||||
"""Create a new Process Subscription document"""
|
||||
doc = frappe.new_doc("Process Subscription")
|
||||
doc.subscription = subscription
|
||||
doc.posting_date = getdate(posting_date)
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.submit()
|
||||
|
@ -676,7 +676,7 @@ def get_prorata_factor(
|
||||
|
||||
|
||||
def process_all(
|
||||
subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None
|
||||
subscription: str | None = None, posting_date: Optional["DateTimeLikeObject"] = None
|
||||
) -> None:
|
||||
"""
|
||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||
|
@ -285,8 +285,8 @@ class ReceivablePayableReport(object):
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) or (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
|
||||
(abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision)
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
|
@ -1,7 +1,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import (
|
||||
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import (
|
||||
get_result,
|
||||
get_tds_docs,
|
||||
)
|
||||
|
@ -214,30 +214,43 @@ frappe.ui.form.on('Asset', {
|
||||
})
|
||||
},
|
||||
|
||||
render_depreciation_schedule_view: function(frm, depr_schedule) {
|
||||
render_depreciation_schedule_view: function(frm, asset_depr_schedule_doc) {
|
||||
let wrapper = $(frm.fields_dict["depreciation_schedule_view"].wrapper).empty();
|
||||
|
||||
let data = [];
|
||||
|
||||
depr_schedule.forEach((sch) => {
|
||||
asset_depr_schedule_doc.depreciation_schedule.forEach((sch) => {
|
||||
const row = [
|
||||
sch['idx'],
|
||||
frappe.format(sch['schedule_date'], { fieldtype: 'Date' }),
|
||||
frappe.format(sch['depreciation_amount'], { fieldtype: 'Currency' }),
|
||||
frappe.format(sch['accumulated_depreciation_amount'], { fieldtype: 'Currency' }),
|
||||
sch['journal_entry'] || ''
|
||||
sch['journal_entry'] || '',
|
||||
];
|
||||
|
||||
if (asset_depr_schedule_doc.shift_based) {
|
||||
row.push(sch['shift']);
|
||||
}
|
||||
|
||||
data.push(row);
|
||||
});
|
||||
|
||||
let columns = [
|
||||
{name: __("No."), editable: false, resizable: false, format: value => value, width: 60},
|
||||
{name: __("Schedule Date"), editable: false, resizable: false, width: 270},
|
||||
{name: __("Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||
{name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||
];
|
||||
|
||||
if (asset_depr_schedule_doc.shift_based) {
|
||||
columns.push({name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 245});
|
||||
columns.push({name: __("Shift"), editable: false, resizable: false, width: 59});
|
||||
} else {
|
||||
columns.push({name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 304});
|
||||
}
|
||||
|
||||
let datatable = new frappe.DataTable(wrapper.get(0), {
|
||||
columns: [
|
||||
{name: __("No."), editable: false, resizable: false, format: value => value, width: 60},
|
||||
{name: __("Schedule Date"), editable: false, resizable: false, width: 270},
|
||||
{name: __("Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||
{name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||
{name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 304}
|
||||
],
|
||||
columns: columns,
|
||||
data: data,
|
||||
layout: "fluid",
|
||||
serialNoColumn: false,
|
||||
@ -272,8 +285,8 @@ frappe.ui.form.on('Asset', {
|
||||
asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
|
||||
}
|
||||
|
||||
let depr_schedule = (await frappe.call(
|
||||
"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
|
||||
let asset_depr_schedule_doc = (await frappe.call(
|
||||
"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_asset_depr_schedule_doc",
|
||||
{
|
||||
asset_name: frm.doc.name,
|
||||
status: "Active",
|
||||
@ -281,7 +294,7 @@ frappe.ui.form.on('Asset', {
|
||||
}
|
||||
)).message;
|
||||
|
||||
$.each(depr_schedule || [], function(i, v) {
|
||||
$.each(asset_depr_schedule_doc.depreciation_schedule || [], function(i, v) {
|
||||
x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' }));
|
||||
var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount'));
|
||||
if(v.journal_entry) {
|
||||
@ -296,7 +309,7 @@ frappe.ui.form.on('Asset', {
|
||||
});
|
||||
|
||||
frm.toggle_display(["depreciation_schedule_view"], 1);
|
||||
frm.events.render_depreciation_schedule_view(frm, depr_schedule);
|
||||
frm.events.render_depreciation_schedule_view(frm, asset_depr_schedule_doc);
|
||||
} else {
|
||||
if(frm.doc.opening_accumulated_depreciation) {
|
||||
x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' }));
|
||||
|
@ -829,6 +829,7 @@ def get_item_details(item_code, asset_category, gross_purchase_amount):
|
||||
"total_number_of_depreciations": d.total_number_of_depreciations,
|
||||
"frequency_of_depreciation": d.frequency_of_depreciation,
|
||||
"daily_prorata_based": d.daily_prorata_based,
|
||||
"shift_based": d.shift_based,
|
||||
"salvage_value_percentage": d.salvage_value_percentage,
|
||||
"expected_value_after_useful_life": flt(gross_purchase_amount)
|
||||
* flt(d.salvage_value_percentage / 100),
|
||||
|
@ -149,12 +149,7 @@ class TestAsset(AssetSetup):
|
||||
("Creditors - _TC", 0.0, 100000.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
pi.name,
|
||||
)
|
||||
gle = get_gl_entries("Purchase Invoice", pi.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
pi.cancel()
|
||||
@ -273,12 +268,7 @@ class TestAsset(AssetSetup):
|
||||
),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Journal Entry' and voucher_no = %s
|
||||
order by account, credit""",
|
||||
asset.journal_entry_for_scrap,
|
||||
)
|
||||
gle = get_gl_entries("Journal Entry", asset.journal_entry_for_scrap)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
restore_asset(asset.name)
|
||||
@ -354,13 +344,7 @@ class TestAsset(AssetSetup):
|
||||
("Debtors - _TC", 25000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Sales Invoice", si.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
si.cancel()
|
||||
@ -434,13 +418,7 @@ class TestAsset(AssetSetup):
|
||||
("Debtors - _TC", 40000.0, 0.0),
|
||||
)
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Sales Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
si.name,
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Sales Invoice", si.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
def test_asset_with_maintenance_required_status_after_sale(self):
|
||||
@ -581,13 +559,7 @@ class TestAsset(AssetSetup):
|
||||
("CWIP Account - _TC", 5250.0, 0.0),
|
||||
)
|
||||
|
||||
pr_gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Purchase Receipt' and voucher_no = %s
|
||||
order by account""",
|
||||
pr.name,
|
||||
)
|
||||
|
||||
pr_gle = get_gl_entries("Purchase Receipt", pr.name)
|
||||
self.assertSequenceEqual(pr_gle, expected_gle)
|
||||
|
||||
pi = make_invoice(pr.name)
|
||||
@ -600,13 +572,7 @@ class TestAsset(AssetSetup):
|
||||
("Creditors - _TC", 0.0, 5500.0),
|
||||
)
|
||||
|
||||
pi_gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Purchase Invoice' and voucher_no = %s
|
||||
order by account""",
|
||||
pi.name,
|
||||
)
|
||||
|
||||
pi_gle = get_gl_entries("Purchase Invoice", pi.name)
|
||||
self.assertSequenceEqual(pi_gle, expected_gle)
|
||||
|
||||
asset = frappe.db.get_value("Asset", {"purchase_receipt": pr.name, "docstatus": 0}, "name")
|
||||
@ -633,13 +599,7 @@ class TestAsset(AssetSetup):
|
||||
|
||||
expected_gle = (("_Test Fixed Asset - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 0.0, 5250.0))
|
||||
|
||||
gle = frappe.db.sql(
|
||||
"""select account, debit, credit from `tabGL Entry`
|
||||
where voucher_type='Asset' and voucher_no = %s
|
||||
order by account""",
|
||||
asset_doc.name,
|
||||
)
|
||||
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertSequenceEqual(gle, expected_gle)
|
||||
|
||||
def test_asset_cwip_toggling_cases(self):
|
||||
@ -662,10 +622,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertFalse(gle)
|
||||
|
||||
# case 1 -- PR with cwip disabled, Asset with cwip enabled
|
||||
@ -679,10 +636,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertFalse(gle)
|
||||
|
||||
# case 2 -- PR with cwip enabled, Asset with cwip disabled
|
||||
@ -695,10 +649,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertTrue(gle)
|
||||
|
||||
# case 3 -- PI with cwip disabled, Asset with cwip enabled
|
||||
@ -711,10 +662,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertFalse(gle)
|
||||
|
||||
# case 4 -- PI with cwip enabled, Asset with cwip disabled
|
||||
@ -727,10 +675,7 @@ class TestAsset(AssetSetup):
|
||||
asset_doc.available_for_use_date = nowdate()
|
||||
asset_doc.calculate_depreciation = 0
|
||||
asset_doc.submit()
|
||||
gle = frappe.db.sql(
|
||||
"""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""",
|
||||
asset_doc.name,
|
||||
)
|
||||
gle = get_gl_entries("Asset", asset_doc.name)
|
||||
self.assertTrue(gle)
|
||||
|
||||
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip)
|
||||
@ -1064,7 +1009,11 @@ class TestDepreciationBasics(AssetSetup):
|
||||
},
|
||||
)
|
||||
|
||||
depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
|
||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active")
|
||||
|
||||
depreciation_amount = get_depreciation_amount(
|
||||
asset_depr_schedule_doc, asset, 100000, asset.finance_books[0]
|
||||
)
|
||||
self.assertEqual(depreciation_amount, 30000)
|
||||
|
||||
def test_make_depr_schedule(self):
|
||||
@ -1710,6 +1659,30 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, jv.insert)
|
||||
|
||||
def test_multi_currency_asset_pr_creation(self):
|
||||
pr = make_purchase_receipt(
|
||||
item_code="Macbook Pro",
|
||||
qty=1,
|
||||
rate=100.0,
|
||||
location="Test Location",
|
||||
supplier="_Test Supplier USD",
|
||||
currency="USD",
|
||||
)
|
||||
|
||||
pr.submit()
|
||||
self.assertTrue(get_gl_entries("Purchase Receipt", pr.name))
|
||||
|
||||
|
||||
def get_gl_entries(doctype, docname):
|
||||
gl_entry = frappe.qb.DocType("GL Entry")
|
||||
return (
|
||||
frappe.qb.from_(gl_entry)
|
||||
.select(gl_entry.account, gl_entry.debit, gl_entry.credit)
|
||||
.where((gl_entry.voucher_type == doctype) & (gl_entry.voucher_no == docname))
|
||||
.orderby(gl_entry.account)
|
||||
.run()
|
||||
)
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
@ -1772,6 +1745,7 @@ def create_asset(**args):
|
||||
"expected_value_after_useful_life": args.expected_value_after_useful_life or 0,
|
||||
"depreciation_start_date": args.depreciation_start_date,
|
||||
"daily_prorata_based": args.daily_prorata_based or 0,
|
||||
"shift_based": args.shift_based or 0,
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -8,11 +8,13 @@ frappe.ui.form.on('Asset Depreciation Schedule', {
|
||||
},
|
||||
|
||||
make_schedules_editable: function(frm) {
|
||||
var is_editable = frm.doc.depreciation_method == "Manual" ? true : false;
|
||||
var is_manual_hence_editable = frm.doc.depreciation_method === "Manual" ? true : false;
|
||||
var is_shift_hence_editable = frm.doc.shift_based ? true : false;
|
||||
|
||||
frm.toggle_enable("depreciation_schedule", is_editable);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_editable);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_editable);
|
||||
frm.toggle_enable("depreciation_schedule", is_manual_hence_editable || is_shift_hence_editable);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", is_manual_hence_editable);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", is_manual_hence_editable);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", is_shift_hence_editable);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
"total_number_of_depreciations",
|
||||
"rate_of_depreciation",
|
||||
"daily_prorata_based",
|
||||
"shift_based",
|
||||
"column_break_8",
|
||||
"frequency_of_depreciation",
|
||||
"expected_value_after_useful_life",
|
||||
@ -184,12 +185,20 @@
|
||||
"label": "Depreciate based on daily pro-rata",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\"",
|
||||
"fieldname": "shift_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on shifts",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-03 21:32:15.021796",
|
||||
"modified": "2023-11-29 00:57:00.461998",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Depreciation Schedule",
|
||||
|
@ -26,6 +26,7 @@ class AssetDepreciationSchedule(Document):
|
||||
self.prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(
|
||||
self.asset, self.finance_book
|
||||
)
|
||||
self.update_shift_depr_schedule()
|
||||
|
||||
def validate(self):
|
||||
self.validate_another_asset_depr_schedule_does_not_exist()
|
||||
@ -73,6 +74,16 @@ class AssetDepreciationSchedule(Document):
|
||||
def on_cancel(self):
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
def update_shift_depr_schedule(self):
|
||||
if not self.shift_based or self.docstatus != 0:
|
||||
return
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
fb_row = asset_doc.finance_books[self.finance_book_id - 1]
|
||||
|
||||
self.make_depr_schedule(asset_doc, fb_row)
|
||||
self.set_accumulated_depreciation(asset_doc, fb_row)
|
||||
|
||||
def prepare_draft_asset_depr_schedule_data_from_asset_name_and_fb_name(self, asset_name, fb_name):
|
||||
asset_doc = frappe.get_doc("Asset", asset_name)
|
||||
|
||||
@ -154,13 +165,14 @@ class AssetDepreciationSchedule(Document):
|
||||
self.rate_of_depreciation = row.rate_of_depreciation
|
||||
self.expected_value_after_useful_life = row.expected_value_after_useful_life
|
||||
self.daily_prorata_based = row.daily_prorata_based
|
||||
self.shift_based = row.shift_based
|
||||
self.status = "Draft"
|
||||
|
||||
def make_depr_schedule(
|
||||
self,
|
||||
asset_doc,
|
||||
row,
|
||||
date_of_disposal,
|
||||
date_of_disposal=None,
|
||||
update_asset_finance_book_row=True,
|
||||
value_after_depreciation=None,
|
||||
):
|
||||
@ -181,6 +193,8 @@ class AssetDepreciationSchedule(Document):
|
||||
num_of_depreciations_completed = 0
|
||||
depr_schedule = []
|
||||
|
||||
self.schedules_before_clearing = self.get("depreciation_schedule")
|
||||
|
||||
for schedule in self.get("depreciation_schedule"):
|
||||
if schedule.journal_entry:
|
||||
num_of_depreciations_completed += 1
|
||||
@ -246,6 +260,7 @@ class AssetDepreciationSchedule(Document):
|
||||
prev_depreciation_amount = 0
|
||||
|
||||
depreciation_amount = get_depreciation_amount(
|
||||
self,
|
||||
asset_doc,
|
||||
value_after_depreciation,
|
||||
row,
|
||||
@ -282,10 +297,7 @@ class AssetDepreciationSchedule(Document):
|
||||
)
|
||||
|
||||
if depreciation_amount > 0:
|
||||
self.add_depr_schedule_row(
|
||||
date_of_disposal,
|
||||
depreciation_amount,
|
||||
)
|
||||
self.add_depr_schedule_row(date_of_disposal, depreciation_amount, n)
|
||||
|
||||
break
|
||||
|
||||
@ -369,10 +381,7 @@ class AssetDepreciationSchedule(Document):
|
||||
skip_row = True
|
||||
|
||||
if flt(depreciation_amount, asset_doc.precision("gross_purchase_amount")) > 0:
|
||||
self.add_depr_schedule_row(
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
)
|
||||
self.add_depr_schedule_row(schedule_date, depreciation_amount, n)
|
||||
|
||||
# to ensure that final accumulated depreciation amount is accurate
|
||||
def get_adjusted_depreciation_amount(
|
||||
@ -394,16 +403,22 @@ class AssetDepreciationSchedule(Document):
|
||||
def get_depreciation_amount_for_first_row(self):
|
||||
return self.get("depreciation_schedule")[0].depreciation_amount
|
||||
|
||||
def add_depr_schedule_row(
|
||||
self,
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
):
|
||||
def add_depr_schedule_row(self, schedule_date, depreciation_amount, schedule_idx):
|
||||
if self.shift_based:
|
||||
shift = (
|
||||
self.schedules_before_clearing[schedule_idx].shift
|
||||
if self.schedules_before_clearing and len(self.schedules_before_clearing) > schedule_idx
|
||||
else frappe.get_cached_value("Asset Shift Factor", {"default": 1}, "shift_name")
|
||||
)
|
||||
else:
|
||||
shift = None
|
||||
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"shift": shift,
|
||||
},
|
||||
)
|
||||
|
||||
@ -445,6 +460,7 @@ class AssetDepreciationSchedule(Document):
|
||||
and i == max(straight_line_idx) - 1
|
||||
and not date_of_disposal
|
||||
and not date_of_return
|
||||
and not row.shift_based
|
||||
):
|
||||
depreciation_amount += flt(
|
||||
value_after_depreciation - flt(row.expected_value_after_useful_life),
|
||||
@ -527,6 +543,7 @@ def get_total_days(date, frequency):
|
||||
|
||||
|
||||
def get_depreciation_amount(
|
||||
asset_depr_schedule,
|
||||
asset,
|
||||
depreciable_value,
|
||||
fb_row,
|
||||
@ -537,7 +554,7 @@ def get_depreciation_amount(
|
||||
):
|
||||
if fb_row.depreciation_method in ("Straight Line", "Manual"):
|
||||
return get_straight_line_or_manual_depr_amount(
|
||||
asset, fb_row, schedule_idx, number_of_pending_depreciations
|
||||
asset_depr_schedule, asset, fb_row, schedule_idx, number_of_pending_depreciations
|
||||
)
|
||||
else:
|
||||
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
|
||||
@ -559,8 +576,11 @@ def get_updated_rate_of_depreciation_for_wdv_and_dd(asset, depreciable_value, fb
|
||||
|
||||
|
||||
def get_straight_line_or_manual_depr_amount(
|
||||
asset, row, schedule_idx, number_of_pending_depreciations
|
||||
asset_depr_schedule, asset, row, schedule_idx, number_of_pending_depreciations
|
||||
):
|
||||
if row.shift_based:
|
||||
return get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx)
|
||||
|
||||
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
|
||||
if asset.flags.increase_in_asset_life:
|
||||
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
|
||||
@ -655,6 +675,41 @@ def get_straight_line_or_manual_depr_amount(
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
|
||||
def get_shift_depr_amount(asset_depr_schedule, asset, row, schedule_idx):
|
||||
if asset_depr_schedule.get("__islocal") and not asset.flags.shift_allocation:
|
||||
return (
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
) / flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
|
||||
|
||||
asset_shift_factors_map = get_asset_shift_factors_map()
|
||||
shift = (
|
||||
asset_depr_schedule.schedules_before_clearing[schedule_idx].shift
|
||||
if len(asset_depr_schedule.schedules_before_clearing) > schedule_idx
|
||||
else None
|
||||
)
|
||||
shift_factor = asset_shift_factors_map.get(shift) if shift else 0
|
||||
|
||||
shift_factors_sum = sum(
|
||||
flt(asset_shift_factors_map.get(schedule.shift))
|
||||
for schedule in asset_depr_schedule.schedules_before_clearing
|
||||
)
|
||||
|
||||
return (
|
||||
(
|
||||
flt(asset.gross_purchase_amount)
|
||||
- flt(asset.opening_accumulated_depreciation)
|
||||
- flt(row.expected_value_after_useful_life)
|
||||
)
|
||||
/ flt(shift_factors_sum)
|
||||
) * shift_factor
|
||||
|
||||
|
||||
def get_asset_shift_factors_map():
|
||||
return dict(frappe.db.get_all("Asset Shift Factor", ["shift_name", "shift_factor"], as_list=True))
|
||||
|
||||
|
||||
def get_wdv_or_dd_depr_amount(
|
||||
depreciable_value,
|
||||
rate_of_depreciation,
|
||||
@ -803,7 +858,12 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
|
||||
|
||||
def get_temp_asset_depr_schedule_doc(
|
||||
asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False
|
||||
asset_doc,
|
||||
row,
|
||||
date_of_disposal=None,
|
||||
date_of_return=None,
|
||||
update_asset_finance_book_row=False,
|
||||
new_depr_schedule=None,
|
||||
):
|
||||
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
|
||||
asset_doc.name, "Active", row.finance_book
|
||||
@ -818,6 +878,21 @@ def get_temp_asset_depr_schedule_doc(
|
||||
|
||||
temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||
|
||||
if new_depr_schedule:
|
||||
temp_asset_depr_schedule_doc.depreciation_schedule = []
|
||||
|
||||
for schedule in new_depr_schedule:
|
||||
temp_asset_depr_schedule_doc.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule.schedule_date,
|
||||
"depreciation_amount": schedule.depreciation_amount,
|
||||
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
|
||||
"journal_entry": schedule.journal_entry,
|
||||
"shift": schedule.shift,
|
||||
},
|
||||
)
|
||||
|
||||
temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
|
||||
asset_doc,
|
||||
row,
|
||||
@ -839,6 +914,7 @@ def get_depr_schedule(asset_name, status, finance_book=None):
|
||||
return asset_depr_schedule_doc.get("depreciation_schedule")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_asset_depr_schedule_doc(asset_name, status, finance_book=None):
|
||||
asset_depr_schedule_name = get_asset_depr_schedule_name(asset_name, status, finance_book)
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"daily_prorata_based",
|
||||
"shift_based",
|
||||
"column_break_5",
|
||||
"frequency_of_depreciation",
|
||||
"depreciation_start_date",
|
||||
@ -97,12 +98,19 @@
|
||||
"fieldname": "daily_prorata_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on daily pro-rata"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.depreciation_method == \"Straight Line\"",
|
||||
"fieldname": "shift_based",
|
||||
"fieldtype": "Check",
|
||||
"label": "Depreciate based on shifts"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-03 21:30:24.266601",
|
||||
"modified": "2023-11-29 00:57:07.579777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
frappe.ui.form.on('Asset Shift Allocation', {
|
||||
onload: function(frm) {
|
||||
frm.events.make_schedules_editable(frm);
|
||||
},
|
||||
|
||||
make_schedules_editable: function(frm) {
|
||||
frm.toggle_enable("depreciation_schedule", true);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("schedule_date", false);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("depreciation_amount", false);
|
||||
frm.fields_dict["depreciation_schedule"].grid.toggle_enable("shift", true);
|
||||
}
|
||||
});
|
@ -0,0 +1,111 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2023-11-24 15:07:44.652133",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_esaa",
|
||||
"asset",
|
||||
"naming_series",
|
||||
"column_break_tdae",
|
||||
"finance_book",
|
||||
"amended_from",
|
||||
"depreciation_schedule_section",
|
||||
"depreciation_schedule"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "section_break_esaa",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Asset Shift Allocation",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Asset",
|
||||
"options": "Asset",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_tdae",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "depreciation_schedule_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Depreciation Schedule"
|
||||
},
|
||||
{
|
||||
"fieldname": "depreciation_schedule",
|
||||
"fieldtype": "Table",
|
||||
"label": "Depreciation Schedule",
|
||||
"options": "Depreciation Schedule"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Naming Series",
|
||||
"options": "ACC-ASA-.YYYY.-",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 04:05:04.683518",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Shift Allocation",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,262 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (
|
||||
add_months,
|
||||
cint,
|
||||
flt,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
is_last_day_of_the_month,
|
||||
)
|
||||
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_asset_depr_schedule_doc,
|
||||
get_asset_shift_factors_map,
|
||||
get_temp_asset_depr_schedule_doc,
|
||||
)
|
||||
|
||||
|
||||
class AssetShiftAllocation(Document):
|
||||
def after_insert(self):
|
||||
self.fetch_and_set_depr_schedule()
|
||||
|
||||
def validate(self):
|
||||
self.asset_depr_schedule_doc = get_asset_depr_schedule_doc(
|
||||
self.asset, "Active", self.finance_book
|
||||
)
|
||||
|
||||
self.validate_invalid_shift_change()
|
||||
self.update_depr_schedule()
|
||||
|
||||
def on_submit(self):
|
||||
self.create_new_asset_depr_schedule()
|
||||
|
||||
def fetch_and_set_depr_schedule(self):
|
||||
if self.asset_depr_schedule_doc:
|
||||
if self.asset_depr_schedule_doc.shift_based:
|
||||
for schedule in self.asset_depr_schedule_doc.get("depreciation_schedule"):
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule.schedule_date,
|
||||
"depreciation_amount": schedule.depreciation_amount,
|
||||
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
|
||||
"journal_entry": schedule.journal_entry,
|
||||
"shift": schedule.shift,
|
||||
},
|
||||
)
|
||||
|
||||
self.flags.ignore_validate = True
|
||||
self.save()
|
||||
else:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Asset Depreciation Schedule for Asset {0} and Finance Book {1} is not using shift based depreciation"
|
||||
).format(self.asset, self.finance_book)
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
|
||||
self.asset, self.finance_book
|
||||
)
|
||||
)
|
||||
|
||||
def validate_invalid_shift_change(self):
|
||||
if not self.get("depreciation_schedule") or self.docstatus == 1:
|
||||
return
|
||||
|
||||
for i, sch in enumerate(self.depreciation_schedule):
|
||||
if (
|
||||
sch.journal_entry and self.asset_depr_schedule_doc.depreciation_schedule[i].shift != sch.shift
|
||||
):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Shift cannot be changed since the depreciation has already been processed"
|
||||
).format(i)
|
||||
)
|
||||
|
||||
def update_depr_schedule(self):
|
||||
if not self.get("depreciation_schedule") or self.docstatus == 1:
|
||||
return
|
||||
|
||||
self.allocate_shift_diff_in_depr_schedule()
|
||||
|
||||
asset_doc = frappe.get_doc("Asset", self.asset)
|
||||
fb_row = asset_doc.finance_books[self.asset_depr_schedule_doc.finance_book_id - 1]
|
||||
|
||||
asset_doc.flags.shift_allocation = True
|
||||
|
||||
temp_depr_schedule = get_temp_asset_depr_schedule_doc(
|
||||
asset_doc, fb_row, new_depr_schedule=self.depreciation_schedule
|
||||
).get("depreciation_schedule")
|
||||
|
||||
self.depreciation_schedule = []
|
||||
|
||||
for schedule in temp_depr_schedule:
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule.schedule_date,
|
||||
"depreciation_amount": schedule.depreciation_amount,
|
||||
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
|
||||
"journal_entry": schedule.journal_entry,
|
||||
"shift": schedule.shift,
|
||||
},
|
||||
)
|
||||
|
||||
def allocate_shift_diff_in_depr_schedule(self):
|
||||
asset_shift_factors_map = get_asset_shift_factors_map()
|
||||
reverse_asset_shift_factors_map = {
|
||||
asset_shift_factors_map[k]: k for k in asset_shift_factors_map
|
||||
}
|
||||
|
||||
original_shift_factors_sum = sum(
|
||||
flt(asset_shift_factors_map.get(schedule.shift))
|
||||
for schedule in self.asset_depr_schedule_doc.depreciation_schedule
|
||||
)
|
||||
|
||||
new_shift_factors_sum = sum(
|
||||
flt(asset_shift_factors_map.get(schedule.shift)) for schedule in self.depreciation_schedule
|
||||
)
|
||||
|
||||
diff = new_shift_factors_sum - original_shift_factors_sum
|
||||
|
||||
if diff > 0:
|
||||
for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
|
||||
if diff <= 0:
|
||||
break
|
||||
|
||||
shift_factor = flt(asset_shift_factors_map.get(schedule.shift))
|
||||
|
||||
if shift_factor <= diff:
|
||||
self.depreciation_schedule.pop()
|
||||
diff -= shift_factor
|
||||
else:
|
||||
try:
|
||||
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(
|
||||
shift_factor - diff
|
||||
)
|
||||
diff = 0
|
||||
except Exception:
|
||||
frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format(
|
||||
shift_factor - diff
|
||||
)
|
||||
elif diff < 0:
|
||||
shift_factors = list(asset_shift_factors_map.values())
|
||||
desc_shift_factors = sorted(shift_factors, reverse=True)
|
||||
depr_schedule_len_diff = self.asset_depr_schedule_doc.total_number_of_depreciations - len(
|
||||
self.depreciation_schedule
|
||||
)
|
||||
subsets_result = []
|
||||
|
||||
if depr_schedule_len_diff > 0:
|
||||
num_rows_to_add = depr_schedule_len_diff
|
||||
|
||||
while not subsets_result and num_rows_to_add > 0:
|
||||
find_subsets_with_sum(shift_factors, num_rows_to_add, abs(diff), [], subsets_result)
|
||||
if subsets_result:
|
||||
break
|
||||
num_rows_to_add -= 1
|
||||
|
||||
if subsets_result:
|
||||
for i in range(num_rows_to_add):
|
||||
schedule_date = add_months(
|
||||
self.depreciation_schedule[-1].schedule_date,
|
||||
cint(self.asset_depr_schedule_doc.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
if is_last_day_of_the_month(self.depreciation_schedule[-1].schedule_date):
|
||||
schedule_date = get_last_day(schedule_date)
|
||||
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"shift": reverse_asset_shift_factors_map.get(subsets_result[0][i]),
|
||||
},
|
||||
)
|
||||
|
||||
if depr_schedule_len_diff <= 0 or not subsets_result:
|
||||
for i, schedule in reversed(list(enumerate(self.depreciation_schedule))):
|
||||
diff = abs(diff)
|
||||
|
||||
if diff <= 0:
|
||||
break
|
||||
|
||||
shift_factor = flt(asset_shift_factors_map.get(schedule.shift))
|
||||
|
||||
if shift_factor <= diff:
|
||||
for sf in desc_shift_factors:
|
||||
if sf - shift_factor <= diff:
|
||||
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(sf)
|
||||
diff -= sf - shift_factor
|
||||
break
|
||||
else:
|
||||
try:
|
||||
self.depreciation_schedule[i].shift = reverse_asset_shift_factors_map.get(
|
||||
shift_factor + diff
|
||||
)
|
||||
diff = 0
|
||||
except Exception:
|
||||
frappe.throw(_("Could not auto update shifts. Shift with shift factor {0} needed.")).format(
|
||||
shift_factor + diff
|
||||
)
|
||||
|
||||
def create_new_asset_depr_schedule(self):
|
||||
new_asset_depr_schedule_doc = frappe.copy_doc(self.asset_depr_schedule_doc)
|
||||
|
||||
new_asset_depr_schedule_doc.depreciation_schedule = []
|
||||
|
||||
for schedule in self.depreciation_schedule:
|
||||
new_asset_depr_schedule_doc.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule.schedule_date,
|
||||
"depreciation_amount": schedule.depreciation_amount,
|
||||
"accumulated_depreciation_amount": schedule.accumulated_depreciation_amount,
|
||||
"journal_entry": schedule.journal_entry,
|
||||
"shift": schedule.shift,
|
||||
},
|
||||
)
|
||||
|
||||
notes = _(
|
||||
"This schedule was created when Asset {0}'s shifts were adjusted through Asset Shift Allocation {1}."
|
||||
).format(
|
||||
get_link_to_form("Asset", self.asset),
|
||||
get_link_to_form(self.doctype, self.name),
|
||||
)
|
||||
|
||||
new_asset_depr_schedule_doc.notes = notes
|
||||
|
||||
self.asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
|
||||
self.asset_depr_schedule_doc.cancel()
|
||||
|
||||
new_asset_depr_schedule_doc.submit()
|
||||
|
||||
add_asset_activity(
|
||||
self.asset,
|
||||
_("Asset's depreciation schedule updated after Asset Shift Allocation {0}").format(
|
||||
get_link_to_form(self.doctype, self.name)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def find_subsets_with_sum(numbers, k, target_sum, current_subset, result):
|
||||
if k == 0 and target_sum == 0:
|
||||
result.append(current_subset.copy())
|
||||
return
|
||||
if k <= 0 or target_sum <= 0 or not numbers:
|
||||
return
|
||||
|
||||
# Include the current number in the subset
|
||||
find_subsets_with_sum(
|
||||
numbers, k - 1, target_sum - numbers[0], current_subset + [numbers[0]], result
|
||||
)
|
||||
|
||||
# Exclude the current number from the subset
|
||||
find_subsets_with_sum(numbers[1:], k, target_sum, current_subset, result)
|
@ -0,0 +1,113 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr
|
||||
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset
|
||||
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
|
||||
get_depr_schedule,
|
||||
)
|
||||
|
||||
|
||||
class TestAssetShiftAllocation(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
create_asset_shift_factors()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_asset_shift_allocation(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2023-01-01",
|
||||
purchase_date="2023-01-01",
|
||||
gross_purchase_amount=120000,
|
||||
depreciation_start_date="2023-01-31",
|
||||
total_number_of_depreciations=12,
|
||||
frequency_of_depreciation=1,
|
||||
shift_based=1,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_schedules = [
|
||||
["2023-01-31", 10000.0, 10000.0, "Single"],
|
||||
["2023-02-28", 10000.0, 20000.0, "Single"],
|
||||
["2023-03-31", 10000.0, 30000.0, "Single"],
|
||||
["2023-04-30", 10000.0, 40000.0, "Single"],
|
||||
["2023-05-31", 10000.0, 50000.0, "Single"],
|
||||
["2023-06-30", 10000.0, 60000.0, "Single"],
|
||||
["2023-07-31", 10000.0, 70000.0, "Single"],
|
||||
["2023-08-31", 10000.0, 80000.0, "Single"],
|
||||
["2023-09-30", 10000.0, 90000.0, "Single"],
|
||||
["2023-10-31", 10000.0, 100000.0, "Single"],
|
||||
["2023-11-30", 10000.0, 110000.0, "Single"],
|
||||
["2023-12-31", 10000.0, 120000.0, "Single"],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
|
||||
for d in get_depr_schedule(asset.name, "Active")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
asset_shift_allocation = frappe.get_doc(
|
||||
{"doctype": "Asset Shift Allocation", "asset": asset.name}
|
||||
).insert()
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
|
||||
for d in asset_shift_allocation.get("depreciation_schedule")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
asset_shift_allocation = frappe.get_doc("Asset Shift Allocation", asset_shift_allocation.name)
|
||||
asset_shift_allocation.depreciation_schedule[4].shift = "Triple"
|
||||
asset_shift_allocation.save()
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
|
||||
for d in asset_shift_allocation.get("depreciation_schedule")
|
||||
]
|
||||
|
||||
expected_schedules = [
|
||||
["2023-01-31", 10000.0, 10000.0, "Single"],
|
||||
["2023-02-28", 10000.0, 20000.0, "Single"],
|
||||
["2023-03-31", 10000.0, 30000.0, "Single"],
|
||||
["2023-04-30", 10000.0, 40000.0, "Single"],
|
||||
["2023-05-31", 20000.0, 60000.0, "Triple"],
|
||||
["2023-06-30", 10000.0, 70000.0, "Single"],
|
||||
["2023-07-31", 10000.0, 80000.0, "Single"],
|
||||
["2023-08-31", 10000.0, 90000.0, "Single"],
|
||||
["2023-09-30", 10000.0, 100000.0, "Single"],
|
||||
["2023-10-31", 10000.0, 110000.0, "Single"],
|
||||
["2023-11-30", 10000.0, 120000.0, "Single"],
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
asset_shift_allocation.submit()
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount, d.shift]
|
||||
for d in get_depr_schedule(asset.name, "Active")
|
||||
]
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
|
||||
def create_asset_shift_factors():
|
||||
shifts = [
|
||||
{"doctype": "Asset Shift Factor", "shift_name": "Half", "shift_factor": 0.5, "default": 0},
|
||||
{"doctype": "Asset Shift Factor", "shift_name": "Single", "shift_factor": 1, "default": 1},
|
||||
{"doctype": "Asset Shift Factor", "shift_name": "Double", "shift_factor": 1.5, "default": 0},
|
||||
{"doctype": "Asset Shift Factor", "shift_name": "Triple", "shift_factor": 2, "default": 0},
|
||||
]
|
||||
|
||||
for s in shifts:
|
||||
frappe.get_doc(s).insert()
|
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Asset Shift Factor", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
@ -0,0 +1,74 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:shift_name",
|
||||
"creation": "2023-11-27 18:16:03.980086",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"shift_name",
|
||||
"shift_factor",
|
||||
"default"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "shift_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Shift Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "shift_factor",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Shift Factor",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "default",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Default"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-29 04:04:24.272872",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Shift Factor",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AssetShiftFactor(Document):
|
||||
def validate(self):
|
||||
self.validate_default()
|
||||
|
||||
def validate_default(self):
|
||||
if self.default:
|
||||
existing_default_shift_factor = frappe.db.get_value(
|
||||
"Asset Shift Factor", {"default": 1}, "name"
|
||||
)
|
||||
|
||||
if existing_default_shift_factor:
|
||||
frappe.throw(
|
||||
_("Asset Shift Factor {0} is set as default currently. Please change it first.").format(
|
||||
frappe.bold(existing_default_shift_factor)
|
||||
)
|
||||
)
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestAssetShiftFactor(FrappeTestCase):
|
||||
pass
|
@ -12,6 +12,7 @@
|
||||
"column_break_3",
|
||||
"accumulated_depreciation_amount",
|
||||
"journal_entry",
|
||||
"shift",
|
||||
"make_depreciation_entry"
|
||||
],
|
||||
"fields": [
|
||||
@ -57,11 +58,17 @@
|
||||
"fieldname": "make_depreciation_entry",
|
||||
"fieldtype": "Button",
|
||||
"label": "Make Depreciation Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "shift",
|
||||
"fieldtype": "Link",
|
||||
"label": "Shift",
|
||||
"options": "Asset Shift Factor"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-07-26 12:56:48.718736",
|
||||
"modified": "2023-11-27 18:28:35.325376",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Depreciation Schedule",
|
||||
|
@ -16,7 +16,7 @@ from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as make_pi_from_po,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
@ -27,6 +27,21 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
|
||||
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_purchase_order_qty(self):
|
||||
po = create_purchase_order(qty=1, do_not_save=True)
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": -1,
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.NonNegativeError, po.save)
|
||||
|
||||
po.items[1].qty = 0
|
||||
self.assertRaises(InvalidQtyError, po.save)
|
||||
|
||||
def test_make_purchase_receipt(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
|
||||
|
@ -214,6 +214,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"oldfieldname": "qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_width": "60px",
|
||||
@ -917,7 +918,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:34:27.267382",
|
||||
"modified": "2023-11-24 13:24:41.298416",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
@ -71,6 +71,10 @@ class AccountMissingError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidQtyError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
force_item_fields = (
|
||||
"item_group",
|
||||
"brand",
|
||||
@ -625,6 +629,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
args["doctype"] = self.doctype
|
||||
args["name"] = self.name
|
||||
args["child_doctype"] = item.doctype
|
||||
args["child_docname"] = item.name
|
||||
args["ignore_pricing_rule"] = (
|
||||
self.ignore_pricing_rule if hasattr(self, "ignore_pricing_rule") else 0
|
||||
@ -910,10 +915,16 @@ class AccountsController(TransactionBase):
|
||||
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
|
||||
|
||||
def validate_qty_is_not_zero(self):
|
||||
if self.doctype != "Purchase Receipt":
|
||||
for item in self.items:
|
||||
if not item.qty:
|
||||
frappe.throw(_("Item quantity can not be zero"))
|
||||
if self.doctype == "Purchase Receipt":
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
if not flt(item.qty):
|
||||
frappe.throw(
|
||||
msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx),
|
||||
title=_("Invalid Quantity"),
|
||||
exc=InvalidQtyError,
|
||||
)
|
||||
|
||||
def validate_account_currency(self, account, account_currency=None):
|
||||
valid_currency = [self.company_currency]
|
||||
@ -3152,16 +3163,19 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||
qty_precision = child_item.precision("qty") or 2
|
||||
|
||||
if flt(child_item.billed_amt, rate_precision) > flt(
|
||||
flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision
|
||||
):
|
||||
# Amount cannot be lesser than billed amount, except for negative amounts
|
||||
row_rate = flt(d.get("rate"), rate_precision)
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format(
|
||||
child_item.idx, child_item.item_code
|
||||
)
|
||||
)
|
||||
else:
|
||||
child_item.rate = flt(d.get("rate"), rate_precision)
|
||||
child_item.rate = row_rate
|
||||
|
||||
if d.get("conversion_factor"):
|
||||
if child_item.stock_uom == child_item.uom:
|
||||
|
@ -1001,6 +1001,7 @@ def make_subcontracted_items():
|
||||
"Subcontracted Item SA5": {},
|
||||
"Subcontracted Item SA6": {},
|
||||
"Subcontracted Item SA7": {},
|
||||
"Subcontracted Item SA8": {},
|
||||
}
|
||||
|
||||
for item, properties in sub_contracted_items.items():
|
||||
@ -1020,6 +1021,7 @@ def make_raw_materials():
|
||||
},
|
||||
"Subcontracted SRM Item 4": {"has_serial_no": 1, "serial_no_series": "SRII.####"},
|
||||
"Subcontracted SRM Item 5": {"has_serial_no": 1, "serial_no_series": "SRIID.####"},
|
||||
"Subcontracted SRM Item 8": {},
|
||||
}
|
||||
|
||||
for item, properties in raw_materials.items():
|
||||
@ -1043,6 +1045,7 @@ def make_service_items():
|
||||
"Subcontracted Service Item 5": {},
|
||||
"Subcontracted Service Item 6": {},
|
||||
"Subcontracted Service Item 7": {},
|
||||
"Subcontracted Service Item 8": {},
|
||||
}
|
||||
|
||||
for item, properties in service_items.items():
|
||||
@ -1066,6 +1069,7 @@ def make_bom_for_subcontracted_items():
|
||||
"Subcontracted Item SA5": ["Subcontracted SRM Item 5"],
|
||||
"Subcontracted Item SA6": ["Subcontracted SRM Item 3"],
|
||||
"Subcontracted Item SA7": ["Subcontracted SRM Item 1"],
|
||||
"Subcontracted Item SA8": ["Subcontracted SRM Item 8"],
|
||||
}
|
||||
|
||||
for item_code, raw_materials in boms.items():
|
||||
|
@ -29,8 +29,16 @@
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-21 12:43:59.106807",
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Competitor Detail",
|
||||
"link_fieldname": "competitor",
|
||||
"parent_doctype": "Quotation",
|
||||
"table_fieldname": "competitors"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:33:54.284279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Competitor",
|
||||
@ -64,5 +72,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -36,6 +36,15 @@ class Lead(SellingController, CRMNote):
|
||||
def before_insert(self):
|
||||
self.contact_doc = None
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
if self.source == "Existing Customer" and self.customer:
|
||||
contact = frappe.db.get_value(
|
||||
"Dynamic Link",
|
||||
{"link_doctype": "Customer", "link_name": self.customer},
|
||||
"parent",
|
||||
)
|
||||
if contact:
|
||||
self.contact_doc = frappe.get_doc("Contact", contact)
|
||||
return
|
||||
self.contact_doc = self.create_contact()
|
||||
|
||||
def after_insert(self):
|
||||
|
@ -419,7 +419,6 @@ scheduler_events = {
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
],
|
||||
"hourly_long": [
|
||||
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
|
||||
"erpnext.utilities.bulk_transaction.retry",
|
||||
],
|
||||
@ -450,6 +449,7 @@ scheduler_events = {
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||
],
|
||||
"daily_long": [
|
||||
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
|
||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
|
||||
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
|
||||
|
@ -185,7 +185,8 @@ class JobCard(Document):
|
||||
# override capacity for employee
|
||||
production_capacity = 1
|
||||
|
||||
if time_logs and production_capacity > len(time_logs):
|
||||
overlap_count = self.get_overlap_count(time_logs)
|
||||
if time_logs and production_capacity > overlap_count:
|
||||
return {}
|
||||
|
||||
if self.workstation_type and time_logs:
|
||||
@ -195,6 +196,37 @@ class JobCard(Document):
|
||||
|
||||
return time_logs[-1]
|
||||
|
||||
@staticmethod
|
||||
def get_overlap_count(time_logs):
|
||||
count = 1
|
||||
|
||||
# Check overlap exists or not between the overlapping time logs with the current Job Card
|
||||
for idx, row in enumerate(time_logs):
|
||||
next_idx = idx
|
||||
if idx + 1 < len(time_logs):
|
||||
next_idx = idx + 1
|
||||
next_row = time_logs[next_idx]
|
||||
if row.name == next_row.name:
|
||||
continue
|
||||
|
||||
if (
|
||||
(
|
||||
get_datetime(next_row.from_time) >= get_datetime(row.from_time)
|
||||
and get_datetime(next_row.from_time) <= get_datetime(row.to_time)
|
||||
)
|
||||
or (
|
||||
get_datetime(next_row.to_time) >= get_datetime(row.from_time)
|
||||
and get_datetime(next_row.to_time) <= get_datetime(row.to_time)
|
||||
)
|
||||
or (
|
||||
get_datetime(next_row.from_time) <= get_datetime(row.from_time)
|
||||
and get_datetime(next_row.to_time) >= get_datetime(row.to_time)
|
||||
)
|
||||
):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def get_time_logs(self, args, doctype, check_next_available_slot=False):
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType(doctype)
|
||||
@ -211,7 +243,14 @@ class JobCard(Document):
|
||||
query = (
|
||||
frappe.qb.from_(jctl)
|
||||
.from_(jc)
|
||||
.select(jc.name.as_("name"), jctl.from_time, jctl.to_time, jc.workstation, jc.workstation_type)
|
||||
.select(
|
||||
jc.name.as_("name"),
|
||||
jctl.name.as_("row_name"),
|
||||
jctl.from_time,
|
||||
jctl.to_time,
|
||||
jc.workstation,
|
||||
jc.workstation_type,
|
||||
)
|
||||
.where(
|
||||
(jctl.parent == jc.name)
|
||||
& (Criterion.any(time_conditions))
|
||||
|
@ -921,6 +921,20 @@ class TestWorkOrder(FrappeTestCase):
|
||||
"Test RM Item 2 for Scrap Item Test",
|
||||
]
|
||||
|
||||
from_time = add_days(now(), -1)
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card Time Log",
|
||||
fields=["distinct parent as name", "docstatus"],
|
||||
filters={"from_time": (">", from_time)},
|
||||
order_by="creation asc",
|
||||
)
|
||||
|
||||
for job_card in job_cards:
|
||||
if job_card.docstatus == 1:
|
||||
frappe.get_doc("Job Card", job_card.name).cancel()
|
||||
|
||||
frappe.delete_doc("Job Card Time Log", job_card.name)
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
for item_code in items:
|
||||
create_item(
|
||||
|
@ -86,6 +86,7 @@ def get_asset_finance_books_map():
|
||||
afb.frequency_of_depreciation,
|
||||
afb.rate_of_depreciation,
|
||||
afb.expected_value_after_useful_life,
|
||||
afb.shift_based,
|
||||
)
|
||||
.where(asset.docstatus < 2)
|
||||
.orderby(afb.idx)
|
||||
|
@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", {
|
||||
frappe.confirm(__(confirm_msg, [__("Issue")]), () => {
|
||||
frm.trigger('make_issue_from_communication');
|
||||
})
|
||||
}, "Create");
|
||||
}, __("Create"));
|
||||
}
|
||||
|
||||
if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) {
|
||||
|
@ -512,6 +512,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
cost_center: item.cost_center,
|
||||
tax_category: me.frm.doc.tax_category,
|
||||
item_tax_template: item.item_tax_template,
|
||||
child_doctype: item.doctype,
|
||||
child_docname: item.name,
|
||||
is_old_subcontracting_flow: me.frm.doc.is_old_subcontracting_flow,
|
||||
}
|
||||
|
@ -141,7 +141,7 @@ def get_total_emiratewise(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
s.vat_emirate as emirate, sum(i.base_amount) as total, sum(i.tax_amount)
|
||||
s.vat_emirate as emirate, sum(i.base_net_amount) as total, sum(i.tax_amount)
|
||||
from
|
||||
`tabSales Invoice Item` i inner join `tabSales Invoice` s
|
||||
on
|
||||
@ -356,7 +356,7 @@ def get_zero_rated_total(filters):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
sum(i.base_amount) as total
|
||||
sum(i.base_net_amount) as total
|
||||
from
|
||||
`tabSales Invoice Item` i inner join `tabSales Invoice` s
|
||||
on
|
||||
@ -383,7 +383,7 @@ def get_exempt_total(filters):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
sum(i.base_amount) as total
|
||||
sum(i.base_net_amount) as total
|
||||
from
|
||||
`tabSales Invoice Item` i inner join `tabSales Invoice` s
|
||||
on
|
||||
|
@ -76,16 +76,19 @@ class ProductBundle(Document):
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
|
||||
product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name")
|
||||
|
||||
item = frappe.qb.DocType("Item")
|
||||
return (
|
||||
query = (
|
||||
frappe.qb.from_(item)
|
||||
.select("*")
|
||||
.select(item.item_code, item.item_name)
|
||||
.where(
|
||||
(item.is_stock_item == 0)
|
||||
& (item.is_fixed_asset == 0)
|
||||
& (item.name.notin(product_bundles))
|
||||
& (item[searchfield].like(f"%{txt}%"))
|
||||
(item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%"))
|
||||
)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
).run()
|
||||
)
|
||||
|
||||
if product_bundles:
|
||||
query = query.where(item.name.notin(product_bundles))
|
||||
|
||||
return query.run()
|
||||
|
@ -214,13 +214,12 @@ frappe.ui.form.on("Sales Order", {
|
||||
label: __("Items to Reserve"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
fieldname: "name",
|
||||
fieldname: "sales_order_item",
|
||||
fieldtype: "Data",
|
||||
label: __("Name"),
|
||||
label: __("Sales Order Item"),
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
@ -260,7 +259,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
],
|
||||
primary_action_label: __("Reserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = {items: dialog.fields_dict.items.grid.get_selected_children()};
|
||||
var data = {items: dialog.fields_dict.items.grid.data};
|
||||
|
||||
if (data.items && data.items.length > 0) {
|
||||
frappe.call({
|
||||
@ -278,9 +277,6 @@ frappe.ui.form.on("Sales Order", {
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(__("Please select items to reserve."));
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
},
|
||||
@ -292,7 +288,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
|
||||
if (unreserved_qty > 0) {
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
'name': item.name,
|
||||
'sales_order_item': item.name,
|
||||
'item_code': item.item_code,
|
||||
'warehouse': item.warehouse,
|
||||
'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor))
|
||||
@ -308,7 +304,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
cancel_stock_reservation_entries(frm) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Stock Unreservation"),
|
||||
size: "large",
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "sr_entries",
|
||||
@ -316,14 +312,13 @@ frappe.ui.form.on("Sales Order", {
|
||||
label: __("Reserved Stock"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
in_place_edit: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
fieldname: "name",
|
||||
fieldname: "sre",
|
||||
fieldtype: "Link",
|
||||
label: __("SRE"),
|
||||
label: __("Stock Reservation Entry"),
|
||||
options: "Stock Reservation Entry",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
@ -360,14 +355,14 @@ frappe.ui.form.on("Sales Order", {
|
||||
],
|
||||
primary_action_label: __("Unreserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = {sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children()};
|
||||
var data = {sr_entries: dialog.fields_dict.sr_entries.grid.data};
|
||||
|
||||
if (data.sr_entries && data.sr_entries.length > 0) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "cancel_stock_reservation_entries",
|
||||
args: {
|
||||
sre_list: data.sr_entries,
|
||||
sre_list: data.sr_entries.map(item => item.sre),
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Unreserving Stock...'),
|
||||
@ -377,9 +372,6 @@ frappe.ui.form.on("Sales Order", {
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(__("Please select items to unreserve."));
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
},
|
||||
@ -396,7 +388,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
r.message.forEach(sre => {
|
||||
if (flt(sre.reserved_qty) > flt(sre.delivered_qty)) {
|
||||
dialog.fields_dict.sr_entries.df.data.push({
|
||||
'name': sre.name,
|
||||
'sre': sre.name,
|
||||
'item_code': sre.item_code,
|
||||
'warehouse': sre.warehouse,
|
||||
'qty': (flt(sre.reserved_qty) - flt(sre.delivered_qty))
|
||||
|
@ -51,6 +51,35 @@ class TestSalesOrder(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_sales_order_with_negative_rate(self):
|
||||
"""
|
||||
Test if negative rate is allowed in Sales Order via doc submission and update items
|
||||
"""
|
||||
so = make_sales_order(qty=1, rate=100, do_not_save=True)
|
||||
so.append("items", {"item_code": "_Test Item", "qty": 1, "rate": -10})
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
first_item = so.get("items")[0]
|
||||
second_item = so.get("items")[1]
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": first_item.item_code,
|
||||
"rate": first_item.rate,
|
||||
"qty": first_item.qty,
|
||||
"docname": first_item.name,
|
||||
},
|
||||
{
|
||||
"item_code": second_item.item_code,
|
||||
"rate": -20,
|
||||
"qty": second_item.qty,
|
||||
"docname": second_item.name,
|
||||
},
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Sales Order", trans_item, so.name)
|
||||
|
||||
def test_make_material_request(self):
|
||||
so = make_sales_order(do_not_submit=True)
|
||||
|
||||
|
@ -200,6 +200,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"oldfieldname": "qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_width": "100px",
|
||||
@ -895,7 +896,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:37:12.787893",
|
||||
"modified": "2023-11-24 13:24:55.756320",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
0
erpnext/selling/report/lost_quotations/__init__.py
Normal file
0
erpnext/selling/report/lost_quotations/__init__.py
Normal file
40
erpnext/selling/report/lost_quotations/lost_quotations.js
Normal file
40
erpnext/selling/report/lost_quotations/lost_quotations.js
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Lost Quotations"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
label: "Timespan",
|
||||
fieldtype: "Select",
|
||||
fieldname: "timespan",
|
||||
options: [
|
||||
"Last Week",
|
||||
"Last Month",
|
||||
"Last Quarter",
|
||||
"Last 6 months",
|
||||
"Last Year",
|
||||
"This Week",
|
||||
"This Month",
|
||||
"This Quarter",
|
||||
"This Year",
|
||||
],
|
||||
default: "This Year",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group By"),
|
||||
fieldtype: "Select",
|
||||
options: ["Lost Reason", "Competitor"],
|
||||
default: "Lost Reason",
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
};
|
30
erpnext/selling/report/lost_quotations/lost_quotations.json
Normal file
30
erpnext/selling/report/lost_quotations/lost_quotations.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-11-23 18:00:19.141922",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": null,
|
||||
"letterhead": null,
|
||||
"modified": "2023-11-23 19:27:28.854108",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Lost Quotations",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Quotation",
|
||||
"report_name": "Lost Quotations",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
}
|
||||
]
|
||||
}
|
98
erpnext/selling/report/lost_quotations/lost_quotations.py
Normal file
98
erpnext/selling/report/lost_quotations/lost_quotations.py
Normal file
@ -0,0 +1,98 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from typing import Literal
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.query_builder.functions import Coalesce, Count, Round, Sum
|
||||
from frappe.utils.data import get_timespan_date_range
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns(filters.get("group_by"))
|
||||
from_date, to_date = get_timespan_date_range(filters.get("timespan").lower())
|
||||
data = get_data(filters.get("company"), from_date, to_date, filters.get("group_by"))
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(group_by: Literal["Lost Reason", "Competitor"]):
|
||||
return [
|
||||
{
|
||||
"fieldname": "lost_reason" if group_by == "Lost Reason" else "competitor",
|
||||
"label": _("Lost Reason") if group_by == "Lost Reason" else _("Competitor"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Quotation Lost Reason" if group_by == "Lost Reason" else "Competitor",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"filedname": "lost_quotations",
|
||||
"label": _("Lost Quotations"),
|
||||
"fieldtype": "Int",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"filedname": "lost_quotations_pct",
|
||||
"label": _("Lost Quotations %"),
|
||||
"fieldtype": "Percent",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"fieldname": "lost_value",
|
||||
"label": _("Lost Value"),
|
||||
"fieldtype": "Currency",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"filedname": "lost_value_pct",
|
||||
"label": _("Lost Value %"),
|
||||
"fieldtype": "Percent",
|
||||
"width": 200,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(
|
||||
company: str, from_date: str, to_date: str, group_by: Literal["Lost Reason", "Competitor"]
|
||||
):
|
||||
"""Return quotation value grouped by lost reason or competitor"""
|
||||
if group_by == "Lost Reason":
|
||||
fieldname = "lost_reason"
|
||||
dimension = frappe.qb.DocType("Quotation Lost Reason Detail")
|
||||
elif group_by == "Competitor":
|
||||
fieldname = "competitor"
|
||||
dimension = frappe.qb.DocType("Competitor Detail")
|
||||
else:
|
||||
frappe.throw(_("Invalid Group By"))
|
||||
|
||||
q = frappe.qb.DocType("Quotation")
|
||||
|
||||
lost_quotation_condition = (
|
||||
(q.status == "Lost")
|
||||
& (q.docstatus == DocStatus.submitted())
|
||||
& (q.transaction_date >= from_date)
|
||||
& (q.transaction_date <= to_date)
|
||||
& (q.company == company)
|
||||
)
|
||||
|
||||
from_lost_quotations = frappe.qb.from_(q).where(lost_quotation_condition)
|
||||
total_quotations = from_lost_quotations.select(Count(q.name))
|
||||
total_value = from_lost_quotations.select(Sum(q.base_net_total))
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(q)
|
||||
.select(
|
||||
Coalesce(dimension[fieldname], _("Not Specified")),
|
||||
Count(q.name).distinct(),
|
||||
Round((Count(q.name).distinct() / total_quotations * 100), 2),
|
||||
Sum(q.base_net_total),
|
||||
Round((Sum(q.base_net_total) / total_value * 100), 2),
|
||||
)
|
||||
.left_join(dimension)
|
||||
.on(dimension.parent == q.name)
|
||||
.where(lost_quotation_condition)
|
||||
.groupby(dimension[fieldname])
|
||||
)
|
||||
|
||||
return query.run()
|
@ -1,83 +1,58 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:order_lost_reason",
|
||||
"beta": 0,
|
||||
"creation": "2013-01-10 16:34:24",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 0,
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "field:order_lost_reason",
|
||||
"creation": "2013-01-10 16:34:24",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"order_lost_reason"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "order_lost_reason",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Quotation Lost Reason",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "order_lost_reason",
|
||||
"oldfieldtype": "Data",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "order_lost_reason",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Quotation Lost Reason",
|
||||
"oldfieldname": "order_lost_reason",
|
||||
"oldfieldtype": "Data",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-flag",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-07-25 05:24:25.533953",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Quotation Lost Reason",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"icon": "fa fa-flag",
|
||||
"idx": 1,
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Quotation Lost Reason Detail",
|
||||
"link_fieldname": "lost_reason",
|
||||
"parent_doctype": "Quotation",
|
||||
"table_fieldname": "lost_reasons"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:31:02.743353",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Quotation Lost Reason",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Master Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Master Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -1247,6 +1247,25 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.reload()
|
||||
self.assertFalse(dn.items[0].target_warehouse)
|
||||
|
||||
def test_serial_no_status(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
item = make_item(
|
||||
"Test Serial Item For Status",
|
||||
{"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TESTSERIAL.#####"},
|
||||
)
|
||||
|
||||
item_code = item.name
|
||||
pi = make_purchase_receipt(qty=1, item_code=item.name)
|
||||
pi.reload()
|
||||
serial_no = get_serial_nos_from_bundle(pi.items[0].serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active")
|
||||
|
||||
dn = create_delivery_note(qty=1, item_code=item_code, serial_no=serial_no)
|
||||
dn.reload()
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
@ -233,7 +233,7 @@ class PickList(Document):
|
||||
for location in self.locations:
|
||||
if location.warehouse and location.sales_order and location.sales_order_item:
|
||||
item_details = {
|
||||
"name": location.sales_order_item,
|
||||
"sales_order_item": location.sales_order_item,
|
||||
"item_code": location.item_code,
|
||||
"warehouse": location.warehouse,
|
||||
"qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),
|
||||
|
@ -573,7 +573,7 @@ class PurchaseReceipt(BuyingController):
|
||||
)
|
||||
|
||||
stock_value_diff = (
|
||||
flt(d.net_amount)
|
||||
flt(d.base_net_amount)
|
||||
+ flt(d.item_tax_amount / self.conversion_rate)
|
||||
+ flt(d.landed_cost_voucher_amount)
|
||||
)
|
||||
@ -784,7 +784,7 @@ class PurchaseReceipt(BuyingController):
|
||||
for item in self.items:
|
||||
if item.sales_order and item.sales_order_item:
|
||||
item_details = {
|
||||
"name": item.sales_order_item,
|
||||
"sales_order_item": item.sales_order_item,
|
||||
"item_code": item.item_code,
|
||||
"warehouse": item.warehouse,
|
||||
"qty_to_reserve": item.stock_qty,
|
||||
|
@ -18,3 +18,22 @@ cur_frm.cscript.onload = function() {
|
||||
frappe.ui.form.on("Serial No", "refresh", function(frm) {
|
||||
frm.toggle_enable("item_code", frm.doc.__islocal);
|
||||
});
|
||||
|
||||
|
||||
frappe.ui.form.on("Serial No", {
|
||||
refresh(frm) {
|
||||
frm.trigger("view_ledgers")
|
||||
},
|
||||
|
||||
view_ledgers(frm) {
|
||||
frm.add_custom_button(__("View Ledgers"), () => {
|
||||
frappe.route_options = {
|
||||
"item_code": frm.doc.item_code,
|
||||
"serial_no": frm.doc.name,
|
||||
"posting_date": frappe.datetime.now_date(),
|
||||
"posting_time": frappe.datetime.now_time()
|
||||
};
|
||||
frappe.set_route("query-report", "Serial No Ledger");
|
||||
}).addClass('btn-primary');
|
||||
}
|
||||
})
|
@ -269,7 +269,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "\nActive\nInactive\nExpired",
|
||||
"options": "\nActive\nInactive\nDelivered\nExpired",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -280,7 +280,7 @@
|
||||
"icon": "fa fa-barcode",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-16 15:58:46.139887",
|
||||
"modified": "2023-11-28 15:37:59.489945",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No",
|
||||
|
@ -869,7 +869,7 @@ def create_stock_reservation_entries_for_so_items(
|
||||
items = []
|
||||
if items_details:
|
||||
for item in items_details:
|
||||
so_item = frappe.get_doc("Sales Order Item", item.get("name"))
|
||||
so_item = frappe.get_doc("Sales Order Item", item.get("sales_order_item"))
|
||||
so_item.warehouse = item.get("warehouse")
|
||||
so_item.qty_to_reserve = (
|
||||
flt(item.get("qty_to_reserve"))
|
||||
@ -1053,12 +1053,14 @@ def cancel_stock_reservation_entries(
|
||||
from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
|
||||
from_voucher_no: str = None,
|
||||
from_voucher_detail_no: str = None,
|
||||
sre_list: list[dict] = None,
|
||||
sre_list: list = None,
|
||||
notify: bool = True,
|
||||
) -> None:
|
||||
"""Cancel Stock Reservation Entries."""
|
||||
|
||||
if not sre_list:
|
||||
sre_list = {}
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
sre_list = get_stock_reservation_entries_for_voucher(
|
||||
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
|
||||
@ -1082,9 +1084,11 @@ def cancel_stock_reservation_entries(
|
||||
|
||||
sre_list = query.run(as_dict=True)
|
||||
|
||||
sre_list = [d.name for d in sre_list]
|
||||
|
||||
if sre_list:
|
||||
for sre in sre_list:
|
||||
frappe.get_doc("Stock Reservation Entry", sre["name"]).cancel()
|
||||
frappe.get_doc("Stock Reservation Entry", sre).cancel()
|
||||
|
||||
if notify:
|
||||
msg = _("Stock Reservation Entries Cancelled")
|
||||
|
@ -8,6 +8,7 @@ import frappe
|
||||
from frappe import _, throw
|
||||
from frappe.model import child_table_fields, default_fields
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, getdate
|
||||
|
||||
@ -571,6 +572,9 @@ def get_item_tax_template(args, item, out):
|
||||
item_tax_template = _get_item_tax_template(args, item_group_doc.taxes, out)
|
||||
item_group = item_group_doc.parent_item_group
|
||||
|
||||
if args.child_doctype and item_tax_template:
|
||||
out.update(get_fetch_values(args.child_doctype, "item_tax_template", item_tax_template))
|
||||
|
||||
|
||||
def _get_item_tax_template(args, taxes, out=None, for_validate=False):
|
||||
if out is None:
|
||||
|
@ -36,21 +36,27 @@ def get_columns(filters):
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "company",
|
||||
"options": "Company",
|
||||
"width": 150,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "warehouse",
|
||||
"options": "Warehouse",
|
||||
"width": 150,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Status"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "status",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Serial No"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "serial_no",
|
||||
"options": "Serial No",
|
||||
"width": 150,
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Valuation Rate"),
|
||||
@ -58,6 +64,12 @@ def get_columns(filters):
|
||||
"fieldname": "valuation_rate",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Qty"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "qty",
|
||||
"width": 150,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
@ -83,12 +95,16 @@ def get_data(filters):
|
||||
"posting_time": row.posting_time,
|
||||
"voucher_type": row.voucher_type,
|
||||
"voucher_no": row.voucher_no,
|
||||
"status": "Active" if row.actual_qty > 0 else "Delivered",
|
||||
"company": row.company,
|
||||
"warehouse": row.warehouse,
|
||||
"qty": 1 if row.actual_qty > 0 else -1,
|
||||
}
|
||||
)
|
||||
|
||||
serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
|
||||
serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}]
|
||||
if row.serial_and_batch_bundle:
|
||||
serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
|
||||
|
||||
for index, bundle_data in enumerate(serial_nos):
|
||||
if index == 0:
|
||||
|
@ -166,4 +166,4 @@ def create_reposting_entries(rows, company):
|
||||
|
||||
if entries:
|
||||
entries = ", ".join(entries)
|
||||
frappe.msgprint(_(f"Reposting entries created: {entries}"))
|
||||
frappe.msgprint(_("Reposting entries created: {0}").format(entries))
|
||||
|
@ -255,11 +255,15 @@ class SerialBatchBundle:
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
status = "Inactive"
|
||||
if self.sle.actual_qty < 0:
|
||||
status = "Delivered"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
(
|
||||
frappe.qb.update(sn_table)
|
||||
.set(sn_table.warehouse, warehouse)
|
||||
.set(sn_table.status, "Active" if warehouse else "Inactive")
|
||||
.set(sn_table.status, "Active" if warehouse else status)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
).run()
|
||||
|
||||
|
@ -8,7 +8,7 @@ from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import is_subcontracting_order_created
|
||||
from erpnext.controllers.subcontracting_controller import SubcontractingController
|
||||
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
|
||||
from erpnext.stock.stock_balance import update_bin_qty
|
||||
from erpnext.stock.utils import get_bin
|
||||
|
||||
|
||||
@ -114,7 +114,32 @@ class SubcontractingOrder(SubcontractingController):
|
||||
):
|
||||
item_wh_list.append([item.item_code, item.warehouse])
|
||||
for item_code, warehouse in item_wh_list:
|
||||
update_bin_qty(item_code, warehouse, {"ordered_qty": get_ordered_qty(item_code, warehouse)})
|
||||
update_bin_qty(
|
||||
item_code, warehouse, {"ordered_qty": self.get_ordered_qty(item_code, warehouse)}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_ordered_qty(item_code, warehouse):
|
||||
table = frappe.qb.DocType("Subcontracting Order")
|
||||
child = frappe.qb.DocType("Subcontracting Order Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select((child.qty - child.received_qty) * child.conversion_factor)
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (child.item_code == item_code)
|
||||
& (child.warehouse == warehouse)
|
||||
& (child.qty > child.received_qty)
|
||||
& (table.status != "Completed")
|
||||
)
|
||||
)
|
||||
|
||||
query = query.run()
|
||||
|
||||
return flt(query[0][0]) if query else 0
|
||||
|
||||
def update_reserved_qty_for_subcontracting(self):
|
||||
for item in self.supplied_items:
|
||||
|
@ -6,6 +6,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_subcontracting_order
|
||||
from erpnext.controllers.subcontracting_controller import (
|
||||
@ -566,6 +567,67 @@ class TestSubcontractingOrder(FrappeTestCase):
|
||||
self.assertEqual(sco.status, "Closed")
|
||||
self.assertEqual(sco.supplied_items[0].returned_qty, 5)
|
||||
|
||||
def test_ordered_qty_for_subcontracting_order(self):
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 8",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA8",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
|
||||
ordered_qty = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
|
||||
fieldname="ordered_qty",
|
||||
)
|
||||
ordered_qty = flt(ordered_qty)
|
||||
|
||||
sco = get_subcontracting_order(service_items=service_items)
|
||||
sco.reload()
|
||||
|
||||
new_ordered_qty = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
|
||||
fieldname="ordered_qty",
|
||||
)
|
||||
new_ordered_qty = flt(new_ordered_qty)
|
||||
|
||||
self.assertEqual(ordered_qty + 10, new_ordered_qty)
|
||||
|
||||
for row in sco.supplied_items:
|
||||
make_stock_entry(
|
||||
target="_Test Warehouse 1 - _TC",
|
||||
item_code=row.rm_item_code,
|
||||
qty=row.required_qty,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
scr = make_subcontracting_receipt(sco.name)
|
||||
scr.submit()
|
||||
|
||||
new_ordered_qty = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
|
||||
fieldname="ordered_qty",
|
||||
)
|
||||
|
||||
self.assertEqual(ordered_qty, new_ordered_qty)
|
||||
|
||||
scr.reload()
|
||||
scr.cancel()
|
||||
|
||||
new_ordered_qty = frappe.db.get_value(
|
||||
"Bin",
|
||||
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "Subcontracted Item SA8"},
|
||||
fieldname="ordered_qty",
|
||||
)
|
||||
|
||||
self.assertEqual(ordered_qty + 10, new_ordered_qty)
|
||||
|
||||
|
||||
def create_subcontracting_order(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -95,12 +95,12 @@ class SubcontractingReceipt(SubcontractingController):
|
||||
)
|
||||
self.update_status_updater_args()
|
||||
self.update_prevdoc_status()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_subcontract_order()
|
||||
self.set_subcontracting_order_status()
|
||||
self.update_stock_ledger()
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.repost_future_sle_and_gle()
|
||||
self.update_status()
|
||||
|
||||
def validate_items_qty(self):
|
||||
|
@ -4,93 +4,67 @@
|
||||
frappe.provide("erpnext.support");
|
||||
|
||||
frappe.ui.form.on("Warranty Claim", {
|
||||
setup: function(frm) {
|
||||
frm.set_query('contact_person', erpnext.queries.contact_query);
|
||||
frm.set_query('customer_address', erpnext.queries.address_query);
|
||||
frm.set_query('customer', erpnext.queries.customer);
|
||||
setup: (frm) => {
|
||||
frm.set_query("contact_person", erpnext.queries.contact_query);
|
||||
frm.set_query("customer_address", erpnext.queries.address_query);
|
||||
frm.set_query("customer", erpnext.queries.customer);
|
||||
|
||||
frm.add_fetch('serial_no', 'item_code', 'item_code');
|
||||
frm.add_fetch('serial_no', 'item_name', 'item_name');
|
||||
frm.add_fetch('serial_no', 'description', 'description');
|
||||
frm.add_fetch('serial_no', 'maintenance_status', 'warranty_amc_status');
|
||||
frm.add_fetch('serial_no', 'warranty_expiry_date', 'warranty_expiry_date');
|
||||
frm.add_fetch('serial_no', 'amc_expiry_date', 'amc_expiry_date');
|
||||
frm.add_fetch('serial_no', 'customer', 'customer');
|
||||
frm.add_fetch('serial_no', 'customer_name', 'customer_name');
|
||||
frm.add_fetch('item_code', 'item_name', 'item_name');
|
||||
frm.add_fetch('item_code', 'description', 'description');
|
||||
frm.set_query("serial_no", () => {
|
||||
let filters = {
|
||||
company: frm.doc.company,
|
||||
};
|
||||
|
||||
if (frm.doc.item_code) {
|
||||
filters["item_code"] = frm.doc.item_code;
|
||||
}
|
||||
|
||||
return { filters: filters };
|
||||
});
|
||||
|
||||
frm.set_query("item_code", () => {
|
||||
return {
|
||||
filters: {
|
||||
disabled: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
onload: function(frm) {
|
||||
if(!frm.doc.status) {
|
||||
frm.set_value('status', 'Open');
|
||||
|
||||
onload: (frm) => {
|
||||
if (!frm.doc.status) {
|
||||
frm.set_value("status", "Open");
|
||||
}
|
||||
},
|
||||
customer: function(frm) {
|
||||
|
||||
refresh: (frm) => {
|
||||
frappe.dynamic_link = {
|
||||
doc: frm.doc,
|
||||
fieldname: "customer",
|
||||
doctype: "Customer",
|
||||
};
|
||||
|
||||
if (
|
||||
!frm.doc.__islocal &&
|
||||
["Open", "Work In Progress"].includes(frm.doc.status)
|
||||
) {
|
||||
frm.add_custom_button(__("Maintenance Visit"), () => {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit",
|
||||
frm: frm,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
customer: (frm) => {
|
||||
erpnext.utils.get_party_details(frm);
|
||||
},
|
||||
customer_address: function(frm) {
|
||||
|
||||
customer_address: (frm) => {
|
||||
erpnext.utils.get_address_display(frm);
|
||||
},
|
||||
contact_person: function(frm) {
|
||||
|
||||
contact_person: (frm) => {
|
||||
erpnext.utils.get_contact_details(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
erpnext.support.WarrantyClaim = class WarrantyClaim extends frappe.ui.form.Controller {
|
||||
refresh() {
|
||||
frappe.dynamic_link = {doc: this.frm.doc, fieldname: 'customer', doctype: 'Customer'}
|
||||
|
||||
if(!cur_frm.doc.__islocal &&
|
||||
(cur_frm.doc.status=='Open' || cur_frm.doc.status == 'Work In Progress')) {
|
||||
cur_frm.add_custom_button(__('Maintenance Visit'),
|
||||
this.make_maintenance_visit);
|
||||
}
|
||||
}
|
||||
|
||||
make_maintenance_visit() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.support.doctype.warranty_claim.warranty_claim.make_maintenance_visit",
|
||||
frm: cur_frm
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.support.WarrantyClaim({frm: cur_frm}));
|
||||
|
||||
cur_frm.fields_dict['serial_no'].get_query = function(doc, cdt, cdn) {
|
||||
var cond = [];
|
||||
var filter = [
|
||||
['Serial No', 'docstatus', '!=', 2]
|
||||
];
|
||||
if(doc.item_code) {
|
||||
cond = ['Serial No', 'item_code', '=', doc.item_code];
|
||||
filter.push(cond);
|
||||
}
|
||||
if(doc.customer) {
|
||||
cond = ['Serial No', 'customer', '=', doc.customer];
|
||||
filter.push(cond);
|
||||
}
|
||||
return{
|
||||
filters:filter
|
||||
}
|
||||
}
|
||||
|
||||
cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
|
||||
if(doc.serial_no) {
|
||||
return{
|
||||
doctype: "Serial No",
|
||||
fields: "item_code",
|
||||
filters:{
|
||||
name: doc.serial_no
|
||||
}
|
||||
}
|
||||
}
|
||||
else{
|
||||
return{
|
||||
filters:[
|
||||
['Item', 'docstatus', '!=', 2],
|
||||
['Item', 'disabled', '=', 0]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -92,7 +92,8 @@
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Serial No",
|
||||
"options": "Serial No"
|
||||
"options": "Serial No",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer",
|
||||
@ -128,6 +129,8 @@
|
||||
"options": "fa fa-ticket"
|
||||
},
|
||||
{
|
||||
"fetch_from": "serial_no.item_code",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -140,6 +143,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.item_code",
|
||||
"fetch_from": "item_code.item_name",
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
@ -149,6 +153,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.item_code",
|
||||
"fetch_from": "item_code.description",
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
@ -164,17 +169,24 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "serial_no.maintenance_status",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "warranty_amc_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Warranty / AMC Status",
|
||||
"options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC"
|
||||
"options": "\nUnder Warranty\nOut of Warranty\nUnder AMC\nOut of AMC",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "serial_no.warranty_expiry_date",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "warranty_expiry_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Warranty Expiry Date"
|
||||
},
|
||||
{
|
||||
"fetch_from": "serial_no.amc_expiry_date",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "amc_expiry_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "AMC Expiry Date"
|
||||
@ -225,6 +237,7 @@
|
||||
{
|
||||
"bold": 1,
|
||||
"depends_on": "customer",
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@ -366,7 +379,7 @@
|
||||
"icon": "fa fa-bug",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-03 16:17:07.694449",
|
||||
"modified": "2023-11-28 17:30:35.676410",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Support",
|
||||
"name": "Warranty Claim",
|
||||
|
0
erpnext/www/all-products/__init__.py
Normal file
0
erpnext/www/all-products/__init__.py
Normal file
0
erpnext/www/shop-by-category/__init__.py
Normal file
0
erpnext/www/shop-by-category/__init__.py
Normal file
Loading…
x
Reference in New Issue
Block a user