mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-15 12:55:10 +00:00
Merge pull request #39640 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -41,7 +41,7 @@ def test_record_generator():
|
||||
]
|
||||
|
||||
start = 2012
|
||||
end = now_datetime().year + 5
|
||||
end = now_datetime().year + 25
|
||||
for year in range(start, end):
|
||||
test_records.append(
|
||||
{
|
||||
|
||||
@@ -78,6 +78,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()
|
||||
|
||||
@@ -631,7 +631,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)},
|
||||
@@ -646,18 +646,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 = "";
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -1453,6 +1454,13 @@ def get_outstanding_reference_documents(args):
|
||||
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"],
|
||||
@@ -1680,6 +1688,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"
|
||||
|
||||
@@ -705,7 +705,50 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe2.submit()
|
||||
|
||||
# create return entry against si1
|
||||
create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
|
||||
cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
|
||||
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
||||
|
||||
# create JE(credit note) manually against si1 and cr_note
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": si1.company,
|
||||
"voucher_type": "Credit Note",
|
||||
"posting_date": nowdate(),
|
||||
}
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": si1.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si1.customer,
|
||||
"debit": 0,
|
||||
"credit": 100,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si1.doctype,
|
||||
"reference_name": si1.name,
|
||||
"cost_center": si1.items[0].cost_center,
|
||||
},
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": cr_note.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": cr_note.customer,
|
||||
"debit": 100,
|
||||
"credit": 0,
|
||||
"debit_in_account_currency": 100,
|
||||
"credit_in_account_currency": 0,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": cr_note.items[0].cost_center,
|
||||
},
|
||||
)
|
||||
je.save().submit()
|
||||
|
||||
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
||||
self.assertEqual(si1_outstanding, -100)
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
cr_note1.return_against = si3.name
|
||||
cr_note1 = cr_note1.save().submit()
|
||||
|
||||
pl_entries = (
|
||||
pl_entries_si3 = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
@@ -309,7 +309,24 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
pl_entries_cr_note1 = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)
|
||||
)
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values_for_si3 = [
|
||||
{
|
||||
"voucher_type": si3.doctype,
|
||||
"voucher_no": si3.name,
|
||||
@@ -317,18 +334,21 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
"against_voucher_no": si3.name,
|
||||
"amount": amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
}
|
||||
]
|
||||
# credit/debit notes post ledger entries against itself
|
||||
expected_values_for_cr_note1 = [
|
||||
{
|
||||
"voucher_type": cr_note1.doctype,
|
||||
"voucher_no": cr_note1.name,
|
||||
"against_voucher_type": si3.doctype,
|
||||
"against_voucher_no": si3.name,
|
||||
"against_voucher_type": cr_note1.doctype,
|
||||
"against_voucher_no": cr_note1.name,
|
||||
"amount": -amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries[0], expected_values[0])
|
||||
self.assertEqual(pl_entries[1], expected_values[1])
|
||||
self.assertEqual(pl_entries_si3, expected_values_for_si3)
|
||||
self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1)
|
||||
|
||||
def test_je_against_inv_and_note(self):
|
||||
ple = self.ple
|
||||
|
||||
@@ -83,6 +83,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({
|
||||
@@ -113,6 +115,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', '');
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"invoice_limit",
|
||||
"payment_limit",
|
||||
"bank_cash_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
@@ -199,6 +201,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,
|
||||
@@ -206,7 +220,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,
|
||||
)
|
||||
@@ -28,6 +29,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.
|
||||
@@ -110,7 +112,7 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def get_payment_entries(self):
|
||||
order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
|
||||
condition = self.get_conditions(get_payments=True)
|
||||
condition = self.get_payment_entry_conditions()
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
self.party_type,
|
||||
@@ -126,66 +128,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)
|
||||
|
||||
@@ -228,20 +231,18 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||
|
||||
self.get_return_invoices()
|
||||
return_invoices = [
|
||||
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
||||
]
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
if self.return_invoices:
|
||||
ple_query = QueryPaymentLedger()
|
||||
return_outstanding = ple_query.get_voucher_outstandings(
|
||||
vouchers=return_invoices,
|
||||
vouchers=self.return_invoices,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
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:
|
||||
@@ -391,8 +392,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"),
|
||||
@@ -408,6 +416,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 = (
|
||||
@@ -430,10 +441,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):
|
||||
@@ -462,7 +473,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"),
|
||||
@@ -485,6 +496,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):
|
||||
@@ -592,6 +609,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()
|
||||
@@ -615,40 +639,58 @@ 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_payment_entry_conditions(self):
|
||||
conditions = []
|
||||
pe = qb.DocType("Payment Entry")
|
||||
conditions.append(pe.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.get("cost_center"):
|
||||
conditions.append(pe.cost_center == self.cost_center)
|
||||
|
||||
if self.from_payment_date:
|
||||
conditions.append(pe.posting_date.gte(self.from_payment_date))
|
||||
|
||||
if self.to_payment_date:
|
||||
conditions.append(pe.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(pe.unallocated_amount.gte(flt(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(pe.unallocated_amount.lte(flt(self.maximum_payment_amount)))
|
||||
|
||||
return condition
|
||||
# pass dynamic dimension filter values to payment query
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
conditions.append(pe[dimension] == self.get(dimension))
|
||||
|
||||
return conditions
|
||||
|
||||
def get_journal_filter_conditions(self):
|
||||
conditions = []
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
conditions.append(je.company == self.company)
|
||||
|
||||
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:
|
||||
conditions.append(je.total_debit.gte(self.minimum_payment_amount))
|
||||
|
||||
if self.maximum_payment_amount:
|
||||
conditions.append(je.total_debit.lte(self.maximum_payment_amount))
|
||||
|
||||
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"
|
||||
|
||||
@@ -698,6 +740,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.skip_remarks_creation = True
|
||||
jv.flags.ignore_exchange_rate = True
|
||||
@@ -731,9 +782,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
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
"currency",
|
||||
"cost_center"
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -157,12 +159,21 @@
|
||||
"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",
|
||||
|
||||
@@ -13,7 +13,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,
|
||||
)
|
||||
@@ -52,7 +51,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()
|
||||
@@ -584,11 +582,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:
|
||||
|
||||
@@ -99,8 +99,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
||||
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
|
||||
@@ -671,9 +671,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1540,12 +1538,8 @@ class PurchaseInvoice(BuyingController):
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
# Check if outstanding amount is 0 due to debit note issued against invoice
|
||||
elif (
|
||||
outstanding_amount <= 0
|
||||
and self.is_return == 0
|
||||
and frappe.db.get_value(
|
||||
"Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||
)
|
||||
elif self.is_return == 0 and frappe.db.get_value(
|
||||
"Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||
):
|
||||
self.status = "Debit Note Issued"
|
||||
elif self.is_return == 1:
|
||||
|
||||
@@ -1893,6 +1893,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 check_gl_entries(
|
||||
doc,
|
||||
|
||||
@@ -91,8 +91,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
|
||||
if(doc.update_stock) this.show_stock_ledger();
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
||||
&& !(cint(doc.is_return) && doc.return_against)) {
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0) {
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
|
||||
@@ -245,7 +245,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()
|
||||
@@ -538,9 +539,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 = [
|
||||
@@ -571,6 +569,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:
|
||||
@@ -1069,9 +1072,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
@@ -1698,12 +1699,8 @@ class SalesInvoice(SellingController):
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
# Check if outstanding amount is 0 due to credit note issued against invoice
|
||||
elif (
|
||||
outstanding_amount <= 0
|
||||
and self.is_return == 0
|
||||
and frappe.db.get_value(
|
||||
"Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||
)
|
||||
elif self.is_return == 0 and frappe.db.get_value(
|
||||
"Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||
):
|
||||
self.status = "Credit Note Issued"
|
||||
elif self.is_return == 1:
|
||||
@@ -1949,12 +1946,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(
|
||||
|
||||
@@ -1528,8 +1528,21 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(party_credited, 1000)
|
||||
|
||||
# Check outstanding amount
|
||||
self.assertFalse(si1.outstanding_amount)
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
@@ -578,6 +578,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)
|
||||
@@ -589,6 +591,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)
|
||||
@@ -625,13 +632,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)
|
||||
@@ -647,14 +665,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]
|
||||
)
|
||||
|
||||
@@ -434,7 +434,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
|
||||
"""
|
||||
@@ -459,6 +471,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)
|
||||
@@ -466,10 +480,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:
|
||||
update_reference_in_payment_entry(
|
||||
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
|
||||
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,
|
||||
dimensions_dict=dimensions_dict,
|
||||
)
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
@@ -618,7 +636,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,
|
||||
@@ -630,6 +648,8 @@ def update_reference_in_payment_entry(
|
||||
if d.difference_amount is not None
|
||||
else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.difference_amount,
|
||||
"account": d.account,
|
||||
"dimensions": d.dimensions,
|
||||
}
|
||||
|
||||
if d.voucher_detail_no:
|
||||
@@ -661,8 +681,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(
|
||||
frappe._dict({"difference_posting_date": d.difference_posting_date})
|
||||
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
|
||||
)
|
||||
|
||||
if not do_not_save:
|
||||
@@ -1991,6 +2012,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"
|
||||
@@ -2024,7 +2046,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(
|
||||
@@ -2040,7 +2063,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()
|
||||
|
||||
@@ -519,16 +519,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) {
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe import _
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
add_years,
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
@@ -23,6 +24,7 @@ from frappe.utils import (
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_depreciation_accounts,
|
||||
get_disposal_account_and_cost_center,
|
||||
@@ -381,14 +383,24 @@ class Asset(AccountsController):
|
||||
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
|
||||
|
||||
depreciation_amount = 0
|
||||
|
||||
number_of_pending_depreciations = final_number_of_depreciations - start[finance_book.idx - 1]
|
||||
yearly_opening_wdv = value_after_depreciation
|
||||
current_fiscal_year_end_date = None
|
||||
|
||||
for n in range(start[finance_book.idx - 1], final_number_of_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
if skip_row:
|
||||
continue
|
||||
|
||||
schedule_date = add_months(
|
||||
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
if not current_fiscal_year_end_date:
|
||||
current_fiscal_year_end_date = get_fiscal_year(finance_book.depreciation_start_date)[2]
|
||||
elif getdate(schedule_date) > getdate(current_fiscal_year_end_date):
|
||||
current_fiscal_year_end_date = add_years(current_fiscal_year_end_date, 1)
|
||||
yearly_opening_wdv = value_after_depreciation
|
||||
|
||||
if n > 0 and len(self.get("schedules")) > n - 1:
|
||||
prev_depreciation_amount = self.get("schedules")[n - 1].depreciation_amount
|
||||
else:
|
||||
@@ -397,6 +409,7 @@ class Asset(AccountsController):
|
||||
depreciation_amount = get_depreciation_amount(
|
||||
self,
|
||||
value_after_depreciation,
|
||||
yearly_opening_wdv,
|
||||
finance_book,
|
||||
n,
|
||||
prev_depreciation_amount,
|
||||
@@ -494,7 +507,10 @@ class Asset(AccountsController):
|
||||
|
||||
if not depreciation_amount:
|
||||
continue
|
||||
value_after_depreciation -= flt(depreciation_amount, self.precision("gross_purchase_amount"))
|
||||
value_after_depreciation = flt(
|
||||
value_after_depreciation - flt(depreciation_amount),
|
||||
self.precision("gross_purchase_amount"),
|
||||
)
|
||||
|
||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
||||
if finance_book.expected_value_after_useful_life and (
|
||||
@@ -1380,6 +1396,7 @@ def get_total_days(date, frequency):
|
||||
def get_depreciation_amount(
|
||||
asset,
|
||||
depreciable_value,
|
||||
yearly_opening_wdv,
|
||||
fb_row,
|
||||
schedule_idx=0,
|
||||
prev_depreciation_amount=0,
|
||||
@@ -1397,6 +1414,7 @@ def get_depreciation_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
yearly_opening_wdv,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
@@ -1542,6 +1560,7 @@ def get_wdv_or_dd_depr_amount(
|
||||
asset,
|
||||
fb_row,
|
||||
depreciable_value,
|
||||
yearly_opening_wdv,
|
||||
schedule_idx,
|
||||
prev_depreciation_amount,
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
|
||||
@@ -845,7 +845,7 @@ class TestDepreciationMethods(AssetSetup):
|
||||
["2030-12-31", 28630.14, 28630.14],
|
||||
["2031-12-31", 35684.93, 64315.07],
|
||||
["2032-12-31", 17842.47, 82157.54],
|
||||
["2033-06-06", 5342.46, 87500.0],
|
||||
["2033-06-06", 5342.47, 87500.01],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
@@ -957,7 +957,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
},
|
||||
)
|
||||
|
||||
depreciation_amount = get_depreciation_amount(asset, 100000, asset.finance_books[0])
|
||||
depreciation_amount = get_depreciation_amount(asset, 100000, 100000, asset.finance_books[0])
|
||||
self.assertEqual(depreciation_amount, 30000)
|
||||
|
||||
def test_make_depreciation_schedule(self):
|
||||
|
||||
@@ -7,6 +7,8 @@ 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 (
|
||||
add_days,
|
||||
@@ -26,6 +28,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,
|
||||
@@ -184,6 +187,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 := [
|
||||
@@ -317,6 +321,20 @@ class AccountsController(TransactionBase):
|
||||
(self.doctype, self.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",
|
||||
@@ -1118,7 +1136,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
|
||||
"""
|
||||
@@ -1173,6 +1193,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(
|
||||
@@ -1253,6 +1274,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(
|
||||
@@ -1344,7 +1366,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 (
|
||||
@@ -2530,6 +2558,9 @@ def get_advance_payment_entries(
|
||||
condition=None,
|
||||
payment_name=None,
|
||||
):
|
||||
pe = qb.DocType("Payment Entry")
|
||||
per = qb.DocType("Payment Entry Reference")
|
||||
|
||||
party_account_field = "paid_from" if party_type == "Customer" else "paid_to"
|
||||
currency_field = (
|
||||
"paid_from_account_currency" if party_type == "Customer" else "paid_to_account_currency"
|
||||
@@ -2540,76 +2571,79 @@ def get_advance_payment_entries(
|
||||
)
|
||||
|
||||
payment_entries_against_order, unallocated_payment_entries = [], []
|
||||
limit_cond = "limit %s" % limit if limit else ""
|
||||
|
||||
if not condition:
|
||||
condition = []
|
||||
|
||||
if payment_name:
|
||||
condition.append(pe.name.like(f"%%{payment_name}%%"))
|
||||
|
||||
if order_list or against_all_orders:
|
||||
orders_condition = []
|
||||
if order_list:
|
||||
reference_condition = " and t2.reference_name in ({0})".format(
|
||||
", ".join(["%s"] * len(order_list))
|
||||
orders_condition.append(per.reference_name.isin(order_list))
|
||||
payment_entries_query = (
|
||||
qb.from_(pe)
|
||||
.inner_join(per)
|
||||
.on(pe.name == per.parent)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("reference_type"),
|
||||
pe.name.as_("reference_name"),
|
||||
pe.remarks,
|
||||
per.allocated_amount.as_("amount"),
|
||||
per.name.as_("reference_row"),
|
||||
per.reference_name.as_("against_order"),
|
||||
pe.posting_date,
|
||||
pe[currency_field].as_("currency"),
|
||||
pe[exchange_rate_field].as_("exchange_rate"),
|
||||
)
|
||||
else:
|
||||
reference_condition = ""
|
||||
order_list = []
|
||||
|
||||
payment_name_filter = ""
|
||||
if payment_name:
|
||||
payment_name_filter = " and t1.name like '%%{0}%%'".format(payment_name)
|
||||
|
||||
if not condition:
|
||||
condition = ""
|
||||
|
||||
payment_entries_against_order = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
'Payment Entry' as reference_type, t1.name as reference_name,
|
||||
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
|
||||
t2.reference_name as against_order, t1.posting_date,
|
||||
t1.{0} as currency, t1.{5} as exchange_rate
|
||||
from `tabPayment Entry` t1, `tabPayment Entry Reference` t2
|
||||
where
|
||||
t1.name = t2.parent and t1.{1} = %s and t1.payment_type = %s
|
||||
and t1.party_type = %s and t1.party = %s and t1.docstatus = 1
|
||||
and t2.reference_doctype = %s {2} {3} {6}
|
||||
order by t1.posting_date {4}
|
||||
""".format(
|
||||
currency_field,
|
||||
party_account_field,
|
||||
reference_condition,
|
||||
condition,
|
||||
limit_cond,
|
||||
exchange_rate_field,
|
||||
payment_name_filter,
|
||||
),
|
||||
[party_account, payment_type, party_type, party, order_doctype] + order_list,
|
||||
as_dict=1,
|
||||
.where(
|
||||
(pe[party_account_field] == party_account)
|
||||
& (pe.payment_type == payment_type)
|
||||
& (pe.party_type == party_type)
|
||||
& (pe.party == party)
|
||||
& (pe.docstatus == 1)
|
||||
& (per.reference_doctype == order_doctype)
|
||||
)
|
||||
.where(Criterion.all(condition))
|
||||
.where(Criterion.all(orders_condition))
|
||||
.orderby(pe.posting_date)
|
||||
)
|
||||
|
||||
if limit:
|
||||
payment_entries_query = payment_entries_query.limit(limit)
|
||||
|
||||
payment_entries_against_order = payment_entries_query.run(as_dict=1)
|
||||
|
||||
if include_unallocated:
|
||||
payment_name_filter = ""
|
||||
if payment_name:
|
||||
payment_name_filter = " and name like '%%{0}%%'".format(payment_name)
|
||||
|
||||
unallocated_payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select 'Payment Entry' as reference_type, name as reference_name, posting_date,
|
||||
remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
{0} = %s and party_type = %s and party = %s and payment_type = %s
|
||||
and docstatus = 1 and unallocated_amount > 0 {condition} {4}
|
||||
order by posting_date {1}
|
||||
""".format(
|
||||
party_account_field,
|
||||
limit_cond,
|
||||
exchange_rate_field,
|
||||
currency_field,
|
||||
payment_name_filter,
|
||||
condition=condition or "",
|
||||
),
|
||||
(party_account, party_type, party, payment_type),
|
||||
as_dict=1,
|
||||
unallocated_payment_query = (
|
||||
qb.from_(pe)
|
||||
.select(
|
||||
ConstantColumn("Payment Entry").as_("reference_type"),
|
||||
pe.name.as_("reference_name"),
|
||||
pe.posting_date,
|
||||
pe.remarks,
|
||||
pe.unallocated_amount.as_("amount"),
|
||||
pe[exchange_rate_field].as_("exchange_rate"),
|
||||
pe[currency_field].as_("currency"),
|
||||
)
|
||||
.where(
|
||||
(pe[party_account_field] == party_account)
|
||||
& (pe.party_type == party_type)
|
||||
& (pe.party == party)
|
||||
& (pe.payment_type == payment_type)
|
||||
& (pe.docstatus == 1)
|
||||
& (pe.unallocated_amount.gt(0))
|
||||
)
|
||||
.where(Criterion.all(condition))
|
||||
.orderby(pe.posting_date)
|
||||
)
|
||||
|
||||
if limit:
|
||||
unallocated_payment_query = unallocated_payment_query.limit(limit)
|
||||
|
||||
unallocated_payment_entries = unallocated_payment_query.run(as_dict=1)
|
||||
|
||||
return list(payment_entries_against_order) + list(unallocated_payment_entries)
|
||||
|
||||
|
||||
|
||||
@@ -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, cstr, flt, get_link_to_form, getdate
|
||||
|
||||
import erpnext
|
||||
@@ -402,11 +402,6 @@ class StockController(AccountsController):
|
||||
d.batch_no = None
|
||||
d.db_set("batch_no", None)
|
||||
|
||||
for data in frappe.get_all(
|
||||
"Batch", {"reference_name": self.name, "reference_doctype": self.doctype}
|
||||
):
|
||||
frappe.delete_doc("Batch", data.name)
|
||||
|
||||
def get_sl_entries(self, d, args):
|
||||
sl_dict = frappe._dict(
|
||||
{
|
||||
@@ -673,6 +668,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()
|
||||
|
||||
@@ -711,6 +709,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.
|
||||
|
||||
@@ -343,7 +343,7 @@ class SubcontractingController(StockController):
|
||||
i += 1
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||
doctype = "BOM Explosion Item" if exploded_item else "BOM Item"
|
||||
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
|
||||
|
||||
alias_dict = {
|
||||
@@ -447,6 +447,16 @@ class SubcontractingController(StockController):
|
||||
rm_obj = self.append(self.raw_material_table, bom_item)
|
||||
rm_obj.reference_name = item_row.name
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
rm_obj.required_qty = qty
|
||||
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
|
||||
else:
|
||||
rm_obj.consumed_qty = 0
|
||||
setattr(
|
||||
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
|
||||
)
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
if self.doctype == "Subcontracting Receipt":
|
||||
args = frappe._dict(
|
||||
{
|
||||
@@ -465,16 +475,6 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
rm_obj.rate = get_incoming_rate(args)
|
||||
|
||||
if self.doctype == self.subcontract_data.order_doctype:
|
||||
rm_obj.required_qty = qty
|
||||
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
|
||||
else:
|
||||
rm_obj.consumed_qty = 0
|
||||
setattr(
|
||||
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
|
||||
)
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
||||
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
|
||||
|
||||
|
||||
@@ -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):
|
||||
@@ -1255,3 +1256,253 @@ class TestAccountsController(FrappeTestCase):
|
||||
)
|
||||
|
||||
frappe.db.set_value("Company", self.company, "cost_center", cc)
|
||||
|
||||
def setup_dimensions(self):
|
||||
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Department",
|
||||
}
|
||||
).insert()
|
||||
else:
|
||||
dimension = frappe.get_doc("Accounting Dimension", "Department")
|
||||
dimension.disabled = 0
|
||||
dimension.save()
|
||||
|
||||
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
|
||||
dimension1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Accounting Dimension",
|
||||
"document_type": "Location",
|
||||
}
|
||||
)
|
||||
dimension1.append(
|
||||
"dimension_defaults",
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"reference_document": "Location",
|
||||
"default_dimension": "Block 1",
|
||||
"mandatory_for_bs": 0,
|
||||
"mandatory_for_pl": 0,
|
||||
},
|
||||
)
|
||||
|
||||
dimension1.insert()
|
||||
dimension1.save()
|
||||
else:
|
||||
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
|
||||
dimension1.disabled = 0
|
||||
dimension1.save()
|
||||
|
||||
def disable_dimensions(self):
|
||||
if frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
|
||||
dimension = frappe.get_doc("Accounting Dimension", "Department")
|
||||
dimension.disabled = 1
|
||||
dimension.save()
|
||||
|
||||
if frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
|
||||
dimension1 = frappe.get_doc("Accounting Dimension", "Location")
|
||||
dimension1.disabled = 1
|
||||
dimension1.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)
|
||||
self.disable_dimensions()
|
||||
|
||||
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"),
|
||||
)
|
||||
self.disable_dimensions()
|
||||
|
||||
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",
|
||||
),
|
||||
)
|
||||
self.disable_dimensions()
|
||||
|
||||
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",
|
||||
),
|
||||
)
|
||||
self.disable_dimensions()
|
||||
|
||||
@@ -501,6 +501,7 @@ def get_party(user=None):
|
||||
contact_name = get_contact_name(user)
|
||||
party = None
|
||||
|
||||
contact = None
|
||||
if contact_name:
|
||||
contact = frappe.get_doc("Contact", contact_name)
|
||||
if contact.links:
|
||||
@@ -538,11 +539,15 @@ def get_party(user=None):
|
||||
customer.flags.ignore_mandatory = True
|
||||
customer.insert(ignore_permissions=True)
|
||||
|
||||
contact = frappe.new_doc("Contact")
|
||||
contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]})
|
||||
if not contact:
|
||||
contact = frappe.new_doc("Contact")
|
||||
contact.update({"first_name": fullname, "email_ids": [{"email_id": user, "is_primary": 1}]})
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact.reload()
|
||||
|
||||
contact.append("links", dict(link_doctype="Customer", link_name=customer.name))
|
||||
contact.flags.ignore_mandatory = True
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact.save(ignore_permissions=True)
|
||||
|
||||
return customer
|
||||
|
||||
|
||||
@@ -548,6 +548,8 @@ accounting_dimension_doctypes = [
|
||||
"Account Closing Balance",
|
||||
"Supplier Quotation",
|
||||
"Supplier Quotation Item",
|
||||
"Payment Reconciliation",
|
||||
"Payment Reconciliation Allocation",
|
||||
]
|
||||
|
||||
# get matching queries for Bank Reconciliation
|
||||
|
||||
@@ -14,7 +14,7 @@ def execute():
|
||||
"label": "For Income Tax",
|
||||
"fieldtype": "Check",
|
||||
"insert_after": "finance_book_name",
|
||||
"description": "If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.",
|
||||
"description": "If the asset is put to use for less than 180 days in the first year, the first year's depreciation rate will be reduced by 50%.",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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'], () => {
|
||||
|
||||
@@ -309,7 +309,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False):
|
||||
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()
|
||||
|
||||
@@ -20,6 +20,7 @@ from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
WarehouseRequired,
|
||||
create_pick_list,
|
||||
make_delivery_note,
|
||||
make_material_request,
|
||||
make_raw_material_request,
|
||||
@@ -2082,6 +2083,83 @@ class TestSalesOrder(FrappeTestCase):
|
||||
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
|
||||
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
|
||||
|
||||
def test_pick_list_without_rejected_materials(self):
|
||||
serial_and_batch_item = make_item(
|
||||
"_Test Serial and Batch Item for Rejected Materials",
|
||||
properties={
|
||||
"has_serial_no": 1,
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BAT-TSBIFRM-.#####",
|
||||
"serial_no_series": "SN-TSBIFRM-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
serial_item = make_item(
|
||||
"_Test Serial Item for Rejected Materials",
|
||||
properties={
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "SN-TSIFRM-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
batch_item = make_item(
|
||||
"_Test Batch Item for Rejected Materials",
|
||||
properties={
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "BAT-TBIFRM-.#####",
|
||||
},
|
||||
).name
|
||||
|
||||
normal_item = make_item("_Test Normal Item for Rejected Materials").name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
rejected_warehouse = "_Test Dummy Rejected Warehouse - _TC"
|
||||
|
||||
if not frappe.db.exists("Warehouse", rejected_warehouse):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Warehouse",
|
||||
"warehouse_name": rejected_warehouse,
|
||||
"company": "_Test Company",
|
||||
"warehouse_group": "_Test Warehouse Group",
|
||||
"is_rejected_warehouse": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
se = make_stock_entry(item_code=normal_item, qty=1, to_warehouse=warehouse, do_not_submit=True)
|
||||
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||
se.append("items", {"item_code": item, "qty": 1, "t_warehouse": warehouse})
|
||||
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
se = make_stock_entry(
|
||||
item_code=normal_item, qty=1, to_warehouse=rejected_warehouse, do_not_submit=True
|
||||
)
|
||||
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||
se.append("items", {"item_code": item, "qty": 1, "t_warehouse": rejected_warehouse})
|
||||
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
so = make_sales_order(item_code=normal_item, qty=2, do_not_submit=True)
|
||||
|
||||
for item in [serial_and_batch_item, serial_item, batch_item]:
|
||||
so.append("items", {"item_code": item, "qty": 2, "warehouse": warehouse})
|
||||
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
pick_list = create_pick_list(so.name)
|
||||
|
||||
pick_list.save()
|
||||
for row in pick_list.locations:
|
||||
self.assertEqual(row.qty, 1.0)
|
||||
self.assertFalse(row.warehouse == rejected_warehouse)
|
||||
self.assertTrue(row.warehouse == warehouse)
|
||||
|
||||
|
||||
def automatically_fetch_payment_terms(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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"],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"for_qty",
|
||||
"column_break_4",
|
||||
"parent_warehouse",
|
||||
"consider_rejected_warehouses",
|
||||
"get_item_locations",
|
||||
"section_break_6",
|
||||
"scan_barcode",
|
||||
@@ -184,11 +185,18 @@
|
||||
"report_hide": 1,
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable it if users want to consider rejected materials to dispatch.",
|
||||
"fieldname": "consider_rejected_warehouses",
|
||||
"fieldtype": "Check",
|
||||
"label": "Consider Rejected Warehouses"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-01-24 10:33:43.244476",
|
||||
"modified": "2024-01-24 17:05:20.317180",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
|
||||
@@ -205,6 +205,7 @@ class PickList(Document):
|
||||
self.item_count_map.get(item_code),
|
||||
self.company,
|
||||
picked_item_details=picked_items_details.get(item_code),
|
||||
consider_rejected_warehouses=self.consider_rejected_warehouses,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -524,6 +525,7 @@ def get_available_item_locations(
|
||||
company,
|
||||
ignore_validation=False,
|
||||
picked_item_details=None,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
locations = []
|
||||
total_picked_qty = (
|
||||
@@ -534,19 +536,39 @@ def get_available_item_locations(
|
||||
|
||||
if has_batch_no and has_serial_no:
|
||||
locations = get_available_item_locations_for_serial_and_batched_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
elif has_serial_no:
|
||||
locations = get_available_item_locations_for_serialized_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
elif has_batch_no:
|
||||
locations = get_available_item_locations_for_batched_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
else:
|
||||
locations = get_available_item_locations_for_other_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
|
||||
total_qty_available = sum(location.get("qty") for location in locations)
|
||||
@@ -597,7 +619,12 @@ def get_available_item_locations(
|
||||
|
||||
|
||||
def get_available_item_locations_for_serialized_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
sn = frappe.qb.DocType("Serial No")
|
||||
query = (
|
||||
@@ -613,6 +640,10 @@ def get_available_item_locations_for_serialized_item(
|
||||
else:
|
||||
query = query.where(Coalesce(sn.warehouse, "") != "")
|
||||
|
||||
if not consider_rejected_warehouses:
|
||||
if rejected_warehouses := get_rejected_warehouses():
|
||||
query = query.where(sn.warehouse.notin(rejected_warehouses))
|
||||
|
||||
serial_nos = query.run(as_list=True)
|
||||
|
||||
warehouse_serial_nos_map = frappe._dict()
|
||||
@@ -627,7 +658,12 @@ def get_available_item_locations_for_serialized_item(
|
||||
|
||||
|
||||
def get_available_item_locations_for_batched_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch = frappe.qb.DocType("Batch")
|
||||
@@ -653,15 +689,28 @@ def get_available_item_locations_for_batched_item(
|
||||
if from_warehouses:
|
||||
query = query.where(sle.warehouse.isin(from_warehouses))
|
||||
|
||||
if not consider_rejected_warehouses:
|
||||
if rejected_warehouses := get_rejected_warehouses():
|
||||
query = query.where(sle.warehouse.notin(rejected_warehouses))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_available_item_locations_for_serial_and_batched_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
# Get batch nos by FIFO
|
||||
locations = get_available_item_locations_for_batched_item(
|
||||
item_code, from_warehouses, required_qty, company
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
consider_rejected_warehouses=consider_rejected_warehouses,
|
||||
)
|
||||
|
||||
if locations:
|
||||
@@ -691,7 +740,12 @@ def get_available_item_locations_for_serial_and_batched_item(
|
||||
|
||||
|
||||
def get_available_item_locations_for_other_item(
|
||||
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
||||
item_code,
|
||||
from_warehouses,
|
||||
required_qty,
|
||||
company,
|
||||
total_picked_qty=0,
|
||||
consider_rejected_warehouses=False,
|
||||
):
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
query = (
|
||||
@@ -708,6 +762,10 @@ def get_available_item_locations_for_other_item(
|
||||
wh = frappe.qb.DocType("Warehouse")
|
||||
query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company))
|
||||
|
||||
if not consider_rejected_warehouses:
|
||||
if rejected_warehouses := get_rejected_warehouses():
|
||||
query = query.where(bin.warehouse.notin(rejected_warehouses))
|
||||
|
||||
item_locations = query.run(as_dict=True)
|
||||
|
||||
return item_locations
|
||||
@@ -1028,3 +1086,15 @@ def update_common_item_properties(item, location):
|
||||
item.serial_no = location.serial_no
|
||||
item.batch_no = location.batch_no
|
||||
item.material_request_item = location.material_request_item
|
||||
|
||||
|
||||
def get_rejected_warehouses():
|
||||
if not hasattr(frappe.local, "rejected_warehouses"):
|
||||
frappe.local.rejected_warehouses = []
|
||||
|
||||
if not frappe.local.rejected_warehouses:
|
||||
frappe.local.rejected_warehouses = frappe.get_all(
|
||||
"Warehouse", filters={"is_rejected_warehouse": 1}, pluck="name"
|
||||
)
|
||||
|
||||
return frappe.local.rejected_warehouses
|
||||
|
||||
@@ -193,7 +193,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
batch_no = pr.items[0].batch_no
|
||||
pr.cancel()
|
||||
|
||||
self.assertFalse(frappe.db.get_value("Batch", {"item": item.name, "reference_name": pr.name}))
|
||||
self.assertFalse(frappe.db.get_all("Serial No", {"batch_no": batch_no}))
|
||||
|
||||
def test_purchase_receipt_gl_entry(self):
|
||||
@@ -1595,9 +1594,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
|
||||
@@ -1620,13 +1620,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)
|
||||
@@ -1638,8 +1640,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)
|
||||
@@ -2172,6 +2176,19 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr_doc.reload()
|
||||
self.assertFalse(pr_doc.items[0].from_warehouse)
|
||||
|
||||
def test_do_not_delete_batch_implicitly(self):
|
||||
item = make_item(
|
||||
"_Test Item With Delete Batch",
|
||||
{"has_batch_no": 1, "create_new_batch": 1, "batch_number_series": "TBWDB.#####"},
|
||||
).name
|
||||
|
||||
pr = make_purchase_receipt(item_code=item, qty=10, rate=100)
|
||||
batch_no = pr.items[0].batch_no
|
||||
self.assertTrue(frappe.db.exists("Batch", batch_no))
|
||||
|
||||
pr.cancel()
|
||||
self.assertTrue(frappe.db.exists("Batch", batch_no))
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -159,7 +159,6 @@ class StockEntry(StockController):
|
||||
set_batch_nos(self, "s_warehouse")
|
||||
|
||||
self.validate_serialized_batch()
|
||||
self.set_actual_qty()
|
||||
self.calculate_rate_and_amount()
|
||||
self.validate_putaway_capacity()
|
||||
|
||||
|
||||
@@ -737,7 +737,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertEqual(batch_in_serial_no, None)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive")
|
||||
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||
self.assertTrue(frappe.db.exists("Batch", batch_no))
|
||||
|
||||
def test_serial_batch_item_qty_deduction(self):
|
||||
"""
|
||||
|
||||
@@ -312,7 +312,7 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
sr.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive")
|
||||
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||
self.assertTrue(frappe.db.exists("Batch", batch_no))
|
||||
|
||||
def test_stock_reco_balance_qty_for_serial_and_batch_item(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"column_break_3",
|
||||
"is_group",
|
||||
"parent_warehouse",
|
||||
"is_rejected_warehouse",
|
||||
"column_break_4",
|
||||
"account",
|
||||
"company",
|
||||
@@ -249,13 +250,20 @@
|
||||
{
|
||||
"fieldname": "column_break_qajx",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If yes, then this warehouse will be used to store rejected materials",
|
||||
"fieldname": "is_rejected_warehouse",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Rejected Warehouse"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-building",
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-29 13:10:43.333160",
|
||||
"modified": "2024-01-24 16:27:28.299520",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -99,7 +99,7 @@ frappe.query_reports["Stock Balance"] = {
|
||||
"fieldname": 'ignore_closing_balance',
|
||||
"label": __('Ignore Closing Balance'),
|
||||
"fieldtype": 'Check',
|
||||
"default": 1
|
||||
"default": 0
|
||||
},
|
||||
],
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<div class="card-text mb-2">
|
||||
{{ address.display }}
|
||||
</div>
|
||||
<a href="/addresses?name={{address.name}}" class="card-link">
|
||||
<a href="/address/{{address.name}}" class="card-link">
|
||||
<svg class="icon icon-sm">
|
||||
<use href="#icon-edit"></use>
|
||||
</svg>
|
||||
{{ _('Edit') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,6 +7,6 @@
|
||||
<p class="card-text text-muted">
|
||||
{{ address.display }}
|
||||
</p>
|
||||
<a href="/addresses?name={{address.name}}" class="card-link">{{ _('Edit') }}</a>
|
||||
<a href="/address/{{address.name}}" class="card-link">{{ _('Edit') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -186,3 +186,80 @@ frappe.ready(() => {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
function get_update_address_dialog() {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: "Select Address",
|
||||
fields: [{
|
||||
'fieldtype': 'HTML',
|
||||
'fieldname': 'address_picker',
|
||||
}],
|
||||
primary_action_label: __('Set Address'),
|
||||
primary_action: () => {
|
||||
const $card = d.$wrapper.find('.address-card.active');
|
||||
const address_type = $card.closest('[data-address-type]').attr('data-address-type');
|
||||
const address_name = $card.closest('[data-address-name]').attr('data-address-name');
|
||||
frappe.call({
|
||||
type: "POST",
|
||||
method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address",
|
||||
freeze: true,
|
||||
args: {
|
||||
address_type,
|
||||
address_name
|
||||
},
|
||||
callback: function(r) {
|
||||
d.hide();
|
||||
if (!r.exc) {
|
||||
$(".cart-tax-items").html(r.message.total);
|
||||
shopping_cart.parent.find(
|
||||
`.address-container[data-address-type="${address_type}"]`
|
||||
).html(r.message.address);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
function get_address_template(type) {
|
||||
return {
|
||||
shipping: `<div class="mb-3" data-section="shipping-address">
|
||||
<div class="row no-gutters" data-fieldname="shipping_address_name">
|
||||
{% for address in shipping_addresses %}
|
||||
<div class="mr-3 mb-3 w-100" data-address-name="{{address.name}}" data-address-type="shipping"
|
||||
{% if doc.shipping_address_name == address.name %} data-active {% endif %}>
|
||||
{% include "templates/includes/cart/address_picker_card.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>`,
|
||||
billing: `<div class="mb-3" data-section="billing-address">
|
||||
<div class="row no-gutters" data-fieldname="customer_address">
|
||||
{% for address in billing_addresses %}
|
||||
<div class="mr-3 mb-3 w-100" data-address-name="{{address.name}}" data-address-type="billing"
|
||||
{% if doc.shipping_address_name == address.name %} data-active {% endif %}>
|
||||
{% include "templates/includes/cart/address_picker_card.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>`,
|
||||
}[type];
|
||||
}
|
||||
|
||||
$(document).find('.btn-change-address').click(function(e) {
|
||||
debugger
|
||||
const d = get_update_address_dialog();
|
||||
const type = $(e.currentTarget).parents('.address-container').attr('data-address-type');
|
||||
|
||||
$(d.get_field('address_picker').wrapper).html(
|
||||
get_address_template(type)
|
||||
);
|
||||
|
||||
d.show();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -12,7 +12,6 @@ $.extend(shopping_cart, {
|
||||
},
|
||||
|
||||
bind_events: function() {
|
||||
shopping_cart.bind_address_picker_dialog();
|
||||
shopping_cart.bind_place_order();
|
||||
shopping_cart.bind_request_quotation();
|
||||
shopping_cart.bind_change_qty();
|
||||
@@ -21,78 +20,6 @@ $.extend(shopping_cart, {
|
||||
shopping_cart.bind_coupon_code();
|
||||
},
|
||||
|
||||
bind_address_picker_dialog: function() {
|
||||
const d = this.get_update_address_dialog();
|
||||
this.parent.find('.btn-change-address').on('click', (e) => {
|
||||
const type = $(e.currentTarget).parents('.address-container').attr('data-address-type');
|
||||
$(d.get_field('address_picker').wrapper).html(
|
||||
this.get_address_template(type)
|
||||
);
|
||||
d.show();
|
||||
});
|
||||
},
|
||||
|
||||
get_update_address_dialog() {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: "Select Address",
|
||||
fields: [{
|
||||
'fieldtype': 'HTML',
|
||||
'fieldname': 'address_picker',
|
||||
}],
|
||||
primary_action_label: __('Set Address'),
|
||||
primary_action: () => {
|
||||
const $card = d.$wrapper.find('.address-card.active');
|
||||
const address_type = $card.closest('[data-address-type]').attr('data-address-type');
|
||||
const address_name = $card.closest('[data-address-name]').attr('data-address-name');
|
||||
frappe.call({
|
||||
type: "POST",
|
||||
method: "erpnext.e_commerce.shopping_cart.cart.update_cart_address",
|
||||
freeze: true,
|
||||
args: {
|
||||
address_type,
|
||||
address_name
|
||||
},
|
||||
callback: function(r) {
|
||||
d.hide();
|
||||
if (!r.exc) {
|
||||
$(".cart-tax-items").html(r.message.total);
|
||||
shopping_cart.parent.find(
|
||||
`.address-container[data-address-type="${address_type}"]`
|
||||
).html(r.message.address);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return d;
|
||||
},
|
||||
|
||||
get_address_template(type) {
|
||||
return {
|
||||
shipping: `<div class="mb-3" data-section="shipping-address">
|
||||
<div class="row no-gutters" data-fieldname="shipping_address_name">
|
||||
{% for address in shipping_addresses %}
|
||||
<div class="mr-3 mb-3 w-100" data-address-name="{{address.name}}" data-address-type="shipping"
|
||||
{% if doc.shipping_address_name == address.name %} data-active {% endif %}>
|
||||
{% include "templates/includes/cart/address_picker_card.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>`,
|
||||
billing: `<div class="mb-3" data-section="billing-address">
|
||||
<div class="row no-gutters" data-fieldname="customer_address">
|
||||
{% for address in billing_addresses %}
|
||||
<div class="mr-3 mb-3 w-100" data-address-name="{{address.name}}" data-address-type="billing"
|
||||
{% if doc.shipping_address_name == address.name %} data-active {% endif %}>
|
||||
{% include "templates/includes/cart/address_picker_card.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>`,
|
||||
}[type];
|
||||
},
|
||||
|
||||
bind_place_order: function() {
|
||||
$(".btn-place-order").on("click", function() {
|
||||
shopping_cart.place_order(this);
|
||||
|
||||
@@ -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": [
|
||||
|
||||
Reference in New Issue
Block a user