Merge pull request #39639 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
commit
32f77eae5d
@ -150,6 +150,20 @@ class JournalEntry(AccountsController):
|
|||||||
if not self.title:
|
if not self.title:
|
||||||
self.title = self.get_title()
|
self.title = self.get_title()
|
||||||
|
|
||||||
|
def submit(self):
|
||||||
|
if len(self.accounts) > 100:
|
||||||
|
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||||
|
self.queue_action("submit", timeout=4600)
|
||||||
|
else:
|
||||||
|
return self._submit()
|
||||||
|
|
||||||
|
def cancel(self):
|
||||||
|
if len(self.accounts) > 100:
|
||||||
|
msgprint(_("The task has been enqueued as a background job."), alert=True)
|
||||||
|
self.queue_action("cancel", timeout=4600)
|
||||||
|
else:
|
||||||
|
return self._cancel()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_cheque_info()
|
self.validate_cheque_info()
|
||||||
self.check_credit_limit()
|
self.check_credit_limit()
|
||||||
|
@ -640,7 +640,7 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
|
|
||||||
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||||
const today = frappe.datetime.get_today();
|
const today = frappe.datetime.get_today();
|
||||||
const fields = [
|
let fields = [
|
||||||
{fieldtype:"Section Break", label: __("Posting Date")},
|
{fieldtype:"Section Break", label: __("Posting Date")},
|
||||||
{fieldtype:"Date", label: __("From Date"),
|
{fieldtype:"Date", label: __("From Date"),
|
||||||
fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)},
|
fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)},
|
||||||
@ -655,18 +655,29 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
fieldname:"outstanding_amt_greater_than", default: 0},
|
fieldname:"outstanding_amt_greater_than", default: 0},
|
||||||
{fieldtype:"Column Break"},
|
{fieldtype:"Column Break"},
|
||||||
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
|
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
|
||||||
{fieldtype:"Section Break"},
|
];
|
||||||
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
|
|
||||||
"get_query": function() {
|
if (frm.dimension_filters) {
|
||||||
return {
|
let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2);
|
||||||
"filters": {"company": frm.doc.company}
|
|
||||||
}
|
fields.push({fieldtype:"Section Break"});
|
||||||
|
frm.dimension_filters.map((elem, idx)=>{
|
||||||
|
fields.push({
|
||||||
|
fieldtype: "Link",
|
||||||
|
label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label,
|
||||||
|
options: elem.document_type,
|
||||||
|
fieldname: elem.fieldname || elem.document_type
|
||||||
|
});
|
||||||
|
if(idx+1 == column_break_insertion_point) {
|
||||||
|
fields.push({fieldtype:"Column Break"});
|
||||||
}
|
}
|
||||||
},
|
});
|
||||||
{fieldtype:"Column Break"},
|
}
|
||||||
|
|
||||||
|
fields = fields.concat([
|
||||||
{fieldtype:"Section Break"},
|
{fieldtype:"Section Break"},
|
||||||
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||||
];
|
]);
|
||||||
|
|
||||||
let btn_text = "";
|
let btn_text = "";
|
||||||
|
|
||||||
|
@ -87,12 +87,14 @@
|
|||||||
"status",
|
"status",
|
||||||
"custom_remarks",
|
"custom_remarks",
|
||||||
"remarks",
|
"remarks",
|
||||||
|
"base_in_words",
|
||||||
"column_break_16",
|
"column_break_16",
|
||||||
"letter_head",
|
"letter_head",
|
||||||
"print_heading",
|
"print_heading",
|
||||||
"bank",
|
"bank",
|
||||||
"bank_account_no",
|
"bank_account_no",
|
||||||
"payment_order",
|
"payment_order",
|
||||||
|
"in_words",
|
||||||
"subscription_section",
|
"subscription_section",
|
||||||
"auto_repeat",
|
"auto_repeat",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
@ -746,6 +748,20 @@
|
|||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Book Advance Payments in Separate Party Account",
|
"label": "Book Advance Payments in Separate Party Account",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_in_words",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "In Words (Company Currency)",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "in_words",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"label": "In Words",
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
@ -759,7 +775,7 @@
|
|||||||
"table_fieldname": "payment_entries"
|
"table_fieldname": "payment_entries"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2023-11-23 12:07:20.887885",
|
"modified": "2024-01-03 12:46:41.759121",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Entry",
|
"name": "Payment Entry",
|
||||||
|
@ -13,6 +13,7 @@ from pypika import Case
|
|||||||
from pypika.functions import Coalesce, Sum
|
from pypika.functions import Coalesce, Sum
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||||
from erpnext.accounts.doctype.bank_account.bank_account import (
|
from erpnext.accounts.doctype.bank_account.bank_account import (
|
||||||
get_bank_account_details,
|
get_bank_account_details,
|
||||||
get_party_bank_account,
|
get_party_bank_account,
|
||||||
@ -95,6 +96,7 @@ class PaymentEntry(AccountsController):
|
|||||||
self.validate_paid_invoices()
|
self.validate_paid_invoices()
|
||||||
self.ensure_supplier_is_not_blocked()
|
self.ensure_supplier_is_not_blocked()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
|
self.set_total_in_words()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.difference_amount:
|
if self.difference_amount:
|
||||||
@ -107,7 +109,7 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
def set_liability_account(self):
|
def set_liability_account(self):
|
||||||
# Auto setting liability account should only be done during 'draft' status
|
# Auto setting liability account should only be done during 'draft' status
|
||||||
if self.docstatus > 0:
|
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||||
return
|
return
|
||||||
|
|
||||||
if not frappe.db.get_value(
|
if not frappe.db.get_value(
|
||||||
@ -702,6 +704,21 @@ class PaymentEntry(AccountsController):
|
|||||||
|
|
||||||
self.db_set("status", self.status, update_modified=True)
|
self.db_set("status", self.status, update_modified=True)
|
||||||
|
|
||||||
|
def set_total_in_words(self):
|
||||||
|
from frappe.utils import money_in_words
|
||||||
|
|
||||||
|
if self.payment_type in ("Pay", "Internal Transfer"):
|
||||||
|
base_amount = abs(self.base_paid_amount)
|
||||||
|
amount = abs(self.paid_amount)
|
||||||
|
currency = self.paid_from_account_currency
|
||||||
|
elif self.payment_type == "Receive":
|
||||||
|
base_amount = abs(self.base_received_amount)
|
||||||
|
amount = abs(self.received_amount)
|
||||||
|
currency = self.paid_to_account_currency
|
||||||
|
|
||||||
|
self.base_in_words = money_in_words(base_amount, self.company_currency)
|
||||||
|
self.in_words = money_in_words(amount, currency)
|
||||||
|
|
||||||
def set_tax_withholding(self):
|
def set_tax_withholding(self):
|
||||||
if not self.party_type == "Supplier":
|
if not self.party_type == "Supplier":
|
||||||
return
|
return
|
||||||
@ -1588,6 +1605,13 @@ def get_outstanding_reference_documents(args, validate=False):
|
|||||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||||
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
|
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
|
||||||
|
|
||||||
|
# dynamic dimension filters
|
||||||
|
active_dimensions = get_dimensions()[0]
|
||||||
|
for dim in active_dimensions:
|
||||||
|
if args.get(dim.fieldname):
|
||||||
|
condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname))
|
||||||
|
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||||
|
|
||||||
date_fields_dict = {
|
date_fields_dict = {
|
||||||
"posting_date": ["from_posting_date", "to_posting_date"],
|
"posting_date": ["from_posting_date", "to_posting_date"],
|
||||||
"due_date": ["from_due_date", "to_due_date"],
|
"due_date": ["from_due_date", "to_due_date"],
|
||||||
@ -1821,6 +1845,12 @@ def get_orders_to_be_billed(
|
|||||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||||
condition = " and cost_center='%s'" % cost_center
|
condition = " and cost_center='%s'" % cost_center
|
||||||
|
|
||||||
|
# dynamic dimension filters
|
||||||
|
active_dimensions = get_dimensions()[0]
|
||||||
|
for dim in active_dimensions:
|
||||||
|
if filters.get(dim.fieldname):
|
||||||
|
condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname))
|
||||||
|
|
||||||
if party_account_currency == company_currency:
|
if party_account_currency == company_currency:
|
||||||
grand_total_field = "base_grand_total"
|
grand_total_field = "base_grand_total"
|
||||||
rounded_total_field = "base_rounded_total"
|
rounded_total_field = "base_rounded_total"
|
||||||
|
@ -95,6 +95,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
this.frm.change_custom_button_type(__('Allocate'), null, 'default');
|
this.frm.change_custom_button_type(__('Allocate'), null, 'default');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.frm.trigger("set_query_for_dimension_filters");
|
||||||
|
|
||||||
// check for any running reconciliation jobs
|
// check for any running reconciliation jobs
|
||||||
if (this.frm.doc.receivable_payable_account) {
|
if (this.frm.doc.receivable_payable_account) {
|
||||||
this.frm.call({
|
this.frm.call({
|
||||||
@ -125,6 +127,25 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
set_query_for_dimension_filters() {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters",
|
||||||
|
args: {
|
||||||
|
company: this.frm.doc.company,
|
||||||
|
},
|
||||||
|
callback: (r) => {
|
||||||
|
if (!r.exc && r.message) {
|
||||||
|
r.message.forEach(x => {
|
||||||
|
this.frm.set_query(x.fieldname, () => {
|
||||||
|
return {
|
||||||
|
'filters': x.filters
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
company() {
|
company() {
|
||||||
this.frm.set_value('party', '');
|
this.frm.set_value('party', '');
|
||||||
|
@ -25,7 +25,9 @@
|
|||||||
"invoice_limit",
|
"invoice_limit",
|
||||||
"payment_limit",
|
"payment_limit",
|
||||||
"bank_cash_account",
|
"bank_cash_account",
|
||||||
|
"accounting_dimensions_section",
|
||||||
"cost_center",
|
"cost_center",
|
||||||
|
"dimension_col_break",
|
||||||
"sec_break1",
|
"sec_break1",
|
||||||
"invoice_name",
|
"invoice_name",
|
||||||
"invoices",
|
"invoices",
|
||||||
@ -208,6 +210,18 @@
|
|||||||
"fieldname": "payment_name",
|
"fieldname": "payment_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Filter on Payment"
|
"label": "Filter on Payment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"collapsible_depends_on": "eval: doc.invoices.length == 0",
|
||||||
|
"depends_on": "eval:doc.receivable_payable_account",
|
||||||
|
"fieldname": "accounting_dimensions_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounting Dimensions Filter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dimension_col_break",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_toolbar": 1,
|
"hide_toolbar": 1,
|
||||||
@ -215,7 +229,7 @@
|
|||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-17 17:33:55.701726",
|
"modified": "2023-12-14 13:38:16.264013",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation",
|
"name": "Payment Reconciliation",
|
||||||
|
@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn
|
|||||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||||
is_any_doc_running,
|
is_any_doc_running,
|
||||||
)
|
)
|
||||||
@ -70,6 +71,7 @@ class PaymentReconciliation(Document):
|
|||||||
self.common_filter_conditions = []
|
self.common_filter_conditions = []
|
||||||
self.accounting_dimension_filter_conditions = []
|
self.accounting_dimension_filter_conditions = []
|
||||||
self.ple_posting_date_filter = []
|
self.ple_posting_date_filter = []
|
||||||
|
self.dimensions = get_dimensions()[0]
|
||||||
|
|
||||||
def load_from_db(self):
|
def load_from_db(self):
|
||||||
# 'modified' attribute is required for `run_doc_method` to work properly.
|
# 'modified' attribute is required for `run_doc_method` to work properly.
|
||||||
@ -172,6 +174,14 @@ class PaymentReconciliation(Document):
|
|||||||
if self.payment_name:
|
if self.payment_name:
|
||||||
condition.update({"name": self.payment_name})
|
condition.update({"name": self.payment_name})
|
||||||
|
|
||||||
|
# pass dynamic dimension filter values to query builder
|
||||||
|
dimensions = {}
|
||||||
|
for x in self.dimensions:
|
||||||
|
dimension = x.fieldname
|
||||||
|
if self.get(dimension):
|
||||||
|
dimensions.update({dimension: self.get(dimension)})
|
||||||
|
condition.update({"accounting_dimensions": dimensions})
|
||||||
|
|
||||||
payment_entries = get_advance_payment_entries_for_regional(
|
payment_entries = get_advance_payment_entries_for_regional(
|
||||||
self.party_type,
|
self.party_type,
|
||||||
self.party,
|
self.party,
|
||||||
@ -185,66 +195,67 @@ class PaymentReconciliation(Document):
|
|||||||
return payment_entries
|
return payment_entries
|
||||||
|
|
||||||
def get_jv_entries(self):
|
def get_jv_entries(self):
|
||||||
condition = self.get_conditions()
|
je = qb.DocType("Journal Entry")
|
||||||
|
jea = qb.DocType("Journal Entry Account")
|
||||||
|
conditions = self.get_journal_filter_conditions()
|
||||||
|
|
||||||
|
# Dimension filters
|
||||||
|
for x in self.dimensions:
|
||||||
|
dimension = x.fieldname
|
||||||
|
if self.get(dimension):
|
||||||
|
conditions.append(jea[dimension] == self.get(dimension))
|
||||||
|
|
||||||
if self.payment_name:
|
if self.payment_name:
|
||||||
condition += f" and t1.name like '%%{self.payment_name}%%'"
|
conditions.append(je.name.like(f"%%{self.payment_name}%%"))
|
||||||
|
|
||||||
if self.get("cost_center"):
|
if self.get("cost_center"):
|
||||||
condition += f" and t2.cost_center = '{self.cost_center}' "
|
conditions.append(jea.cost_center == self.cost_center)
|
||||||
|
|
||||||
dr_or_cr = (
|
dr_or_cr = (
|
||||||
"credit_in_account_currency"
|
"credit_in_account_currency"
|
||||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||||
else "debit_in_account_currency"
|
else "debit_in_account_currency"
|
||||||
)
|
)
|
||||||
|
conditions.append(jea[dr_or_cr].gt(0))
|
||||||
|
|
||||||
bank_account_condition = (
|
if self.bank_cash_account:
|
||||||
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
|
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||||
|
|
||||||
|
journal_query = (
|
||||||
|
qb.from_(je)
|
||||||
|
.inner_join(jea)
|
||||||
|
.on(jea.parent == je.name)
|
||||||
|
.select(
|
||||||
|
ConstantColumn("Journal Entry").as_("reference_type"),
|
||||||
|
je.name.as_("reference_name"),
|
||||||
|
je.posting_date,
|
||||||
|
je.remark.as_("remarks"),
|
||||||
|
jea.name.as_("reference_row"),
|
||||||
|
jea[dr_or_cr].as_("amount"),
|
||||||
|
jea.is_advance,
|
||||||
|
jea.exchange_rate,
|
||||||
|
jea.account_currency.as_("currency"),
|
||||||
|
jea.cost_center.as_("cost_center"),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(je.docstatus == 1)
|
||||||
|
& (jea.party_type == self.party_type)
|
||||||
|
& (jea.party == self.party)
|
||||||
|
& (jea.account == self.receivable_payable_account)
|
||||||
|
& (
|
||||||
|
(jea.reference_type == "")
|
||||||
|
| (jea.reference_type.isnull())
|
||||||
|
| (jea.reference_type.isin(("Sales Order", "Purchase Order")))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(Criterion.all(conditions))
|
||||||
|
.orderby(je.posting_date)
|
||||||
)
|
)
|
||||||
|
|
||||||
limit = f"limit {self.payment_limit}" if self.payment_limit else " "
|
if self.payment_limit:
|
||||||
|
journal_query = journal_query.limit(self.payment_limit)
|
||||||
|
|
||||||
# nosemgrep
|
journal_entries = journal_query.run(as_dict=True)
|
||||||
journal_entries = frappe.db.sql(
|
|
||||||
"""
|
|
||||||
select
|
|
||||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
|
||||||
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
|
||||||
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
|
|
||||||
t2.account_currency as currency, t2.cost_center as cost_center
|
|
||||||
from
|
|
||||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
|
||||||
where
|
|
||||||
t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1
|
|
||||||
and t2.party_type = %(party_type)s and t2.party = %(party)s
|
|
||||||
and t2.account = %(account)s and {dr_or_cr} > 0 {condition}
|
|
||||||
and (t2.reference_type is null or t2.reference_type = '' or
|
|
||||||
(t2.reference_type in ('Sales Order', 'Purchase Order')
|
|
||||||
and t2.reference_name is not null and t2.reference_name != ''))
|
|
||||||
and (CASE
|
|
||||||
WHEN t1.voucher_type in ('Debit Note', 'Credit Note')
|
|
||||||
THEN 1=1
|
|
||||||
ELSE {bank_account_condition}
|
|
||||||
END)
|
|
||||||
order by t1.posting_date
|
|
||||||
{limit}
|
|
||||||
""".format(
|
|
||||||
**{
|
|
||||||
"dr_or_cr": dr_or_cr,
|
|
||||||
"bank_account_condition": bank_account_condition,
|
|
||||||
"condition": condition,
|
|
||||||
"limit": limit,
|
|
||||||
}
|
|
||||||
),
|
|
||||||
{
|
|
||||||
"party_type": self.party_type,
|
|
||||||
"party": self.party,
|
|
||||||
"account": self.receivable_payable_account,
|
|
||||||
"bank_cash_account": "%%%s%%" % self.bank_cash_account,
|
|
||||||
},
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
return list(journal_entries)
|
return list(journal_entries)
|
||||||
|
|
||||||
@ -298,6 +309,7 @@ class PaymentReconciliation(Document):
|
|||||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||||
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
|
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
|
||||||
get_payments=True,
|
get_payments=True,
|
||||||
|
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||||
)
|
)
|
||||||
|
|
||||||
for inv in return_outstanding:
|
for inv in return_outstanding:
|
||||||
@ -446,8 +458,15 @@ class PaymentReconciliation(Document):
|
|||||||
row = self.append("allocation", {})
|
row = self.append("allocation", {})
|
||||||
row.update(entry)
|
row.update(entry)
|
||||||
|
|
||||||
|
def update_dimension_values_in_allocated_entries(self, res):
|
||||||
|
for x in self.dimensions:
|
||||||
|
dimension = x.fieldname
|
||||||
|
if self.get(dimension):
|
||||||
|
res[dimension] = self.get(dimension)
|
||||||
|
return res
|
||||||
|
|
||||||
def get_allocated_entry(self, pay, inv, allocated_amount):
|
def get_allocated_entry(self, pay, inv, allocated_amount):
|
||||||
return frappe._dict(
|
res = frappe._dict(
|
||||||
{
|
{
|
||||||
"reference_type": pay.get("reference_type"),
|
"reference_type": pay.get("reference_type"),
|
||||||
"reference_name": pay.get("reference_name"),
|
"reference_name": pay.get("reference_name"),
|
||||||
@ -463,6 +482,9 @@ class PaymentReconciliation(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
res = self.update_dimension_values_in_allocated_entries(res)
|
||||||
|
return res
|
||||||
|
|
||||||
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
||||||
adjust_allocations_for_taxes(self)
|
adjust_allocations_for_taxes(self)
|
||||||
dr_or_cr = (
|
dr_or_cr = (
|
||||||
@ -485,10 +507,10 @@ class PaymentReconciliation(Document):
|
|||||||
reconciled_entry.append(payment_details)
|
reconciled_entry.append(payment_details)
|
||||||
|
|
||||||
if entry_list:
|
if entry_list:
|
||||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions)
|
||||||
|
|
||||||
if dr_or_cr_notes:
|
if dr_or_cr_notes:
|
||||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
|
reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def reconcile(self):
|
def reconcile(self):
|
||||||
@ -517,7 +539,7 @@ class PaymentReconciliation(Document):
|
|||||||
self.get_unreconciled_entries()
|
self.get_unreconciled_entries()
|
||||||
|
|
||||||
def get_payment_details(self, row, dr_or_cr):
|
def get_payment_details(self, row, dr_or_cr):
|
||||||
return frappe._dict(
|
payment_details = frappe._dict(
|
||||||
{
|
{
|
||||||
"voucher_type": row.get("reference_type"),
|
"voucher_type": row.get("reference_type"),
|
||||||
"voucher_no": row.get("reference_name"),
|
"voucher_no": row.get("reference_name"),
|
||||||
@ -539,6 +561,12 @@ class PaymentReconciliation(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for x in self.dimensions:
|
||||||
|
if row.get(x.fieldname):
|
||||||
|
payment_details[x.fieldname] = row.get(x.fieldname)
|
||||||
|
|
||||||
|
return payment_details
|
||||||
|
|
||||||
def check_mandatory_to_fetch(self):
|
def check_mandatory_to_fetch(self):
|
||||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||||
if not self.get(fieldname):
|
if not self.get(fieldname):
|
||||||
@ -646,6 +674,13 @@ class PaymentReconciliation(Document):
|
|||||||
if not invoices_to_reconcile:
|
if not invoices_to_reconcile:
|
||||||
frappe.throw(_("No records found in Allocation table"))
|
frappe.throw(_("No records found in Allocation table"))
|
||||||
|
|
||||||
|
def build_dimensions_filter_conditions(self):
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
for x in self.dimensions:
|
||||||
|
dimension = x.fieldname
|
||||||
|
if self.get(dimension):
|
||||||
|
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
|
||||||
|
|
||||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||||
self.common_filter_conditions.clear()
|
self.common_filter_conditions.clear()
|
||||||
self.accounting_dimension_filter_conditions.clear()
|
self.accounting_dimension_filter_conditions.clear()
|
||||||
@ -669,40 +704,30 @@ class PaymentReconciliation(Document):
|
|||||||
if self.to_payment_date:
|
if self.to_payment_date:
|
||||||
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
|
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
|
||||||
|
|
||||||
def get_conditions(self, get_payments=False):
|
self.build_dimensions_filter_conditions()
|
||||||
condition = " and company = '{0}' ".format(self.company)
|
|
||||||
|
|
||||||
if self.get("cost_center") and get_payments:
|
def get_journal_filter_conditions(self):
|
||||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
conditions = []
|
||||||
|
je = qb.DocType("Journal Entry")
|
||||||
|
jea = qb.DocType("Journal Entry Account")
|
||||||
|
conditions.append(je.company == self.company)
|
||||||
|
|
||||||
condition += (
|
if self.from_payment_date:
|
||||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
conditions.append(je.posting_date.gte(self.from_payment_date))
|
||||||
if self.from_payment_date
|
|
||||||
else ""
|
if self.to_payment_date:
|
||||||
)
|
conditions.append(je.posting_date.lte(self.to_payment_date))
|
||||||
condition += (
|
|
||||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
|
||||||
if self.to_payment_date
|
|
||||||
else ""
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.minimum_payment_amount:
|
if self.minimum_payment_amount:
|
||||||
condition += (
|
conditions.append(je.total_debit.gte(self.minimum_payment_amount))
|
||||||
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
|
|
||||||
if get_payments
|
|
||||||
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
|
|
||||||
)
|
|
||||||
if self.maximum_payment_amount:
|
if self.maximum_payment_amount:
|
||||||
condition += (
|
conditions.append(je.total_debit.lte(self.maximum_payment_amount))
|
||||||
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
|
|
||||||
if get_payments
|
|
||||||
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
|
|
||||||
)
|
|
||||||
|
|
||||||
return condition
|
return conditions
|
||||||
|
|
||||||
|
|
||||||
def reconcile_dr_cr_note(dr_cr_notes, company):
|
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||||
for inv in dr_cr_notes:
|
for inv in dr_cr_notes:
|
||||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||||
|
|
||||||
@ -752,6 +777,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Credit Note(JE) will inherit the same dimension values as payment
|
||||||
|
dimensions_dict = frappe._dict()
|
||||||
|
if active_dimensions:
|
||||||
|
for dim in active_dimensions:
|
||||||
|
dimensions_dict[dim.fieldname] = inv.get(dim.fieldname)
|
||||||
|
|
||||||
|
jv.accounts[0].update(dimensions_dict)
|
||||||
|
jv.accounts[1].update(dimensions_dict)
|
||||||
|
|
||||||
jv.flags.ignore_mandatory = True
|
jv.flags.ignore_mandatory = True
|
||||||
jv.flags.ignore_exchange_rate = True
|
jv.flags.ignore_exchange_rate = True
|
||||||
jv.remark = None
|
jv.remark = None
|
||||||
@ -785,9 +819,27 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
|||||||
inv.against_voucher,
|
inv.against_voucher,
|
||||||
None,
|
None,
|
||||||
inv.cost_center,
|
inv.cost_center,
|
||||||
|
dimensions_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def adjust_allocations_for_taxes(doc):
|
def adjust_allocations_for_taxes(doc):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_queries_for_dimension_filters(company: str = None):
|
||||||
|
dimensions_with_filters = []
|
||||||
|
for d in get_dimensions()[0]:
|
||||||
|
filters = {}
|
||||||
|
meta = frappe.get_meta(d.document_type)
|
||||||
|
if meta.has_field("company") and company:
|
||||||
|
filters.update({"company": company})
|
||||||
|
|
||||||
|
if meta.is_tree:
|
||||||
|
filters.update({"is_group": 0})
|
||||||
|
|
||||||
|
dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters})
|
||||||
|
|
||||||
|
return dimensions_with_filters
|
||||||
|
@ -23,7 +23,9 @@
|
|||||||
"difference_account",
|
"difference_account",
|
||||||
"exchange_rate",
|
"exchange_rate",
|
||||||
"currency",
|
"currency",
|
||||||
"cost_center"
|
"accounting_dimensions_section",
|
||||||
|
"cost_center",
|
||||||
|
"dimension_col_break"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -151,12 +153,26 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "gain_loss_posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Difference Posting Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "accounting_dimensions_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Accounting Dimensions"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "dimension_col_break",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_virtual": 1,
|
"is_virtual": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-17 17:33:38.612615",
|
"modified": "2023-12-14 13:38:26.104150",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Allocation",
|
"name": "Payment Reconciliation Allocation",
|
||||||
|
@ -11,7 +11,6 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_lo
|
|||||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||||
SalesInvoice,
|
SalesInvoice,
|
||||||
get_bank_cash_account,
|
|
||||||
get_mode_of_payment_info,
|
get_mode_of_payment_info,
|
||||||
update_multi_mode_option,
|
update_multi_mode_option,
|
||||||
)
|
)
|
||||||
@ -208,7 +207,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
self.validate_stock_availablility()
|
self.validate_stock_availablility()
|
||||||
self.validate_return_items_qty()
|
self.validate_return_items_qty()
|
||||||
self.set_status()
|
self.set_status()
|
||||||
self.set_account_for_mode_of_payment()
|
|
||||||
self.validate_pos()
|
self.validate_pos()
|
||||||
self.validate_payment_amount()
|
self.validate_payment_amount()
|
||||||
self.validate_loyalty_transaction()
|
self.validate_loyalty_transaction()
|
||||||
@ -643,11 +641,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
update_multi_mode_option(self, pos_profile)
|
update_multi_mode_option(self, pos_profile)
|
||||||
self.paid_amount = 0
|
self.paid_amount = 0
|
||||||
|
|
||||||
def set_account_for_mode_of_payment(self):
|
|
||||||
for pay in self.payments:
|
|
||||||
if not pay.account:
|
|
||||||
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_payment_request(self):
|
def create_payment_request(self):
|
||||||
for pay in self.payments:
|
for pay in self.payments:
|
||||||
|
@ -1985,6 +1985,21 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
|
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
|
||||||
|
|
||||||
|
def test_debit_note_with_account_mismatch(self):
|
||||||
|
new_creditors = create_account(
|
||||||
|
parent_account="Accounts Payable - _TC",
|
||||||
|
account_name="Creditors 2",
|
||||||
|
company="_Test Company",
|
||||||
|
account_type="Payable",
|
||||||
|
)
|
||||||
|
pi = make_purchase_invoice(qty=1, rate=1000)
|
||||||
|
dr_note = make_purchase_invoice(
|
||||||
|
qty=-1, rate=1000, is_return=1, return_against=pi.name, do_not_save=True
|
||||||
|
)
|
||||||
|
dr_note.credit_to = new_creditors
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, dr_note.save)
|
||||||
|
|
||||||
def test_debit_note_without_item(self):
|
def test_debit_note_without_item(self):
|
||||||
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
|
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
|
||||||
pi.items[0].item_code = ""
|
pi.items[0].item_code = ""
|
||||||
|
@ -420,7 +420,8 @@ class SalesInvoice(SellingController):
|
|||||||
self.calculate_taxes_and_totals()
|
self.calculate_taxes_and_totals()
|
||||||
|
|
||||||
def before_save(self):
|
def before_save(self):
|
||||||
set_account_for_mode_of_payment(self)
|
self.set_account_for_mode_of_payment()
|
||||||
|
self.set_paid_amount()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_pos_paid_amount()
|
self.validate_pos_paid_amount()
|
||||||
@ -706,9 +707,6 @@ class SalesInvoice(SellingController):
|
|||||||
):
|
):
|
||||||
data.sales_invoice = sales_invoice
|
data.sales_invoice = sales_invoice
|
||||||
|
|
||||||
def on_update(self):
|
|
||||||
self.set_paid_amount()
|
|
||||||
|
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
if hasattr(self, "repost_required"):
|
if hasattr(self, "repost_required"):
|
||||||
fields_to_check = [
|
fields_to_check = [
|
||||||
@ -739,6 +737,11 @@ class SalesInvoice(SellingController):
|
|||||||
self.paid_amount = paid_amount
|
self.paid_amount = paid_amount
|
||||||
self.base_paid_amount = base_paid_amount
|
self.base_paid_amount = base_paid_amount
|
||||||
|
|
||||||
|
def set_account_for_mode_of_payment(self):
|
||||||
|
for payment in self.payments:
|
||||||
|
if not payment.account:
|
||||||
|
payment.account = get_bank_cash_account(payment.mode_of_payment, self.company).get("account")
|
||||||
|
|
||||||
def validate_time_sheets_are_submitted(self):
|
def validate_time_sheets_are_submitted(self):
|
||||||
for data in self.timesheets:
|
for data in self.timesheets:
|
||||||
if data.time_sheet:
|
if data.time_sheet:
|
||||||
@ -2107,12 +2110,6 @@ def make_sales_return(source_name, target_doc=None):
|
|||||||
return make_return_doc("Sales Invoice", source_name, target_doc)
|
return make_return_doc("Sales Invoice", source_name, target_doc)
|
||||||
|
|
||||||
|
|
||||||
def set_account_for_mode_of_payment(self):
|
|
||||||
for data in self.payments:
|
|
||||||
if not data.account:
|
|
||||||
data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account")
|
|
||||||
|
|
||||||
|
|
||||||
def get_inter_company_details(doc, doctype):
|
def get_inter_company_details(doc, doctype):
|
||||||
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
|
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
|
||||||
parties = frappe.db.get_all(
|
parties = frappe.db.get_all(
|
||||||
|
@ -1533,6 +1533,19 @@ class TestSalesInvoice(FrappeTestCase):
|
|||||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
|
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
|
||||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
|
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
|
||||||
|
|
||||||
|
def test_return_invoice_with_account_mismatch(self):
|
||||||
|
debtors2 = create_account(
|
||||||
|
parent_account="Accounts Receivable - _TC",
|
||||||
|
account_name="Debtors 2",
|
||||||
|
company="_Test Company",
|
||||||
|
account_type="Receivable",
|
||||||
|
)
|
||||||
|
si = create_sales_invoice(qty=1, rate=1000)
|
||||||
|
cr_note = create_sales_invoice(
|
||||||
|
qty=-1, rate=1000, is_return=1, return_against=si.name, debit_to=debtors2, do_not_save=True
|
||||||
|
)
|
||||||
|
self.assertRaises(frappe.ValidationError, cr_note.save)
|
||||||
|
|
||||||
def test_gle_made_when_asset_is_returned(self):
|
def test_gle_made_when_asset_is_returned(self):
|
||||||
create_asset_data()
|
create_asset_data()
|
||||||
asset = create_asset(item_code="Macbook Pro")
|
asset = create_asset(item_code="Macbook Pro")
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, qb, scrub
|
from frappe import _, qb, query_builder, scrub
|
||||||
from frappe.query_builder import Criterion
|
from frappe.query_builder import Criterion
|
||||||
from frappe.query_builder.functions import Date, Substring, Sum
|
from frappe.query_builder.functions import Date, Substring, Sum
|
||||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||||
@ -576,6 +576,8 @@ class ReceivablePayableReport(object):
|
|||||||
def get_future_payments_from_payment_entry(self):
|
def get_future_payments_from_payment_entry(self):
|
||||||
pe = frappe.qb.DocType("Payment Entry")
|
pe = frappe.qb.DocType("Payment Entry")
|
||||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||||
|
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
frappe.qb.from_(pe)
|
frappe.qb.from_(pe)
|
||||||
.inner_join(pe_ref)
|
.inner_join(pe_ref)
|
||||||
@ -587,6 +589,11 @@ class ReceivablePayableReport(object):
|
|||||||
(pe.posting_date).as_("future_date"),
|
(pe.posting_date).as_("future_date"),
|
||||||
(pe_ref.allocated_amount).as_("future_amount"),
|
(pe_ref.allocated_amount).as_("future_amount"),
|
||||||
(pe.reference_no).as_("future_ref"),
|
(pe.reference_no).as_("future_ref"),
|
||||||
|
ifelse(
|
||||||
|
pe.payment_type == "Receive",
|
||||||
|
pe.source_exchange_rate * pe_ref.allocated_amount,
|
||||||
|
pe.target_exchange_rate * pe_ref.allocated_amount,
|
||||||
|
).as_("future_amount_in_base_currency"),
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(pe.docstatus < 2)
|
(pe.docstatus < 2)
|
||||||
@ -623,13 +630,24 @@ class ReceivablePayableReport(object):
|
|||||||
query = query.select(
|
query = query.select(
|
||||||
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
|
Sum(jea.debit_in_account_currency - jea.credit_in_account_currency).as_("future_amount")
|
||||||
)
|
)
|
||||||
|
query = query.select(Sum(jea.debit - jea.credit).as_("future_amount_in_base_currency"))
|
||||||
else:
|
else:
|
||||||
query = query.select(
|
query = query.select(
|
||||||
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
|
Sum(jea.credit_in_account_currency - jea.debit_in_account_currency).as_("future_amount")
|
||||||
)
|
)
|
||||||
|
query = query.select(Sum(jea.credit - jea.debit).as_("future_amount_in_base_currency"))
|
||||||
else:
|
else:
|
||||||
query = query.select(
|
query = query.select(
|
||||||
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_("future_amount")
|
Sum(jea.debit if self.account_type == "Payable" else jea.credit).as_(
|
||||||
|
"future_amount_in_base_currency"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
query = query.select(
|
||||||
|
Sum(
|
||||||
|
jea.debit_in_account_currency
|
||||||
|
if self.account_type == "Payable"
|
||||||
|
else jea.credit_in_account_currency
|
||||||
|
).as_("future_amount")
|
||||||
)
|
)
|
||||||
|
|
||||||
query = query.having(qb.Field("future_amount") > 0)
|
query = query.having(qb.Field("future_amount") > 0)
|
||||||
@ -645,14 +663,19 @@ class ReceivablePayableReport(object):
|
|||||||
row.remaining_balance = row.outstanding
|
row.remaining_balance = row.outstanding
|
||||||
row.future_amount = 0.0
|
row.future_amount = 0.0
|
||||||
for future in self.future_payments.get((row.voucher_no, row.party), []):
|
for future in self.future_payments.get((row.voucher_no, row.party), []):
|
||||||
if row.remaining_balance > 0 and future.future_amount:
|
if self.filters.in_party_currency:
|
||||||
if future.future_amount > row.outstanding:
|
future_amount_field = "future_amount"
|
||||||
|
else:
|
||||||
|
future_amount_field = "future_amount_in_base_currency"
|
||||||
|
|
||||||
|
if row.remaining_balance > 0 and future.get(future_amount_field):
|
||||||
|
if future.get(future_amount_field) > row.outstanding:
|
||||||
row.future_amount = row.outstanding
|
row.future_amount = row.outstanding
|
||||||
future.future_amount = future.future_amount - row.outstanding
|
future[future_amount_field] = future.get(future_amount_field) - row.outstanding
|
||||||
row.remaining_balance = 0
|
row.remaining_balance = 0
|
||||||
else:
|
else:
|
||||||
row.future_amount += future.future_amount
|
row.future_amount += future.get(future_amount_field)
|
||||||
future.future_amount = 0
|
future[future_amount_field] = 0
|
||||||
row.remaining_balance = row.outstanding - row.future_amount
|
row.remaining_balance = row.outstanding - row.future_amount
|
||||||
|
|
||||||
row.setdefault("future_ref", []).append(
|
row.setdefault("future_ref", []).append(
|
||||||
|
@ -772,3 +772,92 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
||||||
report_output = sorted(report_output, key=lambda x: x[0])
|
report_output = sorted(report_output, key=lambda x: x[0])
|
||||||
self.assertEqual(expected_data, report_output)
|
self.assertEqual(expected_data, report_output)
|
||||||
|
|
||||||
|
def test_future_payments_on_foreign_currency(self):
|
||||||
|
self.customer2 = (
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Customer",
|
||||||
|
"customer_name": "Jane Doe",
|
||||||
|
"type": "Individual",
|
||||||
|
"default_currency": "USD",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.insert()
|
||||||
|
.submit()
|
||||||
|
)
|
||||||
|
|
||||||
|
si = self.create_sales_invoice(do_not_submit=True)
|
||||||
|
si.posting_date = add_days(today(), -1)
|
||||||
|
si.customer = self.customer2
|
||||||
|
si.currency = "USD"
|
||||||
|
si.conversion_rate = 80
|
||||||
|
si.debit_to = self.debtors_usd
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
# full payment in USD
|
||||||
|
pe = get_payment_entry(si.doctype, si.name)
|
||||||
|
pe.posting_date = add_days(today(), 1)
|
||||||
|
pe.base_received_amount = 7500
|
||||||
|
pe.received_amount = 7500
|
||||||
|
pe.source_exchange_rate = 75
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
"show_future_payments": True,
|
||||||
|
"in_party_currency": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
|
||||||
|
expected_data = [8000.0, 8000.0, 500.0, 7500.0]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||||
|
)
|
||||||
|
|
||||||
|
filters.in_party_currency = True
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
expected_data = [100.0, 100.0, 0.0, 100.0]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||||
|
)
|
||||||
|
|
||||||
|
pe.cancel()
|
||||||
|
# partial payment in USD on a future date
|
||||||
|
pe = get_payment_entry(si.doctype, si.name)
|
||||||
|
pe.posting_date = add_days(today(), 1)
|
||||||
|
pe.base_received_amount = 6750
|
||||||
|
pe.received_amount = 6750
|
||||||
|
pe.source_exchange_rate = 75
|
||||||
|
pe.paid_amount = 90 # in USD
|
||||||
|
pe.references[0].allocated_amount = 90
|
||||||
|
pe.save().submit()
|
||||||
|
|
||||||
|
filters.in_party_currency = False
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
expected_data = [8000.0, 8000.0, 1250.0, 6750.0]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||||
|
)
|
||||||
|
|
||||||
|
filters.in_party_currency = True
|
||||||
|
report = execute(filters)[1]
|
||||||
|
self.assertEqual(len(report), 1)
|
||||||
|
expected_data = [100.0, 100.0, 10.0, 90.0]
|
||||||
|
row = report[0]
|
||||||
|
self.assertEqual(
|
||||||
|
expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
|
||||||
|
)
|
||||||
|
@ -8,6 +8,20 @@ frappe.query_reports["Balance Sheet"] = $.extend(
|
|||||||
|
|
||||||
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
erpnext.utils.add_dimensions("Balance Sheet", 10);
|
||||||
|
|
||||||
|
frappe.query_reports["Balance Sheet"]["filters"].push(
|
||||||
|
{
|
||||||
|
"fieldname": "selected_view",
|
||||||
|
"label": __("Select View"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": [
|
||||||
|
{ "value": "Report", "label": __("Report View") },
|
||||||
|
{ "value": "Growth", "label": __("Growth View") }
|
||||||
|
],
|
||||||
|
"default": "Report",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
frappe.query_reports["Balance Sheet"]["filters"].push({
|
frappe.query_reports["Balance Sheet"]["filters"].push({
|
||||||
fieldname: "accumulated_values",
|
fieldname: "accumulated_values",
|
||||||
label: __("Accumulated Values"),
|
label: __("Accumulated Values"),
|
||||||
|
@ -8,6 +8,21 @@ frappe.query_reports["Profit and Loss Statement"] = $.extend(
|
|||||||
|
|
||||||
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
erpnext.utils.add_dimensions("Profit and Loss Statement", 10);
|
||||||
|
|
||||||
|
frappe.query_reports["Profit and Loss Statement"]["filters"].push(
|
||||||
|
{
|
||||||
|
"fieldname": "selected_view",
|
||||||
|
"label": __("Select View"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": [
|
||||||
|
{ "value": "Report", "label": __("Report View") },
|
||||||
|
{ "value": "Growth", "label": __("Growth View") },
|
||||||
|
{ "value": "Margin", "label": __("Margin View") },
|
||||||
|
],
|
||||||
|
"default": "Report",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
frappe.query_reports["Profit and Loss Statement"]["filters"].push({
|
||||||
fieldname: "accumulated_values",
|
fieldname: "accumulated_values",
|
||||||
label: __("Accumulated Values"),
|
label: __("Accumulated Values"),
|
||||||
|
@ -240,7 +240,6 @@ def get_balance_on(
|
|||||||
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
|
cond.append("""gle.cost_center = %s """ % (frappe.db.escape(cost_center, percent=False),))
|
||||||
|
|
||||||
if account:
|
if account:
|
||||||
|
|
||||||
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
|
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
|
||||||
acc.check_permission("read")
|
acc.check_permission("read")
|
||||||
|
|
||||||
@ -286,18 +285,22 @@ def get_balance_on(
|
|||||||
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
|
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
|
||||||
|
|
||||||
if account or (party_type and party) or account_type:
|
if account or (party_type and party) or account_type:
|
||||||
|
precision = get_currency_precision()
|
||||||
if in_account_currency:
|
if in_account_currency:
|
||||||
select_field = "sum(debit_in_account_currency) - sum(credit_in_account_currency)"
|
select_field = (
|
||||||
|
"sum(round(debit_in_account_currency, %s)) - sum(round(credit_in_account_currency, %s))"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
select_field = "sum(debit) - sum(credit)"
|
select_field = "sum(round(debit, %s)) - sum(round(credit, %s))"
|
||||||
|
|
||||||
bal = frappe.db.sql(
|
bal = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
SELECT {0}
|
SELECT {0}
|
||||||
FROM `tabGL Entry` gle
|
FROM `tabGL Entry` gle
|
||||||
WHERE {1}""".format(
|
WHERE {1}""".format(
|
||||||
select_field, " and ".join(cond)
|
select_field, " and ".join(cond)
|
||||||
)
|
),
|
||||||
|
(precision, precision),
|
||||||
)[0][0]
|
)[0][0]
|
||||||
# if bal is None, return 0
|
# if bal is None, return 0
|
||||||
return flt(bal)
|
return flt(bal)
|
||||||
@ -453,7 +456,19 @@ def add_cc(args=None):
|
|||||||
return cc.name
|
return cc.name
|
||||||
|
|
||||||
|
|
||||||
def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
|
def _build_dimensions_dict_for_exc_gain_loss(
|
||||||
|
entry: dict | object = None, active_dimensions: list = None
|
||||||
|
):
|
||||||
|
dimensions_dict = frappe._dict()
|
||||||
|
if entry and active_dimensions:
|
||||||
|
for dim in active_dimensions:
|
||||||
|
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
|
||||||
|
return dimensions_dict
|
||||||
|
|
||||||
|
|
||||||
|
def reconcile_against_document(
|
||||||
|
args, skip_ref_details_update_for_pe=False, active_dimensions=None
|
||||||
|
): # nosemgrep
|
||||||
"""
|
"""
|
||||||
Cancel PE or JV, Update against document, split if required and resubmit
|
Cancel PE or JV, Update against document, split if required and resubmit
|
||||||
"""
|
"""
|
||||||
@ -482,6 +497,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
|
|||||||
check_if_advance_entry_modified(entry)
|
check_if_advance_entry_modified(entry)
|
||||||
validate_allocated_amount(entry)
|
validate_allocated_amount(entry)
|
||||||
|
|
||||||
|
dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions)
|
||||||
|
|
||||||
# update ref in advance entry
|
# update ref in advance entry
|
||||||
if voucher_type == "Journal Entry":
|
if voucher_type == "Journal Entry":
|
||||||
referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
|
referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
|
||||||
@ -489,10 +506,14 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
|
|||||||
# amount and account in args
|
# amount and account in args
|
||||||
# referenced_row is used to deduplicate gain/loss journal
|
# referenced_row is used to deduplicate gain/loss journal
|
||||||
entry.update({"referenced_row": referenced_row})
|
entry.update({"referenced_row": referenced_row})
|
||||||
doc.make_exchange_gain_loss_journal([entry])
|
doc.make_exchange_gain_loss_journal([entry], dimensions_dict)
|
||||||
else:
|
else:
|
||||||
referenced_row = update_reference_in_payment_entry(
|
referenced_row = update_reference_in_payment_entry(
|
||||||
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
|
entry,
|
||||||
|
doc,
|
||||||
|
do_not_save=True,
|
||||||
|
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
|
||||||
|
dimensions_dict=dimensions_dict,
|
||||||
)
|
)
|
||||||
|
|
||||||
doc.save(ignore_permissions=True)
|
doc.save(ignore_permissions=True)
|
||||||
@ -649,7 +670,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
|||||||
|
|
||||||
|
|
||||||
def update_reference_in_payment_entry(
|
def update_reference_in_payment_entry(
|
||||||
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
|
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False, dimensions_dict=None
|
||||||
):
|
):
|
||||||
reference_details = {
|
reference_details = {
|
||||||
"reference_doctype": d.against_voucher_type,
|
"reference_doctype": d.against_voucher_type,
|
||||||
@ -662,6 +683,7 @@ def update_reference_in_payment_entry(
|
|||||||
else payment_entry.get_exchange_rate(),
|
else payment_entry.get_exchange_rate(),
|
||||||
"exchange_gain_loss": d.difference_amount,
|
"exchange_gain_loss": d.difference_amount,
|
||||||
"account": d.account,
|
"account": d.account,
|
||||||
|
"dimensions": d.dimensions,
|
||||||
}
|
}
|
||||||
|
|
||||||
if d.voucher_detail_no:
|
if d.voucher_detail_no:
|
||||||
@ -694,7 +716,9 @@ def update_reference_in_payment_entry(
|
|||||||
if not skip_ref_details_update_for_pe:
|
if not skip_ref_details_update_for_pe:
|
||||||
payment_entry.set_missing_ref_details()
|
payment_entry.set_missing_ref_details()
|
||||||
payment_entry.set_amounts()
|
payment_entry.set_amounts()
|
||||||
payment_entry.make_exchange_gain_loss_journal()
|
payment_entry.make_exchange_gain_loss_journal(
|
||||||
|
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
|
||||||
|
)
|
||||||
|
|
||||||
if not do_not_save:
|
if not do_not_save:
|
||||||
payment_entry.save(ignore_permissions=True)
|
payment_entry.save(ignore_permissions=True)
|
||||||
@ -2031,6 +2055,7 @@ def create_gain_loss_journal(
|
|||||||
ref2_dn,
|
ref2_dn,
|
||||||
ref2_detail_no,
|
ref2_detail_no,
|
||||||
cost_center,
|
cost_center,
|
||||||
|
dimensions,
|
||||||
) -> str:
|
) -> str:
|
||||||
journal_entry = frappe.new_doc("Journal Entry")
|
journal_entry = frappe.new_doc("Journal Entry")
|
||||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||||
@ -2064,7 +2089,8 @@ def create_gain_loss_journal(
|
|||||||
dr_or_cr + "_in_account_currency": 0,
|
dr_or_cr + "_in_account_currency": 0,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if dimensions:
|
||||||
|
journal_account.update(dimensions)
|
||||||
journal_entry.append("accounts", journal_account)
|
journal_entry.append("accounts", journal_account)
|
||||||
|
|
||||||
journal_account = frappe._dict(
|
journal_account = frappe._dict(
|
||||||
@ -2080,7 +2106,8 @@ def create_gain_loss_journal(
|
|||||||
reverse_dr_or_cr: abs(exc_gain_loss),
|
reverse_dr_or_cr: abs(exc_gain_loss),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if dimensions:
|
||||||
|
journal_account.update(dimensions)
|
||||||
journal_entry.append("accounts", journal_account)
|
journal_entry.append("accounts", journal_account)
|
||||||
|
|
||||||
journal_entry.save()
|
journal_entry.save()
|
||||||
|
@ -571,16 +571,16 @@ frappe.ui.form.on('Asset', {
|
|||||||
indicator: 'red'
|
indicator: 'red'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
|
frappe.db.get_value('Item', item.item_code, 'is_grouped_asset', (r) => {
|
||||||
var asset_quantity = is_grouped_asset ? item.qty : 1;
|
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
|
||||||
var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
|
var purchase_amount = flt(item.valuation_rate * asset_quantity, precision('gross_purchase_amount'));
|
||||||
|
|
||||||
frm.set_value('gross_purchase_amount', purchase_amount);
|
|
||||||
frm.set_value('purchase_receipt_amount', purchase_amount);
|
|
||||||
frm.set_value('asset_quantity', asset_quantity);
|
|
||||||
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
|
|
||||||
if(item.asset_location) { frm.set_value('location', item.asset_location); }
|
|
||||||
|
|
||||||
|
frm.set_value('gross_purchase_amount', purchase_amount);
|
||||||
|
frm.set_value('purchase_receipt_amount', purchase_amount);
|
||||||
|
frm.set_value('asset_quantity', asset_quantity);
|
||||||
|
frm.set_value('cost_center', item.cost_center || purchase_doc.cost_center);
|
||||||
|
if(item.asset_location) { frm.set_value('location', item.asset_location); }
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
set_depreciation_rate: function(frm, row) {
|
set_depreciation_rate: function(frm, row) {
|
||||||
|
@ -7,6 +7,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold, qb, throw
|
from frappe import _, bold, qb, throw
|
||||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||||
|
from frappe.query_builder import Criterion
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.query_builder.functions import Abs, Sum
|
from frappe.query_builder.functions import Abs, Sum
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
@ -28,6 +29,7 @@ from frappe.utils import (
|
|||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
|
get_dimensions,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||||
apply_pricing_rule_for_free_items,
|
apply_pricing_rule_for_free_items,
|
||||||
@ -200,6 +202,7 @@ class AccountsController(TransactionBase):
|
|||||||
self.validate_party()
|
self.validate_party()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
self.validate_party_account_currency()
|
self.validate_party_account_currency()
|
||||||
|
self.validate_return_against_account()
|
||||||
|
|
||||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||||
if invalid_advances := [
|
if invalid_advances := [
|
||||||
@ -348,6 +351,20 @@ class AccountsController(TransactionBase):
|
|||||||
for bundle in bundles:
|
for bundle in bundles:
|
||||||
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
|
frappe.delete_doc("Serial and Batch Bundle", bundle.name)
|
||||||
|
|
||||||
|
def validate_return_against_account(self):
|
||||||
|
if (
|
||||||
|
self.doctype in ["Sales Invoice", "Purchase Invoice"] and self.is_return and self.return_against
|
||||||
|
):
|
||||||
|
cr_dr_account_field = "debit_to" if self.doctype == "Sales Invoice" else "credit_to"
|
||||||
|
cr_dr_account_label = "Debit To" if self.doctype == "Sales Invoice" else "Credit To"
|
||||||
|
cr_dr_account = self.get(cr_dr_account_field)
|
||||||
|
if frappe.get_value(self.doctype, self.return_against, cr_dr_account_field) != cr_dr_account:
|
||||||
|
frappe.throw(
|
||||||
|
_("'{0}' account: '{1}' should match the Return Against Invoice").format(
|
||||||
|
frappe.bold(cr_dr_account_label), frappe.bold(cr_dr_account)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def validate_deferred_income_expense_account(self):
|
def validate_deferred_income_expense_account(self):
|
||||||
field_map = {
|
field_map = {
|
||||||
"Sales Invoice": "deferred_revenue_account",
|
"Sales Invoice": "deferred_revenue_account",
|
||||||
@ -1246,7 +1263,9 @@ class AccountsController(TransactionBase):
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def make_exchange_gain_loss_journal(self, args: dict = None) -> None:
|
def make_exchange_gain_loss_journal(
|
||||||
|
self, args: dict = None, dimensions_dict: dict = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Make Exchange Gain/Loss journal for Invoices and Payments
|
Make Exchange Gain/Loss journal for Invoices and Payments
|
||||||
"""
|
"""
|
||||||
@ -1299,6 +1318,7 @@ class AccountsController(TransactionBase):
|
|||||||
self.name,
|
self.name,
|
||||||
arg.get("referenced_row"),
|
arg.get("referenced_row"),
|
||||||
arg.get("cost_center"),
|
arg.get("cost_center"),
|
||||||
|
dimensions_dict,
|
||||||
)
|
)
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||||
@ -1379,6 +1399,7 @@ class AccountsController(TransactionBase):
|
|||||||
self.name,
|
self.name,
|
||||||
d.idx,
|
d.idx,
|
||||||
self.cost_center,
|
self.cost_center,
|
||||||
|
dimensions_dict,
|
||||||
)
|
)
|
||||||
frappe.msgprint(
|
frappe.msgprint(
|
||||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||||
@ -1443,7 +1464,13 @@ class AccountsController(TransactionBase):
|
|||||||
if lst:
|
if lst:
|
||||||
from erpnext.accounts.utils import reconcile_against_document
|
from erpnext.accounts.utils import reconcile_against_document
|
||||||
|
|
||||||
reconcile_against_document(lst)
|
# pass dimension values to utility method
|
||||||
|
active_dimensions = get_dimensions()[0]
|
||||||
|
for x in lst:
|
||||||
|
for dim in active_dimensions:
|
||||||
|
if self.get(dim.fieldname):
|
||||||
|
x.update({dim.fieldname: self.get(dim.fieldname)})
|
||||||
|
reconcile_against_document(lst, active_dimensions=active_dimensions)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
|
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
|
||||||
@ -2712,47 +2739,37 @@ def get_common_query(
|
|||||||
q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))
|
q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))
|
||||||
|
|
||||||
if condition:
|
if condition:
|
||||||
if condition.get("name", None):
|
# conditions should be built as an array and passed as Criterion
|
||||||
q = q.where(payment_entry.name.like(f"%{condition.get('name')}%"))
|
common_filter_conditions = []
|
||||||
|
|
||||||
|
common_filter_conditions.append(payment_entry.company == condition["company"])
|
||||||
|
if condition.get("name", None):
|
||||||
|
common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%"))
|
||||||
|
|
||||||
|
if condition.get("from_payment_date"):
|
||||||
|
common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"]))
|
||||||
|
|
||||||
|
if condition.get("to_payment_date"):
|
||||||
|
common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"]))
|
||||||
|
|
||||||
q = q.where(payment_entry.company == condition["company"])
|
|
||||||
q = (
|
|
||||||
q.where(payment_entry.posting_date >= condition["from_payment_date"])
|
|
||||||
if condition.get("from_payment_date")
|
|
||||||
else q
|
|
||||||
)
|
|
||||||
q = (
|
|
||||||
q.where(payment_entry.posting_date <= condition["to_payment_date"])
|
|
||||||
if condition.get("to_payment_date")
|
|
||||||
else q
|
|
||||||
)
|
|
||||||
if condition.get("get_payments") == True:
|
if condition.get("get_payments") == True:
|
||||||
q = (
|
if condition.get("cost_center"):
|
||||||
q.where(payment_entry.cost_center == condition["cost_center"])
|
common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"])
|
||||||
if condition.get("cost_center")
|
|
||||||
else q
|
if condition.get("accounting_dimensions"):
|
||||||
)
|
for field, val in condition.get("accounting_dimensions").items():
|
||||||
q = (
|
common_filter_conditions.append(payment_entry[field] == val)
|
||||||
q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"])
|
|
||||||
if condition.get("minimum_payment_amount")
|
if condition.get("minimum_payment_amount"):
|
||||||
else q
|
common_filter_conditions.append(
|
||||||
)
|
payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"])
|
||||||
q = (
|
)
|
||||||
q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"])
|
|
||||||
if condition.get("maximum_payment_amount")
|
if condition.get("maximum_payment_amount"):
|
||||||
else q
|
common_filter_conditions.append(
|
||||||
)
|
payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"])
|
||||||
else:
|
)
|
||||||
q = (
|
q = q.where(Criterion.all(common_filter_conditions))
|
||||||
q.where(payment_entry.total_debit >= condition["minimum_payment_amount"])
|
|
||||||
if condition.get("minimum_payment_amount")
|
|
||||||
else q
|
|
||||||
)
|
|
||||||
q = (
|
|
||||||
q.where(payment_entry.total_debit <= condition["maximum_payment_amount"])
|
|
||||||
if condition.get("maximum_payment_amount")
|
|
||||||
else q
|
|
||||||
)
|
|
||||||
|
|
||||||
q = q.orderby(payment_entry.posting_date)
|
q = q.orderby(payment_entry.posting_date)
|
||||||
q = q.limit(limit) if limit else q
|
q = q.limit(limit) if limit else q
|
||||||
|
@ -91,7 +91,8 @@ status_map = {
|
|||||||
],
|
],
|
||||||
"Purchase Receipt": [
|
"Purchase Receipt": [
|
||||||
["Draft", None],
|
["Draft", None],
|
||||||
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
|
["To Bill", "eval:self.per_billed == 0 and self.docstatus == 1"],
|
||||||
|
["Partly Billed", "eval:self.per_billed > 0 and self.per_billed < 100 and self.docstatus == 1"],
|
||||||
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
|
||||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||||
["Cancelled", "eval:self.docstatus==2"],
|
["Cancelled", "eval:self.docstatus==2"],
|
||||||
|
@ -6,7 +6,7 @@ from collections import defaultdict
|
|||||||
from typing import List, Tuple
|
from typing import List, Tuple
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _, bold
|
||||||
from frappe.utils import cint, flt, get_link_to_form, getdate
|
from frappe.utils import cint, flt, get_link_to_form, getdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@ -697,6 +697,9 @@ class StockController(AccountsController):
|
|||||||
self.validate_in_transit_warehouses()
|
self.validate_in_transit_warehouses()
|
||||||
self.validate_multi_currency()
|
self.validate_multi_currency()
|
||||||
self.validate_packed_items()
|
self.validate_packed_items()
|
||||||
|
|
||||||
|
if self.get("is_internal_supplier"):
|
||||||
|
self.validate_internal_transfer_qty()
|
||||||
else:
|
else:
|
||||||
self.validate_internal_transfer_warehouse()
|
self.validate_internal_transfer_warehouse()
|
||||||
|
|
||||||
@ -735,6 +738,116 @@ class StockController(AccountsController):
|
|||||||
if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"):
|
if self.doctype in ("Sales Invoice", "Delivery Note Item") and self.get("packed_items"):
|
||||||
frappe.throw(_("Packed Items cannot be transferred internally"))
|
frappe.throw(_("Packed Items cannot be transferred internally"))
|
||||||
|
|
||||||
|
def validate_internal_transfer_qty(self):
|
||||||
|
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
item_wise_transfer_qty = self.get_item_wise_inter_transfer_qty()
|
||||||
|
if not item_wise_transfer_qty:
|
||||||
|
return
|
||||||
|
|
||||||
|
item_wise_received_qty = self.get_item_wise_inter_received_qty()
|
||||||
|
precision = frappe.get_precision(self.doctype + " Item", "qty")
|
||||||
|
|
||||||
|
over_receipt_allowance = frappe.db.get_single_value(
|
||||||
|
"Stock Settings", "over_delivery_receipt_allowance"
|
||||||
|
)
|
||||||
|
|
||||||
|
parent_doctype = {
|
||||||
|
"Purchase Receipt": "Delivery Note",
|
||||||
|
"Purchase Invoice": "Sales Invoice",
|
||||||
|
}.get(self.doctype)
|
||||||
|
|
||||||
|
for key, transferred_qty in item_wise_transfer_qty.items():
|
||||||
|
recevied_qty = flt(item_wise_received_qty.get(key), precision)
|
||||||
|
if over_receipt_allowance:
|
||||||
|
transferred_qty = transferred_qty + flt(
|
||||||
|
transferred_qty * over_receipt_allowance / 100, precision
|
||||||
|
)
|
||||||
|
|
||||||
|
if recevied_qty > flt(transferred_qty, precision):
|
||||||
|
frappe.throw(
|
||||||
|
_("For Item {0} cannot be received more than {1} qty against the {2} {3}").format(
|
||||||
|
bold(key[1]),
|
||||||
|
bold(flt(transferred_qty, precision)),
|
||||||
|
bold(parent_doctype),
|
||||||
|
get_link_to_form(parent_doctype, self.get("inter_company_reference")),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_item_wise_inter_transfer_qty(self):
|
||||||
|
reference_field = "inter_company_reference"
|
||||||
|
if self.doctype == "Purchase Invoice":
|
||||||
|
reference_field = "inter_company_invoice_reference"
|
||||||
|
|
||||||
|
parent_doctype = {
|
||||||
|
"Purchase Receipt": "Delivery Note",
|
||||||
|
"Purchase Invoice": "Sales Invoice",
|
||||||
|
}.get(self.doctype)
|
||||||
|
|
||||||
|
child_doctype = parent_doctype + " Item"
|
||||||
|
|
||||||
|
parent_tab = frappe.qb.DocType(parent_doctype)
|
||||||
|
child_tab = frappe.qb.DocType(child_doctype)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(parent_doctype)
|
||||||
|
.inner_join(child_tab)
|
||||||
|
.on(child_tab.parent == parent_tab.name)
|
||||||
|
.select(
|
||||||
|
child_tab.name,
|
||||||
|
child_tab.item_code,
|
||||||
|
child_tab.qty,
|
||||||
|
)
|
||||||
|
.where((parent_tab.name == self.get(reference_field)) & (parent_tab.docstatus == 1))
|
||||||
|
)
|
||||||
|
|
||||||
|
data = query.run(as_dict=True)
|
||||||
|
item_wise_transfer_qty = defaultdict(float)
|
||||||
|
for row in data:
|
||||||
|
item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty)
|
||||||
|
|
||||||
|
return item_wise_transfer_qty
|
||||||
|
|
||||||
|
def get_item_wise_inter_received_qty(self):
|
||||||
|
child_doctype = self.doctype + " Item"
|
||||||
|
|
||||||
|
parent_tab = frappe.qb.DocType(self.doctype)
|
||||||
|
child_tab = frappe.qb.DocType(child_doctype)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(self.doctype)
|
||||||
|
.inner_join(child_tab)
|
||||||
|
.on(child_tab.parent == parent_tab.name)
|
||||||
|
.select(
|
||||||
|
child_tab.item_code,
|
||||||
|
child_tab.qty,
|
||||||
|
)
|
||||||
|
.where(parent_tab.docstatus < 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.doctype == "Purchase Invoice":
|
||||||
|
query = query.select(
|
||||||
|
child_tab.sales_invoice_item.as_("name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.where(
|
||||||
|
parent_tab.inter_company_invoice_reference == self.inter_company_invoice_reference
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
query = query.select(
|
||||||
|
child_tab.delivery_note_item.as_("name"),
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.where(parent_tab.inter_company_reference == self.inter_company_reference)
|
||||||
|
|
||||||
|
data = query.run(as_dict=True)
|
||||||
|
item_wise_transfer_qty = defaultdict(float)
|
||||||
|
for row in data:
|
||||||
|
item_wise_transfer_qty[(row.name, row.item_code)] += flt(row.qty)
|
||||||
|
|
||||||
|
return item_wise_transfer_qty
|
||||||
|
|
||||||
def validate_putaway_capacity(self):
|
def validate_putaway_capacity(self):
|
||||||
# if over receipt is attempted while 'apply putaway rule' is disabled
|
# if over receipt is attempted while 'apply putaway rule' is disabled
|
||||||
# and if rule was applied on the transaction, validate it.
|
# and if rule was applied on the transaction, validate it.
|
||||||
|
@ -260,18 +260,22 @@ class SubcontractingController(StockController):
|
|||||||
return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
|
return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
|
||||||
|
|
||||||
def __get_consumed_items(self, doctype, receipt_items):
|
def __get_consumed_items(self, doctype, receipt_items):
|
||||||
|
fields = [
|
||||||
|
"serial_no",
|
||||||
|
"rm_item_code",
|
||||||
|
"reference_name",
|
||||||
|
"batch_no",
|
||||||
|
"consumed_qty",
|
||||||
|
"main_item_code",
|
||||||
|
"parent as voucher_no",
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.subcontract_data.receipt_supplied_items_field != "Purchase Receipt Item Supplied":
|
||||||
|
fields.append("serial_and_batch_bundle")
|
||||||
|
|
||||||
return frappe.get_all(
|
return frappe.get_all(
|
||||||
self.subcontract_data.receipt_supplied_items_field,
|
self.subcontract_data.receipt_supplied_items_field,
|
||||||
fields=[
|
fields=fields,
|
||||||
"serial_no",
|
|
||||||
"rm_item_code",
|
|
||||||
"reference_name",
|
|
||||||
"serial_and_batch_bundle",
|
|
||||||
"batch_no",
|
|
||||||
"consumed_qty",
|
|
||||||
"main_item_code",
|
|
||||||
"parent as voucher_no",
|
|
||||||
],
|
|
||||||
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
|
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
20 series - Sales Invoice against Journals
|
20 series - Sales Invoice against Journals
|
||||||
30 series - Sales Invoice against Credit Notes
|
30 series - Sales Invoice against Credit Notes
|
||||||
40 series - Company default Cost center is unset
|
40 series - Company default Cost center is unset
|
||||||
|
50 series - Dimension inheritence
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -1188,3 +1189,214 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
frappe.db.set_value("Company", self.company, "cost_center", cc)
|
frappe.db.set_value("Company", self.company, "cost_center", cc)
|
||||||
|
|
||||||
|
def setup_dimensions(self):
|
||||||
|
# create dimension
|
||||||
|
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||||
|
create_dimension,
|
||||||
|
)
|
||||||
|
|
||||||
|
create_dimension()
|
||||||
|
# make it non-mandatory
|
||||||
|
loc = frappe.get_doc("Accounting Dimension", "Location")
|
||||||
|
for x in loc.dimension_defaults:
|
||||||
|
x.mandatory_for_bs = False
|
||||||
|
x.mandatory_for_pl = False
|
||||||
|
loc.save()
|
||||||
|
|
||||||
|
def test_50_dimensions_filter(self):
|
||||||
|
"""
|
||||||
|
Test workings of dimension filters
|
||||||
|
"""
|
||||||
|
self.setup_dimensions()
|
||||||
|
rate_in_account_currency = 1
|
||||||
|
|
||||||
|
# Invoices
|
||||||
|
si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||||
|
si1.department = "Management"
|
||||||
|
si1.save().submit()
|
||||||
|
|
||||||
|
si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||||
|
si2.department = "Operations"
|
||||||
|
si2.save().submit()
|
||||||
|
|
||||||
|
# Payments
|
||||||
|
cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||||
|
cr_note1.department = "Management"
|
||||||
|
cr_note1.is_return = 1
|
||||||
|
cr_note1.save().submit()
|
||||||
|
|
||||||
|
cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||||
|
cr_note2.department = "Legal"
|
||||||
|
cr_note2.is_return = 1
|
||||||
|
cr_note2.save().submit()
|
||||||
|
|
||||||
|
pe1 = get_payment_entry(si1.doctype, si1.name)
|
||||||
|
pe1.references = []
|
||||||
|
pe1.department = "Research & Development"
|
||||||
|
pe1.save().submit()
|
||||||
|
|
||||||
|
pe2 = get_payment_entry(si1.doctype, si1.name)
|
||||||
|
pe2.references = []
|
||||||
|
pe2.department = "Management"
|
||||||
|
pe2.save().submit()
|
||||||
|
|
||||||
|
je1 = self.create_journal_entry(
|
||||||
|
acc1=self.debit_usd,
|
||||||
|
acc1_exc_rate=75,
|
||||||
|
acc2=self.cash,
|
||||||
|
acc1_amount=-1,
|
||||||
|
acc2_amount=-75,
|
||||||
|
acc2_exc_rate=1,
|
||||||
|
)
|
||||||
|
je1.accounts[0].party_type = "Customer"
|
||||||
|
je1.accounts[0].party = self.customer
|
||||||
|
je1.accounts[0].department = "Management"
|
||||||
|
je1.save().submit()
|
||||||
|
|
||||||
|
# assert dimension filter's result
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 2)
|
||||||
|
self.assertEqual(len(pr.payments), 5)
|
||||||
|
|
||||||
|
pr.department = "Legal"
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 0)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
|
||||||
|
pr.department = "Management"
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 3)
|
||||||
|
|
||||||
|
pr.department = "Research & Development"
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 0)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
|
||||||
|
def test_51_cr_note_should_inherit_dimension(self):
|
||||||
|
self.setup_dimensions()
|
||||||
|
rate_in_account_currency = 1
|
||||||
|
|
||||||
|
# Invoice
|
||||||
|
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||||
|
si.department = "Management"
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
# Payment
|
||||||
|
cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||||
|
cr_note.department = "Management"
|
||||||
|
cr_note.is_return = 1
|
||||||
|
cr_note.save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.department = "Management"
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [x.as_dict() for x in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
self.assertEqual(len(pr.invoices), 0)
|
||||||
|
self.assertEqual(len(pr.payments), 0)
|
||||||
|
|
||||||
|
# There should be 2 journals, JE(Cr Note) and JE(Exchange Gain/Loss)
|
||||||
|
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||||
|
exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name)
|
||||||
|
self.assertNotEqual(exc_je_for_si, [])
|
||||||
|
self.assertEqual(len(exc_je_for_si), 2)
|
||||||
|
self.assertEqual(len(exc_je_for_cr_note), 2)
|
||||||
|
self.assertEqual(exc_je_for_si, exc_je_for_cr_note)
|
||||||
|
|
||||||
|
for x in exc_je_for_si + exc_je_for_cr_note:
|
||||||
|
with self.subTest(x=x):
|
||||||
|
self.assertEqual(
|
||||||
|
[cr_note.department, cr_note.department],
|
||||||
|
frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_52_dimension_inhertiance_exc_gain_loss(self):
|
||||||
|
# Sales Invoice in Foreign Currency
|
||||||
|
self.setup_dimensions()
|
||||||
|
rate = 80
|
||||||
|
rate_in_account_currency = 1
|
||||||
|
dpt = "Research & Development"
|
||||||
|
|
||||||
|
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True)
|
||||||
|
si.department = dpt
|
||||||
|
si.save().submit()
|
||||||
|
|
||||||
|
pe = self.create_payment_entry(amount=1, source_exc_rate=82).save()
|
||||||
|
pe.department = dpt
|
||||||
|
pe = pe.save().submit()
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation()
|
||||||
|
pr.department = dpt
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [x.as_dict() for x in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
self.assertEqual(len(pr.invoices), 0)
|
||||||
|
self.assertEqual(len(pr.payments), 0)
|
||||||
|
|
||||||
|
# Exc Gain/Loss journals should inherit dimension from parent
|
||||||
|
journals = self.get_journals_for(si.doctype, si.name)
|
||||||
|
self.assertEqual(
|
||||||
|
[dpt, dpt],
|
||||||
|
frappe.db.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={"parent": ("in", [x.parent for x in journals])},
|
||||||
|
pluck="department",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_53_dimension_inheritance_on_advance(self):
|
||||||
|
self.setup_dimensions()
|
||||||
|
dpt = "Research & Development"
|
||||||
|
|
||||||
|
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
|
||||||
|
adv.department = dpt
|
||||||
|
adv.save().submit()
|
||||||
|
adv.reload()
|
||||||
|
|
||||||
|
# Sales Invoices in different exchange rates
|
||||||
|
si = self.create_sales_invoice(qty=1, conversion_rate=82, rate=1, do_not_submit=True)
|
||||||
|
si.department = dpt
|
||||||
|
advances = si.get_advance_entries()
|
||||||
|
self.assertEqual(len(advances), 1)
|
||||||
|
self.assertEqual(advances[0].reference_name, adv.name)
|
||||||
|
si.append(
|
||||||
|
"advances",
|
||||||
|
{
|
||||||
|
"doctype": "Sales Invoice Advance",
|
||||||
|
"reference_type": advances[0].reference_type,
|
||||||
|
"reference_name": advances[0].reference_name,
|
||||||
|
"reference_row": advances[0].reference_row,
|
||||||
|
"advance_amount": 1,
|
||||||
|
"allocated_amount": 1,
|
||||||
|
"ref_exchange_rate": advances[0].exchange_rate,
|
||||||
|
"remarks": advances[0].remarks,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si = si.save().submit()
|
||||||
|
|
||||||
|
# Outstanding in both currencies should be '0'
|
||||||
|
adv.reload()
|
||||||
|
self.assertEqual(si.outstanding_amount, 0)
|
||||||
|
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||||
|
|
||||||
|
# Exc Gain/Loss journals should inherit dimension from parent
|
||||||
|
journals = self.get_journals_for(si.doctype, si.name)
|
||||||
|
self.assertEqual(
|
||||||
|
[dpt, dpt],
|
||||||
|
frappe.db.get_all(
|
||||||
|
"Journal Entry Account",
|
||||||
|
filters={"parent": ("in", [x.parent for x in journals])},
|
||||||
|
pluck="department",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -543,6 +543,8 @@ accounting_dimension_doctypes = [
|
|||||||
"Account Closing Balance",
|
"Account Closing Balance",
|
||||||
"Supplier Quotation",
|
"Supplier Quotation",
|
||||||
"Supplier Quotation Item",
|
"Supplier Quotation Item",
|
||||||
|
"Payment Reconciliation",
|
||||||
|
"Payment Reconciliation Allocation",
|
||||||
]
|
]
|
||||||
|
|
||||||
get_matching_queries = (
|
get_matching_queries = (
|
||||||
|
@ -176,8 +176,10 @@ class BOM(WebsiteGenerator):
|
|||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
# ignore amended documents while calculating current index
|
# ignore amended documents while calculating current index
|
||||||
|
|
||||||
|
search_key = f"{self.doctype}-{self.item}%"
|
||||||
existing_boms = frappe.get_all(
|
existing_boms = frappe.get_all(
|
||||||
"BOM", filters={"item": self.item, "amended_from": ["is", "not set"]}, pluck="name"
|
"BOM", filters={"name": ("like", search_key), "amended_from": ["is", "not set"]}, pluck="name"
|
||||||
)
|
)
|
||||||
|
|
||||||
if existing_boms:
|
if existing_boms:
|
||||||
|
@ -101,6 +101,7 @@ frappe.ui.form.on("BOM Creator", {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
dialog.fields_dict.item_code.get_query = "erpnext.controllers.queries.item_query";
|
||||||
dialog.show();
|
dialog.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -113,6 +114,16 @@ frappe.ui.form.on("BOM Creator", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
frm.set_query("item_code", "items", function() {
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.item_query",
|
||||||
|
}
|
||||||
|
});
|
||||||
|
frm.set_query("fg_item", "items", function() {
|
||||||
|
return {
|
||||||
|
query: "erpnext.controllers.queries.item_query",
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh(frm) {
|
refresh(frm) {
|
||||||
@ -211,4 +222,4 @@ erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionC
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm}));
|
extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm}));
|
||||||
|
@ -790,24 +790,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
if (me.frm.doc.price_list_currency == company_currency) {
|
if (me.frm.doc.price_list_currency == company_currency) {
|
||||||
me.frm.set_value('plc_conversion_rate', 1.0);
|
me.frm.set_value('plc_conversion_rate', 1.0);
|
||||||
}
|
}
|
||||||
if (company_doc && company_doc.default_letter_head) {
|
if (company_doc){
|
||||||
if(me.frm.fields_dict.letter_head) {
|
if (company_doc.default_letter_head) {
|
||||||
me.frm.set_value("letter_head", company_doc.default_letter_head);
|
if(me.frm.fields_dict.letter_head) {
|
||||||
|
me.frm.set_value("letter_head", company_doc.default_letter_head);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
|
||||||
|
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
|
||||||
|
selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
|
||||||
|
me.frm.set_value("tc_name", company_doc.default_selling_terms);
|
||||||
|
}
|
||||||
|
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
|
||||||
|
"Material Request", "Purchase Receipt"];
|
||||||
|
// Purchase Invoice is excluded as per issue #3345
|
||||||
|
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
|
||||||
|
buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
|
||||||
|
me.frm.set_value("tc_name", company_doc.default_buying_terms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let selling_doctypes_for_tc = ["Sales Invoice", "Quotation", "Sales Order", "Delivery Note"];
|
|
||||||
if (company_doc.default_selling_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
|
|
||||||
selling_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
|
|
||||||
me.frm.set_value("tc_name", company_doc.default_selling_terms);
|
|
||||||
}
|
|
||||||
let buying_doctypes_for_tc = ["Request for Quotation", "Supplier Quotation", "Purchase Order",
|
|
||||||
"Material Request", "Purchase Receipt"];
|
|
||||||
// Purchase Invoice is excluded as per issue #3345
|
|
||||||
if (company_doc.default_buying_terms && frappe.meta.has_field(me.frm.doc.doctype, "tc_name") &&
|
|
||||||
buying_doctypes_for_tc.indexOf(me.frm.doc.doctype) != -1) {
|
|
||||||
me.frm.set_value("tc_name", company_doc.default_buying_terms);
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.run_serially([
|
frappe.run_serially([
|
||||||
() => me.frm.script_manager.trigger("currency"),
|
() => me.frm.script_manager.trigger("currency"),
|
||||||
() => me.update_item_tax_map(),
|
() => me.update_item_tax_map(),
|
||||||
|
@ -2,7 +2,58 @@ frappe.provide("erpnext.financial_statements");
|
|||||||
|
|
||||||
erpnext.financial_statements = {
|
erpnext.financial_statements = {
|
||||||
"filters": get_filters(),
|
"filters": get_filters(),
|
||||||
|
"baseData": null,
|
||||||
"formatter": function(value, row, column, data, default_formatter, filter) {
|
"formatter": function(value, row, column, data, default_formatter, filter) {
|
||||||
|
if(frappe.query_report.get_filter_value("selected_view") == "Growth" && data && column.colIndex >= 3){
|
||||||
|
//Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns.
|
||||||
|
const lastAnnualValue = row[column.colIndex - 1].content;
|
||||||
|
const currentAnnualvalue = data[column.fieldname];
|
||||||
|
if(currentAnnualvalue == undefined) return 'NA'; //making this not applicable for undefined/null values
|
||||||
|
let annualGrowth = 0;
|
||||||
|
if(lastAnnualValue == 0 && currentAnnualvalue > 0){
|
||||||
|
//If the previous year value is 0 and the current value is greater than 0
|
||||||
|
annualGrowth = 1;
|
||||||
|
}
|
||||||
|
else if(lastAnnualValue > 0){
|
||||||
|
annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const growthPercent = (Math.round(annualGrowth*10000)/100); //calculating the rounded off percentage
|
||||||
|
|
||||||
|
value = $(`<span>${((growthPercent >=0)? '+':'' )+growthPercent+'%'}</span>`);
|
||||||
|
if(growthPercent < 0){
|
||||||
|
value = $(value).addClass("text-danger");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
value = $(value).addClass("text-success");
|
||||||
|
}
|
||||||
|
value = $(value).wrap("<p></p>").parent().html();
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
else if(frappe.query_report.get_filter_value("selected_view") == "Margin" && data){
|
||||||
|
if(column.fieldname =="account" && data.account_name == __("Income")){
|
||||||
|
//Taking the total income from each column (for all the financial years) as the base (100%)
|
||||||
|
this.baseData = row;
|
||||||
|
}
|
||||||
|
if(column.colIndex >= 2){
|
||||||
|
//Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns.
|
||||||
|
const currentAnnualvalue = data[column.fieldname];
|
||||||
|
const baseValue = this.baseData[column.colIndex].content;
|
||||||
|
if(currentAnnualvalue == undefined || baseValue <= 0) return 'NA';
|
||||||
|
const marginPercent = Math.round((currentAnnualvalue/baseValue)*10000)/100;
|
||||||
|
|
||||||
|
value = $(`<span>${marginPercent+'%'}</span>`);
|
||||||
|
if(marginPercent < 0)
|
||||||
|
value = $(value).addClass("text-danger");
|
||||||
|
else
|
||||||
|
value = $(value).addClass("text-success");
|
||||||
|
value = $(value).wrap("<p></p>").parent().html();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
if (data && column.fieldname=="account") {
|
if (data && column.fieldname=="account") {
|
||||||
value = data.account_name || value;
|
value = data.account_name || value;
|
||||||
|
|
||||||
@ -74,22 +125,24 @@ erpnext.financial_statements = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
|
if (report.page){
|
||||||
|
const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
|
||||||
|
|
||||||
report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
|
report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
|
||||||
var filters = report.get_values();
|
var filters = report.get_values();
|
||||||
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
|
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
|
||||||
});
|
});
|
||||||
|
|
||||||
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
|
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
|
||||||
var filters = report.get_values();
|
var filters = report.get_values();
|
||||||
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
|
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
|
||||||
});
|
});
|
||||||
|
|
||||||
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
|
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
|
||||||
var filters = report.get_values();
|
var filters = report.get_values();
|
||||||
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
|
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,6 +25,10 @@ erpnext.accounts.dimensions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
setup_filters(frm, doctype) {
|
setup_filters(frm, doctype) {
|
||||||
|
if (doctype == 'Payment Entry' && this.accounting_dimensions) {
|
||||||
|
frm.dimension_filters = this.accounting_dimensions
|
||||||
|
}
|
||||||
|
|
||||||
if (this.accounting_dimensions) {
|
if (this.accounting_dimensions) {
|
||||||
this.accounting_dimensions.forEach((dimension) => {
|
this.accounting_dimensions.forEach((dimension) => {
|
||||||
frappe.model.with_doctype(dimension['document_type'], () => {
|
frappe.model.with_doctype(dimension['document_type'], () => {
|
||||||
|
@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
|||||||
let warehouse = this.item?.type_of_transaction === "Outward" ?
|
let warehouse = this.item?.type_of_transaction === "Outward" ?
|
||||||
(this.item.warehouse || this.item.s_warehouse) : "";
|
(this.item.warehouse || this.item.s_warehouse) : "";
|
||||||
|
|
||||||
|
if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') {
|
||||||
|
warehouse = this.get_warehouse();
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'item_code': this.item.item_code,
|
'item_code': this.item.item_code,
|
||||||
'warehouse': ["=", warehouse]
|
'warehouse': ["=", warehouse]
|
||||||
|
@ -391,7 +391,6 @@ def _make_sales_order(source_name, target_doc=None, customer_group=None, ignore_
|
|||||||
balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0)
|
balance_qty = obj.qty - ordered_items.get(obj.item_code, 0.0)
|
||||||
target.qty = balance_qty if balance_qty > 0 else 0
|
target.qty = balance_qty if balance_qty > 0 else 0
|
||||||
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
||||||
target.delivery_date = nowdate()
|
|
||||||
|
|
||||||
if obj.against_blanket_order:
|
if obj.against_blanket_order:
|
||||||
target.against_blanket_order = obj.against_blanket_order
|
target.against_blanket_order = obj.against_blanket_order
|
||||||
|
@ -87,7 +87,6 @@ class TestQuotation(FrappeTestCase):
|
|||||||
self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name)
|
self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name)
|
||||||
self.assertEqual(sales_order.customer, "_Test Customer")
|
self.assertEqual(sales_order.customer, "_Test Customer")
|
||||||
|
|
||||||
sales_order.delivery_date = "2014-01-01"
|
|
||||||
sales_order.naming_series = "_T-Quotation-"
|
sales_order.naming_series = "_T-Quotation-"
|
||||||
sales_order.transaction_date = nowdate()
|
sales_order.transaction_date = nowdate()
|
||||||
sales_order.insert()
|
sales_order.insert()
|
||||||
@ -120,7 +119,6 @@ class TestQuotation(FrappeTestCase):
|
|||||||
self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name)
|
self.assertEqual(sales_order.get("items")[0].prevdoc_docname, quotation.name)
|
||||||
self.assertEqual(sales_order.customer, "_Test Customer")
|
self.assertEqual(sales_order.customer, "_Test Customer")
|
||||||
|
|
||||||
sales_order.delivery_date = "2014-01-01"
|
|
||||||
sales_order.naming_series = "_T-Quotation-"
|
sales_order.naming_series = "_T-Quotation-"
|
||||||
sales_order.transaction_date = nowdate()
|
sales_order.transaction_date = nowdate()
|
||||||
sales_order.insert()
|
sales_order.insert()
|
||||||
|
@ -118,6 +118,7 @@
|
|||||||
"oldfieldtype": "Link",
|
"oldfieldtype": "Link",
|
||||||
"options": "Item",
|
"options": "Item",
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
|
"reqd": 1,
|
||||||
"width": "150px"
|
"width": "150px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -908,7 +909,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-24 13:24:55.756320",
|
"modified": "2024-01-25 14:24:00.330219",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
@ -43,7 +43,8 @@ class SalesOrderItem(Document):
|
|||||||
gross_profit: DF.Currency
|
gross_profit: DF.Currency
|
||||||
image: DF.Attach | None
|
image: DF.Attach | None
|
||||||
is_free_item: DF.Check
|
is_free_item: DF.Check
|
||||||
item_code: DF.Link | None
|
is_stock_item: DF.Check
|
||||||
|
item_code: DF.Link
|
||||||
item_group: DF.Link | None
|
item_group: DF.Link | None
|
||||||
item_name: DF.Data
|
item_name: DF.Data
|
||||||
item_tax_rate: DF.Code | None
|
item_tax_rate: DF.Code | None
|
||||||
|
@ -210,7 +210,6 @@ def get_so_with_invoices(filters):
|
|||||||
.where(
|
.where(
|
||||||
(so.docstatus == 1)
|
(so.docstatus == 1)
|
||||||
& (so.status.isin(["To Deliver and Bill", "To Bill"]))
|
& (so.status.isin(["To Deliver and Bill", "To Bill"]))
|
||||||
& (so.payment_terms_template != "NULL")
|
|
||||||
& (so.company == conditions.company)
|
& (so.company == conditions.company)
|
||||||
& (so.transaction_date[conditions.start_date : conditions.end_date])
|
& (so.transaction_date[conditions.start_date : conditions.end_date])
|
||||||
)
|
)
|
||||||
|
@ -52,7 +52,7 @@ frappe.ui.form.on('Batch', {
|
|||||||
// sort by qty
|
// sort by qty
|
||||||
r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 });
|
r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 });
|
||||||
|
|
||||||
var rows = $('<div></div>').appendTo(section);
|
const rows = $('<div></div>').appendTo(section);
|
||||||
|
|
||||||
// show
|
// show
|
||||||
(r.message || []).forEach(function(d) {
|
(r.message || []).forEach(function(d) {
|
||||||
@ -76,7 +76,7 @@ frappe.ui.form.on('Batch', {
|
|||||||
|
|
||||||
// move - ask for target warehouse and make stock entry
|
// move - ask for target warehouse and make stock entry
|
||||||
rows.find('.btn-move').on('click', function() {
|
rows.find('.btn-move').on('click', function() {
|
||||||
var $btn = $(this);
|
const $btn = $(this);
|
||||||
const fields = [
|
const fields = [
|
||||||
{
|
{
|
||||||
fieldname: 'to_warehouse',
|
fieldname: 'to_warehouse',
|
||||||
@ -115,7 +115,7 @@ frappe.ui.form.on('Batch', {
|
|||||||
// split - ask for new qty and batch ID (optional)
|
// split - ask for new qty and batch ID (optional)
|
||||||
// and make stock entry via batch.batch_split
|
// and make stock entry via batch.batch_split
|
||||||
rows.find('.btn-split').on('click', function() {
|
rows.find('.btn-split').on('click', function() {
|
||||||
var $btn = $(this);
|
const $btn = $(this);
|
||||||
frappe.prompt([{
|
frappe.prompt([{
|
||||||
fieldname: 'qty',
|
fieldname: 'qty',
|
||||||
label: __('New Batch Qty'),
|
label: __('New Batch Qty'),
|
||||||
@ -128,19 +128,16 @@ frappe.ui.form.on('Batch', {
|
|||||||
fieldtype: 'Data',
|
fieldtype: 'Data',
|
||||||
}],
|
}],
|
||||||
(data) => {
|
(data) => {
|
||||||
frappe.call({
|
frappe.xcall(
|
||||||
method: 'erpnext.stock.doctype.batch.batch.split_batch',
|
'erpnext.stock.doctype.batch.batch.split_batch',
|
||||||
args: {
|
{
|
||||||
item_code: frm.doc.item,
|
item_code: frm.doc.item,
|
||||||
batch_no: frm.doc.name,
|
batch_no: frm.doc.name,
|
||||||
qty: data.qty,
|
qty: data.qty,
|
||||||
warehouse: $btn.attr('data-warehouse'),
|
warehouse: $btn.attr('data-warehouse'),
|
||||||
new_batch_id: data.new_batch_id
|
new_batch_id: data.new_batch_id
|
||||||
},
|
}
|
||||||
callback: (r) => {
|
).then(() => frm.reload_doc());
|
||||||
frm.refresh();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
__('Split Batch'),
|
__('Split Batch'),
|
||||||
__('Split')
|
__('Split')
|
||||||
|
@ -9,7 +9,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.naming import make_autoname, revert_series_if_last
|
from frappe.model.naming import make_autoname, revert_series_if_last
|
||||||
from frappe.query_builder.functions import CurDate, Sum
|
from frappe.query_builder.functions import CurDate, Sum
|
||||||
from frappe.utils import cint, flt, get_link_to_form, nowtime, today
|
from frappe.utils import cint, flt, get_link_to_form
|
||||||
from frappe.utils.data import add_days
|
from frappe.utils.data import add_days
|
||||||
from frappe.utils.jinja import render_template
|
from frappe.utils.jinja import render_template
|
||||||
|
|
||||||
@ -248,8 +248,9 @@ def get_batches_by_oldest(item_code, warehouse):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
|
def split_batch(
|
||||||
|
batch_no: str, item_code: str, warehouse: str, qty: float, new_batch_id: str | None = None
|
||||||
|
):
|
||||||
"""Split the batch into a new batch"""
|
"""Split the batch into a new batch"""
|
||||||
batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert()
|
batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert()
|
||||||
qty = flt(qty)
|
qty = flt(qty)
|
||||||
@ -257,29 +258,21 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
|
|||||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||||
|
|
||||||
from_bundle_id = make_batch_bundle(
|
from_bundle_id = make_batch_bundle(
|
||||||
frappe._dict(
|
item_code=item_code,
|
||||||
{
|
warehouse=warehouse,
|
||||||
"item_code": item_code,
|
batches=frappe._dict({batch_no: qty}),
|
||||||
"warehouse": warehouse,
|
company=company,
|
||||||
"batches": frappe._dict({batch_no: qty}),
|
type_of_transaction="Outward",
|
||||||
"company": company,
|
qty=qty,
|
||||||
"type_of_transaction": "Outward",
|
|
||||||
"qty": qty,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
to_bundle_id = make_batch_bundle(
|
to_bundle_id = make_batch_bundle(
|
||||||
frappe._dict(
|
item_code=item_code,
|
||||||
{
|
warehouse=warehouse,
|
||||||
"item_code": item_code,
|
batches=frappe._dict({batch.name: qty}),
|
||||||
"warehouse": warehouse,
|
company=company,
|
||||||
"batches": frappe._dict({batch.name: qty}),
|
type_of_transaction="Inward",
|
||||||
"company": company,
|
qty=qty,
|
||||||
"type_of_transaction": "Inward",
|
|
||||||
"qty": qty,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
stock_entry = frappe.get_doc(
|
stock_entry = frappe.get_doc(
|
||||||
@ -304,21 +297,30 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
|
|||||||
return batch.name
|
return batch.name
|
||||||
|
|
||||||
|
|
||||||
def make_batch_bundle(kwargs):
|
def make_batch_bundle(
|
||||||
|
item_code: str,
|
||||||
|
warehouse: str,
|
||||||
|
batches: dict[str, float],
|
||||||
|
company: str,
|
||||||
|
type_of_transaction: str,
|
||||||
|
qty: float,
|
||||||
|
):
|
||||||
|
from frappe.utils import nowtime, today
|
||||||
|
|
||||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||||
|
|
||||||
return (
|
return (
|
||||||
SerialBatchCreation(
|
SerialBatchCreation(
|
||||||
{
|
{
|
||||||
"item_code": kwargs.item_code,
|
"item_code": item_code,
|
||||||
"warehouse": kwargs.warehouse,
|
"warehouse": warehouse,
|
||||||
"posting_date": today(),
|
"posting_date": today(),
|
||||||
"posting_time": nowtime(),
|
"posting_time": nowtime(),
|
||||||
"voucher_type": "Stock Entry",
|
"voucher_type": "Stock Entry",
|
||||||
"qty": flt(kwargs.qty),
|
"qty": qty,
|
||||||
"type_of_transaction": kwargs.type_of_transaction,
|
"type_of_transaction": type_of_transaction,
|
||||||
"company": kwargs.company,
|
"company": company,
|
||||||
"batches": kwargs.batches,
|
"batches": batches,
|
||||||
"do_not_submit": True,
|
"do_not_submit": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -8,6 +8,7 @@ def get_data():
|
|||||||
"Stock Entry": "delivery_note_no",
|
"Stock Entry": "delivery_note_no",
|
||||||
"Quality Inspection": "reference_name",
|
"Quality Inspection": "reference_name",
|
||||||
"Auto Repeat": "reference_document",
|
"Auto Repeat": "reference_document",
|
||||||
|
"Purchase Receipt": "inter_company_reference",
|
||||||
},
|
},
|
||||||
"internal_links": {
|
"internal_links": {
|
||||||
"Sales Order": ["items", "against_sales_order"],
|
"Sales Order": ["items", "against_sales_order"],
|
||||||
@ -22,6 +23,9 @@ def get_data():
|
|||||||
{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
|
{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
|
||||||
{"label": _("Returns"), "items": ["Stock Entry"]},
|
{"label": _("Returns"), "items": ["Stock Entry"]},
|
||||||
{"label": _("Subscription"), "items": ["Auto Repeat"]},
|
{"label": _("Subscription"), "items": ["Auto Repeat"]},
|
||||||
{"label": _("Internal Transfer"), "items": ["Material Request", "Purchase Order"]},
|
{
|
||||||
|
"label": _("Internal Transfer"),
|
||||||
|
"items": ["Material Request", "Purchase Order", "Purchase Receipt"],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
@ -429,6 +429,9 @@ frappe.ui.form.on("Material Request Item", {
|
|||||||
|
|
||||||
rate: function(frm, doctype, name) {
|
rate: function(frm, doctype, name) {
|
||||||
const item = locals[doctype][name];
|
const item = locals[doctype][name];
|
||||||
|
item.amount = flt(item.qty) * flt(item.rate);
|
||||||
|
frappe.model.set_value(doctype, name, "amount", item.amount);
|
||||||
|
refresh_field("amount", item.name, item.parentfield);
|
||||||
frm.events.get_item_data(frm, item, false);
|
frm.events.get_item_data(frm, item, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -762,6 +762,62 @@ class TestMaterialRequest(FrappeTestCase):
|
|||||||
self.assertEqual(mr.per_ordered, 100)
|
self.assertEqual(mr.per_ordered, 100)
|
||||||
self.assertEqual(existing_requested_qty, current_requested_qty)
|
self.assertEqual(existing_requested_qty, current_requested_qty)
|
||||||
|
|
||||||
|
def test_auto_email_users_with_company_user_permissions(self):
|
||||||
|
from erpnext.stock.reorder_item import get_email_list
|
||||||
|
|
||||||
|
comapnywise_users = {
|
||||||
|
"_Test Company": "test_auto_email_@example.com",
|
||||||
|
"_Test Company 1": "test_auto_email_1@example.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
permissions = []
|
||||||
|
|
||||||
|
for company, user in comapnywise_users.items():
|
||||||
|
if not frappe.db.exists("User", user):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "User",
|
||||||
|
"email": user,
|
||||||
|
"first_name": user,
|
||||||
|
"send_notifications": 0,
|
||||||
|
"enabled": 1,
|
||||||
|
"user_type": "System User",
|
||||||
|
"roles": [{"role": "Purchase Manager"}],
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
if not frappe.db.exists(
|
||||||
|
"User Permission", {"user": user, "allow": "Company", "for_value": company}
|
||||||
|
):
|
||||||
|
perm_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "User Permission",
|
||||||
|
"user": user,
|
||||||
|
"allow": "Company",
|
||||||
|
"for_value": company,
|
||||||
|
"apply_to_all_doctypes": 1,
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
permissions.append(perm_doc)
|
||||||
|
|
||||||
|
comapnywise_mr_list = frappe._dict({})
|
||||||
|
mr1 = make_material_request()
|
||||||
|
comapnywise_mr_list.setdefault(mr1.company, []).append(mr1.name)
|
||||||
|
|
||||||
|
mr2 = make_material_request(
|
||||||
|
company="_Test Company 1", warehouse="Stores - _TC1", cost_center="Main - _TC1"
|
||||||
|
)
|
||||||
|
comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name)
|
||||||
|
|
||||||
|
for company, mr_list in comapnywise_mr_list.items():
|
||||||
|
emails = get_email_list(company)
|
||||||
|
|
||||||
|
self.assertTrue(comapnywise_users[company] in emails)
|
||||||
|
|
||||||
|
for perm in permissions:
|
||||||
|
perm.delete()
|
||||||
|
|
||||||
|
|
||||||
def get_in_transit_warehouse(company):
|
def get_in_transit_warehouse(company):
|
||||||
if not frappe.db.exists("Warehouse Type", "Transit"):
|
if not frappe.db.exists("Warehouse Type", "Transit"):
|
||||||
|
@ -8,8 +8,10 @@ frappe.listview_settings['Purchase Receipt'] = {
|
|||||||
return [__("Closed"), "green", "status,=,Closed"];
|
return [__("Closed"), "green", "status,=,Closed"];
|
||||||
} else if (flt(doc.per_returned, 2) === 100) {
|
} else if (flt(doc.per_returned, 2) === 100) {
|
||||||
return [__("Return Issued"), "grey", "per_returned,=,100"];
|
return [__("Return Issued"), "grey", "per_returned,=,100"];
|
||||||
} else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) < 100) {
|
} else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) == 0) {
|
||||||
return [__("To Bill"), "orange", "per_billed,<,100"];
|
return [__("To Bill"), "orange", "per_billed,<,100"];
|
||||||
|
} else if (flt(doc.per_billed, 2) > 0 && flt(doc.per_billed, 2) < 100) {
|
||||||
|
return [__("Partly Billed"), "yellow", "per_billed,<,100"];
|
||||||
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
|
} else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
|
||||||
return [__("Completed"), "green", "per_billed,=,100"];
|
return [__("Completed"), "green", "per_billed,=,100"];
|
||||||
}
|
}
|
||||||
|
@ -704,7 +704,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
pr2.load_from_db()
|
pr2.load_from_db()
|
||||||
self.assertEqual(pr2.get("items")[0].billed_amt, 2000)
|
self.assertEqual(pr2.get("items")[0].billed_amt, 2000)
|
||||||
self.assertEqual(pr2.per_billed, 80)
|
self.assertEqual(pr2.per_billed, 80)
|
||||||
self.assertEqual(pr2.status, "To Bill")
|
self.assertEqual(pr2.status, "Partly Billed")
|
||||||
|
|
||||||
pr2.cancel()
|
pr2.cancel()
|
||||||
pi2.reload()
|
pi2.reload()
|
||||||
@ -1115,7 +1115,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
pi.load_from_db()
|
pi.load_from_db()
|
||||||
pr.load_from_db()
|
pr.load_from_db()
|
||||||
|
|
||||||
self.assertEqual(pr.status, "To Bill")
|
self.assertEqual(pr.status, "Partly Billed")
|
||||||
self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
|
self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
|
||||||
|
|
||||||
def test_purchase_receipt_with_exchange_rate_difference(self):
|
def test_purchase_receipt_with_exchange_rate_difference(self):
|
||||||
@ -1638,9 +1638,10 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
make_stock_entry(
|
make_stock_entry(
|
||||||
purpose="Material Receipt",
|
purpose="Material Receipt",
|
||||||
item_code=item.name,
|
item_code=item.name,
|
||||||
qty=15,
|
qty=20,
|
||||||
company=company,
|
company=company,
|
||||||
to_warehouse=from_warehouse,
|
to_warehouse=from_warehouse,
|
||||||
|
posting_date=add_days(today(), -3),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Step 3: Create Delivery Note with Internal Customer
|
# Step 3: Create Delivery Note with Internal Customer
|
||||||
@ -1663,13 +1664,15 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
|
||||||
|
|
||||||
pr = make_inter_company_purchase_receipt(dn.name)
|
pr = make_inter_company_purchase_receipt(dn.name)
|
||||||
|
pr.set_posting_time = 1
|
||||||
|
pr.posting_date = today()
|
||||||
pr.items[0].qty = 15
|
pr.items[0].qty = 15
|
||||||
pr.items[0].from_warehouse = target_warehouse
|
pr.items[0].from_warehouse = target_warehouse
|
||||||
pr.items[0].warehouse = to_warehouse
|
pr.items[0].warehouse = to_warehouse
|
||||||
pr.items[0].rejected_warehouse = from_warehouse
|
pr.items[0].rejected_warehouse = from_warehouse
|
||||||
pr.save()
|
pr.save()
|
||||||
|
|
||||||
self.assertRaises(OverAllowanceError, pr.submit)
|
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||||
|
|
||||||
# Step 5: Test Over Receipt Allowance
|
# Step 5: Test Over Receipt Allowance
|
||||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
|
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
|
||||||
@ -1681,8 +1684,10 @@ class TestPurchaseReceipt(FrappeTestCase):
|
|||||||
company=company,
|
company=company,
|
||||||
from_warehouse=from_warehouse,
|
from_warehouse=from_warehouse,
|
||||||
to_warehouse=target_warehouse,
|
to_warehouse=target_warehouse,
|
||||||
|
posting_date=add_days(pr.posting_date, -1),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pr.reload()
|
||||||
pr.submit()
|
pr.submit()
|
||||||
|
|
||||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
||||||
|
@ -250,6 +250,7 @@ class SerialandBatchBundle(Document):
|
|||||||
|
|
||||||
for d in self.entries:
|
for d in self.entries:
|
||||||
available_qty = 0
|
available_qty = 0
|
||||||
|
|
||||||
if self.has_serial_no:
|
if self.has_serial_no:
|
||||||
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
|
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
|
||||||
else:
|
else:
|
||||||
@ -892,6 +893,13 @@ class SerialandBatchBundle(Document):
|
|||||||
elif batch_nos:
|
elif batch_nos:
|
||||||
self.set("entries", batch_nos)
|
self.set("entries", batch_nos)
|
||||||
|
|
||||||
|
def delete_serial_batch_entries(self):
|
||||||
|
SBBE = frappe.qb.DocType("Serial and Batch Entry")
|
||||||
|
|
||||||
|
frappe.qb.from_(SBBE).delete().where(SBBE.parent == self.name).run()
|
||||||
|
|
||||||
|
self.set("entries", [])
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def download_blank_csv_template(content):
|
def download_blank_csv_template(content):
|
||||||
@ -1374,10 +1382,12 @@ def get_available_serial_nos(kwargs):
|
|||||||
elif kwargs.based_on == "Expiry":
|
elif kwargs.based_on == "Expiry":
|
||||||
order_by = "amc_expiry_date asc"
|
order_by = "amc_expiry_date asc"
|
||||||
|
|
||||||
filters = {"item_code": kwargs.item_code, "warehouse": ("is", "set")}
|
filters = {"item_code": kwargs.item_code}
|
||||||
|
|
||||||
if kwargs.warehouse:
|
if not kwargs.get("ignore_warehouse"):
|
||||||
filters["warehouse"] = kwargs.warehouse
|
filters["warehouse"] = ("is", "set")
|
||||||
|
if kwargs.warehouse:
|
||||||
|
filters["warehouse"] = kwargs.warehouse
|
||||||
|
|
||||||
# Since SLEs are not present against Reserved Stock [POS invoices, SRE], need to ignore reserved serial nos.
|
# Since SLEs are not present against Reserved Stock [POS invoices, SRE], need to ignore reserved serial nos.
|
||||||
ignore_serial_nos = get_reserved_serial_nos(kwargs)
|
ignore_serial_nos = get_reserved_serial_nos(kwargs)
|
||||||
|
@ -228,7 +228,6 @@ class StockEntry(StockController):
|
|||||||
self.fg_completed_qty = 0.0
|
self.fg_completed_qty = 0.0
|
||||||
|
|
||||||
self.validate_serialized_batch()
|
self.validate_serialized_batch()
|
||||||
self.set_actual_qty()
|
|
||||||
self.calculate_rate_and_amount()
|
self.calculate_rate_and_amount()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
|
|
||||||
|
@ -156,6 +156,7 @@ class StockReconciliation(StockController):
|
|||||||
"warehouse": item.warehouse,
|
"warehouse": item.warehouse,
|
||||||
"posting_date": self.posting_date,
|
"posting_date": self.posting_date,
|
||||||
"posting_time": self.posting_time,
|
"posting_time": self.posting_time,
|
||||||
|
"ignore_warehouse": 1,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -780,7 +781,20 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
current_qty = 0.0
|
current_qty = 0.0
|
||||||
if row.current_serial_and_batch_bundle:
|
if row.current_serial_and_batch_bundle:
|
||||||
current_qty = self.get_qty_for_serial_and_batch_bundle(row)
|
current_qty = self.get_current_qty_for_serial_or_batch(row)
|
||||||
|
elif row.serial_no:
|
||||||
|
item_dict = get_stock_balance_for(
|
||||||
|
row.item_code,
|
||||||
|
row.warehouse,
|
||||||
|
self.posting_date,
|
||||||
|
self.posting_time,
|
||||||
|
voucher_no=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
current_qty = item_dict.get("qty")
|
||||||
|
row.current_serial_no = item_dict.get("serial_nos")
|
||||||
|
row.current_valuation_rate = item_dict.get("rate")
|
||||||
|
val_rate = item_dict.get("rate")
|
||||||
elif row.batch_no:
|
elif row.batch_no:
|
||||||
current_qty = get_batch_qty_for_stock_reco(
|
current_qty = get_batch_qty_for_stock_reco(
|
||||||
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
||||||
@ -788,15 +802,16 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
precesion = row.precision("current_qty")
|
precesion = row.precision("current_qty")
|
||||||
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||||
val_rate = get_valuation_rate(
|
if not row.serial_no:
|
||||||
row.item_code,
|
val_rate = get_valuation_rate(
|
||||||
row.warehouse,
|
row.item_code,
|
||||||
self.doctype,
|
row.warehouse,
|
||||||
self.name,
|
self.doctype,
|
||||||
company=self.company,
|
self.name,
|
||||||
batch_no=row.batch_no,
|
company=self.company,
|
||||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
batch_no=row.batch_no,
|
||||||
)
|
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||||
|
)
|
||||||
|
|
||||||
row.current_valuation_rate = val_rate
|
row.current_valuation_rate = val_rate
|
||||||
row.current_qty = current_qty
|
row.current_qty = current_qty
|
||||||
@ -842,11 +857,56 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
return allow_negative_stock
|
return allow_negative_stock
|
||||||
|
|
||||||
def get_qty_for_serial_and_batch_bundle(self, row):
|
def get_current_qty_for_serial_or_batch(self, row):
|
||||||
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
|
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
|
||||||
precision = doc.entries[0].precision("qty")
|
current_qty = 0.0
|
||||||
|
if doc.has_serial_no:
|
||||||
|
current_qty = self.get_current_qty_for_serial_nos(doc)
|
||||||
|
elif doc.has_batch_no:
|
||||||
|
current_qty = self.get_current_qty_for_batch_nos(doc)
|
||||||
|
|
||||||
current_qty = 0
|
return abs(current_qty)
|
||||||
|
|
||||||
|
def get_current_qty_for_serial_nos(self, doc):
|
||||||
|
serial_nos_details = get_available_serial_nos(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"item_code": doc.item_code,
|
||||||
|
"warehouse": doc.warehouse,
|
||||||
|
"posting_date": self.posting_date,
|
||||||
|
"posting_time": self.posting_time,
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"ignore_warehouse": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not serial_nos_details:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
doc.delete_serial_batch_entries()
|
||||||
|
current_qty = 0.0
|
||||||
|
for serial_no_row in serial_nos_details:
|
||||||
|
current_qty += 1
|
||||||
|
doc.append(
|
||||||
|
"entries",
|
||||||
|
{
|
||||||
|
"serial_no": serial_no_row.serial_no,
|
||||||
|
"qty": -1,
|
||||||
|
"warehouse": doc.warehouse,
|
||||||
|
"batch_no": serial_no_row.batch_no,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.set_incoming_rate(save=True)
|
||||||
|
doc.calculate_qty_and_amount(save=True)
|
||||||
|
doc.db_update_all()
|
||||||
|
|
||||||
|
return current_qty
|
||||||
|
|
||||||
|
def get_current_qty_for_batch_nos(self, doc):
|
||||||
|
current_qty = 0.0
|
||||||
|
precision = doc.entries[0].precision("qty")
|
||||||
for d in doc.entries:
|
for d in doc.entries:
|
||||||
qty = (
|
qty = (
|
||||||
get_batch_qty(
|
get_batch_qty(
|
||||||
@ -864,7 +924,7 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
current_qty += qty
|
current_qty += qty
|
||||||
|
|
||||||
return abs(current_qty)
|
return current_qty
|
||||||
|
|
||||||
|
|
||||||
def get_batch_qty_for_stock_reco(
|
def get_batch_qty_for_stock_reco(
|
||||||
|
@ -925,6 +925,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
|||||||
|
|
||||||
self.assertEqual(len(serial_batch_bundle), 0)
|
self.assertEqual(len(serial_batch_bundle), 0)
|
||||||
|
|
||||||
|
def test_backdated_purchase_receipt_with_stock_reco(self):
|
||||||
|
item_code = self.make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"has_serial_no": 1,
|
||||||
|
"serial_no_series": "TEST-SERIAL-.###",
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
|
# Step - 1: Create a Backdated Purchase Receipt
|
||||||
|
|
||||||
|
pr1 = make_purchase_receipt(
|
||||||
|
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
||||||
|
)
|
||||||
|
pr1.reload()
|
||||||
|
|
||||||
|
serial_nos = sorted(get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle))[:5]
|
||||||
|
|
||||||
|
# Step - 2: Create a Stock Reconciliation
|
||||||
|
sr1 = create_stock_reconciliation(
|
||||||
|
item_code=item_code,
|
||||||
|
warehouse=warehouse,
|
||||||
|
qty=5,
|
||||||
|
serial_no=serial_nos,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
||||||
|
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
||||||
|
order_by="creation",
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
if d.actual_qty < 0:
|
||||||
|
self.assertEqual(d.actual_qty, -10.0)
|
||||||
|
self.assertAlmostEqual(d.stock_value_difference, -1000.0)
|
||||||
|
else:
|
||||||
|
self.assertEqual(d.actual_qty, 5.0)
|
||||||
|
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||||
|
|
||||||
|
# Step - 3: Create a Purchase Receipt before the first Purchase Receipt
|
||||||
|
make_purchase_receipt(
|
||||||
|
item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = frappe.get_all(
|
||||||
|
"Stock Ledger Entry",
|
||||||
|
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
||||||
|
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
||||||
|
order_by="creation",
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in data:
|
||||||
|
if d.actual_qty < 0:
|
||||||
|
self.assertEqual(d.actual_qty, -20.0)
|
||||||
|
self.assertAlmostEqual(d.stock_value_difference, -3000.0)
|
||||||
|
else:
|
||||||
|
self.assertEqual(d.actual_qty, 5.0)
|
||||||
|
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||||
|
|
||||||
|
active_serial_no = frappe.get_all(
|
||||||
|
"Serial No", filters={"status": "Active", "item_code": item_code}
|
||||||
|
)
|
||||||
|
self.assertEqual(len(active_serial_no), 5)
|
||||||
|
|
||||||
|
|
||||||
def create_batch_item_with_batch(item_name, batch_id):
|
def create_batch_item_with_batch(item_name, batch_id):
|
||||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||||
|
@ -145,6 +145,7 @@ def create_material_request(material_requests):
|
|||||||
|
|
||||||
mr.log_error("Unable to create material request")
|
mr.log_error("Unable to create material request")
|
||||||
|
|
||||||
|
company_wise_mr = frappe._dict({})
|
||||||
for request_type in material_requests:
|
for request_type in material_requests:
|
||||||
for company in material_requests[request_type]:
|
for company in material_requests[request_type]:
|
||||||
try:
|
try:
|
||||||
@ -206,17 +207,19 @@ def create_material_request(material_requests):
|
|||||||
mr.submit()
|
mr.submit()
|
||||||
mr_list.append(mr)
|
mr_list.append(mr)
|
||||||
|
|
||||||
|
company_wise_mr.setdefault(company, []).append(mr)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
_log_exception(mr)
|
_log_exception(mr)
|
||||||
|
|
||||||
if mr_list:
|
if company_wise_mr:
|
||||||
if getattr(frappe.local, "reorder_email_notify", None) is None:
|
if getattr(frappe.local, "reorder_email_notify", None) is None:
|
||||||
frappe.local.reorder_email_notify = cint(
|
frappe.local.reorder_email_notify = cint(
|
||||||
frappe.db.get_value("Stock Settings", None, "reorder_email_notify")
|
frappe.db.get_value("Stock Settings", None, "reorder_email_notify")
|
||||||
)
|
)
|
||||||
|
|
||||||
if frappe.local.reorder_email_notify:
|
if frappe.local.reorder_email_notify:
|
||||||
send_email_notification(mr_list)
|
send_email_notification(company_wise_mr)
|
||||||
|
|
||||||
if exceptions_list:
|
if exceptions_list:
|
||||||
notify_errors(exceptions_list)
|
notify_errors(exceptions_list)
|
||||||
@ -224,20 +227,56 @@ def create_material_request(material_requests):
|
|||||||
return mr_list
|
return mr_list
|
||||||
|
|
||||||
|
|
||||||
def send_email_notification(mr_list):
|
def send_email_notification(company_wise_mr):
|
||||||
"""Notify user about auto creation of indent"""
|
"""Notify user about auto creation of indent"""
|
||||||
|
|
||||||
email_list = frappe.db.sql_list(
|
for company, mr_list in company_wise_mr.items():
|
||||||
"""select distinct r.parent
|
email_list = get_email_list(company)
|
||||||
from `tabHas Role` r, tabUser p
|
|
||||||
where p.name = r.parent and p.enabled = 1 and p.docstatus < 2
|
if not email_list:
|
||||||
and r.role in ('Purchase Manager','Stock Manager')
|
continue
|
||||||
and p.name not in ('Administrator', 'All', 'Guest')"""
|
|
||||||
|
msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list})
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_email_list(company):
|
||||||
|
users = get_comapny_wise_users(company)
|
||||||
|
user_table = frappe.qb.DocType("User")
|
||||||
|
role_table = frappe.qb.DocType("Has Role")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(user_table)
|
||||||
|
.inner_join(role_table)
|
||||||
|
.on(user_table.name == role_table.parent)
|
||||||
|
.select(user_table.email)
|
||||||
|
.where(
|
||||||
|
(role_table.role.isin(["Purchase Manager", "Stock Manager"]))
|
||||||
|
& (user_table.name.notin(["Administrator", "All", "Guest"]))
|
||||||
|
& (user_table.enabled == 1)
|
||||||
|
& (user_table.docstatus < 2)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list})
|
if users:
|
||||||
|
query = query.where(user_table.name.isin(users))
|
||||||
|
|
||||||
frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg)
|
emails = query.run(as_dict=True)
|
||||||
|
|
||||||
|
return list(set([email.email for email in emails]))
|
||||||
|
|
||||||
|
|
||||||
|
def get_comapny_wise_users(company):
|
||||||
|
users = frappe.get_all(
|
||||||
|
"User Permission",
|
||||||
|
filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1},
|
||||||
|
fields=["user"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return [user.user for user in users]
|
||||||
|
|
||||||
|
|
||||||
def notify_errors(exceptions_list):
|
def notify_errors(exceptions_list):
|
||||||
|
@ -22,9 +22,8 @@ def get_columns(filters):
|
|||||||
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
|
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
|
||||||
{
|
{
|
||||||
"label": _("Voucher Type"),
|
"label": _("Voucher Type"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Data",
|
||||||
"fieldname": "voucher_type",
|
"fieldname": "voucher_type",
|
||||||
"options": "DocType",
|
|
||||||
"width": 160,
|
"width": 160,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -99,7 +99,7 @@ frappe.query_reports["Stock Balance"] = {
|
|||||||
"fieldname": 'ignore_closing_balance',
|
"fieldname": 'ignore_closing_balance',
|
||||||
"label": __('Ignore Closing Balance'),
|
"label": __('Ignore Closing Balance'),
|
||||||
"fieldtype": 'Check',
|
"fieldtype": 'Check',
|
||||||
"default": 1
|
"default": 0
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -9,9 +9,18 @@ from typing import Optional, Set, Tuple
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.query_builder import Case
|
|
||||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||||
from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json
|
from frappe.utils import (
|
||||||
|
cint,
|
||||||
|
cstr,
|
||||||
|
flt,
|
||||||
|
get_link_to_form,
|
||||||
|
getdate,
|
||||||
|
now,
|
||||||
|
nowdate,
|
||||||
|
nowtime,
|
||||||
|
parse_json,
|
||||||
|
)
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||||
@ -712,11 +721,10 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
sle.voucher_type == "Stock Reconciliation"
|
sle.voucher_type == "Stock Reconciliation"
|
||||||
and (
|
and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
|
||||||
sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
|
|
||||||
)
|
|
||||||
and sle.voucher_detail_no
|
and sle.voucher_detail_no
|
||||||
and not self.args.get("sle_id")
|
and not self.args.get("sle_id")
|
||||||
|
and sle.is_cancelled == 0
|
||||||
):
|
):
|
||||||
self.reset_actual_qty_for_stock_reco(sle)
|
self.reset_actual_qty_for_stock_reco(sle)
|
||||||
|
|
||||||
@ -737,6 +745,23 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
if sle.serial_and_batch_bundle:
|
if sle.serial_and_batch_bundle:
|
||||||
self.calculate_valuation_for_serial_batch_bundle(sle)
|
self.calculate_valuation_for_serial_batch_bundle(sle)
|
||||||
|
elif sle.serial_no and not self.args.get("sle_id"):
|
||||||
|
# Only run in reposting
|
||||||
|
self.get_serialized_values(sle)
|
||||||
|
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
|
||||||
|
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
||||||
|
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
||||||
|
|
||||||
|
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
|
||||||
|
self.wh_data.valuation_rate
|
||||||
|
)
|
||||||
|
elif (
|
||||||
|
sle.batch_no
|
||||||
|
and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
|
||||||
|
and not self.args.get("sle_id")
|
||||||
|
):
|
||||||
|
# Only run in reposting
|
||||||
|
self.update_batched_values(sle)
|
||||||
else:
|
else:
|
||||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
|
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
|
||||||
# assert
|
# assert
|
||||||
@ -782,6 +807,45 @@ class update_entries_after(object):
|
|||||||
):
|
):
|
||||||
self.update_outgoing_rate_on_transaction(sle)
|
self.update_outgoing_rate_on_transaction(sle)
|
||||||
|
|
||||||
|
def get_serialized_values(self, sle):
|
||||||
|
incoming_rate = flt(sle.incoming_rate)
|
||||||
|
actual_qty = flt(sle.actual_qty)
|
||||||
|
serial_nos = cstr(sle.serial_no).split("\n")
|
||||||
|
|
||||||
|
if incoming_rate < 0:
|
||||||
|
# wrong incoming rate
|
||||||
|
incoming_rate = self.wh_data.valuation_rate
|
||||||
|
|
||||||
|
stock_value_change = 0
|
||||||
|
if actual_qty > 0:
|
||||||
|
stock_value_change = actual_qty * incoming_rate
|
||||||
|
else:
|
||||||
|
# In case of delivery/stock issue, get average purchase rate
|
||||||
|
# of serial nos of current entry
|
||||||
|
if not sle.is_cancelled:
|
||||||
|
outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
|
||||||
|
stock_value_change = -1 * outgoing_value
|
||||||
|
else:
|
||||||
|
stock_value_change = actual_qty * sle.outgoing_rate
|
||||||
|
|
||||||
|
new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
|
||||||
|
|
||||||
|
if new_stock_qty > 0:
|
||||||
|
new_stock_value = (
|
||||||
|
self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
|
||||||
|
) + stock_value_change
|
||||||
|
if new_stock_value >= 0:
|
||||||
|
# calculate new valuation rate only if stock value is positive
|
||||||
|
# else it remains the same as that of previous entry
|
||||||
|
self.wh_data.valuation_rate = new_stock_value / new_stock_qty
|
||||||
|
|
||||||
|
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
|
||||||
|
allow_zero_rate = self.check_if_allow_zero_valuation_rate(
|
||||||
|
sle.voucher_type, sle.voucher_detail_no
|
||||||
|
)
|
||||||
|
if not allow_zero_rate:
|
||||||
|
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||||
|
|
||||||
def reset_actual_qty_for_stock_reco(self, sle):
|
def reset_actual_qty_for_stock_reco(self, sle):
|
||||||
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
||||||
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||||
@ -795,6 +859,36 @@ class update_entries_after(object):
|
|||||||
if abs(sle.actual_qty) == 0.0:
|
if abs(sle.actual_qty) == 0.0:
|
||||||
sle.is_cancelled = 1
|
sle.is_cancelled = 1
|
||||||
|
|
||||||
|
if sle.serial_and_batch_bundle and frappe.get_cached_value(
|
||||||
|
"Item", sle.item_code, "has_serial_no"
|
||||||
|
):
|
||||||
|
self.update_serial_no_status(sle)
|
||||||
|
|
||||||
|
def update_serial_no_status(self, sle):
|
||||||
|
from erpnext.stock.serial_batch_bundle import get_serial_nos
|
||||||
|
|
||||||
|
serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
|
||||||
|
if not serial_nos:
|
||||||
|
return
|
||||||
|
|
||||||
|
warehouse = None
|
||||||
|
status = "Inactive"
|
||||||
|
|
||||||
|
if sle.actual_qty > 0:
|
||||||
|
warehouse = sle.warehouse
|
||||||
|
status = "Active"
|
||||||
|
|
||||||
|
sn_table = frappe.qb.DocType("Serial No")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.update(sn_table)
|
||||||
|
.set(sn_table.warehouse, warehouse)
|
||||||
|
.set(sn_table.status, status)
|
||||||
|
.where(sn_table.name.isin(serial_nos))
|
||||||
|
)
|
||||||
|
|
||||||
|
query.run()
|
||||||
|
|
||||||
def calculate_valuation_for_serial_batch_bundle(self, sle):
|
def calculate_valuation_for_serial_batch_bundle(self, sle):
|
||||||
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||||
|
|
||||||
@ -1171,11 +1265,12 @@ class update_entries_after(object):
|
|||||||
outgoing_rate = get_batch_incoming_rate(
|
outgoing_rate = get_batch_incoming_rate(
|
||||||
item_code=sle.item_code,
|
item_code=sle.item_code,
|
||||||
warehouse=sle.warehouse,
|
warehouse=sle.warehouse,
|
||||||
serial_and_batch_bundle=sle.serial_and_batch_bundle,
|
batch_no=sle.batch_no,
|
||||||
posting_date=sle.posting_date,
|
posting_date=sle.posting_date,
|
||||||
posting_time=sle.posting_time,
|
posting_time=sle.posting_time,
|
||||||
creation=sle.creation,
|
creation=sle.creation,
|
||||||
)
|
)
|
||||||
|
|
||||||
if outgoing_rate is None:
|
if outgoing_rate is None:
|
||||||
# This can *only* happen if qty available for the batch is zero.
|
# This can *only* happen if qty available for the batch is zero.
|
||||||
# in such case fall back various other rates.
|
# in such case fall back various other rates.
|
||||||
@ -1449,11 +1544,10 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_batch_incoming_rate(
|
def get_batch_incoming_rate(
|
||||||
item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
|
item_code, warehouse, batch_no, posting_date, posting_time, creation=None
|
||||||
):
|
):
|
||||||
|
|
||||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
|
|
||||||
|
|
||||||
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
|
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
|
||||||
posting_date, posting_time
|
posting_date, posting_time
|
||||||
@ -1464,28 +1558,13 @@ def get_batch_incoming_rate(
|
|||||||
== CombineDatetime(posting_date, posting_time)
|
== CombineDatetime(posting_date, posting_time)
|
||||||
) & (sle.creation < creation)
|
) & (sle.creation < creation)
|
||||||
|
|
||||||
batches = frappe.get_all(
|
|
||||||
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
|
|
||||||
)
|
|
||||||
|
|
||||||
batch_details = (
|
batch_details = (
|
||||||
frappe.qb.from_(sle)
|
frappe.qb.from_(sle)
|
||||||
.inner_join(batch_ledger)
|
.select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
|
||||||
.on(sle.serial_and_batch_bundle == batch_ledger.parent)
|
|
||||||
.select(
|
|
||||||
Sum(
|
|
||||||
Case()
|
|
||||||
.when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
|
|
||||||
.else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
|
|
||||||
).as_("batch_value"),
|
|
||||||
Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
|
|
||||||
"batch_qty"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.where(
|
.where(
|
||||||
(sle.item_code == item_code)
|
(sle.item_code == item_code)
|
||||||
& (sle.warehouse == warehouse)
|
& (sle.warehouse == warehouse)
|
||||||
& (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
|
& (sle.batch_no == batch_no)
|
||||||
& (sle.is_cancelled == 0)
|
& (sle.is_cancelled == 0)
|
||||||
)
|
)
|
||||||
.where(timestamp_condition)
|
.where(timestamp_condition)
|
||||||
|
@ -8,26 +8,29 @@
|
|||||||
"allow_print": 0,
|
"allow_print": 0,
|
||||||
"amount": 0.0,
|
"amount": 0.0,
|
||||||
"amount_based_on_field": 0,
|
"amount_based_on_field": 0,
|
||||||
|
"anonymous": 0,
|
||||||
|
"apply_document_permissions": 1,
|
||||||
|
"condition_json": "[]",
|
||||||
"creation": "2016-06-24 15:50:33.196990",
|
"creation": "2016-06-24 15:50:33.196990",
|
||||||
"doc_type": "Address",
|
"doc_type": "Address",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Web Form",
|
"doctype": "Web Form",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
|
"list_columns": [],
|
||||||
|
"list_title": "",
|
||||||
"login_required": 1,
|
"login_required": 1,
|
||||||
"max_attachment_size": 0,
|
"max_attachment_size": 0,
|
||||||
"modified": "2019-10-15 06:55:30.405119",
|
"modified": "2024-01-24 10:28:35.026064",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "rohitw1991@gmail.com",
|
||||||
"module": "Utilities",
|
"module": "Utilities",
|
||||||
"name": "addresses",
|
"name": "addresses",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"published": 1,
|
"published": 1,
|
||||||
"route": "address",
|
"route": "address",
|
||||||
"route_to_success_link": 0,
|
|
||||||
"show_attachments": 0,
|
"show_attachments": 0,
|
||||||
"show_in_grid": 0,
|
"show_list": 1,
|
||||||
"show_sidebar": 0,
|
"show_sidebar": 0,
|
||||||
"sidebar_items": [],
|
|
||||||
"success_url": "/addresses",
|
"success_url": "/addresses",
|
||||||
"title": "Address",
|
"title": "Address",
|
||||||
"web_form_fields": [
|
"web_form_fields": [
|
||||||
|
Loading…
x
Reference in New Issue
Block a user