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:
|
||||
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):
|
||||
self.validate_cheque_info()
|
||||
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) {
|
||||
const today = frappe.datetime.get_today();
|
||||
const fields = [
|
||||
let fields = [
|
||||
{fieldtype:"Section Break", label: __("Posting Date")},
|
||||
{fieldtype:"Date", label: __("From Date"),
|
||||
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},
|
||||
{fieldtype:"Column Break"},
|
||||
{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() {
|
||||
return {
|
||||
"filters": {"company": frm.doc.company}
|
||||
}
|
||||
];
|
||||
|
||||
if (frm.dimension_filters) {
|
||||
let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2);
|
||||
|
||||
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:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||
];
|
||||
]);
|
||||
|
||||
let btn_text = "";
|
||||
|
||||
|
@ -87,12 +87,14 @@
|
||||
"status",
|
||||
"custom_remarks",
|
||||
"remarks",
|
||||
"base_in_words",
|
||||
"column_break_16",
|
||||
"letter_head",
|
||||
"print_heading",
|
||||
"bank",
|
||||
"bank_account_no",
|
||||
"payment_order",
|
||||
"in_words",
|
||||
"subscription_section",
|
||||
"auto_repeat",
|
||||
"amended_from",
|
||||
@ -746,6 +748,20 @@
|
||||
"hidden": 1,
|
||||
"label": "Book Advance Payments in Separate Party Account",
|
||||
"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,
|
||||
@ -759,7 +775,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:07:20.887885",
|
||||
"modified": "2024-01-03 12:46:41.759121",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
@ -13,6 +13,7 @@ from pypika import Case
|
||||
from pypika.functions import Coalesce, Sum
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.bank_account.bank_account import (
|
||||
get_bank_account_details,
|
||||
get_party_bank_account,
|
||||
@ -95,6 +96,7 @@ class PaymentEntry(AccountsController):
|
||||
self.validate_paid_invoices()
|
||||
self.ensure_supplier_is_not_blocked()
|
||||
self.set_status()
|
||||
self.set_total_in_words()
|
||||
|
||||
def on_submit(self):
|
||||
if self.difference_amount:
|
||||
@ -107,7 +109,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def set_liability_account(self):
|
||||
# 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
|
||||
|
||||
if not frappe.db.get_value(
|
||||
@ -702,6 +704,21 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
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):
|
||||
if not self.party_type == "Supplier":
|
||||
return
|
||||
@ -1588,6 +1605,13 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
condition += " and cost_center='%s'" % 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 = {
|
||||
"posting_date": ["from_posting_date", "to_posting_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:
|
||||
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:
|
||||
grand_total_field = "base_grand_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.trigger("set_query_for_dimension_filters");
|
||||
|
||||
// check for any running reconciliation jobs
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
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() {
|
||||
this.frm.set_value('party', '');
|
||||
|
@ -25,7 +25,9 @@
|
||||
"invoice_limit",
|
||||
"payment_limit",
|
||||
"bank_cash_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
@ -208,6 +210,18 @@
|
||||
"fieldname": "payment_name",
|
||||
"fieldtype": "Data",
|
||||
"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,
|
||||
@ -215,7 +229,7 @@
|
||||
"is_virtual": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-17 17:33:55.701726",
|
||||
"modified": "2023-12-14 13:38:16.264013",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"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
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_any_doc_running,
|
||||
)
|
||||
@ -70,6 +71,7 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions = []
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
self.dimensions = get_dimensions()[0]
|
||||
|
||||
def load_from_db(self):
|
||||
# 'modified' attribute is required for `run_doc_method` to work properly.
|
||||
@ -172,6 +174,14 @@ class PaymentReconciliation(Document):
|
||||
if 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(
|
||||
self.party_type,
|
||||
self.party,
|
||||
@ -185,66 +195,67 @@ class PaymentReconciliation(Document):
|
||||
return payment_entries
|
||||
|
||||
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:
|
||||
condition += f" and t1.name like '%%{self.payment_name}%%'"
|
||||
conditions.append(je.name.like(f"%%{self.payment_name}%%"))
|
||||
|
||||
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 = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
conditions.append(jea[dr_or_cr].gt(0))
|
||||
|
||||
bank_account_condition = (
|
||||
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
|
||||
if self.bank_cash_account:
|
||||
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 = 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,
|
||||
)
|
||||
journal_entries = journal_query.run(as_dict=True)
|
||||
|
||||
return list(journal_entries)
|
||||
|
||||
@ -298,6 +309,7 @@ class PaymentReconciliation(Document):
|
||||
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,
|
||||
get_payments=True,
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
)
|
||||
|
||||
for inv in return_outstanding:
|
||||
@ -446,8 +458,15 @@ class PaymentReconciliation(Document):
|
||||
row = self.append("allocation", {})
|
||||
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):
|
||||
return frappe._dict(
|
||||
res = frappe._dict(
|
||||
{
|
||||
"reference_type": pay.get("reference_type"),
|
||||
"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):
|
||||
adjust_allocations_for_taxes(self)
|
||||
dr_or_cr = (
|
||||
@ -485,10 +507,10 @@ class PaymentReconciliation(Document):
|
||||
reconciled_entry.append(payment_details)
|
||||
|
||||
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:
|
||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
|
||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reconcile(self):
|
||||
@ -517,7 +539,7 @@ class PaymentReconciliation(Document):
|
||||
self.get_unreconciled_entries()
|
||||
|
||||
def get_payment_details(self, row, dr_or_cr):
|
||||
return frappe._dict(
|
||||
payment_details = frappe._dict(
|
||||
{
|
||||
"voucher_type": row.get("reference_type"),
|
||||
"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):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
@ -646,6 +674,13 @@ class PaymentReconciliation(Document):
|
||||
if not invoices_to_reconcile:
|
||||
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):
|
||||
self.common_filter_conditions.clear()
|
||||
self.accounting_dimension_filter_conditions.clear()
|
||||
@ -669,40 +704,30 @@ class PaymentReconciliation(Document):
|
||||
if 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):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
self.build_dimensions_filter_conditions()
|
||||
|
||||
if self.get("cost_center") and get_payments:
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
def get_journal_filter_conditions(self):
|
||||
conditions = []
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
conditions.append(je.company == self.company)
|
||||
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
if self.from_payment_date:
|
||||
conditions.append(je.posting_date.gte(self.from_payment_date))
|
||||
|
||||
if self.to_payment_date:
|
||||
conditions.append(je.posting_date.lte(self.to_payment_date))
|
||||
|
||||
if self.minimum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
|
||||
)
|
||||
conditions.append(je.total_debit.gte(self.minimum_payment_amount))
|
||||
|
||||
if self.maximum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
|
||||
)
|
||||
conditions.append(je.total_debit.lte(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:
|
||||
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_exchange_rate = True
|
||||
jv.remark = None
|
||||
@ -785,9 +819,27 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
inv.against_voucher,
|
||||
None,
|
||||
inv.cost_center,
|
||||
dimensions_dict,
|
||||
)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def adjust_allocations_for_taxes(doc):
|
||||
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",
|
||||
"exchange_rate",
|
||||
"currency",
|
||||
"cost_center"
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -151,12 +153,26 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "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,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-17 17:33:38.612615",
|
||||
"modified": "2023-12-14 13:38:26.104150",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"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.sales_invoice.sales_invoice import (
|
||||
SalesInvoice,
|
||||
get_bank_cash_account,
|
||||
get_mode_of_payment_info,
|
||||
update_multi_mode_option,
|
||||
)
|
||||
@ -208,7 +207,6 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_stock_availablility()
|
||||
self.validate_return_items_qty()
|
||||
self.set_status()
|
||||
self.set_account_for_mode_of_payment()
|
||||
self.validate_pos()
|
||||
self.validate_payment_amount()
|
||||
self.validate_loyalty_transaction()
|
||||
@ -643,11 +641,6 @@ class POSInvoice(SalesInvoice):
|
||||
update_multi_mode_option(self, pos_profile)
|
||||
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()
|
||||
def create_payment_request(self):
|
||||
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")
|
||||
|
||||
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):
|
||||
pi = make_purchase_invoice(item_name="_Test Item", qty=10, do_not_submit=True)
|
||||
pi.items[0].item_code = ""
|
||||
|
@ -420,7 +420,8 @@ class SalesInvoice(SellingController):
|
||||
self.calculate_taxes_and_totals()
|
||||
|
||||
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):
|
||||
self.validate_pos_paid_amount()
|
||||
@ -706,9 +707,6 @@ class SalesInvoice(SellingController):
|
||||
):
|
||||
data.sales_invoice = sales_invoice
|
||||
|
||||
def on_update(self):
|
||||
self.set_paid_amount()
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if hasattr(self, "repost_required"):
|
||||
fields_to_check = [
|
||||
@ -739,6 +737,11 @@ class SalesInvoice(SellingController):
|
||||
self.paid_amount = 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):
|
||||
for data in self.timesheets:
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]:
|
||||
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", 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):
|
||||
create_asset_data()
|
||||
asset = create_asset(item_code="Macbook Pro")
|
||||
|
@ -5,7 +5,7 @@
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
from frappe import _, qb, query_builder, scrub
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Date, Substring, Sum
|
||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||
@ -576,6 +576,8 @@ class ReceivablePayableReport(object):
|
||||
def get_future_payments_from_payment_entry(self):
|
||||
pe = frappe.qb.DocType("Payment Entry")
|
||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
return (
|
||||
frappe.qb.from_(pe)
|
||||
.inner_join(pe_ref)
|
||||
@ -587,6 +589,11 @@ class ReceivablePayableReport(object):
|
||||
(pe.posting_date).as_("future_date"),
|
||||
(pe_ref.allocated_amount).as_("future_amount"),
|
||||
(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(
|
||||
(pe.docstatus < 2)
|
||||
@ -623,13 +630,24 @@ class ReceivablePayableReport(object):
|
||||
query = query.select(
|
||||
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:
|
||||
query = query.select(
|
||||
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:
|
||||
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)
|
||||
@ -645,14 +663,19 @@ class ReceivablePayableReport(object):
|
||||
row.remaining_balance = row.outstanding
|
||||
row.future_amount = 0.0
|
||||
for future in self.future_payments.get((row.voucher_no, row.party), []):
|
||||
if row.remaining_balance > 0 and future.future_amount:
|
||||
if future.future_amount > row.outstanding:
|
||||
if self.filters.in_party_currency:
|
||||
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
|
||||
future.future_amount = future.future_amount - row.outstanding
|
||||
future[future_amount_field] = future.get(future_amount_field) - row.outstanding
|
||||
row.remaining_balance = 0
|
||||
else:
|
||||
row.future_amount += future.future_amount
|
||||
future.future_amount = 0
|
||||
row.future_amount += future.get(future_amount_field)
|
||||
future[future_amount_field] = 0
|
||||
row.remaining_balance = row.outstanding - row.future_amount
|
||||
|
||||
row.setdefault("future_ref", []).append(
|
||||
|
@ -772,3 +772,92 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
||||
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
||||
report_output = sorted(report_output, key=lambda x: x[0])
|
||||
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);
|
||||
|
||||
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({
|
||||
fieldname: "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);
|
||||
|
||||
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({
|
||||
fieldname: "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),))
|
||||
|
||||
if account:
|
||||
|
||||
if not (frappe.flags.ignore_account_permission or ignore_account_permission):
|
||||
acc.check_permission("read")
|
||||
|
||||
@ -286,18 +285,22 @@ def get_balance_on(
|
||||
cond.append("""gle.company = %s """ % (frappe.db.escape(company, percent=False)))
|
||||
|
||||
if account or (party_type and party) or account_type:
|
||||
|
||||
precision = get_currency_precision()
|
||||
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:
|
||||
select_field = "sum(debit) - sum(credit)"
|
||||
select_field = "sum(round(debit, %s)) - sum(round(credit, %s))"
|
||||
|
||||
bal = frappe.db.sql(
|
||||
"""
|
||||
SELECT {0}
|
||||
FROM `tabGL Entry` gle
|
||||
WHERE {1}""".format(
|
||||
select_field, " and ".join(cond)
|
||||
)
|
||||
),
|
||||
(precision, precision),
|
||||
)[0][0]
|
||||
# if bal is None, return 0
|
||||
return flt(bal)
|
||||
@ -453,7 +456,19 @@ def add_cc(args=None):
|
||||
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
|
||||
"""
|
||||
@ -482,6 +497,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
|
||||
check_if_advance_entry_modified(entry)
|
||||
validate_allocated_amount(entry)
|
||||
|
||||
dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions)
|
||||
|
||||
# update ref in advance entry
|
||||
if voucher_type == "Journal Entry":
|
||||
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
|
||||
# referenced_row is used to deduplicate gain/loss journal
|
||||
entry.update({"referenced_row": referenced_row})
|
||||
doc.make_exchange_gain_loss_journal([entry])
|
||||
doc.make_exchange_gain_loss_journal([entry], dimensions_dict)
|
||||
else:
|
||||
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)
|
||||
@ -649,7 +670,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
|
||||
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_doctype": d.against_voucher_type,
|
||||
@ -662,6 +683,7 @@ def update_reference_in_payment_entry(
|
||||
else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.difference_amount,
|
||||
"account": d.account,
|
||||
"dimensions": d.dimensions,
|
||||
}
|
||||
|
||||
if d.voucher_detail_no:
|
||||
@ -694,7 +716,9 @@ def update_reference_in_payment_entry(
|
||||
if not skip_ref_details_update_for_pe:
|
||||
payment_entry.set_missing_ref_details()
|
||||
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:
|
||||
payment_entry.save(ignore_permissions=True)
|
||||
@ -2031,6 +2055,7 @@ def create_gain_loss_journal(
|
||||
ref2_dn,
|
||||
ref2_detail_no,
|
||||
cost_center,
|
||||
dimensions,
|
||||
) -> str:
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
@ -2064,7 +2089,8 @@ def create_gain_loss_journal(
|
||||
dr_or_cr + "_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
if dimensions:
|
||||
journal_account.update(dimensions)
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_account = frappe._dict(
|
||||
@ -2080,7 +2106,8 @@ def create_gain_loss_journal(
|
||||
reverse_dr_or_cr: abs(exc_gain_loss),
|
||||
}
|
||||
)
|
||||
|
||||
if dimensions:
|
||||
journal_account.update(dimensions)
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_entry.save()
|
||||
|
@ -571,16 +571,16 @@ frappe.ui.form.on('Asset', {
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
var is_grouped_asset = frappe.db.get_value('Item', item.item_code, 'is_grouped_asset');
|
||||
var asset_quantity = is_grouped_asset ? item.qty : 1;
|
||||
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); }
|
||||
frappe.db.get_value('Item', item.item_code, 'is_grouped_asset', (r) => {
|
||||
var asset_quantity = r.is_grouped_asset ? item.qty : 1;
|
||||
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); }
|
||||
});
|
||||
},
|
||||
|
||||
set_depreciation_rate: function(frm, row) {
|
||||
|
@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, bold, qb, throw
|
||||
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.functions import Abs, Sum
|
||||
from frappe.utils import (
|
||||
@ -28,6 +29,7 @@ from frappe.utils import (
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
apply_pricing_rule_for_free_items,
|
||||
@ -200,6 +202,7 @@ class AccountsController(TransactionBase):
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
self.validate_party_account_currency()
|
||||
self.validate_return_against_account()
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||
if invalid_advances := [
|
||||
@ -348,6 +351,20 @@ class AccountsController(TransactionBase):
|
||||
for bundle in bundles:
|
||||
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):
|
||||
field_map = {
|
||||
"Sales Invoice": "deferred_revenue_account",
|
||||
@ -1246,7 +1263,9 @@ class AccountsController(TransactionBase):
|
||||
return True
|
||||
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
|
||||
"""
|
||||
@ -1299,6 +1318,7 @@ class AccountsController(TransactionBase):
|
||||
self.name,
|
||||
arg.get("referenced_row"),
|
||||
arg.get("cost_center"),
|
||||
dimensions_dict,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
@ -1379,6 +1399,7 @@ class AccountsController(TransactionBase):
|
||||
self.name,
|
||||
d.idx,
|
||||
self.cost_center,
|
||||
dimensions_dict,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
@ -1443,7 +1464,13 @@ class AccountsController(TransactionBase):
|
||||
if lst:
|
||||
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):
|
||||
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"))
|
||||
|
||||
if condition:
|
||||
if condition.get("name", None):
|
||||
q = q.where(payment_entry.name.like(f"%{condition.get('name')}%"))
|
||||
# conditions should be built as an array and passed as Criterion
|
||||
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:
|
||||
q = (
|
||||
q.where(payment_entry.cost_center == condition["cost_center"])
|
||||
if condition.get("cost_center")
|
||||
else q
|
||||
)
|
||||
q = (
|
||||
q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"])
|
||||
if condition.get("minimum_payment_amount")
|
||||
else q
|
||||
)
|
||||
q = (
|
||||
q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"])
|
||||
if condition.get("maximum_payment_amount")
|
||||
else q
|
||||
)
|
||||
else:
|
||||
q = (
|
||||
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
|
||||
)
|
||||
if condition.get("cost_center"):
|
||||
common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"])
|
||||
|
||||
if condition.get("accounting_dimensions"):
|
||||
for field, val in condition.get("accounting_dimensions").items():
|
||||
common_filter_conditions.append(payment_entry[field] == val)
|
||||
|
||||
if condition.get("minimum_payment_amount"):
|
||||
common_filter_conditions.append(
|
||||
payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"])
|
||||
)
|
||||
|
||||
if condition.get("maximum_payment_amount"):
|
||||
common_filter_conditions.append(
|
||||
payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"])
|
||||
)
|
||||
q = q.where(Criterion.all(common_filter_conditions))
|
||||
|
||||
q = q.orderby(payment_entry.posting_date)
|
||||
q = q.limit(limit) if limit else q
|
||||
|
@ -91,7 +91,8 @@ status_map = {
|
||||
],
|
||||
"Purchase Receipt": [
|
||||
["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"],
|
||||
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
|
||||
["Cancelled", "eval:self.docstatus==2"],
|
||||
|
@ -6,7 +6,7 @@ from collections import defaultdict
|
||||
from typing import List, Tuple
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, bold
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
@ -697,6 +697,9 @@ class StockController(AccountsController):
|
||||
self.validate_in_transit_warehouses()
|
||||
self.validate_multi_currency()
|
||||
self.validate_packed_items()
|
||||
|
||||
if self.get("is_internal_supplier"):
|
||||
self.validate_internal_transfer_qty()
|
||||
else:
|
||||
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"):
|
||||
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):
|
||||
# if over receipt is attempted while 'apply putaway rule' is disabled
|
||||
# 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)
|
||||
|
||||
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(
|
||||
self.subcontract_data.receipt_supplied_items_field,
|
||||
fields=[
|
||||
"serial_no",
|
||||
"rm_item_code",
|
||||
"reference_name",
|
||||
"serial_and_batch_bundle",
|
||||
"batch_no",
|
||||
"consumed_qty",
|
||||
"main_item_code",
|
||||
"parent as voucher_no",
|
||||
],
|
||||
fields=fields,
|
||||
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
|
||||
)
|
||||
|
||||
|
@ -56,6 +56,7 @@ class TestAccountsController(FrappeTestCase):
|
||||
20 series - Sales Invoice against Journals
|
||||
30 series - Sales Invoice against Credit Notes
|
||||
40 series - Company default Cost center is unset
|
||||
50 series - Dimension inheritence
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
@ -1188,3 +1189,214 @@ class TestAccountsController(FrappeTestCase):
|
||||
)
|
||||
|
||||
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",
|
||||
"Supplier Quotation",
|
||||
"Supplier Quotation Item",
|
||||
"Payment Reconciliation",
|
||||
"Payment Reconciliation Allocation",
|
||||
]
|
||||
|
||||
get_matching_queries = (
|
||||
|
@ -176,8 +176,10 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
def autoname(self):
|
||||
# ignore amended documents while calculating current index
|
||||
|
||||
search_key = f"{self.doctype}-{self.item}%"
|
||||
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:
|
||||
|
@ -101,6 +101,7 @@ frappe.ui.form.on("BOM Creator", {
|
||||
}
|
||||
})
|
||||
|
||||
dialog.fields_dict.item_code.get_query = "erpnext.controllers.queries.item_query";
|
||||
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) {
|
||||
@ -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) {
|
||||
me.frm.set_value('plc_conversion_rate', 1.0);
|
||||
}
|
||||
if (company_doc && company_doc.default_letter_head) {
|
||||
if(me.frm.fields_dict.letter_head) {
|
||||
me.frm.set_value("letter_head", company_doc.default_letter_head);
|
||||
if (company_doc){
|
||||
if (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([
|
||||
() => me.frm.script_manager.trigger("currency"),
|
||||
() => me.update_item_tax_map(),
|
||||
|
@ -2,7 +2,58 @@ frappe.provide("erpnext.financial_statements");
|
||||
|
||||
erpnext.financial_statements = {
|
||||
"filters": get_filters(),
|
||||
"baseData": null,
|
||||
"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") {
|
||||
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() {
|
||||
var filters = report.get_values();
|
||||
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
|
||||
});
|
||||
report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
|
||||
var filters = report.get_values();
|
||||
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
|
||||
});
|
||||
|
||||
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
|
||||
var filters = report.get_values();
|
||||
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
|
||||
});
|
||||
report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
|
||||
var filters = report.get_values();
|
||||
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
|
||||
});
|
||||
|
||||
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
|
||||
var filters = report.get_values();
|
||||
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
|
||||
});
|
||||
report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
|
||||
var filters = report.get_values();
|
||||
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -25,6 +25,10 @@ erpnext.accounts.dimensions = {
|
||||
},
|
||||
|
||||
setup_filters(frm, doctype) {
|
||||
if (doctype == 'Payment Entry' && this.accounting_dimensions) {
|
||||
frm.dimension_filters = this.accounting_dimensions
|
||||
}
|
||||
|
||||
if (this.accounting_dimensions) {
|
||||
this.accounting_dimensions.forEach((dimension) => {
|
||||
frappe.model.with_doctype(dimension['document_type'], () => {
|
||||
|
@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
let warehouse = this.item?.type_of_transaction === "Outward" ?
|
||||
(this.item.warehouse || this.item.s_warehouse) : "";
|
||||
|
||||
if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') {
|
||||
warehouse = this.get_warehouse();
|
||||
}
|
||||
|
||||
return {
|
||||
'item_code': this.item.item_code,
|
||||
'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)
|
||||
target.qty = balance_qty if balance_qty > 0 else 0
|
||||
target.stock_qty = flt(target.qty) * flt(obj.conversion_factor)
|
||||
target.delivery_date = nowdate()
|
||||
|
||||
if 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.customer, "_Test Customer")
|
||||
|
||||
sales_order.delivery_date = "2014-01-01"
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
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.customer, "_Test Customer")
|
||||
|
||||
sales_order.delivery_date = "2014-01-01"
|
||||
sales_order.naming_series = "_T-Quotation-"
|
||||
sales_order.transaction_date = nowdate()
|
||||
sales_order.insert()
|
||||
|
@ -118,6 +118,7 @@
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Item",
|
||||
"print_width": "150px",
|
||||
"reqd": 1,
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
@ -908,7 +909,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-24 13:24:55.756320",
|
||||
"modified": "2024-01-25 14:24:00.330219",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
@ -43,7 +43,8 @@ class SalesOrderItem(Document):
|
||||
gross_profit: DF.Currency
|
||||
image: DF.Attach | None
|
||||
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_name: DF.Data
|
||||
item_tax_rate: DF.Code | None
|
||||
|
@ -210,7 +210,6 @@ def get_so_with_invoices(filters):
|
||||
.where(
|
||||
(so.docstatus == 1)
|
||||
& (so.status.isin(["To Deliver and Bill", "To Bill"]))
|
||||
& (so.payment_terms_template != "NULL")
|
||||
& (so.company == conditions.company)
|
||||
& (so.transaction_date[conditions.start_date : conditions.end_date])
|
||||
)
|
||||
|
@ -52,7 +52,7 @@ frappe.ui.form.on('Batch', {
|
||||
// sort by qty
|
||||
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
|
||||
(r.message || []).forEach(function(d) {
|
||||
@ -76,7 +76,7 @@ frappe.ui.form.on('Batch', {
|
||||
|
||||
// move - ask for target warehouse and make stock entry
|
||||
rows.find('.btn-move').on('click', function() {
|
||||
var $btn = $(this);
|
||||
const $btn = $(this);
|
||||
const fields = [
|
||||
{
|
||||
fieldname: 'to_warehouse',
|
||||
@ -115,7 +115,7 @@ frappe.ui.form.on('Batch', {
|
||||
// split - ask for new qty and batch ID (optional)
|
||||
// and make stock entry via batch.batch_split
|
||||
rows.find('.btn-split').on('click', function() {
|
||||
var $btn = $(this);
|
||||
const $btn = $(this);
|
||||
frappe.prompt([{
|
||||
fieldname: 'qty',
|
||||
label: __('New Batch Qty'),
|
||||
@ -128,19 +128,16 @@ frappe.ui.form.on('Batch', {
|
||||
fieldtype: 'Data',
|
||||
}],
|
||||
(data) => {
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.doctype.batch.batch.split_batch',
|
||||
args: {
|
||||
frappe.xcall(
|
||||
'erpnext.stock.doctype.batch.batch.split_batch',
|
||||
{
|
||||
item_code: frm.doc.item,
|
||||
batch_no: frm.doc.name,
|
||||
qty: data.qty,
|
||||
warehouse: $btn.attr('data-warehouse'),
|
||||
new_batch_id: data.new_batch_id
|
||||
},
|
||||
callback: (r) => {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
).then(() => frm.reload_doc());
|
||||
},
|
||||
__('Split Batch'),
|
||||
__('Split')
|
||||
|
@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname, revert_series_if_last
|
||||
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.jinja import render_template
|
||||
|
||||
@ -248,8 +248,9 @@ def get_batches_by_oldest(item_code, warehouse):
|
||||
|
||||
|
||||
@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"""
|
||||
batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert()
|
||||
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")
|
||||
|
||||
from_bundle_id = make_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"batches": frappe._dict({batch_no: qty}),
|
||||
"company": company,
|
||||
"type_of_transaction": "Outward",
|
||||
"qty": qty,
|
||||
}
|
||||
)
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
batches=frappe._dict({batch_no: qty}),
|
||||
company=company,
|
||||
type_of_transaction="Outward",
|
||||
qty=qty,
|
||||
)
|
||||
|
||||
to_bundle_id = make_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"batches": frappe._dict({batch.name: qty}),
|
||||
"company": company,
|
||||
"type_of_transaction": "Inward",
|
||||
"qty": qty,
|
||||
}
|
||||
)
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
batches=frappe._dict({batch.name: qty}),
|
||||
company=company,
|
||||
type_of_transaction="Inward",
|
||||
qty=qty,
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
return (
|
||||
SerialBatchCreation(
|
||||
{
|
||||
"item_code": kwargs.item_code,
|
||||
"warehouse": kwargs.warehouse,
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": today(),
|
||||
"posting_time": nowtime(),
|
||||
"voucher_type": "Stock Entry",
|
||||
"qty": flt(kwargs.qty),
|
||||
"type_of_transaction": kwargs.type_of_transaction,
|
||||
"company": kwargs.company,
|
||||
"batches": kwargs.batches,
|
||||
"qty": qty,
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"company": company,
|
||||
"batches": batches,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
)
|
||||
|
@ -8,6 +8,7 @@ def get_data():
|
||||
"Stock Entry": "delivery_note_no",
|
||||
"Quality Inspection": "reference_name",
|
||||
"Auto Repeat": "reference_document",
|
||||
"Purchase Receipt": "inter_company_reference",
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "against_sales_order"],
|
||||
@ -22,6 +23,9 @@ def get_data():
|
||||
{"label": _("Reference"), "items": ["Sales Order", "Shipment", "Quality Inspection"]},
|
||||
{"label": _("Returns"), "items": ["Stock Entry"]},
|
||||
{"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) {
|
||||
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);
|
||||
},
|
||||
|
||||
|
@ -762,6 +762,62 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
self.assertEqual(mr.per_ordered, 100)
|
||||
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):
|
||||
if not frappe.db.exists("Warehouse Type", "Transit"):
|
||||
|
@ -8,8 +8,10 @@ frappe.listview_settings['Purchase Receipt'] = {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
} else if (flt(doc.per_returned, 2) === 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"];
|
||||
} 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) {
|
||||
return [__("Completed"), "green", "per_billed,=,100"];
|
||||
}
|
||||
|
@ -704,7 +704,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr2.load_from_db()
|
||||
self.assertEqual(pr2.get("items")[0].billed_amt, 2000)
|
||||
self.assertEqual(pr2.per_billed, 80)
|
||||
self.assertEqual(pr2.status, "To Bill")
|
||||
self.assertEqual(pr2.status, "Partly Billed")
|
||||
|
||||
pr2.cancel()
|
||||
pi2.reload()
|
||||
@ -1115,7 +1115,7 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pi.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)
|
||||
|
||||
def test_purchase_receipt_with_exchange_rate_difference(self):
|
||||
@ -1638,9 +1638,10 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
make_stock_entry(
|
||||
purpose="Material Receipt",
|
||||
item_code=item.name,
|
||||
qty=15,
|
||||
qty=20,
|
||||
company=company,
|
||||
to_warehouse=from_warehouse,
|
||||
posting_date=add_days(today(), -3),
|
||||
)
|
||||
|
||||
# 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
|
||||
|
||||
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].from_warehouse = target_warehouse
|
||||
pr.items[0].warehouse = to_warehouse
|
||||
pr.items[0].rejected_warehouse = from_warehouse
|
||||
pr.save()
|
||||
|
||||
self.assertRaises(OverAllowanceError, pr.submit)
|
||||
self.assertRaises(frappe.ValidationError, pr.submit)
|
||||
|
||||
# Step 5: Test Over Receipt Allowance
|
||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
|
||||
@ -1681,8 +1684,10 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
company=company,
|
||||
from_warehouse=from_warehouse,
|
||||
to_warehouse=target_warehouse,
|
||||
posting_date=add_days(pr.posting_date, -1),
|
||||
)
|
||||
|
||||
pr.reload()
|
||||
pr.submit()
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
||||
|
@ -250,6 +250,7 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
for d in self.entries:
|
||||
available_qty = 0
|
||||
|
||||
if self.has_serial_no:
|
||||
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
|
||||
else:
|
||||
@ -892,6 +893,13 @@ class SerialandBatchBundle(Document):
|
||||
elif 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()
|
||||
def download_blank_csv_template(content):
|
||||
@ -1374,10 +1382,12 @@ def get_available_serial_nos(kwargs):
|
||||
elif kwargs.based_on == "Expiry":
|
||||
order_by = "amc_expiry_date asc"
|
||||
|
||||
filters = {"item_code": kwargs.item_code, "warehouse": ("is", "set")}
|
||||
filters = {"item_code": kwargs.item_code}
|
||||
|
||||
if kwargs.warehouse:
|
||||
filters["warehouse"] = kwargs.warehouse
|
||||
if not kwargs.get("ignore_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.
|
||||
ignore_serial_nos = get_reserved_serial_nos(kwargs)
|
||||
|
@ -228,7 +228,6 @@ class StockEntry(StockController):
|
||||
self.fg_completed_qty = 0.0
|
||||
|
||||
self.validate_serialized_batch()
|
||||
self.set_actual_qty()
|
||||
self.calculate_rate_and_amount()
|
||||
self.validate_putaway_capacity()
|
||||
|
||||
|
@ -156,6 +156,7 @@ class StockReconciliation(StockController):
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"ignore_warehouse": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -780,7 +781,20 @@ class StockReconciliation(StockController):
|
||||
|
||||
current_qty = 0.0
|
||||
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:
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
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")
|
||||
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||
)
|
||||
if not row.serial_no:
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||
)
|
||||
|
||||
row.current_valuation_rate = val_rate
|
||||
row.current_qty = current_qty
|
||||
@ -842,11 +857,56 @@ class StockReconciliation(StockController):
|
||||
|
||||
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)
|
||||
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:
|
||||
qty = (
|
||||
get_batch_qty(
|
||||
@ -864,7 +924,7 @@ class StockReconciliation(StockController):
|
||||
|
||||
current_qty += qty
|
||||
|
||||
return abs(current_qty)
|
||||
return current_qty
|
||||
|
||||
|
||||
def get_batch_qty_for_stock_reco(
|
||||
|
@ -925,6 +925,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
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):
|
||||
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")
|
||||
|
||||
company_wise_mr = frappe._dict({})
|
||||
for request_type in material_requests:
|
||||
for company in material_requests[request_type]:
|
||||
try:
|
||||
@ -206,17 +207,19 @@ def create_material_request(material_requests):
|
||||
mr.submit()
|
||||
mr_list.append(mr)
|
||||
|
||||
company_wise_mr.setdefault(company, []).append(mr)
|
||||
|
||||
except Exception:
|
||||
_log_exception(mr)
|
||||
|
||||
if mr_list:
|
||||
if company_wise_mr:
|
||||
if getattr(frappe.local, "reorder_email_notify", None) is None:
|
||||
frappe.local.reorder_email_notify = cint(
|
||||
frappe.db.get_value("Stock Settings", None, "reorder_email_notify")
|
||||
)
|
||||
|
||||
if frappe.local.reorder_email_notify:
|
||||
send_email_notification(mr_list)
|
||||
send_email_notification(company_wise_mr)
|
||||
|
||||
if exceptions_list:
|
||||
notify_errors(exceptions_list)
|
||||
@ -224,20 +227,56 @@ def create_material_request(material_requests):
|
||||
return mr_list
|
||||
|
||||
|
||||
def send_email_notification(mr_list):
|
||||
def send_email_notification(company_wise_mr):
|
||||
"""Notify user about auto creation of indent"""
|
||||
|
||||
email_list = frappe.db.sql_list(
|
||||
"""select distinct r.parent
|
||||
from `tabHas Role` r, tabUser p
|
||||
where p.name = r.parent and p.enabled = 1 and p.docstatus < 2
|
||||
and r.role in ('Purchase Manager','Stock Manager')
|
||||
and p.name not in ('Administrator', 'All', 'Guest')"""
|
||||
for company, mr_list in company_wise_mr.items():
|
||||
email_list = get_email_list(company)
|
||||
|
||||
if not email_list:
|
||||
continue
|
||||
|
||||
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):
|
||||
|
@ -22,9 +22,8 @@ def get_columns(filters):
|
||||
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
|
||||
{
|
||||
"label": _("Voucher Type"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "voucher_type",
|
||||
"options": "DocType",
|
||||
"width": 160,
|
||||
},
|
||||
{
|
||||
|
@ -99,7 +99,7 @@ frappe.query_reports["Stock Balance"] = {
|
||||
"fieldname": 'ignore_closing_balance',
|
||||
"label": __('Ignore Closing Balance'),
|
||||
"fieldtype": 'Check',
|
||||
"default": 1
|
||||
"default": 0
|
||||
},
|
||||
],
|
||||
|
||||
|
@ -9,9 +9,18 @@ from typing import Optional, Set, Tuple
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Case
|
||||
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
|
||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||
@ -712,11 +721,10 @@ class update_entries_after(object):
|
||||
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and (
|
||||
sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
|
||||
)
|
||||
and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
|
||||
and sle.voucher_detail_no
|
||||
and not self.args.get("sle_id")
|
||||
and sle.is_cancelled == 0
|
||||
):
|
||||
self.reset_actual_qty_for_stock_reco(sle)
|
||||
|
||||
@ -737,6 +745,23 @@ class update_entries_after(object):
|
||||
|
||||
if sle.serial_and_batch_bundle:
|
||||
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:
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
|
||||
# assert
|
||||
@ -782,6 +807,45 @@ class update_entries_after(object):
|
||||
):
|
||||
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):
|
||||
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
||||
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:
|
||||
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):
|
||||
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(
|
||||
item_code=sle.item_code,
|
||||
warehouse=sle.warehouse,
|
||||
serial_and_batch_bundle=sle.serial_and_batch_bundle,
|
||||
batch_no=sle.batch_no,
|
||||
posting_date=sle.posting_date,
|
||||
posting_time=sle.posting_time,
|
||||
creation=sle.creation,
|
||||
)
|
||||
|
||||
if outgoing_rate is None:
|
||||
# This can *only* happen if qty available for the batch is zero.
|
||||
# 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(
|
||||
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")
|
||||
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
|
||||
posting_date, posting_time
|
||||
@ -1464,28 +1558,13 @@ def get_batch_incoming_rate(
|
||||
== CombineDatetime(posting_date, posting_time)
|
||||
) & (sle.creation < creation)
|
||||
|
||||
batches = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
|
||||
)
|
||||
|
||||
batch_details = (
|
||||
frappe.qb.from_(sle)
|
||||
.inner_join(batch_ledger)
|
||||
.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"
|
||||
),
|
||||
)
|
||||
.select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
|
||||
.where(
|
||||
(sle.item_code == item_code)
|
||||
& (sle.warehouse == warehouse)
|
||||
& (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
|
||||
& (sle.batch_no == batch_no)
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.where(timestamp_condition)
|
||||
|
@ -8,26 +8,29 @@
|
||||
"allow_print": 0,
|
||||
"amount": 0.0,
|
||||
"amount_based_on_field": 0,
|
||||
"anonymous": 0,
|
||||
"apply_document_permissions": 1,
|
||||
"condition_json": "[]",
|
||||
"creation": "2016-06-24 15:50:33.196990",
|
||||
"doc_type": "Address",
|
||||
"docstatus": 0,
|
||||
"doctype": "Web Form",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"list_columns": [],
|
||||
"list_title": "",
|
||||
"login_required": 1,
|
||||
"max_attachment_size": 0,
|
||||
"modified": "2019-10-15 06:55:30.405119",
|
||||
"modified_by": "Administrator",
|
||||
"modified": "2024-01-24 10:28:35.026064",
|
||||
"modified_by": "rohitw1991@gmail.com",
|
||||
"module": "Utilities",
|
||||
"name": "addresses",
|
||||
"owner": "Administrator",
|
||||
"published": 1,
|
||||
"route": "address",
|
||||
"route_to_success_link": 0,
|
||||
"show_attachments": 0,
|
||||
"show_in_grid": 0,
|
||||
"show_list": 1,
|
||||
"show_sidebar": 0,
|
||||
"sidebar_items": [],
|
||||
"success_url": "/addresses",
|
||||
"title": "Address",
|
||||
"web_form_fields": [
|
||||
|
Loading…
x
Reference in New Issue
Block a user